Skip to content

Commit

Permalink
[DEV-1362] Add BuildEnv and Strapi utilities (#614)
Browse files Browse the repository at this point in the history
  • Loading branch information
kin0992 authored Feb 8, 2024
1 parent 44ab2b6 commit 910f54a
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-spoons-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nextjs-website": minor
---

[DEV-1362] Create BuildEnv and create function to use to make API calls to Strapi
23 changes: 23 additions & 0 deletions apps/nextjs-website/src/BuildConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { pipe } from 'fp-ts/lib/function';
import * as E from 'fp-ts/lib/Either';
import * as PR from 'io-ts/lib/PathReporter';
import * as t from 'io-ts';
import * as tt from 'io-ts-types';
import { StrapiConfig } from '@/lib/strapi/StrapiConfig';

const BuildConfigCodec = t.intersection([
t.type({
FETCH_FROM_STRAPI: t.string.pipe(tt.BooleanFromString),
}),
StrapiConfig,
]);

export type BuildConfig = t.TypeOf<typeof BuildConfigCodec>;

export const makeBuildConfig = (
env: Record<string, undefined | string>
): E.Either<string, BuildConfig> =>
pipe(
BuildConfigCodec.decode(env),
E.mapLeft((errors) => PR.failure(errors).join('\n'))
);
13 changes: 13 additions & 0 deletions apps/nextjs-website/src/BuildEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BuildConfig } from '@/BuildConfig';
import { StrapiEnv } from '@/lib/strapi/StapiEnv';

// BuildEnv
export type BuildEnv = {
readonly config: BuildConfig;
} & StrapiEnv;

// given environment variables produce an BuildEnv
export const makeBuildEnv = (config: BuildConfig): BuildEnv => ({
config,
fetchFun: fetch,
});
7 changes: 7 additions & 0 deletions apps/nextjs-website/src/lib/strapi/StapiEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { StrapiConfig } from '@/lib/strapi/StrapiConfig';

// This type represents the environment of Strapi.
export type StrapiEnv = {
readonly config: StrapiConfig;
readonly fetchFun: typeof fetch;
};
8 changes: 8 additions & 0 deletions apps/nextjs-website/src/lib/strapi/StrapiConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as t from 'io-ts';

export const StrapiConfig = t.type({
STRAPI_ENDPOINT: t.string,
STRAPI_API_TOKEN: t.string,
});

export type StrapiConfig = t.TypeOf<typeof StrapiConfig>;
116 changes: 116 additions & 0 deletions apps/nextjs-website/src/lib/strapi/__tests__/fetchFromStrapi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as t from 'io-ts';
import { fetchFromStrapi } from '@/lib/strapi/fetchFromStrapi';

const makeTestEnv = () => {
const fetchMock = jest.fn();
return {
fetchMock,
env: {
config: {
STRAPI_ENDPOINT: 'aStrapiEndpoint',
STRAPI_API_TOKEN: 'aStrapiApiToken',
},
fetchFun: fetchMock,
},
};
};

const strapiResponses = {
200: {
data: {
id: 1,
attributes: {
createdAt: '2024-02-08T11:12:02.142Z',
updatedAt: '2024-02-08T11:12:21.438Z',
publishedAt: '2024-02-08T11:12:21.436Z',
},
},
meta: {},
},
404: {
data: null,
error: {
status: 404,
name: 'NotFoundError',
message: 'Not Found',
details: {},
},
},
401: {
data: null,
error: {
status: 401,
name: 'UnauthorizedError',
message: 'Missing or invalid credentials',
details: {},
},
},
};

const codec = t.strict({
data: t.strict({
id: t.number,
}),
});
// This codec is used to test cases when the decode function fails
const badCodec = t.strict({
data: t.strict({
id: t.string,
}),
});

describe('fetchFromStrapi', () => {
it('should return strapi response given a 200 response', async () => {
const { env, fetchMock } = makeTestEnv();
fetchMock.mockResolvedValueOnce({
status: 200,
statusText: 'OK',
json: () => Promise.resolve(strapiResponses[200]),
});
const actual = fetchFromStrapi('aPath', 'aPopulate', codec)(env);
const expected = { data: { id: 1 } };
expect(await actual).toStrictEqual(expected);
});
it('should return error given a 401 response', async () => {
const { env, fetchMock } = makeTestEnv();
fetchMock.mockResolvedValueOnce({
status: 401,
statusText: 'Unauthorized',
json: () => Promise.resolve(strapiResponses[401]),
});
const actual = fetchFromStrapi('aPath', 'aPopulate', codec)(env);
const expected = new Error('401 - Unauthorized');
await expect(actual).rejects.toStrictEqual(expected);
});
it('should return error given a 404 response', async () => {
const { env, fetchMock } = makeTestEnv();
fetchMock.mockResolvedValueOnce({
status: 404,
statusText: 'Not Found',
json: () => Promise.resolve(strapiResponses[404]),
});
const actual = fetchFromStrapi('aPath', 'aPopulate', codec)(env);
const expected = new Error('404 - Not Found');
await expect(actual).rejects.toStrictEqual(expected);
});
it('should return error given a reject', async () => {
const { env, fetchMock } = makeTestEnv();
fetchMock.mockRejectedValueOnce({});
const actual = fetchFromStrapi('aPath', 'aPopulate', codec)(env);
const expected = new Error('[object Object]');
await expect(actual).rejects.toStrictEqual(expected);
});
it('should return error given a decode error', async () => {
const { env, fetchMock } = makeTestEnv();
fetchMock.mockResolvedValueOnce({
status: 200,
statusText: 'OK',
json: () => Promise.resolve(strapiResponses[200]),
});
const actual = fetchFromStrapi('aPath', 'aPopulate', badCodec)(env);
const expected = new Error(
'Invalid value 1 supplied to : {| data: {| id: string |} |}/data: {| id: string |}/id: string'
);
await expect(actual).rejects.toStrictEqual(expected);
});
});
62 changes: 62 additions & 0 deletions apps/nextjs-website/src/lib/strapi/fetchFromStrapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as t from 'io-ts';
import { pipe } from 'fp-ts/lib/function';
import * as R from 'fp-ts/lib/Reader';
import * as E from 'fp-ts/lib/Either';
import * as TE from 'fp-ts/lib/TaskEither';
import * as PR from 'io-ts/lib/PathReporter';
import { StrapiEnv } from '@/lib/strapi/StapiEnv';

// Function to invoke in order to retrieve data from Strapi.
export const fetchFromStrapi = <A, O, I>(
path: string,
populate: '*' | string,
codec: t.Type<A, O, I>
) =>
pipe(
R.ask<StrapiEnv>(),
R.map(
({
config: {
STRAPI_ENDPOINT: strapiEndpoint,
STRAPI_API_TOKEN: strapiApiToken,
},
fetchFun,
}) =>
pipe(
// handle any promise result
TE.tryCatch(
() =>
fetchFun(`${strapiEndpoint}/api/${path}/?populate=${populate}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${strapiApiToken}`,
},
}),
E.toError
),
TE.chain((response) => {
if (response.status === 200) {
return TE.tryCatch(() => response.json(), E.toError);
} else {
return TE.left(makeError(response));
}
}),
TE.chainEitherK((json) =>
// decode the response with the given codec
pipe(
codec.decode(json),
E.mapLeft((errors) => new Error(PR.failure(errors).join('\n')))
)
),
TE.fold(
// eslint-disable-next-line functional/no-promise-reject
(errors) => () => Promise.reject(errors),
(result) => () => Promise.resolve(result)
)
)()
)
);

const makeError = ({ status, statusText }: Response) => {
return new Error(`${status} - ${statusText}`);
};

0 comments on commit 910f54a

Please sign in to comment.