Skip to content

Commit ae4074e

Browse files
committed
nodejs alt
1 parent c1d1a52 commit ae4074e

31 files changed

+7063
-153
lines changed

.github/workflows/pipeline.yaml

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@ jobs:
4444
credentials_json: ${{ env.PIPELINE_SA_KEY }}
4545

4646
- uses: hashicorp/setup-terraform@v3
47-
47+
4848
- name: Terraform fmt
4949
id: fmt
5050
run: terraform fmt -check
5151
continue-on-error: true
52-
52+
5353
- name: Terraform Init
5454
id: init
5555
run: terraform init
56-
56+
5757
- name: Terraform Validate
5858
id: validate
5959
run: terraform validate -no-color
60-
60+
6161
- name: Terraform Plan
6262
id: plan
6363
run: |
@@ -77,7 +77,7 @@ jobs:
7777
-var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \
7878
-var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_DEV }}" \
7979
-auto-approve
80-
80+
8181
deploy_production:
8282
if: github.ref == 'refs/heads/main'
8383
runs-on: ubuntu-latest
@@ -94,20 +94,20 @@ jobs:
9494
credentials_json: ${{ env.PIPELINE_SA_KEY }}
9595

9696
- uses: hashicorp/setup-terraform@v3
97-
97+
9898
- name: Terraform fmt
9999
id: fmt
100100
run: terraform fmt -check
101101
continue-on-error: true
102-
102+
103103
- name: Terraform Init
104104
id: init
105105
run: terraform init
106-
106+
107107
- name: Terraform Validate
108108
id: validate
109109
run: terraform validate -no-color
110-
110+
111111
- name: Terraform Plan
112112
id: plan
113113
run: |
@@ -127,5 +127,4 @@ jobs:
127127
-var="google_service_account_api_gateway=${{ env.PIPELINE_GOOGLE_SERVICE_ACCOUNT_API_GATEWAY }}" \
128128
-var="project_database=${{ env.PIPELINE_PROJECT_DATABASE_PROD }}" \
129129
-auto-approve
130-
131-
130+

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ __pycache__
3434

3535
utils.txt
3636
logs
37+
38+
node_modules/

README_node.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Technology Reports API (Node.js)
2+
3+
This is a unified Google Cloud Run function that provides technology metrics and information via various endpoints.
4+
5+
## Setup
6+
7+
### Prerequisites
8+
9+
- Node.js 18+
10+
- npm
11+
- Google Cloud account with necessary permissions
12+
13+
### Local Development
14+
15+
1. Install dependencies:
16+
```bash
17+
npm install
18+
```
19+
20+
2. Set environment variables:
21+
```bash
22+
export PROJECT=your-gcp-project-id
23+
export DATABASE=your-firestore-database
24+
```
25+
26+
3. Run the application locally:
27+
```bash
28+
npm start
29+
```
30+
31+
The API will be available at http://localhost:8080
32+
33+
## Deployment
34+
35+
### Using Google Cloud Build
36+
37+
```bash
38+
gcloud builds submit --tag gcr.io/PROJECT_ID/tech-report-api
39+
gcloud run deploy tech-report-api --image gcr.io/PROJECT_ID/tech-report-api --platform managed
40+
```
41+
42+
## API Endpoints
43+
44+
### `GET /technologies`
45+
46+
Lists available technologies with optional filtering.
47+
48+
#### Parameters
49+
50+
- `technology` (optional): Filter by technology name(s) - comma-separated list
51+
- `category` (optional): Filter by category - comma-separated list
52+
- `onlyname` (optional): If present, returns only technology names
53+
54+
### `GET /categories`
55+
56+
Lists available categories.
57+
58+
#### Parameters
59+
60+
- `category` (optional): Filter by category name(s) - comma-separated list
61+
- `onlyname` (optional): If present, returns only category names
62+
63+
### `GET /adoption`
64+
65+
Provides technology adoption data.
66+
67+
#### Parameters
68+
69+
- `technology` (required): Filter by technology name(s) - comma-separated list
70+
- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest')
71+
- `end` (optional): Filter by date range end (YYYY-MM-DD)
72+
- `geo` (optional): Filter by geographic location
73+
- `rank` (optional): Filter by rank
74+
75+
### `GET /cwvtech` (Core Web Vitals)
76+
77+
Provides Core Web Vitals metrics for technologies.
78+
79+
#### Parameters
80+
81+
- `technology` (required): Filter by technology name(s) - comma-separated list
82+
- `geo` (required): Filter by geographic location
83+
- `rank` (required): Filter by rank
84+
- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest')
85+
- `end` (optional): Filter by date range end (YYYY-MM-DD)
86+
87+
### `GET /lighthouse`
88+
89+
Provides Lighthouse scores for technologies.
90+
91+
#### Parameters
92+
93+
- `technology` (required): Filter by technology name(s) - comma-separated list
94+
- `geo` (required): Filter by geographic location
95+
- `rank` (required): Filter by rank
96+
- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest')
97+
- `end` (optional): Filter by date range end (YYYY-MM-DD)
98+
99+
### `GET /page-weight`
100+
101+
Provides Page Weight metrics for technologies.
102+
103+
#### Parameters
104+
105+
- `technology` (required): Filter by technology name(s) - comma-separated list
106+
- `geo` (optional): Filter by geographic location
107+
- `rank` (optional): Filter by rank
108+
- `start` (optional): Filter by date range start (YYYY-MM-DD or 'latest')
109+
- `end` (optional): Filter by date range end (YYYY-MM-DD)
110+
111+
### `GET /ranks`
112+
113+
Lists all available ranks.
114+
115+
### `GET /geos`
116+
117+
Lists all available geographic locations.
118+
119+
## Response Format
120+
121+
All API responses follow this format:
122+
123+
```json
124+
{
125+
"success": true,
126+
"result": [
127+
// Array of data objects
128+
]
129+
}
130+
```
131+
132+
Or in case of an error:
133+
134+
```json
135+
{
136+
"success": false,
137+
"errors": [
138+
{"key": "error message"}
139+
]
140+
}
141+
```

src/Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM node:24-slim
2+
3+
WORKDIR /app
4+
5+
# Copy package.json and package-lock.json
6+
COPY package*.json ./
7+
8+
# Install dependencies
9+
RUN npm ci --only=production
10+
11+
# Copy the rest of the application
12+
COPY . .
13+
14+
# Set environment variables
15+
ENV PROJECT=httparchive
16+
ENV DATABASE=tech-report-apis-prod
17+
18+
# Expose the port the app runs on
19+
EXPOSE 8080
20+
21+
# Start the application
22+
CMD [ "npm", "start" ]

src/__tests__/routes.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const request = require('supertest');
2+
const app = require('../src/src/index');
3+
4+
// Mock Firestore
5+
jest.mock('../src/utils/db', () => {
6+
return {
7+
collection: jest.fn().mockReturnThis(),
8+
where: jest.fn().mockReturnThis(),
9+
orderBy: jest.fn().mockReturnThis(),
10+
limit: jest.fn().mockReturnThis(),
11+
get: jest.fn().mockResolvedValue({
12+
empty: false,
13+
forEach: (callback) => {
14+
callback({
15+
data: () => ({
16+
technology: 'Test Technology',
17+
category: 'Test Category',
18+
description: 'Test Description',
19+
icon: 'test-icon.svg',
20+
origins: 1000
21+
}),
22+
get: (field) => 'Test Value'
23+
});
24+
},
25+
docs: [{
26+
data: () => ({
27+
technology: 'Test Technology',
28+
category: 'Test Category',
29+
description: 'Test Description',
30+
icon: 'test-icon.svg',
31+
origins: 1000,
32+
date: '2023-01-01'
33+
})
34+
}]
35+
})
36+
};
37+
});
38+
39+
describe('API Routes', () => {
40+
describe('GET /', () => {
41+
it('should return a health check response', async () => {
42+
const res = await request(app).get('/');
43+
expect(res.statusCode).toEqual(200);
44+
expect(res.body).toHaveProperty('status', 'ok');
45+
});
46+
});
47+
48+
describe('GET /technologies', () => {
49+
it('should return technologies', async () => {
50+
const res = await request(app).get('/technologies');
51+
expect(res.statusCode).toEqual(200);
52+
expect(res.body).toHaveProperty('success', true);
53+
expect(res.body).toHaveProperty('result');
54+
expect(Array.isArray(res.body.result)).toBe(true);
55+
});
56+
57+
it('should filter technologies by name', async () => {
58+
const res = await request(app).get('/technologies?technology=Test');
59+
expect(res.statusCode).toEqual(200);
60+
expect(res.body).toHaveProperty('success', true);
61+
});
62+
63+
it('should return only names when onlyname parameter is provided', async () => {
64+
const res = await request(app).get('/technologies?onlyname=true');
65+
expect(res.statusCode).toEqual(200);
66+
expect(res.body).toHaveProperty('success', true);
67+
});
68+
});
69+
70+
});

src/controllers/adoptionController.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const firestore = require('../utils/db');
2+
const { convertToArray, createSuccessResponse, createErrorResponse } = require('../utils/helpers');
3+
4+
const TABLE = 'adoption';
5+
6+
/**
7+
* Get the latest date in the collection
8+
*/
9+
const getLatestDate = async () => {
10+
const query = firestore.collection(TABLE).orderBy('date', 'desc').limit(1);
11+
const snapshot = await query.get();
12+
if (!snapshot.empty) {
13+
return snapshot.docs[0].data().date;
14+
}
15+
return null;
16+
};
17+
18+
/**
19+
* List adoption data with filtering
20+
*/
21+
const listAdoptionData = async (req, res) => {
22+
try {
23+
const params = req.query;
24+
const data = [];
25+
26+
// Technology is required
27+
if (!params.technology) {
28+
return res.status(400).send(createErrorResponse([
29+
['technology', 'missing technology parameter']
30+
]));
31+
}
32+
33+
// Convert technology parameter to array
34+
const techArray = convertToArray(params.technology);
35+
36+
// Handle 'latest' special value for start parameter
37+
if (params.start && params.start === 'latest') {
38+
params.start = await getLatestDate();
39+
}
40+
41+
// Query for each technology
42+
for (const technology of techArray) {
43+
let query = firestore.collection(TABLE);
44+
45+
// Apply filters
46+
if (params.start) {
47+
query = query.where('date', '>=', params.start);
48+
}
49+
50+
if (params.end) {
51+
query = query.where('date', '<=', params.end);
52+
}
53+
54+
if (params.geo) {
55+
query = query.where('geo', '==', params.geo);
56+
}
57+
58+
if (params.rank) {
59+
query = query.where('rank', '==', params.rank);
60+
}
61+
62+
// Always filter by technology
63+
query = query.where('technology', '==', technology);
64+
65+
// Execute query
66+
const snapshot = await query.get();
67+
snapshot.forEach(doc => {
68+
data.push(doc.data());
69+
});
70+
}
71+
72+
// Send response
73+
res.status(200).send(createSuccessResponse(data));
74+
} catch (error) {
75+
console.error('Error fetching adoption data:', error);
76+
res.status(400).send(createErrorResponse([['query', error.message]]));
77+
}
78+
};
79+
80+
module.exports = {
81+
listAdoptionData
82+
};

0 commit comments

Comments
 (0)