diff --git a/apps/overige-objecten-api/src/__mocks__/getStrapiKennisartikelData.ts b/apps/overige-objecten-api/src/__mocks__/getStrapiKennisartikelData.ts index c48021f4..5ba8b389 100644 --- a/apps/overige-objecten-api/src/__mocks__/getStrapiKennisartikelData.ts +++ b/apps/overige-objecten-api/src/__mocks__/getStrapiKennisartikelData.ts @@ -6,7 +6,7 @@ export const getStrapiKennisartikelData = () => { pagination: { total: 1, page: 1, - pageSize: 10, + pageSize: 1, pageCount: 1, }, }, diff --git a/apps/overige-objecten-api/src/__mocks__/getStrapiVacData.ts b/apps/overige-objecten-api/src/__mocks__/getStrapiVacData.ts index e32cbf58..42dbca3e 100644 --- a/apps/overige-objecten-api/src/__mocks__/getStrapiVacData.ts +++ b/apps/overige-objecten-api/src/__mocks__/getStrapiVacData.ts @@ -2,6 +2,14 @@ export const getStrapiVacData = () => { return { data: { vacs: { + meta: { + pagination: { + total: 3, + page: 1, + pageSize: 1, + pageCount: 1, + }, + }, data: [ { id: '1', diff --git a/apps/overige-objecten-api/src/controllers/objects/index.test.ts b/apps/overige-objecten-api/src/controllers/objects/index.test.ts index 8399b3f2..ba92ea96 100644 --- a/apps/overige-objecten-api/src/controllers/objects/index.test.ts +++ b/apps/overige-objecten-api/src/controllers/objects/index.test.ts @@ -29,13 +29,33 @@ describe('Objects controller', () => { describe('GET /api/objects', () => { it('should return kennisartikel & VAC by default', async () => { + const spy = jest + .spyOn(require('../../utils/getPaginatedResponse'), 'getPaginatedResponse') + .mockImplementation(() => + Promise.resolve({ + page: 1, + pageSize: 10, + count: 3, + total: 2, + next: null, + previous: null, + }), + ); fetchMock.mockResponseOnce(JSON.stringify(getStrapiKennisartikelData())); fetchMock.mockResponseOnce(JSON.stringify(getStrapiVacData())); const response = await request(app).get('/api/v2/objects').set('Authorization', 'Token YOUR_API_TOKEN'); - expect(response.status).toBe(200); expect(response.ok).toBe(true); - expect(response.body).toStrictEqual(objectsResponseData({})); + expect(response.body).toStrictEqual({ + page: 1, + pageSize: 10, + count: 6, + next: null, + previous: null, + total: 4, + ...objectsResponseData({}), + }); + spy.mockRestore(); }); describe('pagination', () => { it('should response the whole data by default', async () => { @@ -44,9 +64,9 @@ describe('Objects controller', () => { .mockImplementation(() => Promise.resolve({ page: 1, - pageSize: 10, - count: 1, - total: 100, + pageSize: 1, + count: 2, + total: 1, next: null, previous: null, }), @@ -59,9 +79,9 @@ describe('Objects controller', () => { expect(response.body).toStrictEqual({ ...objectsResponseData({}), page: 1, - pageSize: 10, - count: 1, - total: 100, + pageSize: 1, + count: 4, + total: 2, next: null, previous: null, }); @@ -75,7 +95,7 @@ describe('Objects controller', () => { page: 2, pageSize: 10, count: 1, - total: 100, + total: 1, next: 'http://localhost:4001/api/v2/objects?page=2&pageSize=10', previous: null, }), @@ -91,8 +111,8 @@ describe('Objects controller', () => { ...objectsResponseData({}), page: 2, pageSize: 10, - count: 1, - total: 100, + count: 2, + total: 2, next: 'http://localhost:4001/api/v2/objects?page=2&pageSize=10', previous: null, }); @@ -101,17 +121,14 @@ describe('Objects controller', () => { }); it('should return kennisartikel objects when type is kennisartikel', async () => { fetchMock.mockResponseOnce(JSON.stringify(getStrapiKennisartikelData())); - fetchMock.mockResponseOnce(JSON.stringify(getStrapiVacData())); const response = await request(app) .get(`/api/v2/objects?type=${encodeURIComponent('http://localhost:4001/api/v2/objecttypes/kennisartikel')}`) .set('Authorization', 'Token YOUR_API_TOKEN'); - expect(response.status).toBe(200); expect(response.ok).toBe(true); expect(response.body).toStrictEqual(objectsResponseData({ type: 'kennisartikel' })); }); - it('should return kennisartikel objects when type is vac', async () => { - fetchMock.mockResponseOnce(JSON.stringify(getStrapiKennisartikelData())); + it('should return vac objects when type is vac', async () => { fetchMock.mockResponseOnce(JSON.stringify(getStrapiVacData())); const response = await request(app) .get(`/api/v2/objects?type=${encodeURIComponent('http://localhost:4001/api/v2/objecttypes/vac')}`) @@ -146,14 +163,65 @@ describe('Objects controller', () => { expect(response.ok).toBe(false); }); it('should return 200 and empty array when no data is returned', async () => { - const spy = jest.spyOn(require('../../utils/vacData.ts'), 'vacData').mockImplementation(() => []); - fetchMock.mockResponseOnce(JSON.stringify([{}])); - fetchMock.mockResponseOnce(JSON.stringify(getStrapiVacData())); + const spy = jest + .spyOn(require('../../utils/getPaginatedResponse'), 'getPaginatedResponse') + .mockImplementation(() => + Promise.resolve({ + page: 1, + pageSize: 10, + count: 0, + total: 0, + next: null, + previous: null, + }), + ); + fetchMock.mockResponseOnce( + JSON.stringify({ + data: { + vacs: { + meta: { + pagination: { + total: 0, + page: 1, + pageSize: 0, + pageCount: 0, + }, + }, + data: [], + }, + }, + }), + ); + fetchMock.mockResponseOnce( + JSON.stringify({ + data: { + kennisartikels: { + meta: { + pagination: { + total: 0, + page: 1, + pageSize: 0, + pageCount: 0, + }, + }, + data: [], + }, + }, + }), + ); const response = await request(app).get('/api/v2/objects').set('Authorization', 'Token YOUR_API_TOKEN'); expect(response.status).toBe(200); expect(response.ok).toBe(true); - expect(response.body).toStrictEqual({ results: [] }); + expect(response.body).toStrictEqual({ + total: 0, + count: 0, + pageSize: 10, + page: 1, + next: null, + previous: null, + results: [], + }); spy.mockRestore(); }); it('should return 500 when fetch fails', async () => { @@ -208,7 +276,23 @@ describe('Objects controller', () => { }; }); const vacResponse = { data: { vacs: { data: vacData } } }; - fetchMock.mockResponseOnce(JSON.stringify({ data: { products: { data: kennisartikelData } } })); + fetchMock.mockResponseOnce( + JSON.stringify({ + data: { + products: { + data: kennisartikelData, + meta: { + pagination: { + total: 1, + page: 1, + pageSize: 1, + pageCount: 1, + }, + }, + }, + }, + }), + ); fetchMock.mockResponseOnce(JSON.stringify(vacResponse)); await request(app).get('/api/v2/objects').set('Authorization', 'Token YOUR_API_TOKEN'); diff --git a/apps/overige-objecten-api/src/controllers/objects/index.ts b/apps/overige-objecten-api/src/controllers/objects/index.ts index 5aee36eb..237f9120 100644 --- a/apps/overige-objecten-api/src/controllers/objects/index.ts +++ b/apps/overige-objecten-api/src/controllers/objects/index.ts @@ -1,11 +1,24 @@ import type { RequestHandler } from 'express'; import { GET_ALL_PRODUCTS, GET_ALL_VAC_ITEMS, GET_PRODUCT_BY_UUID, GET_VAC_ITEM_BY_UUID } from '../../queries'; import type { StrapiProductType, VACSData } from '../../strapi-product-type'; +import type { components } from '../../types/openapi'; import { fetchData, generateKennisartikelObject, getPaginatedResponse, getTheServerURL } from '../../utils'; +import type { PaginationType } from '../../utils'; + +type GetKennisartikelReturnData = components['schemas']['ObjectData']; + +const sum = (a: number, b: number): number => a + b; +const isFiniteNumber = (arg: unknown): arg is number => typeof arg === 'number' && isFinite(arg); +let results: any = []; +let pagination: PaginationType = {}; export const getAllObjectsController: RequestHandler = async (req, res, next) => { try { const locale = req.query?.locale || 'nl'; const type = (req.query?.type as string) || ''; + const typeUrl = req.query?.type ? new URL(req.query?.type as string) : ''; + const isURL = typeof typeUrl === 'object'; + const isVac = isURL && typeUrl.pathname.split('/').includes('vac'); + const isKennisartikel = isURL && typeUrl.pathname.split('/').includes('kennisartikel'); const isAuthHasToken = req.headers?.authorization?.startsWith('Token'); const tokenAuth = isAuthHasToken ? req.headers?.authorization?.split(' ')[1] : req.headers?.authorization; const serverURL = getTheServerURL(req); @@ -25,70 +38,105 @@ export const getAllObjectsController: RequestHandler = async (req, res, next) => limit: !isValidPage && !isValidPageSize ? limit : undefined, start: !isValidPage && !isValidPageSize ? start : undefined, }; - // Fetch product data from GraphQL - const { data } = await fetchData({ - url: graphqlURL.href, - query: GET_ALL_PRODUCTS, - variables: { locale, ...paginationParams }, - headers: { - Authorization: `Bearer ${tokenAuth}`, - }, - }); - // Fetch VACs data from GraphQL - const { data: vacData } = await fetchData<{ data: VACSData }>({ - url: graphqlURL.href, - query: GET_ALL_VAC_ITEMS, - variables: { ...paginationParams }, - headers: { - Authorization: `Bearer ${tokenAuth}`, - }, - }); - - const vac = vacData?.vacs?.data?.map((item) => { - const vacUrl = new URL(`/api/v2/objects/${item.attributes.vac.uuid}`, serverURL).href; - return { - uuid: item.attributes.vac.uuid, - type: vacSchemaURL, - url: vacUrl, - record: { - index: parseInt(item.id, 10), - startAt: item.attributes.createdAt, - typeVersion: 1, - data: { - ...item.attributes.vac, - url: vacUrl, - }, - geometry: null, - endAt: null, - registrationAt: item.attributes.createdAt, + const fetchKennisartikelen = async () => { + // Fetch product data from GraphQL + const { data } = await fetchData({ + url: graphqlURL.href, + query: GET_ALL_PRODUCTS, + variables: { locale, ...paginationParams }, + headers: { + Authorization: `Bearer ${tokenAuth}`, }, - }; - }); - - const pagination = await getPaginatedResponse(req, data?.products); - - // Check if product data is available - const products = data?.products?.data || []; - - if (products.length === 0) return res.status(200).json({ results: [] }); - const kennisartikel = products.map(({ attributes, id }) => - generateKennisartikelObject({ attributes, url: serverURL, id }), - ); - + }); + return data; + }; + const getKennisartikelData = ({ data }: StrapiProductType): GetKennisartikelReturnData[] | [] => { + const products = data?.products?.data || []; + if (products.length === 0) return []; + const kennisartikel = products.map(({ attributes, id }) => + generateKennisartikelObject({ attributes, url: serverURL, id }), + ); + return kennisartikel; + }; + const fetchVac = async () => { + // Fetch VACs data from GraphQL + const { data } = await fetchData<{ data: VACSData }>({ + url: graphqlURL.href, + query: GET_ALL_VAC_ITEMS, + variables: { ...paginationParams }, + headers: { + Authorization: `Bearer ${tokenAuth}`, + }, + }); + return data; + }; + const getVacData = (data: VACSData) => { + // return empty array if no VACs + if (!data?.vacs?.data?.length) return []; + const vac = data?.vacs?.data?.map((item) => { + const vacUrl = new URL(`/api/v2/objects/${item.attributes.vac.uuid}`, serverURL).href; + return { + uuid: item.attributes.vac.uuid, + type: vacSchemaURL, + url: vacUrl, + record: { + index: parseInt(item.id, 10), + startAt: item.attributes.createdAt, + typeVersion: 1, + data: { + ...item.attributes.vac, + url: vacUrl, + }, + geometry: null, + endAt: null, + registrationAt: item.attributes.createdAt, + }, + }; + }); + return vac; + }; // Set response content type res.set('Content-Type', 'application/json'); - // Send results based on the requested type - if (type.endsWith('kennisartikel')) return res.status(200).json({ ...pagination, results: kennisartikel }); - - if (type.endsWith('vac')) return res.status(200).json({ results: vac }); + if (isKennisartikel) { + const data = await fetchKennisartikelen(); + pagination = await getPaginatedResponse(req, data?.products); + const kennisartikelData = getKennisartikelData({ data }); + results = kennisartikelData; + } else if (isVac) { + const data = await fetchVac(); + const vac = getVacData(data); + pagination = await getPaginatedResponse(req, data?.vacs as any); + results = vac; + } else if (!type && !isVac && !isKennisartikel) { + const productsData = await fetchKennisartikelen(); + const data = await fetchVac(); + const kennisartikelPagination = await getPaginatedResponse(req, productsData?.products); + const vacPagination = await getPaginatedResponse(req, data?.vacs as any); + const count = [kennisartikelPagination?.count, vacPagination?.count].filter(isFiniteNumber).reduce(sum, 0); + const total = [kennisartikelPagination?.total, vacPagination?.total].filter(isFiniteNumber).reduce(sum, 0); + pagination = { + ...kennisartikelPagination, + ...vacPagination, + count, + total, + }; + const kennisartikelData = getKennisartikelData({ data: productsData }); + const vac = getVacData(data); + results = [...kennisartikelData, ...vac]; + } else { + pagination = { + page: 0, + pageSize: 0, + total: 0, + count: 0, + next: null, + previous: null, + }; + results = []; + } - // If no specific type, return both kennisartikel and vac objects - if (vac.length > 0 && kennisartikel.length > 0) - return res.status(200).json({ ...pagination, results: [...kennisartikel, ...vac] }); - if (vac.length > 0) return res.status(200).json({ ...pagination, results: [...vac] }); - if (kennisartikel.length > 0) return res.status(200).json({ ...pagination, results: [...kennisartikel] }); - return res.status(200).json({ ...pagination, results: [] }); + return res.status(200).json({ ...pagination, results }); } catch (error) { // Forward any errors to the error handler middleware next(error); diff --git a/apps/overige-objecten-api/src/controllers/openapi/index.test.ts b/apps/overige-objecten-api/src/controllers/openapi/index.test.ts index 919a783d..bf0a3ca0 100644 --- a/apps/overige-objecten-api/src/controllers/openapi/index.test.ts +++ b/apps/overige-objecten-api/src/controllers/openapi/index.test.ts @@ -16,21 +16,4 @@ describe('openAPIController', () => { expect(response.text).toEqual(JSON.stringify({ message: 'An unexpected error occurred.' })); spy.mockRestore(); }); - it('GET api/v2/openapi.json return 200 and json with updated type parameter schema enum contains the correct URLs', async () => { - const spy = jest - .spyOn(require('../../utils/getTheServerURL'), 'getTheServerURL') - .mockImplementation(() => 'https://example.com/'); - const response = await request(app).get('/api/v2/openapi.json'); - const body = response.body as OpenAPI; - expect(response.status).toBe(200); - expect(body).toBeDefined(); - expect(body.paths['/objects'].get.parameters).toBeDefined(); - const typeParameter = body.paths['/objects'].get.parameters.find((parameter) => parameter.name === 'type'); - expect(typeParameter).toBeDefined(); - expect(typeParameter?.schema.enum).toEqual([ - 'https://example.com/api/v2/objecttypes/kennisartikel', - 'https://example.com/api/v2/objecttypes/vac', - ]); - spy.mockRestore(); - }); }); diff --git a/apps/overige-objecten-api/src/controllers/openapi/index.ts b/apps/overige-objecten-api/src/controllers/openapi/index.ts index b203dbd1..c08f005f 100644 --- a/apps/overige-objecten-api/src/controllers/openapi/index.ts +++ b/apps/overige-objecten-api/src/controllers/openapi/index.ts @@ -8,7 +8,6 @@ import { getTheServerURL, readFile } from '../../utils'; export const openAPIController: RequestHandler = async (req, res, next) => { try { const url = new URL('api/v2', getTheServerURL(req)).href; - const OBJECT_TYPE_ENUM = [`${url}/objecttypes/kennisartikel`, `${url}/objecttypes/vac`]; const OPEN_API_YAML = readFile(path.join(__dirname, '../../docs/openapi.yaml')); if (!OPEN_API_YAML) throw new Error('openapi.yaml file not found'); @@ -19,13 +18,6 @@ export const openAPIController: RequestHandler = async (req, res, next) => { url, description: server.description, })); - // Update enum for specific endpoint parameter - const typeParameter = openAPIDocument.paths['/objects']?.get?.parameters?.find( - (parameter) => parameter.name === 'type', - ); - if (typeParameter?.schema) { - typeParameter.schema.enum = OBJECT_TYPE_ENUM; - } res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); diff --git a/apps/overige-objecten-api/src/docs/openapi.yaml b/apps/overige-objecten-api/src/docs/openapi.yaml index 61027866..331a25c1 100644 --- a/apps/overige-objecten-api/src/docs/openapi.yaml +++ b/apps/overige-objecten-api/src/docs/openapi.yaml @@ -21,10 +21,8 @@ paths: schema: type: string format: uri - enum: - - "http://localhost:4001/api/v2/objecttypes/kennisartikel" - - "http://localhost:4001/api/v2/objecttypes/vac" description: Optional parameter to filter by object type. If not specified, both types will be returned. + example: "http://localhost:4001/api/v2/objecttypes/kennisartikel" - in: query name: page required: false diff --git a/apps/overige-objecten-api/src/queries/index.ts b/apps/overige-objecten-api/src/queries/index.ts index 6851e587..d475f3d7 100644 --- a/apps/overige-objecten-api/src/queries/index.ts +++ b/apps/overige-objecten-api/src/queries/index.ts @@ -137,6 +137,14 @@ query getAllVacItems($page: Int, $pageSize: Int, $start: Int, $limit: Int) { pageSize: $pageSize } ) { + meta { + pagination { + total + page + pageSize + pageCount + } + } data { id attributes { diff --git a/apps/overige-objecten-api/src/strapi-product-type.ts b/apps/overige-objecten-api/src/strapi-product-type.ts index 497f2761..e647cad9 100644 --- a/apps/overige-objecten-api/src/strapi-product-type.ts +++ b/apps/overige-objecten-api/src/strapi-product-type.ts @@ -26,6 +26,7 @@ export interface RootObjectVacItem { export interface VACSData { vacs: RootObjectVacItem; + meta: Meta; } export interface StrapiProductType { data: Data; diff --git a/apps/overige-objecten-api/src/utils/getPaginatedResponse.ts b/apps/overige-objecten-api/src/utils/getPaginatedResponse.ts index f610929b..8b324ab5 100644 --- a/apps/overige-objecten-api/src/utils/getPaginatedResponse.ts +++ b/apps/overige-objecten-api/src/utils/getPaginatedResponse.ts @@ -1,7 +1,19 @@ import type { Request } from 'express'; -import type { Products } from '../strapi-product-type'; +import type { Products, VACSData } from '../strapi-product-type'; -export const getPaginatedResponse = async (req: Request, strapiResponse: Products) => { +export type PaginationType = { + page?: number; + pageSize?: number; + count?: number; + total?: number; + next?: string | null; + previous?: string | null; +}; + +export const getPaginatedResponse = async ( + req: Request, + strapiResponse: Products | VACSData, +): Promise => { if (!strapiResponse?.meta?.pagination) return {}; // Get pagination info from Strapi response const { page, pageSize, pageCount, total } = strapiResponse.meta.pagination; diff --git a/apps/overige-objecten-api/src/utils/index.ts b/apps/overige-objecten-api/src/utils/index.ts index 852661f8..eaf6d993 100644 --- a/apps/overige-objecten-api/src/utils/index.ts +++ b/apps/overige-objecten-api/src/utils/index.ts @@ -3,6 +3,7 @@ export { envAvailability } from './envAvailability'; export { ErrorHandler } from './errorHandler'; export { fetchData } from './fetchData'; export { generateKennisartikelObject } from './generateKennisartikelObject'; +export type { PaginationType } from './getPaginatedResponse'; export { getPaginatedResponse } from './getPaginatedResponse'; export { getTheServerURL } from './getTheServerURL'; export { mapContentByCategory } from './mapContentByCategory';