diff --git a/README.md b/README.md index 2724719..7c999b1 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Lists available categories. #### Categories Parameters - `category` (optional): Filter by category name(s) - comma-separated list +- `client` (optional): Filter by client type (e.g., `mobile`, `desktop`) - defaults to `mobile` - `onlyname` (optional): If present, returns only category names - `fields` (optional): Comma-separated list of fields to include in the response (see [Field Selection API Documentation](#field-selection-api-documentation) for details) @@ -63,7 +64,7 @@ Lists available categories. ```bash curl --request GET \ - --url 'https://d{{HOST}}/v1/categories?category=Domain%20parking%2CCI' + --url 'https://{{HOST}}/v1/categories?category=Domain%20parking%2CCI&client=desktop' ``` ```json @@ -74,10 +75,7 @@ curl --request GET \ "Jenkins", "TeamCity" ], - "origins": { - "mobile": 22, - "desktop": 35 - }, + "origins": 22, "category": "CI" }, { @@ -86,10 +84,7 @@ curl --request GET \ "Cloudflare", "Arsys Domain Parking" ], - "origins": { - "mobile": 14, - "desktop": 8 - }, + "origins": 14, "category": "Domain parking" } ] @@ -213,6 +208,7 @@ Provides technology adoption data. - `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) +- `version` (optional): Filter by version name(s) - comma-separated list (only when single technology is specified) #### Adoption Response @@ -248,6 +244,7 @@ Provides Core Web Vitals metrics for technologies. - `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) +- `version` (optional): Filter by version name(s) - comma-separated list (only when single technology is specified) #### CWV Response @@ -291,6 +288,7 @@ Provides Lighthouse scores for technologies. - `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) +- `version` (optional): Filter by version name(s) - comma-separated list (only when single technology is specified) #### Lighthouse Response @@ -338,6 +336,7 @@ Provides Page Weight metrics for technologies. - `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) +- `version` (optional): Filter by version name(s) - comma-separated list (only when single technology is specified) #### Page Weight Response @@ -399,6 +398,7 @@ Provides Lighthouse audits for technologies. - `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) +- `version` (optional): Filter by version name(s) - comma-separated list (only when single technology is specified) #### Audits Response @@ -588,7 +588,7 @@ The categories and technologies endpoints now support custom field selection, al - `category` - Category name - `description` - Category description - `technologies` - Array of technology names in the category -- `origins` - Array of origin companies/organizations +- `origins` - Number of origins Get only category names: @@ -608,7 +608,7 @@ GET /v1/categories?fields=category,description - `category` - Category name - `description` - Technology description - `icon` - Icon filename -- `origins` - Array of origin companies/organizations +- `origins` - Dictionary with origins per client Get only technology names and categories: @@ -643,7 +643,7 @@ GET /v1/technologies?category=JavaScript%20Frameworks&fields=technology,icon - `technology` - Technology name - `version` - Version name -- `origins` - Mobile and desktop origins +- `origins` - Dictionary with origins per client Get only technology and version names: diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..28a5ebd --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +.git +.gitignore +.env +.nyc_output +coverage +*.md +.DS_Store +cloudbuild.yaml +__tests__ +coverage diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..15a1abd --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-slim + +# Set the working directory +WORKDIR /app + +# Copy package files first for better layer caching +COPY package*.json ./ + +# Install dependencies (this layer will be cached unless package files change) +RUN npm ci --only=production --quiet --no-fund --no-audit && npm cache clean --force + +ENV DATABASE=tech-report-api-prod + +# Copy source code +COPY . . + +CMD ["node", "index.js"] diff --git a/src/__tests__/routes.test.js b/src/__tests__/routes.test.js index f84cd9b..38f5d48 100644 --- a/src/__tests__/routes.test.js +++ b/src/__tests__/routes.test.js @@ -233,6 +233,12 @@ describe('API Routes', () => { expect(Array.isArray(res.body)).toBe(true); }); + it('should return adoption data with version parameter', async () => { + const res = await request(app).get('/v1/adoption?technology=WordPress&geo=ALL&rank=ALL&start=latest&version=5.0'); + 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); diff --git a/src/controllers/categoriesController.js b/src/controllers/categoriesController.js index 6a76918..84f7b79 100644 --- a/src/controllers/categoriesController.js +++ b/src/controllers/categoriesController.js @@ -8,7 +8,7 @@ const listCategories = async (req, res) => { const queryBuilder = async (params) => { /* // Validate parameters - const supportedParams = ['category', 'onlyname', 'fields']; + const supportedParams = ['category', 'onlyname', 'fields', 'client']; const providedParams = Object.keys(params); const unsupportedParams = providedParams.filter(param => !supportedParams.includes(param)); @@ -20,6 +20,7 @@ const listCategories = async (req, res) => { */ const isOnlyNames = params.onlyname || typeof params.onlyname === 'string'; + const client = params.client || 'mobile'; // Default client if not provided const hasCustomFields = params.fields && !isOnlyNames; let query = firestore.collection('categories').orderBy('category', 'asc'); @@ -32,11 +33,16 @@ const listCategories = async (req, res) => { } } + // Apply client filter + if (client) { + query = query.where('client', '==', client); + } + // Apply field selection if (isOnlyNames) { query = query.select('category'); } else if (hasCustomFields) { - const requestedFields = params.fields.split(',').map(f => f.trim()); + const requestedFields = params.fields.split(',').map(f => f.trim()) || ['category', 'description', 'technologies', 'origins']; query = query.select(...requestedFields); } @@ -56,7 +62,9 @@ const listCategories = async (req, res) => { // Include onlyname and fields in cache key calculation const customCacheKeyData = { onlyname: req.query.onlyname || false, - fields: req.query.fields + fields: req.query.fields, + category: req.query.category ? validateArrayParameter(req.query.category, 'category') : [], + client: req.query.client || 'mobile', }; await executeQuery(req, res, 'categories', queryBuilder, dataProcessor, customCacheKeyData); diff --git a/src/controllers/reportController.js b/src/controllers/reportController.js index 9f24123..14ffaf7 100644 --- a/src/controllers/reportController.js +++ b/src/controllers/reportController.js @@ -1,5 +1,4 @@ -import { firestoreOld } from '../utils/db.js'; -const firestore = firestoreOld; +import { firestore } from '../utils/db.js'; import { REQUIRED_PARAMS, @@ -55,7 +54,7 @@ const createReportController = (reportType) => { /* // Validate supported parameters - const supportedParams = ['technology', 'geo', 'rank', 'start', 'end']; + const supportedParams = ['technology', 'geo', 'rank', 'start', 'end', 'version']; const providedParams = Object.keys(params); const unsupportedParams = providedParams.filter(param => !supportedParams.includes(param)); @@ -78,8 +77,15 @@ const createReportController = (reportType) => { return; } - // Validate and process technology array - const techArray = validateArrayParameter(params.technology, 'technology'); + // Validate and process technologies + const technologiesArray = validateArrayParameter(params.technology, 'technology'); + + // Validate and process versions + // Apply version filter with special handling for 'ALL' case + let versionsArray = ['ALL']; + if (technologiesArray.length === 1 && params.version) { + versionsArray = validateArrayParameter(params.version, 'version'); + } // Handle 'latest' date substitution let startDate = params.start; @@ -91,7 +97,8 @@ const createReportController = (reportType) => { const queryFilters = { geo: params.geo, rank: params.rank, - technology: techArray, + technology: technologiesArray, + version: versionsArray, startDate: startDate, endDate: params.end }; @@ -113,21 +120,21 @@ const createReportController = (reportType) => { query = query.where('rank', '==', params.rank); // Apply technology filter with batch processing - query = query.where('technology', 'in', techArray); + query = query.where('technology', 'in', technologiesArray); - // 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'); - } + // Apply version filter with batch processing + query = query.where('version', 'in', versionsArray); // Apply date filters if (startDate) query = query.where('date', '>=', startDate); if (params.end) query = query.where('date', '<=', params.end); // Apply field projection to optimize query - query = query.select('date', 'technology', config.dataField); + const selectFields = ['date', 'technology', config.dataField]; + if (!(versionsArray.length === 1 && versionsArray[0] === 'ALL')) { + selectFields.push('version'); + } + query = query.select(...selectFields); // Execute query const snapshot = await query.get(); diff --git a/src/controllers/technologiesController.js b/src/controllers/technologiesController.js index 5d75aba..1858a8a 100644 --- a/src/controllers/technologiesController.js +++ b/src/controllers/technologiesController.js @@ -69,7 +69,9 @@ const listTechnologies = async (req, res) => { // Include onlyname and fields in cache key calculation const customCacheKeyData = { onlyname: req.query.onlyname || false, - fields: req.query.fields + fields: req.query.fields, + technology: req.query.technology ? validateTechnologyArray(req.query.technology) : [], + category: req.query.category ? validateArrayParameter(req.query.category, 'category') : [], }; await executeQuery(req, res, 'technologies', queryBuilder, dataProcessor, customCacheKeyData); diff --git a/src/package.json b/src/package.json index c9764ac..2e0bfa8 100644 --- a/src/package.json +++ b/src/package.json @@ -10,7 +10,9 @@ "scripts": { "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" + "test:live": "bash ../test-api.sh", + "build": "docker build -t tech-report-api .", + "run": "docker run -p 8080:8080 tech-report-api" }, "dependencies": { "@google-cloud/firestore": "7.11.2", diff --git a/terraform/dev/local.auto.tfvars.template b/terraform/dev/local.auto.tfvars.template deleted file mode 100644 index 3ebe52e..0000000 --- a/terraform/dev/local.auto.tfvars.template +++ /dev/null @@ -1,4 +0,0 @@ -google_service_account_api_gateway = "" -google_service_account_cloud_functions = "" -project_database = "" - diff --git a/terraform/dev/main.tf b/terraform/dev/main.tf index db15672..94527ad 100644 --- a/terraform/dev/main.tf +++ b/terraform/dev/main.tf @@ -3,6 +3,13 @@ terraform { bucket = "tfstate-httparchive" prefix = "tech-report-apis/dev" } + + required_providers { + docker = { + source = "kreuzwerker/docker" + version = ">= 3.6.2" + } + } } provider "google" { @@ -11,23 +18,30 @@ provider "google" { request_timeout = "60m" } -resource "google_api_gateway_api" "api" { - provider = google-beta - api_id = "reports-api-dev" - display_name = "Reports API Gateway DEV" - project = var.project +provider "google-beta" { + project = var.project + region = var.region +} + +# Get current Google Cloud access token +data "google_client_config" "default" {} + +# Configure Docker provider with Artifact Registry authentication +provider "docker" { + registry_auth { + address = "${var.region}-docker.pkg.dev" + username = "oauth2accesstoken" + password = data.google_client_config.default.access_token + } } -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" - project = var.project - display_name = "Reports API Config DEV" - openapi_documents { - document { - path = "spec.yaml" - contents = base64encode(<<-EOF +module "gateway" { + source = "../modules/api-gateway" + project = var.project + environment = var.environment + region = var.region + service_account_email = var.google_service_account_api_gateway + spec_yaml = <