Skip to content

Commit 6dc1003

Browse files
committed
chore: release v1.5.3
1 parent 7defe19 commit 6dc1003

6 files changed

Lines changed: 734 additions & 76 deletions

File tree

packages/backend/src/routes/dso.routes.ts

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ const getEnv = (req: Request): 'pre' | 'prod' => {
2020
router.post('/activiteiten/oin', async (req: Request, res: Response) => {
2121
res.set('API-Version', packageJson.version);
2222
try {
23-
const { oin, datumVanaf } = req.body as { oin?: string; datumVanaf?: string };
23+
const { oin, datum } = req.body as { oin?: string; datum?: string };
2424
if (!oin) {
2525
return res.status(400).json({ success: false, error: 'oin is required' });
2626
}
27-
const data = await dsoService.getActiviteitenByOin(oin, getEnv(req), datumVanaf);
27+
const data = await dsoService.getActiviteitenByOin(oin, getEnv(req), datum);
2828
res.status(200).json({ success: true, data });
2929
} catch (error) {
3030
const msg = error instanceof Error ? error.message : 'DSO request failed';
@@ -49,7 +49,10 @@ router.post('/activiteiten/zoek', async (req: Request, res: Response) => {
4949
page?: number;
5050
pageSize?: number;
5151
};
52-
const data = await dsoService.zoekActiviteiten({ datum, lat, lon, page, pageSize }, getEnv(req));
52+
const data = await dsoService.zoekActiviteiten(
53+
{ datum, lat, lon, page, pageSize },
54+
getEnv(req)
55+
);
5356
res.status(200).json({ success: true, data });
5457
} catch (error) {
5558
const msg = error instanceof Error ? error.message : 'DSO request failed';
@@ -100,12 +103,15 @@ router.get('/begrippen', async (req: Request, res: Response) => {
100103
try {
101104
const { zoekTerm, geldigOp, page, pageSize } = req.query;
102105

103-
const data = await dsoService.getBegrippen({
104-
zoekTerm: zoekTerm as string | undefined,
105-
geldigOp: geldigOp as string | undefined,
106-
page: page ? parseInt(page as string, 10) : undefined,
107-
pageSize: pageSize ? parseInt(pageSize as string, 10) : undefined,
108-
}, getEnv(req));
106+
const data = await dsoService.getBegrippen(
107+
{
108+
zoekTerm: zoekTerm as string | undefined,
109+
geldigOp: geldigOp as string | undefined,
110+
page: page ? parseInt(page as string, 10) : undefined,
111+
pageSize: pageSize ? parseInt(pageSize as string, 10) : undefined,
112+
},
113+
getEnv(req)
114+
);
109115

110116
res.status(200).json({ success: true, data });
111117
} catch (error) {
@@ -134,11 +140,14 @@ router.get('/activiteiten', async (req: Request, res: Response) => {
134140
try {
135141
const { datum, page, pageSize } = req.query;
136142

137-
const data = await dsoService.getActiviteiten({
138-
datum: datum as string | undefined,
139-
page: page ? parseInt(page as string, 10) : undefined,
140-
pageSize: pageSize ? parseInt(pageSize as string, 10) : undefined,
141-
}, getEnv(req));
143+
const data = await dsoService.getActiviteiten(
144+
{
145+
datum: datum as string | undefined,
146+
page: page ? parseInt(page as string, 10) : undefined,
147+
pageSize: pageSize ? parseInt(pageSize as string, 10) : undefined,
148+
},
149+
getEnv(req)
150+
);
142151

143152
res.status(200).json({ success: true, data });
144153
} catch (error) {
@@ -152,4 +161,65 @@ router.get('/activiteiten', async (req: Request, res: Response) => {
152161
}
153162
});
154163

164+
/**
165+
* POST /v1/dso/werkzaamheden/zoek
166+
* Search werkzaamheden via the Zoekinterface.
167+
* Body: { zoekterm?: string, page?: number, pageSize?: number }
168+
*/
169+
router.post('/werkzaamheden/zoek', async (req: Request, res: Response) => {
170+
res.set('API-Version', packageJson.version);
171+
try {
172+
const { zoekterm, page, pageSize } = req.body as {
173+
zoekterm?: string;
174+
page?: number;
175+
pageSize?: number;
176+
};
177+
const data = await dsoService.zoekWerkzaamheden({ zoekterm, page, pageSize }, getEnv(req));
178+
res.status(200).json({ success: true, data });
179+
} catch (error) {
180+
const msg = error instanceof Error ? error.message : 'DSO request failed';
181+
logger.error('[DSO Routes] POST /werkzaamheden/zoek failed', { error: msg });
182+
res.status(502).json({ success: false, error: msg });
183+
}
184+
});
185+
186+
/**
187+
* POST /v1/dso/werkzaamheden/suggereer
188+
* Autocomplete suggestions for werkzaamheden search terms.
189+
* Body: { zoekterm: string }
190+
*/
191+
router.post('/werkzaamheden/suggereer', async (req: Request, res: Response) => {
192+
res.set('API-Version', packageJson.version);
193+
try {
194+
const { zoekterm } = req.body as { zoekterm?: string };
195+
if (!zoekterm) {
196+
return res.status(400).json({ success: false, error: 'zoekterm is required' });
197+
}
198+
const data = await dsoService.suggereerWerkzaamheden(zoekterm, getEnv(req));
199+
res.status(200).json({ success: true, data });
200+
} catch (error) {
201+
const msg = error instanceof Error ? error.message : 'DSO request failed';
202+
logger.error('[DSO Routes] POST /werkzaamheden/suggereer failed', { error: msg });
203+
res.status(502).json({ success: false, error: msg });
204+
}
205+
});
206+
207+
/**
208+
* GET /v1/dso/werkzaamheden/:urn
209+
* Retrieve versioned detail for a single werkzaamheid.
210+
*/
211+
router.get('/werkzaamheden/:urn', async (req: Request, res: Response) => {
212+
res.set('API-Version', packageJson.version);
213+
try {
214+
const urn = decodeURIComponent(req.params.urn);
215+
const data = await dsoService.getWerkzaamheidDetail(urn, getEnv(req));
216+
res.status(200).json({ success: true, data });
217+
} catch (error) {
218+
const msg = error instanceof Error ? error.message : 'DSO request failed';
219+
const status = msg.includes('404') ? 404 : 502;
220+
logger.error('[DSO Routes] GET /werkzaamheden/:urn failed', { error: msg });
221+
res.status(status).json({ success: false, error: msg });
222+
}
223+
});
224+
155225
export default router;

packages/backend/src/services/dso.service.ts

Lines changed: 146 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export interface BegrippenOptions {
5555
* GET /begrippen — search or list concepts from the Stelselcatalogus.
5656
* Returns the raw HAL response (items live in _embedded.begrippen).
5757
*/
58-
export async function getBegrippen(opts: BegrippenOptions = {}, env: DsoEnv = 'pre'): Promise<unknown> {
58+
export async function getBegrippen(
59+
opts: BegrippenOptions = {},
60+
env: DsoEnv = 'pre'
61+
): Promise<unknown> {
5962
const params = new URLSearchParams();
6063
if (opts.zoekTerm) params.set('zoekTerm', opts.zoekTerm);
6164
if (opts.geldigOp) params.set('geldigOp', opts.geldigOp);
@@ -85,17 +88,26 @@ export interface ZoekOptions {
8588
pageSize?: number;
8689
}
8790

88-
export async function getActiviteitenByOin(oin: string, env: DsoEnv = 'pre', datumVanaf?: string): Promise<unknown> {
89-
const url = `${getDsoConfig(env).rtrBaseUrl}/activiteiten/_wijzigingen`;
91+
export async function getActiviteitenByOin(
92+
oin: string,
93+
env: DsoEnv = 'pre',
94+
datum?: string
95+
): Promise<unknown> {
96+
const d = new Date();
97+
const today = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
98+
const effectiveDatum = datum ?? today;
9099

91-
// Default to yesterday if no date provided — today returns empty due to DSO timing
92-
if (!datumVanaf) {
93-
const d = new Date();
94-
d.setDate(d.getDate() - 1);
95-
datumVanaf = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
96-
}
100+
const params = new URLSearchParams();
101+
params.set('page', '1');
102+
params.set('pageSize', '100');
103+
104+
const body = {
105+
datum: effectiveDatum,
106+
bestuursorgaan: { oin },
107+
};
97108

98-
logger.info('[DSO] POST activiteiten/_wijzigingen', { env, oin, datumVanaf });
109+
const url = `${getDsoConfig(env).rtrBaseUrl}/activiteiten/_zoek?${params}`;
110+
logger.info('[DSO] POST activiteiten/_zoek by OIN', { env, oin, datum: effectiveDatum });
99111

100112
const controller = new AbortController();
101113
const timeoutId = setTimeout(() => controller.abort(), config.dso.timeout);
@@ -107,7 +119,7 @@ export async function getActiviteitenByOin(oin: string, env: DsoEnv = 'pre', dat
107119
'Content-Type': 'application/json',
108120
Accept: 'application/hal+json',
109121
},
110-
body: JSON.stringify({ oin, datumVanaf }),
122+
body: JSON.stringify(body),
111123
signal: controller.signal,
112124
});
113125
if (!response.ok) {
@@ -120,7 +132,10 @@ export async function getActiviteitenByOin(oin: string, env: DsoEnv = 'pre', dat
120132
}
121133
}
122134

123-
export async function zoekActiviteiten(opts: ZoekOptions = {}, env: DsoEnv = 'pre'): Promise<unknown> {
135+
export async function zoekActiviteiten(
136+
opts: ZoekOptions = {},
137+
env: DsoEnv = 'pre'
138+
): Promise<unknown> {
124139
const d = new Date();
125140
const today = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
126141
const datum = opts.datum ?? today;
@@ -173,7 +188,11 @@ export async function zoekActiviteiten(opts: ZoekOptions = {}, env: DsoEnv = 'pr
173188
}
174189
}
175190

176-
export async function getActiviteit(urn: string, datum?: string, env: DsoEnv = 'pre'): Promise<unknown> {
191+
export async function getActiviteit(
192+
urn: string,
193+
datum?: string,
194+
env: DsoEnv = 'pre'
195+
): Promise<unknown> {
177196
const d = new Date();
178197
const today = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
179198

@@ -189,7 +208,10 @@ export async function getActiviteit(urn: string, datum?: string, env: DsoEnv = '
189208
* GET /activiteiten — all activities valid on a given date.
190209
* `datum` is required by DSO; we default to today when omitted.
191210
*/
192-
export async function getActiviteiten(opts: ActiviteitenOptions = {}, env: DsoEnv = 'pre'): Promise<unknown> {
211+
export async function getActiviteiten(
212+
opts: ActiviteitenOptions = {},
213+
env: DsoEnv = 'pre'
214+
): Promise<unknown> {
193215
const d = new Date();
194216
const datum =
195217
opts.datum ??
@@ -204,3 +226,113 @@ export async function getActiviteiten(opts: ActiviteitenOptions = {}, env: DsoEn
204226
logger.info('[DSO] GET activiteiten', { env, datum, page: opts.page });
205227
return dsoFetch(url, env);
206228
}
229+
230+
// ---------------------------------------------------------------------------
231+
// Zoekinterface API
232+
// ---------------------------------------------------------------------------
233+
234+
export interface ZoekWerkzaamhedenOptions {
235+
zoekterm?: string;
236+
page?: number;
237+
pageSize?: number;
238+
}
239+
240+
export async function zoekWerkzaamheden(
241+
opts: ZoekWerkzaamhedenOptions = {},
242+
env: DsoEnv = 'pre'
243+
): Promise<unknown> {
244+
const params = new URLSearchParams();
245+
params.set('page', String(opts.page ?? 1));
246+
params.set('pageSize', String(opts.pageSize ?? 20));
247+
248+
const body: Record<string, unknown> = {};
249+
if (opts.zoekterm) body.zoekterm = opts.zoekterm;
250+
251+
const url = `${getDsoConfig(env).zoekinterfaceBaseUrl}/werkzaamheden/_zoek?${params}`;
252+
logger.info('[DSO] POST zoekinterface/werkzaamheden/_zoek', { env, zoekterm: opts.zoekterm });
253+
254+
const controller = new AbortController();
255+
const timeoutId = setTimeout(() => controller.abort(), config.dso.timeout);
256+
try {
257+
const response = await fetch(url, {
258+
method: 'POST',
259+
headers: {
260+
'x-api-key': getDsoConfig(env).apiKey,
261+
'Content-Type': 'application/json',
262+
Accept: 'application/hal+json',
263+
},
264+
body: JSON.stringify(body),
265+
signal: controller.signal,
266+
});
267+
if (!response.ok) {
268+
const text = await response.text();
269+
throw new Error(`DSO responded ${response.status}: ${text}`);
270+
}
271+
return response.json();
272+
} finally {
273+
clearTimeout(timeoutId);
274+
}
275+
}
276+
277+
export async function suggereerWerkzaamheden(
278+
zoekterm: string,
279+
env: DsoEnv = 'pre'
280+
): Promise<unknown> {
281+
const url = `${getDsoConfig(env).zoekinterfaceBaseUrl}/werkzaamheden/_suggereer`;
282+
logger.info('[DSO] POST zoekinterface/werkzaamheden/_suggereer', { env, zoekterm });
283+
284+
const controller = new AbortController();
285+
const timeoutId = setTimeout(() => controller.abort(), config.dso.timeout);
286+
try {
287+
const response = await fetch(url, {
288+
method: 'POST',
289+
headers: {
290+
'x-api-key': getDsoConfig(env).apiKey,
291+
'Content-Type': 'application/json',
292+
Accept: 'application/json',
293+
},
294+
body: JSON.stringify({ zoekterm }),
295+
signal: controller.signal,
296+
});
297+
if (!response.ok) {
298+
const text = await response.text();
299+
throw new Error(`DSO responded ${response.status}: ${text}`);
300+
}
301+
return response.json();
302+
} finally {
303+
clearTimeout(timeoutId);
304+
}
305+
}
306+
307+
// ---------------------------------------------------------------------------
308+
// Opvragen Werkzaamheden API
309+
// ---------------------------------------------------------------------------
310+
311+
export async function getWerkzaamheidDetail(urn: string, env: DsoEnv = 'pre'): Promise<unknown> {
312+
// expand=true includes trefwoorden and logischeRelaties
313+
314+
const params = new URLSearchParams({
315+
pageSize: '100',
316+
});
317+
318+
const url = `${getDsoConfig(env).opvragenWerkzaamhedenBaseUrl}/werkzaamheden/${encodeURIComponent(urn)}?${params}`;
319+
logger.info('[DSO] GET opvragen werkzaamheid detail request', { env, urn, url });
320+
321+
const controller = new AbortController();
322+
const timeoutId = setTimeout(() => controller.abort(), config.dso.timeout);
323+
try {
324+
const response = await fetch(url, {
325+
headers: { 'x-api-key': getDsoConfig(env).apiKey, Accept: 'application/hal+json' },
326+
signal: controller.signal,
327+
});
328+
const text = await response.text();
329+
logger.info('[DSO] GET opvragen werkzaamheid detail response', {
330+
status: response.status,
331+
body: text.substring(0, 500),
332+
});
333+
if (!response.ok) throw new Error(`DSO responded ${response.status}: ${text}`);
334+
return JSON.parse(text);
335+
} finally {
336+
clearTimeout(timeoutId);
337+
}
338+
}

packages/backend/src/utils/config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ export const config = {
4343
rtrBaseUrl:
4444
process.env.DSO_RTR_BASE_URL ||
4545
'https://service.pre.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/rtrgegevens/v2',
46+
zoekinterfaceBaseUrl:
47+
process.env.DSO_ZOEKINTERFACE_BASE_URL ||
48+
'https://service.pre.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/zoekinterface/v2',
49+
opvragenWerkzaamhedenBaseUrl:
50+
process.env.DSO_OPVRAGEN_WERKZAAMHEDEN_BASE_URL ||
51+
'https://service.pre.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/opvragenwerkzaamheden/v1',
4652
apiKey: process.env.DSO_API_KEY || '',
4753
timeout: parseInt(process.env.DSO_TIMEOUT || '15000', 10),
4854
},
@@ -54,8 +60,13 @@ export const config = {
5460
rtrBaseUrl:
5561
process.env.DSO_RTR_BASE_URL_PROD ||
5662
'https://service.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/rtrgegevens/v2',
63+
zoekinterfaceBaseUrl:
64+
process.env.DSO_ZOEKINTERFACE_BASE_URL_PROD ||
65+
'https://service.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/zoekinterface/v2',
66+
opvragenWerkzaamhedenBaseUrl:
67+
process.env.DSO_OPVRAGEN_WERKZAAMHEDEN_BASE_URL_PROD ||
68+
'https://service.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/opvragenwerkzaamheden/v1',
5769
apiKey: process.env.DSO_API_KEY_PROD || '',
58-
timeout: parseInt(process.env.DSO_TIMEOUT || '15000', 10),
5970
},
6071

6172
edocs: {

0 commit comments

Comments
 (0)