Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store options metadata in datamodel (v3) #1611

Merged
merged 10 commits into from
Nov 14, 2023
12 changes: 12 additions & 0 deletions src/codegen/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ const common = {
'Describes the location in the data model where the component should store its value(s). A simple ' +
'binding is used for components that only store a single value, usually a string.',
),
IDataModelBindingsOptionsSimple: () =>
new CG.obj(
new CG.prop('simpleBinding', new CG.str()),
new CG.prop(
'metadata',
new CG.str()
.optional()
.setDescription(
'Describes the location where metadata for the option based component should be stored in the datamodel.',
),
),
),
IDataModelBindingsList: () =>
new CG.obj(new CG.prop('list', new CG.str()))
.setTitle('Data model binding')
Expand Down
4 changes: 3 additions & 1 deletion src/codegen/ComponentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ export class ComponentConfig extends GenerateComponentLike {
}

addDataModelBinding(
type: GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList'> | GenerateObject<any>,
type:
| GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList' | 'IDataModelBindingsOptionsSimple'>
| GenerateObject<any>,
): this {
this.ensureNotOverridden();
return super.addDataModelBinding(type);
Expand Down
4 changes: 3 additions & 1 deletion src/codegen/dataTypes/GenerateComponentLike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ export class GenerateComponentLike {
* Adding multiple data model bindings to the component makes it a union
*/
public addDataModelBinding(
type: GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList'> | GenerateObject<any>,
type:
| GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList' | 'IDataModelBindingsOptionsSimple'>
| GenerateObject<any>,
): this {
const name = 'dataModelBindings';
const existing = this.inner.getProperty(name)?.type;
Expand Down
31 changes: 20 additions & 11 deletions src/features/options/fetch/fetchOptionsSagas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
repeatingGroupsSelector,
} from 'src/features/options/fetch/fetchOptionsSagas';
import { staticUseLanguageForTests, staticUseLanguageFromState } from 'src/hooks/useLanguage';
import * as networking from 'src/utils/network/sharedNetworking';
import * as networking from 'src/utils/network/networking';
import { selectNotNull } from 'src/utils/sagas';
import type { ILayouts } from 'src/layout/layout';
import type { IOptions, IRuntimeState } from 'src/types';
Expand All @@ -35,7 +35,7 @@ describe('fetchOptionsSagas', () => {
};

it('should refetch a given option when an updated field is in a option mapping', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);
const optionsWithField: IOptions = {
someOption: {
id: 'someOption',
Expand Down Expand Up @@ -87,7 +87,7 @@ describe('fetchOptionsSagas', () => {

describe('fetchOptionsSaga', () => {
it('should spawn fetchSpecificOptionSaga for each unique optionsId', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);
const formLayoutWithTwoSharedOptionIds: ILayouts = {
formLayout: [
{
Expand Down Expand Up @@ -143,6 +143,7 @@ describe('fetchOptionsSagas', () => {
dataMapping: undefined,
fixedQueryParameters: undefined,
secure: undefined,
metadataBinding: undefined,
})
.fork(fetchSpecificOptionSaga, {
optionsId: 'kommune',
Expand All @@ -151,12 +152,13 @@ describe('fetchOptionsSagas', () => {
},
fixedQueryParameters: undefined,
secure: undefined,
metadataBinding: undefined,
})
.run();
});

it('should spawn multiple fetchSpecificOptionSaga if components have shared optionsId but different mapping', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);
const formLayoutWithSameOptionIdButDifferentMapping: ILayouts = {
formLayout: [
{
Expand Down Expand Up @@ -205,6 +207,7 @@ describe('fetchOptionsSagas', () => {
},
fixedQueryParameters: undefined,
secure: undefined,
metadataBinding: undefined,
})
.fork(fetchSpecificOptionSaga, {
optionsId: 'kommune',
Expand All @@ -213,14 +216,15 @@ describe('fetchOptionsSagas', () => {
},
fixedQueryParameters: undefined,
secure: undefined,
metadataBinding: undefined,
})
.run();
});
});

describe('Fixed query parameters', () => {
it('should include static query parameters and mapping', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);
const formLayout: ILayouts = {
formLayout: [
{
Expand Down Expand Up @@ -270,6 +274,7 @@ describe('fetchOptionsSagas', () => {
dataMapping: undefined,
fixedQueryParameters: { level: '1' },
secure: undefined,
metadataBinding: undefined,
})
.fork(fetchSpecificOptionSaga, {
optionsId: 'kommune',
Expand All @@ -278,12 +283,13 @@ describe('fetchOptionsSagas', () => {
},
fixedQueryParameters: { level: '2' },
secure: undefined,
metadataBinding: undefined,
})
.run();
});

it('should include static query parameters in url', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);

const formData = {
'FlytteTil.Fylke': 'Oslo',
Expand All @@ -294,18 +300,19 @@ describe('fetchOptionsSagas', () => {
dataMapping: undefined,
fixedQueryParameters: { level: '1' },
secure: undefined,
metadataBinding: undefined,
})
.provide([
[select(formDataSelector), formData],
[select(staticUseLanguageFromState), { selectedLanguage: 'nb' }],
[select(instanceIdSelector), 'someId'],
])
.call(networking.httpGet, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1')
.call(networking.httpGetRaw, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1')
.run();
});

it('should include mapping in url', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);

const formData = {
'FlytteTil.Fylke': 'Oslo',
Expand All @@ -318,18 +325,19 @@ describe('fetchOptionsSagas', () => {
},
fixedQueryParameters: undefined,
secure: undefined,
metadataBinding: undefined,
})
.provide([
[select(formDataSelector), formData],
[select(staticUseLanguageFromState), { selectedLanguage: 'nb' }],
[select(instanceIdSelector), 'someId'],
])
.call(networking.httpGet, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo')
.call(networking.httpGetRaw, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo')
.run();
});

it('should include static query parameters and mapping in request url', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]);

const formData = {
'FlytteTil.Fylke': 'Oslo',
Expand All @@ -342,14 +350,15 @@ describe('fetchOptionsSagas', () => {
},
fixedQueryParameters: { level: '1' },
secure: undefined,
metadataBinding: undefined,
})
.provide([
[select(formDataSelector), formData],
[select(staticUseLanguageFromState), { selectedLanguage: 'nb' }],
[select(instanceIdSelector), 'someId'],
])
.call(
networking.httpGet,
networking.httpGetRaw,
'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1&fylke=Oslo',
)
.run();
Expand Down
26 changes: 22 additions & 4 deletions src/features/options/fetch/fetchOptionsSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { call, fork, put, race, select, take } from 'redux-saga/effects';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { SagaIterator } from 'redux-saga';

import { FormDataActions } from 'src/features/formData/formDataSlice';
import { OptionsActions } from 'src/features/options/optionsSlice';
import { staticUseLanguageFromState } from 'src/hooks/useLanguage';
import {
Expand All @@ -10,14 +11,14 @@ import {
getKeyWithoutIndexIndicators,
replaceIndexIndicatorsWithIndexes,
} from 'src/utils/databindings';
import { httpGet } from 'src/utils/network/sharedNetworking';
import { httpGetRaw } from 'src/utils/network/networking';
import { getOptionLookupKey, getOptionLookupKeys } from 'src/utils/options';
import { selectNotNull } from 'src/utils/sagas';
import { getOptionsUrl } from 'src/utils/urls/appUrlHelper';
import type { IFormData } from 'src/features/formData';
import type { IUpdateFormData } from 'src/features/formData/formDataTypes';
import type { IUseLanguage } from 'src/hooks/useLanguage';
import type { IOption, ISelectionComponent } from 'src/layout/common.generated';
import type { IDataModelBindingsOptionsSimple, IOption, ISelectionComponent } from 'src/layout/common.generated';
import type { ILayouts } from 'src/layout/layout';
import type { IFetchSpecificOptionSaga, IOptions, IOptionsMetaData, IRepeatingGroups, IRuntimeState } from 'src/types';

Expand Down Expand Up @@ -53,7 +54,11 @@ export function* fetchOptionsSaga(): SagaIterator {
const optionsWithIndexIndicators: IOptionsMetaData[] = [];
for (const layoutId of Object.keys(layouts)) {
for (const element of layouts[layoutId] || []) {
const { optionsId, mapping, queryParameters, secure } = element as ISelectionComponent;
const { optionsId, mapping, queryParameters, secure, dataModelBindings } = element as ISelectionComponent & {
dataModelBindings?: IDataModelBindingsOptionsSimple;
};

const metadataBinding = dataModelBindings?.metadata;

// if we have index indicators we get up the lookup keys for existing indexes
const { keys, keyWithIndexIndicator } =
Expand Down Expand Up @@ -84,6 +89,7 @@ export function* fetchOptionsSaga(): SagaIterator {
dataMapping: mapping,
fixedQueryParameters: queryParameters,
secure,
metadataBinding,
});
fetchedOptions.push(lookupKey);
}
Expand All @@ -105,6 +111,7 @@ export function* fetchSpecificOptionSaga({
dataMapping,
fixedQueryParameters,
secure,
metadataBinding,
}: IFetchSpecificOptionSaga): SagaIterator {
const key = getOptionLookupKey({ id: optionsId, mapping: dataMapping, fixedQueryParameters });
const instanceId = yield select(instanceIdSelector);
Expand All @@ -129,7 +136,18 @@ export function* fetchSpecificOptionSaga({
instanceId,
});

const options: IOption[] = yield call(httpGet, url);
const optionsResponse = yield call(httpGetRaw, url);
const downstreamParameters: string = optionsResponse.headers['altinn-downstreamparameters'];
if (downstreamParameters && metadataBinding) {
yield put(
FormDataActions.update({
field: metadataBinding,
data: downstreamParameters,
skipValidation: true,
}),
);
}
const options = optionsResponse.data as IOption[];
yield put(OptionsActions.fetchFulfilled({ key, options }));
} catch (error) {
yield put(OptionsActions.fetchRejected({ key, error }));
Expand Down
2 changes: 1 addition & 1 deletion src/layout/Checkboxes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Config = new CG.component({
},
})
.makeSelectionComponent()
.addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal }))
.addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal }))
.addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional()));

// We don't render the label in GenericComponent, but we still need the
Expand Down
2 changes: 1 addition & 1 deletion src/layout/Dropdown/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export const Config = new CG.component({
},
})
.makeSelectionComponent()
.addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal }));
.addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal }));
2 changes: 1 addition & 1 deletion src/layout/Likert/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Config = new CG.component({
renderInAccordionGroup: false,
},
})
.addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal }))
.addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal }))
.addTextResource(
new CG.trb({
name: 'title',
Expand Down
2 changes: 1 addition & 1 deletion src/layout/MultipleSelect/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export const Config = new CG.component({
renderInAccordionGroup: false,
},
})
.addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal }))
.addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal }))
.makeSelectionComponent();
2 changes: 1 addition & 1 deletion src/layout/RadioButtons/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Config = new CG.component({
renderInAccordionGroup: false,
},
})
.addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal }))
.addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal }))
.makeSelectionComponent()
.addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional()))
.addProperty(
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export interface IFetchSpecificOptionSaga {
fixedQueryParameters?: Record<string, string>;
secure?: boolean;
instanceId?: string;
metadataBinding?: string;
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/utils/network/networking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ export async function httpGet(url: string, options?: AxiosRequestConfig): Promis
return response.data ? response.data : null;
}

export async function httpGetRaw(url: string, options?: AxiosRequestConfig): Promise<any> {
const headers = options?.headers as RawAxiosRequestHeaders | undefined;
const response: AxiosResponse = await axios.get(url, {
...options,
headers: { ...headers, Pragma: 'no-cache' },
});
return response.data ? response : null;
}

export async function httpPost(url: string, options?: AxiosRequestConfig, data?: any): Promise<AxiosResponse> {
return await axios.post(url, data, options);
}
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/integration/frontend-test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,22 @@ describe('Options', () => {
cy.findByRole('option', { name: 'Endre fra: 1, Endre til: 2' }).click();
cy.get(appFrontend.group.options).should('have.value', 'Endre fra: 1, Endre til: 2');
});
it('retrieves metadata from header when metadata is set in datamodelBindings', () => {
cy.intercept({ method: 'GET', url: '**/options/test-kommuner**' }, (req) => {
req.reply((res) => {
const headers = res.headers;
headers['altinn-downstreamparameters'] = 'language=nb,id=131,variant=,date=01/01/2021,level=,parentCode=';
res.send(res.body, headers);
});
}).as('optionsMunicipality');

cy.goto('changename');
cy.wait('@optionsMunicipality');

cy.get(appFrontend.changeOfName.municipality).dsSelect('Oslo');

cy.get(appFrontend.changeOfName.municipalityMetadata)
.should('have.prop', 'value')
.should('match', /language=nb,id=131,variant=,date=\d{1,2}\/\d{1,2}\/\d{4},level=,parentCode=/);
});
});
2 changes: 2 additions & 0 deletions test/e2e/pageobjects/app-frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export class AppFrontend {
reference: '#reference',
reference2: '#reference2',
dateOfEffect: '#dateOfEffect',
municipalityMetadata: '#kommuner-metadata',
municipality: '#kommune',
upload: '#fileUpload-changename',
uploadWithTag: {
uploadZone: '#fileUploadWithTags-changename',
Expand Down