Skip to content

Commit 2078ac1

Browse files
james-crossJames-Cross
andauthored
add client groups API route, fetch script, and unit test (#103)
Co-authored-by: James-Cross <james@streetsupport.net>
1 parent 6c9fc25 commit 2078ac1

File tree

7 files changed

+309
-55
lines changed

7 files changed

+309
-55
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222

2323
"fetch:locations": "node ./scripts/fetch-locations.js",
2424
"fetch:service-categories": "node ./scripts/fetch-service-categories.js",
25-
"fetch:all": "npm-run-all fetch:locations fetch:service-categories"
25+
"fetch:client-groups": "node ./scripts/fetch-client-groups.js",
26+
"fetch:all": "npm-run-all fetch:locations fetch:service-categories fetch:client-groups"
2627
},
2728
"dependencies": {
2829
"@googlemaps/js-api-loader": "^1.16.8",

scripts/fetch-client-groups.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// scripts/fetch-client-groups.js
2+
3+
import fs from 'fs';
4+
import path from 'path';
5+
import dotenv from 'dotenv';
6+
import { MongoClient } from 'mongodb';
7+
8+
dotenv.config();
9+
10+
async function main() {
11+
const uri = process.env.MONGODB_URI;
12+
if (!uri) {
13+
throw new Error('Missing MONGODB_URI in environment');
14+
}
15+
16+
const client = new MongoClient(uri);
17+
try {
18+
await client.connect();
19+
const db = client.db('streetsupport');
20+
21+
const groups = await db
22+
.collection('ClientGroups')
23+
.find({})
24+
.project({ _id: 1, Key: 1, Name: 1 })
25+
.toArray();
26+
27+
// Transform to use lower camel case for key + name
28+
const cleaned = groups.map(g => ({
29+
_id: g._id.toString(),
30+
key: g.Key,
31+
name: g.Name,
32+
}));
33+
34+
const outputPath = path.join('./src/data/client-groups.json');
35+
fs.writeFileSync(outputPath, JSON.stringify(cleaned, null, 2));
36+
37+
console.log(`✅ Client groups saved to ${outputPath}`);
38+
} catch (err) {
39+
console.error('❌ Failed to fetch client groups:', err);
40+
} finally {
41+
await client.close();
42+
}
43+
}
44+
45+
main();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// src/app/api/client-groups/helper.ts
2+
3+
import { getClientPromise } from '@/utils/mongodb';
4+
5+
export async function getClientGroups() {
6+
const client = await getClientPromise();
7+
const db = client.db('streetsupport');
8+
9+
const groups = await db
10+
.collection('ClientGroups')
11+
.find({})
12+
.project({ _id: 1, Key: 1, Name: 1 })
13+
.toArray();
14+
15+
return groups.map(g => ({
16+
_id: g._id,
17+
key: g.Key,
18+
name: g.Name,
19+
}));
20+
}

src/app/api/client-groups/route.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
1+
// src/app/api/client-groups/route.ts
2+
13
import { NextResponse } from 'next/server';
2-
import { getClientPromise } from '@/utils/mongodb';
4+
import { getClientGroups } from './helper';
35

46
export async function GET() {
57
try {
6-
const client = await getClientPromise();
7-
const db = client.db('streetsupport');
8-
9-
// Query ClientGroups collection
10-
const groups = await db.collection('ClientGroups').find({}).toArray();
11-
12-
// Shape: id, name, slug (adjust based on actual fields)
13-
const output = groups.map((group) => ({
14-
id: group._id,
15-
name: group.Name,
16-
key: group.Key, // If your data uses a Key field
17-
}));
18-
19-
return NextResponse.json({ status: 'success', data: output });
8+
const groups = await getClientGroups();
9+
return NextResponse.json({ status: 'success', data: groups });
2010
} catch (error) {
21-
console.error(error);
22-
return NextResponse.json({
23-
status: 'error',
24-
message: error instanceof Error ? error.message : 'Unknown error',
25-
});
11+
console.error('Error fetching client groups:', error);
12+
return NextResponse.json(
13+
{ status: 'error', message: 'Failed to fetch client groups' },
14+
{ status: 500 }
15+
);
2616
}
2717
}

src/data/client-groups.json

Lines changed: 170 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,172 @@
11
[
2-
"age-10-24",
3-
"age-under-16",
4-
"families",
5-
"age-under-18",
6-
"age-under-19",
7-
"age-under-21",
8-
"age-under-25",
9-
"age-under-26",
10-
"age-11-25",
11-
"age-16+",
12-
"age-16-21",
13-
"age-16-24",
14-
"age-16-25",
15-
"age-18-25",
16-
"age-18-30",
17-
"age-18+",
18-
"age-25+",
19-
"age-26+",
20-
"age-40+",
21-
"male-only",
22-
"female-only",
23-
"bame",
24-
"asylum-seekers",
25-
"refugees",
26-
"eea-residents",
27-
"rough-sleepers",
28-
"veterans",
29-
"sex-workers",
30-
"ex-offenders",
31-
"lgbt+",
32-
"gay",
33-
"lesbian",
34-
"bisexual",
35-
"transgender"
2+
{
3+
"_id": "5bd060eb46e3da5220f9c8ba",
4+
"key": "refugees",
5+
"name": "Refugees"
6+
},
7+
{
8+
"_id": "5bf6b9e323c7a0408e8e6912",
9+
"key": "age-18-30",
10+
"name": "18-30 only"
11+
},
12+
{
13+
"_id": "5bd060eb46e3da5220f9c8be",
14+
"key": "lgbt+",
15+
"name": "LGBT+"
16+
},
17+
{
18+
"_id": "5bd060eb46e3da5220f9c8b8",
19+
"key": "bame",
20+
"name": "Black, Asian and Minority Ethnic"
21+
},
22+
{
23+
"_id": "5bd060eb46e3da5220f9c8bf",
24+
"key": "gay",
25+
"name": "Gay"
26+
},
27+
{
28+
"_id": "5bd060eb46e3da5220f9c8c1",
29+
"key": "bisexual",
30+
"name": "Bisexual"
31+
},
32+
{
33+
"_id": "5be0274846e3da23605eaf2e",
34+
"key": "age-40+",
35+
"name": "40+ only"
36+
},
37+
{
38+
"_id": "5bf4289c23c7a0408e8e690b",
39+
"key": "age-16+",
40+
"name": "16+ only"
41+
},
42+
{
43+
"_id": "5bd060eb46e3da5220f9c8c2",
44+
"key": "transgender",
45+
"name": "Transgender"
46+
},
47+
{
48+
"_id": "5be0274846e3da23605eaf27",
49+
"key": "age-11-25",
50+
"name": "11-25 only"
51+
},
52+
{
53+
"_id": "5bd060eb46e3da5220f9c8b6",
54+
"key": "female-only",
55+
"name": "Female only"
56+
},
57+
{
58+
"_id": "5be0274846e3da23605eaf26",
59+
"key": "age-under-21",
60+
"name": "Under 21s only"
61+
},
62+
{
63+
"_id": "5bd060eb46e3da5220f9c8b9",
64+
"key": "asylum-seekers",
65+
"name": "Asylum seekers"
66+
},
67+
{
68+
"_id": "5be0274846e3da23605eaf2c",
69+
"key": "age-25+",
70+
"name": "25+ only"
71+
},
72+
{
73+
"_id": "5be0274846e3da23605eaf2d",
74+
"key": "age-26+",
75+
"name": "26+ only"
76+
},
77+
{
78+
"_id": "5bf3ee8a23c7a0408e8e6909",
79+
"key": "age-under-16",
80+
"name": "Under 16s only"
81+
},
82+
{
83+
"_id": "5bf6b9c323c7a0408e8e6911",
84+
"key": "age-under-26",
85+
"name": "Under 26s only"
86+
},
87+
{
88+
"_id": "5bd060eb46e3da5220f9c8bb",
89+
"key": "eea-residents",
90+
"name": "EEA residents"
91+
},
92+
{
93+
"_id": "5bd060eb46e3da5220f9c8c0",
94+
"key": "lesbian",
95+
"name": "Lesbian"
96+
},
97+
{
98+
"_id": "5be0274846e3da23605eaf28",
99+
"key": "age-16-21",
100+
"name": "16-21 only"
101+
},
102+
{
103+
"_id": "5bf4295323c7a0408e8e690c",
104+
"key": "ex-offenders",
105+
"name": "Ex-Offenders"
106+
},
107+
{
108+
"_id": "5be0274846e3da23605eaf2a",
109+
"key": "age-18-25",
110+
"name": "18-25 only"
111+
},
112+
{
113+
"_id": "5be0274846e3da23605eaf29",
114+
"key": "age-16-25",
115+
"name": "16-25 only"
116+
},
117+
{
118+
"_id": "5be0274846e3da23605eaf2b",
119+
"key": "age-18+",
120+
"name": "18+ only"
121+
},
122+
{
123+
"_id": "5bd060eb46e3da5220f9c8b5",
124+
"key": "male-only",
125+
"name": "Male only"
126+
},
127+
{
128+
"_id": "5bf6b96e23c7a0408e8e6910",
129+
"key": "age-10-24",
130+
"name": "10-24 only"
131+
},
132+
{
133+
"_id": "5be0274846e3da23605eaf24",
134+
"key": "age-under-18",
135+
"name": "Under 18s only"
136+
},
137+
{
138+
"_id": "5be0274846e3da23605eaf25",
139+
"key": "age-under-19",
140+
"name": "Under 19s only"
141+
},
142+
{
143+
"_id": "5be0274846e3da23605eaf2f",
144+
"key": "sex-workers",
145+
"name": "Sex workers"
146+
},
147+
{
148+
"_id": "5bd060eb46e3da5220f9c8bc",
149+
"key": "rough-sleepers",
150+
"name": "Rough sleepers"
151+
},
152+
{
153+
"_id": "5bd060eb46e3da5220f9c8bd",
154+
"key": "veterans",
155+
"name": "Armed forces veterans"
156+
},
157+
{
158+
"_id": "5bed9f4623c7a0408e8e6904",
159+
"key": "age-under-25",
160+
"name": "Under 25s only"
161+
},
162+
{
163+
"_id": "5bf435a423c7a0408e8e690d",
164+
"key": "age-16-24",
165+
"name": "16-24 only"
166+
},
167+
{
168+
"_id": "5f844e470532b71444f305ff",
169+
"key": "families",
170+
"name": "Families"
171+
}
36172
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// tests/__tests__/api/client-groups.test.ts
2+
3+
import { getClientGroups } from '../../../src/app/api/client-groups/helper';
4+
5+
jest.mock('@/utils/mongodb', () => ({
6+
getClientPromise: jest.fn(),
7+
}));
8+
9+
const mockClient = {
10+
db: jest.fn().mockReturnThis(),
11+
collection: jest.fn().mockReturnThis(),
12+
find: jest.fn().mockReturnThis(),
13+
project: jest.fn().mockReturnThis(),
14+
toArray: jest.fn(),
15+
};
16+
17+
describe('getClientGroups', () => {
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
it('should return formatted client groups', async () => {
23+
const mockData = [
24+
{ _id: '1', Key: 'refugees', Name: 'Refugees' },
25+
{ _id: '2', Key: 'families', Name: 'Families' },
26+
];
27+
28+
mockClient.toArray.mockResolvedValueOnce(mockData);
29+
30+
const { getClientPromise } = require('@/utils/mongodb');
31+
getClientPromise.mockResolvedValueOnce(mockClient);
32+
33+
const result = await getClientGroups();
34+
35+
expect(result).toEqual([
36+
{ _id: '1', key: 'refugees', name: 'Refugees' },
37+
{ _id: '2', key: 'families', name: 'Families' },
38+
]);
39+
});
40+
41+
it('should throw if the database call fails', async () => {
42+
const { getClientPromise } = require('@/utils/mongodb');
43+
getClientPromise.mockRejectedValueOnce(new Error('DB down'));
44+
45+
await expect(getClientGroups()).rejects.toThrow('DB down');
46+
});
47+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import fs from 'fs';
2+
3+
describe('fetch-client-groups script output', () => {
4+
it('should create a valid client-groups.json file', () => {
5+
const raw = fs.readFileSync('src/data/client-groups.json', 'utf8');
6+
const data = JSON.parse(raw);
7+
8+
expect(Array.isArray(data)).toBe(true);
9+
data.forEach(group => {
10+
expect(group).toHaveProperty('_id');
11+
expect(group).toHaveProperty('key');
12+
expect(group).toHaveProperty('name');
13+
});
14+
});
15+
});

0 commit comments

Comments
 (0)