├── .editorconfig
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .travis.yml
├── LICENSE
├── README.md
├── dist
├── cacheControl.d.ts
├── cacheControl.js
├── cacheControl.js.map
├── errors.d.ts
├── errors.js
├── errors.js.map
├── formatResults.d.ts
├── formatResults.js
├── formatResults.js.map
├── getMeta.d.ts
├── getMeta.js
├── getMeta.js.map
├── getPath.d.ts
├── getPath.js
├── getPath.js.map
├── getRequestHandler.d.ts
├── getRequestHandler.js
├── getRequestHandler.js.map
├── getSwagger.d.ts
├── getSwagger.js
├── getSwagger.js.map
├── index.d.ts
├── index.js
├── index.js.map
├── methods.d.ts
├── methods.js
├── methods.js.map
├── rewriteLarge.d.ts
├── rewriteLarge.js
├── rewriteLarge.js.map
├── types.d.ts
├── types.js
├── types.js.map
├── walkResources.d.ts
├── walkResources.js
└── walkResources.js.map
├── package.json
├── src
├── cacheControl.ts
├── errors.ts
├── formatResults.ts
├── getMeta.ts
├── getPath.ts
├── getRequestHandler.ts
├── getSwagger.ts
├── global.d.ts
├── index.ts
├── methods.ts
├── rewriteLarge.ts
├── types.ts
└── walkResources.ts
├── test
└── index.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | [*]
8 | # Indentation style
9 | # Possible values - tab, space
10 | indent_style = space
11 |
12 | # Indentation size in single-spaced characters
13 | # Possible values - an integer, tab
14 | indent_size = 2
15 |
16 | # Line ending file format
17 | # Possible values - lf, crlf, cr
18 | end_of_line = lf
19 |
20 | # File character encoding
21 | # Possible values - latin1, utf-8, utf-16be, utf-16le
22 | charset = utf-8
23 |
24 | # Denotes whether to trim whitespace at the end of lines
25 | # Possible values - true, false
26 | trim_trailing_whitespace = true
27 |
28 | # Denotes whether file should end with a newline
29 | # Possible values - true, false
30 | insert_final_newline = true
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | build
3 | lib-cov
4 | *.seed
5 | *.log
6 | *.dat
7 | *.out
8 | *.pid
9 | *.gz
10 | _book
11 |
12 | pids
13 | logs
14 | results
15 |
16 | yarn.lock
17 | npm-shrinkwrap.json
18 | npm-debug.log
19 | node_modules
20 | *.sublime*
21 | *.node
22 | coverage
23 | *.orig
24 | .idea
25 | sandbox
26 | docs
27 | package-lock.json
28 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | docs
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '14'
4 | - '15'
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Contra
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Build next-generation realtime APIs simply and easily
4 |
5 |
6 | ## Install
7 |
8 | One command and you're ready to make some killer APIs:
9 |
10 | ```
11 | npm install sutro --save
12 | ```
13 |
14 | **Now**, check out the [documentation](https://github.com/contra/sutro/tree/master/docs) to get started!
15 |
16 | ## Examples
17 |
18 | ### 10-LOC ES7 API
19 |
20 | ```js
21 | const api = {
22 | user: {
23 | create: async ({ data }) => User.create(data),
24 | find: async ({ options }) => User.findAll(options),
25 | findById: async ({ userId }) => User.findById(userId),
26 | updateById: async ({ userId, data }) => User.updateById(userId, data),
27 | replaceById: async ({ userId, data }) => User.replaceById(userId, data),
28 | deleteById: async ({ userId }) => User.deleteById(userId)
29 | }
30 | }
31 | ```
32 |
33 | Yields:
34 |
35 | ```
36 | GET /swagger.json
37 | GET /users
38 | POST /users
39 | GET /users/:userId
40 | PATCH /users/:userId
41 | PUT /users/:userId
42 | DELETE /users/:userId
43 | ```
44 |
--------------------------------------------------------------------------------
/dist/cacheControl.d.ts:
--------------------------------------------------------------------------------
1 | import { CacheOptions } from './types';
2 | declare const _default: (opt: string | CacheOptions) => string;
3 | export default _default;
4 |
--------------------------------------------------------------------------------
/dist/cacheControl.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const parse_duration_1 = __importDefault(require("parse-duration"));
7 | const parseNumber = (v) => {
8 | const n = (typeof v === 'number' ? v : parse_duration_1.default(v));
9 | if (isNaN(n))
10 | throw new Error(`Invalid number: ${v}`);
11 | return n / 1000;
12 | };
13 | exports.default = (opt) => {
14 | if (typeof opt === 'string')
15 | return opt; // already formatted
16 | const stack = [];
17 | if (opt.private)
18 | stack.push('private');
19 | if (opt.public)
20 | stack.push('public');
21 | if (opt.noStore)
22 | stack.push('no-store');
23 | if (opt.noCache)
24 | stack.push('no-cache');
25 | if (opt.noTransform)
26 | stack.push('no-transform');
27 | if (opt.proxyRevalidate)
28 | stack.push('proxy-revalidate');
29 | if (opt.mustRevalidate)
30 | stack.push('proxy-revalidate');
31 | if (opt.staleIfError)
32 | stack.push(`stale-if-error=${parseNumber(opt.staleIfError)}`);
33 | if (opt.staleWhileRevalidate)
34 | stack.push(`stale-while-revalidate=${parseNumber(opt.staleWhileRevalidate)}`);
35 | if (opt.maxAge)
36 | stack.push(`max-age=${parseNumber(opt.maxAge)}`);
37 | if (opt.sMaxAge)
38 | stack.push(`s-maxage=${parseNumber(opt.sMaxAge)}`);
39 | return stack.join(', ');
40 | };
41 | //# sourceMappingURL=cacheControl.js.map
--------------------------------------------------------------------------------
/dist/cacheControl.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"cacheControl.js","sourceRoot":"","sources":["../src/cacheControl.ts"],"names":[],"mappings":";;;;;AAAA,oEAA0C;AAG1C,MAAM,WAAW,GAAG,CAAC,CAAkB,EAAU,EAAE;IACjD,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAa,CAAC,CAAC,CAAC,CAAW,CAAA;IAClE,IAAI,KAAK,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAA;IACrD,OAAO,CAAC,GAAG,IAAI,CAAA;AACjB,CAAC,CAAA;AAED,kBAAe,CAAC,GAA0B,EAAE,EAAE;IAC5C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAA,CAAC,oBAAoB;IAC5D,MAAM,KAAK,GAAG,EAAE,CAAA;IAEhB,IAAI,GAAG,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACtC,IAAI,GAAG,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACpC,IAAI,GAAG,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACvC,IAAI,GAAG,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACvC,IAAI,GAAG,CAAC,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAC/C,IAAI,GAAG,CAAC,eAAe;QAAE,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;IACvD,IAAI,GAAG,CAAC,cAAc;QAAE,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;IACtD,IAAI,GAAG,CAAC,YAAY;QAClB,KAAK,CAAC,IAAI,CAAC,kBAAkB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAC/D,IAAI,GAAG,CAAC,oBAAoB;QAC1B,KAAK,CAAC,IAAI,CACR,0BAA0B,WAAW,CAAC,GAAG,CAAC,oBAAoB,CAAC,EAAE,CAClE,CAAA;IACH,IAAI,GAAG,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAChE,IAAI,GAAG,CAAC,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACnE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC,CAAA"}
--------------------------------------------------------------------------------
/dist/errors.d.ts:
--------------------------------------------------------------------------------
1 | export declare const codes: {
2 | badRequest: number;
3 | unauthorized: number;
4 | forbidden: number;
5 | notFound: number;
6 | serverError: number;
7 | };
8 | export declare class UnauthorizedError extends Error {
9 | message: string;
10 | status: number;
11 | constructor(message?: string, status?: number);
12 | toString: () => string;
13 | }
14 | export declare class BadRequestError extends Error {
15 | message: string;
16 | status: number;
17 | constructor(message?: string, status?: number);
18 | toString(): string;
19 | }
20 | export declare class ValidationError extends BadRequestError {
21 | fields?: any[];
22 | constructor(fields?: any[]);
23 | toString(): string;
24 | }
25 | export declare class NotFoundError extends Error {
26 | message: string;
27 | status: number;
28 | constructor(message?: string, status?: number);
29 | toString: () => string;
30 | }
31 |
--------------------------------------------------------------------------------
/dist/errors.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.NotFoundError = exports.ValidationError = exports.BadRequestError = exports.UnauthorizedError = exports.codes = void 0;
4 | const util_1 = require("util");
5 | const inspectOptions = {
6 | depth: 100,
7 | breakLength: Infinity
8 | };
9 | const serializeIssues = (fields) => fields.map((f) => `\n - ${util_1.inspect(f, inspectOptions)}`);
10 | exports.codes = {
11 | badRequest: 400,
12 | unauthorized: 401,
13 | forbidden: 403,
14 | notFound: 404,
15 | serverError: 500
16 | };
17 | class UnauthorizedError extends Error {
18 | constructor(message = 'Unauthorized', status = exports.codes.unauthorized) {
19 | super(message);
20 | this.toString = () => `${super.toString()} (HTTP ${this.status})`;
21 | this.message = message;
22 | this.status = status;
23 | Error.captureStackTrace(this, UnauthorizedError);
24 | }
25 | }
26 | exports.UnauthorizedError = UnauthorizedError;
27 | class BadRequestError extends Error {
28 | constructor(message = 'Bad Request', status = exports.codes.badRequest) {
29 | super(message);
30 | this.message = message;
31 | this.status = status;
32 | Error.captureStackTrace(this, BadRequestError);
33 | }
34 | toString() {
35 | return `${super.toString()} (HTTP ${this.status})`;
36 | }
37 | }
38 | exports.BadRequestError = BadRequestError;
39 | class ValidationError extends BadRequestError {
40 | constructor(fields) {
41 | super();
42 | this.fields = fields;
43 | Error.captureStackTrace(this, ValidationError);
44 | }
45 | toString() {
46 | const original = super.toString();
47 | if (!this.fields)
48 | return original; // no custom validation
49 | if (Array.isArray(this.fields)) {
50 | return `${original}\nIssues:${serializeIssues(this.fields)}`;
51 | }
52 | return this.fields;
53 | }
54 | }
55 | exports.ValidationError = ValidationError;
56 | class NotFoundError extends Error {
57 | constructor(message = 'Not Found', status = exports.codes.notFound) {
58 | super(message);
59 | this.toString = () => `${super.toString()} (HTTP ${this.status})`;
60 | this.message = message;
61 | this.status = status;
62 | Error.captureStackTrace(this, NotFoundError);
63 | }
64 | }
65 | exports.NotFoundError = NotFoundError;
66 | //# sourceMappingURL=errors.js.map
--------------------------------------------------------------------------------
/dist/errors.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAAA,+BAA8B;AAE9B,MAAM,cAAc,GAAG;IACrB,KAAK,EAAE,GAAG;IACV,WAAW,EAAE,QAAQ;CACtB,CAAA;AAED,MAAM,eAAe,GAAG,CAAC,MAAa,EAAE,EAAE,CACxC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,cAAO,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,CAAC,CAAA;AAE5C,QAAA,KAAK,GAAG;IACnB,UAAU,EAAE,GAAG;IACf,YAAY,EAAE,GAAG;IACjB,SAAS,EAAE,GAAG;IACd,QAAQ,EAAE,GAAG;IACb,WAAW,EAAE,GAAG;CACjB,CAAA;AAED,MAAa,iBAAkB,SAAQ,KAAK;IAI1C,YAAY,OAAO,GAAG,cAAc,EAAE,MAAM,GAAG,aAAK,CAAC,YAAY;QAC/D,KAAK,CAAC,OAAO,CAAC,CAAA;QAKhB,aAAQ,GAAG,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,EAAE,UAAU,IAAI,CAAC,MAAM,GAAG,CAAA;QAJ1D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;IAClD,CAAC;CAEF;AAXD,8CAWC;AAED,MAAa,eAAgB,SAAQ,KAAK;IAIxC,YAAY,OAAO,GAAG,aAAa,EAAE,MAAM,GAAG,aAAK,CAAC,UAAU;QAC5D,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,eAAe,CAAC,CAAA;IAChD,CAAC;IACD,QAAQ;QACN,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,UAAU,IAAI,CAAC,MAAM,GAAG,CAAA;IACpD,CAAC;CACF;AAbD,0CAaC;AAED,MAAa,eAAgB,SAAQ,eAAe;IAGlD,YAAY,MAAc;QACxB,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,eAAe,CAAC,CAAA;IAChD,CAAC;IACD,QAAQ;QACN,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAA;QACjC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,QAAQ,CAAA,CAAC,uBAAuB;QACzD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;YAC9B,OAAO,GAAG,QAAQ,YAAY,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAA;SAC7D;QACD,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;CACF;AAhBD,0CAgBC;AAED,MAAa,aAAc,SAAQ,KAAK;IAItC,YAAY,OAAO,GAAG,WAAW,EAAE,MAAM,GAAG,aAAK,CAAC,QAAQ;QACxD,KAAK,CAAC,OAAO,CAAC,CAAA;QAKhB,aAAQ,GAAG,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,EAAE,UAAU,IAAI,CAAC,MAAM,GAAG,CAAA;QAJ1D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAA;IAC9C,CAAC;CAEF;AAXD,sCAWC"}
--------------------------------------------------------------------------------
/dist/formatResults.d.ts:
--------------------------------------------------------------------------------
1 | export declare const format: (inp?: any, meta?: object) => {
2 | results: any[];
3 | meta: {
4 | results: number;
5 | total: number;
6 | };
7 | };
8 | export declare const stream: {
9 | (counter?: Promise, meta?: object): any;
10 | contentType: string;
11 | };
12 |
--------------------------------------------------------------------------------
/dist/formatResults.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | exports.stream = exports.format = void 0;
7 | const jsonstream_next_1 = __importDefault(require("jsonstream-next"));
8 | const isTypeORM = (inp) => Array.isArray(inp) &&
9 | inp.length === 2 &&
10 | Array.isArray(inp[0]) &&
11 | typeof inp[1] === 'number';
12 | const isSequelize = (inp) => inp.rows && typeof inp.count !== 'undefined';
13 | const format = (inp = [], meta) => {
14 | let rows;
15 | let count;
16 | if (isSequelize(inp)) {
17 | rows = inp.rows;
18 | count = inp.count;
19 | }
20 | else if (isTypeORM(inp)) {
21 | rows = inp[0];
22 | count = inp[1];
23 | }
24 | else if (Array.isArray(inp)) {
25 | rows = inp;
26 | }
27 | else {
28 | throw new Error('Invalid response! Could not format.');
29 | }
30 | return {
31 | results: rows,
32 | meta: {
33 | results: rows.length,
34 | total: typeof count === 'undefined'
35 | ? rows.length
36 | : Math.max(rows.length, count),
37 | ...meta
38 | }
39 | };
40 | };
41 | exports.format = format;
42 | const stream = (counter, meta) => {
43 | let results = 0;
44 | const tail = jsonstream_next_1.default.stringify('{"results":[', ',', (cb) => {
45 | const fin = (res, total) => {
46 | const outMeta = {
47 | results: res,
48 | total,
49 | ...meta
50 | };
51 | cb(null, `],"meta":${JSON.stringify(outMeta)}}`);
52 | };
53 | if (!counter)
54 | return fin(results, results);
55 | counter
56 | .then((total) => {
57 | const totalConstrained = Math.max(results, total); // count should never be below results
58 | fin(results, totalConstrained);
59 | })
60 | .catch((err) => cb(err));
61 | });
62 | const origWrite = tail.write;
63 | tail.write = (...a) => {
64 | ++results;
65 | return origWrite.call(tail, ...a);
66 | };
67 | return tail;
68 | };
69 | exports.stream = stream;
70 | exports.stream.contentType = 'application/json';
71 | //# sourceMappingURL=formatResults.js.map
--------------------------------------------------------------------------------
/dist/formatResults.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"formatResults.js","sourceRoot":"","sources":["../src/formatResults.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/getMeta.d.ts:
--------------------------------------------------------------------------------
1 | import { Resources, Meta } from './types';
2 | declare const _default: ({ base, resources }: {
3 | base?: string;
4 | resources: Resources;
5 | }) => Meta;
6 | export default _default;
7 |
--------------------------------------------------------------------------------
/dist/getMeta.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const url_join_1 = __importDefault(require("url-join"));
7 | const dot_prop_1 = __importDefault(require("dot-prop"));
8 | const walkResources_1 = __importDefault(require("./walkResources"));
9 | exports.default = ({ base, resources }) => {
10 | const paths = {};
11 | walkResources_1.default(resources, ({ hierarchy, path, method, instance, endpoint }) => {
12 | if (endpoint?.hidden)
13 | return; // skip
14 | const descriptor = {
15 | path: base ? url_join_1.default(base, path) : path,
16 | method,
17 | instance
18 | };
19 | dot_prop_1.default.set(paths, hierarchy, descriptor);
20 | });
21 | return paths;
22 | };
23 | //# sourceMappingURL=getMeta.js.map
--------------------------------------------------------------------------------
/dist/getMeta.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"getMeta.js","sourceRoot":"","sources":["../src/getMeta.ts"],"names":[],"mappings":";;;;;AAAA,wDAA2B;AAC3B,wDAAyB;AACzB,oEAA2C;AAG3C,kBAAe,CAAC,EACd,IAAI,EACJ,SAAS,EAIV,EAAQ,EAAE;IACT,MAAM,KAAK,GAAG,EAAE,CAAA;IAChB,uBAAa,CACX,SAAS,EACT,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE;QAClD,IAAI,QAAQ,EAAE,MAAM;YAAE,OAAM,CAAC,OAAO;QACpC,MAAM,UAAU,GAAG;YACjB,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,kBAAI,CAAC,IAAI,EAAE,IAAc,CAAC,CAAC,CAAC,CAAC,IAAI;YAC9C,MAAM;YACN,QAAQ;SACT,CAAA;QACD,kBAAE,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,CAAC,CAAA;IACtC,CAAC,CACF,CAAA;IACD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA"}
--------------------------------------------------------------------------------
/dist/getPath.d.ts:
--------------------------------------------------------------------------------
1 | import { getPathArgs } from './types';
2 | declare const _default: ({ resource, endpoint, instance }: getPathArgs) => string;
3 | export default _default;
4 |
--------------------------------------------------------------------------------
/dist/getPath.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const pluralize_1 = __importDefault(require("pluralize"));
7 | const methods_1 = __importDefault(require("./methods"));
8 | exports.default = ({ resource, endpoint, instance }) => {
9 | let path = '';
10 | if (resource)
11 | path += `/${pluralize_1.default(resource)}`;
12 | if (resource && instance)
13 | path += `/:${resource}Id`;
14 | if (endpoint && !methods_1.default[endpoint])
15 | path += `/${endpoint}`;
16 | return path;
17 | };
18 | //# sourceMappingURL=getPath.js.map
--------------------------------------------------------------------------------
/dist/getPath.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"getPath.js","sourceRoot":"","sources":["../src/getPath.ts"],"names":[],"mappings":";;;;;AAAA,0DAA8B;AAC9B,wDAA+B;AAG/B,kBAAe,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAe,EAAE,EAAE;IAC/D,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,IAAI,QAAQ;QAAE,IAAI,IAAI,IAAI,mBAAM,CAAC,QAAQ,CAAC,EAAE,CAAA;IAC5C,IAAI,QAAQ,IAAI,QAAQ;QAAE,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAA;IACnD,IAAI,QAAQ,IAAI,CAAC,iBAAO,CAAC,QAAsB,CAAC;QAAE,IAAI,IAAI,IAAI,QAAQ,EAAE,CAAA;IACxE,OAAO,IAAI,CAAA;AACb,CAAC,CAAA"}
--------------------------------------------------------------------------------
/dist/getRequestHandler.d.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Response } from 'express';
2 | import { Trace, ExpressRequest, ResourceRoot, SutroArgs } from './types';
3 | declare type Args = {
4 | trace?: Trace;
5 | formatResults: SutroArgs['formatResults'];
6 | augmentContext?: SutroArgs['augmentContext'];
7 | };
8 | declare const _default: (resource: ResourceRoot, { trace, augmentContext, formatResults }: Args) => (req: ExpressRequest, res: Response, next: NextFunction) => Promise;
9 | export default _default;
10 |
--------------------------------------------------------------------------------
/dist/getRequestHandler.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const handle_async_1 = require("handle-async");
7 | const stream_1 = require("stream");
8 | const errors_1 = require("./errors");
9 | const cacheControl_1 = __importDefault(require("./cacheControl"));
10 | const defaultCacheHeaders = {
11 | private: true,
12 | noCache: true
13 | };
14 | const responded = (req, res) => res.headersSent || req.timedout;
15 | const traceAsync = async (trace, name, promise) => {
16 | if (!trace)
17 | return promise; // no tracing, just return
18 | const ourTrace = trace.start(name);
19 | try {
20 | const res = await promise;
21 | ourTrace.end();
22 | return res;
23 | }
24 | catch (err) {
25 | ourTrace.end();
26 | throw err;
27 | }
28 | };
29 | const streamResponse = async (stream, req, res, codes, cacheStream) => {
30 | let hasFirstChunk = false;
31 | return new Promise((resolve, reject) => {
32 | let finished = false;
33 | const ourStream = stream_1.pipeline(stream, new stream_1.Transform({
34 | transform(chunk, _, cb) {
35 | // wait until we get a chunk without an error before writing the headers
36 | if (hasFirstChunk)
37 | return cb(null, chunk);
38 | hasFirstChunk = true;
39 | if (stream.contentType)
40 | res.type(stream.contentType);
41 | res.status(codes.success);
42 | cb(null, chunk);
43 | }
44 | }), (err) => {
45 | finished = true;
46 | if (!err || req.timedout)
47 | return resolve(undefined); // timed out, no point throwing a duplicate error
48 | reject(err);
49 | });
50 | // make sure we don't keep working if the response closed!
51 | res.once('close', () => {
52 | if (finished)
53 | return; // no need to blow up
54 | ourStream.destroy(new Error('Socket closed before response finished'));
55 | });
56 | if (cacheStream) {
57 | ourStream.pipe(cacheStream);
58 | ourStream.pause();
59 | }
60 | // just use a regular pipe to res, since pipeline would close it on error
61 | // which would make us unable to send an error back out
62 | ourStream.pipe(res);
63 | });
64 | };
65 | const sendBufferResponse = (resultData, _req, res, codes) => {
66 | res.status(codes.success);
67 | res.type('json');
68 | if (Buffer.isBuffer(resultData)) {
69 | res.send(resultData);
70 | }
71 | else if (typeof resultData === 'string') {
72 | res.send(Buffer.from(resultData));
73 | }
74 | else {
75 | res.json(resultData);
76 | }
77 | res.end();
78 | };
79 | const sendResponse = async (opt, successCode, resultData, writeCache) => {
80 | const { _res, _req, method, noResponse } = opt;
81 | const codes = {
82 | noResponse: successCode || 204,
83 | success: successCode || 200
84 | };
85 | // no response
86 | if (resultData == null) {
87 | if (method === 'GET')
88 | throw new errors_1.NotFoundError();
89 | return _res.status(codes.noResponse).end();
90 | }
91 | // user asked for no body (save bandwidth)
92 | if (noResponse) {
93 | return _res.status(codes.noResponse).end();
94 | }
95 | // stream response
96 | if (resultData.pipe && resultData.on) {
97 | const cacheStream = await writeCache(resultData);
98 | await streamResponse(resultData, _req, _res, codes, cacheStream);
99 | return;
100 | }
101 | // json obj response
102 | sendBufferResponse(resultData, _req, _res, codes);
103 | await writeCache(resultData);
104 | };
105 | const exec = async (req, res, resource, { trace, augmentContext, formatResults }) => {
106 | if (responded(req, res))
107 | return;
108 | const { endpoint, successCode } = resource;
109 | let opt = {
110 | ...req.params,
111 | ip: req.ip,
112 | url: req.url,
113 | protocol: req.protocol,
114 | method: req.method,
115 | subdomains: req.subdomains,
116 | path: req.path,
117 | headers: req.headers,
118 | cookies: req.cookies,
119 | user: req.user,
120 | data: req.body,
121 | options: req.query,
122 | session: req.session,
123 | noResponse: req.query.response === 'false',
124 | onFinish: (fn) => {
125 | res.once('finish', fn.bind(null, req, res));
126 | },
127 | withRaw: (fn) => {
128 | fn(req, res);
129 | },
130 | _req: req,
131 | _res: res
132 | };
133 | if (augmentContext) {
134 | opt = await traceAsync(trace, 'sutro/augmentContext', handle_async_1.promisify(augmentContext.bind(null, opt, req, resource)));
135 | }
136 | if (responded(req, res))
137 | return;
138 | // check isAuthorized
139 | const authorized = !endpoint?.isAuthorized ||
140 | (await traceAsync(trace, 'sutro/isAuthorized', handle_async_1.promisify(endpoint.isAuthorized.bind(null, opt))));
141 | if (authorized !== true)
142 | throw new errors_1.UnauthorizedError();
143 | if (responded(req, res))
144 | return;
145 | let resultData;
146 | // check cache
147 | const cacheKey = endpoint?.cache &&
148 | endpoint.cache.key &&
149 | (await traceAsync(trace, 'sutro/cache.key', handle_async_1.promisify(endpoint.cache.key.bind(null, opt))));
150 | if (responded(req, res))
151 | return;
152 | const cachedData = endpoint?.cache &&
153 | endpoint.cache.get &&
154 | (await traceAsync(trace, 'sutro/cache.get', handle_async_1.promisify(endpoint.cache.get.bind(null, opt, cacheKey))));
155 | if (responded(req, res))
156 | return;
157 | // call execute
158 | if (!cachedData) {
159 | const executeFn = typeof endpoint === 'function' ? endpoint : endpoint?.execute;
160 | const rawData = typeof executeFn === 'function'
161 | ? await traceAsync(trace, 'sutro/execute', handle_async_1.promisify(executeFn.bind(null, opt)))
162 | : executeFn || null;
163 | if (responded(req, res))
164 | return;
165 | // call format on execute result
166 | resultData = endpoint?.format
167 | ? await traceAsync(trace, 'sutro/format', handle_async_1.promisify(endpoint.format.bind(null, opt, rawData)))
168 | : rawData;
169 | if (responded(req, res))
170 | return;
171 | // call serialize on final result
172 | resultData = formatResults
173 | ? await traceAsync(trace, 'sutro/formatResults', handle_async_1.promisify(formatResults.bind(null, opt, req, endpoint, rawData)))
174 | : resultData;
175 | if (responded(req, res))
176 | return;
177 | }
178 | else {
179 | resultData = cachedData;
180 | }
181 | // call cacheControl
182 | const cacheHeaders = endpoint?.cache && endpoint.cache.header
183 | ? typeof endpoint.cache.header === 'function'
184 | ? await traceAsync(trace, 'sutro/cache.header', handle_async_1.promisify(endpoint.cache.header.bind(null, opt, resultData)))
185 | : endpoint.cache.header
186 | : defaultCacheHeaders;
187 | if (responded(req, res))
188 | return;
189 | if (cacheHeaders)
190 | res.set('Cache-Control', cacheControl_1.default(cacheHeaders));
191 | const writeCache = async (data) => {
192 | if (cachedData || !endpoint?.cache?.set)
193 | return;
194 | return traceAsync(trace, 'sutro/cache.set', handle_async_1.promisify(endpoint.cache.set.bind(null, opt, data, cacheKey)));
195 | };
196 | await sendResponse(opt, successCode, resultData, writeCache);
197 | };
198 | exports.default = (resource, { trace, augmentContext, formatResults }) => {
199 | // wrap it so it has a name
200 | const handleAPIRequest = async (req, res, next) => {
201 | if (req.timedout)
202 | return;
203 | try {
204 | await traceAsync(trace, 'sutro/handleAPIRequest', exec(req, res, resource, { trace, augmentContext, formatResults }));
205 | }
206 | catch (err) {
207 | return next(err);
208 | }
209 | };
210 | return handleAPIRequest;
211 | };
212 | //# sourceMappingURL=getRequestHandler.js.map
--------------------------------------------------------------------------------
/dist/getRequestHandler.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"getRequestHandler.js","sourceRoot":"","sources":["../src/getRequestHandler.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/getSwagger.d.ts:
--------------------------------------------------------------------------------
1 | import { getSwaggerArgs, Swagger } from './types';
2 | declare const _default: ({ swagger, base, resources }: getSwaggerArgs) => Swagger;
3 | export default _default;
4 |
--------------------------------------------------------------------------------
/dist/getSwagger.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const lodash_omit_1 = __importDefault(require("lodash.omit"));
7 | const walkResources_1 = __importDefault(require("./walkResources"));
8 | const param = /:(\w+)/gi;
9 | const getResponses = (method, endpoint) => {
10 | const out = {
11 | 404: {
12 | description: 'Not found'
13 | },
14 | 500: {
15 | description: 'Server error'
16 | },
17 | default: {
18 | description: 'Unexpected error'
19 | }
20 | };
21 | if (method === 'post') {
22 | out['201'] = {
23 | description: 'Success, created'
24 | };
25 | }
26 | else {
27 | out['200'] = {
28 | description: 'Success'
29 | };
30 | out['204'] = {
31 | description: 'Success, no data return necessary'
32 | };
33 | }
34 | if (endpoint.isAuthorized) {
35 | out['401'] = {
36 | description: 'Unauthorized'
37 | };
38 | }
39 | return out;
40 | };
41 | const flattenConfig = (base, override) => {
42 | const filtered = lodash_omit_1.default(override, [
43 | 'consumes',
44 | 'produces',
45 | 'responses',
46 | 'parameters'
47 | ]);
48 | return {
49 | consumes: override.consumes || base.consumes,
50 | produces: override.produces || base.produces,
51 | responses: override.responses
52 | ? {
53 | ...base.responses,
54 | ...override.responses
55 | }
56 | : base.responses,
57 | parameters: override.parameters
58 | ? [...(base.parameters || []), ...override.parameters]
59 | : base.parameters,
60 | ...filtered
61 | };
62 | };
63 | const getPaths = (resources) => {
64 | const paths = {};
65 | walkResources_1.default(resources, ({ path, method, endpoint }) => {
66 | if (endpoint?.hidden || endpoint?.swagger === false)
67 | return; // skip
68 | const params = path?.match(param);
69 | const base = {
70 | consumes: (method !== 'get' && ['application/json']) || undefined,
71 | produces: ['application/json'],
72 | parameters: (params &&
73 | params.map((name) => ({
74 | name: name.slice(1),
75 | in: 'path',
76 | required: true,
77 | type: 'string'
78 | }))) ||
79 | undefined,
80 | responses: getResponses(method, endpoint)
81 | };
82 | const fixedPath = path.replace(param, '{$1}');
83 | if (!paths[fixedPath])
84 | paths[fixedPath] = {};
85 | paths[fixedPath][method] = endpoint?.swagger
86 | ? flattenConfig(base, endpoint?.swagger)
87 | : base;
88 | });
89 | return paths;
90 | };
91 | exports.default = ({ swagger = {}, base, resources }) => {
92 | const out = {
93 | swagger: '2.0',
94 | info: {
95 | title: 'Sutro API',
96 | version: '1.0.0'
97 | },
98 | basePath: base,
99 | schemes: ['http'],
100 | paths: getPaths(resources),
101 | ...swagger
102 | };
103 | return out;
104 | };
105 | //# sourceMappingURL=getSwagger.js.map
--------------------------------------------------------------------------------
/dist/getSwagger.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"getSwagger.js","sourceRoot":"","sources":["../src/getSwagger.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import { format, stream } from './formatResults';
4 | import cacheControl from './cacheControl';
5 | import { SutroArgs, SutroRouter, EndpointIsAuthorized, EndpointExecute, EndpointFormat, EndpointCache, EndpointHTTP, SutroRequest, SutroStream } from './types';
6 | export declare const rewriteLargeRequests: (req: import("express").Request>, res: import("express").Response>, next: import("express").NextFunction) => void;
7 | export type { EndpointIsAuthorized, EndpointExecute, EndpointFormat, EndpointCache, EndpointHTTP, SutroRequest, SutroStream };
8 | export * from './errors';
9 | export { format as formatResults, stream as formatResultsStream };
10 | export { cacheControl };
11 | declare const _default: ({ swagger, base, resources, pre, post, augmentContext, formatResults, trace }: SutroArgs) => SutroRouter;
12 | export default _default;
13 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5 | }) : (function(o, m, k, k2) {
6 | if (k2 === undefined) k2 = k;
7 | o[k2] = m[k];
8 | }));
9 | var __exportStar = (this && this.__exportStar) || function(m, exports) {
10 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11 | };
12 | var __importDefault = (this && this.__importDefault) || function (mod) {
13 | return (mod && mod.__esModule) ? mod : { "default": mod };
14 | };
15 | Object.defineProperty(exports, "__esModule", { value: true });
16 | exports.cacheControl = exports.formatResultsStream = exports.formatResults = exports.rewriteLargeRequests = void 0;
17 | const express_1 = require("express");
18 | const handle_async_1 = require("handle-async");
19 | const stream_1 = require("stream");
20 | const formatResults_1 = require("./formatResults");
21 | Object.defineProperty(exports, "formatResults", { enumerable: true, get: function () { return formatResults_1.format; } });
22 | Object.defineProperty(exports, "formatResultsStream", { enumerable: true, get: function () { return formatResults_1.stream; } });
23 | const errors_1 = require("./errors");
24 | const cacheControl_1 = __importDefault(require("./cacheControl"));
25 | exports.cacheControl = cacheControl_1.default;
26 | const getRequestHandler_1 = __importDefault(require("./getRequestHandler"));
27 | const getSwagger_1 = __importDefault(require("./getSwagger"));
28 | const getMeta_1 = __importDefault(require("./getMeta"));
29 | const walkResources_1 = __importDefault(require("./walkResources"));
30 | const rewriteLarge_1 = __importDefault(require("./rewriteLarge"));
31 | // other exports
32 | exports.rewriteLargeRequests = rewriteLarge_1.default;
33 | __exportStar(require("./errors"), exports);
34 | exports.default = ({ swagger, base, resources, pre, post, augmentContext, formatResults, trace }) => {
35 | if (!resources)
36 | throw new Error('Missing resources option');
37 | const router = express_1.Router({ mergeParams: true });
38 | router.swagger = getSwagger_1.default({ swagger, base, resources });
39 | router.meta = getMeta_1.default({ base, resources });
40 | router.base = base;
41 | router.get('/', (_req, res) => res.status(200).json(router.meta).end());
42 | router.get('/swagger', (_req, res) => res.status(200).json(router.swagger).end());
43 | walkResources_1.default(resources, (resource) => {
44 | const handlers = [
45 | getRequestHandler_1.default(resource, { augmentContext, formatResults, trace })
46 | ];
47 | if (pre) {
48 | handlers.unshift(async (req, res, next) => {
49 | const ourTrace = trace && trace.start('sutro/pre');
50 | try {
51 | await handle_async_1.promisify(pre.bind(null, resource, req, res));
52 | }
53 | catch (err) {
54 | if (ourTrace)
55 | ourTrace.end();
56 | return next(err);
57 | }
58 | if (ourTrace)
59 | ourTrace.end();
60 | next();
61 | });
62 | }
63 | if (post) {
64 | handlers.unshift(async (req, res, next) => {
65 | stream_1.finished(res, async (err) => {
66 | const ourTrace = trace && trace.start('sutro/post');
67 | try {
68 | await handle_async_1.promisify(post.bind(null, resource, req, res, err));
69 | }
70 | catch (err) {
71 | if (ourTrace)
72 | ourTrace.end();
73 | }
74 | if (ourTrace)
75 | ourTrace.end();
76 | });
77 | next();
78 | });
79 | }
80 | router[resource.method](resource.path, ...handlers);
81 | });
82 | // handle 404s
83 | router.use((_req, _res, next) => next(new errors_1.NotFoundError()));
84 | return router;
85 | };
86 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/methods.d.ts:
--------------------------------------------------------------------------------
1 | import { Methods } from './types';
2 | declare const methods: Methods;
3 | export default methods;
4 |
--------------------------------------------------------------------------------
/dist/methods.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const methods = {
4 | find: {
5 | method: 'get',
6 | instance: false
7 | },
8 | create: {
9 | method: 'post',
10 | instance: false,
11 | successCode: 201
12 | },
13 | findById: {
14 | method: 'get',
15 | instance: true
16 | },
17 | replaceById: {
18 | method: 'put',
19 | instance: true
20 | },
21 | updateById: {
22 | method: 'patch',
23 | instance: true
24 | },
25 | deleteById: {
26 | method: 'delete',
27 | instance: true
28 | }
29 | };
30 | exports.default = methods;
31 | //# sourceMappingURL=methods.js.map
--------------------------------------------------------------------------------
/dist/methods.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"methods.js","sourceRoot":"","sources":["../src/methods.ts"],"names":[],"mappings":";;AAEA,MAAM,OAAO,GAAY;IACvB,IAAI,EAAE;QACJ,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,KAAK;KAChB;IACD,MAAM,EAAE;QACN,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,GAAG;KACjB;IACD,QAAQ,EAAE;QACR,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,IAAI;KACf;IACD,WAAW,EAAE;QACX,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,IAAI;KACf;IACD,UAAU,EAAE;QACV,MAAM,EAAE,OAAO;QACf,QAAQ,EAAE,IAAI;KACf;IACD,UAAU,EAAE;QACV,MAAM,EAAE,QAAQ;QAChB,QAAQ,EAAE,IAAI;KACf;CACF,CAAA;AAED,kBAAe,OAAO,CAAA"}
--------------------------------------------------------------------------------
/dist/rewriteLarge.d.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | declare const _default: (req: Request, res: Response, next: NextFunction) => void;
3 | export default _default;
4 |
--------------------------------------------------------------------------------
/dist/rewriteLarge.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | // allow people to send a POST and alias it into a GET
4 | // this is a work-around for really large queries
5 | // this is similar to how method-override works but more opinionated
6 | exports.default = (req, res, next) => {
7 | if (req.method.toLowerCase() !== 'post')
8 | return next();
9 | const override = req.get('x-http-method-override');
10 | if (!override || override.toLowerCase() !== 'get')
11 | return next();
12 | // work
13 | req.method = 'GET';
14 | req.query = {
15 | ...req.query,
16 | ...req.body
17 | };
18 | delete req.body;
19 | next();
20 | };
21 | //# sourceMappingURL=rewriteLarge.js.map
--------------------------------------------------------------------------------
/dist/rewriteLarge.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"rewriteLarge.js","sourceRoot":"","sources":["../src/rewriteLarge.ts"],"names":[],"mappings":";;AAGA,sDAAsD;AACtD,iDAAiD;AACjD,oEAAoE;AACpE,kBAAe,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACjE,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,MAAM;QAAE,OAAO,IAAI,EAAE,CAAA;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;IAClD,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,KAAK;QAAE,OAAO,IAAI,EAAE,CAAA;IAEhE,OAAO;IACP,GAAG,CAAC,MAAM,GAAG,KAAK,CAAA;IAClB,GAAG,CAAC,KAAK,GAAG;QACV,GAAG,GAAG,CAAC,KAAK;QACZ,GAAG,GAAG,CAAC,IAAI;KACZ,CAAA;IACD,OAAO,GAAG,CAAC,IAAI,CAAA;IACf,IAAI,EAAE,CAAA;AACR,CAAC,CAAA"}
--------------------------------------------------------------------------------
/dist/types.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { IRouter, Request, Response } from 'express';
3 | import { Readable } from 'stream';
4 | export declare type PathParams = string | RegExp | Array;
5 | export declare type CacheOptions = {
6 | private?: boolean;
7 | public?: boolean;
8 | noStore?: boolean;
9 | noCache?: boolean;
10 | noTransform?: boolean;
11 | proxyRevalidate?: boolean;
12 | mustRevalidate?: boolean;
13 | staleIfError?: number | string;
14 | staleWhileRevalidate?: number | string;
15 | maxAge?: number | string;
16 | sMaxAge?: number | string;
17 | };
18 | export declare type MethodKeys = 'find' | 'create' | 'findById' | 'replaceById' | 'updateById' | 'deleteById';
19 | export declare type MethodVerbs = 'get' | 'post' | 'put' | 'patch' | 'delete';
20 | export declare type Methods = {
21 | [key: string]: {
22 | method: MethodVerbs;
23 | instance: boolean;
24 | successCode?: number;
25 | };
26 | };
27 | export declare type ResourceRoot = {
28 | hidden?: boolean;
29 | path: PathParams;
30 | method: string;
31 | instance: boolean;
32 | swagger?: SwaggerConfig;
33 | isAuthorized?: EndpointIsAuthorized;
34 | execute: EndpointExecute;
35 | format?: EndpointFormat;
36 | cache?: EndpointCache;
37 | http: EndpointHTTP;
38 | endpoint: ResourceRoot;
39 | successCode?: number;
40 | hierarchy: string;
41 | };
42 | export declare type Resource = {
43 | [key: string]: ResourceRoot;
44 | };
45 | export declare type Resources = {
46 | [key: string]: any;
47 | };
48 | export declare type Handler = (args: ResourceRoot) => void;
49 | export declare type walkResourceArgs = {
50 | base?: string;
51 | name: string;
52 | resource: ResourceRoot;
53 | hierarchy?: string;
54 | handler: Handler;
55 | };
56 | export declare type getPathArgs = {
57 | resource: string;
58 | endpoint?: string;
59 | instance: boolean;
60 | };
61 | export declare type Paths = {
62 | [key: string]: {
63 | [key in MethodVerbs]?: SwaggerConfig;
64 | };
65 | };
66 | export declare type MetaRoot = {
67 | path?: PathParams;
68 | method?: MethodVerbs;
69 | instance?: boolean;
70 | [Key: string]: any;
71 | };
72 | export declare type Meta = {
73 | [key: string]: Meta | MetaRoot;
74 | };
75 | export declare type getSwaggerArgs = {
76 | swagger: Swagger;
77 | base?: string;
78 | resources: Resources;
79 | };
80 | export declare type Swagger = {
81 | swagger?: string;
82 | info?: {
83 | title?: string;
84 | version?: string;
85 | termsOfService?: string;
86 | contact?: {
87 | name?: string;
88 | url?: string;
89 | };
90 | description?: string;
91 | };
92 | basePath?: string;
93 | schemes?: string[];
94 | paths?: {
95 | [key: string]: {
96 | [key: string]: SwaggerConfig;
97 | };
98 | };
99 | };
100 | export declare type SwaggerConfig = {
101 | consumes?: string[];
102 | produces?: string[];
103 | parameters?: {
104 | name: string;
105 | in: string;
106 | required: boolean;
107 | type: string;
108 | }[] | undefined;
109 | responses?: Responses;
110 | operationId?: string;
111 | summary?: string;
112 | description?: string;
113 | };
114 | export declare type Trace = {
115 | start: (name: string) => Trace;
116 | end: () => Trace;
117 | };
118 | export declare type SutroArgs = {
119 | base?: string;
120 | resources: Resources;
121 | swagger?: Swagger;
122 | pre?: (resource: ResourceRoot, req: Request, res: Response) => void;
123 | post?: (resource: ResourceRoot, req: Request, res: Response, err?: any) => void;
124 | augmentContext?: (context: SutroRequest, req: Request, resource: ResourceRoot) => Promise | SutroRequest;
125 | formatResults?: (context: SutroRequest, req: Request, resource: ResourceRoot, rawData: any) => void;
126 | trace?: Trace;
127 | };
128 | export interface SutroRouter extends IRouter {
129 | swagger?: Swagger;
130 | meta?: Meta;
131 | base?: string;
132 | }
133 | export declare type ResponseStatusKeys = 'default' | '200' | '201' | '204' | '401' | '404' | '500';
134 | export declare type Responses = {
135 | [key in ResponseStatusKeys]?: {
136 | description: string;
137 | };
138 | };
139 | export interface ExpressRequest extends Request {
140 | timedout: boolean;
141 | user?: any;
142 | session?: any;
143 | }
144 | export interface SutroRequest {
145 | ip: Request['ip'];
146 | url: Request['url'];
147 | protocol: Request['protocol'];
148 | method: Request['method'];
149 | subdomains: Request['subdomains'];
150 | path: Request['path'];
151 | headers: Request['headers'];
152 | cookies: Request['cookies'];
153 | data?: any;
154 | options: Request['query'];
155 | session?: any;
156 | noResponse?: boolean;
157 | onFinish?: (fn: (req: Request, res: Response) => void) => void;
158 | withRaw?: (fn: (req: Request, res: Response) => void) => void;
159 | _req: ExpressRequest;
160 | _res: Response;
161 | [key: string]: any;
162 | }
163 | export interface SutroStream extends Readable {
164 | contentType?: string;
165 | }
166 | export declare type EndpointIsAuthorized = (opt: SutroRequest) => Promise | boolean;
167 | export declare type EndpointExecute = (opt: SutroRequest) => Promise | any;
168 | export declare type EndpointFormat = (opt: SutroRequest, rawData: any) => Promise | any;
169 | export declare type EndpointCache = {
170 | header?: CacheOptions | (() => CacheOptions);
171 | key?: () => string;
172 | get?: (opt: SutroRequest | string, key: string) => Promise | any;
173 | set?: (opt: SutroRequest | string, data: any, key: string) => Promise | any;
174 | };
175 | export declare type EndpointHTTP = {
176 | method: MethodVerbs;
177 | instance: boolean;
178 | };
179 |
--------------------------------------------------------------------------------
/dist/types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | //# sourceMappingURL=types.js.map
--------------------------------------------------------------------------------
/dist/types.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/dist/walkResources.d.ts:
--------------------------------------------------------------------------------
1 | import { Resources, Handler } from './types';
2 | declare const _default: (resources: Resources, handler: Handler) => void;
3 | export default _default;
4 |
--------------------------------------------------------------------------------
/dist/walkResources.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const url_join_1 = __importDefault(require("url-join"));
7 | const getPath_1 = __importDefault(require("./getPath"));
8 | const methods_1 = __importDefault(require("./methods"));
9 | const idxd = (o) => o.index || o;
10 | const walkResource = ({ base, name, resource, hierarchy, handler }) => {
11 | const res = idxd(resource);
12 | // sort custom stuff first
13 | const endpointNames = [];
14 | Object.keys(res).forEach((k) => methods_1.default[k] ? endpointNames.push(k) : endpointNames.unshift(k));
15 | endpointNames.forEach((endpointName) => {
16 | const endpoint = res[endpointName];
17 | const methodInfo = endpoint.http || methods_1.default[endpointName];
18 | if (!methodInfo) {
19 | // TODO: error if still nothing found
20 | const newBase = getPath_1.default({ resource: name, instance: true });
21 | walkResource({
22 | base: base ? url_join_1.default(base, newBase) : newBase,
23 | name: endpointName,
24 | resource: endpoint,
25 | hierarchy: hierarchy ? `${hierarchy}.${name}` : name,
26 | handler
27 | });
28 | return;
29 | }
30 | const path = endpoint.path ||
31 | getPath_1.default({
32 | resource: name,
33 | endpoint: endpointName,
34 | instance: methodInfo.instance
35 | });
36 | const fullPath = base ? url_join_1.default(base, path) : path;
37 | handler({
38 | hierarchy: hierarchy
39 | ? `${hierarchy}.${name}.${endpointName}`
40 | : `${name}.${endpointName}`,
41 | path: fullPath,
42 | endpoint,
43 | ...methodInfo
44 | });
45 | });
46 | };
47 | exports.default = (resources, handler) => {
48 | Object.keys(idxd(resources)).forEach((resourceName) => {
49 | walkResource({
50 | name: resourceName,
51 | resource: resources[resourceName],
52 | handler
53 | });
54 | });
55 | };
56 | //# sourceMappingURL=walkResources.js.map
--------------------------------------------------------------------------------
/dist/walkResources.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"walkResources.js","sourceRoot":"","sources":["../src/walkResources.ts"],"names":[],"mappings":";;;;;AAAA,wDAA2B;AAC3B,wDAA+B;AAC/B,wDAA+B;AAG/B,MAAM,IAAI,GAAG,CAAC,CAAY,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAA;AAE3C,MAAM,YAAY,GAAG,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,OAAO,EACU,EAAE,EAAE;IACrB,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAA;IAE1B,0BAA0B;IAC1B,MAAM,aAAa,GAAa,EAAE,CAAA;IAClC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAC7B,iBAAO,CAAC,CAAe,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAC5E,CAAA;IAED,aAAa,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QACrC,MAAM,QAAQ,GAAG,GAAG,CAAC,YAAY,CAAC,CAAA;QAClC,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,IAAI,iBAAO,CAAC,YAA0B,CAAC,CAAA;QACvE,IAAI,CAAC,UAAU,EAAE;YACf,qCAAqC;YACrC,MAAM,OAAO,GAAG,iBAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3D,YAAY,CAAC;gBACX,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,kBAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;gBAC1C,IAAI,EAAE,YAAY;gBAClB,QAAQ,EAAE,QAAQ;gBAClB,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;gBACpD,OAAO;aACR,CAAC,CAAA;YACF,OAAM;SACP;QACD,MAAM,IAAI,GACR,QAAQ,CAAC,IAAI;YACb,iBAAO,CAAC;gBACN,QAAQ,EAAE,IAAI;gBACd,QAAQ,EAAE,YAAY;gBACtB,QAAQ,EAAE,UAAU,CAAC,QAAQ;aAC9B,CAAC,CAAA;QACJ,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,kBAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC/C,OAAO,CAAC;YACN,SAAS,EAAE,SAAS;gBAClB,CAAC,CAAC,GAAG,SAAS,IAAI,IAAI,IAAI,YAAY,EAAE;gBACxC,CAAC,CAAC,GAAG,IAAI,IAAI,YAAY,EAAE;YAC7B,IAAI,EAAE,QAAQ;YACd,QAAQ;YACR,GAAG,UAAU;SACd,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED,kBAAe,CAAC,SAAoB,EAAE,OAAgB,EAAE,EAAE;IACxD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QACpD,YAAY,CAAC;YACX,IAAI,EAAE,YAAY;YAClB,QAAQ,EAAE,SAAS,CAAC,YAAY,CAAC;YACjC,OAAO;SACR,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA"}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sutro",
3 | "version": "7.2.7",
4 | "description": "API Resource Framework",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "keywords": [
8 | "api",
9 | "realtime",
10 | "rest",
11 | "http",
12 | "express",
13 | "middleware",
14 | "streaming"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/contra/sutro.git"
19 | },
20 | "author": "Contra (https://contra.io)",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/contra/sutro/issues"
24 | },
25 | "homepage": "https://github.com/contra/sutro#readme",
26 | "files": [
27 | "dist"
28 | ],
29 | "engines": {
30 | "node": ">=12"
31 | },
32 | "scripts": {
33 | "docs": "typedoc src/index.ts --theme minimal && gh-pages -d docs",
34 | "lint": "prettier --write .",
35 | "build": "npm run clean && tsc -b",
36 | "clean": "rimraf dist",
37 | "test": "mocha --require ts-node/register --recursive --reporter spec test/*.ts"
38 | },
39 | "husky": {
40 | "hooks": {
41 | "pre-commit": "pretty-quick --staged"
42 | }
43 | },
44 | "devDependencies": {
45 | "@types/express": "^4.17.11",
46 | "@types/lodash.omit": "^4.5.6",
47 | "@types/mocha": "^9.0.0",
48 | "@types/node": "^16.0.0",
49 | "@types/pluralize": "^0.0.29",
50 | "@types/url-join": "^4.0.0",
51 | "gh-pages": "^3.1.0",
52 | "husky": "^4.3.8",
53 | "lodash.pick": "^4.4.0",
54 | "mocha": "^9.0.0",
55 | "prettier": "^2.2.1",
56 | "pretty-quick": "^3.1.0",
57 | "rimraf": "^3.0.2",
58 | "should": "^13.0.0",
59 | "supertest": "^6.0.0",
60 | "swagger-parser": "^6.0.0",
61 | "ts-node": "^10.0.0",
62 | "typedoc": "^0.21.5",
63 | "typescript": "^4.2.3"
64 | },
65 | "dependencies": {
66 | "dot-prop": "^6.0.0",
67 | "express": "^4.16.3",
68 | "handle-async": "^1.0.1",
69 | "jsonstream-next": "^3.0.0",
70 | "lodash.omit": "^4.5.0",
71 | "parse-duration": "^1.0.0",
72 | "pluralize": "^8.0.0",
73 | "url-join": "^4.0.0"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/cacheControl.ts:
--------------------------------------------------------------------------------
1 | import parseDuration from 'parse-duration'
2 | import { CacheOptions } from './types'
3 |
4 | const parseNumber = (v: number | string): number => {
5 | const n = (typeof v === 'number' ? v : parseDuration(v)) as number
6 | if (isNaN(n)) throw new Error(`Invalid number: ${v}`)
7 | return n / 1000
8 | }
9 |
10 | export default (opt: string | CacheOptions) => {
11 | if (typeof opt === 'string') return opt // already formatted
12 | const stack = []
13 |
14 | if (opt.private) stack.push('private')
15 | if (opt.public) stack.push('public')
16 | if (opt.noStore) stack.push('no-store')
17 | if (opt.noCache) stack.push('no-cache')
18 | if (opt.noTransform) stack.push('no-transform')
19 | if (opt.proxyRevalidate) stack.push('proxy-revalidate')
20 | if (opt.mustRevalidate) stack.push('proxy-revalidate')
21 | if (opt.staleIfError)
22 | stack.push(`stale-if-error=${parseNumber(opt.staleIfError)}`)
23 | if (opt.staleWhileRevalidate)
24 | stack.push(
25 | `stale-while-revalidate=${parseNumber(opt.staleWhileRevalidate)}`
26 | )
27 | if (opt.maxAge) stack.push(`max-age=${parseNumber(opt.maxAge)}`)
28 | if (opt.sMaxAge) stack.push(`s-maxage=${parseNumber(opt.sMaxAge)}`)
29 | return stack.join(', ')
30 | }
31 |
--------------------------------------------------------------------------------
/src/errors.ts:
--------------------------------------------------------------------------------
1 | import { inspect } from 'util'
2 |
3 | const inspectOptions = {
4 | depth: 100,
5 | breakLength: Infinity
6 | }
7 |
8 | const serializeIssues = (fields: any[]) =>
9 | fields.map((f) => `\n - ${inspect(f, inspectOptions)}`)
10 |
11 | export const codes = {
12 | badRequest: 400,
13 | unauthorized: 401,
14 | forbidden: 403,
15 | notFound: 404,
16 | serverError: 500
17 | }
18 |
19 | export class UnauthorizedError extends Error {
20 | message: string
21 | status: number
22 |
23 | constructor(message = 'Unauthorized', status = codes.unauthorized) {
24 | super(message)
25 | this.message = message
26 | this.status = status
27 | Error.captureStackTrace(this, UnauthorizedError)
28 | }
29 | toString = () => `${super.toString()} (HTTP ${this.status})`
30 | }
31 |
32 | export class BadRequestError extends Error {
33 | message: string
34 | status: number
35 |
36 | constructor(message = 'Bad Request', status = codes.badRequest) {
37 | super(message)
38 | this.message = message
39 | this.status = status
40 | Error.captureStackTrace(this, BadRequestError)
41 | }
42 | toString() {
43 | return `${super.toString()} (HTTP ${this.status})`
44 | }
45 | }
46 |
47 | export class ValidationError extends BadRequestError {
48 | fields?: any[]
49 |
50 | constructor(fields?: any[]) {
51 | super()
52 | this.fields = fields
53 | Error.captureStackTrace(this, ValidationError)
54 | }
55 | toString() {
56 | const original = super.toString()
57 | if (!this.fields) return original // no custom validation
58 | if (Array.isArray(this.fields)) {
59 | return `${original}\nIssues:${serializeIssues(this.fields)}`
60 | }
61 | return this.fields
62 | }
63 | }
64 |
65 | export class NotFoundError extends Error {
66 | message: string
67 | status: number
68 |
69 | constructor(message = 'Not Found', status = codes.notFound) {
70 | super(message)
71 | this.message = message
72 | this.status = status
73 | Error.captureStackTrace(this, NotFoundError)
74 | }
75 | toString = () => `${super.toString()} (HTTP ${this.status})`
76 | }
77 |
--------------------------------------------------------------------------------
/src/formatResults.ts:
--------------------------------------------------------------------------------
1 | import JSONStream from 'jsonstream-next'
2 |
3 | const isTypeORM = (inp: any) =>
4 | Array.isArray(inp) &&
5 | inp.length === 2 &&
6 | Array.isArray(inp[0]) &&
7 | typeof inp[1] === 'number'
8 | const isSequelize = (inp: any) => inp.rows && typeof inp.count !== 'undefined'
9 |
10 | export const format = (inp: any = [], meta?: object) => {
11 | let rows: any[]
12 | let count: number
13 | if (isSequelize(inp)) {
14 | rows = inp.rows
15 | count = inp.count
16 | } else if (isTypeORM(inp)) {
17 | rows = inp[0]
18 | count = inp[1]
19 | } else if (Array.isArray(inp)) {
20 | rows = inp
21 | } else {
22 | throw new Error('Invalid response! Could not format.')
23 | }
24 | return {
25 | results: rows,
26 | meta: {
27 | results: rows.length,
28 | total:
29 | typeof count === 'undefined'
30 | ? rows.length
31 | : Math.max(rows.length, count), // count should never be below results
32 | ...meta
33 | }
34 | }
35 | }
36 |
37 | export const stream = (counter?: Promise, meta?: object) => {
38 | let results = 0
39 | const tail = JSONStream.stringify('{"results":[', ',', (cb) => {
40 | const fin = (res: number, total: number) => {
41 | const outMeta = {
42 | results: res,
43 | total,
44 | ...meta
45 | }
46 | cb(null, `],"meta":${JSON.stringify(outMeta)}}`)
47 | }
48 | if (!counter) return fin(results, results)
49 | counter
50 | .then((total) => {
51 | const totalConstrained = Math.max(results, total) // count should never be below results
52 | fin(results, totalConstrained)
53 | })
54 | .catch((err) => cb(err))
55 | })
56 | const origWrite = tail.write
57 | tail.write = (...a) => {
58 | ++results
59 | return origWrite.call(tail, ...a)
60 | }
61 | return tail
62 | }
63 | stream.contentType = 'application/json'
64 |
--------------------------------------------------------------------------------
/src/getMeta.ts:
--------------------------------------------------------------------------------
1 | import join from 'url-join'
2 | import dp from 'dot-prop'
3 | import walkResources from './walkResources'
4 | import { Resources, Meta } from './types'
5 |
6 | export default ({
7 | base,
8 | resources
9 | }: {
10 | base?: string
11 | resources: Resources
12 | }): Meta => {
13 | const paths = {}
14 | walkResources(
15 | resources,
16 | ({ hierarchy, path, method, instance, endpoint }) => {
17 | if (endpoint?.hidden) return // skip
18 | const descriptor = {
19 | path: base ? join(base, path as string) : path,
20 | method,
21 | instance
22 | }
23 | dp.set(paths, hierarchy, descriptor)
24 | }
25 | )
26 | return paths
27 | }
28 |
--------------------------------------------------------------------------------
/src/getPath.ts:
--------------------------------------------------------------------------------
1 | import plural from 'pluralize'
2 | import methods from './methods'
3 | import { getPathArgs, MethodKeys } from './types'
4 |
5 | export default ({ resource, endpoint, instance }: getPathArgs) => {
6 | let path = ''
7 | if (resource) path += `/${plural(resource)}`
8 | if (resource && instance) path += `/:${resource}Id`
9 | if (endpoint && !methods[endpoint as MethodKeys]) path += `/${endpoint}`
10 | return path
11 | }
12 |
--------------------------------------------------------------------------------
/src/getRequestHandler.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Response } from 'express'
2 | import { promisify } from 'handle-async'
3 | import { pipeline, Transform, Writable } from 'stream'
4 | import { NotFoundError, UnauthorizedError } from './errors'
5 | import cacheControl from './cacheControl'
6 | import {
7 | Trace,
8 | ExpressRequest,
9 | SutroRequest,
10 | SutroStream,
11 | ResourceRoot,
12 | CacheOptions,
13 | SutroArgs
14 | } from './types'
15 |
16 | type Args = {
17 | trace?: Trace
18 | formatResults: SutroArgs['formatResults']
19 | augmentContext?: SutroArgs['augmentContext']
20 | }
21 |
22 | const defaultCacheHeaders = {
23 | private: true,
24 | noCache: true
25 | }
26 | const responded = (req, res) => res.headersSent || req.timedout
27 |
28 | const traceAsync = async (
29 | trace: Trace,
30 | name: string,
31 | promise: Promise
32 | ) => {
33 | if (!trace) return promise // no tracing, just return
34 | const ourTrace = trace.start(name)
35 | try {
36 | const res = await promise
37 | ourTrace.end()
38 | return res
39 | } catch (err) {
40 | ourTrace.end()
41 | throw err
42 | }
43 | }
44 |
45 | const streamResponse = async (
46 | stream: SutroStream,
47 | req: ExpressRequest,
48 | res: Response,
49 | codes: { [key in 'noResponse' | 'success']: number },
50 | cacheStream?: Writable
51 | ) => {
52 | let hasFirstChunk = false
53 | return new Promise((resolve, reject) => {
54 | let finished = false
55 | const ourStream = pipeline(
56 | stream,
57 | new Transform({
58 | transform(chunk, _, cb) {
59 | // wait until we get a chunk without an error before writing the headers
60 | if (hasFirstChunk) return cb(null, chunk)
61 | hasFirstChunk = true
62 | if (stream.contentType) res.type(stream.contentType)
63 | res.status(codes.success)
64 | cb(null, chunk)
65 | }
66 | }),
67 | (err) => {
68 | finished = true
69 | if (!err || req.timedout) return resolve(undefined) // timed out, no point throwing a duplicate error
70 | reject(err)
71 | }
72 | )
73 |
74 | // make sure we don't keep working if the response closed!
75 | res.once('close', () => {
76 | if (finished) return // no need to blow up
77 | ourStream.destroy(new Error('Socket closed before response finished'))
78 | })
79 |
80 | if (cacheStream) {
81 | ourStream.pipe(cacheStream)
82 | ourStream.pause()
83 | }
84 |
85 | // just use a regular pipe to res, since pipeline would close it on error
86 | // which would make us unable to send an error back out
87 | ourStream.pipe(res)
88 | })
89 | }
90 |
91 | const sendBufferResponse = (
92 | resultData: any,
93 | _req: ExpressRequest,
94 | res: Response,
95 | codes: { [key in 'noResponse' | 'success']: number }
96 | ) => {
97 | res.status(codes.success)
98 | res.type('json')
99 | if (Buffer.isBuffer(resultData)) {
100 | res.send(resultData)
101 | } else if (typeof resultData === 'string') {
102 | res.send(Buffer.from(resultData))
103 | } else {
104 | res.json(resultData)
105 | }
106 |
107 | res.end()
108 | }
109 |
110 | const sendResponse = async (
111 | opt: SutroRequest,
112 | successCode: number | undefined,
113 | resultData: any,
114 | writeCache: (
115 | data: any
116 | ) => Promise | Writable | undefined
117 | ) => {
118 | const { _res, _req, method, noResponse } = opt
119 | const codes = {
120 | noResponse: successCode || 204,
121 | success: successCode || 200
122 | }
123 |
124 | // no response
125 | if (resultData == null) {
126 | if (method === 'GET') throw new NotFoundError()
127 | return _res.status(codes.noResponse).end()
128 | }
129 |
130 | // user asked for no body (save bandwidth)
131 | if (noResponse) {
132 | return _res.status(codes.noResponse).end()
133 | }
134 |
135 | // stream response
136 | if (resultData.pipe && resultData.on) {
137 | const cacheStream = await writeCache(resultData)
138 | await streamResponse(resultData, _req, _res, codes, cacheStream)
139 | return
140 | }
141 |
142 | // json obj response
143 | sendBufferResponse(resultData, _req, _res, codes)
144 | await writeCache(resultData)
145 | }
146 |
147 | const exec = async (
148 | req: ExpressRequest,
149 | res: Response,
150 | resource: ResourceRoot,
151 | { trace, augmentContext, formatResults }: Args
152 | ) => {
153 | if (responded(req, res)) return
154 |
155 | const { endpoint, successCode } = resource
156 | let opt: SutroRequest = {
157 | ...req.params,
158 | ip: req.ip,
159 | url: req.url,
160 | protocol: req.protocol,
161 | method: req.method,
162 | subdomains: req.subdomains,
163 | path: req.path,
164 | headers: req.headers,
165 | cookies: req.cookies,
166 | user: req.user,
167 | data: req.body,
168 | options: req.query,
169 | session: req.session,
170 | noResponse: req.query.response === 'false',
171 | onFinish: (fn: (req: ExpressRequest, res: Response) => void) => {
172 | res.once('finish', fn.bind(null, req, res))
173 | },
174 | withRaw: (fn: (req: ExpressRequest, res: Response) => void) => {
175 | fn(req, res)
176 | },
177 | _req: req,
178 | _res: res
179 | }
180 | if (augmentContext) {
181 | opt = await traceAsync(
182 | trace,
183 | 'sutro/augmentContext',
184 | promisify(augmentContext.bind(null, opt, req, resource))
185 | )
186 | }
187 |
188 | if (responded(req, res)) return
189 |
190 | // check isAuthorized
191 | const authorized =
192 | !endpoint?.isAuthorized ||
193 | (await traceAsync(
194 | trace,
195 | 'sutro/isAuthorized',
196 | promisify(endpoint.isAuthorized.bind(null, opt))
197 | ))
198 | if (authorized !== true) throw new UnauthorizedError()
199 | if (responded(req, res)) return
200 |
201 | let resultData
202 |
203 | // check cache
204 | const cacheKey =
205 | endpoint?.cache &&
206 | endpoint.cache.key &&
207 | (await traceAsync(
208 | trace,
209 | 'sutro/cache.key',
210 | promisify(endpoint.cache.key.bind(null, opt))
211 | ))
212 | if (responded(req, res)) return
213 |
214 | const cachedData =
215 | endpoint?.cache &&
216 | endpoint.cache.get &&
217 | (await traceAsync(
218 | trace,
219 | 'sutro/cache.get',
220 | promisify(endpoint.cache.get.bind(null, opt, cacheKey as string))
221 | ))
222 | if (responded(req, res)) return
223 |
224 | // call execute
225 | if (!cachedData) {
226 | const executeFn =
227 | typeof endpoint === 'function' ? endpoint : endpoint?.execute
228 | const rawData =
229 | typeof executeFn === 'function'
230 | ? await traceAsync(
231 | trace,
232 | 'sutro/execute',
233 | promisify(executeFn.bind(null, opt))
234 | )
235 | : executeFn || null
236 | if (responded(req, res)) return
237 |
238 | // call format on execute result
239 | resultData = endpoint?.format
240 | ? await traceAsync(
241 | trace,
242 | 'sutro/format',
243 | promisify(endpoint.format.bind(null, opt, rawData))
244 | )
245 | : rawData
246 | if (responded(req, res)) return
247 |
248 | // call serialize on final result
249 | resultData = formatResults
250 | ? await traceAsync(
251 | trace,
252 | 'sutro/formatResults',
253 | promisify(formatResults.bind(null, opt, req, endpoint, rawData))
254 | )
255 | : resultData
256 | if (responded(req, res)) return
257 | } else {
258 | resultData = cachedData
259 | }
260 |
261 | // call cacheControl
262 | const cacheHeaders: string | CacheOptions =
263 | endpoint?.cache && endpoint.cache.header
264 | ? typeof endpoint.cache.header === 'function'
265 | ? await traceAsync(
266 | trace,
267 | 'sutro/cache.header',
268 | promisify(endpoint.cache.header.bind(null, opt, resultData))
269 | )
270 | : endpoint.cache.header
271 | : defaultCacheHeaders
272 | if (responded(req, res)) return
273 | if (cacheHeaders) res.set('Cache-Control', cacheControl(cacheHeaders))
274 |
275 | const writeCache = async (data: any) => {
276 | if (cachedData || !endpoint?.cache?.set) return
277 | return traceAsync(
278 | trace,
279 | 'sutro/cache.set',
280 | promisify(endpoint.cache.set.bind(null, opt, data, cacheKey as string))
281 | ) as Promise
282 | }
283 | await sendResponse(opt, successCode, resultData, writeCache)
284 | }
285 |
286 | export default (
287 | resource: ResourceRoot,
288 | { trace, augmentContext, formatResults }: Args
289 | ) => {
290 | // wrap it so it has a name
291 | const handleAPIRequest = async (
292 | req: ExpressRequest,
293 | res: Response,
294 | next: NextFunction
295 | ) => {
296 | if (req.timedout) return
297 | try {
298 | await traceAsync(
299 | trace,
300 | 'sutro/handleAPIRequest',
301 | exec(req, res, resource, { trace, augmentContext, formatResults })
302 | )
303 | } catch (err) {
304 | return next(err)
305 | }
306 | }
307 | return handleAPIRequest
308 | }
309 |
--------------------------------------------------------------------------------
/src/getSwagger.ts:
--------------------------------------------------------------------------------
1 | import omit from 'lodash.omit'
2 | import walkResources from './walkResources'
3 | import {
4 | getSwaggerArgs,
5 | MethodVerbs,
6 | Resources,
7 | Responses,
8 | Swagger,
9 | ResourceRoot,
10 | SwaggerConfig,
11 | Paths
12 | } from './types'
13 |
14 | const param = /:(\w+)/gi
15 |
16 | const getResponses = (method: MethodVerbs, endpoint: ResourceRoot) => {
17 | const out: Responses = {
18 | 404: {
19 | description: 'Not found'
20 | },
21 | 500: {
22 | description: 'Server error'
23 | },
24 | default: {
25 | description: 'Unexpected error'
26 | }
27 | }
28 |
29 | if (method === 'post') {
30 | out['201'] = {
31 | description: 'Success, created'
32 | }
33 | } else {
34 | out['200'] = {
35 | description: 'Success'
36 | }
37 | out['204'] = {
38 | description: 'Success, no data return necessary'
39 | }
40 | }
41 |
42 | if (endpoint.isAuthorized) {
43 | out['401'] = {
44 | description: 'Unauthorized'
45 | }
46 | }
47 | return out
48 | }
49 |
50 | const flattenConfig = (
51 | base: SwaggerConfig,
52 | override: SwaggerConfig
53 | ): SwaggerConfig => {
54 | const filtered = omit(override, [
55 | 'consumes',
56 | 'produces',
57 | 'responses',
58 | 'parameters'
59 | ])
60 | return {
61 | consumes: override.consumes || base.consumes,
62 | produces: override.produces || base.produces,
63 | responses: override.responses
64 | ? {
65 | ...base.responses,
66 | ...override.responses
67 | }
68 | : base.responses,
69 | parameters: override.parameters
70 | ? [...(base.parameters || []), ...override.parameters]
71 | : base.parameters,
72 | ...filtered
73 | }
74 | }
75 |
76 | const getPaths = (resources: Resources): Paths => {
77 | const paths: Paths = {}
78 | walkResources(resources, ({ path, method, endpoint }) => {
79 | if (endpoint?.hidden || endpoint?.swagger === false) return // skip
80 | const params = (path as string)?.match(param)
81 | const base: SwaggerConfig = {
82 | consumes: (method !== 'get' && ['application/json']) || undefined,
83 | produces: ['application/json'],
84 | parameters:
85 | (params &&
86 | params.map((name: string) => ({
87 | name: name.slice(1),
88 | in: 'path',
89 | required: true,
90 | type: 'string'
91 | }))) ||
92 | undefined,
93 | responses: getResponses(method as MethodVerbs, endpoint as ResourceRoot)
94 | }
95 | const fixedPath = (path as string).replace(param, '{$1}')
96 | if (!paths[fixedPath]) paths[fixedPath] = {}
97 | paths[fixedPath][method as MethodVerbs] = endpoint?.swagger
98 | ? flattenConfig(base, endpoint?.swagger)
99 | : base
100 | })
101 | return paths
102 | }
103 |
104 | export default ({ swagger = {}, base, resources }: getSwaggerArgs): Swagger => {
105 | const out = {
106 | swagger: '2.0',
107 | info: {
108 | title: 'Sutro API',
109 | version: '1.0.0'
110 | },
111 | basePath: base,
112 | schemes: ['http'],
113 | paths: getPaths(resources),
114 | ...swagger
115 | }
116 | return out
117 | }
118 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'handle-async'
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { promisify } from 'handle-async'
3 | import { finished } from 'stream'
4 | import { format, stream } from './formatResults'
5 | import { NotFoundError } from './errors'
6 | import cacheControl from './cacheControl'
7 | import getRequestHandler from './getRequestHandler'
8 | import getSwagger from './getSwagger'
9 | import getMeta from './getMeta'
10 | import walkResources from './walkResources'
11 | import rewriteLarge from './rewriteLarge'
12 | import {
13 | MethodVerbs,
14 | SutroArgs,
15 | SutroRouter,
16 | PathParams,
17 | EndpointIsAuthorized,
18 | EndpointExecute,
19 | EndpointFormat,
20 | EndpointCache,
21 | EndpointHTTP,
22 | SutroRequest,
23 | SutroStream
24 | } from './types'
25 |
26 | // other exports
27 | export const rewriteLargeRequests = rewriteLarge
28 | export type {
29 | EndpointIsAuthorized,
30 | EndpointExecute,
31 | EndpointFormat,
32 | EndpointCache,
33 | EndpointHTTP,
34 | SutroRequest,
35 | SutroStream
36 | }
37 | export * from './errors'
38 | export { format as formatResults, stream as formatResultsStream }
39 | export { cacheControl }
40 |
41 | export default ({
42 | swagger,
43 | base,
44 | resources,
45 | pre,
46 | post,
47 | augmentContext,
48 | formatResults,
49 | trace
50 | }: SutroArgs) => {
51 | if (!resources) throw new Error('Missing resources option')
52 | const router: SutroRouter = Router({ mergeParams: true })
53 | router.swagger = getSwagger({ swagger, base, resources })
54 | router.meta = getMeta({ base, resources })
55 | router.base = base
56 |
57 | router.get('/', (_req, res) => res.status(200).json(router.meta).end())
58 | router.get('/swagger', (_req, res) =>
59 | res.status(200).json(router.swagger).end()
60 | )
61 |
62 | walkResources(resources, (resource) => {
63 | const handlers = [
64 | getRequestHandler(resource, { augmentContext, formatResults, trace })
65 | ]
66 | if (pre) {
67 | handlers.unshift(async (req, res, next) => {
68 | const ourTrace = trace && trace.start('sutro/pre')
69 | try {
70 | await promisify(pre.bind(null, resource, req, res))
71 | } catch (err) {
72 | if (ourTrace) ourTrace.end()
73 | return next(err)
74 | }
75 | if (ourTrace) ourTrace.end()
76 | next()
77 | })
78 | }
79 | if (post) {
80 | handlers.unshift(async (req, res, next) => {
81 | finished(res, async (err) => {
82 | const ourTrace = trace && trace.start('sutro/post')
83 | try {
84 | await promisify(post.bind(null, resource, req, res, err))
85 | } catch (err) {
86 | if (ourTrace) ourTrace.end()
87 | }
88 | if (ourTrace) ourTrace.end()
89 | })
90 | next()
91 | })
92 | }
93 | router[resource.method as MethodVerbs](
94 | resource.path as PathParams,
95 | ...handlers
96 | )
97 | })
98 |
99 | // handle 404s
100 | router.use((_req, _res, next) => next(new NotFoundError()))
101 | return router
102 | }
103 |
--------------------------------------------------------------------------------
/src/methods.ts:
--------------------------------------------------------------------------------
1 | import { Methods } from './types'
2 |
3 | const methods: Methods = {
4 | find: {
5 | method: 'get',
6 | instance: false
7 | },
8 | create: {
9 | method: 'post',
10 | instance: false,
11 | successCode: 201
12 | },
13 | findById: {
14 | method: 'get',
15 | instance: true
16 | },
17 | replaceById: {
18 | method: 'put',
19 | instance: true
20 | },
21 | updateById: {
22 | method: 'patch',
23 | instance: true
24 | },
25 | deleteById: {
26 | method: 'delete',
27 | instance: true
28 | }
29 | }
30 |
31 | export default methods
32 |
--------------------------------------------------------------------------------
/src/rewriteLarge.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express'
2 | import { MethodVerbs } from './types'
3 |
4 | // allow people to send a POST and alias it into a GET
5 | // this is a work-around for really large queries
6 | // this is similar to how method-override works but more opinionated
7 | export default (req: Request, res: Response, next: NextFunction) => {
8 | if (req.method.toLowerCase() !== 'post') return next()
9 | const override = req.get('x-http-method-override')
10 | if (!override || override.toLowerCase() !== 'get') return next()
11 |
12 | // work
13 | req.method = 'GET'
14 | req.query = {
15 | ...req.query,
16 | ...req.body
17 | }
18 | delete req.body
19 | next()
20 | }
21 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { IRouter, Request, Response } from 'express'
2 | import { Readable } from 'stream'
3 |
4 | export type PathParams = string | RegExp | Array
5 |
6 | export type CacheOptions = {
7 | private?: boolean
8 | public?: boolean
9 | noStore?: boolean
10 | noCache?: boolean
11 | noTransform?: boolean
12 | proxyRevalidate?: boolean
13 | mustRevalidate?: boolean
14 | staleIfError?: number | string
15 | staleWhileRevalidate?: number | string
16 | maxAge?: number | string
17 | sMaxAge?: number | string
18 | }
19 |
20 | export type MethodKeys =
21 | | 'find'
22 | | 'create'
23 | | 'findById'
24 | | 'replaceById'
25 | | 'updateById'
26 | | 'deleteById'
27 |
28 | export type MethodVerbs = 'get' | 'post' | 'put' | 'patch' | 'delete'
29 |
30 | export type Methods = {
31 | [key: string]: {
32 | method: MethodVerbs
33 | instance: boolean
34 | successCode?: number
35 | }
36 | }
37 |
38 | export type ResourceRoot = {
39 | hidden?: boolean
40 | path: PathParams
41 | method: string
42 | instance: boolean
43 | swagger?: SwaggerConfig
44 | isAuthorized?: EndpointIsAuthorized
45 | execute: EndpointExecute
46 | format?: EndpointFormat
47 | cache?: EndpointCache
48 | http: EndpointHTTP
49 | endpoint: ResourceRoot
50 | successCode?: number
51 | hierarchy: string
52 | }
53 |
54 | export type Resource = {
55 | [key: string]: ResourceRoot
56 | }
57 |
58 | export type Resources = {
59 | [key: string]: any // this type is recursive and not possible ATM
60 | }
61 |
62 | export type Handler = (args: ResourceRoot) => void
63 |
64 | export type walkResourceArgs = {
65 | base?: string
66 | name: string
67 | resource: ResourceRoot
68 | hierarchy?: string
69 | handler: Handler
70 | }
71 |
72 | export type getPathArgs = {
73 | resource: string
74 | endpoint?: string
75 | instance: boolean
76 | }
77 |
78 | export type Paths = {
79 | [key: string]: {
80 | [key in MethodVerbs]?: SwaggerConfig
81 | }
82 | }
83 |
84 | export type MetaRoot = {
85 | path?: PathParams
86 | method?: MethodVerbs
87 | instance?: boolean
88 | [Key: string]: any
89 | }
90 |
91 | export type Meta = {
92 | [key: string]: Meta | MetaRoot
93 | }
94 |
95 | export type getSwaggerArgs = {
96 | swagger: Swagger
97 | base?: string
98 | resources: Resources
99 | }
100 |
101 | export type Swagger = {
102 | swagger?: string
103 | info?: {
104 | title?: string
105 | version?: string
106 | termsOfService?: string
107 | contact?: {
108 | name?: string
109 | url?: string
110 | }
111 | description?: string
112 | }
113 | basePath?: string
114 | schemes?: string[]
115 | paths?: {
116 | [key: string]: {
117 | [key: string]: SwaggerConfig
118 | }
119 | }
120 | }
121 |
122 | export type SwaggerConfig = {
123 | consumes?: string[]
124 | produces?: string[]
125 | parameters?:
126 | | {
127 | name: string
128 | in: string
129 | required: boolean
130 | type: string
131 | }[]
132 | | undefined
133 | responses?: Responses
134 | operationId?: string
135 | summary?: string
136 | description?: string
137 | }
138 |
139 | export type Trace = {
140 | start: (name: string) => Trace
141 | end: () => Trace
142 | }
143 |
144 | export type SutroArgs = {
145 | base?: string
146 | resources: Resources
147 | swagger?: Swagger
148 | pre?: (resource: ResourceRoot, req: Request, res: Response) => void
149 | post?: (
150 | resource: ResourceRoot,
151 | req: Request,
152 | res: Response,
153 | err?: any
154 | ) => void
155 | augmentContext?: (
156 | context: SutroRequest,
157 | req: Request,
158 | resource: ResourceRoot
159 | ) => Promise | SutroRequest
160 | formatResults?: (
161 | context: SutroRequest,
162 | req: Request,
163 | resource: ResourceRoot,
164 | rawData: any
165 | ) => void
166 | trace?: Trace
167 | }
168 |
169 | export interface SutroRouter extends IRouter {
170 | swagger?: Swagger
171 | meta?: Meta
172 | base?: string
173 | }
174 |
175 | export type ResponseStatusKeys =
176 | | 'default'
177 | | '200'
178 | | '201'
179 | | '204'
180 | | '401'
181 | | '404'
182 | | '500'
183 |
184 | export type Responses = {
185 | [key in ResponseStatusKeys]?: {
186 | description: string
187 | }
188 | }
189 |
190 | // our custom express overrides
191 | export interface ExpressRequest extends Request {
192 | timedout: boolean
193 | user?: any
194 | session?: any
195 | }
196 |
197 | // our core primitives
198 | export interface SutroRequest {
199 | ip: Request['ip']
200 | url: Request['url']
201 | protocol: Request['protocol']
202 | method: Request['method']
203 | subdomains: Request['subdomains']
204 | path: Request['path']
205 | headers: Request['headers']
206 | cookies: Request['cookies']
207 | data?: any
208 | options: Request['query']
209 | session?: any
210 | noResponse?: boolean
211 | onFinish?: (fn: (req: Request, res: Response) => void) => void
212 | withRaw?: (fn: (req: Request, res: Response) => void) => void
213 | _req: ExpressRequest
214 | _res: Response
215 | [key: string]: any
216 | }
217 |
218 | export interface SutroStream extends Readable {
219 | contentType?: string
220 | }
221 |
222 | export type EndpointIsAuthorized = (
223 | opt: SutroRequest
224 | ) => Promise | boolean
225 | export type EndpointExecute = (opt: SutroRequest) => Promise | any
226 | export type EndpointFormat = (
227 | opt: SutroRequest,
228 | rawData: any
229 | ) => Promise | any
230 | export type EndpointCache = {
231 | header?: CacheOptions | (() => CacheOptions)
232 | key?: () => string
233 | get?: (opt: SutroRequest | string, key: string) => Promise | any
234 | set?: (
235 | opt: SutroRequest | string,
236 | data: any,
237 | key: string
238 | ) => Promise | any
239 | }
240 | export type EndpointHTTP = {
241 | method: MethodVerbs
242 | instance: boolean
243 | }
244 |
--------------------------------------------------------------------------------
/src/walkResources.ts:
--------------------------------------------------------------------------------
1 | import join from 'url-join'
2 | import getPath from './getPath'
3 | import methods from './methods'
4 | import { Resources, walkResourceArgs, Handler, MethodKeys } from './types'
5 |
6 | const idxd = (o: Resources) => o.index || o
7 |
8 | const walkResource = ({
9 | base,
10 | name,
11 | resource,
12 | hierarchy,
13 | handler
14 | }: walkResourceArgs) => {
15 | const res = idxd(resource)
16 |
17 | // sort custom stuff first
18 | const endpointNames: string[] = []
19 | Object.keys(res).forEach((k) =>
20 | methods[k as MethodKeys] ? endpointNames.push(k) : endpointNames.unshift(k)
21 | )
22 |
23 | endpointNames.forEach((endpointName) => {
24 | const endpoint = res[endpointName]
25 | const methodInfo = endpoint.http || methods[endpointName as MethodKeys]
26 | if (!methodInfo) {
27 | // TODO: error if still nothing found
28 | const newBase = getPath({ resource: name, instance: true })
29 | walkResource({
30 | base: base ? join(base, newBase) : newBase,
31 | name: endpointName,
32 | resource: endpoint,
33 | hierarchy: hierarchy ? `${hierarchy}.${name}` : name,
34 | handler
35 | })
36 | return
37 | }
38 | const path =
39 | endpoint.path ||
40 | getPath({
41 | resource: name,
42 | endpoint: endpointName,
43 | instance: methodInfo.instance
44 | })
45 | const fullPath = base ? join(base, path) : path
46 | handler({
47 | hierarchy: hierarchy
48 | ? `${hierarchy}.${name}.${endpointName}`
49 | : `${name}.${endpointName}`,
50 | path: fullPath,
51 | endpoint,
52 | ...methodInfo
53 | })
54 | })
55 | }
56 |
57 | export default (resources: Resources, handler: Handler) => {
58 | Object.keys(idxd(resources)).forEach((resourceName) => {
59 | walkResource({
60 | name: resourceName,
61 | resource: resources[resourceName],
62 | handler
63 | })
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | /*eslint no-console: 0*/
2 | import should from 'should'
3 | import sutro, { rewriteLargeRequests } from '../src'
4 | import request from 'supertest'
5 | import express from 'express'
6 | import Parser from 'swagger-parser'
7 | import JSONStream from 'jsonstream-next'
8 | import { SutroArgs } from '../src/types'
9 |
10 | const parser = new Parser()
11 | const users = [
12 | { id: 0, name: 'foo' },
13 | { id: 1, name: 'bar' },
14 | { id: 2, name: 'baz' }
15 | ]
16 |
17 | const passengers = [{ name: 'todd' }, { name: 'rob' }]
18 | const cars = [
19 | [
20 | { id: 0, name: 'foo', passengers },
21 | { id: 1, name: 'bar', passengers },
22 | { id: 2, name: 'baz', passengers }
23 | ],
24 | [
25 | { id: 0, name: 'foo', passengers },
26 | { id: 1, name: 'bar', passengers },
27 | { id: 2, name: 'baz', passengers }
28 | ],
29 | [
30 | { id: 0, name: 'foo', passengers },
31 | { id: 1, name: 'bar', passengers },
32 | { id: 2, name: 'baz', passengers }
33 | ]
34 | ]
35 |
36 | describe('sutro', () => {
37 | it('should export a function', () => {
38 | should.exist(sutro)
39 | should(typeof sutro).eql('function')
40 | })
41 | it('should return a router', () => {
42 | const router = sutro({ resources: {} })
43 | should.exist(router)
44 | should(typeof router).eql('function')
45 | })
46 | it('should error if missing resources', () => {
47 | sutro.should.throw()
48 | sutro.bind(null, {}).should.throw()
49 | })
50 | })
51 |
52 | describe('sutro - function handlers', () => {
53 | const config: SutroArgs = {
54 | pre: async (o, req, res) => {
55 | should.exist(o)
56 | should.exist(req)
57 | should.exist(res)
58 | },
59 | resources: {
60 | user: {
61 | create: (opts, cb) => cb(null, { created: true }),
62 | find: (opts, cb) => cb(null, users),
63 | findById: (opts, cb) => cb(null, users[opts.userId]),
64 | deleteById: (opts, cb) => cb(null, { deleted: true }),
65 | updateById: (opts, cb) => cb(null, { updated: true }),
66 | replaceById: (opts, cb) => cb(null, { replaced: true }),
67 | car: {
68 | create: (opts, cb) => cb(null, { created: true }),
69 | find: (opts, cb) => cb(null, cars[opts.userId]),
70 | findById: (opts, cb) => cb(null, cars[opts.userId][opts.carId]),
71 | deleteById: (opts, cb) => cb(null, { deleted: true }),
72 | updateById: (opts, cb) => cb(null, { updated: true }),
73 | replaceById: (opts, cb) => cb(null, { replaced: true }),
74 |
75 | passenger: {
76 | create: (opts, cb) => cb(null, { created: true }),
77 | find: (opts, cb) =>
78 | cb(null, cars[opts.userId][opts.carId].passengers),
79 | findById: (opts, cb) =>
80 | cb(
81 | null,
82 | cars[opts.userId][opts.carId].passengers[opts.passengerId]
83 | ),
84 | deleteById: (opts, cb) => cb(null, { deleted: true }),
85 | updateById: (opts, cb) => cb(null, { updated: true }),
86 | replaceById: (opts, cb) => cb(null, { replaced: true })
87 | }
88 | },
89 | me: {
90 | execute: (opts, cb) => cb(null, { me: true }),
91 | http: {
92 | method: 'get',
93 | instance: false
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | const app = express().use(sutro(config))
101 |
102 | it('should register a resource find endpoint', async () =>
103 | request(app)
104 | .get('/users')
105 | .set('Accept', 'application/json')
106 | .expect('Content-Type', /json/)
107 | .expect(200, users))
108 |
109 | it('should register a resource findById endpoint', async () =>
110 | request(app)
111 | .get('/users/1')
112 | .set('Accept', 'application/json')
113 | .expect('Content-Type', /json/)
114 | .expect(200, users[1]))
115 |
116 | it('should register a resource create endpoint', async () =>
117 | request(app)
118 | .post('/users')
119 | .set('Accept', 'application/json')
120 | .expect('Content-Type', /json/)
121 | .expect(201, { created: true }))
122 |
123 | it('should register a resource create endpoint that works with response=false', async () =>
124 | request(app)
125 | .post('/users')
126 | .set('Accept', 'application/json')
127 | .query({ response: false })
128 | .expect(201)
129 | .expect(({ body }) => !body))
130 |
131 | it('should register a resource delete endpoint', async () =>
132 | request(app)
133 | .delete('/users/1')
134 | .set('Accept', 'application/json')
135 | .expect('Content-Type', /json/)
136 | .expect(200, { deleted: true }))
137 |
138 | it('should register a resource replace endpoint', async () =>
139 | request(app)
140 | .put('/users/1')
141 | .set('Accept', 'application/json')
142 | .expect('Content-Type', /json/)
143 | .expect(200, { replaced: true }))
144 |
145 | it('should register a resource update endpoint', async () =>
146 | request(app)
147 | .patch('/users/1')
148 | .set('Accept', 'application/json')
149 | .expect('Content-Type', /json/)
150 | .expect(200, { updated: true }))
151 |
152 | it('should register a custom resource', async () =>
153 | request(app)
154 | .get('/users/me')
155 | .set('Accept', 'application/json')
156 | .expect('Content-Type', /json/)
157 | .expect(200, { me: true }))
158 |
159 | it('should register a nested resource find endpoint', async () =>
160 | request(app)
161 | .get('/users/1/cars')
162 | .set('Accept', 'application/json')
163 | .expect('Content-Type', /json/)
164 | .expect(200, cars[1]))
165 |
166 | it('should register a nested resource findById endpoint', async () =>
167 | request(app)
168 | .get('/users/1/cars/1')
169 | .set('Accept', 'application/json')
170 | .expect('Content-Type', /json/)
171 | .expect(200, cars[1][1]))
172 |
173 | it('should register a nested resource create endpoint', async () =>
174 | request(app)
175 | .post('/users/1/cars')
176 | .set('Accept', 'application/json')
177 | .expect('Content-Type', /json/)
178 | .expect(201, { created: true }))
179 |
180 | it('should register a nested resource delete endpoint', async () =>
181 | request(app)
182 | .delete('/users/1/cars/1')
183 | .set('Accept', 'application/json')
184 | .expect('Content-Type', /json/)
185 | .expect(200, { deleted: true }))
186 |
187 | it('should register a nested resource replace endpoint', async () =>
188 | request(app)
189 | .put('/users/1/cars/1')
190 | .set('Accept', 'application/json')
191 | .expect('Content-Type', /json/)
192 | .expect(200, { replaced: true }))
193 |
194 | it('should register a nested resource update endpoint', async () =>
195 | request(app)
196 | .patch('/users/1/cars/1')
197 | .set('Accept', 'application/json')
198 | .expect('Content-Type', /json/)
199 | .expect(200, { updated: true }))
200 | it('should register a double nested resource find endpoint', async () =>
201 | request(app)
202 | .get('/users/1/cars/1/passengers')
203 | .set('Accept', 'application/json')
204 | .expect('Content-Type', /json/)
205 | .expect(200, cars[1][1].passengers))
206 |
207 | it('should register a double nested resource findById endpoint', async () =>
208 | request(app)
209 | .get('/users/1/cars/1/passengers/0')
210 | .set('Accept', 'application/json')
211 | .expect('Content-Type', /json/)
212 | .expect(200, cars[1][1].passengers[0]))
213 |
214 | it('should register a double nested resource create endpoint', async () =>
215 | request(app)
216 | .post('/users/1/cars/1/passengers')
217 | .set('Accept', 'application/json')
218 | .expect('Content-Type', /json/)
219 | .expect(201, { created: true }))
220 |
221 | it('should register a double nested resource delete endpoint', async () =>
222 | request(app)
223 | .delete('/users/1/cars/1/passengers/1')
224 | .set('Accept', 'application/json')
225 | .expect('Content-Type', /json/)
226 | .expect(200, { deleted: true }))
227 |
228 | it('should register a double nested resource replace endpoint', async () =>
229 | request(app)
230 | .put('/users/1/cars/1/passengers/1')
231 | .set('Accept', 'application/json')
232 | .expect('Content-Type', /json/)
233 | .expect(200, { replaced: true }))
234 |
235 | it('should register a double nested resource update endpoint', async () =>
236 | request(app)
237 | .patch('/users/1/cars/1/passengers/1')
238 | .set('Accept', 'application/json')
239 | .expect('Content-Type', /json/)
240 | .expect(200, { updated: true }))
241 |
242 | it('should have a valid swagger file', async () => {
243 | const { body } = await request(app)
244 | .get('/swagger')
245 | .set('Accept', 'application/json')
246 | .expect('Content-Type', /json/)
247 | .expect(200)
248 |
249 | await parser.validate(body)
250 | })
251 |
252 | it('should have a meta index', async () => {
253 | const { body } = await request(app)
254 | .get('/')
255 | .set('Accept', 'application/json')
256 | .expect('Content-Type', /json/)
257 | .expect(200)
258 |
259 | should(body).eql({
260 | user: {
261 | me: {
262 | path: '/users/me',
263 | method: 'get',
264 | instance: false
265 | },
266 | car: {
267 | passenger: {
268 | create: {
269 | path: '/users/:userId/cars/:carId/passengers',
270 | method: 'post',
271 | instance: false
272 | },
273 | find: {
274 | path: '/users/:userId/cars/:carId/passengers',
275 | method: 'get',
276 | instance: false
277 | },
278 | findById: {
279 | path: '/users/:userId/cars/:carId/passengers/:passengerId',
280 | method: 'get',
281 | instance: true
282 | },
283 | deleteById: {
284 | path: '/users/:userId/cars/:carId/passengers/:passengerId',
285 | method: 'delete',
286 | instance: true
287 | },
288 | updateById: {
289 | path: '/users/:userId/cars/:carId/passengers/:passengerId',
290 | method: 'patch',
291 | instance: true
292 | },
293 | replaceById: {
294 | path: '/users/:userId/cars/:carId/passengers/:passengerId',
295 | method: 'put',
296 | instance: true
297 | }
298 | },
299 | create: {
300 | path: '/users/:userId/cars',
301 | method: 'post',
302 | instance: false
303 | },
304 | find: {
305 | path: '/users/:userId/cars',
306 | method: 'get',
307 | instance: false
308 | },
309 | findById: {
310 | path: '/users/:userId/cars/:carId',
311 | method: 'get',
312 | instance: true
313 | },
314 | deleteById: {
315 | path: '/users/:userId/cars/:carId',
316 | method: 'delete',
317 | instance: true
318 | },
319 | updateById: {
320 | path: '/users/:userId/cars/:carId',
321 | method: 'patch',
322 | instance: true
323 | },
324 | replaceById: {
325 | path: '/users/:userId/cars/:carId',
326 | method: 'put',
327 | instance: true
328 | }
329 | },
330 | create: {
331 | path: '/users',
332 | method: 'post',
333 | instance: false
334 | },
335 | find: {
336 | path: '/users',
337 | method: 'get',
338 | instance: false
339 | },
340 | findById: {
341 | path: '/users/:userId',
342 | method: 'get',
343 | instance: true
344 | },
345 | deleteById: {
346 | path: '/users/:userId',
347 | method: 'delete',
348 | instance: true
349 | },
350 | updateById: {
351 | path: '/users/:userId',
352 | method: 'patch',
353 | instance: true
354 | },
355 | replaceById: {
356 | path: '/users/:userId',
357 | method: 'put',
358 | instance: true
359 | }
360 | }
361 | })
362 | })
363 | })
364 |
365 | describe('sutro - async function handlers', () => {
366 | const config: SutroArgs = {
367 | resources: {
368 | user: {
369 | create: async () => ({ created: true }),
370 | find: async () => users,
371 | findById: async (opts) => users[opts.userId],
372 | deleteById: async () => ({ deleted: true }),
373 | updateById: async () => ({ updated: true }),
374 | replaceById: async () => ({ replaced: true }),
375 | me: {
376 | execute: async () => ({ me: true }),
377 | http: {
378 | method: 'get',
379 | instance: false
380 | }
381 | }
382 | }
383 | }
384 | }
385 |
386 | const app = express().use(sutro(config))
387 |
388 | it('should register a resource find endpoint', async () =>
389 | request(app)
390 | .get('/users')
391 | .set('Accept', 'application/json')
392 | .expect('Content-Type', /json/)
393 | .expect(200, users))
394 |
395 | it('should register a resource findById endpoint', async () =>
396 | request(app)
397 | .get('/users/1')
398 | .set('Accept', 'application/json')
399 | .expect('Content-Type', /json/)
400 | .expect(200, users[1]))
401 |
402 | it('should register a resource create endpoint', async () =>
403 | request(app)
404 | .post('/users')
405 | .set('Accept', 'application/json')
406 | .expect('Content-Type', /json/)
407 | .expect(201, { created: true }))
408 |
409 | it('should register a resource delete endpoint', async () =>
410 | request(app)
411 | .delete('/users/1')
412 | .set('Accept', 'application/json')
413 | .expect('Content-Type', /json/)
414 | .expect(200, { deleted: true }))
415 |
416 | it('should register a resource replace endpoint', async () =>
417 | request(app)
418 | .put('/users/1')
419 | .set('Accept', 'application/json')
420 | .expect('Content-Type', /json/)
421 | .expect(200, { replaced: true }))
422 |
423 | it('should register a resource update endpoint', async () =>
424 | request(app)
425 | .patch('/users/1')
426 | .set('Accept', 'application/json')
427 | .expect('Content-Type', /json/)
428 | .expect(200, { updated: true }))
429 |
430 | it('should register a custom resource', async () =>
431 | request(app)
432 | .get('/users/me')
433 | .set('Accept', 'application/json')
434 | .expect('Content-Type', /json/)
435 | .expect(200, { me: true }))
436 | })
437 |
438 | describe('sutro - flat value handlers', () => {
439 | const config: SutroArgs = {
440 | pre: async (o, req, res) => {
441 | should.exist(o)
442 | should.exist(req)
443 | should.exist(res)
444 | },
445 | post: async (o, req, res, err) => {
446 | should.exist(o)
447 | should.exist(req)
448 | should.exist(res)
449 | should.not.exist(err)
450 | },
451 | resources: {
452 | user: {
453 | create: () => ({ created: true }),
454 | find: () => users,
455 | findById: (opts) => users[opts.userId],
456 | deleteById: () => ({ deleted: true }),
457 | updateById: () => ({ updated: true }),
458 | replaceById: () => ({ replaced: true }),
459 | me: {
460 | execute: () => ({ me: true }),
461 | http: {
462 | method: 'get',
463 | instance: false
464 | }
465 | },
466 | isCool: {
467 | execute: () => false,
468 | http: {
469 | method: 'get',
470 | instance: true
471 | }
472 | },
473 | nulled: {
474 | execute: () => null,
475 | http: {
476 | method: 'get',
477 | instance: false
478 | }
479 | }
480 | }
481 | }
482 | }
483 |
484 | const app = express().use(sutro(config))
485 |
486 | it('should register a resource find endpoint', async () =>
487 | request(app)
488 | .get('/users')
489 | .set('Accept', 'application/json')
490 | .expect('Content-Type', /json/)
491 | .expect(200, users))
492 |
493 | it('should register a resource findById endpoint', async () =>
494 | request(app)
495 | .get('/users/1')
496 | .set('Accept', 'application/json')
497 | .expect('Content-Type', /json/)
498 | .expect(200, users[1]))
499 |
500 | it('should register a resource create endpoint', async () =>
501 | request(app)
502 | .post('/users')
503 | .set('Accept', 'application/json')
504 | .expect('Content-Type', /json/)
505 | .expect(201, { created: true }))
506 |
507 | it('should register a resource delete endpoint', async () =>
508 | request(app)
509 | .delete('/users/1')
510 | .set('Accept', 'application/json')
511 | .expect('Content-Type', /json/)
512 | .expect(200, { deleted: true }))
513 |
514 | it('should register a resource replace endpoint', async () =>
515 | request(app)
516 | .put('/users/1')
517 | .set('Accept', 'application/json')
518 | .expect('Content-Type', /json/)
519 | .expect(200, { replaced: true }))
520 |
521 | it('should register a resource update endpoint', async () =>
522 | request(app)
523 | .patch('/users/1')
524 | .set('Accept', 'application/json')
525 | .expect('Content-Type', /json/)
526 | .expect(200, { updated: true }))
527 |
528 | it('should register a custom resource', async () =>
529 | request(app)
530 | .get('/users/me')
531 | .set('Accept', 'application/json')
532 | .expect('Content-Type', /json/)
533 | .expect(200, { me: true }))
534 |
535 | it('should return 200 with data from a custom falsey resource', async () =>
536 | request(app)
537 | .get('/users/123/isCool')
538 | .set('Accept', 'application/json')
539 | .expect('Content-Type', /json/)
540 | .expect(200, 'false'))
541 |
542 | it('should return 404 from a custom null resource', async () =>
543 | request(app)
544 | .get('/users/nulled')
545 | .set('Accept', 'application/json')
546 | .expect(404))
547 | })
548 |
549 | describe('sutro - caching', () => {
550 | let meCache
551 | const findByIdCache = {}
552 | const keyedCache: { yo?: any } = {}
553 | const config: SutroArgs = {
554 | resources: {
555 | user: {
556 | find: {
557 | execute: async () => users,
558 | cache: {
559 | header: () => ({ public: true, maxAge: '1hr' }),
560 | key: () => 'yo',
561 | get: async (opt, key) => keyedCache[key],
562 | set: async (opt, data, key) => (keyedCache[key] = data)
563 | }
564 | },
565 | findById: {
566 | execute: async (opt) => users[opt.userId],
567 | cache: {
568 | header: () => ({ public: true }),
569 | get: async (opt) => findByIdCache[opt.userId],
570 | set: async (opt, data) => (findByIdCache[opt.userId] = data)
571 | }
572 | },
573 | me: {
574 | execute: async () => ({ me: true }),
575 | cache: {
576 | header: { private: true },
577 | get: async () => meCache,
578 | set: async (opt, data) => (meCache = data)
579 | },
580 | http: {
581 | method: 'get',
582 | instance: false
583 | }
584 | }
585 | }
586 | }
587 | }
588 |
589 | const app = express().use(sutro(config))
590 |
591 | it('should cache a findById endpoint', async () => {
592 | await request(app)
593 | .get('/users/1')
594 | .set('Accept', 'application/json')
595 | .expect('Content-Type', /json/)
596 | .expect('Cache-Control', 'public')
597 | .expect(200, users[1])
598 |
599 | await request(app)
600 | .get('/users/2')
601 | .set('Accept', 'application/json')
602 | .expect('Content-Type', /json/)
603 | .expect('Cache-Control', 'public')
604 | .expect(200, users[2])
605 |
606 | await request(app)
607 | .get('/users/1')
608 | .set('Accept', 'application/json')
609 | .expect('Content-Type', /json/)
610 | .expect('Cache-Control', 'public')
611 | .expect(200, users[1])
612 |
613 | await request(app)
614 | .get('/users/2')
615 | .set('Accept', 'application/json')
616 | .expect('Content-Type', /json/)
617 | .expect('Cache-Control', 'public')
618 | .expect(200, users[2])
619 |
620 | should.exist(findByIdCache)
621 | findByIdCache.should.eql({
622 | 1: users[1],
623 | 2: users[2]
624 | })
625 | })
626 | it('should cache a custom resource', async () => {
627 | await request(app)
628 | .get('/users/me')
629 | .set('Accept', 'application/json')
630 | .expect('Content-Type', /json/)
631 | .expect('Cache-Control', 'private')
632 | .expect(200, { me: true })
633 |
634 | should.exist(meCache)
635 | meCache.should.eql({ me: true })
636 | })
637 | it('should cache with a key function', async () => {
638 | await request(app)
639 | .get('/users')
640 | .set('Accept', 'application/json')
641 | .expect('Content-Type', /json/)
642 | .expect('Cache-Control', 'public, max-age=3600')
643 | .expect(200, users)
644 |
645 | should.exist(keyedCache.yo)
646 | keyedCache.yo.should.eql(users)
647 | })
648 | })
649 |
650 | describe('sutro - rewriting requests', () => {
651 | const config: SutroArgs = {
652 | resources: {
653 | user: {
654 | find: () => [{ a: 1 }],
655 | findById: () => ({ a: 1 })
656 | }
657 | }
658 | }
659 | const app = express().use(rewriteLargeRequests).use(sutro(config))
660 |
661 | it('should rewrite a post for a resource find endpoint', async () =>
662 | request(app)
663 | .post('/users')
664 | .set('Accept', 'application/json')
665 | .set('X-HTTP-Method-Override', 'GET')
666 | .expect('Content-Type', /json/)
667 | .expect(200, config.resources.user.find()))
668 |
669 | it('should rewrite a post for a resource findById endpoint', async () =>
670 | request(app)
671 | .post('/users/1')
672 | .set('Accept', 'application/json')
673 | .set('X-HTTP-Method-Override', 'GET')
674 | .expect('Content-Type', /json/)
675 | .expect(200, config.resources.user.findById()))
676 | })
677 |
678 | describe('sutro - streaming requests', () => {
679 | const config: SutroArgs = {
680 | resources: {
681 | user: {
682 | find: ({ options }) => {
683 | const out = JSONStream.stringify()
684 | out.contentType = 'application/json'
685 | if (options.asyncError) {
686 | setTimeout(() => {
687 | out.emit('error', new Error('Bad news!'))
688 | }, 100)
689 | }
690 | if (options.error) {
691 | out.emit('error', new Error('Bad news!'))
692 | }
693 | setTimeout(() => {
694 | out.end({ a: 1 })
695 | }, 200)
696 | return out
697 | }
698 | }
699 | }
700 | }
701 | const app = express()
702 | .use(sutro(config))
703 | .use((err, req, res, _next) => {
704 | res.status(500).send({ error: err.message })
705 | })
706 |
707 | it('should stream a resource find endpoint', async () =>
708 | request(app)
709 | .get('/users')
710 | .set('Accept', 'application/json')
711 | .expect('Content-Type', /json/)
712 | .expect(200, [{ a: 1 }]))
713 | it('should handle async stream errors correctly', async () =>
714 | request(app)
715 | .get('/users')
716 | .query({ asyncError: true })
717 | .set('Accept', 'application/json')
718 | .expect('Content-Type', /json/)
719 | .expect(500, { error: 'Bad news!' }))
720 | it('should handle instant stream errors correctly', async () =>
721 | request(app)
722 | .get('/users')
723 | .query({ error: true })
724 | .set('Accept', 'application/json')
725 | .expect('Content-Type', /json/)
726 | .expect(500, { error: 'Bad news!' }))
727 | })
728 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2020"],
4 | "target": "ES2020",
5 | "module": "commonjs",
6 | "sourceMap": true,
7 | "strict": false,
8 | "rootDir": "src",
9 | "declaration": true,
10 | "esModuleInterop": true,
11 | "resolveJsonModule": true,
12 | "outDir": "dist"
13 | },
14 | "exclude": ["test"]
15 | }
16 |
--------------------------------------------------------------------------------