,
37 | ) {
38 | super(ErrorType.EVENT_ERROR, message);
39 | }
40 | }
41 |
42 | export const isCoreError = (error: Error | undefined): error is CoreError =>
43 | error?.name === ErrorType.CORE_ERROR;
44 |
45 | export const isEventError = (error: Error | undefined): error is EventError =>
46 | error?.name === ErrorType.EVENT_ERROR;
47 |
--------------------------------------------------------------------------------
/packages/core/src/error/specs/error.model.spec.ts:
--------------------------------------------------------------------------------
1 | import { CoreError, isCoreError, EventError, isEventError } from '../error.model';
2 |
3 | describe('Error model', () => {
4 |
5 | beforeEach(() => {
6 | jest.spyOn(Error, 'captureStackTrace');
7 | });
8 |
9 | afterEach(() => {
10 | jest.clearAllMocks();
11 | });
12 |
13 | test('#CoreError creates error object', () => {
14 | // given
15 | const stackTraceFactory = jest.fn();
16 | const error = new CoreError('test-message', {
17 | context: {},
18 | stackTraceFactory,
19 | });
20 |
21 | // when
22 | if (!Error.prepareStackTrace) {
23 | return fail('Error.prepareStackTrace is not defined');
24 | }
25 |
26 | Error.prepareStackTrace(error, []);
27 |
28 | // then
29 | expect(error.name).toBe('CoreError');
30 | expect(error.message).toBe('test-message');
31 | expect(Error.captureStackTrace).toHaveBeenCalled();
32 | expect(stackTraceFactory).toHaveBeenCalledWith('test-message', []);
33 |
34 | return;
35 | });
36 |
37 | test('#isCoreError detects CoreError type', () => {
38 | const coreError = new CoreError('test-message', {
39 | context: {},
40 | stackTraceFactory: jest.fn(),
41 | });
42 | const otherError = new Error();
43 |
44 | expect(isCoreError(coreError)).toBe(true);
45 | expect(isCoreError(otherError)).toBe(false);
46 | });
47 |
48 | test('#isEventError detects EventError type', () => {
49 | const eventError = new EventError({ type: 'TEST', }, 'test-message', {});
50 | const otherError = new Error();
51 |
52 | expect(isEventError(eventError)).toBe(true);
53 | expect(isEventError(otherError)).toBe(false);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/packages/core/src/event/event.interface.ts:
--------------------------------------------------------------------------------
1 | export type EventType = string;
2 |
3 | export interface EventMetadata extends Record {
4 | correlationId?: string;
5 | replyTo?: string;
6 | raw?: any;
7 | }
8 |
9 | export interface Event {
10 | type: T;
11 | payload?: P;
12 | error?: E;
13 | metadata?: EventMetadata;
14 | }
15 |
16 | export interface EventWithoutPayload {
17 | type: T;
18 | metadata?: EventMetadata;
19 | }
20 |
21 | export interface EventWithPayload {
22 | type: T;
23 | payload: P;
24 | metadata?: EventMetadata;
25 | }
26 |
27 | export type ValidatedEvent
=
28 | EventWithPayload
29 |
30 | export type EventsUnion any & { type?: string };
32 | }> = ReturnType;
33 |
--------------------------------------------------------------------------------
/packages/core/src/index.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable deprecation/deprecation */
2 | import * as API from './index';
3 |
4 | describe('@marblejs/core public API', () => {
5 | test('apis are defined', () => {
6 | expect(API.combineEffects).toBeDefined();
7 | expect(API.combineMiddlewares).toBeDefined();
8 | expect(API.createEffectContext).toBeDefined();
9 | expect(API.event).toBeDefined();
10 | expect(API.coreErrorFactory).toBeDefined();
11 |
12 | // errors
13 | expect(API.CoreError).toBeDefined();
14 | expect(API.EventError).toBeDefined();
15 | expect(API.isEventError).toBeDefined();
16 | expect(API.isCoreError).toBeDefined();
17 |
18 | // operators
19 | expect(API.use).toBeDefined();
20 | expect(API.act).toBeDefined();
21 | expect(API.matchEvent).toBeDefined();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | // core - error
2 | export { coreErrorFactory, CoreErrorOptions } from './error/error.factory';
3 | export { CoreError, EventError, isCoreError, isEventError } from './error/error.model';
4 |
5 | // core - effects
6 | export { combineEffects, combineMiddlewares } from './effects/effects.combiner';
7 | export { createEffectContext } from './effects/effectsContext.factory';
8 | export * from './effects/effects.interface';
9 |
10 | // core - operators
11 | export * from './operators';
12 |
13 | // core - logger
14 | export * from './logger';
15 |
16 | // core - event
17 | export * from './event/event';
18 | export * from './event/event.factory';
19 | export * from './event/event.interface';
20 |
21 | // core - listener
22 | export * from './listener/listener.factory';
23 | export * from './listener/listener.interface';
24 |
25 | // core - context
26 | export * from './context/context.hook';
27 | export * from './context/context.logger';
28 | export * from './context/context';
29 | export * from './context/context.helper';
30 | export * from './context/context.reader.factory';
31 | export * from './context/context.token.factory';
32 |
--------------------------------------------------------------------------------
/packages/core/src/listener/listener.factory.ts:
--------------------------------------------------------------------------------
1 | import { flow } from 'fp-ts/lib/function';
2 | import { createReader, ReaderHandler } from '../context/context.reader.factory';
3 | import { ListenerConfig, Listener, ListenerHandler } from './listener.interface';
4 |
5 | export const createListener = (
6 | fn: (config?: T) => ReaderHandler
7 | ): Listener => flow(fn, createReader);
8 |
--------------------------------------------------------------------------------
/packages/core/src/listener/listener.interface.ts:
--------------------------------------------------------------------------------
1 | import { IO } from 'fp-ts/lib/IO';
2 | import { Reader } from 'fp-ts/lib/Reader';
3 | import { Effect } from '../effects/effects.interface';
4 | import { Context, BoundDependency } from '../context/context';
5 |
6 | export interface ListenerConfig {
7 | effects?: any[];
8 | middlewares?: any[];
9 | error$?: Effect;
10 | output$?: Effect;
11 | }
12 |
13 | export type ListenerHandler = (...args: any[]) => void;
14 |
15 | export interface Listener<
16 | T extends ListenerConfig = ListenerConfig,
17 | U extends ListenerHandler = ListenerHandler,
18 | > {
19 | (config?: T): Reader;
20 | }
21 |
22 | export interface ServerIO extends IO> {
23 | context: Context;
24 | }
25 |
26 | export interface ServerConfig<
27 | T extends Effect,
28 | U extends ReturnType = ReturnType,
29 | > {
30 | event$?: T;
31 | listener: U;
32 | dependencies?: (BoundDependency | undefined | null)[];
33 | }
34 |
--------------------------------------------------------------------------------
/packages/core/src/logger/index.ts:
--------------------------------------------------------------------------------
1 | export * from './logger';
2 | export * from './logger.interface';
3 | export * from './logger.token';
4 |
--------------------------------------------------------------------------------
/packages/core/src/logger/logger.interface.ts:
--------------------------------------------------------------------------------
1 | import { IO } from 'fp-ts/lib/IO';
2 |
3 | export type Logger = (opts: LoggerOptions) => IO;
4 |
5 | export enum LoggerLevel { INFO, WARN, ERROR, DEBUG, VERBOSE }
6 |
7 | export type LoggerOptions = {
8 | tag: string;
9 | type: string;
10 | message: string;
11 | level?: LoggerLevel;
12 | data?: Record;
13 | };
14 |
15 | export const enum LoggerTag {
16 | CORE = 'core',
17 | HTTP = 'http',
18 | MESSAGING = 'messaging',
19 | EVENT_BUS = 'event_bus',
20 | WEBSOCKETS = 'websockets',
21 | }
22 |
--------------------------------------------------------------------------------
/packages/core/src/logger/logger.token.ts:
--------------------------------------------------------------------------------
1 | import { createContextToken } from '../context/context.token.factory';
2 | import { Logger } from './logger.interface';
3 |
4 | export const LoggerToken = createContextToken('LoggerToken');
5 |
--------------------------------------------------------------------------------
/packages/core/src/logger/logger.ts:
--------------------------------------------------------------------------------
1 | import * as chalk from 'chalk';
2 | import * as IO from 'fp-ts/lib/IO';
3 | import * as O from 'fp-ts/lib/Option';
4 | import { identity, constUndefined, pipe } from 'fp-ts/lib/function';
5 | import { createReader } from '../context/context.reader.factory';
6 | import { trunc } from '../+internal/utils';
7 | import { Logger, LoggerLevel } from './logger.interface';
8 |
9 | const print = (message: string): IO.IO => () => {
10 | process.stdout.write(message + '\n');
11 | };
12 |
13 | const colorizeText = (level: LoggerLevel): ((s: string) => string) =>
14 | pipe(
15 | O.fromNullable({
16 | [LoggerLevel.ERROR]: chalk.red,
17 | [LoggerLevel.INFO]: chalk.green,
18 | [LoggerLevel.WARN]: chalk.yellow,
19 | [LoggerLevel.DEBUG]: chalk.magenta,
20 | [LoggerLevel.VERBOSE]: identity,
21 | }[level]),
22 | O.getOrElse(() => identity),
23 | );
24 |
25 | const formatDate = (date: Date): string => date
26 | .toISOString()
27 | .replace(/T/, ' ')
28 | .replace(/\..+/, '');
29 |
30 | export const logger = createReader(() => opts => {
31 | const sep = ' - ';
32 | const truncItem = trunc(15);
33 | const colorize = colorizeText(opts.level ?? LoggerLevel.INFO);
34 |
35 | const sign = chalk.magenta('λ');
36 | const now: string = formatDate(new Date());
37 | const pid: string = chalk.green((process.pid.toString() ?? '-'));
38 | const tag: string = chalk.gray(truncItem(opts.tag)) + ' ' + colorize(`[${opts.type}]`);
39 | const message: string = opts.level === LoggerLevel.ERROR ? chalk.red(opts.message) : opts.message;
40 |
41 | return print(sign + sep + pid + sep + now + sep + tag + sep + message);
42 | });
43 |
44 | export const mockLogger = createReader(() => _ => IO.of(constUndefined));
45 |
--------------------------------------------------------------------------------
/packages/core/src/operators/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable deprecation/deprecation */
2 | export { use } from './use/use.operator';
3 | export { act } from './act/act.operator';
4 | export { matchEvent } from './matchEvent/matchEvent.operator';
5 |
--------------------------------------------------------------------------------
/packages/core/src/operators/use/use.operator.ts:
--------------------------------------------------------------------------------
1 | import { Observable, of } from 'rxjs';
2 | import { mergeMap } from 'rxjs/operators';
3 | import { EffectContext, EffectMiddlewareLike } from '../../effects/effects.interface';
4 |
5 | /**
6 | * @deprecated since version 4.0, apply middlewares direcly to the effect Observable chain
7 | *
8 | * `use` operator will be deleted in the next major version (v5.0)
9 | */
10 | export const use =
11 | (middleware: EffectMiddlewareLike, effectContext?: EffectContext) =>
12 | (source$: Observable): Observable =>
13 | source$.pipe(
14 | mergeMap(req => middleware(of(req), effectContext))
15 | );
16 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/http/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/http
8 |
9 | A HTTP module for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/http
15 | ```
16 | Requires `@marblejs/core`, `rxjs` and `fp-ts` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
22 | License: MIT
23 |
--------------------------------------------------------------------------------
/packages/http/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/http/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/http",
3 | "version": "4.1.0",
4 | "description": "HTTP module for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "clean": "rimraf dist",
12 | "test": "jest --config ./jest.config.js"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "marblejs",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "fp-ts": "^2.13.1",
35 | "rxjs": "^7.5.7"
36 | },
37 | "dependencies": {
38 | "@types/json-schema": "^7.0.3",
39 | "@types/qs": "^6.9.0",
40 | "@types/uuid": "^7.0.0",
41 | "chalk": "^3.0.0",
42 | "file-type": "^8.0.0",
43 | "mime": "^2.4.4",
44 | "path-to-regexp": "^6.1.0",
45 | "qs": "^6.9.1",
46 | "uuid": "^7.0.1"
47 | },
48 | "devDependencies": {
49 | "@marblejs/core": "^4.1.0",
50 | "@types/file-type": "^5.2.1",
51 | "@types/mime": "^2.0.1"
52 | },
53 | "publishConfig": {
54 | "access": "public"
55 | },
56 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
57 | }
58 |
--------------------------------------------------------------------------------
/packages/http/src/+internal/header.util.spec.ts:
--------------------------------------------------------------------------------
1 | import { createServer, Server } from 'http';
2 | import * as O from 'fp-ts/lib/Option';
3 | import { getHeaderValue, normalizeHeaders } from './header.util';
4 | import { closeServer, getServerAddress } from './server.util';
5 | import { createHttpRequest } from './testing.util';
6 |
7 | test('#getHeaderValue', () => {
8 | const req = createHttpRequest({
9 | headers: {
10 | 'x-test-1': 'a',
11 | 'x-test-2': ['b', 'c'],
12 | }
13 | });
14 |
15 | expect(getHeaderValue('x-test-1')(req.headers)).toEqual(O.some('a'));
16 | expect(getHeaderValue('x-test-2')(req.headers)).toEqual(O.some('b'));
17 | expect(getHeaderValue('x-test-3')(req.headers)).toEqual(O.none);
18 | });
19 |
20 | test('#getServerAddress', async () => {
21 | const server = await new Promise(res => {
22 | const server = createServer();
23 | server.listen(() => res(server));
24 | });
25 |
26 | expect(getServerAddress(server)).toEqual({
27 | port: expect.any(Number),
28 | host: '127.0.0.1',
29 | });
30 |
31 | await closeServer(server)();
32 | });
33 |
34 | test('#normalizeHeaders', () => {
35 | expect(normalizeHeaders({
36 | 'Content-Type': 'application/json',
37 | 'Authorization': 'Bearer ABC123',
38 | 'x-test-1': 'test-123',
39 | })).toEqual({
40 | 'content-type': 'application/json',
41 | 'authorization': 'Bearer ABC123',
42 | 'x-test-1': 'test-123',
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/packages/http/src/+internal/header.util.ts:
--------------------------------------------------------------------------------
1 | import * as O from 'fp-ts/lib/Option';
2 | import * as A from 'fp-ts/lib/Array';
3 | import { pipe } from 'fp-ts/lib/function';
4 | import { HttpHeaders } from '../http.interface';
5 |
6 | /**
7 | * Get header value for given key from provided headers object
8 | *
9 | * @param key header key
10 | * @since 4.0.0
11 | */
12 | export const getHeaderValue = (key: string) => (headers: HttpHeaders): O.Option =>
13 | pipe(
14 | O.fromNullable(headers[key] ?? headers[key.toLowerCase()]),
15 | O.chain(value => Array.isArray(value)
16 | ? A.head(value) as O.Option
17 | : O.some(String(value)) as O.Option),
18 | );
19 |
20 | /**
21 | * Normalize HTTP headers (transform keys to lowercase)
22 | *
23 | * @param headers
24 | * @since 4.0.0
25 | */
26 | export const normalizeHeaders = (headers: HttpHeaders): HttpHeaders =>
27 | pipe(
28 | Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]),
29 | Object.fromEntries,
30 | );
31 |
--------------------------------------------------------------------------------
/packages/http/src/+internal/metadata.util.ts:
--------------------------------------------------------------------------------
1 | import { JSONSchema7 } from 'json-schema';
2 | import { IO } from 'fp-ts/lib/IO';
3 | import { createUuid } from '@marblejs/core/dist/+internal/utils';
4 | import { HttpHeaders } from '../http.interface';
5 | import { MARBLE_HTTP_REQUEST_METADATA_ENV_KEY } from '../http.config';
6 | import { getHeaderValue } from './header.util';
7 |
8 | export interface RequestMetadata {
9 | path?: string;
10 | body?: JSONSchema7;
11 | headers?: JSONSchema7;
12 | params?: JSONSchema7;
13 | query?: JSONSchema7;
14 | }
15 |
16 | export const HTTP_REQUEST_METADATA_ID_HEADER_KEY = 'X-Request-Metadata-Id';
17 |
18 | /**
19 | * Creates random request metadata header entry
20 | *
21 | * @returns headers `HttpHeaders`
22 | */
23 | export const createRequestMetadataHeader: IO = (): HttpHeaders => ({
24 | [HTTP_REQUEST_METADATA_ID_HEADER_KEY]: createUuid(),
25 | });
26 |
27 | /**
28 | * Get custom request metadata header value
29 | *
30 | * @param headers `HttpHeaders`
31 | * @returns optional header value
32 | */
33 | export const getHttpRequestMetadataIdHeader =
34 | getHeaderValue(HTTP_REQUEST_METADATA_ID_HEADER_KEY);
35 |
36 | /**
37 | * Activates `MARBLE_HTTP_REQUEST_METADATA_ENV_KEY` environment variable
38 | *
39 | * @returns `void`
40 | */
41 | export const enableHttpRequestMetadata: IO = () =>
42 | process.env[MARBLE_HTTP_REQUEST_METADATA_ENV_KEY] = 'true';
43 |
--------------------------------------------------------------------------------
/packages/http/src/+internal/server.util.spec.ts:
--------------------------------------------------------------------------------
1 | import { createServer, Server } from 'http';
2 | import { closeServer, getServerAddress } from './server.util';
3 |
4 | test('#getServerAddress', async () => {
5 | const server = await new Promise(res => {
6 | const server = createServer();
7 | server.listen(() => res(server));
8 | });
9 |
10 | expect(getServerAddress(server)).toEqual({
11 | port: expect.any(Number),
12 | host: '127.0.0.1',
13 | });
14 |
15 | await closeServer(server)();
16 | });
17 |
--------------------------------------------------------------------------------
/packages/http/src/+internal/server.util.ts:
--------------------------------------------------------------------------------
1 | import { AddressInfo } from 'net';
2 | import { Task } from 'fp-ts/lib/Task';
3 | import { HttpServer } from '../http.interface';
4 |
5 | export const closeServer = (server: HttpServer): Task => () =>
6 | new Promise((res, rej) => server.close(err => err ? rej(err) : res(undefined)));
7 |
8 | export const getServerAddress = (server: HttpServer): { host: string; port: number } => {
9 | const serverAddressInfo = server.address() as AddressInfo;
10 | const host = serverAddressInfo.address === '::' ? '127.0.0.1' : serverAddressInfo.address;
11 | const port = serverAddressInfo.port;
12 |
13 | return { host, port };
14 | };
15 |
--------------------------------------------------------------------------------
/packages/http/src/+internal/urlEncoded.util.ts:
--------------------------------------------------------------------------------
1 | import * as qs from 'qs';
2 | import { isString } from '@marblejs/core/dist/+internal/utils';
3 |
4 | export const transformUrlEncoded = (data: any): string =>
5 | !isString(data) ? qs.stringify(data) : data;
6 |
--------------------------------------------------------------------------------
/packages/http/src/effects/http.effects.interface.ts:
--------------------------------------------------------------------------------
1 | import { Event, Effect } from '@marblejs/core';
2 | import { HttpRequest, HttpStatus, HttpHeaders, HttpServer, WithHttpRequest } from '../http.interface';
3 |
4 | export interface HttpEffectResponse {
5 | request?: HttpRequest;
6 | status?: HttpStatus;
7 | headers?: HttpHeaders;
8 | body?: T;
9 | }
10 |
11 | export interface HttpMiddlewareEffect<
12 | I extends HttpRequest = HttpRequest,
13 | O extends HttpRequest = HttpRequest,
14 | > extends HttpEffect {}
15 |
16 | export interface HttpErrorEffect<
17 | Err extends Error = Error,
18 | Req extends HttpRequest = HttpRequest,
19 | > extends HttpEffect<
20 | WithHttpRequest<{ error: Err }, Req>,
21 | WithHttpRequest
22 | > {}
23 |
24 | export interface HttpServerEffect<
25 | Ev extends Event = Event
26 | > extends HttpEffect {}
27 |
28 | export interface HttpOutputEffect<
29 | Req extends HttpRequest = HttpRequest,
30 | > extends HttpEffect<
31 | WithHttpRequest,
32 | WithHttpRequest
33 | > {}
34 |
35 | export interface HttpEffect<
36 | I = HttpRequest,
37 | O = HttpEffectResponse,
38 | > extends Effect {}
39 |
--------------------------------------------------------------------------------
/packages/http/src/effects/http.effects.ts:
--------------------------------------------------------------------------------
1 | import { matchEvent, useContext, LoggerToken, LoggerTag, LoggerLevel } from '@marblejs/core';
2 | import { tap, map } from 'rxjs/operators';
3 | import { ServerEvent } from '../server/http.server.event';
4 | import { HttpServerEffect } from './http.effects.interface';
5 |
6 | export const listening$: HttpServerEffect = (event$, ctx) => {
7 | const logger = useContext(LoggerToken)(ctx.ask);
8 |
9 | return event$.pipe(
10 | matchEvent(ServerEvent.listening),
11 | map(event => event.payload),
12 | tap(({ host, port }) => {
13 | const message = `Server running @ http://${host}:${port}/ 🚀`;
14 | const log = logger({ tag: LoggerTag.HTTP, level: LoggerLevel.INFO, type: 'Server', message });
15 |
16 | log();
17 | }),
18 | );
19 | };
20 |
21 | export const error$: HttpServerEffect = (event$, ctx) => {
22 | const logger = useContext(LoggerToken)(ctx.ask);
23 |
24 | return event$.pipe(
25 | matchEvent(ServerEvent.error),
26 | map(event => event.payload),
27 | tap(({ error }) => {
28 | const message = `Unexpected server error occured: "${error.name}", "${error.message}"`;
29 | const log = logger({ tag: LoggerTag.HTTP, level: LoggerLevel.ERROR, type: 'Server', message });
30 |
31 | log();
32 | }),
33 | );
34 | };
35 |
36 | export const close$: HttpServerEffect = (event$, ctx) => {
37 | const logger = useContext(LoggerToken)(ctx.ask);
38 |
39 | return event$.pipe(
40 | matchEvent(ServerEvent.close),
41 | map(event => event.payload),
42 | tap(() => {
43 | const message = `Server connection was closed`;
44 | const log = logger({ tag: LoggerTag.HTTP, level: LoggerLevel.INFO, type: 'Server', message });
45 |
46 | log();
47 | }),
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/packages/http/src/error/http.error.effect.ts:
--------------------------------------------------------------------------------
1 | import { map } from 'rxjs/operators';
2 | import { HttpErrorEffect } from '../effects/http.effects.interface';
3 | import { HttpStatus } from '../http.interface';
4 | import { HttpError, isHttpError } from './http.error.model';
5 |
6 | interface HttpErrorResponse {
7 | error: {
8 | status: HttpStatus;
9 | message: string;
10 | data?: any;
11 | context?: string;
12 | };
13 | }
14 |
15 | const defaultHttpError = new HttpError(
16 | 'Internal server error',
17 | HttpStatus.INTERNAL_SERVER_ERROR,
18 | );
19 |
20 | const getStatusCode = (error: Error): HttpStatus =>
21 | isHttpError(error)
22 | ? error.status
23 | : HttpStatus.INTERNAL_SERVER_ERROR;
24 |
25 | const errorFactory = (status: HttpStatus) => (error: Error): HttpErrorResponse => ({
26 | error: isHttpError(error)
27 | ? { status, message: error.message, data: error.data, context: error.context }
28 | : { status, message: error.message },
29 | });
30 |
31 | export const defaultError$: HttpErrorEffect = req$ =>
32 | req$.pipe(
33 | map(({ request, error = defaultHttpError }) => {
34 | const status = getStatusCode(error);
35 | const body = errorFactory(status)(error);
36 | return ({ status, body, request });
37 | }),
38 | );
39 |
--------------------------------------------------------------------------------
/packages/http/src/error/http.error.model.spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpError, isHttpError } from './http.error.model';
2 |
3 | describe('Http error model', () => {
4 |
5 | test('#HttpError creates error object', () => {
6 | const error = new HttpError('test-message', 200);
7 |
8 | expect(error.name).toBe('HttpError');
9 | expect(error.status).toBe(200);
10 | expect(error.message).toBe('test-message');
11 | });
12 |
13 | test('#isHttpError detects HttpError type', () => {
14 | const httpError = new HttpError('test-message', 200);
15 | const otherError = new Error();
16 |
17 | expect(isHttpError(httpError)).toBe(true);
18 | expect(isHttpError(otherError)).toBe(false);
19 | });
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/packages/http/src/http.config.ts:
--------------------------------------------------------------------------------
1 | import { getEnvConfigOrElseAsBoolean } from '@marblejs/core/dist/+internal/utils';
2 | import { IO } from 'fp-ts/lib/IO';
3 |
4 | /**
5 | * Flag to indicate whether we should prevent converting (normalizing) all headers to lower case.
6 | * This flag was introduced to prevent breaking changes, for more details see:
7 | * https://github.com/marblejs/marble/issues/311
8 | *
9 | * Flag will be removed in the next major version,
10 | * where all headers are normalized by default.
11 | *
12 | * @since 4.0.0
13 | */
14 | export const MARBLE_HTTP_HEADERS_NORMALIZATION_ENV_KEY = 'MARBLE_HTTP_HEADERS_NORMALIZATION';
15 |
16 | /**
17 | * If enabled applies request metadata to every outgoing HTTP response
18 | *
19 | * @since 2.0.0
20 | */
21 | export const MARBLE_HTTP_REQUEST_METADATA_ENV_KEY = 'MARBLE_HTTP_REQUEST_METADATA';
22 |
23 | type HttpModuleConfiguration = Readonly<{
24 | useHttpHeadersNormalization: IO;
25 | useHttpRequestMetadata: IO;
26 | }>;
27 |
28 | /**
29 | * Initialize and provide environment configuration
30 | *
31 | * @since 4.0.0
32 | */
33 | export const provideConfig: IO = () => ({
34 | useHttpHeadersNormalization: getEnvConfigOrElseAsBoolean(MARBLE_HTTP_HEADERS_NORMALIZATION_ENV_KEY, true),
35 | useHttpRequestMetadata: getEnvConfigOrElseAsBoolean(MARBLE_HTTP_REQUEST_METADATA_ENV_KEY, false),
36 | });
37 |
--------------------------------------------------------------------------------
/packages/http/src/index.ts:
--------------------------------------------------------------------------------
1 | // config
2 | export {
3 | provideConfig,
4 | MARBLE_HTTP_HEADERS_NORMALIZATION_ENV_KEY,
5 | MARBLE_HTTP_REQUEST_METADATA_ENV_KEY,
6 | } from './http.config';
7 |
8 | // http
9 | export { defaultError$ } from './error/http.error.effect';
10 | export { HttpError, HttpRequestError, isHttpError, isHttpRequestError } from './error/http.error.model';
11 | export { createServer } from './server/http.server';
12 | export { combineRoutes } from './router/http.router.combiner';
13 | export { r } from './router/http.router.ixbuilder';
14 | export * from './router/http.router.interface';
15 | export * from './effects/http.effects.interface';
16 | export * from './server/http.server.event';
17 | export * from './server/http.server.interface';
18 | export * from './server/http.server.listener';
19 | export * from './http.interface';
20 |
21 | // http - server - internal dependencies
22 | export * from './server/internal-dependencies/httpRequestMetadataStorage.reader';
23 | export * from './server/internal-dependencies/httpServerClient.reader';
24 | export * from './server/internal-dependencies/httpServerEventStream.reader';
25 | export * from './server/internal-dependencies/httpRequestBus.reader';
26 |
--------------------------------------------------------------------------------
/packages/http/src/response/http.responseBody.factory.ts:
--------------------------------------------------------------------------------
1 | import { Stream } from 'stream';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { bufferFrom, isStream, stringifyJson } from '@marblejs/core/dist/+internal/utils';
4 | import { HttpHeaders } from '../http.interface';
5 | import { ContentType, getContentTypeUnsafe, isJsonContentType } from '../+internal/contentType.util';
6 | import { transformUrlEncoded } from '../+internal/urlEncoded.util';
7 |
8 | export type ResponseBodyFactory = (data: { headers: HttpHeaders, body: any }) => string | Stream | Buffer;
9 |
10 | export const factorizeBody: ResponseBodyFactory = ({ headers, body }) => {
11 | const contentType = getContentTypeUnsafe(headers);
12 |
13 | if (isStream(body))
14 | return body;
15 |
16 | if (isJsonContentType(contentType))
17 | return stringifyJson(body);
18 |
19 | switch (contentType) {
20 | case ContentType.APPLICATION_X_WWW_FORM_URLENCODED:
21 | return transformUrlEncoded(body);
22 | case ContentType.APPLICATION_OCTET_STREAM:
23 | return !Buffer.isBuffer(body)
24 | ? pipe(body, stringifyJson, bufferFrom)
25 | : body;
26 | case ContentType.TEXT_PLAIN:
27 | return String(body);
28 | default:
29 | return body;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/packages/http/src/router/http.router.combiner.ts:
--------------------------------------------------------------------------------
1 | import { HttpMiddlewareEffect } from '../effects/http.effects.interface';
2 | import { RouteCombinerConfig, RouteEffectGroup, RouteEffect, ErrorSubject } from './http.router.interface';
3 | import { isRouteCombinerConfig, decorateMiddleware } from './http.router.helpers';
4 |
5 | export function combineRoutes(path: string, config: RouteCombinerConfig): RouteEffectGroup;
6 | export function combineRoutes(path: string, effects: (RouteEffect | RouteEffectGroup)[]): RouteEffectGroup;
7 | export function combineRoutes(
8 | path: string,
9 | configOrEffects: RouteCombinerConfig | (RouteEffect | RouteEffectGroup)[]
10 | ): RouteEffectGroup {
11 | return {
12 | path,
13 | effects: isRouteCombinerConfig(configOrEffects)
14 | ? configOrEffects.effects
15 | : configOrEffects,
16 | middlewares: isRouteCombinerConfig(configOrEffects)
17 | ? (configOrEffects.middlewares || [])
18 | : [],
19 | };
20 | }
21 |
22 | export const combineRouteMiddlewares =
23 | (decorate: boolean, errorSubject: ErrorSubject) => (...middlewares: HttpMiddlewareEffect[]): HttpMiddlewareEffect => (input$, ctx) =>
24 | middlewares.reduce(
25 | (i$, middleware) => middleware(decorate ? decorateMiddleware(i$, errorSubject) : i$, ctx),
26 | decorate ? decorateMiddleware(input$, errorSubject) : input$,
27 | );
28 |
--------------------------------------------------------------------------------
/packages/http/src/router/http.router.effects.ts:
--------------------------------------------------------------------------------
1 | import { throwError } from 'rxjs';
2 | import { mergeMap } from 'rxjs/operators';
3 | import { HttpError } from '../error/http.error.model';
4 | import { HttpStatus } from '../http.interface';
5 | import { r } from './http.router.ixbuilder';
6 |
7 | export const ROUTE_NOT_FOUND_ERROR = new HttpError('Route not found', HttpStatus.NOT_FOUND);
8 |
9 | export const notFound$ = r.pipe(
10 | r.matchPath('*'),
11 | r.matchType('*'),
12 | r.useEffect(req$ => req$.pipe(mergeMap(() => throwError(() => ROUTE_NOT_FOUND_ERROR)))),
13 | r.applyMeta({ overridable: true }));
14 |
--------------------------------------------------------------------------------
/packages/http/src/router/http.router.matcher.ts:
--------------------------------------------------------------------------------
1 | import { HttpMethod } from '../http.interface';
2 | import { BootstrappedRouting, RouteMatched } from './http.router.interface';
3 |
4 | export const matchRoute = (routing: BootstrappedRouting) => (url: string, method: HttpMethod): RouteMatched | undefined => {
5 | for (let i = 0; i < routing.length; ++i) {
6 | const { regExp, methods, path } = routing[i];
7 | const match = url.match(regExp);
8 |
9 | if (!match) { continue; }
10 |
11 | const matchedMethod = methods[method] || methods['*'];
12 |
13 | if (!matchedMethod) { continue; }
14 |
15 | const params = {};
16 |
17 | if (matchedMethod.parameters) {
18 | for (let p = 0; p < matchedMethod.parameters.length; p++) {
19 | params[matchedMethod.parameters[p]] = decodeURIComponent(match[p + 1]);
20 | }
21 | }
22 |
23 | return {
24 | subject: matchedMethod.subject,
25 | params,
26 | path,
27 | };
28 | }
29 |
30 | return undefined;
31 | };
32 |
--------------------------------------------------------------------------------
/packages/http/src/router/http.router.params.factory.ts:
--------------------------------------------------------------------------------
1 | import { pathToRegexp, Key } from 'path-to-regexp';
2 | import { ParametricRegExp } from './http.router.interface';
3 |
4 | export const factorizeRegExpWithParams = (path: string): ParametricRegExp => {
5 | const keys: Key[] = [];
6 | const preparedPath = path
7 | .replace(/\/\*/g, '/(.*)') /* Transfer wildcards */
8 | .replace(/\/\/+/g, '/') /* Remove repeated backslashes */
9 | .replace(/\/$/, ''); /* Remove trailing backslash */
10 |
11 | const regExp = pathToRegexp(preparedPath, keys, { strict: false });
12 | const regExpParameters = keys
13 | .filter(key => key.name !== 0) /* Filter wildcard groups */
14 | .map(key => String(key.name));
15 |
16 | return {
17 | regExp,
18 | parameters: regExpParameters.length > 0 ? regExpParameters : undefined,
19 | path: preparedPath,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/packages/http/src/router/http.router.query.factory.ts:
--------------------------------------------------------------------------------
1 | import * as qs from 'qs';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { fromNullable, map, getOrElse } from 'fp-ts/lib/Option';
4 | import { QueryParameters } from '../http.interface';
5 |
6 | export const queryParamsFactory = (queryParams: string | undefined | null): QueryParameters => pipe(
7 | fromNullable(queryParams),
8 | map(qs.parse),
9 | getOrElse(() => ({})),
10 | );
11 |
--------------------------------------------------------------------------------
/packages/http/src/router/specs/http.router.helper.spec.ts:
--------------------------------------------------------------------------------
1 | import { isRouteEffectGroup, isRouteCombinerConfig } from '../http.router.helpers';
2 |
3 | describe('Router helper', () => {
4 |
5 | test('#isRouteGroup checks if provided argument is typeof RouteGroup', () => {
6 | expect(isRouteEffectGroup({ path: '/', effects: [], middlewares: [] })).toEqual(true);
7 | expect(isRouteEffectGroup({ path: '/', effects: [] })).toEqual(false);
8 | expect(isRouteEffectGroup({ path: '/', method: 'GET', effect: req$ => req$ })).toEqual(false);
9 | });
10 |
11 | test('#isRouteCombinerConfig checks if provided argument is typeof RouteCombinerConfig', () => {
12 | expect(isRouteCombinerConfig({ effects: [], middlewares: [] })).toEqual(true);
13 | expect(isRouteCombinerConfig([req$ => req$])).toEqual(false);
14 | });
15 |
16 | });
17 |
--------------------------------------------------------------------------------
/packages/http/src/server/http.server.interface.ts:
--------------------------------------------------------------------------------
1 | import * as https from 'https';
2 | import { ServerConfig } from '@marblejs/core';
3 | import { HttpServerEffect } from '../effects/http.effects.interface';
4 | import { httpListener } from './http.server.listener';
5 |
6 | export const DEFAULT_HOSTNAME = '127.0.0.1';
7 |
8 | type HttpListenerFn = ReturnType;
9 |
10 | export interface CreateServerConfig extends ServerConfig {
11 | port?: number;
12 | hostname?: string;
13 | options?: ServerOptions;
14 | }
15 |
16 | export interface ServerOptions {
17 | httpsOptions?: https.ServerOptions;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/http/src/server/internal-dependencies/httpRequestBus.reader.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 | import { createContextToken, createReader } from '@marblejs/core';
3 | import { HttpRequest } from '../../http.interface';
4 |
5 | export type HttpRequestBus = ReturnType;
6 |
7 | export const HttpRequestBusToken = createContextToken('HttpRequestBus');
8 |
9 | export const HttpRequestBus = createReader(_ => new Subject());
10 |
--------------------------------------------------------------------------------
/packages/http/src/server/internal-dependencies/httpServerClient.reader.ts:
--------------------------------------------------------------------------------
1 | import { createContextToken, createReader } from '@marblejs/core';
2 | import { HttpServer } from '../../http.interface';
3 | import { HttpRequestBus } from './httpRequestBus.reader';
4 |
5 | export type HttpServerClient = ReturnType;
6 |
7 | export const HttpServerClientToken = createContextToken('HttpServerClient');
8 |
9 | export const HttpServerClient = (httpServer: HttpServer) => createReader(_ => httpServer);
10 |
--------------------------------------------------------------------------------
/packages/http/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/messaging/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/messaging
8 |
9 | A messaging module for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/messaging
15 | ```
16 | Requires `@marblejs/core`, `rxjs` and `fp-ts` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
22 |
23 | License: MIT
24 |
--------------------------------------------------------------------------------
/packages/messaging/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/messaging/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/messaging",
3 | "version": "4.1.0",
4 | "description": "Messaging module for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "clean": "rimraf dist",
12 | "test": "jest --config ./jest.config.js"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "Józef Flakus ",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "fp-ts": "^2.13.1",
35 | "rxjs": "^7.5.7"
36 | },
37 | "dependencies": {
38 | "chalk": "~2.4.1"
39 | },
40 | "devDependencies": {
41 | "@marblejs/core": "^4.1.0",
42 | "@marblejs/middleware-io": "^4.1.0",
43 | "@types/amqp-connection-manager": "^2.0.4",
44 | "@types/amqplib": "^0.5.11",
45 | "@types/redis": "^2.8.14",
46 | "amqp-connection-manager": "^3.2.0",
47 | "amqplib": "^0.5.3",
48 | "redis": "^3.1.1"
49 | },
50 | "publishConfig": {
51 | "access": "public"
52 | },
53 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
54 | }
55 |
--------------------------------------------------------------------------------
/packages/messaging/src/+internal/testing/index.ts:
--------------------------------------------------------------------------------
1 | export * from './messaging.testBed';
2 |
--------------------------------------------------------------------------------
/packages/messaging/src/+internal/testing/messaging.testBed.ts:
--------------------------------------------------------------------------------
1 | import { Microservice } from '../../server/messaging.server.interface';
2 | import { TransportLayerConnection } from '../../transport/transport.interface';
3 |
4 | export const createMicroserviceTestBed = (microservice: Promise) => {
5 | let connection: TransportLayerConnection;
6 |
7 | const getInstance = () => connection;
8 |
9 | beforeAll(async () => {
10 | const app = await microservice;
11 | connection = await app();
12 | });
13 |
14 | afterAll(async () => connection.close());
15 |
16 | return {
17 | getInstance,
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/packages/messaging/src/ack/ack.spec.ts:
--------------------------------------------------------------------------------
1 | import { createEffectContext, contextFactory, lookup, Event, bindTo } from '@marblejs/core';
2 | import { TransportLayerConnection } from '../transport/transport.interface';
3 | import { EventTimerStoreToken, EventTimerStore } from '../eventStore/eventTimerStore';
4 | import { ackEvent, nackEvent, nackAndResendEvent } from './ack';
5 |
6 | const prepareEffectContext = async () => {
7 | const ctx = await contextFactory(bindTo(EventTimerStoreToken)(EventTimerStore));
8 | const ask = lookup(ctx);
9 | const client = {
10 | ackMessage: jest.fn(),
11 | nackMessage: jest.fn(),
12 | } as unknown as TransportLayerConnection;
13 |
14 | return createEffectContext({ ask, client });
15 | };
16 |
17 | describe('#ackEvent, #nackEvent, #nackAndRequeueEvent', () => {
18 | test('handles event with empty metadata', async () => {
19 | // given
20 | const ctx = await prepareEffectContext();
21 | const event: Event = { type: 'TEST' };
22 |
23 | // when
24 | const result = Promise.all([
25 | ackEvent(ctx)(event)(),
26 | nackEvent(ctx)(event)(),
27 | nackAndResendEvent(ctx)(event)(),
28 | ]);
29 |
30 | expect(result).resolves.toEqual([true, true, true]);
31 | });
32 |
33 | test('handles event with metadata defined', async () => {
34 | // given
35 | const ctx = await prepareEffectContext();
36 | const event: Event = { type: 'TEST', metadata: { correlationId: '123', raw: {} } };
37 |
38 | // when
39 | const result = Promise.all([
40 | ackEvent(ctx)(event)(),
41 | nackEvent(ctx)(event)(),
42 | nackAndResendEvent(ctx)(event)(),
43 | ]);
44 |
45 | expect(result).resolves.toEqual([true, true, true]);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/packages/messaging/src/effects/messaging.effects.interface.ts:
--------------------------------------------------------------------------------
1 | import { Effect, Event } from '@marblejs/core';
2 | import { TransportLayerConnection } from '../transport/transport.interface';
3 |
4 | type MsgClient = TransportLayerConnection;
5 |
6 | export interface MsgMiddlewareEffect<
7 | I = Event,
8 | O = Event,
9 | > extends MsgEffect {}
10 |
11 | export interface MsgErrorEffect<
12 | Err extends Error = Error,
13 | > extends MsgEffect {}
14 |
15 | export interface MsgEffect<
16 | I = Event,
17 | O = Event,
18 | Client = MsgClient,
19 | > extends Effect {}
20 |
21 | export interface MsgOutputEffect<
22 | I = Event,
23 | O = Event,
24 | > extends MsgEffect {}
25 |
26 | export interface MsgServerEffect
27 | extends MsgEffect {}
28 |
--------------------------------------------------------------------------------
/packages/messaging/src/eventbus/messaging.eventBusClient.reader.ts:
--------------------------------------------------------------------------------
1 | import { createContextToken, createReader, useContext, LoggerToken, LoggerTag, LoggerLevel } from '@marblejs/core';
2 | import { MessagingClient } from '../client/messaging.client';
3 |
4 | export interface EventBusClient extends MessagingClient {}
5 |
6 | export const EventBusClientToken = createContextToken('EventBusClient');
7 |
8 | /**
9 | * `EventBusClient` has to be registered eagerly after main `EventBus`
10 | * @returns asynchronous reader of `EventBus`
11 | * @since v3.0
12 | */
13 | export const EventBusClient = createReader(ask => {
14 | const logger = useContext(LoggerToken)(ask);
15 |
16 | const logWarning = logger({
17 | tag: LoggerTag.EVENT_BUS,
18 | level: LoggerLevel.WARN,
19 | type: 'eventBusClient',
20 | message: '"EventBus" requires to be registered eagerly before "EventBusClient" reader.',
21 | });
22 |
23 | logWarning();
24 | });
25 |
26 | /**
27 | * An alias for `EventBusClient`
28 | *
29 | * @deprecated since version `v4.0`. Use `EventBusClient` instead.
30 | * Will be removed in version `v5.0`
31 | */
32 | export const eventBusClient = EventBusClient;
33 |
--------------------------------------------------------------------------------
/packages/messaging/src/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as API from './index';
2 |
3 | describe('@marblejs/messaging', () => {
4 | test('public APIs are defined', () => {
5 | expect(API.Transport).toBeDefined();
6 | expect(API.TransportLayerToken).toBeDefined();
7 | expect(API.ServerEvent).toBeDefined();
8 | expect(API.createMicroservice).toBeDefined();
9 | expect(API.MessagingClient).toBeDefined();
10 | expect(API.messagingListener).toBeDefined();
11 | expect(API.EventBus).toBeDefined();
12 | expect(API.EventBusClient).toBeDefined();
13 | expect(API.EventBusClientToken).toBeDefined();
14 | expect(API.EventBusToken).toBeDefined();
15 | expect(API.reply).toBeDefined();
16 | expect(API.ackEvent).toBeDefined();
17 | expect(API.nackEvent).toBeDefined();
18 | expect(API.nackAndResendEvent).toBeDefined();
19 | expect(API.EVENT_BUS_CHANNEL).toBeDefined();
20 | expect(API.AmqpConnectionStatus).toBeDefined();
21 | expect(API.RedisConnectionStatus).toBeDefined();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/messaging/src/index.ts:
--------------------------------------------------------------------------------
1 | // client
2 | export * from './client/messaging.client';
3 |
4 | // transport
5 | export * from './transport/transport.interface';
6 | export { AmqpStrategyOptions, AmqpConnectionStatus } from './transport/strategies/amqp.strategy.interface';
7 | export { RedisStrategyOptions, RedisConnectionStatus } from './transport/strategies/redis.strategy.interface';
8 | export { LocalStrategyOptions, EVENT_BUS_CHANNEL } from './transport/strategies/local.strategy.interface';
9 |
10 | // effects, middlewares
11 | export * from './effects/messaging.effects.interface';
12 |
13 | // handy functions
14 | export { reply } from './reply/reply';
15 | export { ackEvent, nackEvent, nackAndResendEvent } from './ack/ack';
16 |
17 | // server
18 | export * from './server/messaging.server';
19 | export * from './server/messaging.server.interface';
20 | export * from './server/messaging.server.tokens';
21 | export * from './server/messaging.server.events';
22 | export { messagingListener } from './server/messaging.server.listener';
23 |
24 | // readers
25 | export * from './eventbus/messaging.eventBus.reader';
26 | export * from './eventbus/messaging.eventBusClient.reader';
27 | export * from './eventStore/eventTimerStore';
28 |
--------------------------------------------------------------------------------
/packages/messaging/src/middlewares/messaging.eventInput.middleware.ts:
--------------------------------------------------------------------------------
1 | import { map } from 'rxjs/operators';
2 | import { createUuid } from '@marblejs/core/dist/+internal/utils';
3 | import { MsgMiddlewareEffect } from '../effects/messaging.effects.interface';
4 |
5 | export const idApplier$: MsgMiddlewareEffect = event$ =>
6 | event$.pipe(
7 | map(event => ({
8 | ...event,
9 | metadata: {
10 | ...event.metadata,
11 | correlationId: event.metadata?.correlationId ?? createUuid(),
12 | },
13 | }))
14 | );
15 |
--------------------------------------------------------------------------------
/packages/messaging/src/middlewares/messaging.eventOutput.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Event, useContext } from '@marblejs/core';
2 | import { createUuid, isNonNullable, encodeError } from '@marblejs/core/dist/+internal/utils';
3 | import { map } from 'rxjs/operators';
4 | import { MsgOutputEffect } from '../effects/messaging.effects.interface';
5 | import { TransportLayerToken } from '../server/messaging.server.tokens';
6 |
7 | export const outputRouter$: MsgOutputEffect = (event$, ctx) => {
8 | const transportLayer = useContext(TransportLayerToken)(ctx.ask);
9 | const originChannel = transportLayer.config.channel;
10 |
11 | return event$.pipe(
12 | map(event => ({
13 | ...event,
14 | metadata: {
15 | ...event.metadata,
16 | correlationId: event.metadata?.correlationId ?? createUuid(),
17 | replyTo: event.metadata?.replyTo ?? originChannel
18 | },
19 | }))
20 | );
21 | };
22 |
23 | export const outputErrorEncoder$: MsgOutputEffect> = event$ => {
24 | const hasError = (event: Event<{ error?: any }>): boolean =>
25 | [event.payload?.error, event.error].some(Boolean);
26 |
27 | return event$.pipe(
28 | map(event => {
29 | if (!hasError(event)) return event;
30 |
31 | const eventError = event.error;
32 | const payloadError = event.payload?.error;
33 |
34 | if (isNonNullable(eventError))
35 | event.error = encodeError(eventError);
36 |
37 | if (isNonNullable(payloadError))
38 | event.payload.error = encodeError(payloadError);
39 |
40 | return event;
41 | }),
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/packages/messaging/src/reply/reply.ts:
--------------------------------------------------------------------------------
1 | import { Event, EventMetadata } from '@marblejs/core';
2 | import { isString, NamedError } from '@marblejs/core/dist/+internal/utils';
3 |
4 | export const UNKNOWN_TAG = '_UNKNOWN_';
5 |
6 | export class MissingEventTypeError extends NamedError {
7 | constructor() {
8 | super('MissingEventTypeError', `#reply - Missing type literal`);
9 | }
10 | }
11 |
12 | function assertEventType(event: Partial): asserts event is Required {
13 | if (!event.type) throw new MissingEventTypeError();
14 | }
15 |
16 | function isEventMetadata(metadata: any): metadata is EventMetadata {
17 | return metadata.correlationId || metadata.replyTo;
18 | }
19 |
20 | const composeMetadata = (originMetadata?: EventMetadata) => (customMetadata?: EventMetadata): EventMetadata => ({
21 | raw: originMetadata?.raw ?? customMetadata?.raw,
22 | correlationId: originMetadata?.correlationId ?? customMetadata?.correlationId,
23 | replyTo: originMetadata?.replyTo ?? customMetadata?.replyTo ?? UNKNOWN_TAG,
24 | });
25 |
26 | export const reply = (to: string | EventMetadata | Event) => (event: T): T => {
27 | assertEventType(event);
28 |
29 | return isString(to)
30 | ? { ...event, metadata: composeMetadata(event.metadata)({ replyTo: to }) }
31 | : isEventMetadata(to)
32 | ? { ...event, metadata: composeMetadata(event.metadata)(to) }
33 | : { ...to, ...event, metadata: composeMetadata(event.metadata)(to.metadata) };
34 | };
35 |
--------------------------------------------------------------------------------
/packages/messaging/src/server/messaging.server.events.ts:
--------------------------------------------------------------------------------
1 | import { createEvent, EventsUnion, Event } from '@marblejs/core';
2 |
3 | export enum ServerEventType {
4 | STATUS = 'status',
5 | CLOSE = 'close',
6 | ERROR = 'error',
7 | }
8 |
9 | export const ServerEvent = {
10 | status: createEvent(
11 | ServerEventType.STATUS,
12 | (host: string, channel: string, type: string) => ({ host, channel, type }),
13 | ),
14 | close: createEvent(
15 | ServerEventType.CLOSE,
16 | ),
17 | error: createEvent(
18 | ServerEventType.ERROR,
19 | (error: Error) => ({ error }),
20 | )
21 | };
22 |
23 | export type AllServerEvents = EventsUnion;
24 |
25 | export function isStatusEvent(event: Event): event is ReturnType {
26 | return event.type === ServerEventType.STATUS;
27 | }
28 |
29 | export function isCloseEvent(event: Event): event is ReturnType {
30 | return event.type === ServerEventType.CLOSE;
31 | }
32 |
33 | export function isErrorEvent(event: Event): event is ReturnType {
34 | return event.type === ServerEventType.ERROR;
35 | }
36 |
--------------------------------------------------------------------------------
/packages/messaging/src/server/messaging.server.interface.ts:
--------------------------------------------------------------------------------
1 | import { ServerConfig, ServerIO } from '@marblejs/core';
2 | import { MsgServerEffect } from '../effects/messaging.effects.interface';
3 | import { TransportStrategy, TransportLayerConnection } from '../transport/transport.interface';
4 | import { messagingListener } from './messaging.server.listener';
5 |
6 | type MessagingListenerFn = ReturnType;
7 | type ConfigurationBase = ServerConfig;
8 |
9 | export type CreateMicroserviceConfig =
10 | & TransportStrategy
11 | & ConfigurationBase
12 | ;
13 |
14 | export type Microservice = ServerIO;
15 |
--------------------------------------------------------------------------------
/packages/messaging/src/server/messaging.server.tokens.ts:
--------------------------------------------------------------------------------
1 | import { createContextToken } from '@marblejs/core';
2 | import { Subject } from 'rxjs';
3 | import { TransportLayer } from '../transport/transport.interface';
4 | import { AllServerEvents } from './messaging.server.events';
5 |
6 | export const TransportLayerToken = createContextToken('TransportLayerToken');
7 | export const ServerEventsToken = createContextToken>('ServerEventsToken');
8 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/strategies/amqp.strategy.interface.ts:
--------------------------------------------------------------------------------
1 | import { NamedError } from '@marblejs/core/dist/+internal/utils';
2 | import { Transport } from '../transport.interface';
3 |
4 | export interface AmqpStrategy {
5 | transport: Transport.AMQP;
6 | options: AmqpStrategyOptions;
7 | }
8 |
9 | export interface AmqpStrategyOptions {
10 | host: string;
11 | queue: string;
12 | queueOptions?: {
13 | exclusive?: boolean;
14 | durable?: boolean;
15 | autoDelete?: boolean;
16 | arguments?: any;
17 | messageTtl?: number;
18 | expires?: number;
19 | deadLetterExchange?: string;
20 | deadLetterRoutingKey?: string;
21 | maxLength?: number;
22 | maxPriority?: number;
23 | };
24 | prefetchCount?: number;
25 | expectAck?: boolean;
26 | timeout?: number;
27 | }
28 |
29 | export enum AmqpConnectionStatus {
30 | CONNECTED = 'CONNECTED',
31 | CHANNEL_CONNECTED = 'CHANNEL_CONNECTED',
32 | CONNECTION_LOST = 'CONNECTION_LOST',
33 | CHANNEL_CONNECTION_LOST = 'CHANNEL_CONNECTION_LOST',
34 | }
35 |
36 | export enum AmqpErrorType {
37 | CANNOT_SET_ACK_FOR_NON_CONSUMER_CONNECTION = 'AmqpCannotSetExpectAckForNonConsumerConnection',
38 | }
39 |
40 | export class AmqpCannotSetExpectAckForNonConsumerConnection extends NamedError {
41 | constructor() {
42 | super(
43 | AmqpErrorType.CANNOT_SET_ACK_FOR_NON_CONSUMER_CONNECTION,
44 | `Non consumer connections cannot set "expectAck" attribute`,
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/strategies/local.strategy.interface.ts:
--------------------------------------------------------------------------------
1 | import { Transport } from '../transport.interface';
2 |
3 | export const EVENT_BUS_CHANNEL = 'event_bus';
4 |
5 | export interface LocalStrategy {
6 | transport: Transport.LOCAL;
7 | options: LocalStrategyOptions;
8 | }
9 |
10 | export interface LocalStrategyOptions {
11 | timeout?: number;
12 | }
13 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/strategies/redis.strategy.interface.ts:
--------------------------------------------------------------------------------
1 | import { Transport } from '../transport.interface';
2 |
3 | export interface RedisStrategy {
4 | transport: Transport.REDIS;
5 | options: RedisStrategyOptions;
6 | }
7 |
8 | export interface RedisStrategyOptions {
9 | host: string;
10 | channel: string;
11 | port?: number;
12 | password?: string;
13 | timeout?: number;
14 | }
15 |
16 |
17 | export enum RedisConnectionStatus {
18 | READY = 'READY',
19 | CONNECT = 'CONNECT',
20 | RECONNECTING = 'RECONNECTING',
21 | END = 'END',
22 | }
23 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/strategies/tcp.strategy.interface.ts:
--------------------------------------------------------------------------------
1 | import { Transport } from '../transport.interface';
2 |
3 | export interface TcpStrategy {
4 | transport: Transport.TCP;
5 | options: TcpStrategyOptions;
6 | }
7 |
8 | export interface TcpStrategyOptions {
9 | timeout?: number;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/strategies/tcp.strategy.ts:
--------------------------------------------------------------------------------
1 |
2 | import { TransportLayer, Transport } from '../transport.interface';
3 |
4 | /* istanbul ignore next */
5 | export const createTcpStrategy = (): TransportLayer => {
6 | // @TODO
7 | return {} as any;
8 | };
9 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/transport.error.ts:
--------------------------------------------------------------------------------
1 | import { coreErrorFactory, CoreErrorOptions } from '@marblejs/core';
2 | import { NamedError } from '@marblejs/core/dist/+internal/utils';
3 |
4 | export enum ErrorType {
5 | UNSUPPORTED_ERROR = 'UnsupportedError',
6 | }
7 |
8 | export class UnsupportedError extends NamedError {
9 | constructor(public readonly message: string) {
10 | super(ErrorType.UNSUPPORTED_ERROR, message);
11 | }
12 | }
13 |
14 | export const throwUnsupportedError = (transportName: string) => (method: string) => {
15 | const message = `Unsupported operation.`;
16 | const detail = `Method "${method}" is unsupported for ${transportName} transport layer.`;
17 | const error = new UnsupportedError(`${message} ${detail}`);
18 | const coreErrorOptions: CoreErrorOptions = { contextMethod: method, offset: 1 };
19 | const coreError = coreErrorFactory(error.message, coreErrorOptions);
20 |
21 | console.error(coreError.stack);
22 |
23 | throw error;
24 | };
25 |
--------------------------------------------------------------------------------
/packages/messaging/src/transport/transport.transformer.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 | import { flow, identity, pipe } from 'fp-ts/lib/function';
3 | import * as E from 'fp-ts/lib/Either';
4 | import { Event } from '@marblejs/core';
5 | import { TransportMessageTransformer, TransportMessage } from './transport.interface';
6 |
7 | export type DecodeMessageConfig = { msgTransformer: TransportMessageTransformer; errorSubject: Subject };
8 | export type DecodeMessage = (config: DecodeMessageConfig) => (msg: TransportMessage) => Event;
9 |
10 | export const jsonTransformer: TransportMessageTransformer = {
11 | decode: event => JSON.parse(event.toString()),
12 | encode: event => flow(JSON.stringify, Buffer.from)(event),
13 | };
14 |
15 | const applyMetadata = (raw: TransportMessage) => (event: Event) => ({
16 | ...event,
17 | metadata: {
18 | replyTo: raw.replyTo,
19 | correlationId: raw.correlationId,
20 | raw,
21 | },
22 | });
23 |
24 | export const decodeMessage: DecodeMessage = ({ msgTransformer, errorSubject }) => msg =>
25 | pipe(
26 | E.tryCatch(
27 | () => msgTransformer.decode(msg.data),
28 | error => {
29 | errorSubject.next(error as Error);
30 | return applyMetadata(msg)({ type: 'UNKNOWN' });
31 | }),
32 | E.map(applyMetadata(msg)),
33 | E.fold(identity, identity),
34 | );
35 |
--------------------------------------------------------------------------------
/packages/messaging/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/middleware-body/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/middleware-body
8 |
9 | A request body parser middleware for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/middleware-body
15 | ```
16 | Requires `@marblejs/core` and `@marblejs/http` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
22 | ## Usage
23 |
24 | ```typescript
25 | import { bodyParser$ } from '@marblejs/middleware-body';
26 |
27 | const middlewares = [
28 | bodyParser$(),
29 | // ...
30 | ];
31 |
32 | const effects = [
33 | // ...
34 | ];
35 |
36 | export const app = httpListener({ middlewares, effects });
37 | ```
38 | License: MIT
39 |
--------------------------------------------------------------------------------
/packages/middleware-body/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/middleware-body/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/middleware-body",
3 | "version": "4.1.0",
4 | "description": "Body parser middleware for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "test": "jest --config ./jest.config.js",
12 | "clean": "rimraf dist"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "marblejs",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "@marblejs/http": "^4.0.0",
35 | "fp-ts": "^2.13.1",
36 | "rxjs": "^7.5.7"
37 | },
38 | "dependencies": {
39 | "@types/content-type": "1.1.2",
40 | "@types/qs": "^6.5.1",
41 | "@types/type-is": "~1.6.2",
42 | "content-type": "~1.0.4",
43 | "qs": "^6.6.0",
44 | "type-is": "~1.6.16"
45 | },
46 | "devDependencies": {
47 | "@marblejs/core": "^4.1.0",
48 | "@marblejs/http": "^4.1.0"
49 | },
50 | "publishConfig": {
51 | "access": "public"
52 | },
53 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
54 | }
55 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/body.middleware.ts:
--------------------------------------------------------------------------------
1 | import { HttpError, HttpStatus, HttpMiddlewareEffect } from '@marblejs/http';
2 | import { of, throwError } from 'rxjs';
3 | import { catchError, map, tap, mergeMap } from 'rxjs/operators';
4 | import { pipe } from 'fp-ts/lib/function';
5 | import { defaultParser } from './parsers';
6 | import { RequestBodyParser } from './body.model';
7 | import { matchType, getBody, hasBody, isMultipart } from './body.util';
8 |
9 | const PARSEABLE_METHODS = ['POST', 'PUT', 'PATCH'];
10 |
11 | interface BodyParserOptions {
12 | parser?: RequestBodyParser;
13 | type?: string[];
14 | }
15 |
16 | export const bodyParser$ = ({
17 | type = ['*/*'],
18 | parser = defaultParser,
19 | }: BodyParserOptions = {}): HttpMiddlewareEffect => req$ =>
20 | req$.pipe(
21 | mergeMap(req =>
22 | PARSEABLE_METHODS.includes(req.method)
23 | && !hasBody(req)
24 | && !isMultipart(req)
25 | && matchType(type)(req)
26 | ? pipe(
27 | getBody(req),
28 | map(parser(req)),
29 | tap(body => req.body = body),
30 | map(() => req),
31 | catchError(error => throwError(() =>
32 | new HttpError(`Request body parse error: "${error.toString()}"`, HttpStatus.BAD_REQUEST, undefined, req),
33 | )))
34 | : of(req),
35 | ),
36 | );
37 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/body.model.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest } from '@marblejs/http';
2 |
3 | export type RequestBodyParser = (reqOrContentType: HttpRequest | string) =>
4 | (body: Buffer) => Buffer | Record | Array | string | undefined;
5 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/body.util.ts:
--------------------------------------------------------------------------------
1 | import * as typeIs from 'type-is';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { Observable } from 'rxjs';
4 | import { map, toArray } from 'rxjs/operators';
5 | import { HttpRequest } from '@marblejs/http';
6 | import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util';
7 | import { fromReadableStream } from '@marblejs/core/dist/+internal/observable';
8 |
9 | export const matchType = (type: string[]) => (req: HttpRequest): boolean =>
10 | !!typeIs.is(getContentTypeUnsafe(req.headers), type);
11 |
12 | export const isMultipart = (req: HttpRequest): boolean =>
13 | getContentTypeUnsafe(req.headers).includes('multipart/');
14 |
15 | export const hasBody = (req: HttpRequest): boolean =>
16 | req.body !== undefined && req.body !== null;
17 |
18 | export const getBody = (req: HttpRequest): Observable =>
19 | pipe(
20 | fromReadableStream(req),
21 | toArray(),
22 | map(chunks => Buffer.concat(chunks)),
23 | );
24 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/index.ts:
--------------------------------------------------------------------------------
1 | export { bodyParser$ } from './body.middleware';
2 | export { RequestBodyParser } from './body.model';
3 | export { defaultParser, jsonParser, urlEncodedParser, textParser, rawParser } from './parsers';
4 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/default.body.parser.ts:
--------------------------------------------------------------------------------
1 | import * as typeis from 'type-is';
2 | import { getContentTypeUnsafe } from '@marblejs/http/dist/+internal/contentType.util';
3 | import { RequestBodyParser } from '../body.model';
4 | import { jsonParser } from './json.body.parser';
5 | import { textParser } from './text.body.parser';
6 | import { rawParser } from './raw.body.parser';
7 | import { urlEncodedParser } from './url.body.parser';
8 |
9 | const SUPPORTED_CONTENT_TYPES = ['json', 'urlencoded', 'application/octet-stream', 'text', 'html'];
10 |
11 | export const defaultParser: RequestBodyParser = reqOrContentType => body => {
12 | const contentType = typeof reqOrContentType === 'string'
13 | ? reqOrContentType
14 | : getContentTypeUnsafe(reqOrContentType.headers);
15 |
16 | switch (typeis.is(contentType, SUPPORTED_CONTENT_TYPES)) {
17 | case 'json':
18 | return jsonParser(reqOrContentType)(body);
19 | case 'urlencoded':
20 | return urlEncodedParser(reqOrContentType)(body);
21 | case 'application/octet-stream':
22 | return rawParser(reqOrContentType)(body);
23 | case 'text':
24 | case 'html':
25 | return textParser(reqOrContentType)(body);
26 | default:
27 | return undefined;
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './url.body.parser';
2 | export * from './raw.body.parser';
3 | export * from './json.body.parser';
4 | export * from './text.body.parser';
5 | export * from './default.body.parser';
6 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/json.body.parser.ts:
--------------------------------------------------------------------------------
1 | import * as contentType from 'content-type';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { RequestBodyParser } from '../body.model';
4 |
5 | export const jsonParser: RequestBodyParser = reqOrContentType => body =>
6 | pipe(
7 | contentType.parse(reqOrContentType),
8 | parsedContentType => body.toString(parsedContentType.parameters.charset),
9 | stringifiedBody => JSON.parse(stringifiedBody),
10 | );
11 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/raw.body.parser.ts:
--------------------------------------------------------------------------------
1 | import { RequestBodyParser } from '../body.model';
2 |
3 | export const rawParser: RequestBodyParser = _ => body => body;
4 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/specs/default.body.parser.spec.ts:
--------------------------------------------------------------------------------
1 | import { defaultParser } from '../default.body.parser';
2 |
3 | test('#defaultParser handles Content-Type as a first argument', () => {
4 | const body = {
5 | test: 'value',
6 | };
7 | const buffer = Buffer.from(JSON.stringify(body));
8 | expect(defaultParser('application/json')(buffer)).toEqual(body);
9 | });
10 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/text.body.parser.ts:
--------------------------------------------------------------------------------
1 | import * as contentType from 'content-type';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { RequestBodyParser } from '../body.model';
4 |
5 | export const textParser: RequestBodyParser = reqOrContentType => body =>
6 | pipe(
7 | contentType.parse(reqOrContentType),
8 | parsedContentType => body.toString(parsedContentType.parameters.charset),
9 | );
10 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/parsers/url.body.parser.ts:
--------------------------------------------------------------------------------
1 | import * as qs from 'qs';
2 | import * as contentType from 'content-type';
3 | import { pipe } from 'fp-ts/lib/function';
4 | import { RequestBodyParser } from '../body.model';
5 |
6 | export const urlEncodedParser: RequestBodyParser = reqOrContentType => body =>
7 | pipe(
8 | contentType.parse(reqOrContentType),
9 | parsedContentType => body.toString(parsedContentType.parameters.charset),
10 | stringifiedBody => qs.parse(stringifiedBody),
11 | );
12 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/specs/body.util.spec.ts:
--------------------------------------------------------------------------------
1 | import { createHttpRequest } from '@marblejs/http/dist/+internal/testing.util';
2 | import { hasBody } from '../body.util';
3 |
4 | test('#hasBody checks if request has body', () => {
5 | expect(hasBody(createHttpRequest({ body: null }))).toEqual(false);
6 | expect(hasBody(createHttpRequest({ body: undefined }))).toEqual(false);
7 | expect(hasBody(createHttpRequest({ body: 'test' }))).toEqual(true);
8 | expect(hasBody(createHttpRequest({ body: {} }))).toEqual(true);
9 | expect(hasBody(createHttpRequest({ body: 1 }))).toEqual(true);
10 | });
11 |
--------------------------------------------------------------------------------
/packages/middleware-body/src/specs/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as API from '../index';
2 |
3 | describe('@marblejs/middleware-body public API', () => {
4 | test('apis are defined', () => {
5 | expect(API.bodyParser$).toBeDefined();
6 | expect(API.defaultParser).toBeDefined();
7 | expect(API.jsonParser).toBeDefined();
8 | expect(API.rawParser).toBeDefined();
9 | expect(API.textParser).toBeDefined();
10 | expect(API.urlEncodedParser).toBeDefined();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/packages/middleware-body/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" },
10 | { "path": "../http" }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/middleware-cors/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Edouard Bozon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/middleware-cors/README.md:
--------------------------------------------------------------------------------
1 | Middleware CORS
2 | =======
3 |
4 | A CORS middleware for [Marble.js](https://github.com/marblejs/marble).
5 |
6 | ## Usage
7 |
8 | Example to allow incoming requests.
9 |
10 | ```typescript
11 | import { cors$ } from '@marblejs/middleware-cors';
12 |
13 | const middlewares = [
14 | logger$,
15 | cors$({
16 | origin: '*',
17 | methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
18 | optionsSuccessStatus: 204,
19 | allowHeaders: '*',
20 | maxAge: 3600,
21 | })
22 | ];
23 |
24 | const effects = [
25 | endpoint1$,
26 | endpoint2$,
27 | ...
28 | ];
29 |
30 | const app = httpListener({ middlewares, effects });
31 | ```
32 |
33 | ## Available options
34 |
35 | To configure CORS middleware you can follow this interface.
36 |
37 | ```typescript
38 | interface CORSOptions {
39 | origin?: string | string[] | RegExp;
40 | methods?: HttpMethod[];
41 | optionsSuccessStatus?: HttpStatus;
42 | allowHeaders?: string | string[];
43 | exposeHeaders?: string[];
44 | withCredentials?: boolean;
45 | maxAge?: number;
46 | }
47 | ```
48 |
49 | License: MIT
50 |
--------------------------------------------------------------------------------
/packages/middleware-cors/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/middleware-cors/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/middleware-cors",
3 | "version": "4.1.0",
4 | "description": "A CORS middleware for Marble.js",
5 | "main": "dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "test": "jest --config ./jest.config.js",
12 | "clean": "rimraf dist"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "keywords": [
18 | "marble.js",
19 | "cors",
20 | "middleware",
21 | "http",
22 | "rxjs"
23 | ],
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/marblejs/marble.git"
27 | },
28 | "engines": {
29 | "node": ">= 8.0.0",
30 | "yarn": ">= 1.7.0",
31 | "npm": ">= 5.0.0"
32 | },
33 | "author": "Edouard Bozon ",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/marblejs/marble/issues"
37 | },
38 | "homepage": "https://github.com/marblejs/marble#readme",
39 | "peerDependencies": {
40 | "@marblejs/core": "^4.0.0",
41 | "@marblejs/http": "^4.0.0",
42 | "fp-ts": "^2.13.1",
43 | "rxjs": "^7.5.7"
44 | },
45 | "devDependencies": {
46 | "@marblejs/core": "^4.1.0",
47 | "@marblejs/http": "^4.1.0"
48 | },
49 | "publishConfig": {
50 | "access": "public"
51 | },
52 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
53 | }
54 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/applyHeaders.ts:
--------------------------------------------------------------------------------
1 | import { HttpResponse } from '@marblejs/http';
2 |
3 | export interface ConfiguredHeader {
4 | key: AccessControlHeader;
5 | value: string;
6 | }
7 |
8 | export enum AccessControlHeader {
9 | Origin = 'Access-Control-Allow-Origin',
10 | Methods = 'Access-Control-Allow-Methods',
11 | Headers = 'Access-Control-Allow-Headers',
12 | Credentials = 'Access-Control-Allow-Credentials',
13 | MaxAge = 'Access-Control-Max-Age',
14 | ExposeHeaders = 'Access-Control-Expose-Headers',
15 | }
16 |
17 | export const applyHeaders = (
18 | headers: ConfiguredHeader[],
19 | res: HttpResponse,
20 | ): void => {
21 | headers.forEach(({ key, value }) => {
22 | res.setHeader(key, value);
23 | });
24 | };
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/checkArrayOrigin.ts:
--------------------------------------------------------------------------------
1 | export const checkArrayOrigin = (
2 | origin: string,
3 | option: string | string[] | RegExp,
4 | ): boolean =>
5 | Array.isArray(option) && option.length > 0 && option.includes(origin)
6 | ? true
7 | : false;
8 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/checkOrigin.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest } from '@marblejs/http';
2 | import { checkStringOrigin } from './checkStringOrigin';
3 | import { checkArrayOrigin } from './checkArrayOrigin';
4 | import { checkRegexpOrigin } from './checkRegexpOrigin';
5 |
6 | export const checkOrigin = (
7 | req: HttpRequest,
8 | option: string | string[] | RegExp,
9 | ): boolean => {
10 | const origin = req.headers.origin as string;
11 |
12 | return [
13 | checkStringOrigin,
14 | checkArrayOrigin,
15 | checkRegexpOrigin,
16 | ].some(check => check(origin, option));
17 | };
18 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/checkRegexpOrigin.ts:
--------------------------------------------------------------------------------
1 | export const checkRegexpOrigin = (
2 | origin: string,
3 | option: string | string[] | RegExp,
4 | ): boolean => (option instanceof RegExp && option.test(origin) ? true : false);
5 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/checkStringOrigin.ts:
--------------------------------------------------------------------------------
1 | import { isString } from './util';
2 |
3 | export const checkStringOrigin = (
4 | origin: string,
5 | option: string | string[] | RegExp,
6 | ): boolean => {
7 | if (isString(option) && option === '*') {
8 | return true;
9 | } else if (
10 | isString(option) &&
11 | option !== '*' &&
12 | origin.match(option as string)
13 | ) {
14 | return true;
15 | }
16 |
17 | return false;
18 | };
19 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/configureResponse.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest, HttpResponse } from '@marblejs/http';
2 |
3 | import { AccessControlHeader, applyHeaders, ConfiguredHeader } from './applyHeaders';
4 | import { CORSOptions } from './middleware';
5 | import { capitalize } from './util';
6 |
7 | export function configureResponse(
8 | req: HttpRequest,
9 | res: HttpResponse,
10 | options: CORSOptions,
11 | ): void {
12 | const headers: ConfiguredHeader[] = [];
13 | const origin = req.headers.origin as string;
14 |
15 | headers.push({ key: AccessControlHeader.Origin, value: origin });
16 |
17 | if (options.withCredentials) {
18 | headers.push({ key: AccessControlHeader.Credentials, value: 'true' });
19 | }
20 |
21 | if (
22 | Array.isArray(options.exposeHeaders) &&
23 | options.exposeHeaders.length > 0
24 | ) {
25 | headers.push({
26 | key: AccessControlHeader.ExposeHeaders,
27 | value: options.exposeHeaders.map(header => capitalize(header)).join(', '),
28 | });
29 | }
30 |
31 | applyHeaders(headers, res);
32 | }
33 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/index.ts:
--------------------------------------------------------------------------------
1 | export { cors$ } from './middleware';
2 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { of, EMPTY, defer } from 'rxjs';
2 | import { mergeMap } from 'rxjs/operators';
3 | import { isString } from '@marblejs/core/dist/+internal/utils';
4 | import { endRequest } from '@marblejs/http/dist/response/http.responseHandler';
5 | import { HttpMethod, HttpMiddlewareEffect, HttpRequest, HttpStatus } from '@marblejs/http';
6 | import { pipe } from 'fp-ts/lib/function';
7 | import { configurePreflightResponse } from './configurePreflightResponse';
8 | import { configureResponse } from './configureResponse';
9 |
10 | export interface CORSOptions {
11 | origin?: string | string[] | RegExp;
12 | methods?: HttpMethod[];
13 | optionsSuccessStatus?: HttpStatus;
14 | allowHeaders?: string | string[];
15 | exposeHeaders?: string[];
16 | withCredentials?: boolean;
17 | maxAge?: number;
18 | }
19 |
20 | const DEFAULT_OPTIONS: CORSOptions = {
21 | origin: '*',
22 | methods: ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
23 | withCredentials: false,
24 | optionsSuccessStatus: HttpStatus.NO_CONTENT,
25 | };
26 |
27 | const isCORSRequest = (req: HttpRequest): boolean =>
28 | !isString(req.headers.origin);
29 |
30 | export const cors$ = (options: CORSOptions = {}): HttpMiddlewareEffect => req$ => {
31 | options = { ...DEFAULT_OPTIONS, ...options };
32 |
33 | return req$.pipe(
34 | mergeMap(req => {
35 | if (isCORSRequest(req))
36 | return of(req);
37 |
38 | if (req.method === 'OPTIONS') {
39 | configurePreflightResponse(req, req.response, options);
40 | return pipe(
41 | defer(endRequest(req.response)),
42 | mergeMap(() => EMPTY));
43 | }
44 |
45 | configureResponse(req, req.response, options);
46 | return of(req);
47 | }),
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/applyHeaders.spec.ts:
--------------------------------------------------------------------------------
1 | import { createHttpResponse } from '@marblejs/http/dist/+internal/testing.util';
2 | import { AccessControlHeader, applyHeaders, ConfiguredHeader } from '../applyHeaders';
3 |
4 | describe('applyHeaders', () => {
5 | test('should handle many methods correctly', done => {
6 | const configured: ConfiguredHeader[] = [
7 | { key: AccessControlHeader.Origin, value: '*' },
8 | { key: AccessControlHeader.Methods, value: 'POST' },
9 | ];
10 | const res = createHttpResponse();
11 |
12 | applyHeaders(configured, res);
13 |
14 | expect(res.setHeader).toBeCalledTimes(2);
15 | expect(res.setHeader).toBeCalledWith('Access-Control-Allow-Origin', '*');
16 | expect(res.setHeader).toBeCalledWith(
17 | 'Access-Control-Allow-Methods',
18 | 'POST',
19 | );
20 | done();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/checkArrayOrigin.spec.ts:
--------------------------------------------------------------------------------
1 | import { checkArrayOrigin } from '../checkArrayOrigin';
2 |
3 | describe('checkArrayOrigin', () => {
4 | test('check array option correctly', done => {
5 | const option1 = ['fake-origin'];
6 | const option2 = ['fake-origin-2'];
7 |
8 | expect(checkArrayOrigin('fake-origin', option2)).toBeFalsy();
9 | expect(checkArrayOrigin('fake-origin', option1)).toBeTruthy();
10 | done();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/checkOrigin.spec.ts:
--------------------------------------------------------------------------------
1 | import { checkOrigin } from '../checkOrigin';
2 | import { createMockRequest } from '../util';
3 |
4 | describe('checkOrigin', () => {
5 | test('check wildcard option correctly', done => {
6 | const option = '*';
7 | const req = createMockRequest('GET', { origin: 'fake-origin' });
8 |
9 | expect(checkOrigin(req, option)).toBeTruthy();
10 | done();
11 | });
12 |
13 | test('check string option correctly', done => {
14 | const option1 = 'fake-origin';
15 | const option2 = 'fake-origin-2';
16 | const req1 = createMockRequest('GET', { origin: 'fake-origin' });
17 | const req2 = createMockRequest('GET', { origin: 'fake-origin' });
18 |
19 | expect(checkOrigin(req1, option1)).toBeTruthy();
20 | expect(checkOrigin(req2, option2)).toBeFalsy();
21 | done();
22 | });
23 |
24 | test('check array option correctly', done => {
25 | const option1 = ['fake-origin-b', 'fake-origin-a'];
26 | const option2 = ['another-origin-a', 'another-origin-b'];
27 | const req1 = createMockRequest('GET', { origin: 'fake-origin-a' });
28 | const req2 = createMockRequest('GET', { origin: 'fake-origin-c' });
29 |
30 | expect(checkOrigin(req1, option1)).toBeTruthy();
31 | expect(checkOrigin(req2, option2)).toBeFalsy();
32 | done();
33 | });
34 |
35 | test('check regexp option correctly', done => {
36 | const option1 = /[a-z]/;
37 | const option2 = /[0-9]/;
38 | const req1 = createMockRequest('GET', { origin: 'fake-origin-a' });
39 | const req2 = createMockRequest('GET', { origin: 'fake-origin-c' });
40 |
41 | expect(checkOrigin(req1, option1)).toBeTruthy();
42 | expect(checkOrigin(req2, option2)).toBeFalsy();
43 | done();
44 | });
45 | });
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/checkRegexpOrigin.spec.ts:
--------------------------------------------------------------------------------
1 | import { checkRegexpOrigin } from '../checkRegexpOrigin';
2 |
3 | describe('checkStringOrigin', () => {
4 | test('check regexp option correctly', done => {
5 | const option1 = /[a-z]/;
6 | const option2 = /[0-9]/;
7 |
8 | expect(checkRegexpOrigin('fake-origin', option1)).toBeTruthy();
9 | expect(checkRegexpOrigin('fake-origin', option2)).toBeFalsy();
10 | done();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/checkStringOrigin.spec.ts:
--------------------------------------------------------------------------------
1 | import { checkStringOrigin } from '../checkStringOrigin';
2 |
3 | describe('checkStringOrigin', () => {
4 | test('check string option correctly', done => {
5 | const option1 = 'fake-origin';
6 | const option2 = 'fake-origin-2';
7 |
8 | expect(checkStringOrigin('fake-origin', option1)).toBeTruthy();
9 | expect(checkStringOrigin('fake-origin', option2)).toBeFalsy();
10 | done();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { cors$ } from '../index';
2 |
3 | describe('@marblejs/middleware-cors public API', () => {
4 | it('should be defined', () => {
5 | expect(cors$).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/spec/util.spec.ts:
--------------------------------------------------------------------------------
1 | import { capitalize, isString } from '../util';
2 |
3 | describe('Utils', () => {
4 | describe('Capitalize', () => {
5 | test('should capitalize a header correctly', done => {
6 | const header = 'capitalize-any-header';
7 | expect(capitalize(header)).toEqual('Capitalize-Any-Header');
8 | done();
9 | });
10 | });
11 |
12 | describe('isString', () => {
13 | test('should return true for a string', done => {
14 | const str = 'string';
15 | expect(isString(str)).toBeTruthy();
16 | done();
17 | });
18 |
19 | test('should return false for any other type', done => {
20 | const obj = {};
21 | const num = 42;
22 | const arr = [];
23 | const n = null;
24 | expect(isString(obj)).toBeFalsy();
25 | expect(isString(num)).toBeFalsy();
26 | expect(isString(arr)).toBeFalsy();
27 | expect(isString(n)).toBeFalsy();
28 | done();
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/middleware-cors/src/util.ts:
--------------------------------------------------------------------------------
1 | import * as http from 'http';
2 | import { createEffectContext, createContext, lookup } from '@marblejs/core';
3 | import { HttpMethod } from '@marblejs/http';
4 | import { createHttpRequest } from '@marblejs/http/dist/+internal/testing.util';
5 |
6 | export const capitalize = (str: string): string =>
7 | str
8 | .split('-')
9 | .map(part => part.charAt(0).toUpperCase() + part.slice(1))
10 | .join('-');
11 |
12 | export const isString = (str: any): boolean =>
13 | typeof str === 'string' || str instanceof String;
14 |
15 | export const createMockRequest = (
16 | method: HttpMethod = 'GET',
17 | headers: any = { origin: 'fake-origin' },
18 | ) => createHttpRequest({ method, headers });
19 |
20 | export const createMockEffectContext = () => {
21 | const context = createContext();
22 | const client = http.createServer();
23 | return createEffectContext({ ask: lookup(context), client });
24 | };
25 |
--------------------------------------------------------------------------------
/packages/middleware-cors/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" },
10 | { "path": "../http" }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/middleware-io/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/middleware-io
8 |
9 | IO validation middleware for Marble.js for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/middleware-io
15 | ```
16 | Requires `@marblejs/core` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
22 | ## Usage
23 |
24 | ```typescript
25 | import { r } from '@marblejs/http';
26 | import { requestValidator$, t } from '@marblejs/middleware-io';
27 |
28 | const userSchema = t.type({
29 | id: t.string,
30 | firstName: t.string,
31 | lastName: t.string,
32 | roles: t.array(t.union([
33 | t.literal('ADMIN'),
34 | t.literal('GUEST'),
35 | ])),
36 | });
37 |
38 | type User = t.TypeOf;
39 |
40 | const effect$ = r.pipe(
41 | r.matchPath('/'),
42 | r.matchType('POST'),
43 | r.useEffect(req$ => req$.pipe(
44 | requestValidator$({ body: userSchema }),
45 | // ..
46 | )));
47 | ```
48 | License: MIT
49 |
--------------------------------------------------------------------------------
/packages/middleware-io/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/middleware-io/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/middleware-io",
3 | "version": "4.1.0",
4 | "description": "IO middleware for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "clean": "rimraf dist",
12 | "test": "jest --config ./jest.config.js"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "marblejs",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "@marblejs/http": "^4.0.0",
35 | "fp-ts": "^2.13.1",
36 | "rxjs": "^7.5.7"
37 | },
38 | "dependencies": {
39 | "@types/json-schema": "^7.0.3",
40 | "io-ts": "^2.2.19"
41 | },
42 | "devDependencies": {
43 | "@marblejs/core": "^4.1.0",
44 | "@marblejs/http": "^4.1.0"
45 | },
46 | "publishConfig": {
47 | "access": "public"
48 | },
49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
50 | }
51 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as t from 'io-ts';
2 |
3 | // package public API
4 | export { Schema, ValidatorOptions, validator$ } from './io.middleware';
5 | export { requestValidator$ } from './io.request.middleware';
6 | export { eventValidator$ } from './io.event.middleware';
7 | export { defaultReporter } from './io.reporter';
8 | export { ioTypeToJsonSchema, withJsonSchema } from './io.json-schema';
9 | export { t };
10 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/io.error.ts:
--------------------------------------------------------------------------------
1 | import { NamedError } from '@marblejs/core/dist/+internal/utils';
2 |
3 | export enum ErrorType {
4 | IO_ERROR = 'IOError',
5 | }
6 |
7 | export class IOError extends NamedError {
8 | constructor(
9 | public readonly message: string,
10 | public readonly data: Record | Array,
11 | public readonly context?: string,
12 | ) {
13 | super(ErrorType.IO_ERROR, message);
14 | }
15 | }
16 |
17 | export const isIOError = (error: Error): error is IOError =>
18 | error.name === ErrorType.IO_ERROR;
19 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/io.event.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Event, EventError, ValidatedEvent, isEventCodec } from '@marblejs/core';
2 | import { Observable, of, throwError, isObservable } from 'rxjs';
3 | import { pipe } from 'fp-ts/lib/function';
4 | import { mergeMap, catchError, map } from 'rxjs/operators';
5 | import { Schema, ValidatorOptions, validator$ } from './io.middleware';
6 | import { IOError } from './io.error';
7 |
8 | type ValidationResult = U['_A'] extends { type: string; payload: any }
9 | ? ValidatedEvent
10 | : ValidatedEvent
11 |
12 | export const eventValidator$ = (schema: U, options?: ValidatorOptions) => {
13 | const eventValidator$ = validator$(schema, options);
14 |
15 | const validateByEventSchema = (incomingEvent: Event) =>
16 | pipe(
17 | of(incomingEvent),
18 | eventValidator$,
19 | map(decodedEvent => ({ ...incomingEvent, ...decodedEvent }) as ValidatedEvent),
20 | );
21 |
22 | const validateByPayloadSchema = (incomingEvent: Event) =>
23 | pipe(
24 | of(incomingEvent.payload),
25 | eventValidator$,
26 | map(payload => ({ ...incomingEvent, payload }) as ValidatedEvent),
27 | );
28 |
29 | const validate = (event: Event) =>
30 | pipe(
31 | isEventCodec(schema)
32 | ? validateByEventSchema(event)
33 | : validateByPayloadSchema(event),
34 | catchError((error: IOError) => throwError(() =>
35 | new EventError(event, error.message, error.data),
36 | )),
37 | ) as Observable>;
38 |
39 | return (input: Observable | Event) =>
40 | isObservable(input)
41 | ? input.pipe(mergeMap(validate))
42 | : validate(input);
43 | };
44 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/io.middleware.ts:
--------------------------------------------------------------------------------
1 | import * as t from 'io-ts';
2 | import { Reporter } from 'io-ts/lib/Reporter';
3 | import { identity } from 'fp-ts/lib/function';
4 | import * as E from 'fp-ts/lib/Either';
5 | import { Observable } from 'rxjs';
6 | import { map} from 'rxjs/operators';
7 | import { throwException } from '@marblejs/core/dist/+internal/utils';
8 | import { defaultReporter } from './io.reporter';
9 | import { IOError } from './io.error';
10 |
11 | export type Schema = t.Any;
12 |
13 | export interface ValidatorOptions {
14 | reporter?: Reporter;
15 | context?: string;
16 | }
17 |
18 | const validateError = (reporter: Reporter = defaultReporter, context?: string) => (result: E.Either) =>
19 | E.fold(
20 | () => throwException(new IOError('Validation error', reporter.report(result), context)),
21 | identity,
22 | )(result);
23 |
24 | export function validator$(schema: U, options?: ValidatorOptions): (i$: Observable) => Observable>;
25 | export function validator$(schema: undefined, options?: ValidatorOptions): (i$: Observable) => Observable;
26 | export function validator$(schema: U | undefined, options: ValidatorOptions = {}) {
27 | return (i$: Observable) =>
28 | !schema ? i$ : i$.pipe(
29 | map(input => schema.decode(input)),
30 | map(validateError(options.reporter, options.context)),
31 | );
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/io.reporter.ts:
--------------------------------------------------------------------------------
1 | import * as t from 'io-ts';
2 | import * as O from 'fp-ts/lib/Option';
3 | import * as E from 'fp-ts/lib/Either';
4 | import { pipe } from 'fp-ts/lib/function';
5 | import { Reporter } from 'io-ts/lib/Reporter';
6 | import { stringify, getLast } from '@marblejs/core/dist/+internal/utils';
7 |
8 | export interface ReporterResult {
9 | path: string;
10 | expected: string;
11 | got: any;
12 | }
13 |
14 | const getPath = (context: t.Context) =>
15 | context
16 | .map(c => c.key)
17 | .filter(Boolean)
18 | .join('.');
19 |
20 | const getExpectedType = (context: t.ContextEntry[]) => pipe(
21 | getLast(context),
22 | O.map(c => c.type.name),
23 | O.getOrElse(() => 'any'),
24 | );
25 |
26 | const getErrorMessage = (value: any, context: t.Context): ReporterResult => ({
27 | path: getPath(context),
28 | expected: getExpectedType(context as t.ContextEntry[]),
29 | got: stringify(value),
30 | });
31 |
32 | const failure = (errors: t.ValidationError[]): ReporterResult[] =>
33 | errors.map(error => getErrorMessage(error.value, error.context));
34 |
35 | const success = () => [];
36 |
37 | export const defaultReporter: Reporter = {
38 | report: E.fold(failure, success),
39 | };
40 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/specs/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as API from '../index';
2 |
3 | describe('@marblejs/middleware-io public API', () => {
4 | test('apis are defined', () => {
5 | expect(API.defaultReporter).toBeDefined();
6 | expect(API.eventValidator$).toBeDefined();
7 | expect(API.requestValidator$).toBeDefined();
8 | expect(API.validator$).toBeDefined();
9 | expect(API.t).toBeDefined();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/middleware-io/src/specs/io.error.spec.ts:
--------------------------------------------------------------------------------
1 | import { IOError, isIOError } from '../io.error';
2 |
3 | test('#isIOError checks if error is of type IOError', () => {
4 | const ioError = new IOError('test', {});
5 | const otherError = new Error();
6 |
7 | expect(isIOError(ioError)).toBe(true);
8 | expect(isIOError(otherError)).toBe(false);
9 | });
10 |
--------------------------------------------------------------------------------
/packages/middleware-io/test/io-http.integration.spec.ts:
--------------------------------------------------------------------------------
1 | import { createHttpTestBed, createTestBedSetup } from '@marblejs/testing';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { listener } from './io-http.integration';
4 |
5 | const testBed = createHttpTestBed({ listener });
6 | const useTestBedSetup = createTestBedSetup({ testBed });
7 |
8 | describe('@marblejs/middleware-io - HTTP integration', () => {
9 | const testBedSetup = useTestBedSetup();
10 |
11 | afterEach(async () => {
12 | await testBedSetup.cleanup();
13 | });
14 |
15 | test('POST / returns 200 with user object', async () => {
16 | const { request } = await testBedSetup.useTestBed();
17 | const user = { id: 'id', name: 'name', age: 100 };
18 |
19 | const response = await pipe(
20 | request('POST'),
21 | request.withPath('/'),
22 | request.withBody({ user }),
23 | request.send,
24 | );
25 |
26 | expect(response.statusCode).toEqual(200);
27 | expect(response.body).toEqual(user);
28 | });
29 |
30 | test('POST / returns 400 with validation error object', async () => {
31 | const { request } = await testBedSetup.useTestBed();
32 | const user = { id: 'id', name: 'name', age: '100' };
33 |
34 | const response = await pipe(
35 | request('POST'),
36 | request.withPath('/'),
37 | request.withBody({ user }),
38 | request.send,
39 | );
40 |
41 | expect(response.statusCode).toEqual(400);
42 | expect(response.body).toEqual({
43 | error: {
44 | status: 400,
45 | message: 'Validation error',
46 | context: 'body',
47 | data: [{
48 | path: 'user.age',
49 | expected: 'number',
50 | got: '"100"',
51 | }]
52 | }
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/packages/middleware-io/test/io-http.integration.ts:
--------------------------------------------------------------------------------
1 | import { r, httpListener } from '@marblejs/http';
2 | import { bodyParser$ } from '@marblejs/middleware-body';
3 | import { map } from 'rxjs/operators';
4 | import { requestValidator$, t } from '../src';
5 |
6 | const user = t.type({
7 | id: t.string,
8 | name: t.string,
9 | age: t.number,
10 | });
11 |
12 | const validateRequest = requestValidator$({
13 | body: t.type({ user })
14 | });
15 |
16 | const effect$ = r.pipe(
17 | r.matchPath('/'),
18 | r.matchType('POST'),
19 | r.useEffect(req$ => req$.pipe(
20 | validateRequest,
21 | map(req => ({ body: req.body.user })),
22 | )));
23 |
24 | export const listener = httpListener({
25 | middlewares: [bodyParser$()],
26 | effects: [effect$],
27 | });
28 |
--------------------------------------------------------------------------------
/packages/middleware-io/test/io-ws.integration.ts:
--------------------------------------------------------------------------------
1 | import { act, matchEvent } from '@marblejs/core';
2 | import { webSocketListener, WsEffect } from '@marblejs/websockets';
3 | import { eventValidator$, t } from '../src';
4 |
5 | const user = t.type({
6 | id: t.string,
7 | age: t.number,
8 | });
9 |
10 | const postUser$: WsEffect = event$ =>
11 | event$.pipe(
12 | matchEvent('POST_USER'),
13 | act(eventValidator$(user)),
14 | );
15 |
16 | export const listener = webSocketListener({
17 | effects: [postUser$],
18 | });
19 |
--------------------------------------------------------------------------------
/packages/middleware-io/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/middleware-logger/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/middleware-logger
8 |
9 | A logger middleware for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/middleware-logger
15 | ```
16 | Requires `@marblejs/core` and `@marblejs/http` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
22 | ## Usage
23 |
24 | ```typescript
25 | import { logger$ } from '@marblejs/middleware-logger';
26 |
27 | const middlewares = [
28 | logger$(),
29 | ...
30 | ];
31 |
32 | const effects = [
33 | ...
34 | ];
35 |
36 | export const app = httpListener({ middlewares, effects });
37 | ```
38 | License: MIT
39 |
--------------------------------------------------------------------------------
/packages/middleware-logger/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/middleware-logger/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/middleware-logger",
3 | "version": "4.1.0",
4 | "description": "Logger middleware for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "clean": "rimraf dist",
12 | "test": "jest --config ./jest.config.js"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "marblejs",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "@marblejs/http": "^4.0.0",
35 | "fp-ts": "^2.13.1",
36 | "rxjs": "^7.5.7"
37 | },
38 | "dependencies": {
39 | "chalk": "^2.4.1"
40 | },
41 | "devDependencies": {
42 | "@marblejs/core": "^4.1.0",
43 | "@marblejs/http": "^4.1.0",
44 | "@types/chalk": "^2.2.0"
45 | },
46 | "publishConfig": {
47 | "access": "public"
48 | },
49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
50 | }
51 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/index.ts:
--------------------------------------------------------------------------------
1 | export { logger$ } from './logger.middleware';
2 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/logger.factory.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp } from 'rxjs';
2 | import { HttpRequest } from '@marblejs/http';
3 | import { factorizeTime } from './logger.util';
4 |
5 | export const factorizeLog = (stamp: Timestamp) => (req: HttpRequest) => {
6 | const { method, url } = stamp.value;
7 | const statusCode = String(req.response.statusCode);
8 | const time = factorizeTime(stamp.timestamp);
9 |
10 | return `${method} ${url} ${statusCode} ${time}`;
11 | };
12 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/logger.handler.ts:
--------------------------------------------------------------------------------
1 | import { Logger, LoggerTag, LoggerLevel } from '@marblejs/core';
2 | import { HttpRequest } from '@marblejs/http';
3 | import { fromEvent, Timestamp, Observable } from 'rxjs';
4 | import { take, filter, map, tap } from 'rxjs/operators';
5 | import { factorizeLog } from './logger.factory';
6 | import { LoggerOptions } from './logger.model';
7 | import { isNotSilent, filterResponse } from './logger.util';
8 |
9 | export const loggerHandler = (opts: LoggerOptions, logger: Logger) => (stamp: Timestamp): Observable => {
10 | const req = stamp.value;
11 | const res = req.response;
12 |
13 | return fromEvent(res, 'finish').pipe(
14 | take(1),
15 | map(() => req),
16 | filter(isNotSilent(opts)),
17 | filter(filterResponse(opts)),
18 | map(factorizeLog(stamp)),
19 | tap(message => {
20 | const level = res.statusCode >= 500
21 | ? LoggerLevel.ERROR
22 | : res.statusCode >= 400
23 | ? LoggerLevel.WARN
24 | : LoggerLevel.INFO;
25 |
26 | const log = logger({ tag: LoggerTag.HTTP, type: 'RequestLogger', message, level });
27 | return log();
28 | }),
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/logger.middleware.ts:
--------------------------------------------------------------------------------
1 | import { useContext, LoggerToken } from '@marblejs/core';
2 | import { HttpMiddlewareEffect } from '@marblejs/http';
3 | import { timestamp, tap, map } from 'rxjs/operators';
4 | import { LoggerOptions } from './logger.model';
5 | import { loggerHandler } from './logger.handler';
6 |
7 | export const logger$ = (opts: LoggerOptions = {}): HttpMiddlewareEffect => (req$, ctx) => {
8 | const logger = useContext(LoggerToken)(ctx.ask);
9 |
10 | return req$.pipe(
11 | timestamp(),
12 | tap(stamp => loggerHandler(opts, logger)(stamp).subscribe()),
13 | map(({ value: req }) => req),
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/logger.model.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest } from '@marblejs/http';
2 |
3 | export type WritableLike = {
4 | write: (chunk: any) => void;
5 | }
6 |
7 | export interface LoggerOptions {
8 | silent?: boolean;
9 | filter?: (req: HttpRequest) => boolean;
10 | }
11 |
12 | export interface LogParams {
13 | method: string;
14 | url: string;
15 | statusCode: string;
16 | time: string;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/logger.util.ts:
--------------------------------------------------------------------------------
1 | import * as O from 'fp-ts/lib/Option';
2 | import { flow, pipe } from 'fp-ts/lib/function';
3 | import { HttpRequest } from '@marblejs/http';
4 | import { LoggerOptions } from './logger.model';
5 |
6 | export const getDateFromTimestamp = (t: number) => new Date(t);
7 |
8 | export const isNotSilent = (opts: LoggerOptions) => (_: HttpRequest) =>
9 | !opts.silent;
10 |
11 | export const filterResponse = (opts: LoggerOptions) => (req: HttpRequest) => pipe(
12 | O.fromNullable(opts.filter),
13 | O.map(filter => filter(req)),
14 | O.getOrElse(() => true),
15 | );
16 |
17 | export const formatTime = (timeInMms: number) =>
18 | timeInMms > 1000
19 | ? `+${timeInMms / 1000}s`
20 | : `+${timeInMms}ms`;
21 |
22 | export const getTimeDifferenceInMs = (startDate: Date): number =>
23 | new Date().getTime() - startDate.getTime();
24 |
25 | export const factorizeTime = flow(
26 | getDateFromTimestamp,
27 | getTimeDifferenceInMs,
28 | formatTime,
29 | );
30 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/spec/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as index from '../index';
2 |
3 | test('index exposes public API', () => {
4 | expect(index.logger$).toBeDefined();
5 | });
6 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/spec/logger.factory.spec.ts:
--------------------------------------------------------------------------------
1 | import { createHttpResponse, createHttpRequest } from '@marblejs/http/dist/+internal/testing.util';
2 | import { factorizeLog } from '../logger.factory';
3 |
4 | describe('Logger factory', () => {
5 | let loggerUtilModule;
6 |
7 | beforeEach(() => {
8 | jest.unmock('../logger.util.ts');
9 | loggerUtilModule = require('../logger.util.ts');
10 | });
11 |
12 | test('#factorizeLog factorizes logger message', () => {
13 | // given
14 | const response = createHttpResponse({ statusCode: 200 });
15 | const req = createHttpRequest({ method: 'GET', url: '/api/v1', response });
16 | const stamp = { value: req, timestamp: 1539031930521 };
17 |
18 | // when
19 | loggerUtilModule.factorizeTime = jest.fn(() => '+300ms');
20 | const log = factorizeLog(stamp)(req);
21 |
22 | // then
23 | expect(log).toEqual('GET /api/v1 200 +300ms');
24 | });
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/packages/middleware-logger/src/spec/logger.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import { firstValueFrom, of } from 'rxjs';
2 | import { HttpServer } from '@marblejs/http';
3 | import { createHttpRequest } from '@marblejs/http/dist/+internal/testing.util';
4 | import { createEffectContext, lookup, register, LoggerToken, bindTo, createContext, Logger, EffectContext } from '@marblejs/core';
5 | import { logger$ } from '../logger.middleware';
6 |
7 | describe('logger$', () => {
8 | let logger: Logger;
9 | let ctx: EffectContext;
10 |
11 | beforeEach(() => {
12 | const client = jest.fn() as any as HttpServer;
13 | logger = jest.fn(() => jest.fn());
14 |
15 | const boundLogger = bindTo(LoggerToken)(() => logger);
16 | const context = register(boundLogger)(createContext());
17 |
18 | ctx = createEffectContext({ ask: lookup(context), client });
19 | });
20 |
21 | test('reacts to 200 status', async () => {
22 | // given
23 | const req = createHttpRequest({ url: '/', method: 'GET' });
24 | const req$ = of(req);
25 | req.response.statusCode = 200;
26 |
27 | // when
28 | await firstValueFrom(logger$()(req$, ctx));
29 | req.response.emit('finish');
30 |
31 | // then
32 | expect(logger).toHaveBeenCalled();
33 | });
34 |
35 | test('reacts to 400 status', async () => {
36 | // given
37 | const req = createHttpRequest({ url: '/test', method: 'POST' });
38 | const req$ = of(req);
39 | req.response.statusCode = 403;
40 |
41 | // when
42 | await firstValueFrom(logger$()(req$, ctx));
43 | req.response.emit('finish');
44 |
45 | // then
46 | expect(logger).toHaveBeenCalled();
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/packages/middleware-logger/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/middleware-multipart",
3 | "version": "4.1.0",
4 | "description": "A multipart/form-data middleware for Marble.js",
5 | "main": "dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "test": "jest --config ./jest.config.js",
12 | "clean": "rimraf dist"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "keywords": [
18 | "marble.js",
19 | "multipart",
20 | "middleware",
21 | "http",
22 | "rxjs"
23 | ],
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/marblejs/marble.git"
27 | },
28 | "engines": {
29 | "node": ">= 8.0.0",
30 | "yarn": ">= 1.7.0",
31 | "npm": ">= 5.0.0"
32 | },
33 | "author": "marblejs",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/marblejs/marble/issues"
37 | },
38 | "homepage": "https://github.com/marblejs/marble#readme",
39 | "dependencies": {
40 | "@fastify/busboy": "^1.0.0"
41 | },
42 | "peerDependencies": {
43 | "@marblejs/core": "^4.0.0",
44 | "@marblejs/http": "^4.0.0",
45 | "fp-ts": "^2.13.1",
46 | "rxjs": "^7.5.7"
47 | },
48 | "devDependencies": {
49 | "@marblejs/core": "^4.1.0",
50 | "@marblejs/http": "^4.1.0",
51 | "@marblejs/testing": "^4.1.0",
52 | "@types/form-data": "^2.5.0",
53 | "form-data": "^3.0.0"
54 | },
55 | "publishConfig": {
56 | "access": "public"
57 | },
58 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
59 | }
60 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './multipart.middleware';
2 | export * from './multipart.interface';
3 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/src/multipart.interface.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 |
3 | export interface File {
4 | destination?: any;
5 | buffer?: Buffer;
6 | size?: number;
7 | encoding: string;
8 | mimetype: string;
9 | filename?: string;
10 | fieldname: string;
11 | }
12 |
13 | export interface FileIncomingData {
14 | file: NodeJS.ReadableStream;
15 | filename: string;
16 | fieldname: string;
17 | encoding: string;
18 | mimetype: string;
19 | }
20 |
21 | export interface WithFile {
22 | files: Record;
23 | }
24 |
25 | interface StreamHandlerOutput {
26 | destination: any;
27 | size?: number;
28 | }
29 |
30 | export interface StreamHandler {
31 | (opts: FileIncomingData): Promise | Observable;
32 | }
33 |
34 | export interface ParserOpts {
35 | files?: string[];
36 | stream?: StreamHandler;
37 | maxFileSize?: number;
38 | maxFileCount?: number;
39 | maxFieldSize?: number;
40 | maxFieldCount?: number;
41 | }
42 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/src/multipart.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Observable, throwError } from 'rxjs';
2 | import { mergeMap, map } from 'rxjs/operators';
3 | import { HttpRequest, HttpError, HttpStatus } from '@marblejs/http';
4 | import { ContentType } from '@marblejs/http/dist/+internal/contentType.util';
5 | import { parseMultipart } from './multipart.parser';
6 | import { shouldParseMultipart } from './multipart.util';
7 | import { WithFile, ParserOpts } from './multipart.interface';
8 |
9 | export const multipart$ = (opts: ParserOpts = {}) => (req$: Observable) =>
10 | req$.pipe(
11 | mergeMap(req => shouldParseMultipart(req)
12 | ? parseMultipart(opts)(req)
13 | : throwError(() =>
14 | new HttpError(`Content-Type must be of type ${ContentType.MULTIPART_FORM_DATA}`, HttpStatus.PRECONDITION_FAILED, undefined, req)
15 | )),
16 | map(req => req as T & WithFile),
17 | );
18 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/src/multipart.parser.field.ts:
--------------------------------------------------------------------------------
1 | import { HttpRequest } from '@marblejs/http';
2 | import { Observable } from 'rxjs';
3 | import { takeUntil, tap, defaultIfEmpty, map } from 'rxjs/operators';
4 |
5 | export type FieldEvent = [string, any, boolean, boolean, string, string];
6 |
7 | export const parseField = (req: HttpRequest) => (event$: Observable, finish$: Observable) =>
8 | event$.pipe(
9 | takeUntil(finish$),
10 | tap(([ fieldname, value ]) => {
11 | req.body = {
12 | ...req.body ?? {},
13 | [fieldname]: value,
14 | };
15 | }),
16 | map(() => req),
17 | defaultIfEmpty(req),
18 | );
19 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/src/specs/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as API from '../index';
2 |
3 | describe('@marblejs/middleware-multipart public API', () => {
4 | test('apis are defined', () => {
5 | expect(API.multipart$).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/middleware-multipart/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/testing/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/testing
8 |
9 | A testing module for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/testing
15 | ```
16 | Requires `@marblejs/core`, `@marblejs/http`, `@marblejs/messaging`, `@marblejs/websockets`, `fp-ts` and `rxjs` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
--------------------------------------------------------------------------------
/packages/testing/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/testing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/testing",
3 | "version": "4.1.0",
4 | "description": "Testing module for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "clean": "rimraf dist",
12 | "test": "jest --config ./jest.config.js"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "Józef Flakus ",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "@marblejs/http": "^4.0.0",
35 | "@marblejs/messaging": "^4.0.0",
36 | "@marblejs/websockets": "^4.0.0",
37 | "fp-ts": "^2.13.1",
38 | "rxjs": "^7.5.7"
39 | },
40 | "devDependencies": {
41 | "@marblejs/core": "^4.1.0",
42 | "@marblejs/http": "^4.1.0",
43 | "@marblejs/messaging": "^4.1.0",
44 | "@marblejs/websockets": "^4.1.0"
45 | },
46 | "publishConfig": {
47 | "access": "public"
48 | },
49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
50 | }
51 |
--------------------------------------------------------------------------------
/packages/testing/src/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as API from './index';
2 |
3 | describe('@marblejs/testing', () => {
4 | test('public APIs are defined', () => {
5 | expect(API.TestBedType).toBeDefined();
6 | expect(API.createTestBedSetup).toBeDefined();
7 | expect(API.createHttpTestBed).toBeDefined();
8 | expect(API.createTestBedContainer).toBeDefined();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/packages/testing/src/index.ts:
--------------------------------------------------------------------------------
1 | // TestBed setup
2 | export * from './testBed/testBedSetup';
3 | export * from './testBed/testBedSetup.interface';
4 |
5 | // TestBed container
6 | export * from './testBed/testBedContainer';
7 | export * from './testBed/testBedContainer.interface';
8 |
9 | // TestBed
10 | export * from './testBed/testBed.interface';
11 |
12 | // TestBed HTTP
13 | export * from './testBed/http/http.testBed';
14 | export * from './testBed/http/http.testBed.interface';
15 | export * from './testBed/http/http.testBed.request.interface';
16 | export * from './testBed/http/http.testBed.response.interface';
17 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/http/http.testBed.interface.ts:
--------------------------------------------------------------------------------
1 | import { Reader } from 'fp-ts/lib/Reader';
2 | import { Context } from '@marblejs/core';
3 | import { HttpHeaders, HttpListener, HttpMethod } from '@marblejs/http';
4 | import { TestBed, TestBedType } from '../testBed.interface';
5 | import { HttpTestBedRequest, HttpTestBedRequestBuilder } from './http.testBed.request.interface';
6 | import { HttpTestBedResponse } from './http.testBed.response.interface';
7 |
8 | export interface HttpTestBedConfig {
9 | listener: Reader;
10 | defaultHeaders?: HttpHeaders;
11 | }
12 |
13 | export interface HttpTestBed extends TestBed {
14 | type: TestBedType.HTTP;
15 | send: (req: HttpTestBedRequest) => Promise;
16 | request: HttpTestBedRequestBuilder;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/http/http.testBed.request.interface.ts:
--------------------------------------------------------------------------------
1 | import { HttpMethod, HttpHeaders } from '@marblejs/http';
2 | import { withHeaders, withBody, withPath } from './http.testBed.request';
3 | import { HttpTestBedResponse } from './http.testBed.response.interface';
4 |
5 | export interface HttpTestBedRequest extends Readonly<{
6 | host: string;
7 | port: number;
8 | protocol: string;
9 | headers: HttpHeaders;
10 | method: T;
11 | path: string;
12 | body?: any;
13 | }> {}
14 |
15 | export interface WithBodyApplied {
16 | readonly body: T;
17 | }
18 |
19 | export interface HttpTestBedRequestBuilder {
20 | (method: T): HttpTestBedRequest;
21 | withHeaders: typeof withHeaders;
22 | withBody: typeof withBody;
23 | withPath: typeof withPath;
24 | send: (req: HttpTestBedRequest) => Promise;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/http/http.testBed.request.ts:
--------------------------------------------------------------------------------
1 | import * as O from 'fp-ts/lib/Option';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { HttpMethod, HttpHeaders } from '@marblejs/http';
4 | import { createRequestMetadataHeader } from '@marblejs/http/dist/+internal/metadata.util';
5 | import { ContentType, getMimeType, getContentType } from '@marblejs/http/dist/+internal/contentType.util';
6 | import { HttpTestBedRequest, WithBodyApplied } from './http.testBed.request.interface';
7 |
8 | export const createRequest = (port: number, host: string, headers?: HttpHeaders) =>
9 | (method: T): HttpTestBedRequest => ({
10 | protocol: 'http:',
11 | path: '/',
12 | headers: {
13 | ...createRequestMetadataHeader(),
14 | ...headers,
15 | },
16 | method,
17 | host,
18 | port,
19 | });
20 |
21 | export const withPath = (path: string) => (req: HttpTestBedRequest): HttpTestBedRequest => ({
22 | ...req,
23 | path,
24 | });
25 |
26 | export const withHeaders = (headers: HttpHeaders) => (req: HttpTestBedRequest): HttpTestBedRequest => ({
27 | ...req,
28 | headers: { ...req.headers, ...headers },
29 | });
30 |
31 | export const withBody = (body: T) => (req: HttpTestBedRequest): HttpTestBedRequest & WithBodyApplied =>
32 | pipe(
33 | getContentType(req.headers),
34 | O.map(() => ({ ...req, body })),
35 | O.getOrElse(() => pipe(
36 | getMimeType(body, req.path),
37 | type => withHeaders({ 'Content-Type': type ?? ContentType.APPLICATION_JSON })(req),
38 | req => ({ ...req, body }),
39 | ))
40 | );
41 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/http/http.testBed.response.interface.ts:
--------------------------------------------------------------------------------
1 | import { HttpHeaders, HttpRequestMetadata, HttpStatus } from '@marblejs/http';
2 | import { HttpTestBedRequest } from './http.testBed.request.interface';
3 |
4 | export interface HttpTestBedResponseProps {
5 | statusCode?: number;
6 | statusMessage?: string;
7 | headers: HttpHeaders;
8 | body?: Buffer;
9 | }
10 |
11 | export interface HttpTestBedResponse extends HttpTestBedResponseProps {
12 | req: HttpTestBedRequest;
13 | metadata: HttpRequestMetadata;
14 | statusCode: HttpStatus;
15 | body?: any;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/http/http.testBed.response.ts:
--------------------------------------------------------------------------------
1 | import * as O from 'fp-ts/lib/Option';
2 | import { pipe } from 'fp-ts/lib/function';
3 | import { HttpStatus } from '@marblejs/http';
4 | import { parseJson } from '@marblejs/core/dist/+internal/utils';
5 | import { getContentTypeUnsafe, ContentType } from '@marblejs/http/dist/+internal/contentType.util';
6 | import { HttpTestBedResponseProps, HttpTestBedResponse } from './http.testBed.response.interface';
7 | import { HttpTestBedRequest } from './http.testBed.request.interface';
8 |
9 | const parseResponseBody = (props: HttpTestBedResponseProps): string | Array | Record | undefined =>
10 | pipe(
11 | O.fromNullable(props.body),
12 | O.map(body => {
13 | switch (getContentTypeUnsafe(props.headers)) {
14 | case ContentType.APPLICATION_JSON:
15 | return pipe(
16 | body.toString(),
17 | O.fromPredicate(Boolean),
18 | O.map(parseJson),
19 | O.toUndefined);
20 | case ContentType.TEXT_PLAIN:
21 | case ContentType.TEXT_HTML:
22 | return body.toString();
23 | default:
24 | return body;
25 | }
26 | }),
27 | O.toUndefined);
28 |
29 | export const createResponse = (req: HttpTestBedRequest) => (props: HttpTestBedResponseProps): HttpTestBedResponse =>
30 | pipe(
31 | parseResponseBody(props),
32 | body => ({
33 | statusCode: props.statusCode ?? HttpStatus.OK,
34 | statusMessage: props.statusMessage,
35 | headers: props.headers,
36 | metadata: {},
37 | req,
38 | body,
39 | }),
40 | );
41 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/testBed.interface.ts:
--------------------------------------------------------------------------------
1 | import { Task } from 'fp-ts/lib/Task';
2 | import { ContextProvider, BoundDependency } from '@marblejs/core';
3 |
4 | export enum TestBedType {
5 | HTTP,
6 | MESSAGING,
7 | WEBSOCKETS,
8 | }
9 |
10 | export interface TestBed {
11 | type: TestBedType;
12 | ask: ContextProvider;
13 | finish: Task;
14 | }
15 |
16 | export type TestBedFactory = (dependencies?: BoundDependency[]) => Promise
17 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/testBedContainer.interface.ts:
--------------------------------------------------------------------------------
1 | import { Task } from 'fp-ts/lib/Task';
2 | import { ContextToken } from '@marblejs/core';
3 | import { TestBed } from './testBed.interface';
4 |
5 | export type DependencyCleanup = {
6 | token: ContextToken;
7 | cleanup: (dependency: T) => Promise;
8 | }
9 |
10 | export interface TestBedContainerConfig {
11 | cleanups?: readonly DependencyCleanup[];
12 | }
13 |
14 | export interface TestBedContainer {
15 | cleanup: Task;
16 | register: (instance: T) => Task;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/testBedSetup.interface.ts:
--------------------------------------------------------------------------------
1 | import { Task } from 'fp-ts/lib/Task';
2 | import { BoundDependency } from '@marblejs/core';
3 | import { DependencyCleanup } from './testBedContainer.interface';
4 | import { TestBed, TestBedFactory } from './testBed.interface';
5 |
6 | export interface TestBedSetupConfig {
7 | testBed: TestBedFactory;
8 | dependencies?: readonly BoundDependency[];
9 | cleanups?: readonly DependencyCleanup[];
10 | }
11 |
12 | export interface TestBedSetup {
13 | useTestBed: TestBedFactory;
14 | cleanup: Task;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/testing/src/testBed/testBedSetup.ts:
--------------------------------------------------------------------------------
1 | import { BoundDependency } from '@marblejs/core';
2 | import * as T from 'fp-ts/lib/Task';
3 | import { pipe } from 'fp-ts/lib/function';
4 | import { TestBedSetupConfig, TestBedSetup } from './testBedSetup.interface';
5 | import { createTestBedContainer } from './testBedContainer';
6 | import { TestBed } from './testBed.interface';
7 |
8 | type CreateTestBedSetup =
9 | (config: TestBedSetupConfig) =>
10 | (prependDependencies?: readonly BoundDependency[]) =>
11 | TestBedSetup
12 |
13 | export const createTestBedSetup: CreateTestBedSetup = config => prependDependencies => {
14 | const { dependencies: defaultDependencies, cleanups = [] } = config;
15 |
16 | const { cleanup, register } = createTestBedContainer({ cleanups });
17 |
18 | const useTestBed = async (dependencies: BoundDependency[] = []) => pipe(
19 | () => config.testBed([
20 | ...defaultDependencies ?? [],
21 | ...prependDependencies ?? [],
22 | ...dependencies
23 | ]),
24 | T.chain(register),
25 | )();
26 |
27 | return {
28 | useTestBed,
29 | cleanup,
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/packages/testing/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/",
5 | "rootDir": "./src"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "references": [
9 | { "path": "../core" },
10 | { "path": "../http" },
11 | { "path": "../messaging" },
12 | { "path": "../websockets" }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/websockets/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # @marblejs/websockets
8 |
9 | A WebSockets module for [Marble.js](https://github.com/marblejs/marble).
10 |
11 | ## Installation
12 |
13 | ```
14 | $ npm i @marblejs/websockets
15 | ```
16 | Requires `@marblejs/core`, `rxjs` and `fp-ts` to be installed.
17 |
18 | ## Documentation
19 |
20 | For the latest updates, documentation, change log, and release information visit [docs.marblejs.com](https://docs.marblejs.com) and follow [@marble_js](https://twitter.com/marble_js) on Twitter.
21 |
22 | License: MIT
23 |
--------------------------------------------------------------------------------
/packages/websockets/jest.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../../jest.config');
2 | module.exports = config;
3 |
--------------------------------------------------------------------------------
/packages/websockets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@marblejs/websockets",
3 | "version": "4.1.0",
4 | "description": "Websockets module for Marble.js",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "start": "yarn watch",
9 | "watch": "tsc -w",
10 | "build": "tsc",
11 | "clean": "rimraf dist",
12 | "test": "jest --config ./jest.config.js"
13 | },
14 | "files": [
15 | "dist/"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/marblejs/marble.git"
20 | },
21 | "engines": {
22 | "node": ">= 8.0.0",
23 | "yarn": ">= 1.7.0",
24 | "npm": ">= 5.0.0"
25 | },
26 | "author": "Józef Flakus ",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/marblejs/marble/issues"
30 | },
31 | "homepage": "https://github.com/marblejs/marble#readme",
32 | "peerDependencies": {
33 | "@marblejs/core": "^4.0.0",
34 | "fp-ts": "^2.13.1",
35 | "rxjs": "^7.5.7"
36 | },
37 | "dependencies": {
38 | "@types/ws": "~6.0.1",
39 | "path-to-regexp": "^6.1.0",
40 | "ws": "~7.4.6"
41 | },
42 | "devDependencies": {
43 | "@marblejs/core": "^4.1.0",
44 | "@marblejs/http": "^4.1.0"
45 | },
46 | "publishConfig": {
47 | "access": "public"
48 | },
49 | "gitHead": "208643c17d76a01d0a9cb304611d4474c4a3bff6"
50 | }
51 |
--------------------------------------------------------------------------------
/packages/websockets/src/+internal/index.ts:
--------------------------------------------------------------------------------
1 | export * from './websocket.test.util';
2 |
--------------------------------------------------------------------------------
/packages/websockets/src/effects/websocket.effects.interface.ts:
--------------------------------------------------------------------------------
1 | import * as http from 'http';
2 | import { Event, Effect } from '@marblejs/core';
3 | import { WebSocketClientConnection } from '../server/websocket.server.interface';
4 |
5 | export interface WsMiddlewareEffect<
6 | I = Event,
7 | O = Event,
8 | > extends WsEffect {}
9 |
10 | export interface WsErrorEffect<
11 | Err extends Error = Error,
12 | > extends WsEffect {}
13 |
14 | export interface WsConnectionEffect<
15 | T extends http.IncomingMessage = http.IncomingMessage
16 | > extends WsEffect {}
17 |
18 | export interface WsServerEffect
19 | extends WsEffect {}
20 |
21 | export interface WsOutputEffect<
22 | T extends Event = Event
23 | > extends WsEffect {}
24 |
25 | export interface WsEffect<
26 | T = Event,
27 | U = Event,
28 | V = WebSocketClientConnection,
29 | > extends Effect