From ae4074e789d5d53973cd716881aa1691d369c615 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 10 May 2025 22:31:33 +0200 Subject: [PATCH 01/44] nodejs alt --- .github/workflows/pipeline.yaml | 21 +- .gitignore | 2 + README_node.md | 141 + src/Dockerfile | 22 + src/__tests__/routes.test.js | 70 + src/controllers/adoptionController.js | 82 + src/controllers/categoriesController.js | 48 + src/controllers/cwvtechController.js | 88 + src/controllers/geosController.js | 239 + src/controllers/lighthouseController.js | 88 + src/controllers/pageWeightController.js | 82 + src/controllers/ranksController.js | 27 + src/controllers/technologiesController.js | 67 + src/index.js | 77 + src/package-lock.json | 5532 ++++++++++++++++++++ src/package.json | 42 + src/routes/adoption.js | 9 + src/routes/categories.js | 9 + src/routes/cwvtech.js | 9 + src/routes/geos.js | 9 + src/routes/lighthouse.js | 9 + src/routes/pageWeight.js | 9 + src/routes/ranks.js | 9 + src/routes/technologies.js | 9 + src/utils/db.js | 9 + src/utils/helpers.js | 53 + terraform/dev/main.tf | 163 +- terraform/modules/run-service/main.tf | 99 + terraform/modules/run-service/outputs.tf | 5 + terraform/modules/run-service/variables.tf | 75 + test-api.sh | 112 + 31 files changed, 7063 insertions(+), 153 deletions(-) create mode 100644 README_node.md create mode 100644 src/Dockerfile create mode 100644 src/__tests__/routes.test.js create mode 100644 src/controllers/adoptionController.js create mode 100644 src/controllers/categoriesController.js create mode 100644 src/controllers/cwvtechController.js create mode 100644 src/controllers/geosController.js create mode 100644 src/controllers/lighthouseController.js create mode 100644 src/controllers/pageWeightController.js create mode 100644 src/controllers/ranksController.js create mode 100644 src/controllers/technologiesController.js create mode 100644 src/index.js create mode 100644 src/package-lock.json create mode 100644 src/package.json create mode 100644 src/routes/adoption.js create mode 100644 src/routes/categories.js create mode 100644 src/routes/cwvtech.js create mode 100644 src/routes/geos.js create mode 100644 src/routes/lighthouse.js create mode 100644 src/routes/pageWeight.js create mode 100644 src/routes/ranks.js create mode 100644 src/routes/technologies.js create mode 100644 src/utils/db.js create mode 100644 src/utils/helpers.js create mode 100644 terraform/modules/run-service/main.tf create mode 100644 terraform/modules/run-service/outputs.tf create mode 100644 terraform/modules/run-service/variables.tf create mode 100755 test-api.sh diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 87cec4e..54cef52 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -44,20 +44,20 @@ jobs: credentials_json: ${{ env.PIPELINE_SA_KEY }} - uses: hashicorp/setup-terraform@v3 - + - name: Terraform fmt id: fmt run: terraform fmt -check continue-on-error: true - + - name: Terraform Init id: init run: terraform init - + - name: Terraform Validate id: validate run: terraform validate -no-color - + - name: Terraform Plan id: plan run: | @@ -77,7 +77,7 @@ jobs: -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \ -var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_DEV }}" \ -auto-approve - + deploy_production: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest @@ -94,20 +94,20 @@ jobs: credentials_json: ${{ env.PIPELINE_SA_KEY }} - uses: hashicorp/setup-terraform@v3 - + - name: Terraform fmt id: fmt run: terraform fmt -check continue-on-error: true - + - name: Terraform Init id: init run: terraform init - + - name: Terraform Validate id: validate run: terraform validate -no-color - + - name: Terraform Plan id: plan run: | @@ -127,5 +127,4 @@ jobs: -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \ -var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_PROD }}" \ -auto-approve - - \ No newline at end of file + diff --git a/.gitignore b/.gitignore index 35f4544..e9791a5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ __pycache__ utils.txt logs + +node_modules/ diff --git a/README_node.md b/README_node.md new file mode 100644 index 0000000..eebf3ab --- /dev/null +++ b/README_node.md @@ -0,0 +1,141 @@ +# Technology Reports API (Node.js) + +This is a unified Google Cloud Run function that provides technology metrics and information via various endpoints. + +## Setup + +### Prerequisites + +- Node.js 18+ +- npm +- Google Cloud account with necessary permissions + +### Local Development + +1. Install dependencies: +```bash +npm install +``` + +2. Set environment variables: +```bash +export PROJECT=your-gcp-project-id +export DATABASE=your-firestore-database +``` + +3. Run the application locally: +```bash +npm start +``` + +The API will be available at http://localhost:8080 + +## Deployment + +### Using Google Cloud Build + +```bash +gcloud builds submit --tag gcr.io/PROJECT_ID/tech-report-api +gcloud run deploy tech-report-api --image gcr.io/PROJECT_ID/tech-report-api --platform managed +``` + +## API Endpoints + +### `GET /technologies` + +Lists available technologies with optional filtering. + +#### Parameters + +- `technology` (optional): Filter by technology name(s) - comma-separated list +- `category` (optional): Filter by category - comma-separated list +- `onlyname` (optional): If present, returns only technology names + +### `GET /categories` + +Lists available categories. + +#### Parameters + +- `category` (optional): Filter by category name(s) - comma-separated list +- `onlyname` (optional): If present, returns only category names + +### `GET /adoption` + +Provides technology adoption data. + +#### Parameters + +- `technology` (required): Filter by technology name(s) - comma-separated list +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) +- `geo` (optional): Filter by geographic location +- `rank` (optional): Filter by rank + +### `GET /cwvtech` (Core Web Vitals) + +Provides Core Web Vitals metrics for technologies. + +#### Parameters + +- `technology` (required): Filter by technology name(s) - comma-separated list +- `geo` (required): Filter by geographic location +- `rank` (required): Filter by rank +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) + +### `GET /lighthouse` + +Provides Lighthouse scores for technologies. + +#### Parameters + +- `technology` (required): Filter by technology name(s) - comma-separated list +- `geo` (required): Filter by geographic location +- `rank` (required): Filter by rank +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) + +### `GET /page-weight` + +Provides Page Weight metrics for technologies. + +#### Parameters + +- `technology` (required): Filter by technology name(s) - comma-separated list +- `geo` (optional): Filter by geographic location +- `rank` (optional): Filter by rank +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) + +### `GET /ranks` + +Lists all available ranks. + +### `GET /geos` + +Lists all available geographic locations. + +## Response Format + +All API responses follow this format: + +```json +{ + "success": true, + "result": [ + // Array of data objects + ] +} +``` + +Or in case of an error: + +```json +{ + "success": false, + "errors": [ + {"key": "error message"} + ] +} +``` diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..a4ec023 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,22 @@ +FROM node:24-slim + +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy the rest of the application +COPY . . + +# Set environment variables +ENV PROJECT=httparchive +ENV DATABASE=tech-report-apis-prod + +# Expose the port the app runs on +EXPOSE 8080 + +# Start the application +CMD [ "npm", "start" ] diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js new file mode 100644 index 0000000..6d468db --- /dev/null +++ b/src/__tests__/routes.test.js @@ -0,0 +1,70 @@ +const request = require('supertest'); +const app = require('../src/src/index'); + +// Mock Firestore +jest.mock('../src/utils/db', () => { + return { + collection: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + get: jest.fn().mockResolvedValue({ + empty: false, + forEach: (callback) => { + callback({ + data: () => ({ + technology: 'Test Technology', + category: 'Test Category', + description: 'Test Description', + icon: 'test-icon.svg', + origins: 1000 + }), + get: (field) => 'Test Value' + }); + }, + docs: [{ + data: () => ({ + technology: 'Test Technology', + category: 'Test Category', + description: 'Test Description', + icon: 'test-icon.svg', + origins: 1000, + date: '2023-01-01' + }) + }] + }) + }; +}); + +describe('API Routes', () => { + describe('GET /', () => { + it('should return a health check response', async () => { + const res = await request(app).get('/'); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('status', 'ok'); + }); + }); + + describe('GET /technologies', () => { + it('should return technologies', async () => { + const res = await request(app).get('/technologies'); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('success', true); + expect(res.body).toHaveProperty('result'); + expect(Array.isArray(res.body.result)).toBe(true); + }); + + it('should filter technologies by name', async () => { + const res = await request(app).get('/technologies?technology=Test'); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('success', true); + }); + + it('should return only names when onlyname parameter is provided', async () => { + const res = await request(app).get('/technologies?onlyname=true'); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('success', true); + }); + }); + +}); diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js new file mode 100644 index 0000000..41ccf97 --- /dev/null +++ b/src/controllers/adoptionController.js @@ -0,0 +1,82 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +const TABLE = 'adoption'; + +/** + * Get the latest date in the collection + */ +const getLatestDate = async () => { + const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); + const snapshot = await query.get(); + if (!snapshot.empty) { + return snapshot.docs[0].data().date; + } + return null; +}; + +/** + * List adoption data with filtering + */ +const listAdoptionData = async (req, res) => { + try { + const params = req.query; + const data = []; + + // Technology is required + if (!params.technology) { + return res.status(400).send(createErrorResponse([ + ['technology', 'missing technology parameter'] + ])); + } + + // Convert technology parameter to array + const techArray = convertToArray(params.technology); + + // Handle 'latest' special value for start parameter + if (params.start && params.start === 'latest') { + params.start = await getLatestDate(); + } + + // Query for each technology + for (const technology of techArray) { + let query = firestore.collection(TABLE); + + // Apply filters + if (params.start) { + query = query.where('date', '>=', params.start); + } + + if (params.end) { + query = query.where('date', '<=', params.end); + } + + if (params.geo) { + query = query.where('geo', '==', params.geo); + } + + if (params.rank) { + query = query.where('rank', '==', params.rank); + } + + // Always filter by technology + query = query.where('technology', '==', technology); + + // Execute query + const snapshot = await query.get(); + snapshot.forEach(doc => { + data.push(doc.data()); + }); + } + + // Send response + res.status(200).send(createSuccessResponse(data)); + } catch (error) { + console.error('Error fetching adoption data:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listAdoptionData +}; diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js new file mode 100644 index 0000000..3cecf68 --- /dev/null +++ b/src/controllers/categoriesController.js @@ -0,0 +1,48 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +/** + * List categories with optional filtering + */ +const listCategories = async (req, res) => { + try { + const params = req.query; + let ref = firestore.collection('categories'); + let query = ref.orderBy('category', 'asc'); + + // Filter by category if provided + if (params.category) { + const categoryArray = convertToArray(params.category); + if (categoryArray.length > 0) { + // Using 'in' operator instead of multiple 'or' filters for simplicity + query = query.where('category', 'in', categoryArray); + } + } + + // Execute query + const snapshot = await query.get(); + const data = []; + + // Return only category names if onlyname parameter exists + if (params.onlyname || typeof params.onlyname === 'string') { + snapshot.forEach(doc => { + data.push(doc.get('category')); + }); + } else { + // Return full category objects + snapshot.forEach(doc => { + data.push(doc.data()); + }); + } + + // Send response + res.status(200).send(createSuccessResponse(data)); + } catch (error) { + console.error('Error fetching categories:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listCategories +}; diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js new file mode 100644 index 0000000..7e0d431 --- /dev/null +++ b/src/controllers/cwvtechController.js @@ -0,0 +1,88 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +const TABLE = 'core_web_vitals'; + +/** + * Get the latest date in the collection + */ +const getLatestDate = async () => { + const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); + const snapshot = await query.get(); + if (!snapshot.empty) { + return snapshot.docs[0].data().date; + } + return null; +}; + +/** + * List Core Web Vitals data with filtering + */ +const listCWVTechData = async (req, res) => { + try { + const params = req.query; + const data = []; + + // Required parameters check + if (!params.technology) { + return res.status(400).send(createErrorResponse([ + ['technology', 'missing technology parameter'] + ])); + } + + if (!params.geo) { + return res.status(400).send(createErrorResponse([ + ['geo', 'missing geo parameter'] + ])); + } + + if (!params.rank) { + return res.status(400).send(createErrorResponse([ + ['rank', 'missing rank parameter'] + ])); + } + + // Convert technology parameter to array + const techArray = convertToArray(params.technology); + + // Handle 'latest' special value for start parameter + if (params.start && params.start === 'latest') { + params.start = await getLatestDate(); + } + + // Query for each technology + for (const technology of techArray) { + let query = firestore.collection(TABLE); + + // Apply filters + if (params.start) { + query = query.where('date', '>=', params.start); + } + + if (params.end) { + query = query.where('date', '<=', params.end); + } + + // Always filter by required parameters + query = query.where('geo', '==', params.geo); + query = query.where('rank', '==', params.rank); + query = query.where('technology', '==', technology); + + // Execute query + const snapshot = await query.get(); + snapshot.forEach(doc => { + data.push(doc.data()); + }); + } + + // Send response + res.status(200).send(createSuccessResponse(data)); + } catch (error) { + console.error('Error fetching Core Web Vitals data:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listCWVTechData +}; diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js new file mode 100644 index 0000000..bf8d94e --- /dev/null +++ b/src/controllers/geosController.js @@ -0,0 +1,239 @@ +const { createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +// GEOS/COUNTRIES data from the existing Python implementation +// Note: Full list is shortened for brevity. In a real implementation, the complete list would be included. +const COUNTRIES = [ + {"geo": "ALL", "num_origins": "9731427"}, + {"geo": "United States of America", "num_origins": "1707677"}, + {"geo": "India", "num_origins": "826143"}, + {"geo": "Japan", "num_origins": "690984"}, + {"geo": "Germany", "num_origins": "678201"}, + {"geo": "Brazil", "num_origins": "644760"}, + { + "geo": "United Kingdom of Great Britain and Northern Ireland", + "num_origins": "560753", + }, + {"geo": "Russian Federation", "num_origins": "529803"}, + {"geo": "France", "num_origins": "515925"}, + {"geo": "Italy", "num_origins": "503015"}, + {"geo": "Spain", "num_origins": "459739"}, + {"geo": "Indonesia", "num_origins": "401253"}, + {"geo": "Poland", "num_origins": "350837"}, + {"geo": "Canada", "num_origins": "335548"}, + {"geo": "Mexico", "num_origins": "317337"}, + {"geo": "Turkey", "num_origins": "292310"}, + {"geo": "Netherlands", "num_origins": "291785"}, + {"geo": "Argentina", "num_origins": "252487"}, + {"geo": "Australia", "num_origins": "215909"}, + {"geo": "Korea, Republic of", "num_origins": "209013"}, + {"geo": "Philippines", "num_origins": "204637"}, + {"geo": "Colombia", "num_origins": "198020"}, + {"geo": "Malaysia", "num_origins": "193444"}, + {"geo": "Ukraine", "num_origins": "189866"}, + {"geo": "Viet Nam", "num_origins": "176473"}, + {"geo": "Thailand", "num_origins": "167337"}, + {"geo": "Pakistan", "num_origins": "157400"}, + {"geo": "Belgium", "num_origins": "157266"}, + {"geo": "South Africa", "num_origins": "150004"}, + {"geo": "Czechia", "num_origins": "148638"}, + {"geo": "Romania", "num_origins": "148176"}, + {"geo": "Taiwan, Province of China", "num_origins": "147383"}, + {"geo": "Chile", "num_origins": "144592"}, + {"geo": "Greece", "num_origins": "135996"}, + {"geo": "Austria", "num_origins": "135821"}, + {"geo": "Bangladesh", "num_origins": "134081"}, + {"geo": "Peru", "num_origins": "124954"}, + {"geo": "Iran (Islamic Republic of)", "num_origins": "122949"}, + {"geo": "Singapore", "num_origins": "121397"}, + {"geo": "Egypt", "num_origins": "119105"}, + {"geo": "Hungary", "num_origins": "117857"}, + {"geo": "Nigeria", "num_origins": "115407"}, + {"geo": "Portugal", "num_origins": "113035"}, + {"geo": "Kazakhstan", "num_origins": "111471"}, + {"geo": "Belarus", "num_origins": "109161"}, + {"geo": "Sweden", "num_origins": "108230"}, + {"geo": "Switzerland", "num_origins": "106121"}, + {"geo": "Saudi Arabia", "num_origins": "100966"}, + {"geo": "Israel", "num_origins": "99539"}, + {"geo": "Algeria", "num_origins": "98160"}, + {"geo": "Morocco", "num_origins": "96973"}, + {"geo": "Ireland", "num_origins": "96613"}, + {"geo": "Hong Kong", "num_origins": "95717"}, + {"geo": "United Arab Emirates", "num_origins": "91116"}, + {"geo": "Croatia", "num_origins": "85514"}, + {"geo": "Venezuela (Bolivarian Republic of)", "num_origins": "84283"}, + {"geo": "Slovakia", "num_origins": "84177"}, + {"geo": "Finland", "num_origins": "83107"}, + {"geo": "Serbia", "num_origins": "80789"}, + {"geo": "Ecuador", "num_origins": "80083"}, + {"geo": "Bulgaria", "num_origins": "75818"}, + {"geo": "Denmark", "num_origins": "69550"}, + {"geo": "New Zealand", "num_origins": "68444"}, + {"geo": "Uzbekistan", "num_origins": "65735"}, + {"geo": "Iraq", "num_origins": "65305"}, + {"geo": "Kenya", "num_origins": "62330"}, + {"geo": "Nepal", "num_origins": "60371"}, + {"geo": "Norway", "num_origins": "58300"}, + {"geo": "China", "num_origins": "57495"}, + {"geo": "Bolivia (Plurinational State of)", "num_origins": "55245"}, + {"geo": "Tunisia", "num_origins": "54813"}, + {"geo": "Sri Lanka", "num_origins": "53879"}, + {"geo": "Guatemala", "num_origins": "50897"}, + {"geo": "Azerbaijan", "num_origins": "46317"}, + {"geo": "Kyrgyzstan", "num_origins": "45478"}, + {"geo": "Lithuania", "num_origins": "45215"}, + {"geo": "Costa Rica", "num_origins": "44736"}, + {"geo": "Dominican Republic", "num_origins": "42618"}, + {"geo": "Moldova, Republic of", "num_origins": "41976"}, + {"geo": "Bosnia and Herzegovina", "num_origins": "41953"}, + {"geo": "Jordan", "num_origins": "41773"}, + {"geo": "Uruguay", "num_origins": "41139"}, + {"geo": "Panama", "num_origins": "38437"}, + {"geo": "Slovenia", "num_origins": "36027"}, + {"geo": "Ghana", "num_origins": "35980"}, + {"geo": "Paraguay", "num_origins": "35415"}, + {"geo": "Georgia", "num_origins": "34921"}, + {"geo": "Qatar", "num_origins": "34403"}, + {"geo": "Lebanon", "num_origins": "33694"}, + {"geo": "Puerto Rico", "num_origins": "33617"}, + {"geo": "El Salvador", "num_origins": "31654"}, + {"geo": "Syrian Arab Republic", "num_origins": "30714"}, + {"geo": "Latvia", "num_origins": "30530"}, + {"geo": "Honduras", "num_origins": "29712"}, + {"geo": "Myanmar", "num_origins": "29348"}, + {"geo": "Cyprus", "num_origins": "29012"}, + {"geo": "Oman", "num_origins": "27345"}, + {"geo": "Tanzania, United Republic of", "num_origins": "27335"}, + {"geo": "Cameroon", "num_origins": "26828"}, + {"geo": "Kuwait", "num_origins": "26458"}, + {"geo": "Armenia", "num_origins": "26355"}, + {"geo": "Nicaragua", "num_origins": "26015"}, + {"geo": "Estonia", "num_origins": "25576"}, + {"geo": "Côte d'Ivoire", "num_origins": "25208"}, + {"geo": "Cambodia", "num_origins": "24593"}, + {"geo": "Uganda", "num_origins": "24532"}, + {"geo": "Libya", "num_origins": "23730"}, + {"geo": "Cuba", "num_origins": "23056"}, + {"geo": "Ethiopia", "num_origins": "22650"}, + {"geo": "Albania", "num_origins": "22445"}, + {"geo": "Yemen", "num_origins": "22186"}, + {"geo": "North Macedonia", "num_origins": "21259"}, + {"geo": "Palestine, State of", "num_origins": "20468"}, + {"geo": "Senegal", "num_origins": "20323"}, + {"geo": "Montenegro", "num_origins": "20212"}, + {"geo": "Sudan", "num_origins": "20152"}, + {"geo": "Jamaica", "num_origins": "18847"}, + {"geo": "Iceland", "num_origins": "18261"}, + {"geo": "Zambia", "num_origins": "17567"}, + {"geo": "Bahrain", "num_origins": "17522"}, + {"geo": "Réunion", "num_origins": "17251"}, + {"geo": "Trinidad and Tobago", "num_origins": "16445"}, + {"geo": "Mauritius", "num_origins": "16238"}, + {"geo": "Zimbabwe", "num_origins": "15515"}, + {"geo": "Tajikistan", "num_origins": "14835"}, + {"geo": "Lao People's Democratic Republic", "num_origins": "14796"}, + {"geo": "Luxembourg", "num_origins": "14647"}, + {"geo": "Congo, Democratic Republic of the", "num_origins": "14545"}, + {"geo": "Angola", "num_origins": "13428"}, + {"geo": "Haiti", "num_origins": "13083"}, + {"geo": "Malta", "num_origins": "12984"}, + {"geo": "Mozambique", "num_origins": "12706"}, + {"geo": "Mongolia", "num_origins": "12574"}, + {"geo": "Burkina Faso", "num_origins": "12325"}, + {"geo": "Benin", "num_origins": "12292"}, + {"geo": "Somalia", "num_origins": "12176"}, + {"geo": "Mali", "num_origins": "10834"}, + {"geo": "Turkmenistan", "num_origins": "10192"}, + {"geo": "Afghanistan", "num_origins": "9613"}, + {"geo": "Martinique", "num_origins": "9314"}, + {"geo": "Guadeloupe", "num_origins": "8961"}, + {"geo": "Brunei Darussalam", "num_origins": "8854"}, + {"geo": "Botswana", "num_origins": "8657"}, + {"geo": "Namibia", "num_origins": "8535"}, + {"geo": "Papua New Guinea", "num_origins": "8447"}, + {"geo": "Togo", "num_origins": "8308"}, + {"geo": "Malawi", "num_origins": "8305"}, + {"geo": "Maldives", "num_origins": "8262"}, + {"geo": "Kosovo", "num_origins": "7807"}, + {"geo": "Gabon", "num_origins": "7754"}, + {"geo": "Bhutan", "num_origins": "6919"}, + {"geo": "Guinea", "num_origins": "6702"}, + {"geo": "Madagascar", "num_origins": "6620"}, + {"geo": "Guyana", "num_origins": "6303"}, + {"geo": "Rwanda", "num_origins": "6129"}, + {"geo": "Mauritania", "num_origins": "5995"}, + {"geo": "Macao", "num_origins": "5889"}, + {"geo": "Suriname", "num_origins": "5827"}, + {"geo": "Niger", "num_origins": "5484"}, + {"geo": "Fiji", "num_origins": "5388"}, + {"geo": "Congo", "num_origins": "4697"}, + {"geo": "Barbados", "num_origins": "4509"}, + {"geo": "Bahamas", "num_origins": "4467"}, + {"geo": "Chad", "num_origins": "4426"}, + {"geo": "Sierra Leone", "num_origins": "4345"}, + {"geo": "Cabo Verde", "num_origins": "4125"}, + {"geo": "Liberia", "num_origins": "3899"}, + {"geo": "Belize", "num_origins": "3871"}, + {"geo": "French Guiana", "num_origins": "3603"}, + {"geo": "Eswatini", "num_origins": "3554"}, + {"geo": "French Polynesia", "num_origins": "3489"}, + {"geo": "New Caledonia", "num_origins": "3379"}, + {"geo": "Lesotho", "num_origins": "3265"}, + {"geo": "Gambia", "num_origins": "3217"}, + {"geo": "Timor-Leste", "num_origins": "3074"}, + {"geo": "Andorra", "num_origins": "3073"}, + {"geo": "South Sudan", "num_origins": "3040"}, + {"geo": "Curaçao", "num_origins": "2987"}, + {"geo": "Western Sahara", "num_origins": "2739"}, + {"geo": "Saint Lucia", "num_origins": "2493"}, + {"geo": "Guam", "num_origins": "2466"}, + {"geo": "Antigua and Barbuda", "num_origins": "2449"}, + {"geo": "Aruba", "num_origins": "2420"}, + {"geo": "Djibouti", "num_origins": "2395"}, + {"geo": "Burundi", "num_origins": "2301"}, + {"geo": "Seychelles", "num_origins": "2007"}, + {"geo": "Mayotte", "num_origins": "1820"}, + {"geo": "Grenada", "num_origins": "1597"}, + {"geo": "Guinea-Bissau", "num_origins": "1592"}, + {"geo": "Comoros", "num_origins": "1563"}, + {"geo": "Cayman Islands", "num_origins": "1549"}, + {"geo": "Jersey", "num_origins": "1499"}, + {"geo": "Saint Vincent and the Grenadines", "num_origins": "1453"}, + {"geo": "Isle of Man", "num_origins": "1374"}, + {"geo": "Faroe Islands", "num_origins": "1233"}, + {"geo": "Equatorial Guinea", "num_origins": "1218"}, + {"geo": "Virgin Islands (U.S.)", "num_origins": "1074"}, + {"geo": "Dominica", "num_origins": "1049"}, + {"geo": "Sint Maarten (Dutch part)", "num_origins": "952"}, + {"geo": "Solomon Islands", "num_origins": "946"}, + {"geo": "Guernsey", "num_origins": "936"}, + {"geo": "Saint Kitts and Nevis", "num_origins": "917"}, + {"geo": "Central African Republic", "num_origins": "879"}, + {"geo": "Virgin Islands (British)", "num_origins": "864"}, + {"geo": "San Marino", "num_origins": "845"}, + {"geo": "Bermuda", "num_origins": "796"}, + {"geo": "Samoa", "num_origins": "771"}, + {"geo": "Gibraltar", "num_origins": "710"}, + {"geo": "Vanuatu", "num_origins": "697"}, + {"geo": "Saint Martin (French part)", "num_origins": "642"}, + {"geo": "Greenland", "num_origins": "631"}, + {"geo": "Bonaire, Sint Eustatius and Saba", "num_origins": "615"}, + {"geo": "Marshall Islands", "num_origins": "604"}, + {"geo": "Turks and Caicos Islands", "num_origins": "548"}, +]; + +/** + * List all geographic locations + */ +const listGeos = async (req, res) => { + try { + res.status(200).send(createSuccessResponse(COUNTRIES)); + } catch (error) { + console.error('Error fetching geographic locations:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listGeos +}; diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js new file mode 100644 index 0000000..3450b90 --- /dev/null +++ b/src/controllers/lighthouseController.js @@ -0,0 +1,88 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +const TABLE = 'lighthouse'; + +/** + * Get the latest date in the collection + */ +const getLatestDate = async () => { + const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); + const snapshot = await query.get(); + if (!snapshot.empty) { + return snapshot.docs[0].data().date; + } + return null; +}; + +/** + * List Lighthouse data with filtering + */ +const listLighthouseData = async (req, res) => { + try { + const params = req.query; + const data = []; + + // Required parameters check + if (!params.technology) { + return res.status(400).send(createErrorResponse([ + ['technology', 'missing technology parameter'] + ])); + } + + if (!params.geo) { + return res.status(400).send(createErrorResponse([ + ['geo', 'missing geo parameter'] + ])); + } + + if (!params.rank) { + return res.status(400).send(createErrorResponse([ + ['rank', 'missing rank parameter'] + ])); + } + + // Convert technology parameter to array + const techArray = convertToArray(params.technology); + + // Handle 'latest' special value for start parameter + if (params.start && params.start === 'latest') { + params.start = await getLatestDate(); + } + + // Query for each technology + for (const technology of techArray) { + let query = firestore.collection(TABLE); + + // Apply filters + if (params.start) { + query = query.where('date', '>=', params.start); + } + + if (params.end) { + query = query.where('date', '<=', params.end); + } + + // Always filter by required parameters + query = query.where('geo', '==', params.geo); + query = query.where('rank', '==', params.rank); + query = query.where('technology', '==', technology); + + // Execute query + const snapshot = await query.get(); + snapshot.forEach(doc => { + data.push(doc.data()); + }); + } + + // Send response + res.status(200).send(createSuccessResponse(data)); + } catch (error) { + console.error('Error fetching Lighthouse data:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listLighthouseData +}; diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js new file mode 100644 index 0000000..fa996de --- /dev/null +++ b/src/controllers/pageWeightController.js @@ -0,0 +1,82 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +const TABLE = 'page_weight'; + +/** + * Get the latest date in the collection + */ +const getLatestDate = async () => { + const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); + const snapshot = await query.get(); + if (!snapshot.empty) { + return snapshot.docs[0].data().date; + } + return null; +}; + +/** + * List Page Weight data with filtering + */ +const listPageWeightData = async (req, res) => { + try { + const params = req.query; + const data = []; + + // Required parameters check + if (!params.technology) { + return res.status(400).send(createErrorResponse([ + ['technology', 'missing technology parameter'] + ])); + } + + // Convert technology parameter to array + const techArray = convertToArray(params.technology); + + // Handle 'latest' special value for start parameter + if (params.start && params.start === 'latest') { + params.start = await getLatestDate(); + } + + // Query for each technology + for (const technology of techArray) { + let query = firestore.collection(TABLE); + + // Apply filters + if (params.start) { + query = query.where('date', '>=', params.start); + } + + if (params.end) { + query = query.where('date', '<=', params.end); + } + + if (params.geo) { + query = query.where('geo', '==', params.geo); + } + + if (params.rank) { + query = query.where('rank', '==', params.rank); + } + + // Always filter by technology + query = query.where('technology', '==', technology); + + // Execute query + const snapshot = await query.get(); + snapshot.forEach(doc => { + data.push(doc.data()); + }); + } + + // Send response + res.status(200).send(createSuccessResponse(data)); + } catch (error) { + console.error('Error fetching Page Weight data:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listPageWeightData +}; diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js new file mode 100644 index 0000000..564054f --- /dev/null +++ b/src/controllers/ranksController.js @@ -0,0 +1,27 @@ +const { createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +// Ranks data from the existing Python implementation +const RANKS = [ + {"num_origins": "9731427", "rank": "ALL"}, + {"num_origins": "7232806", "rank": "Top 10M"}, + {"num_origins": "881817", "rank": "Top 1M"}, + {"num_origins": "91410", "rank": "Top 100k"}, + {"num_origins": "9524", "rank": "Top 10k"}, + {"num_origins": "965", "rank": "Top 1k"}, +]; + +/** + * List all rank options + */ +const listRanks = async (req, res) => { + try { + res.status(200).send(createSuccessResponse(RANKS)); + } catch (error) { + console.error('Error fetching ranks:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listRanks +}; diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js new file mode 100644 index 0000000..da0185b --- /dev/null +++ b/src/controllers/technologiesController.js @@ -0,0 +1,67 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +// Technology Presenter +const presentTechnology = (item) => { + return { + technology: item.technology, + category: item.category, + description: item.description, + icon: item.icon, + origins: item.origins + }; +}; + +/** + * List technologies with optional filtering + */ +const listTechnologies = async (req, res) => { + try { + const params = req.query; + let ref = firestore.collection('technologies'); + let query = ref.orderBy('technology', 'asc'); + + // Filter by technology if provided + if (params.technology) { + const techArray = convertToArray(params.technology); + if (techArray.length > 0) { + // Using 'in' operator instead of multiple 'or' filters for simplicity + query = query.where('technology', 'in', techArray); + } + } + + // Filter by category if provided + if (params.category) { + const categoryArray = convertToArray(params.category); + if (categoryArray.length > 0) { + query = query.where('category_obj', 'array-contains-any', categoryArray); + } + } + + // Execute query + const snapshot = await query.get(); + const data = []; + + if (params.onlyname || typeof params.onlyname === 'string') { + // Return only technology names if onlyname parameter exists + snapshot.forEach(doc => { + data.push(doc.get('technology')); + }); + } else { + // Return full technology objects + snapshot.forEach(doc => { + data.push(presentTechnology(doc.data())); + }); + }; + + // Send response + res.status(200).send(createSuccessResponse(data)); + } catch (error) { + console.error('Error fetching technologies:', error); + res.status(400).send(createErrorResponse([['query', error.message]])); + } +}; + +module.exports = { + listTechnologies +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a4d3b40 --- /dev/null +++ b/src/index.js @@ -0,0 +1,77 @@ +const express = require('express'); +const cors = require('cors'); +const { Firestore } = require('@google-cloud/firestore'); + +// Import routes +const technologiesRoutes = require('./routes/technologies'); +const categoriesRoutes = require('./routes/categories'); +const adoptionRoutes = require('./routes/adoption'); +const cwvtechRoutes = require('./routes/cwvtech'); +const lighthouseRoutes = require('./routes/lighthouse'); +const pageWeightRoutes = require('./routes/pageWeight'); +const ranksRoutes = require('./routes/ranks'); +const geosRoutes = require('./routes/geos'); + +// Initialize Firebase +const firestore = new Firestore({ + projectId: process.env.PROJECT, + databaseId: process.env.DATABASE +}); + +// Create Express app +const app = express(); + +// Middleware +app.use(cors({ + origin: '*', + methods: 'GET, OPTIONS', + allowedHeaders: 'Content-Type, Timing-Allow-Origin', + maxAge: 86400 +})); + +// Explicitly handle OPTIONS requests for CORS preflight +app.options('*', cors()); + +app.use(express.json()); + +// Set common response headers +app.use((req, res, next) => { + res.set({ + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=21600', + 'Timing-Allow-Origin': '*' + }); + next(); +}); + +// Define routes +app.use('/technologies', technologiesRoutes); +app.use('/categories', categoriesRoutes); +app.use('/adoption', adoptionRoutes); +app.use('/cwv', cwvtechRoutes); +app.use('/lighthouse', lighthouseRoutes); +app.use('/page-weight', pageWeightRoutes); +app.use('/ranks', ranksRoutes); +app.use('/geos', geosRoutes); + +// Health check endpoint +app.get('/', (req, res) => { + res.status(200).send(JSON.stringify({ status: 'ok' })); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Error:', err); + res.status(400).send(JSON.stringify({ + errors: [{ error: err.message || 'Unknown error occurred' }] + })); +}); + +// Start server +const port = process.env.PORT || 8080; +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); + +// Export for Cloud Run +module.exports = app; diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 0000000..c704213 --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,5532 @@ +{ + "name": "tech-report-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tech-report-api", + "version": "1.0.0", + "dependencies": { + "@google-cloud/firestore": "7.3.0", + "cors": "2.8.5", + "express": "4.21.2" + }, + "devDependencies": { + "jest": "29.7.0", + "nodemon": "3.0.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@google-cloud/firestore": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.0.tgz", + "integrity": "sha512-2IftQLAbCuVp0nTd3neeu+d3OYIegJpV/V9R4USQj51LzJcXPe8h8jZ7j3+svSNhJVGy6JsN0T1QqlJdMDhTwg==", + "license": "Apache-2.0", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.0.4", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz", + "integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "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", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.151", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", + "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "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==", + "license": "MIT" + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.0.tgz", + "integrity": "sha512-zKKLeLfcYBVOzzM48Brtn4EQkKcTli9w6c1ilzFK2NbJvcd4ATD8/XqFExImvE/W5IwMlKKwa5qqVufji3ioNQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "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", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz", + "integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..776bd86 --- /dev/null +++ b/src/package.json @@ -0,0 +1,42 @@ +{ + "name": "tech-report-api", + "version": "1.0.0", + "description": "API for HTTP Archive technology reports", + "main": "index.js", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js", + "test": "jest" + }, + "dependencies": { + "@google-cloud/firestore": "7.3.0", + "cors": "2.8.5", + "express": "4.21.2" + }, + "devDependencies": { + "jest": "29.7.0", + "nodemon": "3.0.1" + }, + "jest": { + "preset": "jest", + "testEnvironment": "node", + "verbose": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src /**/*.js", + "!index.js" + ], + "coverageDirectory": "coverage", + "coverageReporters": [ + "text", + "lcov" + ], + "testMatch": [ + "**/__tests__ /**/*.js", + "**/?(*.)+(spec|test).js" + ] + } +} diff --git a/src/routes/adoption.js b/src/routes/adoption.js new file mode 100644 index 0000000..b3fad1e --- /dev/null +++ b/src/routes/adoption.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listAdoptionData } = require('../controllers/adoptionController'); + +const router = express.Router(); + +// GET /adoption endpoint +router.get('/', listAdoptionData); + +module.exports = router; diff --git a/src/routes/categories.js b/src/routes/categories.js new file mode 100644 index 0000000..fbc0b2e --- /dev/null +++ b/src/routes/categories.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listCategories } = require('../controllers/categoriesController'); + +const router = express.Router(); + +// GET /categories endpoint +router.get('/', listCategories); + +module.exports = router; diff --git a/src/routes/cwvtech.js b/src/routes/cwvtech.js new file mode 100644 index 0000000..03c806f --- /dev/null +++ b/src/routes/cwvtech.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listCWVTechData } = require('../controllers/cwvtechController'); + +const router = express.Router(); + +// GET /cwvtech endpoint +router.get('/', listCWVTechData); + +module.exports = router; diff --git a/src/routes/geos.js b/src/routes/geos.js new file mode 100644 index 0000000..a8cc7e5 --- /dev/null +++ b/src/routes/geos.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listGeos } = require('../controllers/geosController'); + +const router = express.Router(); + +// GET /geos endpoint +router.get('/', listGeos); + +module.exports = router; diff --git a/src/routes/lighthouse.js b/src/routes/lighthouse.js new file mode 100644 index 0000000..bd2293e --- /dev/null +++ b/src/routes/lighthouse.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listLighthouseData } = require('../controllers/lighthouseController'); + +const router = express.Router(); + +// GET /lighthouse endpoint +router.get('/', listLighthouseData); + +module.exports = router; diff --git a/src/routes/pageWeight.js b/src/routes/pageWeight.js new file mode 100644 index 0000000..486411a --- /dev/null +++ b/src/routes/pageWeight.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listPageWeightData } = require('../controllers/pageWeightController'); + +const router = express.Router(); + +// GET /page-weight endpoint +router.get('/', listPageWeightData); + +module.exports = router; diff --git a/src/routes/ranks.js b/src/routes/ranks.js new file mode 100644 index 0000000..0a8ffe5 --- /dev/null +++ b/src/routes/ranks.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listRanks } = require('../controllers/ranksController'); + +const router = express.Router(); + +// GET /ranks endpoint +router.get('/', listRanks); + +module.exports = router; diff --git a/src/routes/technologies.js b/src/routes/technologies.js new file mode 100644 index 0000000..8527cd6 --- /dev/null +++ b/src/routes/technologies.js @@ -0,0 +1,9 @@ +const express = require('express'); +const { listTechnologies } = require('../controllers/technologiesController'); + +const router = express.Router(); + +// GET /technologies endpoint +router.get('/', listTechnologies); + +module.exports = router; diff --git a/src/utils/db.js b/src/utils/db.js new file mode 100644 index 0000000..844516f --- /dev/null +++ b/src/utils/db.js @@ -0,0 +1,9 @@ +const { Firestore } = require('@google-cloud/firestore'); + +// Initialize Firestore +const firestore = new Firestore({ + projectId: process.env.PROJECT, + databaseId: process.env.DATABASE +}); + +module.exports = firestore; diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..a6a426b --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,53 @@ +/** + * Utility functions for API requests and responses + */ + +/** + * Converts a comma-separated string to an array + * @param {string} dataString - The string to convert + * @returns {string[]} The resulting array + */ +const convertToArray = (dataString) => { + if (!dataString) return []; + + // URL decode and split by comma + const decoded = decodeURIComponent(dataString); + return decoded.split(','); +}; + +/** + * Converts error arrays to hash format + * @param {Array>} arr - Array of [key, message] arrays + * @returns {Array} Array of {key: message} objects + */ +const convertToHashes = (arr) => { + return arr.map(([key, message]) => ({ [key]: message })); +}; + +/** + * Creates a successful response object + * @param {*} data - The data to return + * @returns {Object} Success response object + */ +const createSuccessResponse = (data) => { + return data; +}; + +/** + * Creates an error response object + * @param {Array>} errors - Array of [key, message] arrays + * @returns {Object} Error response object + */ +const createErrorResponse = (errors) => { + return { + success: false, + errors: convertToHashes(errors) + }; +}; + +module.exports = { + convertToArray, + convertToHashes, + createSuccessResponse, + createErrorResponse +}; diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 8a8cb78..33720cb 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -1,7 +1,7 @@ provider "google" { project = "httparchive" - region = "us-east1" + region = "us-central1" request_timeout = "60m" } @@ -28,7 +28,7 @@ resource "google_api_gateway_api_config" "api_config" { display_name = "The dev Config" openapi_documents { document { - path = "spec.yaml" + path = "spec.yaml" contents = base64encode(<<-EOF swagger: "2.0" info: @@ -45,10 +45,8 @@ paths: summary: categories operationId: getCategories x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/categories-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -57,10 +55,8 @@ paths: summary: adoption operationId: getadoptionReports x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/adoption-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -69,10 +65,8 @@ paths: summary: pageWeight operationId: getpageWeight x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/page-weight-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -81,10 +75,8 @@ paths: summary: lighthouse operationId: getLighthouseReports x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/lighthouse-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -93,10 +85,8 @@ paths: summary: cwv operationId: getCwv x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/cwvtech-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -105,10 +95,8 @@ paths: summary: ranks operationId: getRanks x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/ranks-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -117,22 +105,18 @@ paths: summary: geos operationId: getGeos x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/geos-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String /v1/technologies: get: - summary: geos + summary: technologies operationId: getTechnologies x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/technologies-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -150,7 +134,7 @@ EOF resource "google_api_gateway_gateway" "gateway" { provider = google-beta project = "httparchive" - region = "us-east1" + region = "us-central1" api_config = google_api_gateway_api_config.api_config.id gateway_id = "dev-gw" display_name = "devApi Gateway" @@ -166,122 +150,17 @@ resource "google_api_gateway_gateway" "gateway" { } } -module "cwvtech" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/cwvtech" - function_name = "cwvtech" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "lighthouse" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/lighthouse" - function_name = "lighthouse" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "adoption" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/adoption" - function_name = "adoption" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "page-weight" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/page-weight" - function_name = "page-weight" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "categories" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/categories" - function_name = "categories" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "technologies" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/technologies" - function_name = "technologies" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "ranks" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/ranks" - function_name = "ranks" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = "httparchive", - "DATABASE" = var.project_database - } -} - -module "geos" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "dev" - source_directory = "../../functions/geos" - function_name = "geos" - service_account_email = var.google_service_account_cloud_functions +module "endpoints" { + source = "./../modules/run-service" + entry_point = "app" + project = "httparchive" + environment = "dev" + source_directory = "../../src" + function_name = "tech-report-api" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } diff --git a/terraform/modules/run-service/main.tf b/terraform/modules/run-service/main.tf new file mode 100644 index 0000000..7585b1b --- /dev/null +++ b/terraform/modules/run-service/main.tf @@ -0,0 +1,99 @@ +locals { + bucketName = "tf-cloudfunctions-backingapi-20230314" +} +data "archive_file" "source" { + type = "zip" + source_dir = var.source_directory + output_path = "/tmp/${var.function_name}.zip" +} +resource "google_storage_bucket_object" "zip" { + name = "${var.environment}-${var.function_name}-${data.archive_file.source.output_sha}" + bucket = local.bucketName + source = data.archive_file.source.output_path +} + +resource "google_cloudfunctions2_function" "function" { + name = "${var.function_name}-${var.environment}" + location = var.region + + build_config { + runtime = "nodejs24" + entry_point = var.entry_point + + source { + storage_source { + bucket = local.bucketName + object = google_storage_bucket_object.zip.name + } + } + } + + service_config { + all_traffic_on_latest_revision = true + available_memory = var.available_memory_mb + ingress_settings = var.ingress_settings + + environment_variables = var.environment_variables + + min_instance_count = var.min_instances + max_instance_count = var.max_instances + timeout_seconds = var.timeout + max_instance_request_concurrency = var.max_instance_request_concurrency + service_account_email = var.service_account_email + } + + labels = { + owner = "tech_report_api" + environment = var.environment + } + + depends_on = [ + google_storage_bucket_object.zip + ] +} + +resource "google_cloudfunctions2_function_iam_member" "variable_service_account_function_invoker" { + project = google_cloudfunctions2_function.function.project + location = google_cloudfunctions2_function.function.location + cloud_function = google_cloudfunctions2_function.function.name + role = "roles/cloudfunctions.invoker" + member = "serviceAccount:${var.service_account_email}" + depends_on = [google_cloudfunctions2_function.function] +} +data "google_cloud_run_service" "run-service" { + name = google_cloudfunctions2_function.function.name + location = var.region + depends_on = [google_cloudfunctions2_function.function] +} +resource "google_cloud_run_v2_service_iam_member" "variable_service_account_run_invoker" { + project = var.project + location = var.region + name = data.google_cloud_run_service.run-service.name + role = "roles/run.invoker" + member = "serviceAccount:${var.service_account_email}" +} +// TODO: Conditionally apply if the function needs to be invoked by API Gateway +resource "google_cloudfunctions2_function_iam_member" "api_gw_variable_service_account_function_invoker" { + project = google_cloudfunctions2_function.function.project + location = google_cloudfunctions2_function.function.location + cloud_function = google_cloudfunctions2_function.function.name + role = "roles/cloudfunctions.invoker" + #member = "serviceAccount:api-gateway@httparchive.iam.gserviceaccount.com" + member = "serviceAccount:${var.service_account_api_gateway}" + depends_on = [google_cloudfunctions2_function.function] +} +// TODO: Conditionally apply if the function needs to be invoked by API Gateway +resource "google_cloud_run_v2_service_iam_member" "api_gw_variable_service_account_run_invoker" { + project = var.project + location = var.region + name = data.google_cloud_run_service.run-service.name + role = "roles/run.invoker" + member = "serviceAccount:${var.service_account_api_gateway}" +} + + + + + + + diff --git a/terraform/modules/run-service/outputs.tf b/terraform/modules/run-service/outputs.tf new file mode 100644 index 0000000..c7bb95b --- /dev/null +++ b/terraform/modules/run-service/outputs.tf @@ -0,0 +1,5 @@ + +output "name" { + description = "Name of the Cloud Function" + value = google_cloudfunctions2_function.function.name +} diff --git a/terraform/modules/run-service/variables.tf b/terraform/modules/run-service/variables.tf new file mode 100644 index 0000000..cad4a5c --- /dev/null +++ b/terraform/modules/run-service/variables.tf @@ -0,0 +1,75 @@ +variable "secrets" { + default = [] +} +variable "region" { + default = "us-east1" + type = string +} +variable "environment" { + description = "The 'Environment' that is being created/deployed. Applied as a suffix to many resources." + type = string +} +variable "source_directory" { + description = "The folder of the package containing function that will be executed when the Google Cloud Function is triggered!" + type = string +} +variable "function_name" { + description = "Optional: Can be used to create more than function from the same package" + type = string +} +variable "entry_point" { + description = "The entry point; This is either what is registered with 'http' or exported from the code as a handler!" + type = string +} +variable "available_memory_mb" { + default = "1Gi" + type = string + description = "The amount of memory for the Cloud Function" +} +variable "ingress_settings" { + type = string + default = "ALLOW_ALL" + description = "String value that controls what traffic can reach the function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY. Check ingress documentation to see the impact of each settings value. Changes to this field will recreate the cloud function." +} +variable "vpc_connector_egress_settings" { + type = string + default = null + description = "The egress settings for the connector, controlling what traffic is diverted through it. Allowed values are ALL_TRAFFIC and PRIVATE_RANGES_ONLY. Defaults to PRIVATE_RANGES_ONLY. If unset, this field preserves the previously set value." +} +variable "project" { + description = "The ID of the project in which the resource belongs. If it is not provided, the provider project is used." + type = string +} +variable "timeout" { + default = 60 + type = number + description = "Timeout (in seconds) for the function. Default value is 60 seconds. Cannot be more than 540 seconds." +} +variable "service_account_email" { + type = string + description = "Service account who can invoke this function. This is required!" +} +variable "service_account_api_gateway" { + type = string + description = "API Gateway service account who can invoke this function. This is required!" +} +variable "max_instances" { + default = 5 + type = number + description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." +} +variable "min_instances" { + description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." + type = number + default = 0 +} +variable "max_instance_request_concurrency" { + description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." + type = number + default = 5 +} +variable "environment_variables" { + description = "environment_variables" + default = {} + type = map(string) +} diff --git a/test-api.sh b/test-api.sh new file mode 100755 index 0000000..c42ae16 --- /dev/null +++ b/test-api.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Function to test an endpoint +test_endpoint() { + local endpoint=$1 + local params=$2 + local url="http://localhost:8080${endpoint}${params}" + + echo "Testing endpoint: ${url}" + response=$(curl -s -w "\n%{http_code}" "${url}") + http_code=$(echo "$response" | tail -n1) + body=$(echo "$response" | sed '$d') + + echo "$body" | jq . + echo "Status code: $http_code" + + if [[ $http_code -ne 200 ]]; then + echo "Error: Endpoint returned non-200 status code" + exit 1 + fi + + echo "" + echo "----------------------" + echo "" +} + +# Function to test CORS preflight with OPTIONS request +test_cors_preflight() { + local endpoint=$1 + local url="http://localhost:8080${endpoint}" + + echo "Testing CORS preflight for: ${url}" + + # Send OPTIONS request with CORS headers + response=$(curl -s -X OPTIONS -w "\n%{http_code}" \ + -H "Origin: http://example.com" \ + -H "Access-Control-Request-Method: GET" \ + -H "Access-Control-Request-Headers: Content-Type" \ + "${url}") + + http_code=$(echo "$response" | tail -n1) + headers=$(curl -s -X OPTIONS -I \ + -H "Origin: http://example.com" \ + -H "Access-Control-Request-Method: GET" \ + -H "Access-Control-Request-Headers: Content-Type" \ + "${url}") + + # Check for CORS headers + echo "CORS Headers:" + echo "$headers" | grep -i "access-control" + + echo "Status code: $http_code" + + # OPTIONS preflight should return 204 (No Content) or 200 + if [[ $http_code -ne 204 && $http_code -ne 200 ]]; then + echo "Error: CORS preflight failed with non-200/204 status code" + exit 1 + fi + + # Check for required CORS headers + if ! echo "$headers" | grep -q "Access-Control-Allow-Origin"; then + echo "Error: Missing Access-Control-Allow-Origin header" + exit 1 + fi + + if ! echo "$headers" | grep -q "Access-Control-Allow-Methods"; then + echo "Error: Missing Access-Control-Allow-Methods header" + exit 1 + fi + + echo "CORS preflight check passed!" + echo "" + echo "----------------------" + echo "" +} + +# Start tests +echo "Testing API endpoints..." +echo "----------------------" +echo "" + +# Test health check +test_cors_preflight "/" +test_endpoint "/" "" + +# Test technologies endpoint +test_cors_preflight "/technologies" +test_endpoint "/technologies" "?technology=WordPress&onlyname=true" + +# Test categories endpoint +test_cors_preflight "/categories" +test_endpoint "/categories" "?category=CMS&onlyname=true" + +# Test ranks endpoint +test_endpoint "/ranks" "" + +# Test geos endpoint +test_endpoint "/geos" "" + +# Test adoption endpoint +test_endpoint "/adoption" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" + +# Test cwv endpoint +test_endpoint "/cwv" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" + +# Test lighthouse endpoint +test_endpoint "/lighthouse" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" + +# Test page-weight endpoint +test_endpoint "/page-weight" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" + +echo "API tests complete! All endpoints returned 200 status code and CORS is properly configured." From 619c26700342426de17ec8dc8c1c955af0322ce9 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 10 May 2025 22:38:43 +0200 Subject: [PATCH 02/44] node22 --- terraform/modules/run-service/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/modules/run-service/main.tf b/terraform/modules/run-service/main.tf index 7585b1b..9cdb28a 100644 --- a/terraform/modules/run-service/main.tf +++ b/terraform/modules/run-service/main.tf @@ -17,7 +17,7 @@ resource "google_cloudfunctions2_function" "function" { location = var.region build_config { - runtime = "nodejs24" + runtime = "nodejs22" entry_point = var.entry_point source { From 8ede3a549e2fb4149a66eb16b6ed9f96145399ec Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 10 May 2025 23:00:45 +0200 Subject: [PATCH 03/44] max_instance dev override --- terraform/dev/variables.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 7e8a919..0c2032c 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -19,3 +19,9 @@ variable "min_instances" { type = number default = 0 } + +variable "max_instance_request_concurrency" { + description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." + type = number + default = 1 +} From 57d17a4c2a77e0805f0bc79dd3bd472120eeb2e1 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 10 May 2025 23:08:14 +0200 Subject: [PATCH 04/44] missing vars --- terraform/dev/main.tf | 18 ++++++++++-------- terraform/modules/run-service/main.tf | 16 ++++++++-------- terraform/modules/run-service/variables.tf | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 33720cb..6bdd3d2 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -151,14 +151,16 @@ resource "google_api_gateway_gateway" "gateway" { } module "endpoints" { - source = "./../modules/run-service" - entry_point = "app" - project = "httparchive" - environment = "dev" - source_directory = "../../src" - function_name = "tech-report-api" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway + source = "./../modules/run-service" + entry_point = "app" + project = "httparchive" + environment = "dev" + source_directory = "../../src" + function_name = "tech-report-api" + service_account_email = var.google_service_account_cloud_functions + service_account_api_gateway = var.google_service_account_api_gateway + max_instance_request_concurrency = var.max_instance_request_concurrency + min_instances = var.min_instances environment_variables = { "PROJECT" = "httparchive", "DATABASE" = var.project_database diff --git a/terraform/modules/run-service/main.tf b/terraform/modules/run-service/main.tf index 9cdb28a..7c21146 100644 --- a/terraform/modules/run-service/main.tf +++ b/terraform/modules/run-service/main.tf @@ -1,5 +1,5 @@ locals { - bucketName = "tf-cloudfunctions-backingapi-20230314" + bucketName = "tf-cloudfunctions-backingapi-20230314" } data "archive_file" "source" { type = "zip" @@ -13,7 +13,7 @@ resource "google_storage_bucket_object" "zip" { } resource "google_cloudfunctions2_function" "function" { - name = "${var.function_name}-${var.environment}" + name = "${var.function_name}-${var.environment}" location = var.region build_config { @@ -35,11 +35,11 @@ resource "google_cloudfunctions2_function" "function" { environment_variables = var.environment_variables - min_instance_count = var.min_instances - max_instance_count = var.max_instances - timeout_seconds = var.timeout + min_instance_count = var.min_instances + max_instance_count = var.max_instances + timeout_seconds = var.timeout max_instance_request_concurrency = var.max_instance_request_concurrency - service_account_email = var.service_account_email + service_account_email = var.service_account_email } labels = { @@ -79,8 +79,8 @@ resource "google_cloudfunctions2_function_iam_member" "api_gw_variable_service_a cloud_function = google_cloudfunctions2_function.function.name role = "roles/cloudfunctions.invoker" #member = "serviceAccount:api-gateway@httparchive.iam.gserviceaccount.com" - member = "serviceAccount:${var.service_account_api_gateway}" - depends_on = [google_cloudfunctions2_function.function] + member = "serviceAccount:${var.service_account_api_gateway}" + depends_on = [google_cloudfunctions2_function.function] } // TODO: Conditionally apply if the function needs to be invoked by API Gateway resource "google_cloud_run_v2_service_iam_member" "api_gw_variable_service_account_run_invoker" { diff --git a/terraform/modules/run-service/variables.tf b/terraform/modules/run-service/variables.tf index cad4a5c..710b4d0 100644 --- a/terraform/modules/run-service/variables.tf +++ b/terraform/modules/run-service/variables.tf @@ -3,7 +3,7 @@ variable "secrets" { } variable "region" { default = "us-east1" - type = string + type = string } variable "environment" { description = "The 'Environment' that is being created/deployed. Applied as a suffix to many resources." From 7a8675bb3550e54f6db198b41f1494162eab8eb0 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 10 May 2025 23:21:48 +0200 Subject: [PATCH 05/44] region dev override --- terraform/dev/main.tf | 1 + terraform/dev/variables.tf | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 6bdd3d2..c9cd864 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -157,6 +157,7 @@ module "endpoints" { environment = "dev" source_directory = "../../src" function_name = "tech-report-api" + region = var.region service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway max_instance_request_concurrency = var.max_instance_request_concurrency diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 0c2032c..de3edfc 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -25,3 +25,8 @@ variable "max_instance_request_concurrency" { type = number default = 1 } + +variable "region" { + default = "us-central1" + type = string +} From f32d7117b58cf5b2058f8a807c3c82948947d578 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 10 May 2025 23:59:36 +0200 Subject: [PATCH 06/44] formatting --- terraform/modules/api-gateway/networking.tf | 16 +-- terraform/modules/cloud-function/variables.tf | 2 +- terraform/modules/service-account/main.tf | 8 +- terraform/modules/service-account/outputs.tf | 2 +- .../modules/service-account/variables.tf | 2 +- terraform/prod/main.tf | 132 +++++++++--------- 6 files changed, 81 insertions(+), 81 deletions(-) diff --git a/terraform/modules/api-gateway/networking.tf b/terraform/modules/api-gateway/networking.tf index 9fb3462..a07e6e2 100644 --- a/terraform/modules/api-gateway/networking.tf +++ b/terraform/modules/api-gateway/networking.tf @@ -29,7 +29,7 @@ resource "google_compute_managed_ssl_certificate" "default" { managed { domains = ["api.httparchive.org"] } - + } resource "google_compute_target_https_proxy" "default" { @@ -40,7 +40,7 @@ resource "google_compute_target_https_proxy" "default" { project = var.project name = "httparchive-api-gateway-https-proxy" url_map = google_compute_url_map.default[count.index].id - ssl_certificates = [google_compute_managed_ssl_certificate.default[count.index].id] + ssl_certificates = [google_compute_managed_ssl_certificate.default[count.index].id] } resource "google_compute_region_network_endpoint_group" "function_neg" { @@ -57,7 +57,7 @@ resource "google_compute_region_network_endpoint_group" "function_neg" { platform = "apigateway.googleapis.com" resource = google_api_gateway_gateway.gateway.gateway_id } - + } resource "google_compute_backend_service" "backend_neg" { @@ -70,17 +70,17 @@ resource "google_compute_backend_service" "backend_neg" { load_balancing_scheme = "EXTERNAL_MANAGED" protocol = "HTTP" backend { - group = google_compute_region_network_endpoint_group.function_neg[count.index].self_link - } - + group = google_compute_region_network_endpoint_group.function_neg[count.index].self_link + } + } resource "google_compute_url_map" "default" { #count = var.environment == "prod" ? 1 : 0 count = 0 - + provider = google-beta project = var.project name = "httparchive-api-gateway-url-map" default_service = google_compute_backend_service.backend_neg[count.index].self_link -} \ No newline at end of file +} diff --git a/terraform/modules/cloud-function/variables.tf b/terraform/modules/cloud-function/variables.tf index cad4a5c..710b4d0 100644 --- a/terraform/modules/cloud-function/variables.tf +++ b/terraform/modules/cloud-function/variables.tf @@ -3,7 +3,7 @@ variable "secrets" { } variable "region" { default = "us-east1" - type = string + type = string } variable "environment" { description = "The 'Environment' that is being created/deployed. Applied as a suffix to many resources." diff --git a/terraform/modules/service-account/main.tf b/terraform/modules/service-account/main.tf index a635292..ab94d00 100644 --- a/terraform/modules/service-account/main.tf +++ b/terraform/modules/service-account/main.tf @@ -6,9 +6,9 @@ resource "google_service_account" "service_account" { display_name = var.display_name } resource "google_project_iam_member" "permissions" { - for_each = toset(var.permissions) - project = var.project - role = each.key - member = google_service_account.service_account.member + for_each = toset(var.permissions) + project = var.project + role = each.key + member = google_service_account.service_account.member depends_on = [google_service_account.service_account] } diff --git a/terraform/modules/service-account/outputs.tf b/terraform/modules/service-account/outputs.tf index 970c423..d5764fd 100644 --- a/terraform/modules/service-account/outputs.tf +++ b/terraform/modules/service-account/outputs.tf @@ -5,4 +5,4 @@ output "email" { output "member" { description = "The Identity of the service account in the form serviceAccount:{email}. This value is often used to refer to the service account in order to grant IAM permissions." value = google_service_account.service_account.member -} \ No newline at end of file +} diff --git a/terraform/modules/service-account/variables.tf b/terraform/modules/service-account/variables.tf index b0f5e44..c3ab94b 100644 --- a/terraform/modules/service-account/variables.tf +++ b/terraform/modules/service-account/variables.tf @@ -14,4 +14,4 @@ variable "permissions" { default = [] type = list(string) description = "A list of IAM Permissions for the Service Account" -} \ No newline at end of file +} diff --git a/terraform/prod/main.tf b/terraform/prod/main.tf index f0cc004..6169d45 100644 --- a/terraform/prod/main.tf +++ b/terraform/prod/main.tf @@ -27,7 +27,7 @@ resource "google_api_gateway_api_config" "api_config" { display_name = "The prod Config" openapi_documents { document { - path = "spec.yaml" + path = "spec.yaml" contents = base64encode(<<-EOF swagger: "2.0" info: @@ -166,122 +166,122 @@ resource "google_api_gateway_gateway" "gateway" { } module "cwvtech" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/cwvtech" - function_name = "cwvtech" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/cwvtech" + function_name = "cwvtech" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "lighthouse" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/lighthouse" - function_name = "lighthouse" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/lighthouse" + function_name = "lighthouse" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "adoption" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/adoption" - function_name = "adoption" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/adoption" + function_name = "adoption" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "page-weight" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/page-weight" - function_name = "page-weight" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/page-weight" + function_name = "page-weight" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "categories" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/categories" - function_name = "categories" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/categories" + function_name = "categories" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "technologies" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/technologies" - function_name = "technologies" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/technologies" + function_name = "technologies" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway - min_instances = var.min_instances + min_instances = var.min_instances environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "ranks" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/ranks" - function_name = "ranks" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/ranks" + function_name = "ranks" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } module "geos" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = "httparchive" - environment = "prod" - source_directory = "../../functions/geos" - function_name = "geos" - service_account_email = var.google_service_account_cloud_functions + source = "./../modules/cloud-function" + entry_point = "dispatcher" + project = "httparchive" + environment = "prod" + source_directory = "../../functions/geos" + function_name = "geos" + service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = "httparchive", "DATABASE" = var.project_database } } From 6508edd20b7ce2829848e05f52b01db8af1e45be Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 00:54:32 +0200 Subject: [PATCH 07/44] exports entry point --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index a4d3b40..ff2c78d 100644 --- a/src/index.js +++ b/src/index.js @@ -74,4 +74,4 @@ app.listen(port, () => { }); // Export for Cloud Run -module.exports = app; +exports.app = app; From 31672700e63a9eec347b611c45aa9546d4838bb6 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 01:09:48 +0200 Subject: [PATCH 08/44] no prod port --- src/index.js | 13 ++++++++----- terraform/dev/main.tf | 5 +++-- terraform/dev/variables.tf | 12 ++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index ff2c78d..dea6a54 100644 --- a/src/index.js +++ b/src/index.js @@ -67,11 +67,14 @@ app.use((err, req, res, next) => { })); }); -// Start server -const port = process.env.PORT || 8080; -app.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); +// For local development +if (process.env.ENVIRONMENT === 'dev') { + // Start server locally + const port = process.env.PORT || 8080; + app.listen(port, () => { + console.log(`Server listening on port ${port}`); + }); +} // Export for Cloud Run exports.app = app; diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index c9cd864..46fc4b2 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -154,7 +154,7 @@ module "endpoints" { source = "./../modules/run-service" entry_point = "app" project = "httparchive" - environment = "dev" + environment = var.environment source_directory = "../../src" function_name = "tech-report-api" region = var.region @@ -163,7 +163,8 @@ module "endpoints" { max_instance_request_concurrency = var.max_instance_request_concurrency min_instances = var.min_instances environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database + "ENVIRONMENT" = var.environment } } diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index de3edfc..d6227a4 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -30,3 +30,15 @@ variable "region" { default = "us-central1" type = string } + +variable "environment" { + description = "The environment name" + type = string + default = "dev" +} + +variable "project" { + description = "The project name" + type = string + default = "httparchive" +} From aa983c35dbc07cb7ac3c803b20adb0c714a682be Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 01:18:47 +0200 Subject: [PATCH 09/44] fix port --- src/index.js | 11 +- src/package-lock.json | 724 ++++++++++++++++++++++++++++++++++++++++++ src/package.json | 4 +- terraform/dev/main.tf | 3 +- 4 files changed, 728 insertions(+), 14 deletions(-) diff --git a/src/index.js b/src/index.js index dea6a54..713eb8b 100644 --- a/src/index.js +++ b/src/index.js @@ -67,14 +67,5 @@ app.use((err, req, res, next) => { })); }); -// For local development -if (process.env.ENVIRONMENT === 'dev') { - // Start server locally - const port = process.env.PORT || 8080; - app.listen(port, () => { - console.log(`Server listening on port ${port}`); - }); -} - -// Export for Cloud Run +// Export the app for Cloud Functions Framework exports.app = app; diff --git a/src/package-lock.json b/src/package-lock.json index c704213..d357f56 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -13,6 +13,7 @@ "express": "4.21.2" }, "devDependencies": { + "@google-cloud/functions-framework": "^4.0.0", "jest": "29.7.0", "nodemon": "3.0.1" }, @@ -585,6 +586,43 @@ "node": ">=14.0.0" } }, + "node_modules/@google-cloud/functions-framework": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-4.0.0.tgz", + "integrity": "sha512-CNcYrz0/hw35Oq0D9RipHUB8KzH4ixq7o12L//qoOg0TFYv4953KrzCo0L2VP++19P39RShKTftDKMFmQhCeEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/express": "^4.17.21", + "body-parser": "1.20.3", + "cloudevents": "^8.0.2", + "express": "^4.21.2", + "minimist": "^1.2.8", + "on-finished": "^2.3.0", + "read-package-up": "^11.0.0", + "semver": "^7.7.1" + }, + "bin": { + "functions-framework": "build/src/main.js", + "functions-framework-nodejs": "build/src/main.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google-cloud/functions-framework/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz", @@ -1143,12 +1181,59 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "license": "MIT" }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1159,6 +1244,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1192,6 +1284,13 @@ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.15.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", @@ -1201,6 +1300,27 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/request": { "version": "2.48.12", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", @@ -1213,6 +1333,29 @@ "form-data": "^2.5.0" } }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1277,6 +1420,41 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1353,6 +1531,22 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1631,6 +1825,25 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1790,6 +2003,34 @@ "node": ">=12" } }, + "node_modules/cloudevents": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-8.0.3.tgz", + "integrity": "sha512-wTixKNjfLeyj9HQpESvLVVO4xgdqdvX4dTeg1IZ2SCunu/fxVzCamcIZneEyj31V82YolFCKwVeSkr8zResB0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "json-bigint": "^1.0.0", + "process": "^0.11.10", + "util": "^0.12.4", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=16 <=22" + } + }, + "node_modules/cloudevents/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1972,6 +2213,24 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2332,6 +2591,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2387,6 +2663,35 @@ "node": ">=8" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", @@ -2703,6 +3008,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2742,6 +3060,26 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2909,6 +3247,19 @@ "node": ">=0.8.19" } }, + "node_modules/index-to-position": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", + "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2936,6 +3287,23 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2956,6 +3324,19 @@ "node": ">=8" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3001,6 +3382,25 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3024,6 +3424,25 @@ "node": ">=0.12.0" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3036,6 +3455,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3798,6 +4233,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4046,6 +4488,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4184,6 +4636,34 @@ "node": ">=4" } }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4443,6 +4923,16 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4471,6 +4961,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4604,6 +5104,88 @@ "dev": true, "license": "MIT" }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4640,6 +5222,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4728,6 +5320,24 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4798,6 +5408,24 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4970,6 +5598,42 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5301,6 +5965,19 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5341,6 +6018,20 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5384,6 +6075,17 @@ "node": ">=10.12.0" } }, + "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", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5435,6 +6137,28 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/src/package.json b/src/package.json index 776bd86..900423b 100644 --- a/src/package.json +++ b/src/package.json @@ -7,8 +7,7 @@ "node": ">=22.0.0" }, "scripts": { - "start": "node index.js", - "dev": "nodemon index.js", + "start": "functions-framework --target=app", "test": "jest" }, "dependencies": { @@ -17,6 +16,7 @@ "express": "4.21.2" }, "devDependencies": { + "@google-cloud/functions-framework": "^4.0.0", "jest": "29.7.0", "nodemon": "3.0.1" }, diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 46fc4b2..8571d77 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -140,7 +140,7 @@ resource "google_api_gateway_gateway" "gateway" { display_name = "devApi Gateway" labels = { owner = "tech_report_api" - environment = "dev" + environment = var.environment } depends_on = [google_api_gateway_api_config.api_config] lifecycle { @@ -165,6 +165,5 @@ module "endpoints" { environment_variables = { "PROJECT" = var.project "DATABASE" = var.project_database - "ENVIRONMENT" = var.environment } } From df29edbeb6b66af673aad94163716f003202213b Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 01:26:40 +0200 Subject: [PATCH 10/44] sync routes --- terraform/dev/main.tf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 8571d77..3d1f24f 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -45,7 +45,7 @@ paths: summary: categories operationId: getCategories x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/categories deadline: 60 responses: 200: @@ -55,7 +55,7 @@ paths: summary: adoption operationId: getadoptionReports x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/adoption deadline: 60 responses: 200: @@ -65,7 +65,7 @@ paths: summary: pageWeight operationId: getpageWeight x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/page-weight deadline: 60 responses: 200: @@ -75,7 +75,7 @@ paths: summary: lighthouse operationId: getLighthouseReports x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/lighthouse deadline: 60 responses: 200: @@ -85,7 +85,7 @@ paths: summary: cwv operationId: getCwv x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/cwv deadline: 60 responses: 200: @@ -95,7 +95,7 @@ paths: summary: ranks operationId: getRanks x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/ranks deadline: 60 responses: 200: @@ -105,7 +105,7 @@ paths: summary: geos operationId: getGeos x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/geos deadline: 60 responses: 200: @@ -115,7 +115,7 @@ paths: summary: technologies operationId: getTechnologies x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/technologies deadline: 60 responses: 200: From 18e560f3427f4794f42a1bebbba80f1992931a7a Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 02:54:14 +0200 Subject: [PATCH 11/44] test global backend --- terraform/dev/main.tf | 47 +++++++++++++------------------------------ 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 3d1f24f..24bdbdc 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -1,14 +1,14 @@ provider "google" { - project = "httparchive" - region = "us-central1" + project = var.project + region = var.region request_timeout = "60m" } terraform { backend "gcs" { bucket = "tf-state-backingapi-20230314" - prefix = "dev" + prefix = var.environment } } @@ -16,7 +16,7 @@ resource "google_api_gateway_api" "api" { provider = google-beta api_id = "api-gw-dev" display_name = "The dev API Gateway" - project = "httparchive" + project = var.project } # A Configuration, consisting of an OpenAPI specification @@ -24,7 +24,7 @@ resource "google_api_gateway_api_config" "api_config" { provider = google-beta api = google_api_gateway_api.api.api_id api_config_id_prefix = "api" - project = "httparchive" + project = var.project display_name = "The dev Config" openapi_documents { document { @@ -39,14 +39,16 @@ schemes: - https produces: - application/json +x-google-backend: + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev + deadline: 60 + path_translation: APPEND_PATH_TO_ADDRESS + protocol: h2 paths: - /v1/categories: + /categories: get: summary: categories operationId: getCategories - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/categories - deadline: 60 responses: 200: description: String @@ -54,9 +56,6 @@ paths: get: summary: adoption operationId: getadoptionReports - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/adoption - deadline: 60 responses: 200: description: String @@ -64,9 +63,6 @@ paths: get: summary: pageWeight operationId: getpageWeight - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/page-weight - deadline: 60 responses: 200: description: String @@ -74,9 +70,6 @@ paths: get: summary: lighthouse operationId: getLighthouseReports - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/lighthouse - deadline: 60 responses: 200: description: String @@ -84,9 +77,6 @@ paths: get: summary: cwv operationId: getCwv - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/cwv - deadline: 60 responses: 200: description: String @@ -94,9 +84,6 @@ paths: get: summary: ranks operationId: getRanks - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/ranks - deadline: 60 responses: 200: description: String @@ -104,9 +91,6 @@ paths: get: summary: geos operationId: getGeos - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/geos - deadline: 60 responses: 200: description: String @@ -114,9 +98,6 @@ paths: get: summary: technologies operationId: getTechnologies - x-google-backend: - address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-dev/technologies - deadline: 60 responses: 200: description: String @@ -133,8 +114,8 @@ EOF # The actual API Gateway resource "google_api_gateway_gateway" "gateway" { provider = google-beta - project = "httparchive" - region = "us-central1" + project = var.project + region = var.region api_config = google_api_gateway_api_config.api_config.id gateway_id = "dev-gw" display_name = "devApi Gateway" @@ -153,7 +134,7 @@ resource "google_api_gateway_gateway" "gateway" { module "endpoints" { source = "./../modules/run-service" entry_point = "app" - project = "httparchive" + project = var.project environment = var.environment source_directory = "../../src" function_name = "tech-report-api" From 14344d25df9a0d75a6299bf1b97410a049aa8064 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 02:56:31 +0200 Subject: [PATCH 12/44] no var --- terraform/dev/main.tf | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 24bdbdc..b4af062 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -1,3 +1,9 @@ +terraform { + backend "gcs" { + bucket = "tf-state-backingapi-20230314" + prefix = "dev" + } +} provider "google" { project = var.project @@ -5,13 +11,6 @@ provider "google" { request_timeout = "60m" } -terraform { - backend "gcs" { - bucket = "tf-state-backingapi-20230314" - prefix = var.environment - } -} - resource "google_api_gateway_api" "api" { provider = google-beta api_id = "api-gw-dev" From e65c4dc6bcfc532e38daaf391ae653d883a03619 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 03:06:52 +0200 Subject: [PATCH 13/44] v1 path --- src/index.js | 16 ++++++++-------- terraform/dev/main.tf | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 713eb8b..b77e659 100644 --- a/src/index.js +++ b/src/index.js @@ -45,14 +45,14 @@ app.use((req, res, next) => { }); // Define routes -app.use('/technologies', technologiesRoutes); -app.use('/categories', categoriesRoutes); -app.use('/adoption', adoptionRoutes); -app.use('/cwv', cwvtechRoutes); -app.use('/lighthouse', lighthouseRoutes); -app.use('/page-weight', pageWeightRoutes); -app.use('/ranks', ranksRoutes); -app.use('/geos', geosRoutes); +app.use('/v1/technologies', technologiesRoutes); +app.use('/v1/categories', categoriesRoutes); +app.use('/v1/adoption', adoptionRoutes); +app.use('/v1/cwv', cwvtechRoutes); +app.use('/v1/lighthouse', lighthouseRoutes); +app.use('/v1/page-weight', pageWeightRoutes); +app.use('/v1/ranks', ranksRoutes); +app.use('/v1/geos', geosRoutes); // Health check endpoint app.get('/', (req, res) => { diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index b4af062..ea7e08d 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -44,7 +44,7 @@ x-google-backend: path_translation: APPEND_PATH_TO_ADDRESS protocol: h2 paths: - /categories: + /v1/categories: get: summary: categories operationId: getCategories From 7523248d147105dd1c4b3cbc5eb2d2d5571348bd Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 03:53:00 +0200 Subject: [PATCH 14/44] fix vars --- .github/workflows/pipeline.yaml | 15 +--- terraform/dev/main.tf | 2 - terraform/dev/variables.tf | 46 ++++-------- terraform/modules/run-service/variables.tf | 4 +- terraform/prod/main.tf | 87 +++++++++------------- terraform/prod/variables.tf | 35 +++++++-- 6 files changed, 85 insertions(+), 104 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 54cef52..b297142 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -3,10 +3,7 @@ name: Tech Report API Pipeline on: [push] env: - PIPELINE_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} PIPELINE_SA_KEY: ${{ secrets.GCP_SA_KEY }} - PIPELINE_PROJECT_DATABASE_DEV: ${{ secrets.GCP_PROJECT_DATABASE_DEV }} - PIPELINE_PROJECT_DATABASE_PROD: ${{ secrets.GCP_PROJECT_DATABASE_PROD }} PIPELINE_GOOGLE_SERVICE_ACCOUNT_CLOUD_FUNCTIONS: ${{ secrets.GCP_SERVICE_ACCOUNT_CLOUD_FUNCTIONS }} PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY: ${{ secrets.GCP_SERVICE_ACCOUNT_API_GATEWAY }} @@ -40,7 +37,7 @@ jobs: - name: Google Cloud Auth uses: 'google-github-actions/auth@v2' with: - project_id: ${{ env.PIPELINE_PROJECT_ID }} + project_id: 'httparchive' credentials_json: ${{ env.PIPELINE_SA_KEY }} - uses: hashicorp/setup-terraform@v3 @@ -62,8 +59,7 @@ jobs: id: plan run: | terraform plan -no-color -var="google_service_account_cloud_functions=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_CLOUD_FUNCTIONS }}" \ - -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \ - -var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_DEV }}" + -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" continue-on-error: true - name: Terraform Plan status @@ -75,7 +71,6 @@ jobs: run: | terraform apply -var="google_service_account_cloud_functions=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_CLOUD_FUNCTIONS }}" \ -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \ - -var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_DEV }}" \ -auto-approve deploy_production: @@ -90,7 +85,7 @@ jobs: - name: Google Cloud Auth uses: 'google-github-actions/auth@v2' with: - project_id: ${{ env.PIPELINE_PROJECT_ID }} + project_id: 'httparchive' credentials_json: ${{ env.PIPELINE_SA_KEY }} - uses: hashicorp/setup-terraform@v3 @@ -112,8 +107,7 @@ jobs: id: plan run: | terraform plan -no-color -var="google_service_account_cloud_functions=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_CLOUD_FUNCTIONS }}" \ - -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \ - -var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_PROD }}" + -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" continue-on-error: true - name: Terraform Plan status @@ -125,6 +119,5 @@ jobs: run: | terraform apply -var="google_service_account_cloud_functions=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_CLOUD_FUNCTIONS }}" \ -var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \ - -var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_PROD }}" \ -auto-approve diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index ea7e08d..d6d8a63 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -140,8 +140,6 @@ module "endpoints" { region = var.region service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway - max_instance_request_concurrency = var.max_instance_request_concurrency - min_instances = var.min_instances environment_variables = { "PROJECT" = var.project "DATABASE" = var.project_database diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index d6227a4..aaa466e 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -1,44 +1,28 @@ -variable "google_service_account_cloud_functions" { - type = string - description = "Service account for Cloud Functions" -} - -variable "google_service_account_api_gateway" { - type = string - description = "Service account for API Gateway" -} - -variable "project_database" { +variable "project" { + description = "The project name" type = string - description = "The database name" - -} - -variable "min_instances" { - description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." - type = number - default = 0 -} - -variable "max_instance_request_concurrency" { - description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." - type = number - default = 1 + default = "httparchive" } - variable "region" { - default = "us-central1" type = string + default = "us-central1" } - variable "environment" { description = "The environment name" type = string default = "dev" } +variable "project_database" { + type = string + description = "The database name" + default = "tech-report-apis-prod" // TODO: Update this to the correct database name +} -variable "project" { - description = "The project name" +variable "google_service_account_cloud_functions" { type = string - default = "httparchive" + description = "Service account for Cloud Functions" +} +variable "google_service_account_api_gateway" { + type = string + description = "Service account for API Gateway" } diff --git a/terraform/modules/run-service/variables.tf b/terraform/modules/run-service/variables.tf index 710b4d0..e204693 100644 --- a/terraform/modules/run-service/variables.tf +++ b/terraform/modules/run-service/variables.tf @@ -54,7 +54,7 @@ variable "service_account_api_gateway" { description = "API Gateway service account who can invoke this function. This is required!" } variable "max_instances" { - default = 5 + default = 1 type = number description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." } @@ -66,7 +66,7 @@ variable "min_instances" { variable "max_instance_request_concurrency" { description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." type = number - default = 5 + default = 1 } variable "environment_variables" { description = "environment_variables" diff --git a/terraform/prod/main.tf b/terraform/prod/main.tf index 6169d45..12c9c3d 100644 --- a/terraform/prod/main.tf +++ b/terraform/prod/main.tf @@ -1,10 +1,3 @@ - -provider "google" { - project = "httparchive" - region = "us-east1" - request_timeout = "60m" -} - terraform { backend "gcs" { bucket = "tf-state-backingapi-20230314" @@ -12,18 +5,24 @@ terraform { } } +provider "google" { + project = var.project + region = var.region + request_timeout = "60m" +} + resource "google_api_gateway_api" "api" { provider = google-beta api_id = "api-gw-prod" display_name = "The prod API Gateway" - project = "httparchive" + project = var.project } resource "google_api_gateway_api_config" "api_config" { provider = google-beta api = google_api_gateway_api.api.api_id api_config_id_prefix = "api" - project = "httparchive" + project = var.project display_name = "The prod Config" openapi_documents { document { @@ -46,8 +45,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/categories-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -58,8 +55,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/adoption-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -70,8 +65,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/page-weight-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -82,8 +75,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/lighthouse-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -94,8 +85,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/cwvtech-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -106,8 +95,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/ranks-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -118,8 +105,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/geos-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -130,8 +115,6 @@ paths: x-google-backend: address: https://us-east1-httparchive.cloudfunctions.net/technologies-prod deadline: 60 - # security: - # - api_key: [] responses: 200: description: String @@ -148,14 +131,14 @@ EOF resource "google_api_gateway_gateway" "gateway" { provider = google-beta - project = "httparchive" - region = "us-east1" + project = var.project + region = var.region api_config = google_api_gateway_api_config.api_config.id gateway_id = "prod-gw" display_name = "prod Api Gateway" labels = { owner = "tech_report_api" - environment = "prod" + environment = var.environment } depends_on = [google_api_gateway_api_config.api_config] lifecycle { @@ -168,14 +151,14 @@ resource "google_api_gateway_gateway" "gateway" { module "cwvtech" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/cwvtech" function_name = "cwvtech" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -183,14 +166,14 @@ module "cwvtech" { module "lighthouse" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/lighthouse" function_name = "lighthouse" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -198,14 +181,14 @@ module "lighthouse" { module "adoption" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/adoption" function_name = "adoption" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -213,14 +196,14 @@ module "adoption" { module "page-weight" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/page-weight" function_name = "page-weight" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -228,14 +211,14 @@ module "page-weight" { module "categories" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/categories" function_name = "categories" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -243,15 +226,15 @@ module "categories" { module "technologies" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/technologies" function_name = "technologies" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway min_instances = var.min_instances environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -259,14 +242,14 @@ module "technologies" { module "ranks" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/ranks" function_name = "ranks" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } @@ -274,14 +257,14 @@ module "ranks" { module "geos" { source = "./../modules/cloud-function" entry_point = "dispatcher" - project = "httparchive" - environment = "prod" + project = var.project + environment = var.environment source_directory = "../../functions/geos" function_name = "geos" service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway environment_variables = { - "PROJECT" = "httparchive", + "PROJECT" = var.project "DATABASE" = var.project_database } } diff --git a/terraform/prod/variables.tf b/terraform/prod/variables.tf index 05d2b80..d3a38bd 100644 --- a/terraform/prod/variables.tf +++ b/terraform/prod/variables.tf @@ -1,21 +1,44 @@ -variable "google_service_account_cloud_functions" { +variable "project" { + description = "The project name" type = string - description = "Service account for Cloud Functions" + default = "httparchive" } - -variable "google_service_account_api_gateway" { +variable "region" { + default = "us-east1" + type = string +} +variable "environment" { + description = "The environment name" type = string - description = "Service account for API Gateway" + default = "prod" } - variable "project_database" { type = string description = "The database name" + default = "tech-report-apis-prod" +} +variable "google_service_account_cloud_functions" { + type = string + description = "Service account for Cloud Functions" +} +variable "google_service_account_api_gateway" { + type = string + description = "Service account for API Gateway" } +variable "max_instances" { + default = 5 + type = number + description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." +} variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number default = 1 } +variable "max_instance_request_concurrency" { + description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." + type = number + default = 5 +} From 3a8860c81e4371e9a3fdd619f2388036ef08093b Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 03:56:25 +0200 Subject: [PATCH 15/44] dependabot --- .github/dependabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7bbdfe0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: "/src" + schedule: + interval: "weekly" + - package-ecosystem: "terraform" + directory: "/terraform" + schedule: + interval: "weekly" From 136e5ca456b8a2bd37849c1d1f2f3e572a8f27d2 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 05:05:56 +0200 Subject: [PATCH 16/44] scale test --- terraform/dev/main.tf | 3 +++ terraform/dev/variables.tf | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index d6d8a63..f7911a2 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -140,6 +140,9 @@ module "endpoints" { region = var.region service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway + max_instances = var.max_instances + min_instances = var.min_instances + max_instance_request_concurrency = var.max_instance_request_concurrency environment_variables = { "PROJECT" = var.project "DATABASE" = var.project_database diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index aaa466e..40c24c0 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -26,3 +26,19 @@ variable "google_service_account_api_gateway" { type = string description = "Service account for API Gateway" } + +variable "max_instances" { + default = 10 + type = number + description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." +} +variable "min_instances" { + description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." + type = number + default = 1 +} +variable "max_instance_request_concurrency" { + description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." + type = number + default = 10 +} From 898b041a8e215e3a970a0c252eb83192cdb50f30 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 19:06:15 +0200 Subject: [PATCH 17/44] 1 cpu --- terraform/dev/main.tf | 2 -- terraform/dev/variables.tf | 12 +----------- terraform/modules/cloud-function/variables.tf | 2 +- terraform/modules/run-service/main.tf | 1 + terraform/modules/run-service/variables.tf | 11 ++++++++--- terraform/prod/variables.tf | 11 ----------- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index f7911a2..3a621ea 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -140,9 +140,7 @@ module "endpoints" { region = var.region service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway - max_instances = var.max_instances min_instances = var.min_instances - max_instance_request_concurrency = var.max_instance_request_concurrency environment_variables = { "PROJECT" = var.project "DATABASE" = var.project_database diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 40c24c0..5d8c530 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -27,18 +27,8 @@ variable "google_service_account_api_gateway" { description = "Service account for API Gateway" } -variable "max_instances" { - default = 10 - type = number - description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." -} variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number - default = 1 -} -variable "max_instance_request_concurrency" { - description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." - type = number - default = 10 + default = 1 # TODO: Update this to 0 after performance testing } diff --git a/terraform/modules/cloud-function/variables.tf b/terraform/modules/cloud-function/variables.tf index 710b4d0..3cfe6af 100644 --- a/terraform/modules/cloud-function/variables.tf +++ b/terraform/modules/cloud-function/variables.tf @@ -61,7 +61,7 @@ variable "max_instances" { variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number - default = 0 + default = 1 } variable "max_instance_request_concurrency" { description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." diff --git a/terraform/modules/run-service/main.tf b/terraform/modules/run-service/main.tf index 7c21146..2120114 100644 --- a/terraform/modules/run-service/main.tf +++ b/terraform/modules/run-service/main.tf @@ -31,6 +31,7 @@ resource "google_cloudfunctions2_function" "function" { service_config { all_traffic_on_latest_revision = true available_memory = var.available_memory_mb + available_cpu = var.available_cpu ingress_settings = var.ingress_settings environment_variables = var.environment_variables diff --git a/terraform/modules/run-service/variables.tf b/terraform/modules/run-service/variables.tf index e204693..60e9b38 100644 --- a/terraform/modules/run-service/variables.tf +++ b/terraform/modules/run-service/variables.tf @@ -26,6 +26,11 @@ variable "available_memory_mb" { type = string description = "The amount of memory for the Cloud Function" } +variable "available_cpu" { + default = "1" + type = string + description = "The amount of CPU for the Cloud Function" +} variable "ingress_settings" { type = string default = "ALLOW_ALL" @@ -54,19 +59,19 @@ variable "service_account_api_gateway" { description = "API Gateway service account who can invoke this function. This is required!" } variable "max_instances" { - default = 1 + default = 10 type = number description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." } variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number - default = 0 + default = 1 } variable "max_instance_request_concurrency" { description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." type = number - default = 1 + default = 18 } variable "environment_variables" { description = "environment_variables" diff --git a/terraform/prod/variables.tf b/terraform/prod/variables.tf index d3a38bd..2e42cd8 100644 --- a/terraform/prod/variables.tf +++ b/terraform/prod/variables.tf @@ -26,19 +26,8 @@ variable "google_service_account_api_gateway" { type = string description = "Service account for API Gateway" } - -variable "max_instances" { - default = 5 - type = number - description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." -} variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number default = 1 } -variable "max_instance_request_concurrency" { - description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." - type = number - default = 5 -} From 249b26e5333ba9eff1314c7d948a0adefa3f972e Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 23:39:25 +0200 Subject: [PATCH 18/44] cleanup and tests --- .github/workflows/pipeline.yaml | 15 +- .gitignore | 11 +- conftest.py | 2 - functions/adoption/libs/__init__.py | 0 functions/adoption/libs/network.py | 37 --- functions/adoption/libs/queries.py | 48 --- functions/adoption/libs/result.py | 20 -- functions/adoption/libs/utils.py | 14 - functions/adoption/libs/validator.py | 27 -- functions/adoption/main.py | 24 -- functions/adoption/requirements.txt | 3 - functions/categories/libs/__init__.py | 0 functions/categories/libs/network.py | 37 --- functions/categories/libs/queries.py | 34 -- functions/categories/libs/result.py | 20 -- functions/categories/libs/utils.py | 14 - functions/categories/libs/validator.py | 22 -- functions/categories/main.py | 24 -- functions/categories/requirements.txt | 3 - functions/cwvtech/libs/__init__.py | 0 functions/cwvtech/libs/network.py | 37 --- functions/cwvtech/libs/queries.py | 42 --- functions/cwvtech/libs/result.py | 20 -- functions/cwvtech/libs/utils.py | 14 - functions/cwvtech/libs/validator.py | 27 -- functions/cwvtech/main.py | 24 -- functions/cwvtech/requirements.txt | 3 - functions/geos/libs/__init__.py | 0 functions/geos/libs/network.py | 37 --- functions/geos/libs/result.py | 20 -- functions/geos/libs/utils.py | 227 -------------- functions/geos/main.py | 15 - functions/geos/requirements.txt | 3 - functions/lighthouse/libs/__init__.py | 0 functions/lighthouse/libs/network.py | 37 --- functions/lighthouse/libs/queries.py | 43 --- functions/lighthouse/libs/result.py | 20 -- functions/lighthouse/libs/utils.py | 14 - functions/lighthouse/libs/validator.py | 27 -- functions/lighthouse/main.py | 24 -- functions/lighthouse/requirements.txt | 3 - functions/page-weight/libs/__init__.py | 0 functions/page-weight/libs/network.py | 37 --- functions/page-weight/libs/queries.py | 48 --- functions/page-weight/libs/result.py | 20 -- functions/page-weight/libs/utils.py | 14 - functions/page-weight/libs/validator.py | 27 -- functions/page-weight/main.py | 24 -- functions/page-weight/requirements.txt | 3 - functions/ranks/libs/__init__.py | 0 functions/ranks/libs/network.py | 37 --- functions/ranks/libs/result.py | 20 -- functions/ranks/libs/utils.py | 16 - functions/ranks/main.py | 15 - functions/ranks/requirements.txt | 3 - functions/technologies/libs/__init__.py | 0 functions/technologies/libs/network.py | 37 --- functions/technologies/libs/presenters.py | 10 - functions/technologies/libs/queries.py | 47 --- functions/technologies/libs/result.py | 20 -- functions/technologies/libs/utils.py | 14 - functions/technologies/libs/validator.py | 21 -- functions/technologies/main.py | 24 -- functions/technologies/requirements.txt | 3 - perf_lab_test/main.js | 98 ++++++ perf_lab_test/package-lock.json | 294 ++++++++++++++++++ perf_lab_test/package.json | 14 + requirements.txt | 5 - src/__tests__/routes.test.js | 20 +- src/index.js | 7 - src/package-lock.json | 175 ++++++++++- src/package.json | 8 +- tests/test_geos/libs/test_geos_result.py | 32 -- tests/test_geos/libs/test_geos_utils.py | 8 - tests/test_geos/test_geos_main.py | 20 -- tests/test_ranks/libs/test_ranks_result.py | 31 -- tests/test_ranks/libs/test_ranks_utils.py | 8 - tests/test_ranks/test_ranks_main.py | 0 .../libs/test_technologies_result.py | 32 -- .../libs/test_technologies_utils.py | 8 - .../test_technologies_main.py | 0 81 files changed, 597 insertions(+), 1595 deletions(-) delete mode 100644 conftest.py delete mode 100644 functions/adoption/libs/__init__.py delete mode 100644 functions/adoption/libs/network.py delete mode 100644 functions/adoption/libs/queries.py delete mode 100644 functions/adoption/libs/result.py delete mode 100644 functions/adoption/libs/utils.py delete mode 100644 functions/adoption/libs/validator.py delete mode 100644 functions/adoption/main.py delete mode 100644 functions/adoption/requirements.txt delete mode 100644 functions/categories/libs/__init__.py delete mode 100644 functions/categories/libs/network.py delete mode 100644 functions/categories/libs/queries.py delete mode 100644 functions/categories/libs/result.py delete mode 100644 functions/categories/libs/utils.py delete mode 100644 functions/categories/libs/validator.py delete mode 100644 functions/categories/main.py delete mode 100644 functions/categories/requirements.txt delete mode 100644 functions/cwvtech/libs/__init__.py delete mode 100644 functions/cwvtech/libs/network.py delete mode 100644 functions/cwvtech/libs/queries.py delete mode 100644 functions/cwvtech/libs/result.py delete mode 100644 functions/cwvtech/libs/utils.py delete mode 100644 functions/cwvtech/libs/validator.py delete mode 100644 functions/cwvtech/main.py delete mode 100644 functions/cwvtech/requirements.txt delete mode 100644 functions/geos/libs/__init__.py delete mode 100644 functions/geos/libs/network.py delete mode 100644 functions/geos/libs/result.py delete mode 100644 functions/geos/libs/utils.py delete mode 100644 functions/geos/main.py delete mode 100644 functions/geos/requirements.txt delete mode 100644 functions/lighthouse/libs/__init__.py delete mode 100644 functions/lighthouse/libs/network.py delete mode 100644 functions/lighthouse/libs/queries.py delete mode 100644 functions/lighthouse/libs/result.py delete mode 100644 functions/lighthouse/libs/utils.py delete mode 100644 functions/lighthouse/libs/validator.py delete mode 100644 functions/lighthouse/main.py delete mode 100644 functions/lighthouse/requirements.txt delete mode 100644 functions/page-weight/libs/__init__.py delete mode 100644 functions/page-weight/libs/network.py delete mode 100644 functions/page-weight/libs/queries.py delete mode 100644 functions/page-weight/libs/result.py delete mode 100644 functions/page-weight/libs/utils.py delete mode 100644 functions/page-weight/libs/validator.py delete mode 100644 functions/page-weight/main.py delete mode 100644 functions/page-weight/requirements.txt delete mode 100644 functions/ranks/libs/__init__.py delete mode 100644 functions/ranks/libs/network.py delete mode 100644 functions/ranks/libs/result.py delete mode 100644 functions/ranks/libs/utils.py delete mode 100644 functions/ranks/main.py delete mode 100644 functions/ranks/requirements.txt delete mode 100644 functions/technologies/libs/__init__.py delete mode 100644 functions/technologies/libs/network.py delete mode 100644 functions/technologies/libs/presenters.py delete mode 100644 functions/technologies/libs/queries.py delete mode 100644 functions/technologies/libs/result.py delete mode 100644 functions/technologies/libs/utils.py delete mode 100644 functions/technologies/libs/validator.py delete mode 100644 functions/technologies/main.py delete mode 100644 functions/technologies/requirements.txt create mode 100644 perf_lab_test/main.js create mode 100644 perf_lab_test/package-lock.json create mode 100644 perf_lab_test/package.json delete mode 100644 requirements.txt delete mode 100644 tests/test_geos/libs/test_geos_result.py delete mode 100644 tests/test_geos/libs/test_geos_utils.py delete mode 100644 tests/test_geos/test_geos_main.py delete mode 100644 tests/test_ranks/libs/test_ranks_result.py delete mode 100644 tests/test_ranks/libs/test_ranks_utils.py delete mode 100644 tests/test_ranks/test_ranks_main.py delete mode 100644 tests/test_technologies/libs/test_technologies_result.py delete mode 100644 tests/test_technologies/libs/test_technologies_utils.py delete mode 100644 tests/test_technologies/test_technologies_main.py diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index b297142..cd08ea7 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -12,18 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pytest - if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi - - name: Test with pytest - run: | - python -m pytest -W "ignore" + - run: | + npm ci + npm run test deploy_development: if: github.ref == 'refs/heads/development' diff --git a/.gitignore b/.gitignore index e9791a5..7230e41 100644 --- a/.gitignore +++ b/.gitignore @@ -24,15 +24,6 @@ spec.yaml # CLI .DS_Store -# asdf -/.tool-versions - -# python -__pycache__ -.pytest_cache -.venv - -utils.txt -logs node_modules/ +src/coverage/ diff --git a/conftest.py b/conftest.py deleted file mode 100644 index ddd5225..0000000 --- a/conftest.py +++ /dev/null @@ -1,2 +0,0 @@ -import pytest -from functions import * \ No newline at end of file diff --git a/functions/adoption/libs/__init__.py b/functions/adoption/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/adoption/libs/network.py b/functions/adoption/libs/network.py deleted file mode 100644 index 76a731f..0000000 --- a/functions/adoption/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) \ No newline at end of file diff --git a/functions/adoption/libs/queries.py b/functions/adoption/libs/queries.py deleted file mode 100644 index 026cee9..0000000 --- a/functions/adoption/libs/queries.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from google.cloud import firestore -from google.cloud.firestore_v1.base_query import FieldFilter -from .result import Result -from .utils import convert_to_array - -DB = firestore.Client(project=os.environ.get('PROJECT'), database=os.environ.get('DATABASE')) -TABLE = 'adoption' - -def get_latest_date(): - """Retrieve the latest date in the collection.""" - query = DB.collection(TABLE).order_by('date', direction=firestore.Query.DESCENDING).limit(1) - docs = query.stream() - for doc in docs: - return doc.to_dict().get('date') - return None - -def list_data(params): - - technology_array = convert_to_array(params['technology']) - data = [] - - if 'start' in params and params['start'] == 'latest': - params['start'] = get_latest_date() - - for technology in technology_array: - query = DB.collection(TABLE) - - if 'start' in params: - query = query.where(filter=FieldFilter('date', '>=', params['start'])) - - if 'end' in params: - query = query.where(filter=FieldFilter('date', '<=', params['end'])) - - if 'geo' in params: - query = query.where(filter=FieldFilter('geo', '==', params['geo'])) - - if 'rank' in params: - query = query.where(filter=FieldFilter('rank', '==', params['rank'])) - - query = query.where(filter=FieldFilter('technology', '==', technology)) - - documents = query.stream() - - for doc in documents: - data.append(doc.to_dict()) - - return Result(result=data) \ No newline at end of file diff --git a/functions/adoption/libs/result.py b/functions/adoption/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/adoption/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/adoption/libs/utils.py b/functions/adoption/libs/utils.py deleted file mode 100644 index 997c85e..0000000 --- a/functions/adoption/libs/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -from urllib.parse import unquote - -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - -def convert_to_array(data_string): - decoded_data = unquote(data_string) - list = decoded_data.split(',') - return list diff --git a/functions/adoption/libs/validator.py b/functions/adoption/libs/validator.py deleted file mode 100644 index 4c202a4..0000000 --- a/functions/adoption/libs/validator.py +++ /dev/null @@ -1,27 +0,0 @@ -from .result import Result - -class Validator(): - def __init__(self, params): - self.params = params - self.errors = [] - self.normalizer_params = self.normalize(params) - - def validate(self): - result = Result(status="ok", result="()") - - if 'geo' not in self.params: - self.add_error("geo", "missing geo parameter") - - if 'technology' not in self.params: - self.add_error("technology", "missing technology parameter") - - if 'rank' not in self.params: - self.add_error("rank", "missing rank parameter") - - return Result(errors=self.errors, result=self.params) - - def add_error(self, key, error): - self.errors.append([key, error]) - - def normalize(self, params): - return "" diff --git a/functions/adoption/main.py b/functions/adoption/main.py deleted file mode 100644 index 65865dc..0000000 --- a/functions/adoption/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import functions_framework - -from .libs.validator import Validator -from .libs.queries import list_data -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - args = request.args.to_dict() - - validator = Validator(params=args) - result = validator.validate() - - if result.failure(): - print("error", result.errors) - return respond(result) - - response = list_data(result.result) - - return respond(response) diff --git a/functions/adoption/requirements.txt b/functions/adoption/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/adoption/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/categories/libs/__init__.py b/functions/categories/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/categories/libs/network.py b/functions/categories/libs/network.py deleted file mode 100644 index 76a731f..0000000 --- a/functions/categories/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) \ No newline at end of file diff --git a/functions/categories/libs/queries.py b/functions/categories/libs/queries.py deleted file mode 100644 index 141b14d..0000000 --- a/functions/categories/libs/queries.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from google.cloud import firestore -from google.cloud.firestore_v1.base_query import FieldFilter, Or -from .result import Result -from .utils import convert_to_array - -DB = firestore.Client( - project=os.environ.get("PROJECT"), database=os.environ.get("DATABASE") -) - -def list_data(params): - ref = DB.collection("categories") - - query = ref.order_by("category", direction=firestore.Query.ASCENDING) - - if "category" in params: - category_array = convert_to_array(params["category"]) - filter_array = [] - for category in category_array: - filter_array.append(FieldFilter("category", "==", category)) - query = query.where(filter=Or(filters=filter_array)) - - documents = query.stream() - data = [] - - if "onlyname" in params: - for doc in documents: - data.append(doc.get("category")) - - else: - for doc in documents: - data.append(doc.to_dict()) - - return Result(result=data) diff --git a/functions/categories/libs/result.py b/functions/categories/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/categories/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/categories/libs/utils.py b/functions/categories/libs/utils.py deleted file mode 100644 index 29692f4..0000000 --- a/functions/categories/libs/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -from urllib.parse import unquote - -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - -def convert_to_array(data_string): - decoded_data = unquote(data_string) - list = decoded_data.split(',') - return list \ No newline at end of file diff --git a/functions/categories/libs/validator.py b/functions/categories/libs/validator.py deleted file mode 100644 index ff19ca9..0000000 --- a/functions/categories/libs/validator.py +++ /dev/null @@ -1,22 +0,0 @@ -from .result import Result - -class Validator(): - def __init__(self, params): - self.params = params - self.errors = [] - self.normalizer_params = self.normalize(params) - - def validate(self): - result = Result(status="ok", result="()") - - # if 'onlyname' not in self.params: - # if 'category' not in self.params: - # self.add_error("category", "missing category parameter") - - return Result(errors=self.errors, result=self.params) - - def add_error(self, key, error): - self.errors.append([key, error]) - - def normalize(self, params): - return "" diff --git a/functions/categories/main.py b/functions/categories/main.py deleted file mode 100644 index e14f7ac..0000000 --- a/functions/categories/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import functions_framework - -from .libs.validator import Validator -from .libs.queries import list_data -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - args = request.args.to_dict() - - validator = Validator(params=args) - result = validator.validate() - - if result.failure(): - print("error", result.errors) - return respond(result) - - response = list_data(result.result) - - return respond(response) \ No newline at end of file diff --git a/functions/categories/requirements.txt b/functions/categories/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/categories/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/cwvtech/libs/__init__.py b/functions/cwvtech/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/cwvtech/libs/network.py b/functions/cwvtech/libs/network.py deleted file mode 100644 index d097aa8..0000000 --- a/functions/cwvtech/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) diff --git a/functions/cwvtech/libs/queries.py b/functions/cwvtech/libs/queries.py deleted file mode 100644 index 72c8ba6..0000000 --- a/functions/cwvtech/libs/queries.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -from google.cloud import firestore -from google.cloud.firestore_v1.base_query import FieldFilter -from .result import Result -from .utils import convert_to_array - -DB = firestore.Client(project=os.environ.get('PROJECT'), database=os.environ.get('DATABASE')) -TABLE = 'core_web_vitals' - -def get_latest_date(): - """Retrieve the latest date in the collection.""" - query = DB.collection(TABLE).order_by('date', direction=firestore.Query.DESCENDING).limit(1) - docs = query.stream() - for doc in docs: - return doc.to_dict().get('date') - return None - -def list_data(params): - technology_array = convert_to_array(params['technology']) - data = [] - - if 'start' in params and params['start'] == 'latest': - params['start'] = get_latest_date() - - for technology in technology_array: - query = DB.collection(TABLE) - - if 'start' in params: - query = query.where(filter=FieldFilter('date', '>=', params['start'])) - if 'end' in params: - query = query.where(filter=FieldFilter('date', '<=', params['end'])) - - query = query.where(filter=FieldFilter('geo', '==', params['geo'])) - query = query.where(filter=FieldFilter('rank', '==', params['rank'])) - query = query.where(filter=FieldFilter('technology', '==', technology)) - - documents = query.stream() - - for doc in documents: - data.append(doc.to_dict()) - - return Result(result=data) diff --git a/functions/cwvtech/libs/result.py b/functions/cwvtech/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/cwvtech/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/cwvtech/libs/utils.py b/functions/cwvtech/libs/utils.py deleted file mode 100644 index 29692f4..0000000 --- a/functions/cwvtech/libs/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -from urllib.parse import unquote - -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - -def convert_to_array(data_string): - decoded_data = unquote(data_string) - list = decoded_data.split(',') - return list \ No newline at end of file diff --git a/functions/cwvtech/libs/validator.py b/functions/cwvtech/libs/validator.py deleted file mode 100644 index 1797b49..0000000 --- a/functions/cwvtech/libs/validator.py +++ /dev/null @@ -1,27 +0,0 @@ -from .result import Result - -class Validator(): - def __init__(self, params): - self.params = params - self.errors = [] - self.normalizer_params = self.normalize(params) - - def validate(self): - result = Result(status="ok", result="()") - - if 'geo' not in self.params: - self.add_error("geo", "missing geo parameter") - - if 'technology' not in self.params: - self.add_error("technology", "missing technology parameter") - - if 'rank' not in self.params: - self.add_error("rank", "missing rank parameter") - - return Result(errors=self.errors, result=self.params) - - def add_error(self, key, error): - self.errors.append([key, error]) - - def normalize(self, params): - return "" diff --git a/functions/cwvtech/main.py b/functions/cwvtech/main.py deleted file mode 100644 index b68f15e..0000000 --- a/functions/cwvtech/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import functions_framework - -from .libs.validator import Validator -from .libs.queries import list_data -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - args = request.args.to_dict() - - validator = Validator(params=args) - result = validator.validate() - - if result.failure(): - print("error", result.errors) - return respond(result) - - response = list_data(result.result) - - return respond(response) \ No newline at end of file diff --git a/functions/cwvtech/requirements.txt b/functions/cwvtech/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/cwvtech/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/geos/libs/__init__.py b/functions/geos/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/geos/libs/network.py b/functions/geos/libs/network.py deleted file mode 100644 index 76a731f..0000000 --- a/functions/geos/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) \ No newline at end of file diff --git a/functions/geos/libs/result.py b/functions/geos/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/geos/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/geos/libs/utils.py b/functions/geos/libs/utils.py deleted file mode 100644 index 14dbde0..0000000 --- a/functions/geos/libs/utils.py +++ /dev/null @@ -1,227 +0,0 @@ -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - - -COUNTRIES = [ - {"geo": "ALL", "num_origins": "9731427"}, - {"geo": "United States of America", "num_origins": "1707677"}, - {"geo": "India", "num_origins": "826143"}, - {"geo": "Japan", "num_origins": "690984"}, - {"geo": "Germany", "num_origins": "678201"}, - {"geo": "Brazil", "num_origins": "644760"}, - { - "geo": "United Kingdom of Great Britain and Northern Ireland", - "num_origins": "560753", - }, - {"geo": "Russian Federation", "num_origins": "529803"}, - {"geo": "France", "num_origins": "515925"}, - {"geo": "Italy", "num_origins": "503015"}, - {"geo": "Spain", "num_origins": "459739"}, - {"geo": "Indonesia", "num_origins": "401253"}, - {"geo": "Poland", "num_origins": "350837"}, - {"geo": "Canada", "num_origins": "335548"}, - {"geo": "Mexico", "num_origins": "317337"}, - {"geo": "Turkey", "num_origins": "292310"}, - {"geo": "Netherlands", "num_origins": "291785"}, - {"geo": "Argentina", "num_origins": "252487"}, - {"geo": "Australia", "num_origins": "215909"}, - {"geo": "Korea, Republic of", "num_origins": "209013"}, - {"geo": "Philippines", "num_origins": "204637"}, - {"geo": "Colombia", "num_origins": "198020"}, - {"geo": "Malaysia", "num_origins": "193444"}, - {"geo": "Ukraine", "num_origins": "189866"}, - {"geo": "Viet Nam", "num_origins": "176473"}, - {"geo": "Thailand", "num_origins": "167337"}, - {"geo": "Pakistan", "num_origins": "157400"}, - {"geo": "Belgium", "num_origins": "157266"}, - {"geo": "South Africa", "num_origins": "150004"}, - {"geo": "Czechia", "num_origins": "148638"}, - {"geo": "Romania", "num_origins": "148176"}, - {"geo": "Taiwan, Province of China", "num_origins": "147383"}, - {"geo": "Chile", "num_origins": "144592"}, - {"geo": "Greece", "num_origins": "135996"}, - {"geo": "Austria", "num_origins": "135821"}, - {"geo": "Bangladesh", "num_origins": "134081"}, - {"geo": "Peru", "num_origins": "124954"}, - {"geo": "Iran (Islamic Republic of)", "num_origins": "122949"}, - {"geo": "Singapore", "num_origins": "121397"}, - {"geo": "Egypt", "num_origins": "119105"}, - {"geo": "Hungary", "num_origins": "117857"}, - {"geo": "Nigeria", "num_origins": "115407"}, - {"geo": "Portugal", "num_origins": "113035"}, - {"geo": "Kazakhstan", "num_origins": "111471"}, - {"geo": "Belarus", "num_origins": "109161"}, - {"geo": "Sweden", "num_origins": "108230"}, - {"geo": "Switzerland", "num_origins": "106121"}, - {"geo": "Saudi Arabia", "num_origins": "100966"}, - {"geo": "Israel", "num_origins": "99539"}, - {"geo": "Algeria", "num_origins": "98160"}, - {"geo": "Morocco", "num_origins": "96973"}, - {"geo": "Ireland", "num_origins": "96613"}, - {"geo": "Hong Kong", "num_origins": "95717"}, - {"geo": "United Arab Emirates", "num_origins": "91116"}, - {"geo": "Croatia", "num_origins": "85514"}, - {"geo": "Venezuela (Bolivarian Republic of)", "num_origins": "84283"}, - {"geo": "Slovakia", "num_origins": "84177"}, - {"geo": "Finland", "num_origins": "83107"}, - {"geo": "Serbia", "num_origins": "80789"}, - {"geo": "Ecuador", "num_origins": "80083"}, - {"geo": "Bulgaria", "num_origins": "75818"}, - {"geo": "Denmark", "num_origins": "69550"}, - {"geo": "New Zealand", "num_origins": "68444"}, - {"geo": "Uzbekistan", "num_origins": "65735"}, - {"geo": "Iraq", "num_origins": "65305"}, - {"geo": "Kenya", "num_origins": "62330"}, - {"geo": "Nepal", "num_origins": "60371"}, - {"geo": "Norway", "num_origins": "58300"}, - {"geo": "China", "num_origins": "57495"}, - {"geo": "Bolivia (Plurinational State of)", "num_origins": "55245"}, - {"geo": "Tunisia", "num_origins": "54813"}, - {"geo": "Sri Lanka", "num_origins": "53879"}, - {"geo": "Guatemala", "num_origins": "50897"}, - {"geo": "Azerbaijan", "num_origins": "46317"}, - {"geo": "Kyrgyzstan", "num_origins": "45478"}, - {"geo": "Lithuania", "num_origins": "45215"}, - {"geo": "Costa Rica", "num_origins": "44736"}, - {"geo": "Dominican Republic", "num_origins": "42618"}, - {"geo": "Moldova, Republic of", "num_origins": "41976"}, - {"geo": "Bosnia and Herzegovina", "num_origins": "41953"}, - {"geo": "Jordan", "num_origins": "41773"}, - {"geo": "Uruguay", "num_origins": "41139"}, - {"geo": "Panama", "num_origins": "38437"}, - {"geo": "Slovenia", "num_origins": "36027"}, - {"geo": "Ghana", "num_origins": "35980"}, - {"geo": "Paraguay", "num_origins": "35415"}, - {"geo": "Georgia", "num_origins": "34921"}, - {"geo": "Qatar", "num_origins": "34403"}, - {"geo": "Lebanon", "num_origins": "33694"}, - {"geo": "Puerto Rico", "num_origins": "33617"}, - {"geo": "El Salvador", "num_origins": "31654"}, - {"geo": "Syrian Arab Republic", "num_origins": "30714"}, - {"geo": "Latvia", "num_origins": "30530"}, - {"geo": "Honduras", "num_origins": "29712"}, - {"geo": "Myanmar", "num_origins": "29348"}, - {"geo": "Cyprus", "num_origins": "29012"}, - {"geo": "Oman", "num_origins": "27345"}, - {"geo": "Tanzania, United Republic of", "num_origins": "27335"}, - {"geo": "Cameroon", "num_origins": "26828"}, - {"geo": "Kuwait", "num_origins": "26458"}, - {"geo": "Armenia", "num_origins": "26355"}, - {"geo": "Nicaragua", "num_origins": "26015"}, - {"geo": "Estonia", "num_origins": "25576"}, - {"geo": "Côte d'Ivoire", "num_origins": "25208"}, - {"geo": "Cambodia", "num_origins": "24593"}, - {"geo": "Uganda", "num_origins": "24532"}, - {"geo": "Libya", "num_origins": "23730"}, - {"geo": "Cuba", "num_origins": "23056"}, - {"geo": "Ethiopia", "num_origins": "22650"}, - {"geo": "Albania", "num_origins": "22445"}, - {"geo": "Yemen", "num_origins": "22186"}, - {"geo": "North Macedonia", "num_origins": "21259"}, - {"geo": "Palestine, State of", "num_origins": "20468"}, - {"geo": "Senegal", "num_origins": "20323"}, - {"geo": "Montenegro", "num_origins": "20212"}, - {"geo": "Sudan", "num_origins": "20152"}, - {"geo": "Jamaica", "num_origins": "18847"}, - {"geo": "Iceland", "num_origins": "18261"}, - {"geo": "Zambia", "num_origins": "17567"}, - {"geo": "Bahrain", "num_origins": "17522"}, - {"geo": "Réunion", "num_origins": "17251"}, - {"geo": "Trinidad and Tobago", "num_origins": "16445"}, - {"geo": "Mauritius", "num_origins": "16238"}, - {"geo": "Zimbabwe", "num_origins": "15515"}, - {"geo": "Tajikistan", "num_origins": "14835"}, - {"geo": "Lao People's Democratic Republic", "num_origins": "14796"}, - {"geo": "Luxembourg", "num_origins": "14647"}, - {"geo": "Congo, Democratic Republic of the", "num_origins": "14545"}, - {"geo": "Angola", "num_origins": "13428"}, - {"geo": "Haiti", "num_origins": "13083"}, - {"geo": "Malta", "num_origins": "12984"}, - {"geo": "Mozambique", "num_origins": "12706"}, - {"geo": "Mongolia", "num_origins": "12574"}, - {"geo": "Burkina Faso", "num_origins": "12325"}, - {"geo": "Benin", "num_origins": "12292"}, - {"geo": "Somalia", "num_origins": "12176"}, - {"geo": "Mali", "num_origins": "10834"}, - {"geo": "Turkmenistan", "num_origins": "10192"}, - {"geo": "Afghanistan", "num_origins": "9613"}, - {"geo": "Martinique", "num_origins": "9314"}, - {"geo": "Guadeloupe", "num_origins": "8961"}, - {"geo": "Brunei Darussalam", "num_origins": "8854"}, - {"geo": "Botswana", "num_origins": "8657"}, - {"geo": "Namibia", "num_origins": "8535"}, - {"geo": "Papua New Guinea", "num_origins": "8447"}, - {"geo": "Togo", "num_origins": "8308"}, - {"geo": "Malawi", "num_origins": "8305"}, - {"geo": "Maldives", "num_origins": "8262"}, - {"geo": "Kosovo", "num_origins": "7807"}, - {"geo": "Gabon", "num_origins": "7754"}, - {"geo": "Bhutan", "num_origins": "6919"}, - {"geo": "Guinea", "num_origins": "6702"}, - {"geo": "Madagascar", "num_origins": "6620"}, - {"geo": "Guyana", "num_origins": "6303"}, - {"geo": "Rwanda", "num_origins": "6129"}, - {"geo": "Mauritania", "num_origins": "5995"}, - {"geo": "Macao", "num_origins": "5889"}, - {"geo": "Suriname", "num_origins": "5827"}, - {"geo": "Niger", "num_origins": "5484"}, - {"geo": "Fiji", "num_origins": "5388"}, - {"geo": "Congo", "num_origins": "4697"}, - {"geo": "Barbados", "num_origins": "4509"}, - {"geo": "Bahamas", "num_origins": "4467"}, - {"geo": "Chad", "num_origins": "4426"}, - {"geo": "Sierra Leone", "num_origins": "4345"}, - {"geo": "Cabo Verde", "num_origins": "4125"}, - {"geo": "Liberia", "num_origins": "3899"}, - {"geo": "Belize", "num_origins": "3871"}, - {"geo": "French Guiana", "num_origins": "3603"}, - {"geo": "Eswatini", "num_origins": "3554"}, - {"geo": "French Polynesia", "num_origins": "3489"}, - {"geo": "New Caledonia", "num_origins": "3379"}, - {"geo": "Lesotho", "num_origins": "3265"}, - {"geo": "Gambia", "num_origins": "3217"}, - {"geo": "Timor-Leste", "num_origins": "3074"}, - {"geo": "Andorra", "num_origins": "3073"}, - {"geo": "South Sudan", "num_origins": "3040"}, - {"geo": "Curaçao", "num_origins": "2987"}, - {"geo": "Western Sahara", "num_origins": "2739"}, - {"geo": "Saint Lucia", "num_origins": "2493"}, - {"geo": "Guam", "num_origins": "2466"}, - {"geo": "Antigua and Barbuda", "num_origins": "2449"}, - {"geo": "Aruba", "num_origins": "2420"}, - {"geo": "Djibouti", "num_origins": "2395"}, - {"geo": "Burundi", "num_origins": "2301"}, - {"geo": "Seychelles", "num_origins": "2007"}, - {"geo": "Mayotte", "num_origins": "1820"}, - {"geo": "Grenada", "num_origins": "1597"}, - {"geo": "Guinea-Bissau", "num_origins": "1592"}, - {"geo": "Comoros", "num_origins": "1563"}, - {"geo": "Cayman Islands", "num_origins": "1549"}, - {"geo": "Jersey", "num_origins": "1499"}, - {"geo": "Saint Vincent and the Grenadines", "num_origins": "1453"}, - {"geo": "Isle of Man", "num_origins": "1374"}, - {"geo": "Faroe Islands", "num_origins": "1233"}, - {"geo": "Equatorial Guinea", "num_origins": "1218"}, - {"geo": "Virgin Islands (U.S.)", "num_origins": "1074"}, - {"geo": "Dominica", "num_origins": "1049"}, - {"geo": "Sint Maarten (Dutch part)", "num_origins": "952"}, - {"geo": "Solomon Islands", "num_origins": "946"}, - {"geo": "Guernsey", "num_origins": "936"}, - {"geo": "Saint Kitts and Nevis", "num_origins": "917"}, - {"geo": "Central African Republic", "num_origins": "879"}, - {"geo": "Virgin Islands (British)", "num_origins": "864"}, - {"geo": "San Marino", "num_origins": "845"}, - {"geo": "Bermuda", "num_origins": "796"}, - {"geo": "Samoa", "num_origins": "771"}, - {"geo": "Gibraltar", "num_origins": "710"}, - {"geo": "Vanuatu", "num_origins": "697"}, - {"geo": "Saint Martin (French part)", "num_origins": "642"}, - {"geo": "Greenland", "num_origins": "631"}, - {"geo": "Bonaire, Sint Eustatius and Saba", "num_origins": "615"}, - {"geo": "Marshall Islands", "num_origins": "604"}, - {"geo": "Turks and Caicos Islands", "num_origins": "548"}, -] diff --git a/functions/geos/main.py b/functions/geos/main.py deleted file mode 100644 index 0fd5e9d..0000000 --- a/functions/geos/main.py +++ /dev/null @@ -1,15 +0,0 @@ -import functions_framework - -from .libs.utils import ( COUNTRIES ) -from .libs.result import Result -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - response = Result(result=COUNTRIES) - - return respond(response) \ No newline at end of file diff --git a/functions/geos/requirements.txt b/functions/geos/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/geos/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/lighthouse/libs/__init__.py b/functions/lighthouse/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/lighthouse/libs/network.py b/functions/lighthouse/libs/network.py deleted file mode 100644 index d097aa8..0000000 --- a/functions/lighthouse/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) diff --git a/functions/lighthouse/libs/queries.py b/functions/lighthouse/libs/queries.py deleted file mode 100644 index 09d33c9..0000000 --- a/functions/lighthouse/libs/queries.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -from google.cloud import firestore -from google.cloud.firestore_v1.base_query import FieldFilter -from .result import Result -from .utils import convert_to_array - -DB = firestore.Client(project=os.environ.get('PROJECT'), database=os.environ.get('DATABASE')) -TABLE = 'lighthouse' - -def get_latest_date(): - """Retrieve the latest date in the collection.""" - query = DB.collection(TABLE).order_by('date', direction=firestore.Query.DESCENDING).limit(1) - docs = query.stream() - for doc in docs: - return doc.to_dict().get('date') - return None - -def list_data(params): - - technology_array = convert_to_array(params['technology']) - data = [] - - if 'start' in params and params['start'] == 'latest': - params['start'] = get_latest_date() - - for technology in technology_array: - query = DB.collection(TABLE) - - if 'start' in params: - query = query.where(filter=FieldFilter('date', '>=', params['start'])) - if 'end' in params: - query = query.where(filter=FieldFilter('date', '<=', params['end'])) - - query = query.where(filter=FieldFilter('geo', '==', params['geo'])) - query = query.where(filter=FieldFilter('rank', '==', params['rank'])) - query = query.where(filter=FieldFilter('technology', '==', technology)) - - documents = query.stream() - - for doc in documents: - data.append(doc.to_dict()) - - return Result(result=data) \ No newline at end of file diff --git a/functions/lighthouse/libs/result.py b/functions/lighthouse/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/lighthouse/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/lighthouse/libs/utils.py b/functions/lighthouse/libs/utils.py deleted file mode 100644 index 29692f4..0000000 --- a/functions/lighthouse/libs/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -from urllib.parse import unquote - -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - -def convert_to_array(data_string): - decoded_data = unquote(data_string) - list = decoded_data.split(',') - return list \ No newline at end of file diff --git a/functions/lighthouse/libs/validator.py b/functions/lighthouse/libs/validator.py deleted file mode 100644 index 4c202a4..0000000 --- a/functions/lighthouse/libs/validator.py +++ /dev/null @@ -1,27 +0,0 @@ -from .result import Result - -class Validator(): - def __init__(self, params): - self.params = params - self.errors = [] - self.normalizer_params = self.normalize(params) - - def validate(self): - result = Result(status="ok", result="()") - - if 'geo' not in self.params: - self.add_error("geo", "missing geo parameter") - - if 'technology' not in self.params: - self.add_error("technology", "missing technology parameter") - - if 'rank' not in self.params: - self.add_error("rank", "missing rank parameter") - - return Result(errors=self.errors, result=self.params) - - def add_error(self, key, error): - self.errors.append([key, error]) - - def normalize(self, params): - return "" diff --git a/functions/lighthouse/main.py b/functions/lighthouse/main.py deleted file mode 100644 index 3fab032..0000000 --- a/functions/lighthouse/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import functions_framework - -from .libs.validator import Validator -from .libs.queries import list_data -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - args = request.args.to_dict() - - validator = Validator(params=args) - result = validator.validate() - - if result.failure(): - print("error", result.errors) - return respond(result) - - response = list_data(result.result) - - return respond(response) \ No newline at end of file diff --git a/functions/lighthouse/requirements.txt b/functions/lighthouse/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/lighthouse/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/page-weight/libs/__init__.py b/functions/page-weight/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/page-weight/libs/network.py b/functions/page-weight/libs/network.py deleted file mode 100644 index 76a731f..0000000 --- a/functions/page-weight/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) \ No newline at end of file diff --git a/functions/page-weight/libs/queries.py b/functions/page-weight/libs/queries.py deleted file mode 100644 index 8d4d29d..0000000 --- a/functions/page-weight/libs/queries.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from google.cloud import firestore -from google.cloud.firestore_v1.base_query import FieldFilter -from .result import Result -from .utils import convert_to_array - -DB = firestore.Client(project=os.environ.get('PROJECT'), database=os.environ.get('DATABASE')) -TABLE = 'page_weight' - -def get_latest_date(): - """Retrieve the latest date in the collection.""" - query = DB.collection(TABLE).order_by('date', direction=firestore.Query.DESCENDING).limit(1) - docs = query.stream() - for doc in docs: - return doc.to_dict().get('date') - return None - -def list_data(params): - - technology_array = convert_to_array(params['technology']) - data = [] - - if 'start' in params and params['start'] == 'latest': - params['start'] = get_latest_date() - - for technology in technology_array: - query = DB.collection(TABLE) - - if 'start' in params: - query = query.where(filter=FieldFilter('date', '>=', params['start'])) - - if 'end' in params: - query = query.where(filter=FieldFilter('date', '<=', params['end'])) - - if 'geo' in params: - query = query.where(filter=FieldFilter('geo', '==', params['geo'])) - - if 'rank' in params: - query = query.where(filter=FieldFilter('rank', '==', params['rank'])) - - query = query.where(filter=FieldFilter('technology', '==', technology)) - - documents = query.stream() - - for doc in documents: - data.append(doc.to_dict()) - - return Result(result=data) diff --git a/functions/page-weight/libs/result.py b/functions/page-weight/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/page-weight/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/page-weight/libs/utils.py b/functions/page-weight/libs/utils.py deleted file mode 100644 index 997c85e..0000000 --- a/functions/page-weight/libs/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -from urllib.parse import unquote - -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - -def convert_to_array(data_string): - decoded_data = unquote(data_string) - list = decoded_data.split(',') - return list diff --git a/functions/page-weight/libs/validator.py b/functions/page-weight/libs/validator.py deleted file mode 100644 index 4c202a4..0000000 --- a/functions/page-weight/libs/validator.py +++ /dev/null @@ -1,27 +0,0 @@ -from .result import Result - -class Validator(): - def __init__(self, params): - self.params = params - self.errors = [] - self.normalizer_params = self.normalize(params) - - def validate(self): - result = Result(status="ok", result="()") - - if 'geo' not in self.params: - self.add_error("geo", "missing geo parameter") - - if 'technology' not in self.params: - self.add_error("technology", "missing technology parameter") - - if 'rank' not in self.params: - self.add_error("rank", "missing rank parameter") - - return Result(errors=self.errors, result=self.params) - - def add_error(self, key, error): - self.errors.append([key, error]) - - def normalize(self, params): - return "" diff --git a/functions/page-weight/main.py b/functions/page-weight/main.py deleted file mode 100644 index 3fab032..0000000 --- a/functions/page-weight/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import functions_framework - -from .libs.validator import Validator -from .libs.queries import list_data -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - args = request.args.to_dict() - - validator = Validator(params=args) - result = validator.validate() - - if result.failure(): - print("error", result.errors) - return respond(result) - - response = list_data(result.result) - - return respond(response) \ No newline at end of file diff --git a/functions/page-weight/requirements.txt b/functions/page-weight/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/page-weight/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/ranks/libs/__init__.py b/functions/ranks/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/ranks/libs/network.py b/functions/ranks/libs/network.py deleted file mode 100644 index d097aa8..0000000 --- a/functions/ranks/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) diff --git a/functions/ranks/libs/result.py b/functions/ranks/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/ranks/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/ranks/libs/utils.py b/functions/ranks/libs/utils.py deleted file mode 100644 index 568afae..0000000 --- a/functions/ranks/libs/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - - -RANKS = [ - {"num_origins": "9731427", "rank": "ALL"}, - {"num_origins": "7232806", "rank": "Top 10M"}, - {"num_origins": "881817", "rank": "Top 1M"}, - {"num_origins": "91410", "rank": "Top 100k"}, - {"num_origins": "9524", "rank": "Top 10k"}, - {"num_origins": "965", "rank": "Top 1k"}, -] diff --git a/functions/ranks/main.py b/functions/ranks/main.py deleted file mode 100644 index 0b9147b..0000000 --- a/functions/ranks/main.py +++ /dev/null @@ -1,15 +0,0 @@ -import functions_framework - -from .libs.utils import ( RANKS ) -from .libs.result import Result -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - response = Result(result=RANKS) - - return respond(response) \ No newline at end of file diff --git a/functions/ranks/requirements.txt b/functions/ranks/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/ranks/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/functions/technologies/libs/__init__.py b/functions/technologies/libs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functions/technologies/libs/network.py b/functions/technologies/libs/network.py deleted file mode 100644 index d097aa8..0000000 --- a/functions/technologies/libs/network.py +++ /dev/null @@ -1,37 +0,0 @@ - -""" -Network - -Handles formatting responses to match the tuple pattern required by -the flask/GCP wrapper for Cloud Functions. -""" -import json -from .utils import convert_to_hashes - -PREFLIGHT_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Timing-Allow-Origin", - "Access-Control-Max-Age": "3600", - } - -HEADERS = { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - "cache-control": "public, max-age=21600", - "Timing-Allow-Origin": "*" - } - -def respond_cors(): - """ - To be used to return OPTIONS responses to satisfy CORS preflight requests. - """ - return ("", 204, PREFLIGHT_HEADERS) - -def respond(result, headers=HEADERS): - """ - To be used to return responses to satisfy CORS requests. - """ - status = 200 if result.success() else 400 - payload = result.result if result.success() else convert_to_hashes(result.errors) - return (json.dumps(payload), status, headers) diff --git a/functions/technologies/libs/presenters.py b/functions/technologies/libs/presenters.py deleted file mode 100644 index 02c1a4d..0000000 --- a/functions/technologies/libs/presenters.py +++ /dev/null @@ -1,10 +0,0 @@ -class Presenters: - @staticmethod - def technology(item): - return { - 'technology': item['technology'], - 'category': item['category'], - 'description': item['description'], - 'icon': item['icon'], - 'origins': item['origins'] - } diff --git a/functions/technologies/libs/queries.py b/functions/technologies/libs/queries.py deleted file mode 100644 index 6868472..0000000 --- a/functions/technologies/libs/queries.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import json -from google.cloud import firestore -from google.cloud.firestore_v1.base_query import FieldFilter, Or - -from .result import Result -from .utils import convert_to_array -from .presenters import Presenters - -DB = firestore.Client( - project=os.environ.get("PROJECT"), database=os.environ.get("DATABASE") -) - -def list_data(params): - ref = DB.collection("technologies") - - query = ref.order_by("technology", direction=firestore.Query.ASCENDING) - - if "technology" in params: - arfilters = [] - params_array = convert_to_array(params["technology"]) - for tech in params_array: - arfilters.append(FieldFilter("technology", "==", tech)) - query = query.where(filter=Or(filters=arfilters)) - - if "category" in params: - params_array = convert_to_array(params["category"]) - query = query.where( - filter=FieldFilter("category_obj", "array_contains_any", params_array) - ) - - documents = query.stream() - data = [] - - if "onlyname" in params: - appended_tech = set() - for doc in documents: - tech = doc.get("technology") - if tech not in appended_tech: - appended_tech.add(tech) - data.append(tech) - - else: - for doc in documents: - data.append(Presenters.technology(doc.to_dict())) - - return Result(result=data) diff --git a/functions/technologies/libs/result.py b/functions/technologies/libs/result.py deleted file mode 100644 index 63034b6..0000000 --- a/functions/technologies/libs/result.py +++ /dev/null @@ -1,20 +0,0 @@ - -class Result(): - def __init__(self, status=None, result=None, errors=[]): - self._status = status - self.result = result - self.errors = errors - - def success(self) -> bool: - return not self.failure() - - def failure(self) -> bool: - return len(self.errors) > 0 - - @property - def status(self): - if self._status != None: - return self._status - - return "ok" if self.success else "error" - \ No newline at end of file diff --git a/functions/technologies/libs/utils.py b/functions/technologies/libs/utils.py deleted file mode 100644 index 29692f4..0000000 --- a/functions/technologies/libs/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -from urllib.parse import unquote - -def convert_to_hashes(arr): - hashes_arr = [] - for inner_arr in arr: - hash_dict = {inner_arr[0]: inner_arr[1]} - hashes_arr.append(hash_dict) - return hashes_arr - -def convert_to_array(data_string): - decoded_data = unquote(data_string) - list = decoded_data.split(',') - return list \ No newline at end of file diff --git a/functions/technologies/libs/validator.py b/functions/technologies/libs/validator.py deleted file mode 100644 index 6a34617..0000000 --- a/functions/technologies/libs/validator.py +++ /dev/null @@ -1,21 +0,0 @@ -from .result import Result - -class Validator(): - def __init__(self, params): - self.params = params - self.errors = [] - self.normalizer_params = self.normalize(params) - - def validate(self): - result = Result(status="ok", result="()") - - # if 'technology' not in self.params: - # self.add_error("technology", "missing technology parameter") - - return Result(errors=self.errors, result=self.params) - - def add_error(self, key, error): - self.errors.append([key, error]) - - def normalize(self, params): - return "" diff --git a/functions/technologies/main.py b/functions/technologies/main.py deleted file mode 100644 index 3fab032..0000000 --- a/functions/technologies/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import functions_framework - -from .libs.validator import Validator -from .libs.queries import list_data -from .libs.network import respond_cors, respond - -@functions_framework.http -def dispatcher(request): - - if request.method == "OPTIONS": - return respond_cors() - - args = request.args.to_dict() - - validator = Validator(params=args) - result = validator.validate() - - if result.failure(): - print("error", result.errors) - return respond(result) - - response = list_data(result.result) - - return respond(response) \ No newline at end of file diff --git a/functions/technologies/requirements.txt b/functions/technologies/requirements.txt deleted file mode 100644 index f336d0d..0000000 --- a/functions/technologies/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -functions-framework -google-cloud-firestore -pytest \ No newline at end of file diff --git a/perf_lab_test/main.js b/perf_lab_test/main.js new file mode 100644 index 0000000..6495584 --- /dev/null +++ b/perf_lab_test/main.js @@ -0,0 +1,98 @@ +const axios = require('axios'); + +const ENDPOINTS = { + A: 'https://prod-gw-2vzgiib6.ue.gateway.dev/v1/technologies', + B: 'https://dev-gw-2vzgiib6.uc.gateway.dev/v1/technologies' +}; + +const NUM_REQUESTS = 200; +const CONCURRENCY = 10; +const MAX_JITTER_MS = 100; + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function makeRequest(name, url) { + const jitter = Math.floor(Math.random() * MAX_JITTER_MS); + await sleep(jitter); + + const fullUrl = `${url}?nocache=${Math.random().toString(36).substring(2)}`; + + const start = process.hrtime.bigint(); + try { + const res = await axios.get(fullUrl, { + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } + }); + const end = process.hrtime.bigint(); + if (res.status === 200) { + return { name, duration: Number(end - start) / 1e6 }; // ms + } else { + return { name, error: `Status: ${res.status}` }; + } + } catch (e) { + return { name, error: e.message }; + } +} + +async function benchmarkAlternating() { + const results = { + A: { durations: [], errors: [] }, + B: { durations: [], errors: [] } + }; + + const endpointNames = Object.keys(ENDPOINTS); + + for (let i = 0; i < NUM_REQUESTS; i += CONCURRENCY) { + const batch = []; + + for (let j = 0; j < CONCURRENCY; j++) { + const name = endpointNames[(i + j) % endpointNames.length]; + const url = ENDPOINTS[name]; + batch.push(makeRequest(name, url)); + } + + const responses = await Promise.all(batch); + + for (const res of responses) { + if (res.duration !== undefined) { + results[res.name].durations.push(res.duration); + } else { + results[res.name].errors.push(res.error); + } + } + } + + return results; +} + +function printStats(name, durations, errors) { + const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length; + const p90 = (arr) => { + const sorted = [...arr].sort((a, b) => a - b); + return sorted[Math.floor(sorted.length * 0.9)]; + }; + + console.log(`\n🔍 Results for ${name} (${ENDPOINTS[name]})`); + console.log(`✅ Successful responses: ${durations.length}`); + console.log(`❌ Errors: ${errors.length}`); + if (durations.length) { + console.log(`📈 Avg latency: ${avg(durations).toFixed(2)} ms`); + console.log(`🚀 Fastest: ${Math.min(...durations).toFixed(2)} ms`); + console.log(`🐢 Slowest: ${Math.max(...durations).toFixed(2)} ms`); + console.log(`📊 P90 latency: ${p90(durations).toFixed(2)} ms`); + } +} + +async function main() { + const results = await benchmarkAlternating(); + for (const name of Object.keys(ENDPOINTS)) { + const { durations, errors } = results[name]; + printStats(name, durations, errors); + } +} + +main(); diff --git a/perf_lab_test/package-lock.json b/perf_lab_test/package-lock.json new file mode 100644 index 0000000..ca5982f --- /dev/null +++ b/perf_lab_test/package-lock.json @@ -0,0 +1,294 @@ +{ + "name": "test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.9.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + } + } +} diff --git a/perf_lab_test/package.json b/perf_lab_test/package.json new file mode 100644 index 0000000..bed8ee8 --- /dev/null +++ b/perf_lab_test/package.json @@ -0,0 +1,14 @@ +{ + "name": "test", + "version": "1.0.0", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.9.0" + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index da587a0..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -functions-framework -google-cloud-firestore -google-cloud-error-reporting -mock-firestore -pytest \ No newline at end of file diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index 6d468db..896d6cb 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -1,8 +1,8 @@ const request = require('supertest'); -const app = require('../src/src/index'); +const { app } = require('../index'); // Mock Firestore -jest.mock('../src/utils/db', () => { +jest.mock('../utils/db', () => { return { collection: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), @@ -45,25 +45,23 @@ describe('API Routes', () => { }); }); - describe('GET /technologies', () => { + describe('GET /v1/technologies', () => { it('should return technologies', async () => { - const res = await request(app).get('/technologies'); + const res = await request(app).get('/v1/technologies'); expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('success', true); - expect(res.body).toHaveProperty('result'); - expect(Array.isArray(res.body.result)).toBe(true); + expect(Array.isArray(res.body)).toBe(true); }); it('should filter technologies by name', async () => { - const res = await request(app).get('/technologies?technology=Test'); + const res = await request(app).get('/v1/technologies?technology=Test'); expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('success', true); + expect(Array.isArray(res.body)).toBe(true); }); it('should return only names when onlyname parameter is provided', async () => { - const res = await request(app).get('/technologies?onlyname=true'); + const res = await request(app).get('/v1/technologies?onlyname=true'); expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('success', true); + expect(Array.isArray(res.body)).toBe(true); }); }); diff --git a/src/index.js b/src/index.js index b77e659..acb847a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,5 @@ const express = require('express'); const cors = require('cors'); -const { Firestore } = require('@google-cloud/firestore'); // Import routes const technologiesRoutes = require('./routes/technologies'); @@ -12,12 +11,6 @@ const pageWeightRoutes = require('./routes/pageWeight'); const ranksRoutes = require('./routes/ranks'); const geosRoutes = require('./routes/geos'); -// Initialize Firebase -const firestore = new Firestore({ - projectId: process.env.PROJECT, - databaseId: process.env.DATABASE -}); - // Create Express app const app = express(); diff --git a/src/package-lock.json b/src/package-lock.json index d357f56..a46154c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -15,7 +15,8 @@ "devDependencies": { "@google-cloud/functions-framework": "^4.0.0", "jest": "29.7.0", - "nodemon": "3.0.1" + "nodemon": "3.0.1", + "supertest": "^7.1.0" }, "engines": { "node": ">=22.0.0" @@ -1036,6 +1037,29 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1525,6 +1549,13 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2079,6 +2110,16 @@ "node": ">= 0.8" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2129,6 +2170,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2269,6 +2317,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2591,6 +2650,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -2708,6 +2774,24 @@ "node": ">= 0.12" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5766,6 +5850,95 @@ "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "license": "MIT" }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz", + "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/src/package.json b/src/package.json index 900423b..e2bb87a 100644 --- a/src/package.json +++ b/src/package.json @@ -18,15 +18,15 @@ "devDependencies": { "@google-cloud/functions-framework": "^4.0.0", "jest": "29.7.0", - "nodemon": "3.0.1" + "nodemon": "3.0.1", + "supertest": "^7.1.0" }, "jest": { - "preset": "jest", "testEnvironment": "node", "verbose": true, "collectCoverage": true, "collectCoverageFrom": [ - "src /**/*.js", + "**/*.js", "!index.js" ], "coverageDirectory": "coverage", @@ -35,7 +35,7 @@ "lcov" ], "testMatch": [ - "**/__tests__ /**/*.js", + "**/__tests__/**/*.js", "**/?(*.)+(spec|test).js" ] } diff --git a/tests/test_geos/libs/test_geos_result.py b/tests/test_geos/libs/test_geos_result.py deleted file mode 100644 index 5b306cb..0000000 --- a/tests/test_geos/libs/test_geos_result.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest -from functions.geos.libs.result import Result - -def test_success(): - r = Result(status="success") - assert r.success() == True - assert r.failure() == False - -def test_failure(): - r = Result(errors=["some error"]) - assert r.success() == False - assert r.failure() == True - -def test_default_status(): - r = Result() - assert r.status == "ok" - assert r.success() == True - assert r.failure() == False - -def test_custom_status(): - r = Result(status="custom") - assert r.status == "custom" - assert r.success() == True - assert r.failure() == False - -def test_result(): - r = Result(result="some result") - assert r.result == "some result" - -def test_errors(): - r = Result(errors=["some error"]) - assert r.errors == ["some error"] diff --git a/tests/test_geos/libs/test_geos_utils.py b/tests/test_geos/libs/test_geos_utils.py deleted file mode 100644 index e3b55d7..0000000 --- a/tests/test_geos/libs/test_geos_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -from functions.geos.libs.utils import * -from functions.geos.libs.result import Result -import json - -def test_convert_to_hashes(): - input_arr = [["geo", "missing geo parameters"], ["app", "missing geo parameters"]] - expected_output_arr = [{'geo': 'missing geo parameters'}, {'app': 'missing geo parameters'}] - assert convert_to_hashes(input_arr) == expected_output_arr diff --git a/tests/test_geos/test_geos_main.py b/tests/test_geos/test_geos_main.py deleted file mode 100644 index f346370..0000000 --- a/tests/test_geos/test_geos_main.py +++ /dev/null @@ -1,20 +0,0 @@ -import unittest -import json -from unittest.mock import Mock - -from functions.geos.main import dispatcher -from functions.geos.libs.utils import COUNTRIES - -class TestCloudFunction(unittest.TestCase): - - def test_success(self): - request = Mock() - response = dispatcher(request) - expected_data = json.dumps(COUNTRIES) - - self.assertEqual(response[1], 200) - self.assertEqual(response[0], expected_data) - -if __name__ == '__main__': - unittest.main() - diff --git a/tests/test_ranks/libs/test_ranks_result.py b/tests/test_ranks/libs/test_ranks_result.py deleted file mode 100644 index 7118f70..0000000 --- a/tests/test_ranks/libs/test_ranks_result.py +++ /dev/null @@ -1,31 +0,0 @@ -from functions.ranks.libs.result import Result - -def test_success(): - r = Result(status="success") - assert r.success() == True - assert r.failure() == False - -def test_failure(): - r = Result(errors=["some error"]) - assert r.success() == False - assert r.failure() == True - -def test_default_status(): - r = Result() - assert r.status == "ok" - assert r.success() == True - assert r.failure() == False - -def test_custom_status(): - r = Result(status="custom") - assert r.status == "custom" - assert r.success() == True - assert r.failure() == False - -def test_result(): - r = Result(result="some result") - assert r.result == "some result" - -def test_errors(): - r = Result(errors=["some error"]) - assert r.errors == ["some error"] diff --git a/tests/test_ranks/libs/test_ranks_utils.py b/tests/test_ranks/libs/test_ranks_utils.py deleted file mode 100644 index cfe142a..0000000 --- a/tests/test_ranks/libs/test_ranks_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -from functions.ranks.libs.utils import * -from functions.ranks.libs.result import Result -import json - -def test_convert_to_hashes(): - input_arr = [["geo", "missing geo parameters"], ["app", "missing geo parameters"]] - expected_output_arr = [{'geo': 'missing geo parameters'}, {'app': 'missing geo parameters'}] - assert convert_to_hashes(input_arr) == expected_output_arr diff --git a/tests/test_ranks/test_ranks_main.py b/tests/test_ranks/test_ranks_main.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_technologies/libs/test_technologies_result.py b/tests/test_technologies/libs/test_technologies_result.py deleted file mode 100644 index 29f9695..0000000 --- a/tests/test_technologies/libs/test_technologies_result.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest -from functions.technologies.libs.result import Result - -def test_success(): - r = Result(status="success") - assert r.success() == True - assert r.failure() == False - -def test_failure(): - r = Result(errors=["some error"]) - assert r.success() == False - assert r.failure() == True - -def test_default_status(): - r = Result() - assert r.status == "ok" - assert r.success() == True - assert r.failure() == False - -def test_custom_status(): - r = Result(status="custom") - assert r.status == "custom" - assert r.success() == True - assert r.failure() == False - -def test_result(): - r = Result(result="some result") - assert r.result == "some result" - -def test_errors(): - r = Result(errors=["some error"]) - assert r.errors == ["some error"] diff --git a/tests/test_technologies/libs/test_technologies_utils.py b/tests/test_technologies/libs/test_technologies_utils.py deleted file mode 100644 index 797ba38..0000000 --- a/tests/test_technologies/libs/test_technologies_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -from functions.technologies.libs.utils import * -from functions.technologies.libs.result import Result -import json - -def test_convert_to_hashes(): - input_arr = [["geo", "missing geo parameters"], ["app", "missing geo parameters"]] - expected_output_arr = [{'geo': 'missing geo parameters'}, {'app': 'missing geo parameters'}] - assert convert_to_hashes(input_arr) == expected_output_arr diff --git a/tests/test_technologies/test_technologies_main.py b/tests/test_technologies/test_technologies_main.py deleted file mode 100644 index e69de29..0000000 From 79eeeea19cb09f838286a16f9303723981485c48 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 11 May 2025 23:41:25 +0200 Subject: [PATCH 19/44] cd --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index cd08ea7..b91cb25 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -13,6 +13,7 @@ jobs: steps: - uses: actions/checkout@v3 - run: | + cd src npm ci npm run test From eb313cc5c03b23853f61b5c0310ecb834f802afc Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 25 May 2025 00:31:50 +0200 Subject: [PATCH 20/44] remove express js --- README.md | 277 ++++++++++++++-------- README_node.md | 141 ----------- src/controllers/adoptionController.js | 24 +- src/controllers/categoriesController.js | 6 +- src/controllers/cwvtechController.js | 32 ++- src/controllers/geosController.js | 6 +- src/controllers/lighthouseController.js | 32 ++- src/controllers/pageWeightController.js | 14 +- src/controllers/ranksController.js | 6 +- src/controllers/technologiesController.js | 6 +- src/index.js | 200 +++++++++++----- src/package-lock.json | 79 ++++-- src/package.json | 8 +- src/routes/adoption.js | 9 - src/routes/categories.js | 9 - src/routes/cwvtech.js | 9 - src/routes/geos.js | 9 - src/routes/lighthouse.js | 9 - src/routes/pageWeight.js | 9 - src/routes/ranks.js | 9 - src/routes/technologies.js | 9 - 21 files changed, 458 insertions(+), 445 deletions(-) delete mode 100644 README_node.md delete mode 100644 src/routes/adoption.js delete mode 100644 src/routes/categories.js delete mode 100644 src/routes/cwvtech.js delete mode 100644 src/routes/geos.js delete mode 100644 src/routes/lighthouse.js delete mode 100644 src/routes/pageWeight.js delete mode 100644 src/routes/ranks.js delete mode 100644 src/routes/technologies.js diff --git a/README.md b/README.md index c741a3a..c00e297 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,132 @@ -# tech-report-apis +# Technology Reports API (Node.js) -APIs for the HTTP Archive Technology Report +This is a unified Google Cloud Run function that provides technology metrics and information via various endpoints. -## API +## Setup -## Endpoints +### Prerequisites -### `GET /adoption` +- Node.js 18+ +- npm +- Google Cloud account with necessary permissions +- Set environment variables: + + ```bash + export PROJECT=httparchive + export DATABASE=tech-report-apis-prod + ``` + +### Local Development + + ```bash + npm install + npm start:functions + ``` + +The API will be available at + +### Google Cloud Functions Mode + + ```bash + npm install + npm run start:functions + ``` + +The function will run on `http://localhost:8080` + +## Deployment + +### Deploy to Google Cloud Run Function + +```bash +# Deploy to Google Cloud Functions +gcloud functions deploy tech-report-api \ + --runtime nodejs22 \ + --trigger-http \ + --allow-unauthenticated \ + --entry-point api \ + --source . +``` + +## API Endpoints + +## Features + +- **ETag Support**: All endpoints include ETag headers for efficient caching +- **CORS Enabled**: Cross-origin requests are supported +- **Cache Headers**: 6-hour cache control for static data +- **Health Check**: GET `/` returns health status +- **RESTful API**: All endpoints follow REST conventions + +### `GET /` + +Health check + +### `GET /technologies` + +Lists available technologies with optional filtering. #### Parameters -The following parameters can be used to filter the data: +- `technology` (optional): Filter by technology name(s) - comma-separated list +- `category` (optional): Filter by category - comma-separated list +- `onlyname` (optional): If present, returns only technology names + +#### Example Request & Response + +```bash +curl --request GET \ + --url 'https://{{HOST}}/v1/technologies?category=Live%20chat%2C%20blog&technology=Smartsupp' +``` -- `geo` (`required`): A string representing the geographic location. -- `technology` (`required`): A comma-separated string representing the technology name(s). -- `rank` (`required`): An string representing the rank. -- `start` (optional): A string representing the start date in the format `YYYY-MM-DD`. -- `end` (optional): A string representing the end date in the format `YYYY-MM-DD`. +Returns a JSON object with the following schema: -#### Response +```json +[ + { + "technology": "Smartsupp", + "category": "Live chat", + "description": "Smartsupp is a live chat tool that offers visitor recording feature.", + "icon": "Smartsupp.svg", + "origins": { + "mobile": 24115, + "desktop": 20250 + } + } +] +``` ```bash curl --request GET \ - --url 'https://{{HOST}}/v1/adoption?start=2023-01-01&end=2023-09-01&geo=Mexico&technology=GoCache&rank=ALL' + --url 'https://{{HOST}}/v1/technologies?onlyname' ``` Returns a JSON object with the following schema: ```json [ - { - "technology": "GoCache", - "geo": "Mexico", - "date": "2023-06-01", - "rank": "ALL", - "adoption": { - "mobile": 19, - "desktop": 11 - } - }, + "1C-Bitrix", + "2B Advice", + "33Across", + "34SP.com", + "4-Tell", + "42stores", + "51.LA", + "5centsCDN", ... -] +} ``` ### `GET /categories` -This endpoint can return a full list of categories names or a categories with all the associated technologies - -#### Parameters +Lists available categories. -The following parameters can be used to filter the data: +#### Categories Parameters -- `category` (optional): A comma-separated string representing the category name(s). -- `onlyname` (optional): No value required. If present, only the category names will be returned. +- `category` (optional): Filter by category name(s) - comma-separated list +- `onlyname` (optional): If present, returns only category names -#### Response +#### Categories Response ```bash curl --request GET \ @@ -105,22 +177,58 @@ curl --request GET \ "Analytics", ... ] +``` + +### `GET /adoption` + +Provides technology adoption data. + +#### Adoption Parameters +- `technology` (required): Filter by technology name(s) - comma-separated list +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) +- `geo` (optional): Filter by geographic location +- `rank` (optional): Filter by rank + +#### Adoption Response + +```bash +curl --request GET \ + --url 'https://{{HOST}}/v1/adoption?start=2023-01-01&end=2023-09-01&geo=Mexico&technology=GoCache&rank=ALL' ``` -### `GET /cwv` +Returns a JSON object with the following schema: -#### Parameters +```json +[ + { + "technology": "GoCache", + "geo": "Mexico", + "date": "2023-06-01", + "rank": "ALL", + "adoption": { + "mobile": 19, + "desktop": 11 + } + }, + ... +] +``` + +### `GET /cwv` (Core Web Vitals) + +Provides Core Web Vitals metrics for technologies. -The following parameters can be used to filter the data: +#### CWV Parameters -- `geo` (`required`): A string representing the geographic location. -- `technology` (`required`): A string representing the technology name. -- `rank` (`required`): An string representing the rank. -- `start` (optional): A string representing the start date in the format `YYYY-MM-DD`. -- `end` (optional): A string representing the end date in the format `YYYY-MM-DD`. +- `technology` (required): Filter by technology name(s) - comma-separated list +- `geo` (required): Filter by geographic location +- `rank` (required): Filter by rank +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) -#### Response +#### CWV Response ```bash curl --request GET \ @@ -151,22 +259,21 @@ curl --request GET \ ] } ] - ``` ### `GET /lighthouse` -#### Parameters +Provides Lighthouse scores for technologies. -The following parameters can be used to filter the data: +#### Lighthouse Parameters -- `technology` (`required`): A comma-separated string representing the technology name(s). -- `geo` (`required`): A string representing the geographic location. -- `rank` (`required`): An string representing the rank. -- `start` (optional): A string representing the start date in the format `YYYY-MM-DD`. -- `end` (optional): A string representing the end date in the format `YYYY-MM-DD`. +- `technology` (required): Filter by technology name(s) - comma-separated list +- `geo` (required): Filter by geographic location +- `rank` (required): Filter by rank +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) -#### Response +#### Lighthouse Response ```bash curl --request GET \ @@ -205,17 +312,17 @@ Returns a JSON object with the following schema: ### `GET /page-weight` -#### Parameters +Provides Page Weight metrics for technologies. -The following parameters can be used to filter the data: +#### Page Weight Parameters -- `geo` (`required`): A string representing the geographic location. -- `technology` (`required`): A comma-separated string representing the technology name(s). -- `rank` (`required`): An string representing the rank. -- `start` (optional): A string representing the start date in the format `YYYY-MM-DD`. -- `end` (optional): A string representing the end date in the format `YYYY-MM-DD`. +- `technology` (required): Filter by technology name(s) - comma-separated list +- `geo` (optional): Filter by geographic location +- `rank` (optional): Filter by rank +- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') +- `end` (optional): Filter by date range end (YYYY-MM-DD) -#### Response +#### Page Weight Response ```bash curl --request GET \ @@ -240,57 +347,41 @@ Returns a JSON object with the following schema: ] ``` -### `GET /technologies` +### `GET /ranks` -#### Parameters +Lists all available ranks. -The following parameters can be used to filter the data: +### `GET /geos` -- `technology` (optional): A comma-separated string representing the technology name(s) or `ALL`. -- `category` (optional): A comma-separated string representing the category name(s). -- `onlyname` (optional): No value required. If present, only the technology names will be returned. +Lists all available geographic locations. -#### Response +## Testing ```bash -curl --request GET \ - --url 'https://{{HOST}}/v1/technologies?category=Live%20chat%2C%20blog&technology=Smartsupp' +# Run all tests +npm test + +# Run tests with coverage +npm run test ``` -Returns a JSON object with the following schema: +## Response Format + +All API responses follow this format: ```json [ - { - "technology": "Smartsupp", - "category": "Live chat", - "description": "Smartsupp is a live chat tool that offers visitor recording feature.", - "icon": "Smartsupp.svg", - "origins": { - "mobile": 24115, - "desktop": 20250 - } - } + // Array of data objects ] ``` -```bash -curl --request GET \ - --url 'https://{{HOST}}/v1/technologies?onlyname' -``` - -Returns a JSON object with the following schema: +Or in case of an error: ```json -[ - "1C-Bitrix", - "2B Advice", - "33Across", - "34SP.com", - "4-Tell", - "42stores", - "51.LA", - "5centsCDN", - ... +{ + "success": false, + "errors": [ + {"key": "error message"} + ] } ``` diff --git a/README_node.md b/README_node.md deleted file mode 100644 index eebf3ab..0000000 --- a/README_node.md +++ /dev/null @@ -1,141 +0,0 @@ -# Technology Reports API (Node.js) - -This is a unified Google Cloud Run function that provides technology metrics and information via various endpoints. - -## Setup - -### Prerequisites - -- Node.js 18+ -- npm -- Google Cloud account with necessary permissions - -### Local Development - -1. Install dependencies: -```bash -npm install -``` - -2. Set environment variables: -```bash -export PROJECT=your-gcp-project-id -export DATABASE=your-firestore-database -``` - -3. Run the application locally: -```bash -npm start -``` - -The API will be available at http://localhost:8080 - -## Deployment - -### Using Google Cloud Build - -```bash -gcloud builds submit --tag gcr.io/PROJECT_ID/tech-report-api -gcloud run deploy tech-report-api --image gcr.io/PROJECT_ID/tech-report-api --platform managed -``` - -## API Endpoints - -### `GET /technologies` - -Lists available technologies with optional filtering. - -#### Parameters - -- `technology` (optional): Filter by technology name(s) - comma-separated list -- `category` (optional): Filter by category - comma-separated list -- `onlyname` (optional): If present, returns only technology names - -### `GET /categories` - -Lists available categories. - -#### Parameters - -- `category` (optional): Filter by category name(s) - comma-separated list -- `onlyname` (optional): If present, returns only category names - -### `GET /adoption` - -Provides technology adoption data. - -#### Parameters - -- `technology` (required): Filter by technology name(s) - comma-separated list -- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') -- `end` (optional): Filter by date range end (YYYY-MM-DD) -- `geo` (optional): Filter by geographic location -- `rank` (optional): Filter by rank - -### `GET /cwvtech` (Core Web Vitals) - -Provides Core Web Vitals metrics for technologies. - -#### Parameters - -- `technology` (required): Filter by technology name(s) - comma-separated list -- `geo` (required): Filter by geographic location -- `rank` (required): Filter by rank -- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') -- `end` (optional): Filter by date range end (YYYY-MM-DD) - -### `GET /lighthouse` - -Provides Lighthouse scores for technologies. - -#### Parameters - -- `technology` (required): Filter by technology name(s) - comma-separated list -- `geo` (required): Filter by geographic location -- `rank` (required): Filter by rank -- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') -- `end` (optional): Filter by date range end (YYYY-MM-DD) - -### `GET /page-weight` - -Provides Page Weight metrics for technologies. - -#### Parameters - -- `technology` (required): Filter by technology name(s) - comma-separated list -- `geo` (optional): Filter by geographic location -- `rank` (optional): Filter by rank -- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest') -- `end` (optional): Filter by date range end (YYYY-MM-DD) - -### `GET /ranks` - -Lists all available ranks. - -### `GET /geos` - -Lists all available geographic locations. - -## Response Format - -All API responses follow this format: - -```json -{ - "success": true, - "result": [ - // Array of data objects - ] -} -``` - -Or in case of an error: - -```json -{ - "success": false, - "errors": [ - {"key": "error message"} - ] -} -``` diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 41ccf97..025bddc 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -25,14 +25,16 @@ const listAdoptionData = async (req, res) => { // Technology is required if (!params.technology) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['technology', 'missing technology parameter'] - ])); + ]))); + return; } // Convert technology parameter to array const techArray = convertToArray(params.technology); - + // Handle 'latest' special value for start parameter if (params.start && params.start === 'latest') { params.start = await getLatestDate(); @@ -46,19 +48,19 @@ const listAdoptionData = async (req, res) => { if (params.start) { query = query.where('date', '>=', params.start); } - + if (params.end) { query = query.where('date', '<=', params.end); } - + if (params.geo) { query = query.where('geo', '==', params.geo); } - + if (params.rank) { query = query.where('rank', '==', params.rank); } - + // Always filter by technology query = query.where('technology', '==', technology); @@ -70,13 +72,15 @@ const listAdoptionData = async (req, res) => { } // Send response - res.status(200).send(createSuccessResponse(data)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching adoption data:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; module.exports = { - listAdoptionData + listAdoption: listAdoptionData }; diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index 3cecf68..c16ca5f 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -36,10 +36,12 @@ const listCategories = async (req, res) => { } // Send response - res.status(200).send(createSuccessResponse(data)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching categories:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index 7e0d431..ccbd185 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -25,26 +25,32 @@ const listCWVTechData = async (req, res) => { // Required parameters check if (!params.technology) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['technology', 'missing technology parameter'] - ])); + ]))); + return; } if (!params.geo) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['geo', 'missing geo parameter'] - ])); + ]))); + return; } if (!params.rank) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['rank', 'missing rank parameter'] - ])); + ]))); + return; } // Convert technology parameter to array const techArray = convertToArray(params.technology); - + // Handle 'latest' special value for start parameter if (params.start && params.start === 'latest') { params.start = await getLatestDate(); @@ -58,11 +64,11 @@ const listCWVTechData = async (req, res) => { if (params.start) { query = query.where('date', '>=', params.start); } - + if (params.end) { query = query.where('date', '<=', params.end); } - + // Always filter by required parameters query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); @@ -76,13 +82,15 @@ const listCWVTechData = async (req, res) => { } // Send response - res.status(200).send(createSuccessResponse(data)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching Core Web Vitals data:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; module.exports = { - listCWVTechData + listCwvtech: listCWVTechData }; diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index bf8d94e..269f2b9 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -227,10 +227,12 @@ const COUNTRIES = [ */ const listGeos = async (req, res) => { try { - res.status(200).send(createSuccessResponse(COUNTRIES)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(COUNTRIES))); } catch (error) { console.error('Error fetching geographic locations:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index 3450b90..3be21bc 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -25,26 +25,32 @@ const listLighthouseData = async (req, res) => { // Required parameters check if (!params.technology) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['technology', 'missing technology parameter'] - ])); + ]))); + return; } if (!params.geo) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['geo', 'missing geo parameter'] - ])); + ]))); + return; } if (!params.rank) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['rank', 'missing rank parameter'] - ])); + ]))); + return; } // Convert technology parameter to array const techArray = convertToArray(params.technology); - + // Handle 'latest' special value for start parameter if (params.start && params.start === 'latest') { params.start = await getLatestDate(); @@ -58,11 +64,11 @@ const listLighthouseData = async (req, res) => { if (params.start) { query = query.where('date', '>=', params.start); } - + if (params.end) { query = query.where('date', '<=', params.end); } - + // Always filter by required parameters query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); @@ -76,13 +82,15 @@ const listLighthouseData = async (req, res) => { } // Send response - res.status(200).send(createSuccessResponse(data)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching Lighthouse data:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; module.exports = { - listLighthouseData + listLighthouse: listLighthouseData }; diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index fa996de..251b5ad 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -25,9 +25,11 @@ const listPageWeightData = async (req, res) => { // Required parameters check if (!params.technology) { - return res.status(400).send(createErrorResponse([ + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([ ['technology', 'missing technology parameter'] - ])); + ]))); + return; } // Convert technology parameter to array @@ -70,13 +72,15 @@ const listPageWeightData = async (req, res) => { } // Send response - res.status(200).send(createSuccessResponse(data)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching Page Weight data:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; module.exports = { - listPageWeightData + listPageWeight: listPageWeightData }; diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index 564054f..1fdd48b 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -15,10 +15,12 @@ const RANKS = [ */ const listRanks = async (req, res) => { try { - res.status(200).send(createSuccessResponse(RANKS)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(RANKS))); } catch (error) { console.error('Error fetching ranks:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index da0185b..764f019 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -55,10 +55,12 @@ const listTechnologies = async (req, res) => { }; // Send response - res.status(200).send(createSuccessResponse(data)); + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching technologies:', error); - res.status(400).send(createErrorResponse([['query', error.message]])); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); } }; diff --git a/src/index.js b/src/index.js index acb847a..05f530b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,64 +1,138 @@ -const express = require('express'); -const cors = require('cors'); - -// Import routes -const technologiesRoutes = require('./routes/technologies'); -const categoriesRoutes = require('./routes/categories'); -const adoptionRoutes = require('./routes/adoption'); -const cwvtechRoutes = require('./routes/cwvtech'); -const lighthouseRoutes = require('./routes/lighthouse'); -const pageWeightRoutes = require('./routes/pageWeight'); -const ranksRoutes = require('./routes/ranks'); -const geosRoutes = require('./routes/geos'); - -// Create Express app -const app = express(); - -// Middleware -app.use(cors({ - origin: '*', - methods: 'GET, OPTIONS', - allowedHeaders: 'Content-Type, Timing-Allow-Origin', - maxAge: 86400 -})); - -// Explicitly handle OPTIONS requests for CORS preflight -app.options('*', cors()); - -app.use(express.json()); - -// Set common response headers -app.use((req, res, next) => { - res.set({ - 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=21600', - 'Timing-Allow-Origin': '*' +const http = require('http'); +const url = require('url'); +const crypto = require('crypto'); + +// Import controllers +const { listTechnologies } = require('./controllers/technologiesController'); +const { listCategories } = require('./controllers/categoriesController'); +const { listAdoption } = require('./controllers/adoptionController'); +const { listCwvtech } = require('./controllers/cwvtechController'); +const { listLighthouse } = require('./controllers/lighthouseController'); +const { listPageWeight } = require('./controllers/pageWeightController'); +const { listRanks } = require('./controllers/ranksController'); +const { listGeos } = require('./controllers/geosController'); + +// Helper function to set CORS headers +const setCORSHeaders = (res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Timing-Allow-Origin'); + res.setHeader('Access-Control-Max-Age', '86400'); +}; + +// Helper function to set common response headers +const setCommonHeaders = (res) => { + setCORSHeaders(res); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'public, max-age=21600'); + res.setHeader('Timing-Allow-Origin', '*'); +}; + +// Helper function to generate ETag +const generateETag = (data) => { + return crypto.createHash('md5').update(JSON.stringify(data)).digest('hex'); +}; + +// Helper function to send JSON response with ETag support +const sendJSONResponse = (res, data, statusCode = 200) => { + const jsonData = JSON.stringify(data); + const etag = generateETag(data); + + res.setHeader('ETag', `"${etag}"`); + res.statusCode = statusCode; + res.end(jsonData); +}; + +// Helper function to check if resource is modified +const isModified = (req, etag) => { + const ifNoneMatch = req.headers['if-none-match']; + return !ifNoneMatch || ifNoneMatch !== `"${etag}"`; +}; + +// Helper function to parse query parameters +const parseQuery = (queryString) => { + const params = new URLSearchParams(queryString); + const result = {}; + for (const [key, value] of params) { + result[key] = value; + } + return result; +}; + +// Route handler function +const handleRequest = async (req, res) => { + try { + setCommonHeaders(res); + + // Handle OPTIONS requests for CORS preflight + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); + return; + } + + // Parse URL + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; + const query = parsedUrl.query; + + // Add query to req object for compatibility with existing controllers + req.query = query; + + // Route handling + if (pathname === '/' && req.method === 'GET') { + // Health check endpoint + const data = { status: 'ok' }; + sendJSONResponse(res, data); + } else if (pathname === '/v1/technologies' && req.method === 'GET') { + await listTechnologies(req, res); + } else if (pathname === '/v1/categories' && req.method === 'GET') { + await listCategories(req, res); + } else if (pathname === '/v1/adoption' && req.method === 'GET') { + await listAdoption(req, res); + } else if (pathname === '/v1/cwv' && req.method === 'GET') { + await listCwvtech(req, res); + } else if (pathname === '/v1/lighthouse' && req.method === 'GET') { + await listLighthouse(req, res); + } else if (pathname === '/v1/page-weight' && req.method === 'GET') { + await listPageWeight(req, res); + } else if (pathname === '/v1/ranks' && req.method === 'GET') { + await listRanks(req, res); + } else if (pathname === '/v1/geos' && req.method === 'GET') { + await listGeos(req, res); + } else { + // 404 Not Found + res.statusCode = 404; + res.end(JSON.stringify({ error: 'Not Found' })); + } + } catch (error) { + console.error('Error:', error); + res.statusCode = 400; + res.end(JSON.stringify({ + errors: [{ error: error.message || 'Unknown error occurred' }] + })); + } +}; + +// Create HTTP server +const server = http.createServer(handleRequest); + +// Export the server and handleRequest for testing and cloud functions +exports.app = server; +exports.handleRequest = handleRequest; + +// Start server in development mode (not when imported as module) +if (require.main === module) { + const PORT = process.env.PORT || 3000; + server.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); }); - next(); -}); - -// Define routes -app.use('/v1/technologies', technologiesRoutes); -app.use('/v1/categories', categoriesRoutes); -app.use('/v1/adoption', adoptionRoutes); -app.use('/v1/cwv', cwvtechRoutes); -app.use('/v1/lighthouse', lighthouseRoutes); -app.use('/v1/page-weight', pageWeightRoutes); -app.use('/v1/ranks', ranksRoutes); -app.use('/v1/geos', geosRoutes); - -// Health check endpoint -app.get('/', (req, res) => { - res.status(200).send(JSON.stringify({ status: 'ok' })); -}); - -// Error handling middleware -app.use((err, req, res, next) => { - console.error('Error:', err); - res.status(400).send(JSON.stringify({ - errors: [{ error: err.message || 'Unknown error occurred' }] - })); -}); - -// Export the app for Cloud Functions Framework -exports.app = app; +} + +// Functions Framework wrapper for Google Cloud Functions +try { + const functions = require('@google-cloud/functions-framework'); + functions.http('api', handleRequest); +} catch (error) { + // Functions Framework not available in development mode +} diff --git a/src/package-lock.json b/src/package-lock.json index a46154c..070b4e1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8,9 +8,7 @@ "name": "tech-report-api", "version": "1.0.0", "dependencies": { - "@google-cloud/firestore": "7.3.0", - "cors": "2.8.5", - "express": "4.21.2" + "@google-cloud/firestore": "7.3.0" }, "devDependencies": { "@google-cloud/functions-framework": "^4.0.0", @@ -1426,6 +1424,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -1547,6 +1546,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, "license": "MIT" }, "node_modules/asap": { @@ -1747,6 +1747,7 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -1851,6 +1852,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -1892,6 +1894,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2131,6 +2134,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -2143,6 +2147,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2159,6 +2164,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2168,6 +2174,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, "license": "MIT" }, "node_modules/cookiejar": { @@ -2177,19 +2184,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2231,6 +2225,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -2292,6 +2287,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2301,6 +2297,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -2377,6 +2374,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -2409,6 +2407,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2491,6 +2490,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -2521,6 +2521,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2589,6 +2590,7 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -2701,6 +2703,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -2796,6 +2799,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2805,6 +2809,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3175,6 +3180,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -3286,6 +3292,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -3366,6 +3373,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -4472,6 +4480,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4481,6 +4490,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4497,6 +4507,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4520,6 +4531,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -4586,6 +4598,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -4599,6 +4612,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4771,15 +4785,6 @@ "node": ">=8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "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", @@ -4793,6 +4798,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4805,6 +4811,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -4916,6 +4923,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -4962,6 +4970,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -5109,6 +5118,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -5146,6 +5156,7 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -5161,6 +5172,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5170,6 +5182,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -5426,6 +5439,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -5442,6 +5456,7 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -5466,6 +5481,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5475,12 +5491,14 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -5514,6 +5532,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shebang-command": { @@ -5543,6 +5562,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5562,6 +5582,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5578,6 +5599,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5596,6 +5618,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5742,6 +5765,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6068,6 +6092,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -6116,6 +6141,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -6155,6 +6181,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6215,6 +6242,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -6263,6 +6291,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" diff --git a/src/package.json b/src/package.json index e2bb87a..40877e7 100644 --- a/src/package.json +++ b/src/package.json @@ -7,13 +7,13 @@ "node": ">=22.0.0" }, "scripts": { - "start": "functions-framework --target=app", + "start": "node index.js", + "start:functions": "functions-framework --target=api", + "dev": "nodemon index.js", "test": "jest" }, "dependencies": { - "@google-cloud/firestore": "7.3.0", - "cors": "2.8.5", - "express": "4.21.2" + "@google-cloud/firestore": "7.3.0" }, "devDependencies": { "@google-cloud/functions-framework": "^4.0.0", diff --git a/src/routes/adoption.js b/src/routes/adoption.js deleted file mode 100644 index b3fad1e..0000000 --- a/src/routes/adoption.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listAdoptionData } = require('../controllers/adoptionController'); - -const router = express.Router(); - -// GET /adoption endpoint -router.get('/', listAdoptionData); - -module.exports = router; diff --git a/src/routes/categories.js b/src/routes/categories.js deleted file mode 100644 index fbc0b2e..0000000 --- a/src/routes/categories.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listCategories } = require('../controllers/categoriesController'); - -const router = express.Router(); - -// GET /categories endpoint -router.get('/', listCategories); - -module.exports = router; diff --git a/src/routes/cwvtech.js b/src/routes/cwvtech.js deleted file mode 100644 index 03c806f..0000000 --- a/src/routes/cwvtech.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listCWVTechData } = require('../controllers/cwvtechController'); - -const router = express.Router(); - -// GET /cwvtech endpoint -router.get('/', listCWVTechData); - -module.exports = router; diff --git a/src/routes/geos.js b/src/routes/geos.js deleted file mode 100644 index a8cc7e5..0000000 --- a/src/routes/geos.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listGeos } = require('../controllers/geosController'); - -const router = express.Router(); - -// GET /geos endpoint -router.get('/', listGeos); - -module.exports = router; diff --git a/src/routes/lighthouse.js b/src/routes/lighthouse.js deleted file mode 100644 index bd2293e..0000000 --- a/src/routes/lighthouse.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listLighthouseData } = require('../controllers/lighthouseController'); - -const router = express.Router(); - -// GET /lighthouse endpoint -router.get('/', listLighthouseData); - -module.exports = router; diff --git a/src/routes/pageWeight.js b/src/routes/pageWeight.js deleted file mode 100644 index 486411a..0000000 --- a/src/routes/pageWeight.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listPageWeightData } = require('../controllers/pageWeightController'); - -const router = express.Router(); - -// GET /page-weight endpoint -router.get('/', listPageWeightData); - -module.exports = router; diff --git a/src/routes/ranks.js b/src/routes/ranks.js deleted file mode 100644 index 0a8ffe5..0000000 --- a/src/routes/ranks.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listRanks } = require('../controllers/ranksController'); - -const router = express.Router(); - -// GET /ranks endpoint -router.get('/', listRanks); - -module.exports = router; diff --git a/src/routes/technologies.js b/src/routes/technologies.js deleted file mode 100644 index 8527cd6..0000000 --- a/src/routes/technologies.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const { listTechnologies } = require('../controllers/technologiesController'); - -const router = express.Router(); - -// GET /technologies endpoint -router.get('/', listTechnologies); - -module.exports = router; From 7dd31b5631b98f352437c8500581d57e9f03d567 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 25 May 2025 00:50:58 +0200 Subject: [PATCH 21/44] update port listener --- src/Dockerfile | 22 ---------------------- src/index.js | 17 ++++++----------- src/package.json | 6 +++--- 3 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 src/Dockerfile diff --git a/src/Dockerfile b/src/Dockerfile deleted file mode 100644 index a4ec023..0000000 --- a/src/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:24-slim - -WORKDIR /app - -# Copy package.json and package-lock.json -COPY package*.json ./ - -# Install dependencies -RUN npm ci --only=production - -# Copy the rest of the application -COPY . . - -# Set environment variables -ENV PROJECT=httparchive -ENV DATABASE=tech-report-apis-prod - -# Expose the port the app runs on -EXPOSE 8080 - -# Start the application -CMD [ "npm", "start" ] diff --git a/src/index.js b/src/index.js index 05f530b..68c1fd5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ const http = require('http'); const url = require('url'); const crypto = require('crypto'); +const functions = require('@google-cloud/functions-framework'); // Import controllers const { listTechnologies } = require('./controllers/technologiesController'); @@ -117,22 +118,16 @@ const handleRequest = async (req, res) => { // Create HTTP server const server = http.createServer(handleRequest); -// Export the server and handleRequest for testing and cloud functions +// Export the server for testing exports.app = server; -exports.handleRequest = handleRequest; -// Start server in development mode (not when imported as module) +// Register with Functions Framework for Cloud Functions +functions.http('app', handleRequest); + +// For standalone server mode (local development) if (require.main === module) { const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); } - -// Functions Framework wrapper for Google Cloud Functions -try { - const functions = require('@google-cloud/functions-framework'); - functions.http('api', handleRequest); -} catch (error) { - // Functions Framework not available in development mode -} diff --git a/src/package.json b/src/package.json index 40877e7..f6f0570 100644 --- a/src/package.json +++ b/src/package.json @@ -8,15 +8,15 @@ }, "scripts": { "start": "node index.js", - "start:functions": "functions-framework --target=api", + "start:functions": "functions-framework --target=app", "dev": "nodemon index.js", "test": "jest" }, "dependencies": { - "@google-cloud/firestore": "7.3.0" + "@google-cloud/firestore": "7.3.0", + "@google-cloud/functions-framework": "^4.0.0" }, "devDependencies": { - "@google-cloud/functions-framework": "^4.0.0", "jest": "29.7.0", "nodemon": "3.0.1", "supertest": "^7.1.0" From addc1f421143d5514e3c0530549e79867e5f37c6 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 25 May 2025 17:46:55 +0200 Subject: [PATCH 22/44] switch to us-central db --- README.md | 2 +- terraform/dev/variables.tf | 4 ++-- terraform/modules/cloud-function/main.tf | 5 ++--- terraform/modules/run-service/main.tf | 5 ++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c00e297..24bb67d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This is a unified Google Cloud Run function that provides technology metrics and ```bash export PROJECT=httparchive - export DATABASE=tech-report-apis-prod + export DATABASE=tech-report-api-prod ``` ### Local Development diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 5d8c530..77e6d54 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -15,7 +15,7 @@ variable "environment" { variable "project_database" { type = string description = "The database name" - default = "tech-report-apis-prod" // TODO: Update this to the correct database name + default = "tech-report-api-prod" // TODO: Update this to the DEV database name } variable "google_service_account_cloud_functions" { @@ -30,5 +30,5 @@ variable "google_service_account_api_gateway" { variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number - default = 1 # TODO: Update this to 0 after performance testing + default = 0 } diff --git a/terraform/modules/cloud-function/main.tf b/terraform/modules/cloud-function/main.tf index f34e080..6f876d0 100644 --- a/terraform/modules/cloud-function/main.tf +++ b/terraform/modules/cloud-function/main.tf @@ -72,17 +72,16 @@ resource "google_cloud_run_v2_service_iam_member" "variable_service_account_run_ role = "roles/run.invoker" member = "serviceAccount:${var.service_account_email}" } -// TODO: Conditionally apply if the function needs to be invoked by API Gateway + resource "google_cloudfunctions2_function_iam_member" "api_gw_variable_service_account_function_invoker" { project = google_cloudfunctions2_function.function.project location = google_cloudfunctions2_function.function.location cloud_function = google_cloudfunctions2_function.function.name role = "roles/cloudfunctions.invoker" - #member = "serviceAccount:api-gateway@httparchive.iam.gserviceaccount.com" member = "serviceAccount:${var.service_account_api_gateway}" depends_on = [google_cloudfunctions2_function.function] } -// TODO: Conditionally apply if the function needs to be invoked by API Gateway + resource "google_cloud_run_v2_service_iam_member" "api_gw_variable_service_account_run_invoker" { project = var.project location = var.region diff --git a/terraform/modules/run-service/main.tf b/terraform/modules/run-service/main.tf index 2120114..e2c6760 100644 --- a/terraform/modules/run-service/main.tf +++ b/terraform/modules/run-service/main.tf @@ -73,17 +73,16 @@ resource "google_cloud_run_v2_service_iam_member" "variable_service_account_run_ role = "roles/run.invoker" member = "serviceAccount:${var.service_account_email}" } -// TODO: Conditionally apply if the function needs to be invoked by API Gateway + resource "google_cloudfunctions2_function_iam_member" "api_gw_variable_service_account_function_invoker" { project = google_cloudfunctions2_function.function.project location = google_cloudfunctions2_function.function.location cloud_function = google_cloudfunctions2_function.function.name role = "roles/cloudfunctions.invoker" - #member = "serviceAccount:api-gateway@httparchive.iam.gserviceaccount.com" member = "serviceAccount:${var.service_account_api_gateway}" depends_on = [google_cloudfunctions2_function.function] } -// TODO: Conditionally apply if the function needs to be invoked by API Gateway + resource "google_cloud_run_v2_service_iam_member" "api_gw_variable_service_account_run_invoker" { project = var.project location = var.region From 21d577a3127aa11b835adb14d943d44ca5a2b755 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 25 May 2025 19:29:47 +0200 Subject: [PATCH 23/44] dynamic geos and ranks --- src/controllers/geosController.js | 236 ++--------------------------- src/controllers/ranksController.js | 24 +-- 2 files changed, 24 insertions(+), 236 deletions(-) diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index 269f2b9..20c7da7 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -1,234 +1,22 @@ +const firestore = require('../utils/db'); const { createSuccessResponse, createErrorResponse } = require('../utils/helpers'); -// GEOS/COUNTRIES data from the existing Python implementation -// Note: Full list is shortened for brevity. In a real implementation, the complete list would be included. -const COUNTRIES = [ - {"geo": "ALL", "num_origins": "9731427"}, - {"geo": "United States of America", "num_origins": "1707677"}, - {"geo": "India", "num_origins": "826143"}, - {"geo": "Japan", "num_origins": "690984"}, - {"geo": "Germany", "num_origins": "678201"}, - {"geo": "Brazil", "num_origins": "644760"}, - { - "geo": "United Kingdom of Great Britain and Northern Ireland", - "num_origins": "560753", - }, - {"geo": "Russian Federation", "num_origins": "529803"}, - {"geo": "France", "num_origins": "515925"}, - {"geo": "Italy", "num_origins": "503015"}, - {"geo": "Spain", "num_origins": "459739"}, - {"geo": "Indonesia", "num_origins": "401253"}, - {"geo": "Poland", "num_origins": "350837"}, - {"geo": "Canada", "num_origins": "335548"}, - {"geo": "Mexico", "num_origins": "317337"}, - {"geo": "Turkey", "num_origins": "292310"}, - {"geo": "Netherlands", "num_origins": "291785"}, - {"geo": "Argentina", "num_origins": "252487"}, - {"geo": "Australia", "num_origins": "215909"}, - {"geo": "Korea, Republic of", "num_origins": "209013"}, - {"geo": "Philippines", "num_origins": "204637"}, - {"geo": "Colombia", "num_origins": "198020"}, - {"geo": "Malaysia", "num_origins": "193444"}, - {"geo": "Ukraine", "num_origins": "189866"}, - {"geo": "Viet Nam", "num_origins": "176473"}, - {"geo": "Thailand", "num_origins": "167337"}, - {"geo": "Pakistan", "num_origins": "157400"}, - {"geo": "Belgium", "num_origins": "157266"}, - {"geo": "South Africa", "num_origins": "150004"}, - {"geo": "Czechia", "num_origins": "148638"}, - {"geo": "Romania", "num_origins": "148176"}, - {"geo": "Taiwan, Province of China", "num_origins": "147383"}, - {"geo": "Chile", "num_origins": "144592"}, - {"geo": "Greece", "num_origins": "135996"}, - {"geo": "Austria", "num_origins": "135821"}, - {"geo": "Bangladesh", "num_origins": "134081"}, - {"geo": "Peru", "num_origins": "124954"}, - {"geo": "Iran (Islamic Republic of)", "num_origins": "122949"}, - {"geo": "Singapore", "num_origins": "121397"}, - {"geo": "Egypt", "num_origins": "119105"}, - {"geo": "Hungary", "num_origins": "117857"}, - {"geo": "Nigeria", "num_origins": "115407"}, - {"geo": "Portugal", "num_origins": "113035"}, - {"geo": "Kazakhstan", "num_origins": "111471"}, - {"geo": "Belarus", "num_origins": "109161"}, - {"geo": "Sweden", "num_origins": "108230"}, - {"geo": "Switzerland", "num_origins": "106121"}, - {"geo": "Saudi Arabia", "num_origins": "100966"}, - {"geo": "Israel", "num_origins": "99539"}, - {"geo": "Algeria", "num_origins": "98160"}, - {"geo": "Morocco", "num_origins": "96973"}, - {"geo": "Ireland", "num_origins": "96613"}, - {"geo": "Hong Kong", "num_origins": "95717"}, - {"geo": "United Arab Emirates", "num_origins": "91116"}, - {"geo": "Croatia", "num_origins": "85514"}, - {"geo": "Venezuela (Bolivarian Republic of)", "num_origins": "84283"}, - {"geo": "Slovakia", "num_origins": "84177"}, - {"geo": "Finland", "num_origins": "83107"}, - {"geo": "Serbia", "num_origins": "80789"}, - {"geo": "Ecuador", "num_origins": "80083"}, - {"geo": "Bulgaria", "num_origins": "75818"}, - {"geo": "Denmark", "num_origins": "69550"}, - {"geo": "New Zealand", "num_origins": "68444"}, - {"geo": "Uzbekistan", "num_origins": "65735"}, - {"geo": "Iraq", "num_origins": "65305"}, - {"geo": "Kenya", "num_origins": "62330"}, - {"geo": "Nepal", "num_origins": "60371"}, - {"geo": "Norway", "num_origins": "58300"}, - {"geo": "China", "num_origins": "57495"}, - {"geo": "Bolivia (Plurinational State of)", "num_origins": "55245"}, - {"geo": "Tunisia", "num_origins": "54813"}, - {"geo": "Sri Lanka", "num_origins": "53879"}, - {"geo": "Guatemala", "num_origins": "50897"}, - {"geo": "Azerbaijan", "num_origins": "46317"}, - {"geo": "Kyrgyzstan", "num_origins": "45478"}, - {"geo": "Lithuania", "num_origins": "45215"}, - {"geo": "Costa Rica", "num_origins": "44736"}, - {"geo": "Dominican Republic", "num_origins": "42618"}, - {"geo": "Moldova, Republic of", "num_origins": "41976"}, - {"geo": "Bosnia and Herzegovina", "num_origins": "41953"}, - {"geo": "Jordan", "num_origins": "41773"}, - {"geo": "Uruguay", "num_origins": "41139"}, - {"geo": "Panama", "num_origins": "38437"}, - {"geo": "Slovenia", "num_origins": "36027"}, - {"geo": "Ghana", "num_origins": "35980"}, - {"geo": "Paraguay", "num_origins": "35415"}, - {"geo": "Georgia", "num_origins": "34921"}, - {"geo": "Qatar", "num_origins": "34403"}, - {"geo": "Lebanon", "num_origins": "33694"}, - {"geo": "Puerto Rico", "num_origins": "33617"}, - {"geo": "El Salvador", "num_origins": "31654"}, - {"geo": "Syrian Arab Republic", "num_origins": "30714"}, - {"geo": "Latvia", "num_origins": "30530"}, - {"geo": "Honduras", "num_origins": "29712"}, - {"geo": "Myanmar", "num_origins": "29348"}, - {"geo": "Cyprus", "num_origins": "29012"}, - {"geo": "Oman", "num_origins": "27345"}, - {"geo": "Tanzania, United Republic of", "num_origins": "27335"}, - {"geo": "Cameroon", "num_origins": "26828"}, - {"geo": "Kuwait", "num_origins": "26458"}, - {"geo": "Armenia", "num_origins": "26355"}, - {"geo": "Nicaragua", "num_origins": "26015"}, - {"geo": "Estonia", "num_origins": "25576"}, - {"geo": "Côte d'Ivoire", "num_origins": "25208"}, - {"geo": "Cambodia", "num_origins": "24593"}, - {"geo": "Uganda", "num_origins": "24532"}, - {"geo": "Libya", "num_origins": "23730"}, - {"geo": "Cuba", "num_origins": "23056"}, - {"geo": "Ethiopia", "num_origins": "22650"}, - {"geo": "Albania", "num_origins": "22445"}, - {"geo": "Yemen", "num_origins": "22186"}, - {"geo": "North Macedonia", "num_origins": "21259"}, - {"geo": "Palestine, State of", "num_origins": "20468"}, - {"geo": "Senegal", "num_origins": "20323"}, - {"geo": "Montenegro", "num_origins": "20212"}, - {"geo": "Sudan", "num_origins": "20152"}, - {"geo": "Jamaica", "num_origins": "18847"}, - {"geo": "Iceland", "num_origins": "18261"}, - {"geo": "Zambia", "num_origins": "17567"}, - {"geo": "Bahrain", "num_origins": "17522"}, - {"geo": "Réunion", "num_origins": "17251"}, - {"geo": "Trinidad and Tobago", "num_origins": "16445"}, - {"geo": "Mauritius", "num_origins": "16238"}, - {"geo": "Zimbabwe", "num_origins": "15515"}, - {"geo": "Tajikistan", "num_origins": "14835"}, - {"geo": "Lao People's Democratic Republic", "num_origins": "14796"}, - {"geo": "Luxembourg", "num_origins": "14647"}, - {"geo": "Congo, Democratic Republic of the", "num_origins": "14545"}, - {"geo": "Angola", "num_origins": "13428"}, - {"geo": "Haiti", "num_origins": "13083"}, - {"geo": "Malta", "num_origins": "12984"}, - {"geo": "Mozambique", "num_origins": "12706"}, - {"geo": "Mongolia", "num_origins": "12574"}, - {"geo": "Burkina Faso", "num_origins": "12325"}, - {"geo": "Benin", "num_origins": "12292"}, - {"geo": "Somalia", "num_origins": "12176"}, - {"geo": "Mali", "num_origins": "10834"}, - {"geo": "Turkmenistan", "num_origins": "10192"}, - {"geo": "Afghanistan", "num_origins": "9613"}, - {"geo": "Martinique", "num_origins": "9314"}, - {"geo": "Guadeloupe", "num_origins": "8961"}, - {"geo": "Brunei Darussalam", "num_origins": "8854"}, - {"geo": "Botswana", "num_origins": "8657"}, - {"geo": "Namibia", "num_origins": "8535"}, - {"geo": "Papua New Guinea", "num_origins": "8447"}, - {"geo": "Togo", "num_origins": "8308"}, - {"geo": "Malawi", "num_origins": "8305"}, - {"geo": "Maldives", "num_origins": "8262"}, - {"geo": "Kosovo", "num_origins": "7807"}, - {"geo": "Gabon", "num_origins": "7754"}, - {"geo": "Bhutan", "num_origins": "6919"}, - {"geo": "Guinea", "num_origins": "6702"}, - {"geo": "Madagascar", "num_origins": "6620"}, - {"geo": "Guyana", "num_origins": "6303"}, - {"geo": "Rwanda", "num_origins": "6129"}, - {"geo": "Mauritania", "num_origins": "5995"}, - {"geo": "Macao", "num_origins": "5889"}, - {"geo": "Suriname", "num_origins": "5827"}, - {"geo": "Niger", "num_origins": "5484"}, - {"geo": "Fiji", "num_origins": "5388"}, - {"geo": "Congo", "num_origins": "4697"}, - {"geo": "Barbados", "num_origins": "4509"}, - {"geo": "Bahamas", "num_origins": "4467"}, - {"geo": "Chad", "num_origins": "4426"}, - {"geo": "Sierra Leone", "num_origins": "4345"}, - {"geo": "Cabo Verde", "num_origins": "4125"}, - {"geo": "Liberia", "num_origins": "3899"}, - {"geo": "Belize", "num_origins": "3871"}, - {"geo": "French Guiana", "num_origins": "3603"}, - {"geo": "Eswatini", "num_origins": "3554"}, - {"geo": "French Polynesia", "num_origins": "3489"}, - {"geo": "New Caledonia", "num_origins": "3379"}, - {"geo": "Lesotho", "num_origins": "3265"}, - {"geo": "Gambia", "num_origins": "3217"}, - {"geo": "Timor-Leste", "num_origins": "3074"}, - {"geo": "Andorra", "num_origins": "3073"}, - {"geo": "South Sudan", "num_origins": "3040"}, - {"geo": "Curaçao", "num_origins": "2987"}, - {"geo": "Western Sahara", "num_origins": "2739"}, - {"geo": "Saint Lucia", "num_origins": "2493"}, - {"geo": "Guam", "num_origins": "2466"}, - {"geo": "Antigua and Barbuda", "num_origins": "2449"}, - {"geo": "Aruba", "num_origins": "2420"}, - {"geo": "Djibouti", "num_origins": "2395"}, - {"geo": "Burundi", "num_origins": "2301"}, - {"geo": "Seychelles", "num_origins": "2007"}, - {"geo": "Mayotte", "num_origins": "1820"}, - {"geo": "Grenada", "num_origins": "1597"}, - {"geo": "Guinea-Bissau", "num_origins": "1592"}, - {"geo": "Comoros", "num_origins": "1563"}, - {"geo": "Cayman Islands", "num_origins": "1549"}, - {"geo": "Jersey", "num_origins": "1499"}, - {"geo": "Saint Vincent and the Grenadines", "num_origins": "1453"}, - {"geo": "Isle of Man", "num_origins": "1374"}, - {"geo": "Faroe Islands", "num_origins": "1233"}, - {"geo": "Equatorial Guinea", "num_origins": "1218"}, - {"geo": "Virgin Islands (U.S.)", "num_origins": "1074"}, - {"geo": "Dominica", "num_origins": "1049"}, - {"geo": "Sint Maarten (Dutch part)", "num_origins": "952"}, - {"geo": "Solomon Islands", "num_origins": "946"}, - {"geo": "Guernsey", "num_origins": "936"}, - {"geo": "Saint Kitts and Nevis", "num_origins": "917"}, - {"geo": "Central African Republic", "num_origins": "879"}, - {"geo": "Virgin Islands (British)", "num_origins": "864"}, - {"geo": "San Marino", "num_origins": "845"}, - {"geo": "Bermuda", "num_origins": "796"}, - {"geo": "Samoa", "num_origins": "771"}, - {"geo": "Gibraltar", "num_origins": "710"}, - {"geo": "Vanuatu", "num_origins": "697"}, - {"geo": "Saint Martin (French part)", "num_origins": "642"}, - {"geo": "Greenland", "num_origins": "631"}, - {"geo": "Bonaire, Sint Eustatius and Saba", "num_origins": "615"}, - {"geo": "Marshall Islands", "num_origins": "604"}, - {"geo": "Turks and Caicos Islands", "num_origins": "548"}, -]; - /** - * List all geographic locations + * List all geographic locations from database */ const listGeos = async (req, res) => { try { + const snapshot = await firestore.collection('geos').orderBy('mobile_origins', 'desc').get(); + const data = []; + + // Extract only the 'geo' property from each document + snapshot.forEach(doc => { + const docData = doc.data(); + data.push({ geo: docData.geo }); + }); + res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(COUNTRIES))); + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching geographic locations:', error); res.statusCode = 400; diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index 1fdd48b..582462f 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -1,22 +1,22 @@ +const firestore = require('../utils/db'); const { createSuccessResponse, createErrorResponse } = require('../utils/helpers'); -// Ranks data from the existing Python implementation -const RANKS = [ - {"num_origins": "9731427", "rank": "ALL"}, - {"num_origins": "7232806", "rank": "Top 10M"}, - {"num_origins": "881817", "rank": "Top 1M"}, - {"num_origins": "91410", "rank": "Top 100k"}, - {"num_origins": "9524", "rank": "Top 10k"}, - {"num_origins": "965", "rank": "Top 1k"}, -]; - /** - * List all rank options + * List all rank options from database */ const listRanks = async (req, res) => { try { + const snapshot = await firestore.collection('ranks').orderBy('mobile_origins', 'desc').get(); + const data = []; + + // Extract only the 'rank' property from each document + snapshot.forEach(doc => { + const docData = doc.data(); + data.push({ rank: docData.rank }); + }); + res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(RANKS))); + res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { console.error('Error fetching ranks:', error); res.statusCode = 400; From d036b31762b806a62b83ec4243861ac4704565f5 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 25 May 2025 23:02:20 +0200 Subject: [PATCH 24/44] version filter --- src/controllers/adoptionController.js | 7 ++++++- src/controllers/cwvtechController.js | 5 +++++ src/controllers/lighthouseController.js | 5 +++++ src/controllers/pageWeightController.js | 7 ++++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 025bddc..97db9e5 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -61,8 +61,13 @@ const listAdoptionData = async (req, res) => { query = query.where('rank', '==', params.rank); } - // Always filter by technology + // Always filter by technology and version query = query.where('technology', '==', technology); + if (params.version && techArray.length === 1) { + query = query.where('version', '==', params.version); + } else { + query = query.where('version', '==', 'ALL'); + } // Execute query const snapshot = await query.get(); diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index ccbd185..fd6a8ce 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -73,6 +73,11 @@ const listCWVTechData = async (req, res) => { query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); query = query.where('technology', '==', technology); + if (params.version && techArray.length === 1) { + query = query.where('version', '==', params.version); + } else { + query = query.where('version', '==', 'ALL'); + } // Execute query const snapshot = await query.get(); diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index 3be21bc..78c1ced 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -73,6 +73,11 @@ const listLighthouseData = async (req, res) => { query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); query = query.where('technology', '==', technology); + if (params.version && techArray.length === 1) { + query = query.where('version', '==', params.version); + } else { + query = query.where('version', '==', 'ALL'); + } // Execute query const snapshot = await query.get(); diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index 251b5ad..cc4f6a1 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -61,8 +61,13 @@ const listPageWeightData = async (req, res) => { query = query.where('rank', '==', params.rank); } - // Always filter by technology + // Always filter by technology and version query = query.where('technology', '==', technology); + if (params.version && techArray.length === 1) { + query = query.where('version', '==', params.version); + } else { + query = query.where('version', '==', 'ALL'); + } // Execute query const snapshot = await query.get(); From 18ea8330feb385bdb94e955931f8711a725e2a2c Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 25 May 2025 23:24:05 +0200 Subject: [PATCH 25/44] versions endpoint --- src/controllers/versionsController.js | 42 +++++++++++++++++++++++++++ src/index.js | 3 ++ 2 files changed, 45 insertions(+) create mode 100644 src/controllers/versionsController.js diff --git a/src/controllers/versionsController.js b/src/controllers/versionsController.js new file mode 100644 index 0000000..e17075a --- /dev/null +++ b/src/controllers/versionsController.js @@ -0,0 +1,42 @@ +const firestore = require('../utils/db'); +const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); + +/** + * List versions with optional technology filtering + */ +const listVersions = async (req, res) => { + try { + const params = req.query; + let ref = firestore.collection('versions'); + let query = ref; + + // Filter by technology if provided + if (params.technology) { + const technologyArray = convertToArray(params.technology); + if (technologyArray.length > 0) { + // Using 'in' operator for filtering by technology names + query = query.where('technology', 'in', technologyArray); + } + } + + // Execute query + const snapshot = await query.get(); + const data = []; + + // Extract all version documents + snapshot.forEach(doc => { + data.push(doc.data()); + }); + + res.statusCode = 200; + res.end(JSON.stringify(createSuccessResponse(data))); + } catch (error) { + console.error('Error fetching versions:', error); + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + } +}; + +module.exports = { + listVersions +}; diff --git a/src/index.js b/src/index.js index 68c1fd5..74e362b 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const { listLighthouse } = require('./controllers/lighthouseController'); const { listPageWeight } = require('./controllers/pageWeightController'); const { listRanks } = require('./controllers/ranksController'); const { listGeos } = require('./controllers/geosController'); +const { listVersions } = require('./controllers/versionsController'); // Helper function to set CORS headers const setCORSHeaders = (res) => { @@ -101,6 +102,8 @@ const handleRequest = async (req, res) => { await listRanks(req, res); } else if (pathname === '/v1/geos' && req.method === 'GET') { await listGeos(req, res); + } else if (pathname === '/v1/versions' && req.method === 'GET') { + await listVersions(req, res); } else { // 404 Not Found res.statusCode = 404; From d1954912a8410313c7a2e2bde5f7a75b95d7db55 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sat, 31 May 2025 23:55:25 +0200 Subject: [PATCH 26/44] min dev instances --- terraform/dev/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 77e6d54..090f67e 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -30,5 +30,5 @@ variable "google_service_account_api_gateway" { variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number - default = 0 + default = 1 } From 7e99850323799855c46b6d33c8b5a5b163a6245a Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:52:39 +0200 Subject: [PATCH 27/44] field selection --- README.md | 124 +++++++++ src/__tests__/routes.test.js | 305 +++++++++++++++++++++- src/controllers/adoptionController.js | 81 ++---- src/controllers/categoriesController.js | 45 ++-- src/controllers/cwvtechController.js | 89 ++----- src/controllers/geosController.js | 10 +- src/controllers/lighthouseController.js | 89 ++----- src/controllers/pageWeightController.js | 81 ++---- src/controllers/ranksController.js | 7 +- src/controllers/technologiesController.js | 72 +++-- src/controllers/versionsController.js | 21 +- src/package.json | 5 +- src/utils/controllerHelpers.js | 193 ++++++++++++++ src/utils/db.js | 12 +- test-api.sh | 23 +- 15 files changed, 809 insertions(+), 348 deletions(-) create mode 100644 src/utils/controllerHelpers.js diff --git a/README.md b/README.md index 24bb67d..ed9718d 100644 --- a/README.md +++ b/README.md @@ -385,3 +385,127 @@ Or in case of an error: ] } ``` + +## Field Selection API Documentation + +### Overview + +The categories and technologies endpoints now support custom field selection, allowing clients to specify exactly which fields they want in the response. This feature helps reduce payload size and improves API performance by returning only the needed data. + +### Endpoints Supporting Field Selection + +- `GET /v1/technologies` +- `GET /v1/categories` + +### Usage + +#### Basic Syntax + +Add a `fields` parameter to your request with comma-separated field names: + +``` +GET /v1/technologies?fields=technology,category +GET /v1/categories?fields=category,description +``` + +#### Examples + +##### Technologies Endpoint + +**Get only technology names and categories:** + +``` +GET /v1/technologies?fields=technology,category +``` + +Response: + +```json +{ + "data": [ + { + "technology": "React", + "category": "JavaScript Frameworks" + }, + { + "technology": "Angular", + "category": "JavaScript Frameworks" + } + ] +} +``` + +**Get technology names and descriptions:** + +``` +GET /v1/technologies?fields=technology,description +``` + +**Combine with existing filters:** + +``` +GET /v1/technologies?category=JavaScript%20Frameworks&fields=technology,icon +``` + +##### Categories Endpoint + +**Get only category names:** + +``` +GET /v1/categories?fields=category +``` + +**Get categories with descriptions:** + +``` +GET /v1/categories?fields=category,description +``` + +#### Behavior Notes + +1. **Field Priority**: The `fields` parameter takes precedence over other response formatting options, except for `onlyname` +2. **Invalid Fields**: Non-existent fields are silently ignored +3. **Empty Fields**: If no valid fields are specified, the full object is returned +4. **Backward Compatibility**: When `fields` is not specified, endpoints return their default response format +5. **onlyname Override**: The `onlyname` parameter still takes precedence over `fields` for backward compatibility + +#### Available Fields + +##### Technologies Endpoint + +- `technology` - Technology name +- `category` - Category name +- `description` - Technology description +- `icon` - Icon filename +- `origins` - Array of origin companies/organizations + +##### Categories Endpoint + +- `category` - Category name +- Additional fields depend on your data structure + +#### Error Handling + +The field selection feature handles errors gracefully: + +- Invalid field names are ignored +- Empty field lists return full objects +- Malformed field parameters fallback to default behavior + +#### Performance Benefits + +- **Reduced Payload Size**: Only requested fields are included +- **Faster Parsing**: Clients process smaller JSON objects +- **Bandwidth Savings**: Less data transferred over the network +- **Improved Caching**: More specific responses can be cached more effectively + +#### Migration Guide + +Existing API consumers are not affected by this change. The field selection feature is entirely opt-in through the `fields` parameter. + +To adopt field selection: + +1. Identify which fields your application actually uses +2. Add the `fields` parameter with those field names +3. Update your client code to handle the new response structure +4. Test thoroughly with your specific use cases diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index 896d6cb..bd45f36 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -11,24 +11,34 @@ jest.mock('../utils/db', () => { get: jest.fn().mockResolvedValue({ empty: false, forEach: (callback) => { + // Mock technology data callback({ data: () => ({ - technology: 'Test Technology', - category: 'Test Category', - description: 'Test Description', - icon: 'test-icon.svg', - origins: 1000 + technology: 'WordPress', + category: 'CMS', + description: 'A popular content management system', + icon: 'wordpress.svg', + origins: ['WordPress Foundation'] }), - get: (field) => 'Test Value' + get: (field) => { + const mockData = { + technology: 'WordPress', + category: 'CMS', + rank: 'ALL', + geo: 'ALL', + date: '2023-01-01' + }; + return mockData[field] || 'Mock Value'; + } }); }, docs: [{ data: () => ({ - technology: 'Test Technology', - category: 'Test Category', - description: 'Test Description', - icon: 'test-icon.svg', - origins: 1000, + technology: 'WordPress', + category: 'CMS', + description: 'A popular content management system', + icon: 'wordpress.svg', + origins: ['WordPress Foundation'], date: '2023-01-01' }) }] @@ -37,12 +47,24 @@ jest.mock('../utils/db', () => { }); describe('API Routes', () => { - describe('GET /', () => { + describe('Health Check', () => { it('should return a health check response', async () => { const res = await request(app).get('/'); expect(res.statusCode).toEqual(200); expect(res.body).toHaveProperty('status', 'ok'); }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + expect(res.headers['access-control-allow-methods']).toContain('GET'); + }); }); describe('GET /v1/technologies', () => { @@ -53,16 +75,271 @@ describe('API Routes', () => { }); it('should filter technologies by name', async () => { - const res = await request(app).get('/v1/technologies?technology=Test'); + const res = await request(app).get('/v1/technologies?technology=WordPress'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should return only names when onlyname parameter is provided', async () => { + const res = await request(app).get('/v1/technologies?technology=WordPress&onlyname=true'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should support field selection', async () => { + const res = await request(app).get('/v1/technologies?technology=WordPress&fields=technology,icon'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should prioritize onlyname over fields parameter', async () => { + const res = await request(app).get('/v1/technologies?technology=WordPress&onlyname=true&fields=technology,icon'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/technologies') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/categories', () => { + it('should return categories', async () => { + const res = await request(app).get('/v1/categories'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should filter categories by name', async () => { + const res = await request(app).get('/v1/categories?category=CMS'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBe(true); }); it('should return only names when onlyname parameter is provided', async () => { - const res = await request(app).get('/v1/technologies?onlyname=true'); + const res = await request(app).get('/v1/categories?category=CMS&onlyname=true'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should support field selection', async () => { + const res = await request(app).get('/v1/categories?category=CMS&fields=category'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/categories') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/ranks', () => { + it('should return ranks', async () => { + const res = await request(app).get('/v1/ranks'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/ranks') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/geos', () => { + it('should return geos', async () => { + const res = await request(app).get('/v1/geos'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBe(true); }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/geos') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); }); + describe('GET /v1/versions', () => { + it('should return versions', async () => { + const res = await request(app).get('/v1/versions'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should filter versions by technology', async () => { + const res = await request(app).get('/v1/versions?technology=WordPress'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/versions') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/adoption', () => { + it('should return adoption data with required parameters', async () => { + const res = await request(app).get('/v1/adoption?technology=WordPress&geo=ALL&rank=ALL&start=latest'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle missing required parameters', async () => { + const res = await request(app).get('/v1/adoption'); + expect(res.statusCode).toEqual(400); + expect(res.body).toHaveProperty('errors'); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/adoption') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/cwv', () => { + it('should return CWV data with required parameters', async () => { + const res = await request(app).get('/v1/cwv?technology=WordPress&geo=ALL&rank=ALL&start=latest'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle missing required parameters', async () => { + const res = await request(app).get('/v1/cwv'); + expect(res.statusCode).toEqual(400); + expect(res.body).toHaveProperty('errors'); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/cwv') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/lighthouse', () => { + it('should return lighthouse data with required parameters', async () => { + const res = await request(app).get('/v1/lighthouse?technology=WordPress&geo=ALL&rank=ALL&start=latest'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle missing required parameters', async () => { + const res = await request(app).get('/v1/lighthouse'); + expect(res.statusCode).toEqual(400); + expect(res.body).toHaveProperty('errors'); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/lighthouse') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('GET /v1/page-weight', () => { + it('should return page weight data with required parameters', async () => { + const res = await request(app).get('/v1/page-weight?technology=WordPress&geo=ALL&rank=ALL&start=latest'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it('should handle missing required parameters', async () => { + const res = await request(app).get('/v1/page-weight'); + expect(res.statusCode).toEqual(400); + expect(res.body).toHaveProperty('errors'); + }); + + it('should handle CORS preflight requests', async () => { + const res = await request(app) + .options('/v1/page-weight') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET') + .set('Access-Control-Request-Headers', 'Content-Type'); + + expect(res.statusCode).toEqual(204); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + }); + }); + + describe('Error Handling', () => { + it('should return 404 for unknown endpoints', async () => { + const res = await request(app).get('/v1/unknown-endpoint'); + expect(res.statusCode).toEqual(404); + expect(res.body).toHaveProperty('error', 'Not Found'); + }); + + it('should handle invalid query parameters gracefully', async () => { + const res = await request(app).get('/v1/technologies?invalid=parameter'); + expect(res.statusCode).toEqual(200); + expect(Array.isArray(res.body)).toBe(true); + }); + }); + + describe('Response Headers', () => { + it('should include proper CORS headers', async () => { + const res = await request(app).get('/v1/technologies'); + expect(res.headers['access-control-allow-origin']).toEqual('*'); + expect(res.headers['content-type']).toContain('application/json'); + expect(res.headers['cache-control']).toContain('public'); + }); + + it('should include ETag headers for caching', async () => { + const res = await request(app).get('/'); + expect(res.headers).toHaveProperty('etag'); + }); + + it('should include timing headers', async () => { + const res = await request(app).get('/v1/technologies'); + expect(res.headers['timing-allow-origin']).toEqual('*'); + }); + }); }); diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 97db9e5..85813b6 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -1,73 +1,50 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { + REQUIRED_PARAMS, + validateRequiredParams, + sendValidationError, + applyDateFilters, + applyStandardFilters, + preprocessParams, + handleControllerError +} = require('../utils/controllerHelpers'); const TABLE = 'adoption'; -/** - * Get the latest date in the collection - */ -const getLatestDate = async () => { - const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); - const snapshot = await query.get(); - if (!snapshot.empty) { - return snapshot.docs[0].data().date; - } - return null; -}; - /** * List adoption data with filtering */ const listAdoptionData = async (req, res) => { try { const params = req.query; - const data = []; - // Technology is required - if (!params.technology) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['technology', 'missing technology parameter'] - ]))); + // Validate required parameters + const requiredParams = [ + REQUIRED_PARAMS.GEO, + REQUIRED_PARAMS.RANK, + REQUIRED_PARAMS.TECHNOLOGY + ]; + + const validationErrors = validateRequiredParams(params, requiredParams); + if (validationErrors) { + sendValidationError(res, validationErrors); return; } - // Convert technology parameter to array - const techArray = convertToArray(params.technology); - - // Handle 'latest' special value for start parameter - if (params.start && params.start === 'latest') { - params.start = await getLatestDate(); - } + // Preprocess parameters and get technology array + const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); + const data = []; // Query for each technology for (const technology of techArray) { let query = firestore.collection(TABLE); - // Apply filters - if (params.start) { - query = query.where('date', '>=', params.start); - } - - if (params.end) { - query = query.where('date', '<=', params.end); - } - - if (params.geo) { - query = query.where('geo', '==', params.geo); - } - - if (params.rank) { - query = query.where('rank', '==', params.rank); - } + // Apply standard filters including version filter + query = applyStandardFilters(query, processedParams, technology, techArray); - // Always filter by technology and version - query = query.where('technology', '==', technology); - if (params.version && techArray.length === 1) { - query = query.where('version', '==', params.version); - } else { - query = query.where('version', '==', 'ALL'); - } + // Apply date filters + query = applyDateFilters(query, processedParams); // Execute query const snapshot = await query.get(); @@ -80,9 +57,7 @@ const listAdoptionData = async (req, res) => { res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching adoption data:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching adoption data'); } }; diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index c16ca5f..e77dc6e 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -1,47 +1,44 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { applyArrayFilter, selectFields, handleControllerError } = require('../utils/controllerHelpers'); /** - * List categories with optional filtering + * List categories with optional filtering and field selection */ const listCategories = async (req, res) => { try { const params = req.query; - let ref = firestore.collection('categories'); - let query = ref.orderBy('category', 'asc'); + const isOnlyNames = params.onlyname || typeof params.onlyname === 'string'; + const hasCustomFields = params.fields && !isOnlyNames; - // Filter by category if provided - if (params.category) { - const categoryArray = convertToArray(params.category); - if (categoryArray.length > 0) { - // Using 'in' operator instead of multiple 'or' filters for simplicity - query = query.where('category', 'in', categoryArray); - } - } + let query = firestore.collection('categories').orderBy('category', 'asc'); + + // Apply category filter using shared utility + query = applyArrayFilter(query, 'category', params.category); // Execute query const snapshot = await query.get(); const data = []; - // Return only category names if onlyname parameter exists - if (params.onlyname || typeof params.onlyname === 'string') { - snapshot.forEach(doc => { + // Process results based on response type + snapshot.forEach(doc => { + if (isOnlyNames) { data.push(doc.get('category')); - }); - } else { - // Return full category objects - snapshot.forEach(doc => { + } else if (hasCustomFields) { + // Use custom field selection + const fullData = doc.data(); + data.push(selectFields(fullData, params.fields)); + } else { + // Return full data data.push(doc.data()); - }); - } + } + }); // Send response res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching categories:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching categories'); } }; diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index fd6a8ce..47dd505 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -1,83 +1,50 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { + REQUIRED_PARAMS, + validateRequiredParams, + sendValidationError, + applyDateFilters, + applyStandardFilters, + preprocessParams, + handleControllerError +} = require('../utils/controllerHelpers'); const TABLE = 'core_web_vitals'; -/** - * Get the latest date in the collection - */ -const getLatestDate = async () => { - const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); - const snapshot = await query.get(); - if (!snapshot.empty) { - return snapshot.docs[0].data().date; - } - return null; -}; - /** * List Core Web Vitals data with filtering */ const listCWVTechData = async (req, res) => { try { const params = req.query; - const data = []; - // Required parameters check - if (!params.technology) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['technology', 'missing technology parameter'] - ]))); - return; - } + // Validate required parameters + const requiredParams = [ + REQUIRED_PARAMS.GEO, + REQUIRED_PARAMS.RANK, + REQUIRED_PARAMS.TECHNOLOGY + ]; - if (!params.geo) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['geo', 'missing geo parameter'] - ]))); + const validationErrors = validateRequiredParams(params, requiredParams); + if (validationErrors) { + sendValidationError(res, validationErrors); return; } - if (!params.rank) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['rank', 'missing rank parameter'] - ]))); - return; - } - - // Convert technology parameter to array - const techArray = convertToArray(params.technology); - - // Handle 'latest' special value for start parameter - if (params.start && params.start === 'latest') { - params.start = await getLatestDate(); - } + // Preprocess parameters and get technology array + const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); + const data = []; // Query for each technology for (const technology of techArray) { let query = firestore.collection(TABLE); - // Apply filters - if (params.start) { - query = query.where('date', '>=', params.start); - } - - if (params.end) { - query = query.where('date', '<=', params.end); - } + // Apply standard filters including version filter + query = applyStandardFilters(query, processedParams, technology, techArray); - // Always filter by required parameters - query = query.where('geo', '==', params.geo); - query = query.where('rank', '==', params.rank); - query = query.where('technology', '==', technology); - if (params.version && techArray.length === 1) { - query = query.where('version', '==', params.version); - } else { - query = query.where('version', '==', 'ALL'); - } + // Apply date filters + query = applyDateFilters(query, processedParams); // Execute query const snapshot = await query.get(); @@ -90,9 +57,7 @@ const listCWVTechData = async (req, res) => { res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching Core Web Vitals data:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching Core Web Vitals data'); } }; diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index 20c7da7..d4f1538 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -1,5 +1,6 @@ const firestore = require('../utils/db'); -const { createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { handleControllerError } = require('../utils/controllerHelpers'); /** * List all geographic locations from database @@ -11,16 +12,13 @@ const listGeos = async (req, res) => { // Extract only the 'geo' property from each document snapshot.forEach(doc => { - const docData = doc.data(); - data.push({ geo: docData.geo }); + data.push({ geo: doc.data().geo }); }); res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching geographic locations:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching geographic locations'); } }; diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index 78c1ced..6d3b6e4 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -1,83 +1,50 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { + REQUIRED_PARAMS, + validateRequiredParams, + sendValidationError, + applyDateFilters, + applyStandardFilters, + preprocessParams, + handleControllerError +} = require('../utils/controllerHelpers'); const TABLE = 'lighthouse'; -/** - * Get the latest date in the collection - */ -const getLatestDate = async () => { - const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); - const snapshot = await query.get(); - if (!snapshot.empty) { - return snapshot.docs[0].data().date; - } - return null; -}; - /** * List Lighthouse data with filtering */ const listLighthouseData = async (req, res) => { try { const params = req.query; - const data = []; - // Required parameters check - if (!params.technology) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['technology', 'missing technology parameter'] - ]))); - return; - } + // Validate required parameters + const requiredParams = [ + REQUIRED_PARAMS.GEO, + REQUIRED_PARAMS.RANK, + REQUIRED_PARAMS.TECHNOLOGY + ]; - if (!params.geo) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['geo', 'missing geo parameter'] - ]))); + const validationErrors = validateRequiredParams(params, requiredParams); + if (validationErrors) { + sendValidationError(res, validationErrors); return; } - if (!params.rank) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['rank', 'missing rank parameter'] - ]))); - return; - } - - // Convert technology parameter to array - const techArray = convertToArray(params.technology); - - // Handle 'latest' special value for start parameter - if (params.start && params.start === 'latest') { - params.start = await getLatestDate(); - } + // Preprocess parameters and get technology array + const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); + const data = []; // Query for each technology for (const technology of techArray) { let query = firestore.collection(TABLE); - // Apply filters - if (params.start) { - query = query.where('date', '>=', params.start); - } - - if (params.end) { - query = query.where('date', '<=', params.end); - } + // Apply date filters first + query = applyDateFilters(query, processedParams); - // Always filter by required parameters - query = query.where('geo', '==', params.geo); - query = query.where('rank', '==', params.rank); - query = query.where('technology', '==', technology); - if (params.version && techArray.length === 1) { - query = query.where('version', '==', params.version); - } else { - query = query.where('version', '==', 'ALL'); - } + // Apply standard filters and version filter + query = applyStandardFilters(query, processedParams, technology, techArray); // Execute query const snapshot = await query.get(); @@ -90,9 +57,7 @@ const listLighthouseData = async (req, res) => { res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching Lighthouse data:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching Lighthouse data'); } }; diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index cc4f6a1..d69a9b0 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -1,73 +1,50 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { + REQUIRED_PARAMS, + validateRequiredParams, + sendValidationError, + applyDateFilters, + applyStandardFilters, + preprocessParams, + handleControllerError +} = require('../utils/controllerHelpers'); const TABLE = 'page_weight'; -/** - * Get the latest date in the collection - */ -const getLatestDate = async () => { - const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1); - const snapshot = await query.get(); - if (!snapshot.empty) { - return snapshot.docs[0].data().date; - } - return null; -}; - /** * List Page Weight data with filtering */ const listPageWeightData = async (req, res) => { try { const params = req.query; - const data = []; - // Required parameters check - if (!params.technology) { - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([ - ['technology', 'missing technology parameter'] - ]))); + // Validate required parameters + const requiredParams = [ + REQUIRED_PARAMS.GEO, + REQUIRED_PARAMS.RANK, + REQUIRED_PARAMS.TECHNOLOGY + ]; + + const validationErrors = validateRequiredParams(params, requiredParams); + if (validationErrors) { + sendValidationError(res, validationErrors); return; } - // Convert technology parameter to array - const techArray = convertToArray(params.technology); - - // Handle 'latest' special value for start parameter - if (params.start && params.start === 'latest') { - params.start = await getLatestDate(); - } + // Preprocess parameters and get technology array + const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); + const data = []; // Query for each technology for (const technology of techArray) { let query = firestore.collection(TABLE); - // Apply filters - if (params.start) { - query = query.where('date', '>=', params.start); - } - - if (params.end) { - query = query.where('date', '<=', params.end); - } - - if (params.geo) { - query = query.where('geo', '==', params.geo); - } - - if (params.rank) { - query = query.where('rank', '==', params.rank); - } + // Apply date filters + query = applyDateFilters(query, processedParams); - // Always filter by technology and version - query = query.where('technology', '==', technology); - if (params.version && techArray.length === 1) { - query = query.where('version', '==', params.version); - } else { - query = query.where('version', '==', 'ALL'); - } + // Apply optional standard filters (geo, rank are optional for page weight) and version filter + query = applyStandardFilters(query, processedParams, technology, techArray); // Execute query const snapshot = await query.get(); @@ -80,9 +57,7 @@ const listPageWeightData = async (req, res) => { res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching Page Weight data:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching Page Weight data'); } }; diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index 582462f..a77c35c 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -1,5 +1,6 @@ const firestore = require('../utils/db'); -const { createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { handleControllerError } = require('../utils/controllerHelpers'); /** * List all rank options from database @@ -18,9 +19,7 @@ const listRanks = async (req, res) => { res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching ranks:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching ranks'); } }; diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index 764f019..444968f 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -1,66 +1,54 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); - -// Technology Presenter -const presentTechnology = (item) => { - return { - technology: item.technology, - category: item.category, - description: item.description, - icon: item.icon, - origins: item.origins - }; -}; +const { createSuccessResponse } = require('../utils/helpers'); +const { applyArrayFilter, selectFields, handleControllerError } = require('../utils/controllerHelpers'); + +// Technology Presenter - optimized with destructuring +const presentTechnology = ({ technology, category, description, icon, origins }) => ({ + technology, + category, + description, + icon, + origins +}); /** - * List technologies with optional filtering + * List technologies with optional filtering and field selection */ const listTechnologies = async (req, res) => { try { const params = req.query; - let ref = firestore.collection('technologies'); - let query = ref.orderBy('technology', 'asc'); + const isOnlyNames = params.onlyname || typeof params.onlyname === 'string'; + const hasCustomFields = params.fields && !isOnlyNames; - // Filter by technology if provided - if (params.technology) { - const techArray = convertToArray(params.technology); - if (techArray.length > 0) { - // Using 'in' operator instead of multiple 'or' filters for simplicity - query = query.where('technology', 'in', techArray); - } - } + let query = firestore.collection('technologies').orderBy('technology', 'asc'); - // Filter by category if provided - if (params.category) { - const categoryArray = convertToArray(params.category); - if (categoryArray.length > 0) { - query = query.where('category_obj', 'array-contains-any', categoryArray); - } - } + // Apply filters using shared utilities + query = applyArrayFilter(query, 'technology', params.technology); + query = applyArrayFilter(query, 'category_obj', params.category, 'array-contains-any'); // Execute query const snapshot = await query.get(); const data = []; - if (params.onlyname || typeof params.onlyname === 'string') { - // Return only technology names if onlyname parameter exists - snapshot.forEach(doc => { + // Process results based on response type + snapshot.forEach(doc => { + if (isOnlyNames) { data.push(doc.get('technology')); - }); - } else { - // Return full technology objects - snapshot.forEach(doc => { + } else if (hasCustomFields) { + // Use custom field selection + const fullData = doc.data(); + data.push(selectFields(fullData, params.fields)); + } else { + // Use default presenter data.push(presentTechnology(doc.data())); - }); - }; + } + }); // Send response res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching technologies:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching technologies'); } }; diff --git a/src/controllers/versionsController.js b/src/controllers/versionsController.js index e17075a..ee9e62e 100644 --- a/src/controllers/versionsController.js +++ b/src/controllers/versionsController.js @@ -1,5 +1,6 @@ const firestore = require('../utils/db'); -const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers'); +const { createSuccessResponse } = require('../utils/helpers'); +const { applyArrayFilter, handleControllerError } = require('../utils/controllerHelpers'); /** * List versions with optional technology filtering @@ -7,17 +8,10 @@ const { convertToArray, createSuccessResponse, createErrorResponse } = require(' const listVersions = async (req, res) => { try { const params = req.query; - let ref = firestore.collection('versions'); - let query = ref; + let query = firestore.collection('versions'); - // Filter by technology if provided - if (params.technology) { - const technologyArray = convertToArray(params.technology); - if (technologyArray.length > 0) { - // Using 'in' operator for filtering by technology names - query = query.where('technology', 'in', technologyArray); - } - } + // Apply technology filter using shared utility + query = applyArrayFilter(query, 'technology', params.technology); // Execute query const snapshot = await query.get(); @@ -28,12 +22,11 @@ const listVersions = async (req, res) => { data.push(doc.data()); }); + // Send response res.statusCode = 200; res.end(JSON.stringify(createSuccessResponse(data))); } catch (error) { - console.error('Error fetching versions:', error); - res.statusCode = 400; - res.end(JSON.stringify(createErrorResponse([['query', error.message]]))); + handleControllerError(res, error, 'fetching versions'); } }; diff --git a/src/package.json b/src/package.json index f6f0570..6efafde 100644 --- a/src/package.json +++ b/src/package.json @@ -8,9 +8,10 @@ }, "scripts": { "start": "node index.js", - "start:functions": "functions-framework --target=app", + "start:functions": "export DATABASE=tech-report-apis-prod && functions-framework --target=app", "dev": "nodemon index.js", - "test": "jest" + "test": "jest", + "test:live": "bash ../test-api.sh" }, "dependencies": { "@google-cloud/firestore": "7.3.0", diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js new file mode 100644 index 0000000..ffd2875 --- /dev/null +++ b/src/utils/controllerHelpers.js @@ -0,0 +1,193 @@ +const { createErrorResponse } = require('./helpers'); + +/** + * Common parameter validation patterns + */ +const REQUIRED_PARAMS = { + TECHNOLOGY: 'technology', + GEO: 'geo', + RANK: 'rank', + VERSION: 'version' +}; + +/** + * Validate required parameters for a request + * @param {Object} params - Request query parameters + * @param {Array} required - Array of required parameter names + * @returns {Array|null} - Array of errors or null if valid + */ +const validateRequiredParams = (params, required) => { + const errors = []; + + for (const param of required) { + if (!params[param]) { + errors.push([param, `missing ${param} parameter`]); + } + } + + return errors.length > 0 ? errors : null; +}; + +/** + * Send error response for missing parameters + * @param {Object} res - Response object + * @param {Array} errors - Array of error tuples + */ +const sendValidationError = (res, errors) => { + res.statusCode = 400; + res.end(JSON.stringify(createErrorResponse(errors))); +}; + +/** + * Get the latest date from a collection + * @param {Object} firestore - Firestore instance + * @param {string} collection - Collection name + * @returns {string|null} - Latest date or null + */ +const getLatestDate = async (firestore, collection) => { + const query = firestore.collection(collection).orderBy('date', 'desc').limit(1); + const snapshot = await query.get(); + if (!snapshot.empty) { + return snapshot.docs[0].data().date; + } + return null; +}; + +/** + * Apply date filters to a query + * @param {Object} query - Firestore query + * @param {Object} params - Request parameters + * @returns {Object} - Modified query + */ +const applyDateFilters = (query, params) => { + if (params.start) { + query = query.where('date', '>=', params.start); + } + if (params.end) { + query = query.where('date', '<=', params.end); + } + return query; +}; + +/** + * Apply standard filters (geo, rank, technology, version) to a query + * @param {Object} query - Firestore query + * @param {Object} params - Request parameters + * @param {string} technology - Technology name + * @param {Array} techArray - Array of technologies (used for version filtering) + * @returns {Object} - Modified query + */ +const applyStandardFilters = (query, params, technology, techArray = []) => { + if (params.geo) { + query = query.where('geo', '==', params.geo); + } + if (params.rank) { + query = query.where('rank', '==', params.rank); + } + if (technology) { + query = query.where('technology', '==', technology); + } + + // Apply version filter with special handling for 'ALL' case + if (params.version && techArray.length === 1) { + //query = query.where('version', '==', params.version); // TODO: Uncomment when migrating to a new data schema + } else { + //query = query.where('version', '==', 'ALL'); + } + + return query; +}; + +/** + * Process technology array and handle 'latest' date substitution + * @param {Object} firestore - Firestore instance + * @param {Object} params - Request parameters + * @param {string} collection - Collection name + * @returns {Object} - Processed parameters and tech array + */ +const preprocessParams = async (firestore, params, collection) => { + // Handle 'latest' special value for start parameter + if (params.start && params.start === 'latest') { + params.start = await getLatestDate(firestore, collection); + } + + // Handle version 'ALL' special case for multiple technologies + const techArray = require('./helpers').convertToArray(params.technology); + if (!params.version || techArray.length > 1) { + params.version = 'ALL'; + } + + return { params, techArray }; +}; + +/** + * Apply array-based filters using 'in' or 'array-contains-any' operators + * @param {Object} query - Firestore query + * @param {string} field - Field name to filter on + * @param {string} value - Comma-separated values or single value + * @param {string} operator - Firestore operator ('in' or 'array-contains-any') + * @returns {Object} - Modified query + */ +const applyArrayFilter = (query, field, value, operator = 'in') => { + if (!value) return query; + + const { convertToArray } = require('./helpers'); + const valueArray = convertToArray(value); + + if (valueArray.length > 0) { + query = query.where(field, operator, valueArray); + } + + return query; +}; + +/** + * Select specific fields from an object based on comma-separated field names + * @param {Object} data - Source data object + * @param {string} fieldsParam - Comma-separated field names (e.g., "technology,category") + * @returns {Object} - Object containing only requested fields + */ +const selectFields = (data, fieldsParam) => { + if (!fieldsParam) return data; + + const { convertToArray } = require('./helpers'); + const fields = convertToArray(fieldsParam); + + if (fields.length === 0) return data; + + const result = {}; + fields.forEach(field => { + if (data.hasOwnProperty(field)) { + result[field] = data[field]; + } + }); + + return result; +}; + +/** + * Handle controller errors with consistent error response format + * @param {Object} res - Response object + * @param {Error} error - Error object + * @param {string} operation - Description of the operation that failed + */ +const handleControllerError = (res, error, operation) => { + console.error(`Error ${operation}:`, error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: `Failed to ${operation}` }] + })); +}; + +module.exports = { + REQUIRED_PARAMS, + validateRequiredParams, + sendValidationError, + getLatestDate, + applyDateFilters, + applyStandardFilters, + preprocessParams, + applyArrayFilter, + selectFields, + handleControllerError +}; diff --git a/src/utils/db.js b/src/utils/db.js index 844516f..41bbe13 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -1,9 +1,17 @@ const { Firestore } = require('@google-cloud/firestore'); -// Initialize Firestore +// Initialize Firestore with basic optimizations const firestore = new Firestore({ projectId: process.env.PROJECT, - databaseId: process.env.DATABASE + databaseId: process.env.DATABASE, + settings: { + // Enable connection pooling + maxIdleChannels: 10, + // Enable keepalive to reduce connection overhead + keepaliveTime: 30000, + keepaliveTimeout: 5000, + keepalivePermitWithoutCalls: true + } }); module.exports = firestore; diff --git a/test-api.sh b/test-api.sh index c42ae16..b373a00 100755 --- a/test-api.sh +++ b/test-api.sh @@ -84,29 +84,32 @@ test_cors_preflight "/" test_endpoint "/" "" # Test technologies endpoint -test_cors_preflight "/technologies" -test_endpoint "/technologies" "?technology=WordPress&onlyname=true" +test_cors_preflight "/v1/technologies" +test_endpoint "/v1/technologies" "?technology=WordPress&onlyname=true" +test_endpoint "/v1/technologies" "?technology=WordPress&onlyname=true&fields=technology,icon" +test_endpoint "/v1/technologies" "?technology=WordPress&fields=technology,icon" # Test categories endpoint -test_cors_preflight "/categories" -test_endpoint "/categories" "?category=CMS&onlyname=true" +test_cors_preflight "/v1/categories" +test_endpoint "/v1/categories" "?category=CMS&onlyname=true" +test_endpoint "/v1/categories" "?category=CMS&fields=category" # Test ranks endpoint -test_endpoint "/ranks" "" +test_endpoint "/v1/ranks" "" # Test geos endpoint -test_endpoint "/geos" "" +test_endpoint "/v1/geos" "" # Test adoption endpoint -test_endpoint "/adoption" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" +test_endpoint "/v1/adoption" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" # Test cwv endpoint -test_endpoint "/cwv" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" +test_endpoint "/v1/cwv" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" # Test lighthouse endpoint -test_endpoint "/lighthouse" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" +test_endpoint "/v1/lighthouse" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" # Test page-weight endpoint -test_endpoint "/page-weight" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" +test_endpoint "/v1/page-weight" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" echo "API tests complete! All endpoints returned 200 status code and CORS is properly configured." From c6351c4bc909864068e830a11c03809ef564436d Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:56:29 +0200 Subject: [PATCH 28/44] test with the current db --- terraform/dev/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 090f67e..f3c11c3 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -15,7 +15,7 @@ variable "environment" { variable "project_database" { type = string description = "The database name" - default = "tech-report-api-prod" // TODO: Update this to the DEV database name + default = "tech-report-apis-prod" // TODO: Update this to the DEV database name } variable "google_service_account_cloud_functions" { From eaab1bc69699c9bc0beb37954b3d92e00d22b978 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Tue, 3 Jun 2025 23:47:08 +0200 Subject: [PATCH 29/44] 2 DBs at the same time --- src/__tests__/routes.test.js | 7 ++++++- src/controllers/adoptionController.js | 4 +++- src/controllers/categoriesController.js | 2 +- src/controllers/cwvtechController.js | 4 +++- src/controllers/geosController.js | 2 +- src/controllers/lighthouseController.js | 4 +++- src/controllers/pageWeightController.js | 4 +++- src/controllers/ranksController.js | 2 +- src/controllers/technologiesController.js | 2 +- src/controllers/versionsController.js | 2 +- src/package.json | 2 +- src/utils/db.js | 22 ++++++++++++++++++++-- terraform/dev/variables.tf | 2 +- 13 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index bd45f36..ca87d12 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -3,7 +3,7 @@ const { app } = require('../index'); // Mock Firestore jest.mock('../utils/db', () => { - return { + const mockFirestoreInstance = { collection: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), @@ -44,6 +44,11 @@ jest.mock('../utils/db', () => { }] }) }; + + return { + firestore: mockFirestoreInstance, + firestoreOld: mockFirestoreInstance + }; }); describe('API Routes', () => { diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 85813b6..5002c42 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -1,4 +1,6 @@ -const firestore = require('../utils/db'); +const { firestoreOld } = require('../utils/db'); +const firestore = firestoreOld; + const { createSuccessResponse } = require('../utils/helpers'); const { REQUIRED_PARAMS, diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index e77dc6e..5c44cbb 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -1,4 +1,4 @@ -const firestore = require('../utils/db'); +const { firestore } = require('../utils/db'); const { createSuccessResponse } = require('../utils/helpers'); const { applyArrayFilter, selectFields, handleControllerError } = require('../utils/controllerHelpers'); diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index 47dd505..caf16f6 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -1,4 +1,6 @@ -const firestore = require('../utils/db'); +const { firestoreOld } = require('../utils/db'); +const firestore = firestoreOld; + const { createSuccessResponse } = require('../utils/helpers'); const { REQUIRED_PARAMS, diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index d4f1538..2ce1e35 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -1,4 +1,4 @@ -const firestore = require('../utils/db'); +const { firestore } = require('../utils/db'); const { createSuccessResponse } = require('../utils/helpers'); const { handleControllerError } = require('../utils/controllerHelpers'); diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index 6d3b6e4..6f358c6 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -1,4 +1,6 @@ -const firestore = require('../utils/db'); +const { firestoreOld } = require('../utils/db'); +const firestore = firestoreOld; + const { createSuccessResponse } = require('../utils/helpers'); const { REQUIRED_PARAMS, diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index d69a9b0..ca9fbff 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -1,4 +1,6 @@ -const firestore = require('../utils/db'); +const { firestoreOld } = require('../utils/db'); +const firestore = firestoreOld; + const { createSuccessResponse } = require('../utils/helpers'); const { REQUIRED_PARAMS, diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index a77c35c..ed6cd3e 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -1,4 +1,4 @@ -const firestore = require('../utils/db'); +const { firestore } = require('../utils/db'); const { createSuccessResponse } = require('../utils/helpers'); const { handleControllerError } = require('../utils/controllerHelpers'); diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index 444968f..158197d 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -1,4 +1,4 @@ -const firestore = require('../utils/db'); +const { firestore } = require('../utils/db'); const { createSuccessResponse } = require('../utils/helpers'); const { applyArrayFilter, selectFields, handleControllerError } = require('../utils/controllerHelpers'); diff --git a/src/controllers/versionsController.js b/src/controllers/versionsController.js index ee9e62e..09b1934 100644 --- a/src/controllers/versionsController.js +++ b/src/controllers/versionsController.js @@ -1,4 +1,4 @@ -const firestore = require('../utils/db'); +const { firestore } = require('../utils/db'); const { createSuccessResponse } = require('../utils/helpers'); const { applyArrayFilter, handleControllerError } = require('../utils/controllerHelpers'); diff --git a/src/package.json b/src/package.json index 6efafde..0c2423f 100644 --- a/src/package.json +++ b/src/package.json @@ -8,7 +8,7 @@ }, "scripts": { "start": "node index.js", - "start:functions": "export DATABASE=tech-report-apis-prod && functions-framework --target=app", + "start:functions": "export DATABASE=tech-report-api-prod && functions-framework --target=app", "dev": "nodemon index.js", "test": "jest", "test:live": "bash ../test-api.sh" diff --git a/src/utils/db.js b/src/utils/db.js index 41bbe13..41941bb 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -1,6 +1,6 @@ const { Firestore } = require('@google-cloud/firestore'); -// Initialize Firestore with basic optimizations +// Initialize Firestore with basic optimizations (default connection using env variables) const firestore = new Firestore({ projectId: process.env.PROJECT, databaseId: process.env.DATABASE, @@ -14,4 +14,22 @@ const firestore = new Firestore({ } }); -module.exports = firestore; +// Initialize production Firestore connection with hardcoded database +const firestoreOld = new Firestore({ + projectId: process.env.PROJECT, + databaseId: 'tech-report-apis-prod', + settings: { + // Enable connection pooling + maxIdleChannels: 10, + // Enable keepalive to reduce connection overhead + keepaliveTime: 30000, + keepaliveTimeout: 5000, + keepalivePermitWithoutCalls: true + } +}); + +// Export both connections - maintain backward compatibility +module.exports = { + firestore, + firestoreOld +}; diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index f3c11c3..090f67e 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -15,7 +15,7 @@ variable "environment" { variable "project_database" { type = string description = "The database name" - default = "tech-report-apis-prod" // TODO: Update this to the DEV database name + default = "tech-report-api-prod" // TODO: Update this to the DEV database name } variable "google_service_account_cloud_functions" { From 91a31499f79b71642da412f10f03a3db0accf864 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 00:17:05 +0200 Subject: [PATCH 30/44] remove caching --- src/__tests__/routes.test.js | 2 +- src/index.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index ca87d12..91a3d1f 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -334,7 +334,7 @@ describe('API Routes', () => { const res = await request(app).get('/v1/technologies'); expect(res.headers['access-control-allow-origin']).toEqual('*'); expect(res.headers['content-type']).toContain('application/json'); - expect(res.headers['cache-control']).toContain('public'); + expect(res.headers['cache-control']).toContain('no-cache'); }); it('should include ETag headers for caching', async () => { diff --git a/src/index.js b/src/index.js index 74e362b..27996cd 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,11 @@ const setCORSHeaders = (res) => { const setCommonHeaders = (res) => { setCORSHeaders(res); res.setHeader('Content-Type', 'application/json'); - res.setHeader('Cache-Control', 'public, max-age=21600'); + //res.setHeader('Cache-Control', 'public, max-age=21600'); + + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); res.setHeader('Timing-Allow-Origin', '*'); }; From 99d57a8b2af7ef90fe52b7aaa923bab0be1847e9 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:45:13 +0200 Subject: [PATCH 31/44] revert cache and upgrade run instance --- src/index.js | 6 +----- terraform/modules/run-service/variables.tf | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 27996cd..74e362b 100644 --- a/src/index.js +++ b/src/index.js @@ -26,11 +26,7 @@ const setCORSHeaders = (res) => { const setCommonHeaders = (res) => { setCORSHeaders(res); res.setHeader('Content-Type', 'application/json'); - //res.setHeader('Cache-Control', 'public, max-age=21600'); - - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); + res.setHeader('Cache-Control', 'public, max-age=21600'); res.setHeader('Timing-Allow-Origin', '*'); }; diff --git a/terraform/modules/run-service/variables.tf b/terraform/modules/run-service/variables.tf index 60e9b38..5ef5596 100644 --- a/terraform/modules/run-service/variables.tf +++ b/terraform/modules/run-service/variables.tf @@ -2,7 +2,7 @@ variable "secrets" { default = [] } variable "region" { - default = "us-east1" + default = "us-central1" type = string } variable "environment" { @@ -22,12 +22,12 @@ variable "entry_point" { type = string } variable "available_memory_mb" { - default = "1Gi" + default = "2Gi" type = string description = "The amount of memory for the Cloud Function" } variable "available_cpu" { - default = "1" + default = "2" type = string description = "The amount of CPU for the Cloud Function" } From aaa9900145490b7ba5f2574c18a02dc04f5b9755 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:50:24 +0200 Subject: [PATCH 32/44] test fix --- src/__tests__/routes.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index 91a3d1f..ca87d12 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -334,7 +334,7 @@ describe('API Routes', () => { const res = await request(app).get('/v1/technologies'); expect(res.headers['access-control-allow-origin']).toEqual('*'); expect(res.headers['content-type']).toContain('application/json'); - expect(res.headers['cache-control']).toContain('no-cache'); + expect(res.headers['cache-control']).toContain('public'); }); it('should include ETag headers for caching', async () => { From 4fbb6ec7b16389d2c18411be7ce7f79994ad8b7b Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:10:39 +0200 Subject: [PATCH 33/44] no cache --- src/__tests__/routes.test.js | 2 +- src/index.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index ca87d12..91a3d1f 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -334,7 +334,7 @@ describe('API Routes', () => { const res = await request(app).get('/v1/technologies'); expect(res.headers['access-control-allow-origin']).toEqual('*'); expect(res.headers['content-type']).toContain('application/json'); - expect(res.headers['cache-control']).toContain('public'); + expect(res.headers['cache-control']).toContain('no-cache'); }); it('should include ETag headers for caching', async () => { diff --git a/src/index.js b/src/index.js index 74e362b..99f1052 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,9 @@ const setCORSHeaders = (res) => { const setCommonHeaders = (res) => { setCORSHeaders(res); res.setHeader('Content-Type', 'application/json'); - res.setHeader('Cache-Control', 'public, max-age=21600'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); res.setHeader('Timing-Allow-Origin', '*'); }; From 0de112ab90a0084420236fb56b189a6faefc7c7c Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:47:59 +0200 Subject: [PATCH 34/44] recreate gateway --- src/__tests__/routes.test.js | 2 +- src/index.js | 4 +--- terraform/dev/main.tf | 15 +++++++-------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index 91a3d1f..ca87d12 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -334,7 +334,7 @@ describe('API Routes', () => { const res = await request(app).get('/v1/technologies'); expect(res.headers['access-control-allow-origin']).toEqual('*'); expect(res.headers['content-type']).toContain('application/json'); - expect(res.headers['cache-control']).toContain('no-cache'); + expect(res.headers['cache-control']).toContain('public'); }); it('should include ETag headers for caching', async () => { diff --git a/src/index.js b/src/index.js index 99f1052..74e362b 100644 --- a/src/index.js +++ b/src/index.js @@ -26,9 +26,7 @@ const setCORSHeaders = (res) => { const setCommonHeaders = (res) => { setCORSHeaders(res); res.setHeader('Content-Type', 'application/json'); - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); + res.setHeader('Cache-Control', 'public, max-age=21600'); res.setHeader('Timing-Allow-Origin', '*'); }; diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 3a621ea..3b35fde 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -13,8 +13,8 @@ provider "google" { resource "google_api_gateway_api" "api" { provider = google-beta - api_id = "api-gw-dev" - display_name = "The dev API Gateway" + api_id = "reports-api" + display_name = "Reports API Gateway" project = var.project } @@ -22,17 +22,16 @@ resource "google_api_gateway_api" "api" { resource "google_api_gateway_api_config" "api_config" { provider = google-beta api = google_api_gateway_api.api.api_id - api_config_id_prefix = "api" + api_config_id_prefix = "reports_api_config_dev" project = var.project - display_name = "The dev Config" + display_name = "Reports API Config DEV" openapi_documents { document { path = "spec.yaml" contents = base64encode(<<-EOF swagger: "2.0" info: - title: reports-backend-api - description: API tech report + title: reports_api_config_dev version: 1.0.0 schemes: - https @@ -116,8 +115,8 @@ resource "google_api_gateway_gateway" "gateway" { project = var.project region = var.region api_config = google_api_gateway_api_config.api_config.id - gateway_id = "dev-gw" - display_name = "devApi Gateway" + gateway_id = "reports-dev" + display_name = "Reports API Gateway DEV" labels = { owner = "tech_report_api" environment = var.environment From f2a1ac17e32bc4c6afed4f0a15296aafa14ead04 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:52:38 +0200 Subject: [PATCH 35/44] id fix --- terraform/dev/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 3b35fde..1cd4625 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -22,7 +22,7 @@ resource "google_api_gateway_api" "api" { resource "google_api_gateway_api_config" "api_config" { provider = google-beta api = google_api_gateway_api.api.api_id - api_config_id_prefix = "reports_api_config_dev" + api_config_id_prefix = "reports-api-config-dev" project = var.project display_name = "Reports API Config DEV" openapi_documents { From a9bf5117f10d106c02aa963e4d667eb871d81937 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Wed, 4 Jun 2025 23:39:33 +0200 Subject: [PATCH 36/44] esm version --- src/__tests__/routes.test.js | 95 ++-- src/controllers/adoptionController.js | 12 +- src/controllers/categoriesController.js | 10 +- src/controllers/cwvtechController.js | 12 +- src/controllers/geosController.js | 8 +- src/controllers/lighthouseController.js | 12 +- src/controllers/pageWeightController.js | 12 +- src/controllers/ranksController.js | 8 +- src/controllers/technologiesController.js | 8 +- src/controllers/versionsController.js | 8 +- src/index.js | 92 +++- src/package-lock.json | 609 +++++++++++++++++----- src/package.json | 12 +- src/utils/controllerHelpers.js | 9 +- src/utils/db.js | 7 +- src/utils/helpers.js | 7 +- 16 files changed, 663 insertions(+), 258 deletions(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index ca87d12..e6c6728 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -1,48 +1,54 @@ -const request = require('supertest'); -const { app } = require('../index'); +import request from 'supertest'; +import { jest } from '@jest/globals'; + +// Mock the entire utils/db module using ESM-compatible mocking +jest.unstable_mockModule('../utils/db.js', () => { + const mockDoc = { + data: () => ({ + technology: 'WordPress', + category: 'CMS', + description: 'A popular content management system', + icon: 'wordpress.svg', + origins: ['WordPress Foundation'], + rank: 'ALL', + geo: 'ALL', + date: '2023-01-01' + }), + get: (field) => { + const mockData = { + technology: 'WordPress', + category: 'CMS', + rank: 'ALL', + geo: 'ALL', + date: '2023-01-01' + }; + return mockData[field] || 'Mock Value'; + } + }; + + const mockQuerySnapshot = { + empty: false, + forEach: (callback) => { + callback(mockDoc); + }, + docs: [mockDoc] + }; + + // Create a chainable query mock - avoid infinite recursion + const mockQuery = { + where: jest.fn(), + orderBy: jest.fn(), + limit: jest.fn(), + get: jest.fn().mockResolvedValue(mockQuerySnapshot) + }; + + // Make the methods chainable by returning the same mock object + mockQuery.where.mockReturnValue(mockQuery); + mockQuery.orderBy.mockReturnValue(mockQuery); + mockQuery.limit.mockReturnValue(mockQuery); -// Mock Firestore -jest.mock('../utils/db', () => { const mockFirestoreInstance = { - collection: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - get: jest.fn().mockResolvedValue({ - empty: false, - forEach: (callback) => { - // Mock technology data - callback({ - data: () => ({ - technology: 'WordPress', - category: 'CMS', - description: 'A popular content management system', - icon: 'wordpress.svg', - origins: ['WordPress Foundation'] - }), - get: (field) => { - const mockData = { - technology: 'WordPress', - category: 'CMS', - rank: 'ALL', - geo: 'ALL', - date: '2023-01-01' - }; - return mockData[field] || 'Mock Value'; - } - }); - }, - docs: [{ - data: () => ({ - technology: 'WordPress', - category: 'CMS', - description: 'A popular content management system', - icon: 'wordpress.svg', - origins: ['WordPress Foundation'], - date: '2023-01-01' - }) - }] - }) + collection: jest.fn().mockImplementation((collectionName) => mockQuery) }; return { @@ -51,6 +57,9 @@ jest.mock('../utils/db', () => { }; }); +// Import app after mocking +const { app } = await import('../index.js'); + describe('API Routes', () => { describe('Health Check', () => { it('should return a health check response', async () => { diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 5002c42..442e6ac 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -1,8 +1,8 @@ -const { firestoreOld } = require('../utils/db'); +import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -const { createSuccessResponse } = require('../utils/helpers'); -const { +import { createSuccessResponse } from '../utils/helpers.js'; +import { REQUIRED_PARAMS, validateRequiredParams, sendValidationError, @@ -10,7 +10,7 @@ const { applyStandardFilters, preprocessParams, handleControllerError -} = require('../utils/controllerHelpers'); +} from '../utils/controllerHelpers.js'; const TABLE = 'adoption'; @@ -63,6 +63,6 @@ const listAdoptionData = async (req, res) => { } }; -module.exports = { - listAdoption: listAdoptionData +export { + listAdoptionData }; diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index 5c44cbb..b873ab3 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -1,6 +1,6 @@ -const { firestore } = require('../utils/db'); -const { createSuccessResponse } = require('../utils/helpers'); -const { applyArrayFilter, selectFields, handleControllerError } = require('../utils/controllerHelpers'); +import { firestore } from '../utils/db.js'; +import { createSuccessResponse } from '../utils/helpers.js'; +import { applyArrayFilter, selectFields, handleControllerError } from '../utils/controllerHelpers.js'; /** * List categories with optional filtering and field selection @@ -42,6 +42,4 @@ const listCategories = async (req, res) => { } }; -module.exports = { - listCategories -}; +export { listCategories }; diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index caf16f6..c7b748b 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -1,8 +1,8 @@ -const { firestoreOld } = require('../utils/db'); +import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -const { createSuccessResponse } = require('../utils/helpers'); -const { +import { createSuccessResponse } from '../utils/helpers.js'; +import { REQUIRED_PARAMS, validateRequiredParams, sendValidationError, @@ -10,7 +10,7 @@ const { applyStandardFilters, preprocessParams, handleControllerError -} = require('../utils/controllerHelpers'); +} from '../utils/controllerHelpers.js'; const TABLE = 'core_web_vitals'; @@ -63,6 +63,6 @@ const listCWVTechData = async (req, res) => { } }; -module.exports = { - listCwvtech: listCWVTechData +export { + listCWVTechData }; diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index 2ce1e35..1dfa95a 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -1,6 +1,6 @@ -const { firestore } = require('../utils/db'); -const { createSuccessResponse } = require('../utils/helpers'); -const { handleControllerError } = require('../utils/controllerHelpers'); +import { firestore } from '../utils/db.js'; +import { createSuccessResponse } from '../utils/helpers.js'; +import { handleControllerError } from '../utils/controllerHelpers.js'; /** * List all geographic locations from database @@ -22,6 +22,6 @@ const listGeos = async (req, res) => { } }; -module.exports = { +export { listGeos }; diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index 6f358c6..f9f4bbc 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -1,8 +1,8 @@ -const { firestoreOld } = require('../utils/db'); +import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -const { createSuccessResponse } = require('../utils/helpers'); -const { +import { createSuccessResponse } from '../utils/helpers.js'; +import { REQUIRED_PARAMS, validateRequiredParams, sendValidationError, @@ -10,7 +10,7 @@ const { applyStandardFilters, preprocessParams, handleControllerError -} = require('../utils/controllerHelpers'); +} from '../utils/controllerHelpers.js'; const TABLE = 'lighthouse'; @@ -63,6 +63,6 @@ const listLighthouseData = async (req, res) => { } }; -module.exports = { - listLighthouse: listLighthouseData +export { + listLighthouseData }; diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index ca9fbff..4a91e61 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -1,8 +1,8 @@ -const { firestoreOld } = require('../utils/db'); +import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -const { createSuccessResponse } = require('../utils/helpers'); -const { +import { createSuccessResponse } from '../utils/helpers.js'; +import { REQUIRED_PARAMS, validateRequiredParams, sendValidationError, @@ -10,7 +10,7 @@ const { applyStandardFilters, preprocessParams, handleControllerError -} = require('../utils/controllerHelpers'); +} from '../utils/controllerHelpers.js'; const TABLE = 'page_weight'; @@ -63,6 +63,6 @@ const listPageWeightData = async (req, res) => { } }; -module.exports = { - listPageWeight: listPageWeightData +export { + listPageWeightData }; diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index ed6cd3e..1d2f7fd 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -1,6 +1,6 @@ -const { firestore } = require('../utils/db'); -const { createSuccessResponse } = require('../utils/helpers'); -const { handleControllerError } = require('../utils/controllerHelpers'); +import { firestore } from '../utils/db.js'; +import { createSuccessResponse } from '../utils/helpers.js'; +import { handleControllerError } from '../utils/controllerHelpers.js'; /** * List all rank options from database @@ -23,6 +23,6 @@ const listRanks = async (req, res) => { } }; -module.exports = { +export { listRanks }; diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index 158197d..32ac3c5 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -1,6 +1,6 @@ -const { firestore } = require('../utils/db'); -const { createSuccessResponse } = require('../utils/helpers'); -const { applyArrayFilter, selectFields, handleControllerError } = require('../utils/controllerHelpers'); +import { firestore } from '../utils/db.js'; +import { createSuccessResponse } from '../utils/helpers.js'; +import { applyArrayFilter, selectFields, handleControllerError } from '../utils/controllerHelpers.js'; // Technology Presenter - optimized with destructuring const presentTechnology = ({ technology, category, description, icon, origins }) => ({ @@ -52,6 +52,6 @@ const listTechnologies = async (req, res) => { } }; -module.exports = { +export { listTechnologies }; diff --git a/src/controllers/versionsController.js b/src/controllers/versionsController.js index 09b1934..8120e5a 100644 --- a/src/controllers/versionsController.js +++ b/src/controllers/versionsController.js @@ -1,6 +1,6 @@ -const { firestore } = require('../utils/db'); -const { createSuccessResponse } = require('../utils/helpers'); -const { applyArrayFilter, handleControllerError } = require('../utils/controllerHelpers'); +import { firestore } from '../utils/db.js'; +import { createSuccessResponse } from '../utils/helpers.js'; +import { applyArrayFilter, handleControllerError } from '../utils/controllerHelpers.js'; /** * List versions with optional technology filtering @@ -30,6 +30,6 @@ const listVersions = async (req, res) => { } }; -module.exports = { +export { listVersions }; diff --git a/src/index.js b/src/index.js index 74e362b..e66e1cc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,18 +1,56 @@ -const http = require('http'); -const url = require('url'); -const crypto = require('crypto'); -const functions = require('@google-cloud/functions-framework'); - -// Import controllers -const { listTechnologies } = require('./controllers/technologiesController'); -const { listCategories } = require('./controllers/categoriesController'); -const { listAdoption } = require('./controllers/adoptionController'); -const { listCwvtech } = require('./controllers/cwvtechController'); -const { listLighthouse } = require('./controllers/lighthouseController'); -const { listPageWeight } = require('./controllers/pageWeightController'); -const { listRanks } = require('./controllers/ranksController'); -const { listGeos } = require('./controllers/geosController'); -const { listVersions } = require('./controllers/versionsController'); +import http from 'http'; +import url from 'url'; +import crypto from 'crypto'; +import functions from '@google-cloud/functions-framework'; + +// Dynamic imports for better performance - only load when needed +const controllers = { + technologies: null, + categories: null, + adoption: null, + cwvtech: null, + lighthouse: null, + pageWeight: null, + ranks: null, + geos: null, + versions: null +}; + +// Helper function to dynamically import controllers +const getController = async (name) => { + if (!controllers[name]) { + switch (name) { + case 'technologies': + controllers[name] = await import('./controllers/technologiesController.js'); + break; + case 'categories': + controllers[name] = await import('./controllers/categoriesController.js'); + break; + case 'adoption': + controllers[name] = await import('./controllers/adoptionController.js'); + break; + case 'cwvtech': + controllers[name] = await import('./controllers/cwvtechController.js'); + break; + case 'lighthouse': + controllers[name] = await import('./controllers/lighthouseController.js'); + break; + case 'pageWeight': + controllers[name] = await import('./controllers/pageWeightController.js'); + break; + case 'ranks': + controllers[name] = await import('./controllers/ranksController.js'); + break; + case 'geos': + controllers[name] = await import('./controllers/geosController.js'); + break; + case 'versions': + controllers[name] = await import('./controllers/versionsController.js'); + break; + } + } + return controllers[name]; +}; // Helper function to set CORS headers const setCORSHeaders = (res) => { @@ -87,22 +125,31 @@ const handleRequest = async (req, res) => { const data = { status: 'ok' }; sendJSONResponse(res, data); } else if (pathname === '/v1/technologies' && req.method === 'GET') { + const { listTechnologies } = await getController('technologies'); await listTechnologies(req, res); } else if (pathname === '/v1/categories' && req.method === 'GET') { + const { listCategories } = await getController('categories'); await listCategories(req, res); } else if (pathname === '/v1/adoption' && req.method === 'GET') { - await listAdoption(req, res); + const { listAdoptionData } = await getController('adoption'); + await listAdoptionData(req, res); } else if (pathname === '/v1/cwv' && req.method === 'GET') { - await listCwvtech(req, res); + const { listCWVTechData } = await getController('cwvtech'); + await listCWVTechData(req, res); } else if (pathname === '/v1/lighthouse' && req.method === 'GET') { - await listLighthouse(req, res); + const { listLighthouseData } = await getController('lighthouse'); + await listLighthouseData(req, res); } else if (pathname === '/v1/page-weight' && req.method === 'GET') { - await listPageWeight(req, res); + const { listPageWeightData } = await getController('pageWeight'); + await listPageWeightData(req, res); } else if (pathname === '/v1/ranks' && req.method === 'GET') { + const { listRanks } = await getController('ranks'); await listRanks(req, res); } else if (pathname === '/v1/geos' && req.method === 'GET') { + const { listGeos } = await getController('geos'); await listGeos(req, res); } else if (pathname === '/v1/versions' && req.method === 'GET') { + const { listVersions } = await getController('versions'); await listVersions(req, res); } else { // 404 Not Found @@ -122,13 +169,16 @@ const handleRequest = async (req, res) => { const server = http.createServer(handleRequest); // Export the server for testing -exports.app = server; +export { server as app }; // Register with Functions Framework for Cloud Functions functions.http('app', handleRequest); // For standalone server mode (local development) -if (require.main === module) { +// Note: In ES modules, there's no require.main === module equivalent +// We'll use import.meta.url to check if this is the main module +const isMain = import.meta.url === `file://${process.argv[1]}`; +if (isMain) { const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); diff --git a/src/package-lock.json b/src/package-lock.json index 070b4e1..89b6733 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8,10 +8,11 @@ "name": "tech-report-api", "version": "1.0.0", "dependencies": { - "@google-cloud/firestore": "7.3.0" + "@google-cloud/firestore": "7.3.0", + "@google-cloud/functions-framework": "^4.0.0" }, "devDependencies": { - "@google-cloud/functions-framework": "^4.0.0", + "@jest/transform": "^30.0.0-beta.3", "jest": "29.7.0", "nodemon": "3.0.1", "supertest": "^7.1.0" @@ -38,7 +39,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -205,7 +205,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -589,7 +588,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-4.0.0.tgz", "integrity": "sha512-CNcYrz0/hw35Oq0D9RipHUB8KzH4ixq7o12L//qoOg0TFYv4953KrzCo0L2VP++19P39RShKTftDKMFmQhCeEw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@types/express": "^4.17.21", @@ -613,7 +611,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -746,6 +743,47 @@ } } }, + "node_modules/@jest/core/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -823,6 +861,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.0-beta.3.tgz", + "integrity": "sha512-IuB9mweyJI5ToVBRdptKb2w97LGnNHFI+V9/cGaYeFareL7BYD6KiUH022OC51K1841c6YzgYjyQmJHFxELZSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.0-beta.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz", + "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -867,6 +929,47 @@ } } }, + "node_modules/@jest/reporters/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -928,30 +1031,201 @@ } }, "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.0-beta.3.tgz", + "integrity": "sha512-2gixxaYdRh3MQaRsEenHejw0qBIW72DfwG1q9HPLXpnLkm5TKZlTOvOS33S00PGEoa4UG1Iq9tNHh7fxOJAGwQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", + "@jest/types": "30.0.0-beta.3", "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", + "babel-plugin-istanbul": "^7.0.0", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", + "jest-haste-map": "30.0.0-beta.3", + "jest-regex-util": "30.0.0-beta.3", + "jest-util": "30.0.0-beta.3", + "micromatch": "^4.0.8", "pirates": "^4.0.4", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "write-file-atomic": "^5.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jest/schemas": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.0-beta.3.tgz", + "integrity": "sha512-tiT79EKOlJGT5v8fYr9UKLSyjlA3Ek+nk0cVZwJGnRqVp26EQSOTYXBCzj0dGMegkgnPTt3f7wP1kGGI8q/e0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jest/types": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.0-beta.3.tgz", + "integrity": "sha512-x7GyHD8rxZ4Ygmp4rea3uPDIPZ6Jglcglaav8wQNqXsVUAByapDwLF52Cp3wEYMPMnvH4BicEj56j8fqZx5jng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.0-beta.3", + "@jest/schemas": "30.0.0-beta.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@sinclair/typebox": { + "version": "0.34.33", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", + "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/transform/node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/jest-haste-map": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.0-beta.3.tgz", + "integrity": "sha512-MafsVPIca9E4HR3Fp9gYX+AET4YZmU/VtyLcnRJ9QHdVqHSCzOaElxX30BlyNf5Nw6ZcCafkbB0RGXqSwwsjxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "30.0.0-beta.3", + "jest-util": "30.0.0-beta.3", + "jest-worker": "30.0.0-beta.3", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/@jest/transform/node_modules/jest-regex-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.0-beta.3.tgz", + "integrity": "sha512-kiDaZ35ogPivxgLEGJ1jNW2KBtvmPwGlPjy5ASHiVE3kjn3g80galEIcWC0hZV6g5BtTx15VKzSyfOTiKXPnxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/transform/node_modules/jest-util": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.0-beta.3.tgz", + "integrity": "sha512-kob8YNaO1UPrG0TgGdH5l0ciNGuXDX93Yn2b2VCkALuqOXbqzT2xCr6O7dBuwhM7tmzBbpM6CkcK7Qyf/JmLZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.0-beta.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^4.0.0", + "graceful-fs": "^4.2.9", + "picomatch": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/transform/node_modules/jest-worker": { + "version": "30.0.0-beta.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.0-beta.3.tgz", + "integrity": "sha512-v17y4Jg9geh3tDm8aU2snuwr8oCJtFefuuPrMRqmC6Ew8K+sLfOcuB3moJ15PHoe4MjTGgsC1oO2PK/GaF1vTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.2.0", + "jest-util": "30.0.0-beta.3", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/@jest/transform/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/@jest/types": { @@ -1207,7 +1481,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1224,7 +1497,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1234,7 +1506,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1247,7 +1518,6 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1270,7 +1540,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -1310,7 +1579,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1326,21 +1594,18 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/request": { @@ -1359,7 +1624,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1370,7 +1634,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -1408,6 +1671,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1424,7 +1694,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -1447,7 +1716,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1464,7 +1732,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -1546,7 +1813,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, "license": "MIT" }, "node_modules/asap": { @@ -1566,7 +1832,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -1600,6 +1865,47 @@ "@babel/core": "^7.8.0" } }, + "node_modules/babel-jest/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-jest/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -1747,7 +2053,6 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -1852,7 +2157,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -1862,7 +2166,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -1894,7 +2197,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2041,7 +2343,6 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-8.0.3.tgz", "integrity": "sha512-wTixKNjfLeyj9HQpESvLVVO4xgdqdvX4dTeg1IZ2SCunu/fxVzCamcIZneEyj31V82YolFCKwVeSkr8zResB0Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "ajv": "^8.11.0", @@ -2059,7 +2360,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -2134,7 +2434,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -2147,7 +2446,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2164,7 +2462,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2174,7 +2471,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, "license": "MIT" }, "node_modules/cookiejar": { @@ -2225,7 +2521,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -2260,7 +2555,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -2287,7 +2581,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2297,7 +2590,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -2374,7 +2666,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -2407,7 +2698,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2490,7 +2780,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -2521,7 +2810,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2590,7 +2878,6 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -2663,7 +2950,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, "funding": [ { "type": "github", @@ -2703,7 +2989,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -2736,7 +3021,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2749,7 +3033,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -2799,7 +3082,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2809,7 +3091,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3101,7 +3382,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -3153,7 +3433,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" @@ -3166,7 +3445,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/html-escaper": { @@ -3180,7 +3458,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -3292,7 +3569,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -3342,7 +3618,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3373,7 +3648,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -3383,7 +3657,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3420,7 +3693,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3478,7 +3750,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -3520,7 +3791,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3551,7 +3821,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -4095,6 +4364,47 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runner/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/jest-runtime": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", @@ -4129,6 +4439,47 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runtime/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", @@ -4161,6 +4512,33 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-snapshot/node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -4174,6 +4552,20 @@ "node": ">=10" } }, + "node_modules/jest-snapshot/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -4279,7 +4671,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4329,7 +4720,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -4480,7 +4870,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4490,7 +4879,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4507,7 +4895,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4531,7 +4918,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -4588,7 +4974,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4598,7 +4983,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -4612,7 +4996,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4738,7 +5121,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^7.0.0", @@ -4753,7 +5135,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4798,7 +5179,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4811,7 +5191,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -4923,7 +5302,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -4970,14 +5348,12 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5020,7 +5396,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5058,7 +5433,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -5118,7 +5492,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -5156,7 +5529,6 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -5172,7 +5544,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5182,7 +5553,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -5205,7 +5575,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, "license": "MIT", "dependencies": { "find-up-simple": "^1.0.0", @@ -5223,7 +5592,6 @@ "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -5236,7 +5604,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.3", @@ -5256,7 +5623,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -5274,7 +5640,6 @@ "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -5323,7 +5688,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5421,7 +5785,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5439,7 +5802,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -5456,7 +5818,6 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -5481,7 +5842,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5491,14 +5851,12 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -5514,7 +5872,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -5532,7 +5889,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/shebang-command": { @@ -5562,7 +5918,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5582,7 +5937,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5599,7 +5953,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5618,7 +5971,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5709,7 +6061,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -5720,14 +6071,12 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -5738,7 +6087,6 @@ "version": "3.0.21", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, "license": "CC0-1.0" }, "node_modules/sprintf-js": { @@ -5765,7 +6113,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6092,7 +6439,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -6141,7 +6487,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -6168,7 +6513,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6181,7 +6525,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6222,7 +6565,6 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -6242,7 +6584,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -6280,7 +6621,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -6291,7 +6631,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6343,7 +6682,6 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -6385,17 +6723,30 @@ "license": "ISC" }, "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "signal-exit": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/y18n": { diff --git a/src/package.json b/src/package.json index 0c2423f..7be7bfd 100644 --- a/src/package.json +++ b/src/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "API for HTTP Archive technology reports", "main": "index.js", + "type": "module", "engines": { "node": ">=22.0.0" }, @@ -10,7 +11,7 @@ "start": "node index.js", "start:functions": "export DATABASE=tech-report-api-prod && functions-framework --target=app", "dev": "nodemon index.js", - "test": "jest", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "test:live": "bash ../test-api.sh" }, "dependencies": { @@ -18,6 +19,7 @@ "@google-cloud/functions-framework": "^4.0.0" }, "devDependencies": { + "@jest/transform": "^30.0.0-beta.3", "jest": "29.7.0", "nodemon": "3.0.1", "supertest": "^7.1.0" @@ -38,6 +40,12 @@ "testMatch": [ "**/__tests__/**/*.js", "**/?(*.)+(spec|test).js" - ] + ], + "globals": { + "__filename": false, + "__dirname": false + }, + "transformIgnorePatterns": ["node_modules/(?!(.*\\.mjs$))"], + "transform": {} } } diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js index ffd2875..6519591 100644 --- a/src/utils/controllerHelpers.js +++ b/src/utils/controllerHelpers.js @@ -1,4 +1,4 @@ -const { createErrorResponse } = require('./helpers'); +import { createErrorResponse, convertToArray } from './helpers.js'; /** * Common parameter validation patterns @@ -112,7 +112,7 @@ const preprocessParams = async (firestore, params, collection) => { } // Handle version 'ALL' special case for multiple technologies - const techArray = require('./helpers').convertToArray(params.technology); + const techArray = convertToArray(params.technology); if (!params.version || techArray.length > 1) { params.version = 'ALL'; } @@ -130,8 +130,6 @@ const preprocessParams = async (firestore, params, collection) => { */ const applyArrayFilter = (query, field, value, operator = 'in') => { if (!value) return query; - - const { convertToArray } = require('./helpers'); const valueArray = convertToArray(value); if (valueArray.length > 0) { @@ -150,7 +148,6 @@ const applyArrayFilter = (query, field, value, operator = 'in') => { const selectFields = (data, fieldsParam) => { if (!fieldsParam) return data; - const { convertToArray } = require('./helpers'); const fields = convertToArray(fieldsParam); if (fields.length === 0) return data; @@ -179,7 +176,7 @@ const handleControllerError = (res, error, operation) => { })); }; -module.exports = { +export { REQUIRED_PARAMS, validateRequiredParams, sendValidationError, diff --git a/src/utils/db.js b/src/utils/db.js index 41941bb..41eed29 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -1,4 +1,4 @@ -const { Firestore } = require('@google-cloud/firestore'); +import { Firestore } from '@google-cloud/firestore'; // Initialize Firestore with basic optimizations (default connection using env variables) const firestore = new Firestore({ @@ -29,7 +29,4 @@ const firestoreOld = new Firestore({ }); // Export both connections - maintain backward compatibility -module.exports = { - firestore, - firestoreOld -}; +export { firestore, firestoreOld }; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index a6a426b..4033178 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -45,9 +45,4 @@ const createErrorResponse = (errors) => { }; }; -module.exports = { - convertToArray, - convertToHashes, - createSuccessResponse, - createErrorResponse -}; +export { convertToArray, convertToHashes, createSuccessResponse, createErrorResponse }; From c0bcaa70506b13666d9ca4533e9527f118159003 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:16:02 +0200 Subject: [PATCH 37/44] cached latest --- perf_lab_test/main.js | 6 +++--- src/utils/controllerHelpers.js | 29 ++++++++++++++++++++++++++--- test-api.sh | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/perf_lab_test/main.js b/perf_lab_test/main.js index 6495584..4ce5ca1 100644 --- a/perf_lab_test/main.js +++ b/perf_lab_test/main.js @@ -1,11 +1,11 @@ const axios = require('axios'); const ENDPOINTS = { - A: 'https://prod-gw-2vzgiib6.ue.gateway.dev/v1/technologies', - B: 'https://dev-gw-2vzgiib6.uc.gateway.dev/v1/technologies' + A: 'https://prod-gw-2vzgiib6.ue.gateway.dev/v1/cwv?technology=WordPress,Shopify,Wix,Joomla,Drupal,Squarespace,PrestaShop,Webflow,1C-Bitrix,Tilda&geo=United%20States%20of%20America&rank=Top%20100k&start=latest', + B: 'https://reports-dev-2vzgiib6.uc.gateway.dev/v1/cwv?technology=WordPress,Shopify,Wix,Joomla,Drupal,Squarespace,PrestaShop,Webflow,1C-Bitrix,Tilda&geo=United%20States%20of%20America&rank=Top%20100k&start=latest' }; -const NUM_REQUESTS = 200; +const NUM_REQUESTS = 100; const CONCURRENCY = 10; const MAX_JITTER_MS = 100; diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js index 6519591..fd673fc 100644 --- a/src/utils/controllerHelpers.js +++ b/src/utils/controllerHelpers.js @@ -38,19 +38,42 @@ const sendValidationError = (res, errors) => { res.end(JSON.stringify(createErrorResponse(errors))); }; +// Cache for latest dates to avoid repeated queries +const latestDateCache = new Map(); +const CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds + /** - * Get the latest date from a collection + * Get the latest date from a collection with caching * @param {Object} firestore - Firestore instance * @param {string} collection - Collection name * @returns {string|null} - Latest date or null */ const getLatestDate = async (firestore, collection) => { + const now = Date.now(); + const cacheKey = collection; + const cached = latestDateCache.get(cacheKey); + + // Check if we have a valid cached result + if (cached && (now - cached.timestamp) < CACHE_TTL) { + return cached.date; + } + + // Query for latest date const query = firestore.collection(collection).orderBy('date', 'desc').limit(1); const snapshot = await query.get(); + + let latestDate = null; if (!snapshot.empty) { - return snapshot.docs[0].data().date; + latestDate = snapshot.docs[0].data().date; } - return null; + + // Cache the result + latestDateCache.set(cacheKey, { + date: latestDate, + timestamp: now + }); + + return latestDate; }; /** diff --git a/test-api.sh b/test-api.sh index b373a00..96e4651 100755 --- a/test-api.sh +++ b/test-api.sh @@ -104,7 +104,7 @@ test_endpoint "/v1/geos" "" test_endpoint "/v1/adoption" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" # Test cwv endpoint -test_endpoint "/v1/cwv" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" +test_endpoint "/v1/cwv" "?technology=WordPress,Drupal&geo=ALL&rank=ALL&start=latest" # Test lighthouse endpoint test_endpoint "/v1/lighthouse" "?technology=WordPress&geo=ALL&rank=ALL&start=latest" From c0e696d480cefd2a7e77654ed8d17c097cd1ca91 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:23:12 +0200 Subject: [PATCH 38/44] deduped tech requests --- src/controllers/cwvtechController.js | 105 ++++++++++++++++++--------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index c7b748b..61267f9 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -1,65 +1,98 @@ import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -import { createSuccessResponse } from '../utils/helpers.js'; import { - REQUIRED_PARAMS, - validateRequiredParams, - sendValidationError, - applyDateFilters, - applyStandardFilters, - preprocessParams, - handleControllerError + getLatestDate } from '../utils/controllerHelpers.js'; const TABLE = 'core_web_vitals'; /** - * List Core Web Vitals data with filtering + * List Core Web Vitals data with filtering - Optimized version */ const listCWVTechData = async (req, res) => { try { const params = req.query; - // Validate required parameters - const requiredParams = [ - REQUIRED_PARAMS.GEO, - REQUIRED_PARAMS.RANK, - REQUIRED_PARAMS.TECHNOLOGY - ]; - - const validationErrors = validateRequiredParams(params, requiredParams); - if (validationErrors) { - sendValidationError(res, validationErrors); + // Validate required parameters inline for speed + if (!params.geo || !params.rank || !params.technology) { + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [ + ...(!params.geo ? [{ geo: 'missing geo parameter' }] : []), + ...(!params.rank ? [{ rank: 'missing rank parameter' }] : []), + ...(!params.technology ? [{ technology: 'missing technology parameter' }] : []) + ] + })); return; } - // Preprocess parameters and get technology array - const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); - const data = []; + // Fast preprocessing - handle 'latest' date and technology array + const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; + + // Handle 'latest' date with caching + let startDate = params.start; + if (startDate === 'latest') { + startDate = await getLatestDate(firestore, TABLE); + } + + // Build optimized query + let query = firestore.collection(TABLE); - // Query for each technology - for (const technology of techArray) { - let query = firestore.collection(TABLE); + // Apply required filters + query = query.where('geo', '==', params.geo); + query = query.where('rank', '==', params.rank); - // Apply standard filters including version filter - query = applyStandardFilters(query, processedParams, technology, techArray); + // Apply technology filter efficiently + if (techArray.length <= 30) { + // Use 'in' operator for batch processing (Firestore limit: 30 values) + query = query.where('technology', 'in', techArray); + } else { + // Parallel queries for >10 technologies (rare case) + const queryPromises = techArray.map(async (technology) => { + let individualQuery = firestore.collection(TABLE) + .where('geo', '==', params.geo) + .where('rank', '==', params.rank) + .where('technology', '==', technology); - // Apply date filters - query = applyDateFilters(query, processedParams); + if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); + if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - // Execute query - const snapshot = await query.get(); - snapshot.forEach(doc => { - data.push(doc.data()); + const snapshot = await individualQuery.get(); + const results = []; + snapshot.forEach(doc => results.push(doc.data())); + return results; }); + + const results = await Promise.all(queryPromises); + const data = results.flat(); + + res.statusCode = 200; + res.end(JSON.stringify(data)); + return; } - // Send response + // Apply date filters + if (startDate) query = query.where('date', '>=', startDate); + if (params.end) query = query.where('date', '<=', params.end); + + // Execute single optimized query + const snapshot = await query.get(); + const data = []; + snapshot.forEach(doc => { + data.push(doc.data()); + }); + + // Direct response without wrapper functions res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { - handleControllerError(res, error, 'fetching Core Web Vitals data'); + console.error('Error fetching Core Web Vitals data:', error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: 'Failed to fetch Core Web Vitals data' }] + })); } }; From 14328bf870fc937ecc313a2cf9ed76b825aafbf2 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:46:21 +0200 Subject: [PATCH 39/44] query cache --- src/controllers/cwvtechController.js | 31 ++++++++++++++++++-- src/utils/controllerHelpers.js | 44 +++++++++++++++++++++++++++- src/utils/helpers.js | 2 +- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index 61267f9..fe7c7cb 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -2,7 +2,10 @@ import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; import { - getLatestDate + getLatestDate, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult } from '../utils/controllerHelpers.js'; const TABLE = 'core_web_vitals'; @@ -37,6 +40,24 @@ const listCWVTechData = async (req, res) => { startDate = await getLatestDate(firestore, TABLE); } + // Create cache key for this specific query + const queryFilters = { + geo: params.geo, + rank: params.rank, + technology: techArray, + startDate: startDate, + endDate: params.end + }; + const cacheKey = generateQueryCacheKey(TABLE, queryFilters); + + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); + return; + } + // Build optimized query let query = firestore.collection(TABLE); @@ -49,7 +70,7 @@ const listCWVTechData = async (req, res) => { // Use 'in' operator for batch processing (Firestore limit: 30 values) query = query.where('technology', 'in', techArray); } else { - // Parallel queries for >10 technologies (rare case) + // Parallel queries for >30 technologies (rare case) const queryPromises = techArray.map(async (technology) => { let individualQuery = firestore.collection(TABLE) .where('geo', '==', params.geo) @@ -68,6 +89,9 @@ const listCWVTechData = async (req, res) => { const results = await Promise.all(queryPromises); const data = results.flat(); + // Cache the result + setCachedQueryResult(cacheKey, data); + res.statusCode = 200; res.end(JSON.stringify(data)); return; @@ -84,6 +108,9 @@ const listCWVTechData = async (req, res) => { data.push(doc.data()); }); + // Cache the result + setCachedQueryResult(cacheKey, data); + // Direct response without wrapper functions res.statusCode = 200; res.end(JSON.stringify(data)); diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js index fd673fc..f6dceba 100644 --- a/src/utils/controllerHelpers.js +++ b/src/utils/controllerHelpers.js @@ -42,6 +42,45 @@ const sendValidationError = (res, errors) => { const latestDateCache = new Map(); const CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds +// Cache for query results to eliminate bimodal performance +const queryResultCache = new Map(); +const QUERY_CACHE_TTL = 10 * 60 * 1000; // 10 minutes for query results + +/** + * Generate a cache key for a query + * @param {string} collection - Collection name + * @param {Object} filters - Query filters + * @returns {string} - Cache key + */ +const generateQueryCacheKey = (collection, filters) => { + return `${collection}:${JSON.stringify(filters)}`; +}; + +/** + * Get cached query result if available and not expired + * @param {string} cacheKey - Cache key + * @returns {Array|null} - Cached result or null + */ +const getCachedQueryResult = (cacheKey) => { + const cached = queryResultCache.get(cacheKey); + if (cached && (Date.now() - cached.timestamp) < QUERY_CACHE_TTL) { + return cached.data; + } + return null; +}; + +/** + * Cache a query result + * @param {string} cacheKey - Cache key + * @param {Array} data - Query result data + */ +const setCachedQueryResult = (cacheKey, data) => { + queryResultCache.set(cacheKey, { + data: data, + timestamp: Date.now() + }); +}; + /** * Get the latest date from a collection with caching * @param {Object} firestore - Firestore instance @@ -209,5 +248,8 @@ export { preprocessParams, applyArrayFilter, selectFields, - handleControllerError + handleControllerError, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult }; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 4033178..d4616c3 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -45,4 +45,4 @@ const createErrorResponse = (errors) => { }; }; -export { convertToArray, convertToHashes, createSuccessResponse, createErrorResponse }; +export { convertToArray, createSuccessResponse, createErrorResponse }; From a925eae803bd3ad57a6f8de0b211a7dc23e76af9 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 01:14:40 +0200 Subject: [PATCH 40/44] internal caching --- src/controllers/adoptionController.js | 130 ++++++++++++++++------ src/controllers/categoriesController.js | 40 ++++++- src/controllers/geosController.js | 19 +++- src/controllers/lighthouseController.js | 130 ++++++++++++++++------ src/controllers/pageWeightController.js | 130 ++++++++++++++++------ src/controllers/ranksController.js | 19 +++- src/controllers/technologiesController.js | 41 ++++++- src/controllers/versionsController.js | 59 +++++++++- src/utils/controllerHelpers.js | 15 ++- src/utils/helpers.js | 22 +--- 10 files changed, 454 insertions(+), 151 deletions(-) diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 442e6ac..472b8eb 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -1,65 +1,125 @@ import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -import { createSuccessResponse } from '../utils/helpers.js'; import { - REQUIRED_PARAMS, - validateRequiredParams, - sendValidationError, - applyDateFilters, - applyStandardFilters, - preprocessParams, - handleControllerError + getLatestDate, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult } from '../utils/controllerHelpers.js'; const TABLE = 'adoption'; /** - * List adoption data with filtering + * List adoption data with filtering - Optimized version */ const listAdoptionData = async (req, res) => { try { const params = req.query; - // Validate required parameters - const requiredParams = [ - REQUIRED_PARAMS.GEO, - REQUIRED_PARAMS.RANK, - REQUIRED_PARAMS.TECHNOLOGY - ]; + // Validate required parameters inline for speed + if (!params.geo || !params.rank || !params.technology) { + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [ + ...(!params.geo ? [{ geo: 'missing geo parameter' }] : []), + ...(!params.rank ? [{ rank: 'missing rank parameter' }] : []), + ...(!params.technology ? [{ technology: 'missing technology parameter' }] : []) + ] + })); + return; + } + + // Fast preprocessing - handle 'latest' date and technology array + const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; + + // Handle 'latest' date with caching + let startDate = params.start; + if (startDate === 'latest') { + startDate = await getLatestDate(firestore, TABLE); + } + + // Create cache key for this specific query + const queryFilters = { + geo: params.geo, + rank: params.rank, + technology: techArray, + startDate: startDate, + endDate: params.end + }; + const cacheKey = generateQueryCacheKey(TABLE, queryFilters); - const validationErrors = validateRequiredParams(params, requiredParams); - if (validationErrors) { - sendValidationError(res, validationErrors); + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); return; } - // Preprocess parameters and get technology array - const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); - const data = []; + // Build optimized query + let query = firestore.collection(TABLE); - // Query for each technology - for (const technology of techArray) { - let query = firestore.collection(TABLE); + // Apply required filters + query = query.where('geo', '==', params.geo); + query = query.where('rank', '==', params.rank); - // Apply standard filters including version filter - query = applyStandardFilters(query, processedParams, technology, techArray); + // Apply technology filter efficiently + if (techArray.length <= 30) { + // Use 'in' operator for batch processing (Firestore limit: 30 values) + query = query.where('technology', 'in', techArray); + } else { + // Parallel queries for >30 technologies (rare case) + const queryPromises = techArray.map(async (technology) => { + let individualQuery = firestore.collection(TABLE) + .where('geo', '==', params.geo) + .where('rank', '==', params.rank) + .where('technology', '==', technology); - // Apply date filters - query = applyDateFilters(query, processedParams); + if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); + if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - // Execute query - const snapshot = await query.get(); - snapshot.forEach(doc => { - data.push(doc.data()); + const snapshot = await individualQuery.get(); + const results = []; + snapshot.forEach(doc => results.push(doc.data())); + return results; }); + + const results = await Promise.all(queryPromises); + const data = results.flat(); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + res.statusCode = 200; + res.end(JSON.stringify(data)); + return; } - // Send response + // Apply date filters + if (startDate) query = query.where('date', '>=', startDate); + if (params.end) query = query.where('date', '<=', params.end); + + // Execute single optimized query + const snapshot = await query.get(); + const data = []; + snapshot.forEach(doc => { + data.push(doc.data()); + }); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + // Direct response without wrapper functions res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { - handleControllerError(res, error, 'fetching adoption data'); + console.error('Error fetching adoption data:', error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: 'Failed to fetch adoption data' }] + })); } }; diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index b873ab3..da76b2b 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -1,9 +1,14 @@ import { firestore } from '../utils/db.js'; -import { createSuccessResponse } from '../utils/helpers.js'; -import { applyArrayFilter, selectFields, handleControllerError } from '../utils/controllerHelpers.js'; +import { + applyArrayFilter, + selectFields, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult +} from '../utils/controllerHelpers.js'; /** - * List categories with optional filtering and field selection + * List categories with optional filtering and field selection - Optimized version */ const listCategories = async (req, res) => { try { @@ -11,6 +16,22 @@ const listCategories = async (req, res) => { const isOnlyNames = params.onlyname || typeof params.onlyname === 'string'; const hasCustomFields = params.fields && !isOnlyNames; + // Create cache key for this specific query + const queryFilters = { + category: params.category, + onlyname: isOnlyNames, + fields: params.fields + }; + const cacheKey = generateQueryCacheKey('categories', queryFilters); + + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); + return; + } + let query = firestore.collection('categories').orderBy('category', 'asc'); // Apply category filter using shared utility @@ -34,11 +55,18 @@ const listCategories = async (req, res) => { } }); - // Send response + // Cache the result + setCachedQueryResult(cacheKey, data); + + // Direct response res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { - handleControllerError(res, error, 'fetching categories'); + console.error('Error fetching categories:', error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: 'Failed to fetch categories' }] + })); } }; diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index 1dfa95a..b1da98d 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -1,12 +1,22 @@ import { firestore } from '../utils/db.js'; -import { createSuccessResponse } from '../utils/helpers.js'; -import { handleControllerError } from '../utils/controllerHelpers.js'; +import { handleControllerError, generateQueryCacheKey, getCachedQueryResult, setCachedQueryResult } from '../utils/controllerHelpers.js'; /** * List all geographic locations from database */ const listGeos = async (req, res) => { try { + // Generate cache key for this query + const cacheKey = generateQueryCacheKey('geos', { orderBy: 'mobile_origins' }); + + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); + return; + } + const snapshot = await firestore.collection('geos').orderBy('mobile_origins', 'desc').get(); const data = []; @@ -15,8 +25,11 @@ const listGeos = async (req, res) => { data.push({ geo: doc.data().geo }); }); + // Cache the result + setCachedQueryResult(cacheKey, data); + res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { handleControllerError(res, error, 'fetching geographic locations'); } diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index f9f4bbc..34b6d88 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -1,65 +1,125 @@ import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -import { createSuccessResponse } from '../utils/helpers.js'; import { - REQUIRED_PARAMS, - validateRequiredParams, - sendValidationError, - applyDateFilters, - applyStandardFilters, - preprocessParams, - handleControllerError + getLatestDate, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult } from '../utils/controllerHelpers.js'; const TABLE = 'lighthouse'; /** - * List Lighthouse data with filtering + * List Lighthouse data with filtering - Optimized version */ const listLighthouseData = async (req, res) => { try { const params = req.query; - // Validate required parameters - const requiredParams = [ - REQUIRED_PARAMS.GEO, - REQUIRED_PARAMS.RANK, - REQUIRED_PARAMS.TECHNOLOGY - ]; + // Validate required parameters inline for speed + if (!params.geo || !params.rank || !params.technology) { + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [ + ...(!params.geo ? [{ geo: 'missing geo parameter' }] : []), + ...(!params.rank ? [{ rank: 'missing rank parameter' }] : []), + ...(!params.technology ? [{ technology: 'missing technology parameter' }] : []) + ] + })); + return; + } + + // Fast preprocessing - handle 'latest' date and technology array + const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; + + // Handle 'latest' date with caching + let startDate = params.start; + if (startDate === 'latest') { + startDate = await getLatestDate(firestore, TABLE); + } + + // Create cache key for this specific query + const queryFilters = { + geo: params.geo, + rank: params.rank, + technology: techArray, + startDate: startDate, + endDate: params.end + }; + const cacheKey = generateQueryCacheKey(TABLE, queryFilters); - const validationErrors = validateRequiredParams(params, requiredParams); - if (validationErrors) { - sendValidationError(res, validationErrors); + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); return; } - // Preprocess parameters and get technology array - const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); - const data = []; + // Build optimized query + let query = firestore.collection(TABLE); - // Query for each technology - for (const technology of techArray) { - let query = firestore.collection(TABLE); + // Apply required filters + query = query.where('geo', '==', params.geo); + query = query.where('rank', '==', params.rank); - // Apply date filters first - query = applyDateFilters(query, processedParams); + // Apply technology filter efficiently + if (techArray.length <= 30) { + // Use 'in' operator for batch processing (Firestore limit: 30 values) + query = query.where('technology', 'in', techArray); + } else { + // Parallel queries for >30 technologies (rare case) + const queryPromises = techArray.map(async (technology) => { + let individualQuery = firestore.collection(TABLE) + .where('geo', '==', params.geo) + .where('rank', '==', params.rank) + .where('technology', '==', technology); - // Apply standard filters and version filter - query = applyStandardFilters(query, processedParams, technology, techArray); + if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); + if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - // Execute query - const snapshot = await query.get(); - snapshot.forEach(doc => { - data.push(doc.data()); + const snapshot = await individualQuery.get(); + const results = []; + snapshot.forEach(doc => results.push(doc.data())); + return results; }); + + const results = await Promise.all(queryPromises); + const data = results.flat(); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + res.statusCode = 200; + res.end(JSON.stringify(data)); + return; } - // Send response + // Apply date filters + if (startDate) query = query.where('date', '>=', startDate); + if (params.end) query = query.where('date', '<=', params.end); + + // Execute single optimized query + const snapshot = await query.get(); + const data = []; + snapshot.forEach(doc => { + data.push(doc.data()); + }); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + // Direct response without wrapper functions res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { - handleControllerError(res, error, 'fetching Lighthouse data'); + console.error('Error fetching Lighthouse data:', error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: 'Failed to fetch Lighthouse data' }] + })); } }; diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index 4a91e61..26afe77 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -1,65 +1,125 @@ import { firestoreOld } from '../utils/db.js'; const firestore = firestoreOld; -import { createSuccessResponse } from '../utils/helpers.js'; import { - REQUIRED_PARAMS, - validateRequiredParams, - sendValidationError, - applyDateFilters, - applyStandardFilters, - preprocessParams, - handleControllerError + getLatestDate, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult } from '../utils/controllerHelpers.js'; const TABLE = 'page_weight'; /** - * List Page Weight data with filtering + * List Page Weight data with filtering - Optimized version */ const listPageWeightData = async (req, res) => { try { const params = req.query; - // Validate required parameters - const requiredParams = [ - REQUIRED_PARAMS.GEO, - REQUIRED_PARAMS.RANK, - REQUIRED_PARAMS.TECHNOLOGY - ]; + // Validate required parameters inline for speed + if (!params.geo || !params.rank || !params.technology) { + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [ + ...(!params.geo ? [{ geo: 'missing geo parameter' }] : []), + ...(!params.rank ? [{ rank: 'missing rank parameter' }] : []), + ...(!params.technology ? [{ technology: 'missing technology parameter' }] : []) + ] + })); + return; + } + + // Fast preprocessing - handle 'latest' date and technology array + const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; + + // Handle 'latest' date with caching + let startDate = params.start; + if (startDate === 'latest') { + startDate = await getLatestDate(firestore, TABLE); + } + + // Create cache key for this specific query + const queryFilters = { + geo: params.geo, + rank: params.rank, + technology: techArray, + startDate: startDate, + endDate: params.end + }; + const cacheKey = generateQueryCacheKey(TABLE, queryFilters); - const validationErrors = validateRequiredParams(params, requiredParams); - if (validationErrors) { - sendValidationError(res, validationErrors); + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); return; } - // Preprocess parameters and get technology array - const { params: processedParams, techArray } = await preprocessParams(firestore, params, TABLE); - const data = []; + // Build optimized query + let query = firestore.collection(TABLE); - // Query for each technology - for (const technology of techArray) { - let query = firestore.collection(TABLE); + // Apply required filters + query = query.where('geo', '==', params.geo); + query = query.where('rank', '==', params.rank); - // Apply date filters - query = applyDateFilters(query, processedParams); + // Apply technology filter efficiently + if (techArray.length <= 30) { + // Use 'in' operator for batch processing (Firestore limit: 30 values) + query = query.where('technology', 'in', techArray); + } else { + // Parallel queries for >30 technologies (rare case) + const queryPromises = techArray.map(async (technology) => { + let individualQuery = firestore.collection(TABLE) + .where('geo', '==', params.geo) + .where('rank', '==', params.rank) + .where('technology', '==', technology); - // Apply optional standard filters (geo, rank are optional for page weight) and version filter - query = applyStandardFilters(query, processedParams, technology, techArray); + if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); + if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - // Execute query - const snapshot = await query.get(); - snapshot.forEach(doc => { - data.push(doc.data()); + const snapshot = await individualQuery.get(); + const results = []; + snapshot.forEach(doc => results.push(doc.data())); + return results; }); + + const results = await Promise.all(queryPromises); + const data = results.flat(); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + res.statusCode = 200; + res.end(JSON.stringify(data)); + return; } - // Send response + // Apply date filters + if (startDate) query = query.where('date', '>=', startDate); + if (params.end) query = query.where('date', '<=', params.end); + + // Execute single optimized query + const snapshot = await query.get(); + const data = []; + snapshot.forEach(doc => { + data.push(doc.data()); + }); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + // Direct response without wrapper functions res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { - handleControllerError(res, error, 'fetching Page Weight data'); + console.error('Error fetching Page Weight data:', error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: 'Failed to fetch Page Weight data' }] + })); } }; diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index 1d2f7fd..461771d 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -1,12 +1,22 @@ import { firestore } from '../utils/db.js'; -import { createSuccessResponse } from '../utils/helpers.js'; -import { handleControllerError } from '../utils/controllerHelpers.js'; +import { handleControllerError, generateQueryCacheKey, getCachedQueryResult, setCachedQueryResult } from '../utils/controllerHelpers.js'; /** * List all rank options from database */ const listRanks = async (req, res) => { try { + // Generate cache key for this query + const cacheKey = generateQueryCacheKey('ranks', { orderBy: 'mobile_origins' }); + + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); + return; + } + const snapshot = await firestore.collection('ranks').orderBy('mobile_origins', 'desc').get(); const data = []; @@ -16,8 +26,11 @@ const listRanks = async (req, res) => { data.push({ rank: docData.rank }); }); + // Cache the result + setCachedQueryResult(cacheKey, data); + res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { handleControllerError(res, error, 'fetching ranks'); } diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index 32ac3c5..bc4c929 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -1,6 +1,11 @@ import { firestore } from '../utils/db.js'; -import { createSuccessResponse } from '../utils/helpers.js'; -import { applyArrayFilter, selectFields, handleControllerError } from '../utils/controllerHelpers.js'; +import { + applyArrayFilter, + selectFields, + generateQueryCacheKey, + getCachedQueryResult, + setCachedQueryResult +} from '../utils/controllerHelpers.js'; // Technology Presenter - optimized with destructuring const presentTechnology = ({ technology, category, description, icon, origins }) => ({ @@ -12,7 +17,7 @@ const presentTechnology = ({ technology, category, description, icon, origins }) }); /** - * List technologies with optional filtering and field selection + * List technologies with optional filtering and field selection - Optimized version */ const listTechnologies = async (req, res) => { try { @@ -20,6 +25,23 @@ const listTechnologies = async (req, res) => { const isOnlyNames = params.onlyname || typeof params.onlyname === 'string'; const hasCustomFields = params.fields && !isOnlyNames; + // Create cache key for this specific query + const queryFilters = { + technology: params.technology, + category: params.category, + onlyname: isOnlyNames, + fields: params.fields + }; + const cacheKey = generateQueryCacheKey('technologies', queryFilters); + + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); + return; + } + let query = firestore.collection('technologies').orderBy('technology', 'asc'); // Apply filters using shared utilities @@ -44,11 +66,18 @@ const listTechnologies = async (req, res) => { } }); - // Send response + // Cache the result + setCachedQueryResult(cacheKey, data); + + // Direct response res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { - handleControllerError(res, error, 'fetching technologies'); + console.error('Error fetching technologies:', error); + res.statusCode = 500; + res.end(JSON.stringify({ + errors: [{ error: 'Failed to fetch technologies' }] + })); } }; diff --git a/src/controllers/versionsController.js b/src/controllers/versionsController.js index 8120e5a..5c33bc9 100644 --- a/src/controllers/versionsController.js +++ b/src/controllers/versionsController.js @@ -1,6 +1,6 @@ import { firestore } from '../utils/db.js'; -import { createSuccessResponse } from '../utils/helpers.js'; -import { applyArrayFilter, handleControllerError } from '../utils/controllerHelpers.js'; +import { convertToArray } from '../utils/helpers.js'; +import { handleControllerError, generateQueryCacheKey, getCachedQueryResult, setCachedQueryResult } from '../utils/controllerHelpers.js'; /** * List versions with optional technology filtering @@ -8,12 +8,56 @@ import { applyArrayFilter, handleControllerError } from '../utils/controllerHelp const listVersions = async (req, res) => { try { const params = req.query; + + // Generate cache key for this query + const cacheKey = generateQueryCacheKey('versions', params); + + // Check cache first + const cachedResult = getCachedQueryResult(cacheKey); + if (cachedResult) { + res.statusCode = 200; + res.end(JSON.stringify(cachedResult)); + return; + } + let query = firestore.collection('versions'); - // Apply technology filter using shared utility - query = applyArrayFilter(query, 'technology', params.technology); + // Apply technology filter - optimize for multiple technologies + if (params.technology) { + const technologies = convertToArray(params.technology); + if (technologies.length <= 30) { + // Use single query with 'in' operator for up to 30 technologies (Firestore limit) + query = query.where('technology', 'in', technologies); + } else { + // For more than 30 technologies, split into multiple queries and run in parallel + const chunks = []; + for (let i = 0; i < technologies.length; i += 30) { + chunks.push(technologies.slice(i, i + 30)); + } + + const promises = chunks.map(chunk => + firestore.collection('versions').where('technology', 'in', chunk).get() + ); + + const snapshots = await Promise.all(promises); + const data = []; - // Execute query + snapshots.forEach(snapshot => { + snapshot.forEach(doc => { + data.push(doc.data()); + }); + }); + + // Cache the result + setCachedQueryResult(cacheKey, data); + + res.statusCode = 200; + res.end(JSON.stringify(data)); + return; + } + } + + // Execute single query const snapshot = await query.get(); const data = []; @@ -22,9 +66,12 @@ const listVersions = async (req, res) => { data.push(doc.data()); }); + // Cache the result + setCachedQueryResult(cacheKey, data); + // Send response res.statusCode = 200; - res.end(JSON.stringify(createSuccessResponse(data))); + res.end(JSON.stringify(data)); } catch (error) { handleControllerError(res, error, 'fetching versions'); } diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js index f6dceba..1a4706f 100644 --- a/src/utils/controllerHelpers.js +++ b/src/utils/controllerHelpers.js @@ -1,4 +1,4 @@ -import { createErrorResponse, convertToArray } from './helpers.js'; +import { convertToHashes, convertToArray } from './helpers.js'; /** * Common parameter validation patterns @@ -28,6 +28,19 @@ const validateRequiredParams = (params, required) => { return errors.length > 0 ? errors : null; }; + +/** + * Creates an error response object + * @param {Array>} errors - Array of [key, message] arrays + * @returns {Object} Error response object + */ +const createErrorResponse = (errors) => { + return { + success: false, + errors: convertToHashes(errors) + }; +}; + /** * Send error response for missing parameters * @param {Object} res - Response object diff --git a/src/utils/helpers.js b/src/utils/helpers.js index d4616c3..9e7ed9c 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -24,25 +24,5 @@ const convertToHashes = (arr) => { return arr.map(([key, message]) => ({ [key]: message })); }; -/** - * Creates a successful response object - * @param {*} data - The data to return - * @returns {Object} Success response object - */ -const createSuccessResponse = (data) => { - return data; -}; - -/** - * Creates an error response object - * @param {Array>} errors - Array of [key, message] arrays - * @returns {Object} Error response object - */ -const createErrorResponse = (errors) => { - return { - success: false, - errors: convertToHashes(errors) - }; -}; -export { convertToArray, createSuccessResponse, createErrorResponse }; +export { convertToArray, convertToHashes }; From 45bcaeb820dbad76f21940473942b2d779055ec5 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:36:22 +0200 Subject: [PATCH 41/44] firestore query select method --- src/__tests__/routes.test.js | 2 + src/controllers/adoptionController.js | 45 +++++++---------------- src/controllers/categoriesController.js | 21 +++++++---- src/controllers/cwvtechController.js | 42 +++++++-------------- src/controllers/geosController.js | 4 +- src/controllers/lighthouseController.js | 41 ++++++--------------- src/controllers/pageWeightController.js | 41 ++++++--------------- src/controllers/ranksController.js | 6 +-- src/controllers/technologiesController.js | 37 ++++++++++--------- test-api.sh | 4 +- 10 files changed, 92 insertions(+), 151 deletions(-) diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index e6c6728..90ac268 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -39,6 +39,7 @@ jest.unstable_mockModule('../utils/db.js', () => { where: jest.fn(), orderBy: jest.fn(), limit: jest.fn(), + select: jest.fn(), get: jest.fn().mockResolvedValue(mockQuerySnapshot) }; @@ -46,6 +47,7 @@ jest.unstable_mockModule('../utils/db.js', () => { mockQuery.where.mockReturnValue(mockQuery); mockQuery.orderBy.mockReturnValue(mockQuery); mockQuery.limit.mockReturnValue(mockQuery); + mockQuery.select.mockReturnValue(mockQuery); const mockFirestoreInstance = { collection: jest.fn().mockImplementation((collectionName) => mockQuery) diff --git a/src/controllers/adoptionController.js b/src/controllers/adoptionController.js index 472b8eb..b06b5d9 100644 --- a/src/controllers/adoptionController.js +++ b/src/controllers/adoptionController.js @@ -11,7 +11,7 @@ import { const TABLE = 'adoption'; /** - * List adoption data with filtering - Optimized version + * List adoption data with filtering */ const listAdoptionData = async (req, res) => { try { @@ -34,7 +34,6 @@ const listAdoptionData = async (req, res) => { // Fast preprocessing - handle 'latest' date and technology array const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; - // Handle 'latest' date with caching let startDate = params.start; if (startDate === 'latest') { startDate = await getLatestDate(firestore, TABLE); @@ -58,42 +57,23 @@ const listAdoptionData = async (req, res) => { return; } - // Build optimized query + // Build query let query = firestore.collection(TABLE); // Apply required filters query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); - // Apply technology filter efficiently + // Apply technology filter if (techArray.length <= 30) { - // Use 'in' operator for batch processing (Firestore limit: 30 values) + // Use 'in' operator for batch processing (Firestore limit: 30 values https://cloud.google.com/firestore/docs/query-data/queries#limits_on_or_queries) query = query.where('technology', 'in', techArray); } else { - // Parallel queries for >30 technologies (rare case) - const queryPromises = techArray.map(async (technology) => { - let individualQuery = firestore.collection(TABLE) - .where('geo', '==', params.geo) - .where('rank', '==', params.rank) - .where('technology', '==', technology); - - if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); - if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - - const snapshot = await individualQuery.get(); - const results = []; - snapshot.forEach(doc => results.push(doc.data())); - return results; - }); - - const results = await Promise.all(queryPromises); - const data = results.flat(); - - // Cache the result - setCachedQueryResult(cacheKey, data); - - res.statusCode = 200; - res.end(JSON.stringify(data)); + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [{ technology: 'Too many technologies specified. Maximum 30 allowed.' }] + })); return; } @@ -101,7 +81,10 @@ const listAdoptionData = async (req, res) => { if (startDate) query = query.where('date', '>=', startDate); if (params.end) query = query.where('date', '<=', params.end); - // Execute single optimized query + // Apply field projection to exclude geo/rank + query = query.select('date', 'technology', 'adoption'); + + // Execute query const snapshot = await query.get(); const data = []; snapshot.forEach(doc => { @@ -111,7 +94,7 @@ const listAdoptionData = async (req, res) => { // Cache the result setCachedQueryResult(cacheKey, data); - // Direct response without wrapper functions + // Direct response res.statusCode = 200; res.end(JSON.stringify(data)); } catch (error) { diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index da76b2b..66170cb 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -37,21 +37,28 @@ const listCategories = async (req, res) => { // Apply category filter using shared utility query = applyArrayFilter(query, 'category', params.category); + if (isOnlyNames) { + // Only select category field for names-only queries + query = query.select('category'); + } else if (hasCustomFields) { + // Select only requested fields + const requestedFields = params.fields.split(',').map(f => f.trim()); + query = query.select(...requestedFields); + } + // Execute query const snapshot = await query.get(); const data = []; // Process results based on response type snapshot.forEach(doc => { + const docData = doc.data(); + if (isOnlyNames) { - data.push(doc.get('category')); - } else if (hasCustomFields) { - // Use custom field selection - const fullData = doc.data(); - data.push(selectFields(fullData, params.fields)); + data.push(docData.category); } else { - // Return full data - data.push(doc.data()); + // Data already filtered by select(), just return it + data.push(docData); } }); diff --git a/src/controllers/cwvtechController.js b/src/controllers/cwvtechController.js index fe7c7cb..35a51fc 100644 --- a/src/controllers/cwvtechController.js +++ b/src/controllers/cwvtechController.js @@ -11,7 +11,7 @@ import { const TABLE = 'core_web_vitals'; /** - * List Core Web Vitals data with filtering - Optimized version + * List Core Web Vitals data with filtering */ const listCWVTechData = async (req, res) => { try { @@ -34,7 +34,6 @@ const listCWVTechData = async (req, res) => { // Fast preprocessing - handle 'latest' date and technology array const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; - // Handle 'latest' date with caching let startDate = params.start; if (startDate === 'latest') { startDate = await getLatestDate(firestore, TABLE); @@ -58,42 +57,24 @@ const listCWVTechData = async (req, res) => { return; } - // Build optimized query + // Build query let query = firestore.collection(TABLE); // Apply required filters query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); - // Apply technology filter efficiently + + // Apply technology filter if (techArray.length <= 30) { // Use 'in' operator for batch processing (Firestore limit: 30 values) query = query.where('technology', 'in', techArray); } else { - // Parallel queries for >30 technologies (rare case) - const queryPromises = techArray.map(async (technology) => { - let individualQuery = firestore.collection(TABLE) - .where('geo', '==', params.geo) - .where('rank', '==', params.rank) - .where('technology', '==', technology); - - if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); - if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - - const snapshot = await individualQuery.get(); - const results = []; - snapshot.forEach(doc => results.push(doc.data())); - return results; - }); - - const results = await Promise.all(queryPromises); - const data = results.flat(); - - // Cache the result - setCachedQueryResult(cacheKey, data); - - res.statusCode = 200; - res.end(JSON.stringify(data)); + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [{ technology: 'Too many technologies specified. Maximum 30 allowed.' }] + })); return; } @@ -101,7 +82,10 @@ const listCWVTechData = async (req, res) => { if (startDate) query = query.where('date', '>=', startDate); if (params.end) query = query.where('date', '<=', params.end); - // Execute single optimized query + // Apply field projection to exclude geo/rank + query = query.select('date', 'technology', 'vitals'); + + // Execute query const snapshot = await query.get(); const data = []; snapshot.forEach(doc => { diff --git a/src/controllers/geosController.js b/src/controllers/geosController.js index b1da98d..31c8581 100644 --- a/src/controllers/geosController.js +++ b/src/controllers/geosController.js @@ -17,12 +17,12 @@ const listGeos = async (req, res) => { return; } - const snapshot = await firestore.collection('geos').orderBy('mobile_origins', 'desc').get(); + const snapshot = await firestore.collection('geos').orderBy('mobile_origins', 'desc').select('geo').get(); const data = []; // Extract only the 'geo' property from each document snapshot.forEach(doc => { - data.push({ geo: doc.data().geo }); + data.push(doc.data()); }); // Cache the result diff --git a/src/controllers/lighthouseController.js b/src/controllers/lighthouseController.js index 34b6d88..e7f0d49 100644 --- a/src/controllers/lighthouseController.js +++ b/src/controllers/lighthouseController.js @@ -11,7 +11,7 @@ import { const TABLE = 'lighthouse'; /** - * List Lighthouse data with filtering - Optimized version + * List Lighthouse data with filtering */ const listLighthouseData = async (req, res) => { try { @@ -34,7 +34,6 @@ const listLighthouseData = async (req, res) => { // Fast preprocessing - handle 'latest' date and technology array const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; - // Handle 'latest' date with caching let startDate = params.start; if (startDate === 'latest') { startDate = await getLatestDate(firestore, TABLE); @@ -58,42 +57,23 @@ const listLighthouseData = async (req, res) => { return; } - // Build optimized query + // Build query let query = firestore.collection(TABLE); // Apply required filters query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); - // Apply technology filter efficiently + // Apply technology filter if (techArray.length <= 30) { // Use 'in' operator for batch processing (Firestore limit: 30 values) query = query.where('technology', 'in', techArray); } else { - // Parallel queries for >30 technologies (rare case) - const queryPromises = techArray.map(async (technology) => { - let individualQuery = firestore.collection(TABLE) - .where('geo', '==', params.geo) - .where('rank', '==', params.rank) - .where('technology', '==', technology); - - if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); - if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - - const snapshot = await individualQuery.get(); - const results = []; - snapshot.forEach(doc => results.push(doc.data())); - return results; - }); - - const results = await Promise.all(queryPromises); - const data = results.flat(); - - // Cache the result - setCachedQueryResult(cacheKey, data); - - res.statusCode = 200; - res.end(JSON.stringify(data)); + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [{ technology: 'Too many technologies specified. Maximum 30 allowed.' }] + })); return; } @@ -101,7 +81,10 @@ const listLighthouseData = async (req, res) => { if (startDate) query = query.where('date', '>=', startDate); if (params.end) query = query.where('date', '<=', params.end); - // Execute single optimized query + // Apply field projection to exclude geo/rank + query = query.select('date', 'technology', 'lighthouse'); + + // Execute query const snapshot = await query.get(); const data = []; snapshot.forEach(doc => { diff --git a/src/controllers/pageWeightController.js b/src/controllers/pageWeightController.js index 26afe77..7fd6ff7 100644 --- a/src/controllers/pageWeightController.js +++ b/src/controllers/pageWeightController.js @@ -11,7 +11,7 @@ import { const TABLE = 'page_weight'; /** - * List Page Weight data with filtering - Optimized version + * List Page Weight data with filtering */ const listPageWeightData = async (req, res) => { try { @@ -34,7 +34,6 @@ const listPageWeightData = async (req, res) => { // Fast preprocessing - handle 'latest' date and technology array const techArray = params.technology ? decodeURIComponent(params.technology).split(',') : []; - // Handle 'latest' date with caching let startDate = params.start; if (startDate === 'latest') { startDate = await getLatestDate(firestore, TABLE); @@ -58,42 +57,23 @@ const listPageWeightData = async (req, res) => { return; } - // Build optimized query + // Build query let query = firestore.collection(TABLE); // Apply required filters query = query.where('geo', '==', params.geo); query = query.where('rank', '==', params.rank); - // Apply technology filter efficiently + // Apply technology filter if (techArray.length <= 30) { // Use 'in' operator for batch processing (Firestore limit: 30 values) query = query.where('technology', 'in', techArray); } else { - // Parallel queries for >30 technologies (rare case) - const queryPromises = techArray.map(async (technology) => { - let individualQuery = firestore.collection(TABLE) - .where('geo', '==', params.geo) - .where('rank', '==', params.rank) - .where('technology', '==', technology); - - if (startDate) individualQuery = individualQuery.where('date', '>=', startDate); - if (params.end) individualQuery = individualQuery.where('date', '<=', params.end); - - const snapshot = await individualQuery.get(); - const results = []; - snapshot.forEach(doc => results.push(doc.data())); - return results; - }); - - const results = await Promise.all(queryPromises); - const data = results.flat(); - - // Cache the result - setCachedQueryResult(cacheKey, data); - - res.statusCode = 200; - res.end(JSON.stringify(data)); + res.statusCode = 400; + res.end(JSON.stringify({ + success: false, + errors: [{ technology: 'Too many technologies specified. Maximum 30 allowed.' }] + })); return; } @@ -101,7 +81,10 @@ const listPageWeightData = async (req, res) => { if (startDate) query = query.where('date', '>=', startDate); if (params.end) query = query.where('date', '<=', params.end); - // Execute single optimized query + // Apply field projection to exclude geo/rank and select only needed fields + query = query.select('date', 'technology', 'pageWeight'); + + // Execute query const snapshot = await query.get(); const data = []; snapshot.forEach(doc => { diff --git a/src/controllers/ranksController.js b/src/controllers/ranksController.js index 461771d..4fa7635 100644 --- a/src/controllers/ranksController.js +++ b/src/controllers/ranksController.js @@ -17,13 +17,11 @@ const listRanks = async (req, res) => { return; } - const snapshot = await firestore.collection('ranks').orderBy('mobile_origins', 'desc').get(); + const snapshot = await firestore.collection('ranks').orderBy('mobile_origins', 'desc').select('rank').get(); const data = []; - // Extract only the 'rank' property from each document snapshot.forEach(doc => { - const docData = doc.data(); - data.push({ rank: docData.rank }); + data.push(doc.data()); }); // Cache the result diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index bc4c929..0ed4a32 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -7,17 +7,8 @@ import { setCachedQueryResult } from '../utils/controllerHelpers.js'; -// Technology Presenter - optimized with destructuring -const presentTechnology = ({ technology, category, description, icon, origins }) => ({ - technology, - category, - description, - icon, - origins -}); - /** - * List technologies with optional filtering and field selection - Optimized version + * List technologies with optional filtering and field selection */ const listTechnologies = async (req, res) => { try { @@ -48,21 +39,31 @@ const listTechnologies = async (req, res) => { query = applyArrayFilter(query, 'technology', params.technology); query = applyArrayFilter(query, 'category_obj', params.category, 'array-contains-any'); + if (isOnlyNames) { + // Only select technology field for names-only queries + query = query.select('technology'); + } else if (hasCustomFields) { + // Select only requested fields + const requestedFields = params.fields.split(',').map(f => f.trim()); + query = query.select(...requestedFields); + } else { + // Select default presentation fields + query = query.select('technology', 'category', 'description', 'icon', 'origins'); + } + // Execute query const snapshot = await query.get(); - const data = []; + let data = []; // Process results based on response type snapshot.forEach(doc => { + const docData = doc.data(); + if (isOnlyNames) { - data.push(doc.get('technology')); - } else if (hasCustomFields) { - // Use custom field selection - const fullData = doc.data(); - data.push(selectFields(fullData, params.fields)); + data.push(docData.technology); } else { - // Use default presenter - data.push(presentTechnology(doc.data())); + // Data already filtered by select(), just return it + data.push(docData) } }); diff --git a/test-api.sh b/test-api.sh index 96e4651..7a22c7c 100755 --- a/test-api.sh +++ b/test-api.sh @@ -11,7 +11,7 @@ test_endpoint() { http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$d') - echo "$body" | jq . + echo "$body" | jq . | head -10 echo "Status code: $http_code" if [[ $http_code -ne 200 ]]; then @@ -87,7 +87,7 @@ test_endpoint "/" "" test_cors_preflight "/v1/technologies" test_endpoint "/v1/technologies" "?technology=WordPress&onlyname=true" test_endpoint "/v1/technologies" "?technology=WordPress&onlyname=true&fields=technology,icon" -test_endpoint "/v1/technologies" "?technology=WordPress&fields=technology,icon" +test_endpoint "/v1/technologies" "?technology=WordPress&category=CMS&fields=technology,icon" # Test categories endpoint test_cors_preflight "/v1/categories" From 141959a76021cd4570e20194472ed71a9ee9bac0 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:56:09 +0200 Subject: [PATCH 42/44] cache cleanup --- perf_lab_test/main.js | 98 ----------- perf_lab_test/package-lock.json | 294 -------------------------------- perf_lab_test/package.json | 14 -- src/index.js | 5 + src/package.json | 4 +- src/utils/controllerHelpers.js | 89 +++++++++- terraform/dev/main.tf | 3 +- terraform/dev/variables.tf | 2 +- 8 files changed, 96 insertions(+), 413 deletions(-) delete mode 100644 perf_lab_test/main.js delete mode 100644 perf_lab_test/package-lock.json delete mode 100644 perf_lab_test/package.json diff --git a/perf_lab_test/main.js b/perf_lab_test/main.js deleted file mode 100644 index 4ce5ca1..0000000 --- a/perf_lab_test/main.js +++ /dev/null @@ -1,98 +0,0 @@ -const axios = require('axios'); - -const ENDPOINTS = { - A: 'https://prod-gw-2vzgiib6.ue.gateway.dev/v1/cwv?technology=WordPress,Shopify,Wix,Joomla,Drupal,Squarespace,PrestaShop,Webflow,1C-Bitrix,Tilda&geo=United%20States%20of%20America&rank=Top%20100k&start=latest', - B: 'https://reports-dev-2vzgiib6.uc.gateway.dev/v1/cwv?technology=WordPress,Shopify,Wix,Joomla,Drupal,Squarespace,PrestaShop,Webflow,1C-Bitrix,Tilda&geo=United%20States%20of%20America&rank=Top%20100k&start=latest' -}; - -const NUM_REQUESTS = 100; -const CONCURRENCY = 10; -const MAX_JITTER_MS = 100; - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -async function makeRequest(name, url) { - const jitter = Math.floor(Math.random() * MAX_JITTER_MS); - await sleep(jitter); - - const fullUrl = `${url}?nocache=${Math.random().toString(36).substring(2)}`; - - const start = process.hrtime.bigint(); - try { - const res = await axios.get(fullUrl, { - headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - } - }); - const end = process.hrtime.bigint(); - if (res.status === 200) { - return { name, duration: Number(end - start) / 1e6 }; // ms - } else { - return { name, error: `Status: ${res.status}` }; - } - } catch (e) { - return { name, error: e.message }; - } -} - -async function benchmarkAlternating() { - const results = { - A: { durations: [], errors: [] }, - B: { durations: [], errors: [] } - }; - - const endpointNames = Object.keys(ENDPOINTS); - - for (let i = 0; i < NUM_REQUESTS; i += CONCURRENCY) { - const batch = []; - - for (let j = 0; j < CONCURRENCY; j++) { - const name = endpointNames[(i + j) % endpointNames.length]; - const url = ENDPOINTS[name]; - batch.push(makeRequest(name, url)); - } - - const responses = await Promise.all(batch); - - for (const res of responses) { - if (res.duration !== undefined) { - results[res.name].durations.push(res.duration); - } else { - results[res.name].errors.push(res.error); - } - } - } - - return results; -} - -function printStats(name, durations, errors) { - const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length; - const p90 = (arr) => { - const sorted = [...arr].sort((a, b) => a - b); - return sorted[Math.floor(sorted.length * 0.9)]; - }; - - console.log(`\n🔍 Results for ${name} (${ENDPOINTS[name]})`); - console.log(`✅ Successful responses: ${durations.length}`); - console.log(`❌ Errors: ${errors.length}`); - if (durations.length) { - console.log(`📈 Avg latency: ${avg(durations).toFixed(2)} ms`); - console.log(`🚀 Fastest: ${Math.min(...durations).toFixed(2)} ms`); - console.log(`🐢 Slowest: ${Math.max(...durations).toFixed(2)} ms`); - console.log(`📊 P90 latency: ${p90(durations).toFixed(2)} ms`); - } -} - -async function main() { - const results = await benchmarkAlternating(); - for (const name of Object.keys(ENDPOINTS)) { - const { durations, errors } = results[name]; - printStats(name, durations, errors); - } -} - -main(); diff --git a/perf_lab_test/package-lock.json b/perf_lab_test/package-lock.json deleted file mode 100644 index ca5982f..0000000 --- a/perf_lab_test/package-lock.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "name": "test", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "test", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "axios": "^1.9.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - } - } -} diff --git a/perf_lab_test/package.json b/perf_lab_test/package.json deleted file mode 100644 index bed8ee8..0000000 --- a/perf_lab_test/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "test", - "version": "1.0.0", - "main": "main.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "axios": "^1.9.0" - } -} diff --git a/src/index.js b/src/index.js index e66e1cc..897e782 100644 --- a/src/index.js +++ b/src/index.js @@ -151,6 +151,11 @@ const handleRequest = async (req, res) => { } else if (pathname === '/v1/versions' && req.method === 'GET') { const { listVersions } = await getController('versions'); await listVersions(req, res); + } else if (pathname === '/v1/cache-stats' && req.method === 'GET') { + // Cache monitoring endpoint + const { getCacheStats } = await import('./utils/controllerHelpers.js'); + const stats = getCacheStats(); + sendJSONResponse(res, stats); } else { // 404 Not Found res.statusCode = 404; diff --git a/src/package.json b/src/package.json index 7be7bfd..ca8a44a 100644 --- a/src/package.json +++ b/src/package.json @@ -8,9 +8,7 @@ "node": ">=22.0.0" }, "scripts": { - "start": "node index.js", - "start:functions": "export DATABASE=tech-report-api-prod && functions-framework --target=app", - "dev": "nodemon index.js", + "start": "export DATABASE=tech-report-api-prod &&node index.js", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "test:live": "bash ../test-api.sh" }, diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js index 1a4706f..3185a87 100644 --- a/src/utils/controllerHelpers.js +++ b/src/utils/controllerHelpers.js @@ -59,6 +59,39 @@ const CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds const queryResultCache = new Map(); const QUERY_CACHE_TTL = 10 * 60 * 1000; // 10 minutes for query results +// Cache size limit +const MAX_CACHE_SIZE = 3000; // Maximum number of cache entries + +/** + * Clean up cache when it exceeds size limit (LRU-style cleanup) + * Removes oldest entries first, including expired ones + */ +const cleanupCacheToSize = () => { + const targetSize = Math.floor(MAX_CACHE_SIZE * 0.5); // Clean to 50% of max size + if (queryResultCache.size <= targetSize) return 0; + + const now = Date.now(); + const entries = Array.from(queryResultCache.entries()); + + // Sort by timestamp (oldest first), prioritizing expired entries + entries.sort((a, b) => { + const aExpired = (now - a[1].timestamp) > QUERY_CACHE_TTL; + const bExpired = (now - b[1].timestamp) > QUERY_CACHE_TTL; + + // If one is expired and the other isn't, prioritize expired for deletion + if (aExpired && !bExpired) return -1; + if (!aExpired && bExpired) return 1; + + // If both have same expiry status, sort by timestamp (oldest first) + return a[1].timestamp - b[1].timestamp; + }); + + const deleteCount = queryResultCache.size - targetSize; + for (let i = 0; i < deleteCount && i < entries.length; i++) { + queryResultCache.delete(entries[i][0]); + } +}; + /** * Generate a cache key for a query * @param {string} collection - Collection name @@ -88,6 +121,11 @@ const getCachedQueryResult = (cacheKey) => { * @param {Array} data - Query result data */ const setCachedQueryResult = (cacheKey, data) => { + // Clean up if cache is getting too large before adding new entry + if (queryResultCache.size >= MAX_CACHE_SIZE) { + cleanupCacheToSize(); + } + queryResultCache.set(cacheKey, { data: data, timestamp: Date.now() @@ -237,6 +275,54 @@ const selectFields = (data, fieldsParam) => { return result; }; +/** + * Get cache statistics for monitoring + * @returns {Object} Cache statistics + */ +const getCacheStats = () => { + const now = Date.now(); + + // Count valid vs expired entries + let queryValidCount = 0; + let queryExpiredCount = 0; + for (const [key, value] of queryResultCache) { + if (now - value.timestamp < QUERY_CACHE_TTL) { + queryValidCount++; + } else { + queryExpiredCount++; + } + } + + let dateValidCount = 0; + let dateExpiredCount = 0; + for (const [key, value] of latestDateCache) { + if (now - value.timestamp < CACHE_TTL) { + dateValidCount++; + } else { + dateExpiredCount++; + } + } + + return { + queryCache: { + total: queryResultCache.size, + valid: queryValidCount, + expired: queryExpiredCount, + ttl: QUERY_CACHE_TTL + }, + dateCache: { + total: latestDateCache.size, + valid: dateValidCount, + expired: dateExpiredCount, + ttl: CACHE_TTL + }, + config: { + maxCacheSize: MAX_CACHE_SIZE, + cleanupStrategy: 'size-based-lru' + } + }; +}; + /** * Handle controller errors with consistent error response format * @param {Object} res - Response object @@ -264,5 +350,6 @@ export { handleControllerError, generateQueryCacheKey, getCachedQueryResult, - setCachedQueryResult + setCachedQueryResult, + getCacheStats }; diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index 1cd4625..f73acbb 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -18,7 +18,6 @@ resource "google_api_gateway_api" "api" { project = var.project } -# A Configuration, consisting of an OpenAPI specification resource "google_api_gateway_api_config" "api_config" { provider = google-beta api = google_api_gateway_api.api.api_id @@ -109,7 +108,7 @@ EOF } } } -# The actual API Gateway + resource "google_api_gateway_gateway" "gateway" { provider = google-beta project = var.project diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 090f67e..4dd84ad 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -30,5 +30,5 @@ variable "google_service_account_api_gateway" { variable "min_instances" { description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." type = number - default = 1 + default = 1 // TODO: Update this to 0 } From c7b8cdb34004bb1b75dfb7423b58dd1f88024dff Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:02:42 +0200 Subject: [PATCH 43/44] prod tf --- terraform/modules/cloud-function/main.tf | 98 ----------- terraform/modules/cloud-function/outputs.tf | 5 - terraform/modules/cloud-function/variables.tf | 75 -------- terraform/prod/main.tf | 162 ++---------------- terraform/prod/variables.tf | 4 +- test-api.sh | 4 +- 6 files changed, 23 insertions(+), 325 deletions(-) delete mode 100644 terraform/modules/cloud-function/main.tf delete mode 100644 terraform/modules/cloud-function/outputs.tf delete mode 100644 terraform/modules/cloud-function/variables.tf diff --git a/terraform/modules/cloud-function/main.tf b/terraform/modules/cloud-function/main.tf deleted file mode 100644 index 6f876d0..0000000 --- a/terraform/modules/cloud-function/main.tf +++ /dev/null @@ -1,98 +0,0 @@ -locals { - bucketName = "tf-cloudfunctions-backingapi-20230314" -} -data "archive_file" "source" { - type = "zip" - source_dir = var.source_directory - output_path = "/tmp/${var.function_name}.zip" -} -resource "google_storage_bucket_object" "zip" { - name = "${var.environment}-${var.function_name}-${data.archive_file.source.output_sha}" - bucket = local.bucketName - source = data.archive_file.source.output_path -} - -resource "google_cloudfunctions2_function" "function" { - name = "${var.function_name}-${var.environment}" - location = var.region - - build_config { - runtime = "python312" - entry_point = var.entry_point - - source { - storage_source { - bucket = local.bucketName - object = google_storage_bucket_object.zip.name - } - } - } - - service_config { - all_traffic_on_latest_revision = true - available_memory = var.available_memory_mb - ingress_settings = var.ingress_settings - - environment_variables = var.environment_variables - - min_instance_count = var.min_instances - max_instance_count = var.max_instances - timeout_seconds = var.timeout - max_instance_request_concurrency = var.max_instance_request_concurrency - service_account_email = var.service_account_email - } - - labels = { - owner = "tech_report_api" - environment = var.environment - } - - depends_on = [ - google_storage_bucket_object.zip - ] -} - -resource "google_cloudfunctions2_function_iam_member" "variable_service_account_function_invoker" { - project = google_cloudfunctions2_function.function.project - location = google_cloudfunctions2_function.function.location - cloud_function = google_cloudfunctions2_function.function.name - role = "roles/cloudfunctions.invoker" - member = "serviceAccount:${var.service_account_email}" - depends_on = [google_cloudfunctions2_function.function] -} -data "google_cloud_run_service" "run-service" { - name = google_cloudfunctions2_function.function.name - location = var.region - depends_on = [google_cloudfunctions2_function.function] -} -resource "google_cloud_run_v2_service_iam_member" "variable_service_account_run_invoker" { - project = var.project - location = var.region - name = data.google_cloud_run_service.run-service.name - role = "roles/run.invoker" - member = "serviceAccount:${var.service_account_email}" -} - -resource "google_cloudfunctions2_function_iam_member" "api_gw_variable_service_account_function_invoker" { - project = google_cloudfunctions2_function.function.project - location = google_cloudfunctions2_function.function.location - cloud_function = google_cloudfunctions2_function.function.name - role = "roles/cloudfunctions.invoker" - member = "serviceAccount:${var.service_account_api_gateway}" - depends_on = [google_cloudfunctions2_function.function] -} - -resource "google_cloud_run_v2_service_iam_member" "api_gw_variable_service_account_run_invoker" { - project = var.project - location = var.region - name = data.google_cloud_run_service.run-service.name - role = "roles/run.invoker" - member = "serviceAccount:${var.service_account_api_gateway}" -} - - - - - - - diff --git a/terraform/modules/cloud-function/outputs.tf b/terraform/modules/cloud-function/outputs.tf deleted file mode 100644 index c7bb95b..0000000 --- a/terraform/modules/cloud-function/outputs.tf +++ /dev/null @@ -1,5 +0,0 @@ - -output "name" { - description = "Name of the Cloud Function" - value = google_cloudfunctions2_function.function.name -} diff --git a/terraform/modules/cloud-function/variables.tf b/terraform/modules/cloud-function/variables.tf deleted file mode 100644 index 3cfe6af..0000000 --- a/terraform/modules/cloud-function/variables.tf +++ /dev/null @@ -1,75 +0,0 @@ -variable "secrets" { - default = [] -} -variable "region" { - default = "us-east1" - type = string -} -variable "environment" { - description = "The 'Environment' that is being created/deployed. Applied as a suffix to many resources." - type = string -} -variable "source_directory" { - description = "The folder of the package containing function that will be executed when the Google Cloud Function is triggered!" - type = string -} -variable "function_name" { - description = "Optional: Can be used to create more than function from the same package" - type = string -} -variable "entry_point" { - description = "The entry point; This is either what is registered with 'http' or exported from the code as a handler!" - type = string -} -variable "available_memory_mb" { - default = "1Gi" - type = string - description = "The amount of memory for the Cloud Function" -} -variable "ingress_settings" { - type = string - default = "ALLOW_ALL" - description = "String value that controls what traffic can reach the function. Allowed values are ALLOW_ALL, ALLOW_INTERNAL_AND_GCLB and ALLOW_INTERNAL_ONLY. Check ingress documentation to see the impact of each settings value. Changes to this field will recreate the cloud function." -} -variable "vpc_connector_egress_settings" { - type = string - default = null - description = "The egress settings for the connector, controlling what traffic is diverted through it. Allowed values are ALL_TRAFFIC and PRIVATE_RANGES_ONLY. Defaults to PRIVATE_RANGES_ONLY. If unset, this field preserves the previously set value." -} -variable "project" { - description = "The ID of the project in which the resource belongs. If it is not provided, the provider project is used." - type = string -} -variable "timeout" { - default = 60 - type = number - description = "Timeout (in seconds) for the function. Default value is 60 seconds. Cannot be more than 540 seconds." -} -variable "service_account_email" { - type = string - description = "Service account who can invoke this function. This is required!" -} -variable "service_account_api_gateway" { - type = string - description = "API Gateway service account who can invoke this function. This is required!" -} -variable "max_instances" { - default = 5 - type = number - description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time." -} -variable "min_instances" { - description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time." - type = number - default = 1 -} -variable "max_instance_request_concurrency" { - description = "(Optional) The limit on the maximum number of requests that an instance can handle simultaneously. This can be used to control costs when scaling. Defaults to 1." - type = number - default = 5 -} -variable "environment_variables" { - description = "environment_variables" - default = {} - type = map(string) -} diff --git a/terraform/prod/main.tf b/terraform/prod/main.tf index 12c9c3d..5a08aba 100644 --- a/terraform/prod/main.tf +++ b/terraform/prod/main.tf @@ -13,38 +13,39 @@ provider "google" { resource "google_api_gateway_api" "api" { provider = google-beta - api_id = "api-gw-prod" - display_name = "The prod API Gateway" + api_id = "reports-api-prod" + display_name = "Reports API Gateway" project = var.project } resource "google_api_gateway_api_config" "api_config" { provider = google-beta api = google_api_gateway_api.api.api_id - api_config_id_prefix = "api" + api_config_id_prefix = "reports-api-config-prod" project = var.project - display_name = "The prod Config" + display_name = "Reports API Config PROD" openapi_documents { document { path = "spec.yaml" contents = base64encode(<<-EOF swagger: "2.0" info: - title: reports-backend-api - description: API tech report + title: reports_api_config_prod version: 1.0.0 schemes: - https produces: - application/json +x-google-backend: + address: https://us-central1-httparchive.cloudfunctions.net/tech-report-api-prod + deadline: 60 + path_translation: APPEND_PATH_TO_ADDRESS + protocol: h2 paths: /v1/categories: get: summary: categories operationId: getCategories - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/categories-prod - deadline: 60 responses: 200: description: String @@ -52,9 +53,6 @@ paths: get: summary: adoption operationId: getadoptionReports - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/adoption-prod - deadline: 60 responses: 200: description: String @@ -62,9 +60,6 @@ paths: get: summary: pageWeight operationId: getpageWeight - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/page-weight-prod - deadline: 60 responses: 200: description: String @@ -72,9 +67,6 @@ paths: get: summary: lighthouse operationId: getLighthouseReports - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/lighthouse-prod - deadline: 60 responses: 200: description: String @@ -82,9 +74,6 @@ paths: get: summary: cwv operationId: getCwv - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/cwvtech-prod - deadline: 60 responses: 200: description: String @@ -92,9 +81,6 @@ paths: get: summary: ranks operationId: getRanks - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/ranks-prod - deadline: 60 responses: 200: description: String @@ -102,19 +88,13 @@ paths: get: summary: geos operationId: getGeos - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/geos-prod - deadline: 60 responses: 200: description: String /v1/technologies: get: - summary: geos + summary: technologies operationId: getTechnologies - x-google-backend: - address: https://us-east1-httparchive.cloudfunctions.net/technologies-prod - deadline: 60 responses: 200: description: String @@ -134,8 +114,8 @@ resource "google_api_gateway_gateway" "gateway" { project = var.project region = var.region api_config = google_api_gateway_api_config.api_config.id - gateway_id = "prod-gw" - display_name = "prod Api Gateway" + gateway_id = "reports-prod" + display_name = "Reports API Gateway PROD" labels = { owner = "tech_report_api" environment = var.environment @@ -148,88 +128,14 @@ resource "google_api_gateway_gateway" "gateway" { } } -module "cwvtech" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/cwvtech" - function_name = "cwvtech" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} - -module "lighthouse" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/lighthouse" - function_name = "lighthouse" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} - -module "adoption" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/adoption" - function_name = "adoption" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} - -module "page-weight" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" +module "endpoints" { + source = "./../modules/run-service" + entry_point = "app" project = var.project environment = var.environment - source_directory = "../../functions/page-weight" - function_name = "page-weight" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} - -module "categories" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/categories" - function_name = "categories" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} - -module "technologies" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/technologies" - function_name = "technologies" + source_directory = "../../src" + function_name = "tech-report-api" + region = var.region service_account_email = var.google_service_account_cloud_functions service_account_api_gateway = var.google_service_account_api_gateway min_instances = var.min_instances @@ -238,33 +144,3 @@ module "technologies" { "DATABASE" = var.project_database } } - -module "ranks" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/ranks" - function_name = "ranks" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} - -module "geos" { - source = "./../modules/cloud-function" - entry_point = "dispatcher" - project = var.project - environment = var.environment - source_directory = "../../functions/geos" - function_name = "geos" - service_account_email = var.google_service_account_cloud_functions - service_account_api_gateway = var.google_service_account_api_gateway - environment_variables = { - "PROJECT" = var.project - "DATABASE" = var.project_database - } -} diff --git a/terraform/prod/variables.tf b/terraform/prod/variables.tf index 2e42cd8..edec25f 100644 --- a/terraform/prod/variables.tf +++ b/terraform/prod/variables.tf @@ -4,7 +4,7 @@ variable "project" { default = "httparchive" } variable "region" { - default = "us-east1" + default = "us-central1" type = string } variable "environment" { @@ -15,7 +15,7 @@ variable "environment" { variable "project_database" { type = string description = "The database name" - default = "tech-report-apis-prod" + default = "tech-report-api-prod" } variable "google_service_account_cloud_functions" { diff --git a/test-api.sh b/test-api.sh index 7a22c7c..ea98db3 100755 --- a/test-api.sh +++ b/test-api.sh @@ -4,7 +4,7 @@ test_endpoint() { local endpoint=$1 local params=$2 - local url="http://localhost:8080${endpoint}${params}" + local url="http://localhost:3000${endpoint}${params}" echo "Testing endpoint: ${url}" response=$(curl -s -w "\n%{http_code}" "${url}") @@ -27,7 +27,7 @@ test_endpoint() { # Function to test CORS preflight with OPTIONS request test_cors_preflight() { local endpoint=$1 - local url="http://localhost:8080${endpoint}" + local url="http://localhost:3000${endpoint}" echo "Testing CORS preflight for: ${url}" From 43f1fdaeba0d2bb26c216082dc5a8d8d72c14685 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:22:01 +0200 Subject: [PATCH 44/44] extended cache --- src/utils/controllerHelpers.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/controllerHelpers.js b/src/utils/controllerHelpers.js index 3185a87..31e1c00 100644 --- a/src/utils/controllerHelpers.js +++ b/src/utils/controllerHelpers.js @@ -55,12 +55,11 @@ const sendValidationError = (res, errors) => { const latestDateCache = new Map(); const CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds -// Cache for query results to eliminate bimodal performance const queryResultCache = new Map(); -const QUERY_CACHE_TTL = 10 * 60 * 1000; // 10 minutes for query results +const QUERY_CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds // Cache size limit -const MAX_CACHE_SIZE = 3000; // Maximum number of cache entries +const MAX_CACHE_SIZE = 5000; // Maximum number of cache entries /** * Clean up cache when it exceeds size limit (LRU-style cleanup)