Skip to content

Commit

Permalink
#167 šŸ˜ call server response interceptors for database requests, add dā€¦
Browse files Browse the repository at this point in the history
ā€¦etabase api interceptors, add baseUrl field for database
  • Loading branch information
RiceWithMeat committed May 20, 2024
1 parent e033e56 commit 36a030b
Show file tree
Hide file tree
Showing 12 changed files with 513 additions and 281 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { z } from 'zod';

import { baseUrlSchema } from '../baseUrlSchema/baseUrlSchema';
import { interceptorsSchema } from '../interceptorsSchema/interceptorsSchema';
import { plainObjectSchema, stringForwardSlashSchema, stringJsonFilenameSchema } from '../utils';

export const databaseConfigSchema = z.strictObject({
baseUrl: baseUrlSchema.optional(),
data: z.union([plainObjectSchema(z.record(z.unknown())), stringJsonFilenameSchema]),
routes: z
.union([
plainObjectSchema(z.record(stringForwardSlashSchema, stringForwardSlashSchema)),
stringJsonFilenameSchema
])
.optional()
.optional(),
interceptors: plainObjectSchema(interceptorsSchema).optional()
});
178 changes: 106 additions & 72 deletions src/core/database/createDatabaseRoutes/createDatabaseRoutes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,108 +5,142 @@ import path from 'path';
import request from 'supertest';

import { createDatabaseRoutes } from '@/core/database';
import { urlJoin } from '@/utils/helpers';
import { createTmpDir } from '@/utils/helpers/tests';
import type { DatabaseConfig, MockServerConfig } from '@/utils/types';

import { findIndexById } from './helpers';

describe('createDatabaseRoutes', () => {
const createServer = (
mockServerConfig: Pick<MockServerConfig, 'baseUrl'> & { database: DatabaseConfig }
) => {
const server = express();
const routerBase = express.Router();
const routesWithDatabaseRoutes = createDatabaseRoutes(routerBase, mockServerConfig.database);
const createServer = (
mockServerConfig: Pick<MockServerConfig, 'interceptors' | 'baseUrl'> & {
database: DatabaseConfig;
}
) => {
const { baseUrl, database, interceptors } = mockServerConfig;
const server = express();
const routerBase = express.Router();
const routesWithDatabaseRoutes = createDatabaseRoutes({
router: routerBase,
databaseConfig: database,
serverResponseInterceptor: interceptors?.response
});

server.use(mockServerConfig.baseUrl ?? '/', routesWithDatabaseRoutes);
return server;
};
const databaseBaseUrl = urlJoin(baseUrl ?? '/', database?.baseUrl ?? '/');

describe('createDatabaseRoutes: routes and data successfully works when passing them by object', () => {
const data = { profile: { name: 'John' }, users: [{ id: 1 }, { id: 2 }] };
const routes = { '/api/profile': '/profile' } as const;
const server = createServer({ database: { data, routes } });
server.use(databaseBaseUrl, routesWithDatabaseRoutes);
return server;
};

test('Should overwrite routes according to routes object (but default url should work too)', async () => {
const overwrittenUrlResponse = await request(server).get('/api/profile');
expect(overwrittenUrlResponse.body).toStrictEqual(data.profile);
describe('createDatabaseRoutes: routes and data successfully works when passing them by object', () => {
const data = { profile: { name: 'John' }, users: [{ id: 1 }, { id: 2 }] };
const routes = { '/api/profile': '/profile' } as const;
const server = createServer({ database: { data, routes } });

const defaultUrlResponse = await request(server).get('/profile');
expect(defaultUrlResponse.body).toStrictEqual(data.profile);
});
test('Should overwrite routes according to routes object (but default url should work too)', async () => {
const overwrittenUrlResponse = await request(server).get('/api/profile');
expect(overwrittenUrlResponse.body).toStrictEqual(data.profile);

test('Should successfully handle requests to shallow and nested database parts', async () => {
const shallowDatabaseResponse = await request(server).get('/profile');
expect(shallowDatabaseResponse.body).toStrictEqual(data.profile);
const defaultUrlResponse = await request(server).get('/profile');
expect(defaultUrlResponse.body).toStrictEqual(data.profile);
});

const nestedDatabaseCollectionResponse = await request(server).get('/users');
expect(nestedDatabaseCollectionResponse.body).toStrictEqual(data.users);
test('Should successfully handle requests to shallow and nested database parts', async () => {
const shallowDatabaseResponse = await request(server).get('/profile');
expect(shallowDatabaseResponse.body).toStrictEqual(data.profile);

const nestedDatabaseItemResponse = await request(server).get('/users/1');
expect(nestedDatabaseItemResponse.body).toStrictEqual(
data.users[findIndexById(data.users, 1)]
);
});
const nestedDatabaseCollectionResponse = await request(server).get('/users');
expect(nestedDatabaseCollectionResponse.body).toStrictEqual(data.users);

const nestedDatabaseItemResponse = await request(server).get('/users/1');
expect(nestedDatabaseItemResponse.body).toStrictEqual(data.users[findIndexById(data.users, 1)]);
});
});

describe('createDatabaseRoutes: routes and data successfully works when passing them by file', () => {
const data = { profile: { name: 'John' }, users: [{ id: 1 }, { id: 2 }] };
const routes = { '/api/profile': '/profile' } as const;
describe('createDatabaseRoutes: routes and data successfully works when passing them by file', () => {
const data = { profile: { name: 'John' }, users: [{ id: 1 }, { id: 2 }] };
const routes = { '/api/profile': '/profile' } as const;

let tmpDirPath: string;
let server: Express;
let tmpDirPath: string;
let server: Express;

beforeAll(() => {
tmpDirPath = createTmpDir();
beforeAll(() => {
tmpDirPath = createTmpDir();

const pathToData = path.join(tmpDirPath, './data.json') as `${string}.json`;
fs.writeFileSync(pathToData, JSON.stringify(data));
const pathToData = path.join(tmpDirPath, './data.json') as `${string}.json`;
fs.writeFileSync(pathToData, JSON.stringify(data));

const pathToRoutes = path.join(tmpDirPath, './routes.json') as `${string}.json`;
fs.writeFileSync(pathToRoutes, JSON.stringify(routes));
const pathToRoutes = path.join(tmpDirPath, './routes.json') as `${string}.json`;
fs.writeFileSync(pathToRoutes, JSON.stringify(routes));

server = createServer({ database: { data: pathToData, routes: pathToRoutes } });
});
server = createServer({ database: { data: pathToData, routes: pathToRoutes } });
});

afterAll(() => {
fs.rmSync(tmpDirPath, { recursive: true, force: true });
});
afterAll(() => {
fs.rmSync(tmpDirPath, { recursive: true, force: true });
});

test('Should overwrite routes according to routes object (but default url should work too)', async () => {
const overwrittenUrlResponse = await request(server).get('/api/profile');
expect(overwrittenUrlResponse.body).toStrictEqual(data.profile);
test('Should overwrite routes according to routes object (but default url should work too)', async () => {
const overwrittenUrlResponse = await request(server).get('/api/profile');
expect(overwrittenUrlResponse.body).toStrictEqual(data.profile);

const defaultUrlResponse = await request(server).get('/profile');
expect(defaultUrlResponse.body).toStrictEqual(data.profile);
});
const defaultUrlResponse = await request(server).get('/profile');
expect(defaultUrlResponse.body).toStrictEqual(data.profile);
});

test('Should successfully handle requests to shallow and nested database parts', async () => {
const shallowDatabaseResponse = await request(server).get('/profile');
expect(shallowDatabaseResponse.body).toStrictEqual(data.profile);
test('Should successfully handle requests to shallow and nested database parts', async () => {
const shallowDatabaseResponse = await request(server).get('/profile');
expect(shallowDatabaseResponse.body).toStrictEqual(data.profile);

const nestedDatabaseCollectionResponse = await request(server).get('/users');
expect(nestedDatabaseCollectionResponse.body).toStrictEqual(data.users);
const nestedDatabaseCollectionResponse = await request(server).get('/users');
expect(nestedDatabaseCollectionResponse.body).toStrictEqual(data.users);

const nestedDatabaseItemResponse = await request(server).get('/users/1');
expect(nestedDatabaseItemResponse.body).toStrictEqual(
data.users[findIndexById(data.users, 1)]
);
});
const nestedDatabaseItemResponse = await request(server).get('/users/1');
expect(nestedDatabaseItemResponse.body).toStrictEqual(data.users[findIndexById(data.users, 1)]);
});
});

describe('createDatabaseRoutes: routes /__routes and /__db', () => {
const data = { profile: { name: 'John' }, users: [{ id: 1 }, { id: 2 }] };
const routes = { '/api/profile': '/profile' } as const;
const server = createServer({ database: { data, routes } });

test('Should create /__db route that return data from databaseConfig', async () => {
const response = await request(server).get('/__db');
expect(response.body).toStrictEqual(data);
});

describe('createDatabaseRoutes: routes /__routes and /__db', () => {
test('Should create /__routes route that return routes from databaseConfig', async () => {
const response = await request(server).get('/__routes');
expect(response.body).toStrictEqual(routes);
});
});

describe('createDatabaseRoutes: interceptors', () => {
test('Should call response interceptors in order: api -> server', async () => {
const apiInterceptor = jest.fn();
const serverInterceptor = jest.fn();

const data = { profile: { name: 'John' }, users: [{ id: 1 }, { id: 2 }] };
const routes = { '/api/profile': '/profile' } as const;
const server = createServer({ database: { data, routes } });

test('Should create /__db route that return data from databaseConfig', async () => {
const response = await request(server).get('/__db');
expect(response.body).toStrictEqual(data);
const server = createServer({
database: {
data,
routes,
interceptors: {
response: apiInterceptor
}
},
interceptors: {
response: serverInterceptor
}
});

test('Should create /__routes route that return routes from databaseConfig', async () => {
const response = await request(server).get('/__routes');
expect(response.body).toStrictEqual(routes);
});
await request(server).get('/profile');

expect(apiInterceptor.mock.calls.length).toBe(1);
expect(serverInterceptor.mock.calls.length).toBe(1);
expect(apiInterceptor.mock.invocationCallOrder[0]).toBeLessThan(
serverInterceptor.mock.invocationCallOrder[0]
);
});
});
71 changes: 61 additions & 10 deletions src/core/database/createDatabaseRoutes/createDatabaseRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IRouter } from 'express';

import type { DatabaseConfig, NestedDatabase, ShallowDatabase } from '@/utils/types';
import { asyncHandler, callResponseInterceptors } from '@/utils/helpers';
import type { DatabaseConfig, Interceptors, NestedDatabase, ShallowDatabase } from '@/utils/types';

import {
createNestedDatabaseRoutes,
Expand All @@ -13,26 +14,76 @@ import { FileStorage, MemoryStorage } from './storages';
const isVariableJsonFile = (variable: unknown): variable is `${string}.json` =>
typeof variable === 'string' && variable.endsWith('.json');

export const createDatabaseRoutes = (router: IRouter, { data, routes }: DatabaseConfig) => {
interface CreateDatabaseRoutesParams {
router: IRouter;
databaseConfig: DatabaseConfig;
serverResponseInterceptor?: Interceptors['response'];
}

export const createDatabaseRoutes = ({
router,
databaseConfig,
serverResponseInterceptor
}: CreateDatabaseRoutesParams) => {
const { data, routes } = databaseConfig;

if (routes) {
const storage = isVariableJsonFile(routes)
? new FileStorage(routes)
: new MemoryStorage(routes);
createRewrittenDatabaseRoutes(router, storage.read());

router.route('/__routes').get((_request, response) => {
response.json(storage.read());
});
router.route('/__routes').get(
asyncHandler(async (request, response) => {
const data = await callResponseInterceptors({
data: storage.read(),
request,
response,
interceptors: {
apiInterceptor: databaseConfig.interceptors?.response,
serverInterceptor: serverResponseInterceptor
}
});
response.json(data);
})
);
}

const storage = isVariableJsonFile(data) ? new FileStorage(data) : new MemoryStorage(data);
const { shallowDatabase, nestedDatabase } = splitDatabaseByNesting(storage.read());
createShallowDatabaseRoutes(router, shallowDatabase, storage as MemoryStorage<ShallowDatabase>);
createNestedDatabaseRoutes(router, nestedDatabase, storage as MemoryStorage<NestedDatabase>);

router.route('/__db').get((_request, response) => {
response.json(storage.read());
createShallowDatabaseRoutes({
router,
database: shallowDatabase,
storage: storage as MemoryStorage<ShallowDatabase>,
responseInterceptors: {
apiInterceptor: databaseConfig.interceptors?.response,
serverInterceptor: serverResponseInterceptor
}
});
createNestedDatabaseRoutes({
router,
database: nestedDatabase,
storage: storage as MemoryStorage<NestedDatabase>,
responseInterceptors: {
apiInterceptor: databaseConfig.interceptors?.response,
serverInterceptor: serverResponseInterceptor
}
});

router.route('/__db').get(
asyncHandler(async (request, response) => {
const data = await callResponseInterceptors({
data: storage.read(),
request,
response,
interceptors: {
apiInterceptor: databaseConfig.interceptors?.response,
serverInterceptor: serverResponseInterceptor
}
});
response.json(data);
})
);

return router;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ describe('CreateNestedDatabaseRoutes', () => {
const routerBase = express.Router();
const storage = new MemoryStorage(nestedDatabase);

const routerWithNestedDatabaseRoutes = createNestedDatabaseRoutes(
routerBase,
nestedDatabase,
const routerWithNestedDatabaseRoutes = createNestedDatabaseRoutes({
router: routerBase,
database: nestedDatabase,
storage
);
});

server.use(express.json());
server.use(express.text());
Expand Down
Loading

0 comments on commit 36a030b

Please sign in to comment.