Skip to content

Commit

Permalink
🏷️ [#445] Convert API utils and AppDebug component to TS
Browse files Browse the repository at this point in the history
Type checker reported bugs:

* subtracting dates from each other (sort-of false positive...)
* the way we handle params/headers in API calls didn't account for
  null
* some parseInt call had the radix _outside_ of the function call
  • Loading branch information
sergei-maertens committed Mar 6, 2025
1 parent 69013f4 commit c9c2bf0
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 23 deletions.
46 changes: 29 additions & 17 deletions src/api.js → src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,29 @@ import {
import {CSPNonce, CSRFToken, ContentLanguage, IsFormDesigner} from './headers';
import {setLanguage} from './i18n';

const fetchDefaults = {
credentials: 'include', // required for Firefox 60, which is used in werkplekken
interface ApiCallOptions extends Omit<RequestInit, 'headers'> {
headers?: Record<string, string>;
}

const fetchDefaults: ApiCallOptions = {
credentials: 'include',
};

const SessionExpiresInHeader = 'X-Session-Expires-In';

let sessionExpiresAt = createState({expiry: null});
interface SessionExpiryState {
expiry: Date | null;
}

const sessionExpiresAt = createState<SessionExpiryState>({expiry: null});

export const updateSessionExpiry = seconds => {
export const updateSessionExpiry = (seconds: number) => {
const newExpiry = new Date();
newExpiry.setSeconds(newExpiry.getSeconds() + seconds);
sessionExpiresAt.setValue({expiry: newExpiry});
};

const throwForStatus = async response => {
const throwForStatus = async (response: Response) => {
if (response.ok) return;

let responseData = null;
Expand Down Expand Up @@ -75,7 +83,7 @@ const throwForStatus = async response => {
throw new ErrorClass(errorMessage, response.status, responseData.detail, responseData.code);
};

const addHeaders = (headers, method) => {
const addHeaders = (headers: Record<string, string> | undefined, method: string) => {
if (!headers) headers = {};

// add the CSP nonce request header in case the backend needs to do any post-processing
Expand All @@ -94,10 +102,10 @@ const addHeaders = (headers, method) => {
return headers;
};

const updateStoredHeadersValues = headers => {
const updateStoredHeadersValues = (headers: Headers) => {
const sessionExpiry = headers.get(SessionExpiresInHeader);
if (sessionExpiry) {
updateSessionExpiry(parseInt(sessionExpiry), 10);
updateSessionExpiry(parseInt(sessionExpiry, 10));
}

const CSRFTokenValue = headers.get(CSRFToken.headerName);
Expand All @@ -117,7 +125,7 @@ const updateStoredHeadersValues = headers => {
}
};

const apiCall = async (url, opts = {}) => {
const apiCall = async (url: string, opts: ApiCallOptions = {}) => {
const method = opts.method || 'GET';
const options = {...fetchDefaults, ...opts};
options.headers = addHeaders(options.headers, method);
Expand All @@ -129,7 +137,11 @@ const apiCall = async (url, opts = {}) => {
return response;
};

const get = async (url, params = {}, multiParams = []) => {
const get = async (
url: string,
params: Record<string, string> = {},
multiParams: Record<string, string>[] = []
) => {
let searchParams = new URLSearchParams();
if (Object.keys(params).length) {
searchParams = new URLSearchParams(params);
Expand All @@ -146,12 +158,12 @@ const get = async (url, params = {}, multiParams = []) => {
return data;
};

const _unsafe = async (method = 'POST', url, data, signal) => {
const opts = {
const _unsafe = async (method = 'POST', url: string, data: any, signal?: AbortSignal) => {

Check failure on line 161 in src/api.ts

View workflow job for this annotation

GitHub Actions / Lint code (ESLint)

Unexpected any. Specify a different type

Check failure on line 161 in src/api.ts

View workflow job for this annotation

GitHub Actions / Create 'production' build

Unexpected any. Specify a different type
const opts: ApiCallOptions = {
method,
headers: {
'Content-Type': 'application/json',
[CSRFToken.headerName]: CSRFToken.getValue(),
[CSRFToken.headerName]: CSRFToken.getValue() ?? '',
},
};
if (data) {
Expand All @@ -169,22 +181,22 @@ const _unsafe = async (method = 'POST', url, data, signal) => {
};
};

const post = async (url, data, signal) => {
const post = async (url: string, data: any, signal?: AbortSignal) => {

Check failure on line 184 in src/api.ts

View workflow job for this annotation

GitHub Actions / Lint code (ESLint)

Unexpected any. Specify a different type

Check failure on line 184 in src/api.ts

View workflow job for this annotation

GitHub Actions / Create 'production' build

Unexpected any. Specify a different type
const resp = await _unsafe('POST', url, data, signal);
return resp;
};

const patch = async (url, data = {}) => {
const patch = async (url: string, data: any = {}) => {

Check failure on line 189 in src/api.ts

View workflow job for this annotation

GitHub Actions / Lint code (ESLint)

Unexpected any. Specify a different type

Check failure on line 189 in src/api.ts

View workflow job for this annotation

GitHub Actions / Create 'production' build

Unexpected any. Specify a different type
const resp = await _unsafe('PATCH', url, data);
return resp;
};

const put = async (url, data = {}) => {
const put = async (url: string, data: any = {}) => {

Check failure on line 194 in src/api.ts

View workflow job for this annotation

GitHub Actions / Lint code (ESLint)

Unexpected any. Specify a different type

Check failure on line 194 in src/api.ts

View workflow job for this annotation

GitHub Actions / Create 'production' build

Unexpected any. Specify a different type
const resp = await _unsafe('PUT', url, data);
return resp;
};

const destroy = async url => {
const destroy = async (url: string) => {
const opts = {
method: 'DELETE',
};
Expand Down
21 changes: 15 additions & 6 deletions src/components/AppDebug.jsx → src/components/AppDebug.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import {FormattedDate, FormattedRelativeTime, useIntl} from 'react-intl';
import {useState as useGlobalState} from 'state-pool';

import {sessionExpiresAt} from 'api';
import {getVersion} from 'utils';
import {sessionExpiresAt} from '@/api';
import {getVersion} from '@/utils';

const DebugInfo = ({label, value, children}) => (
export interface DebugInfoProps {
label: string;
value?: string;
children?: React.ReactNode;
}

const DebugInfo: React.FC<DebugInfoProps> = ({label, value, children}) => (
<div className="debug-info">
<div className="debug-info__label">{label}</div>
<div className="debug-info__value">{value ?? children}</div>
</div>
);

const AppDebug = () => {
const AppDebug: React.FC = () => {
const {locale} = useIntl();
const [{expiry}] = useGlobalState(sessionExpiresAt);
const expiryDelta = (expiry - new Date()) / 1000;
return (
<div className="debug-info-container" title="Debug information (only available in dev)">
<DebugInfo label="Current locale" value={locale} />
Expand All @@ -23,7 +28,11 @@ const AppDebug = () => {
<>
<FormattedDate value={expiry} hour="numeric" minute="numeric" second="numeric" />
&nbsp;(
<FormattedRelativeTime value={expiryDelta} numeric="auto" updateIntervalInSeconds={1} />
<FormattedRelativeTime
value={(expiry.getTime() - new Date().getTime()) / 1000}
numeric="auto"
updateIntervalInSeconds={1}
/>
)
</>
) : (
Expand Down

0 comments on commit c9c2bf0

Please sign in to comment.