Skip to content

Commit

Permalink
Removed . import from packages to reduce the chance for circular depe…
Browse files Browse the repository at this point in the history
…ndencies issues
  • Loading branch information
oskardudycz committed Mar 7, 2024
1 parent bc8f116 commit c87acd9
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 181 deletions.
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ The `startAPI` method encapsulates the default startup options like the default

To configure API, we need to provide router configuration. We can do it via the `apis` property of the `getApplication` options. WebApi setup is a simple function that takes the router and defines needed routings on it.

<<< @./../packages/emmett-expressjs/src/index.ts#web-api-setup
<<< @./../packages/emmett-expressjs/src/application.ts#web-api-setup

We recommend providing different web app configurations for different endpoints' logical groupings. It's also worth injecting all needed dependencies from the top, as that will make integration testing easier.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import assert from 'node:assert';
import { randomUUID } from 'node:crypto';
import { after, before, describe, it } from 'node:test';
import { getEventStoreDBEventStore } from '.';
import { getEventStoreDBEventStore } from './eventstoreDBEventStore';

// Events & Entity

Expand Down
77 changes: 77 additions & 0 deletions packages/emmett-expressjs/src/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import express, { Router, type Application } from 'express';
import 'express-async-errors';
import http from 'http';
import { problemDetailsMiddleware } from './middlewares/problemDetailsMiddleware';
import type { ErrorToProblemDetailsMapping } from './responses';

// #region web-api-setup
export type WebApiSetup = (router: Router) => void;
// #endregion web-api-setup

export type ApplicationOptions = {
apis: WebApiSetup[];
mapError?: ErrorToProblemDetailsMapping;
enableDefaultExpressEtag?: boolean;
disableJsonMiddleware?: boolean;
disableUrlEncodingMiddleware?: boolean;
disableProblemDetailsMiddleware?: boolean;
};

export const getApplication = (options: ApplicationOptions) => {
const app: Application = express();

const {
apis,
mapError,
enableDefaultExpressEtag,
disableJsonMiddleware,
disableUrlEncodingMiddleware,
disableProblemDetailsMiddleware,
} = options;

const router = Router();

// disabling default etag behaviour
// to use etags in if-match and if-not-match headers
app.set('etag', enableDefaultExpressEtag ?? false);

// add json middleware
if (!disableJsonMiddleware) app.use(express.json());

// enable url encoded urls and bodies
if (!disableUrlEncodingMiddleware)
app.use(
express.urlencoded({
extended: true,
}),
);

for (const api of apis) {
api(router);
}
app.use(router);

// add problem details middleware
if (!disableProblemDetailsMiddleware)
app.use(problemDetailsMiddleware(mapError));

return app;
};

export type StartApiOptions = {
port?: number;
};

export const startAPI = (
app: Application,
options: StartApiOptions = { port: 3000 },
) => {
const { port } = options;
const server = http.createServer(app);

server.on('listening', () => {
console.info('server up listening');
});

return server.listen(port);
};
181 changes: 2 additions & 179 deletions packages/emmett-expressjs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,184 +1,7 @@
import express, {
Router,
type Application,
type Request,
type Response,
} from 'express';
import 'express-async-errors';
import http from 'http';
import { ProblemDocument } from 'http-problem-details';
import { setETag, type ETag } from './etag';
import { problemDetailsMiddleware } from './middlewares/problemDetailsMiddleware';

export * from './application';
export * from './etag';
export * from './handler';
export * from './responses';
export * from './testing';

export type ErrorToProblemDetailsMapping = (
error: Error,
request: Request,
) => ProblemDocument | undefined;

// #region web-api-setup
export type WebApiSetup = (router: Router) => void;
// #endregion web-api-setup

export type ApplicationOptions = {
apis: WebApiSetup[];
mapError?: ErrorToProblemDetailsMapping;
enableDefaultExpressEtag?: boolean;
disableJsonMiddleware?: boolean;
disableUrlEncodingMiddleware?: boolean;
disableProblemDetailsMiddleware?: boolean;
};

export const getApplication = (options: ApplicationOptions) => {
const app: Application = express();

const {
apis,
mapError,
enableDefaultExpressEtag,
disableJsonMiddleware,
disableUrlEncodingMiddleware,
disableProblemDetailsMiddleware,
} = options;

const router = Router();

// disabling default etag behaviour
// to use etags in if-match and if-not-match headers
app.set('etag', enableDefaultExpressEtag ?? false);

// add json middleware
if (!disableJsonMiddleware) app.use(express.json());

// enable url encoded urls and bodies
if (!disableUrlEncodingMiddleware)
app.use(
express.urlencoded({
extended: true,
}),
);

for (const api of apis) {
api(router);
}
app.use(router);

// add problem details middleware
if (!disableProblemDetailsMiddleware)
app.use(problemDetailsMiddleware(mapError));

return app;
};

export type StartApiOptions = {
port?: number;
};

export const startAPI = (
app: Application,
options: StartApiOptions = { port: 3000 },
) => {
const { port } = options;
const server = http.createServer(app);

server.on('listening', () => {
console.info('server up listening');
});

return server.listen(port);
};

export type HttpResponseOptions = {
body?: unknown;
location?: string;
eTag?: ETag;
};
export const DefaultHttpResponseOptions: HttpResponseOptions = {};

export type HttpProblemResponseOptions = {
location?: string;
eTag?: ETag;
} & Omit<HttpResponseOptions, 'body'> &
(
| {
problem: ProblemDocument;
}
| { problemDetails: string }
);
export const DefaultHttpProblemResponseOptions: HttpProblemResponseOptions = {
problemDetails: 'Error occured!',
};

export type CreatedHttpResponseOptions = {
createdId: string;
urlPrefix?: string;
} & HttpResponseOptions;

export const sendCreated = (
response: Response,
{ createdId, urlPrefix, eTag }: CreatedHttpResponseOptions,
): void =>
send(response, 201, {
location: `${urlPrefix ?? response.req.url}/${createdId}`,
body: { id: createdId },
eTag,
});

export type AcceptedHttpResponseOptions = {
location: string;
} & HttpResponseOptions;

export const sendAccepted = (
response: Response,
options: AcceptedHttpResponseOptions,
): void => send(response, 202, options);

export type NoContentHttpResponseOptions = Omit<HttpResponseOptions, 'body'>;

export const send = (
response: Response,
statusCode: number,
options?: HttpResponseOptions,
): void => {
const { location, body, eTag } = options ?? DefaultHttpResponseOptions;
// HEADERS
if (eTag) setETag(response, eTag);
if (location) response.setHeader('Location', location);

if (body) {
response.statusCode = statusCode;
response.send(body);
} else {
response.sendStatus(statusCode);
}
};

export const sendProblem = (
response: Response,
statusCode: number,
options?: HttpProblemResponseOptions,
): void => {
options = options ?? DefaultHttpProblemResponseOptions;

const { location, eTag } = options;

const problemDetails =
'problem' in options
? options.problem
: new ProblemDocument({
detail: options.problemDetails,
status: statusCode,
});

// HEADERS
if (eTag) setETag(response, eTag);
if (location) response.setHeader('Location', location);

response.setHeader('Content-Type', 'application/problem+json');

response.statusCode = statusCode;
response.json(problemDetails);
};
100 changes: 100 additions & 0 deletions packages/emmett-expressjs/src/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { type Request, type Response } from 'express';
import { ProblemDocument } from 'http-problem-details';
import { setETag, type ETag } from './etag';

export type ErrorToProblemDetailsMapping = (
error: Error,
request: Request,
) => ProblemDocument | undefined;

export type HttpResponseOptions = {
body?: unknown;
location?: string;
eTag?: ETag;
};
export const DefaultHttpResponseOptions: HttpResponseOptions = {};

export type HttpProblemResponseOptions = {
location?: string;
eTag?: ETag;
} & Omit<HttpResponseOptions, 'body'> &
(
| {
problem: ProblemDocument;
}
| { problemDetails: string }
);
export const DefaultHttpProblemResponseOptions: HttpProblemResponseOptions = {
problemDetails: 'Error occured!',
};

export type CreatedHttpResponseOptions = {
createdId: string;
urlPrefix?: string;
} & HttpResponseOptions;

export const sendCreated = (
response: Response,
{ createdId, urlPrefix, eTag }: CreatedHttpResponseOptions,
): void =>
send(response, 201, {
location: `${urlPrefix ?? response.req.url}/${createdId}`,
body: { id: createdId },
eTag,
});

export type AcceptedHttpResponseOptions = {
location: string;
} & HttpResponseOptions;

export const sendAccepted = (
response: Response,
options: AcceptedHttpResponseOptions,
): void => send(response, 202, options);

export type NoContentHttpResponseOptions = Omit<HttpResponseOptions, 'body'>;

export const send = (
response: Response,
statusCode: number,
options?: HttpResponseOptions,
): void => {
const { location, body, eTag } = options ?? DefaultHttpResponseOptions;
// HEADERS
if (eTag) setETag(response, eTag);
if (location) response.setHeader('Location', location);

if (body) {
response.statusCode = statusCode;
response.send(body);
} else {
response.sendStatus(statusCode);
}
};

export const sendProblem = (
response: Response,
statusCode: number,
options?: HttpProblemResponseOptions,
): void => {
options = options ?? DefaultHttpProblemResponseOptions;

const { location, eTag } = options;

const problemDetails =
'problem' in options
? options.problem
: new ProblemDocument({
detail: options.problemDetails,
status: statusCode,
});

// HEADERS
if (eTag) setETag(response, eTag);
if (location) response.setHeader('Location', location);

response.setHeader('Content-Type', 'application/problem+json');

response.statusCode = statusCode;
response.json(problemDetails);
};

0 comments on commit c87acd9

Please sign in to comment.