├── .npmignore ├── .gitignore ├── docs ├── assets │ └── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png ├── globals.html ├── modules │ ├── _exceptions_.html │ └── _related_.html ├── interfaces │ ├── _helpers_normalization_.basenormalizeroptions.html │ └── _index_.relatedliteral.html └── classes │ ├── _exceptions_.cacheerror.html │ └── _exceptions_.attributeerror.html ├── .vscode └── settings.json ├── dist ├── dom │ └── rest-resource.min.js.map ├── util.d.ts ├── exceptions.d.ts ├── util.js.map ├── exceptions.js.map ├── helpers │ ├── normalization.d.ts │ ├── normalization.js.map │ └── normalization.js ├── util.js ├── exceptions.js ├── client.d.ts ├── related.d.ts ├── client.js.map ├── related.js.map ├── client.js └── index.d.ts ├── .prettierrc ├── tests ├── specs │ ├── 000_environment.spec.ts │ ├── 003_client.spec.ts │ ├── 005_validation.spec.ts │ ├── 004_normalization.spec.ts │ ├── 002_related.spec.ts │ └── 001_resource.spec.ts ├── repl.ts └── index.ts ├── examples ├── simple.js ├── related.js └── todos.html ├── .circleci └── config.yml ├── src ├── exceptions.ts ├── util.ts ├── helpers │ └── normalization.ts ├── client.ts └── related.ts ├── LICENSE ├── tsconfig.json ├── webpack.config.js └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | ./docs 2 | ./examples -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | tests/fixtures-local.json 4 | tests/scratch* 5 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseprime/rest-resource/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseprime/rest-resource/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseprime/rest-resource/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true, 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baseprime/rest-resource/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /dist/dom/rest-resource.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"rest-resource.min.js","sourceRoot":""} -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "printWidth": 300, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "arrowParens": "always", 7 | "tabWidth": 4, 8 | "semi": false, 9 | "singleQuote": true, 10 | "useTabs": false 11 | } -------------------------------------------------------------------------------- /tests/specs/000_environment.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BaseTestingResource } from '..' 3 | 4 | describe('Environment', () => { 5 | 6 | it('testing server is running', async () => { 7 | let response = await BaseTestingResource.client.get('/users') 8 | expect(response.status, 'Testing server is not running. Run `npm run test-server`').to.equal(200) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | const Resource = require('../dist').default 2 | const Client = require('../dist/client').DefaultClient 3 | 4 | const UserResource = Resource.extend({ 5 | endpoint: '/users', 6 | client: new Client('https://jsonplaceholder.typicode.com') 7 | }) 8 | 9 | UserResource.list() 10 | .then((response) => { 11 | response.resources.forEach((resource) => { 12 | console.log(resource.get('name')) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /dist/util.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes an input and camelizes it 3 | * @param str 4 | */ 5 | export declare function camelize(str: string): string; 6 | /** 7 | * This is a very quick and primitive implementation of RFC 4122 UUID 8 | * Creates a basic variant UUID 9 | * Warning: Shouldn't be used of N >> 1e9 10 | */ 11 | export declare function uuidWeak(): string; 12 | export declare function getContentTypeWeak(value: any): any; 13 | export declare function urlStringify(object: any): string; 14 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "package.json" }} 12 | - v1-dependencies- 13 | - run: npm install 14 | - save_cache: 15 | paths: 16 | - node_modules 17 | key: v1-dependencies-{{ checksum "package.json" }} 18 | - run: npm run test-standalone 19 | -------------------------------------------------------------------------------- /dist/exceptions.d.ts: -------------------------------------------------------------------------------- 1 | export declare class BaseError extends Error { 2 | name: string; 3 | } 4 | export declare class ImproperlyConfiguredError extends BaseError { 5 | name: string; 6 | } 7 | export declare class CacheError extends BaseError { 8 | name: string; 9 | } 10 | export declare class AttributeError extends BaseError { 11 | name: string; 12 | } 13 | export declare class ValidationError extends BaseError { 14 | name: string; 15 | field: string; 16 | constructor(fieldOrArray: string | Error[], message?: string); 17 | } 18 | -------------------------------------------------------------------------------- /examples/related.js: -------------------------------------------------------------------------------- 1 | const Resource = require('../dist').default 2 | const Client = require('../dist/client').DefaultClient 3 | 4 | const BaseResource = Resource.extend({ 5 | client: new Client('https://jsonplaceholder.typicode.com') 6 | }) 7 | 8 | const UserResource = BaseResource.extend({ 9 | endpoint: '/users' 10 | }) 11 | 12 | const TodoResource = BaseResource.extend({ 13 | endpoint: '/todos', 14 | related: { 15 | userId: UserResource 16 | } 17 | }) 18 | 19 | TodoResource.list() 20 | .then((response) => { 21 | response.resources.forEach(async (resource) => { 22 | let title = await resource.resolveAttribute('title') 23 | let author = await resource.resolveAttribute('userId.name') 24 | let doneText = await resource.resolveAttribute('completed') ? 'x' : '-' 25 | console.log(`${doneText}\t${title}\n\t${author}\n\n`) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/exceptions.ts: -------------------------------------------------------------------------------- 1 | export class BaseError extends Error { 2 | name: string 3 | } 4 | 5 | export class ImproperlyConfiguredError extends BaseError { 6 | name: string = 'ImproperlyConfiguredError' 7 | } 8 | 9 | export class CacheError extends BaseError { 10 | name: string = 'CacheError' 11 | } 12 | 13 | export class AttributeError extends BaseError { 14 | name: string = 'AttributeError' 15 | } 16 | 17 | export class ValidationError extends BaseError { 18 | name: string = 'ValidationError' 19 | field: string 20 | constructor(fieldOrArray: string | Error[], message: string = '') { 21 | super(message) 22 | if (Array.isArray(fieldOrArray)) { 23 | this.message = fieldOrArray.join('\n') 24 | } else if (!this.message && fieldOrArray) { 25 | this.message = `${fieldOrArray}: This field is not valid` 26 | this.field = fieldOrArray 27 | } else if (this.message && 'string' === typeof fieldOrArray) { 28 | this.message = `${fieldOrArray}: ${this.message}` 29 | this.field = fieldOrArray 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dist/util.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAAA,0DAA8B;AAC9B,iCAAwC;AACxC,qCAA0C;AAE1C;;;GAGG;AACH,SAAgB,QAAQ,CAAC,GAAW;IAChC,OAAO,GAAG,CAAC,OAAO,CAAC,yBAAyB,EAAE,UAAC,KAAU,EAAE,KAAa;QACpE,IAAI,CAAC,KAAK,KAAK,CAAC;YAAE,OAAO,EAAE,CAAA;QAC3B,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAA;IACjE,CAAC,CAAC,CAAA;AACN,CAAC;AALD,4BAKC;AAED;;;;GAIG;AACH,SAAgB,QAAQ;IACpB,OAAO,sCAAsC,CAAC,OAAO,CAAC,OAAO,EAAE,UAAC,SAAS;QACrE,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,KAAK,GAAG,SAAS,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;QACzD,OAAO,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;AACN,CAAC;AAND,4BAMC;AAED,SAAgB,kBAAkB,CAAC,KAAU;IACzC,IAAI,IAAI,GAAG,cAAM,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IACnC,IAAI,IAAI,GAAG,IAAI,CAAC,WAAW,CAAA;IAC3B,IAAI,IAAI,CAAC,SAAS,YAAY,eAAQ,EAAE;QACpC,OAAO,eAAQ,CAAA;KAClB;SAAM;QACH,OAAO,IAAI,CAAA;KACd;AACL,CAAC;AARD,gDAQC;AAED,SAAgB,YAAY,CAAC,MAAW;IACpC,OAAO,CAAC,IAAI,0BAAe,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;AACnD,CAAC;AAFD,oCAEC"} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Greg Sabia Tucker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/exceptions.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"exceptions.js","sourceRoot":"","sources":["../src/exceptions.ts"],"names":[],"mappings":";;;AAAA;IAA+B,qCAAK;IAApC;;IAEA,CAAC;IAAD,gBAAC;AAAD,CAAC,AAFD,CAA+B,KAAK,GAEnC;AAFY,8BAAS;AAItB;IAA+C,qDAAS;IAAxD;QAAA,qEAEC;QADG,UAAI,GAAW,2BAA2B,CAAA;;IAC9C,CAAC;IAAD,gCAAC;AAAD,CAAC,AAFD,CAA+C,SAAS,GAEvD;AAFY,8DAAyB;AAItC;IAAgC,sCAAS;IAAzC;QAAA,qEAEC;QADG,UAAI,GAAW,YAAY,CAAA;;IAC/B,CAAC;IAAD,iBAAC;AAAD,CAAC,AAFD,CAAgC,SAAS,GAExC;AAFY,gCAAU;AAIvB;IAAoC,0CAAS;IAA7C;QAAA,qEAEC;QADG,UAAI,GAAW,gBAAgB,CAAA;;IACnC,CAAC;IAAD,qBAAC;AAAD,CAAC,AAFD,CAAoC,SAAS,GAE5C;AAFY,wCAAc;AAI3B;IAAqC,2CAAS;IAG1C,yBAAY,YAA8B,EAAE,OAAoB;QAApB,wBAAA,EAAA,YAAoB;QAAhE,YACI,kBAAM,OAAO,CAAC,SAUjB;QAbD,UAAI,GAAW,iBAAiB,CAAA;QAI5B,IAAI,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE;YAC7B,KAAI,CAAC,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;SACzC;aAAM,IAAI,CAAC,KAAI,CAAC,OAAO,IAAI,YAAY,EAAE;YACtC,KAAI,CAAC,OAAO,GAAM,YAAY,8BAA2B,CAAA;YACzD,KAAI,CAAC,KAAK,GAAG,YAAY,CAAA;SAC5B;aAAM,IAAI,KAAI,CAAC,OAAO,IAAI,QAAQ,KAAK,OAAO,YAAY,EAAE;YACzD,KAAI,CAAC,OAAO,GAAM,YAAY,UAAK,KAAI,CAAC,OAAS,CAAA;YACjD,KAAI,CAAC,KAAK,GAAG,YAAY,CAAA;SAC5B;;IACL,CAAC;IACL,sBAAC;AAAD,CAAC,AAfD,CAAqC,SAAS,GAe7C;AAfY,0CAAe"} -------------------------------------------------------------------------------- /dist/helpers/normalization.d.ts: -------------------------------------------------------------------------------- 1 | export declare function normalizerFactory(name: T, options?: BaseNormalizerOptions): BaseNormalizer; 2 | export declare class BaseNormalizer { 3 | normalizeTo: Function; 4 | uniqueKey: string; 5 | nullable: boolean; 6 | constructor({ uniqueKey }?: BaseNormalizerOptions); 7 | getType(value: any): any; 8 | normalize(value: any): any; 9 | } 10 | export declare class StringNormalizer extends BaseNormalizer { 11 | } 12 | export declare class NumberNormalizer extends BaseNormalizer { 13 | nullable: boolean; 14 | normalizeTo: NumberConstructor; 15 | } 16 | export declare class BooleanNormalizer extends StringNormalizer { 17 | nullable: boolean; 18 | normalizeTo: BooleanConstructor; 19 | } 20 | export declare class CurrencyNormalizer extends NumberNormalizer { 21 | normalize(value: any): string | string[]; 22 | } 23 | export interface BaseNormalizerOptions { 24 | uniqueKey?: string; 25 | } 26 | export declare type NormalizerFunc = (value: any) => any; 27 | export declare type ValidNormalizer = BaseNormalizer | NormalizerFunc; 28 | export declare type NormalizerDict = Record; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "strictNullChecks": false, 8 | "strictFunctionTypes": false, 9 | "jsx": "preserve", 10 | "importHelpers": true, 11 | "declaration": true, 12 | "declarationDir": "./dist", 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "sourceMap": true, 18 | "noImplicitThis": false, 19 | "baseUrl": ".", 20 | "types": [ 21 | "mocha", 22 | "chai", 23 | "node" 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "src/*" 28 | ] 29 | }, 30 | "lib": [ 31 | "esnext", 32 | "dom", 33 | "dom.iterable", 34 | "scripthost" 35 | ] 36 | }, 37 | "include": [ 38 | "src/**/*.ts", 39 | "src/**/*.tsx", "tests/normalization.spec.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const package = require('./package'); 4 | 5 | module.exports = { 6 | entry: { 7 | 'rest-resource': './dist/index.js', 8 | 'rest-resource.min': './dist/index.js' 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, 'dist/dom'), 12 | filename: '[name].js', 13 | library: 'Resource', 14 | libraryTarget: 'var', 15 | libraryExport: 'default' 16 | }, 17 | devtool: 'source-map', 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.js$/, 22 | loader: 'babel-loader', 23 | query: { 24 | presets: [require.resolve('babel-preset-es2015')] 25 | } 26 | } 27 | ] 28 | }, 29 | plugins: [ 30 | new webpack.optimize.UglifyJsPlugin({ 31 | include: /\.min\.js$/, 32 | compress: { warnings: false } 33 | }), 34 | new webpack.BannerPlugin(`REST Resource\n${package.repository.url}\n\n@author ${package.author}\n@link ${package.link}\n@version ${package.version}\n\nReleased under MIT License. See LICENSE.txt or http://opensource.org/licenses/MIT`) 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import Resource from './index' 2 | import { first as _first } from 'lodash' 3 | import { URLSearchParams } from 'url-shim' 4 | 5 | /** 6 | * Takes an input and camelizes it 7 | * @param str 8 | */ 9 | export function camelize(str: string) { 10 | return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, (match: any, index: number) => { 11 | if (+match === 0) return '' 12 | return index == 0 ? match.toLowerCase() : match.toUpperCase() 13 | }) 14 | } 15 | 16 | /** 17 | * This is a very quick and primitive implementation of RFC 4122 UUID 18 | * Creates a basic variant UUID 19 | * Warning: Shouldn't be used of N >> 1e9 20 | */ 21 | export function uuidWeak() { 22 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character) => { 23 | let rand = (Math.random() * 16) | 0 24 | let value = character === 'x' ? rand : (rand & 0x3) | 0x8 25 | return value.toString(16) 26 | }) 27 | } 28 | 29 | export function getContentTypeWeak(value: any): any { 30 | let node = _first([].concat(value)) 31 | let Ctor = node.constructor 32 | if (Ctor.prototype instanceof Resource) { 33 | return Resource 34 | } else { 35 | return Ctor 36 | } 37 | } 38 | 39 | export function urlStringify(object: any): string { 40 | return (new URLSearchParams(object)).toString() 41 | } 42 | -------------------------------------------------------------------------------- /tests/repl.ts: -------------------------------------------------------------------------------- 1 | import { start } from 'repl' 2 | import Resource from '../src' 3 | import { DefaultClient } from '../src/client' 4 | 5 | class CustomClient extends DefaultClient { 6 | _wrapMethod(method: string, args: IArguments) { 7 | console.log(method.toUpperCase(), args[0], args[1] || {}) 8 | // @ts-ignore 9 | return DefaultClient.prototype[method.toLowerCase()].apply(this, args) 10 | } 11 | get() { 12 | return this._wrapMethod('GET', arguments) 13 | } 14 | post() { 15 | return this._wrapMethod('POST', arguments) 16 | } 17 | patch() { 18 | return this._wrapMethod('PATCH', arguments) 19 | } 20 | put() { 21 | return this._wrapMethod('PUT', arguments) 22 | } 23 | delete() { 24 | return this._wrapMethod('DELETE', arguments) 25 | } 26 | } 27 | 28 | class BaseResource extends Resource { 29 | static client = new CustomClient('https://jsonplaceholder.typicode.com') 30 | } 31 | 32 | class UserResource extends BaseResource { 33 | static endpoint = '/users' 34 | } 35 | 36 | class TodoResource extends BaseResource { 37 | static endpoint = '/todos' 38 | static related = { 39 | userId: UserResource 40 | } 41 | } 42 | 43 | Object.assign(start('> ').context, { 44 | Client: CustomClient, 45 | Resource: BaseResource, 46 | UserResource, 47 | TodoResource 48 | }) 49 | -------------------------------------------------------------------------------- /tests/specs/003_client.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { TestingClient, BaseTestingResource, PostResource, UserResource, CommentResource } from '..' 3 | 4 | 5 | describe('Client', () => { 6 | 7 | it('resource client classes/subclasses updates axios baseURL', async () => { 8 | class CustomClient extends TestingClient {} 9 | class CustomResource extends BaseTestingResource { 10 | static client: CustomClient = new CustomClient('/') 11 | } 12 | expect(CustomResource.client).to.be.instanceOf(CustomClient) 13 | CustomResource.client.hostname = 'http://some-nonexistend-domain' 14 | expect(CustomResource.client.config.baseURL).to.equal('http://some-nonexistend-domain') 15 | expect(CustomResource.client.axios.defaults.baseURL).to.equal('http://some-nonexistend-domain') 16 | class SomeExtendedResource extends CustomResource {} 17 | expect(SomeExtendedResource.client).to.be.instanceOf(CustomClient) 18 | expect(SomeExtendedResource.client.config.baseURL).to.equal('http://some-nonexistend-domain') 19 | expect(SomeExtendedResource.client.axios.defaults.baseURL).to.equal('http://some-nonexistend-domain') 20 | CustomResource.client.hostname = 'http://localhost' 21 | expect(SomeExtendedResource.client.hostname).to.equal('http://localhost') 22 | expect(SomeExtendedResource.client.config.baseURL).to.equal('http://localhost') 23 | expect(SomeExtendedResource.client.axios.defaults.baseURL).to.equal('http://localhost') 24 | }) 25 | 26 | }) 27 | -------------------------------------------------------------------------------- /dist/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var index_1 = tslib_1.__importDefault(require("./index")); 5 | var lodash_1 = require("lodash"); 6 | var url_shim_1 = require("url-shim"); 7 | /** 8 | * Takes an input and camelizes it 9 | * @param str 10 | */ 11 | function camelize(str) { 12 | return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { 13 | if (+match === 0) 14 | return ''; 15 | return index == 0 ? match.toLowerCase() : match.toUpperCase(); 16 | }); 17 | } 18 | exports.camelize = camelize; 19 | /** 20 | * This is a very quick and primitive implementation of RFC 4122 UUID 21 | * Creates a basic variant UUID 22 | * Warning: Shouldn't be used of N >> 1e9 23 | */ 24 | function uuidWeak() { 25 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (character) { 26 | var rand = (Math.random() * 16) | 0; 27 | var value = character === 'x' ? rand : (rand & 0x3) | 0x8; 28 | return value.toString(16); 29 | }); 30 | } 31 | exports.uuidWeak = uuidWeak; 32 | function getContentTypeWeak(value) { 33 | var node = lodash_1.first([].concat(value)); 34 | var Ctor = node.constructor; 35 | if (Ctor.prototype instanceof index_1.default) { 36 | return index_1.default; 37 | } 38 | else { 39 | return Ctor; 40 | } 41 | } 42 | exports.getContentTypeWeak = getContentTypeWeak; 43 | function urlStringify(object) { 44 | return (new url_shim_1.URLSearchParams(object)).toString(); 45 | } 46 | exports.urlStringify = urlStringify; 47 | //# sourceMappingURL=util.js.map -------------------------------------------------------------------------------- /dist/helpers/normalization.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"normalization.js","sourceRoot":"","sources":["../../src/helpers/normalization.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-resource", 3 | "version": "0.11.0", 4 | "description": "Simplify your REST API resources", 5 | "main": "./dist/index.js", 6 | "unpkg": "./dist/dom/rest-resource.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "test": "mocha -t 8000 -r ts-node/register ./tests/**/*.spec.ts", 10 | "test-standalone": "npm run serve-tests & (sleep 5 && npm test)", 11 | "build": "tsc && webpack && npm run generate-docs", 12 | "dev": "tsc --watch", 13 | "prettier": "./node_modules/prettier/bin-prettier.js --write ./src/**/*.ts ./src/*.ts", 14 | "repl": "ts-node ./tests/repl.ts", 15 | "generate-docs": "./node_modules/typedoc/bin/typedoc --out ./docs ./src/index.ts --excludeNotExported --excludePrivate", 16 | "reset-ts": "rm -r ./dist && ./node_modules/.bin/tsc", 17 | "scratch": "LOG_LEVEL=info ts-node -T --pretty ./tests/scratch.ts", 18 | "serve-tests": "cp ./tests/fixtures.json ./tests/fixtures-local.json && json-server -p ${TEST_PORT:-8099} --quiet --delay ${TEST_DELAY_MS:-0} ./tests/fixtures-local.json", 19 | "serve-docs": "./node_modules/serve/bin/serve.js ./docs/" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/baseprime/rest-resource.git" 24 | }, 25 | "keywords": [ 26 | "rest", 27 | "api", 28 | "model", 29 | "cache", 30 | "caching", 31 | "express", 32 | "restify", 33 | "vue" 34 | ], 35 | "author": "Greg Sabia Tucker (http://basepri.me)", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/baseprime/rest-resource/issues" 39 | }, 40 | "homepage": "https://github.com/baseprime/rest-resource#readme", 41 | "devDependencies": { 42 | "@types/chai": "^4.1.7", 43 | "@types/mocha": "^5.2.5", 44 | "babel-core": "^6.26.0", 45 | "babel-loader": "^6.4.1", 46 | "babel-preset-es2015": "^6.24.1", 47 | "chai": "^4.2.0", 48 | "json-server": "^0.14.2", 49 | "mocha": "^5.2.0", 50 | "prettier": "^1.16.2", 51 | "serve": "^11.2.0", 52 | "ts-node": "^8.0.2", 53 | "typedoc": "^0.15.0", 54 | "webpack": "^3.4.1" 55 | }, 56 | "dependencies": { 57 | "@types/lodash": "^4.14.137", 58 | "@types/node": "^10.12.19", 59 | "assert": "^1.4.1", 60 | "axios": "^0.21.4", 61 | "lodash": "^4.17.15", 62 | "tslib": "^1.9.3", 63 | "typescript": "^3.3.3", 64 | "url-shim": "^1.0.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /dist/exceptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var BaseError = /** @class */ (function (_super) { 5 | tslib_1.__extends(BaseError, _super); 6 | function BaseError() { 7 | return _super !== null && _super.apply(this, arguments) || this; 8 | } 9 | return BaseError; 10 | }(Error)); 11 | exports.BaseError = BaseError; 12 | var ImproperlyConfiguredError = /** @class */ (function (_super) { 13 | tslib_1.__extends(ImproperlyConfiguredError, _super); 14 | function ImproperlyConfiguredError() { 15 | var _this = _super !== null && _super.apply(this, arguments) || this; 16 | _this.name = 'ImproperlyConfiguredError'; 17 | return _this; 18 | } 19 | return ImproperlyConfiguredError; 20 | }(BaseError)); 21 | exports.ImproperlyConfiguredError = ImproperlyConfiguredError; 22 | var CacheError = /** @class */ (function (_super) { 23 | tslib_1.__extends(CacheError, _super); 24 | function CacheError() { 25 | var _this = _super !== null && _super.apply(this, arguments) || this; 26 | _this.name = 'CacheError'; 27 | return _this; 28 | } 29 | return CacheError; 30 | }(BaseError)); 31 | exports.CacheError = CacheError; 32 | var AttributeError = /** @class */ (function (_super) { 33 | tslib_1.__extends(AttributeError, _super); 34 | function AttributeError() { 35 | var _this = _super !== null && _super.apply(this, arguments) || this; 36 | _this.name = 'AttributeError'; 37 | return _this; 38 | } 39 | return AttributeError; 40 | }(BaseError)); 41 | exports.AttributeError = AttributeError; 42 | var ValidationError = /** @class */ (function (_super) { 43 | tslib_1.__extends(ValidationError, _super); 44 | function ValidationError(fieldOrArray, message) { 45 | if (message === void 0) { message = ''; } 46 | var _this = _super.call(this, message) || this; 47 | _this.name = 'ValidationError'; 48 | if (Array.isArray(fieldOrArray)) { 49 | _this.message = fieldOrArray.join('\n'); 50 | } 51 | else if (!_this.message && fieldOrArray) { 52 | _this.message = fieldOrArray + ": This field is not valid"; 53 | _this.field = fieldOrArray; 54 | } 55 | else if (_this.message && 'string' === typeof fieldOrArray) { 56 | _this.message = fieldOrArray + ": " + _this.message; 57 | _this.field = fieldOrArray; 58 | } 59 | return _this; 60 | } 61 | return ValidationError; 62 | }(BaseError)); 63 | exports.ValidationError = ValidationError; 64 | //# sourceMappingURL=exceptions.js.map -------------------------------------------------------------------------------- /src/helpers/normalization.ts: -------------------------------------------------------------------------------- 1 | import Resource from '../index' 2 | 3 | export function normalizerFactory(name: T, options: BaseNormalizerOptions = {}): BaseNormalizer { 4 | try { 5 | return new exports[name](options) 6 | } catch (e) { 7 | if (e instanceof TypeError) { 8 | throw new Error(`${name} is not a valid normalizer instance. Please see ${__filename} for valid choices`) 9 | } else { 10 | throw e 11 | } 12 | } 13 | } 14 | 15 | export class BaseNormalizer { 16 | normalizeTo: Function = String 17 | uniqueKey: string = 'id' 18 | nullable: boolean = true 19 | 20 | constructor({ uniqueKey = 'id' }: BaseNormalizerOptions = {}) { 21 | this.uniqueKey = uniqueKey 22 | } 23 | 24 | getType(value: any) { 25 | if (value === null || value === undefined) { 26 | return false 27 | } 28 | 29 | return value.constructor 30 | } 31 | 32 | normalize(value: any): any { 33 | let Ctor = this.getType(value) 34 | 35 | if (!Ctor && !this.nullable) { 36 | return this.normalizeTo() 37 | } else if (!Ctor) { 38 | return value 39 | } 40 | 41 | if (Ctor === this.normalizeTo) { 42 | return value 43 | } else if (Ctor === Resource) { 44 | return value.id 45 | } else if (Ctor === Object && Ctor !== null) { 46 | return this.normalize(value[this.uniqueKey]) 47 | } else if (Ctor === Array) { 48 | return value.map((item: any) => this.normalize(item)) 49 | } else if (Ctor === Boolean) { 50 | return Boolean(value) ? 'true' : '' 51 | } else { 52 | return this.normalizeTo(value) 53 | } 54 | } 55 | } 56 | 57 | export class StringNormalizer extends BaseNormalizer {} 58 | 59 | export class NumberNormalizer extends BaseNormalizer { 60 | nullable = false 61 | normalizeTo = Number 62 | } 63 | 64 | export class BooleanNormalizer extends StringNormalizer { 65 | nullable = false 66 | normalizeTo = Boolean 67 | } 68 | 69 | export class CurrencyNormalizer extends NumberNormalizer { 70 | normalize(value: any): string | string[] { 71 | let superVal = super.normalize(value) 72 | let intermediateVal = [].concat(superVal).map((val) => Number(val).toFixed(2)) 73 | return Array.isArray(superVal) ? intermediateVal : intermediateVal.shift() 74 | } 75 | } 76 | 77 | export interface BaseNormalizerOptions { 78 | uniqueKey?: string 79 | } 80 | 81 | export type NormalizerFunc = (value: any) => any 82 | 83 | export type ValidNormalizer = BaseNormalizer | NormalizerFunc 84 | 85 | export type NormalizerDict = Record 86 | -------------------------------------------------------------------------------- /tests/specs/005_validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { UserResource } from '..' 3 | import { ValidationError as ValidationErrorOriginal } from '../../src/exceptions' 4 | 5 | describe('Validation', () => { 6 | const badPhoneNumber = '0118 999 88199 9119 725... 3' 7 | const goodPhoneNumber = '4155551234' 8 | const badString = 'Th(s*s3' 9 | const goodString = 'abc123' 10 | 11 | function phoneValidator(value: string, instance: any, ValidationError: any) { 12 | let regExp = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/im 13 | if (!regExp.test(value)) { 14 | throw new ValidationError('phone', 'Phone number is invalid') 15 | } 16 | } 17 | 18 | function isAlphaNumeric(value: string, instance: any, ValidationError: any) { 19 | let regExp = /^[a-z0-9]+$/i 20 | if (!regExp.test(value)) { 21 | throw new ValidationError('', 'This field must be alphanumeric') 22 | } 23 | } 24 | 25 | const CustomUserResource = UserResource.extend({ 26 | _cache: {}, 27 | validation: { 28 | phone: [phoneValidator, isAlphaNumeric], 29 | username: isAlphaNumeric 30 | }, 31 | }) 32 | 33 | const CustomUserResourceDefinedWithFunction = UserResource.extend({ 34 | _cache: {}, 35 | validation() { 36 | return { 37 | phone: phoneValidator, 38 | } 39 | }, 40 | }) 41 | 42 | let failsPhoneValidation = new CustomUserResource({ phone: badPhoneNumber }) 43 | let passesPhoneValidation = new CustomUserResource({ phone: goodPhoneNumber }) 44 | let failsStringValidation = new CustomUserResource({ username: badString }) 45 | let typicalResource = new CustomUserResource({ phone: goodPhoneNumber, username: goodString }) 46 | 47 | it('validators initialize correctly', async () => { 48 | let phoneErrors = failsPhoneValidation.validate() 49 | let stringErrors = failsStringValidation.validate() 50 | expect(phoneErrors.length).to.equal(2) 51 | expect(stringErrors.length).to.equal(2) 52 | expect(passesPhoneValidation.validate()).to.be.empty 53 | expect(typicalResource.validate()).to.be.empty 54 | }) 55 | 56 | it('validators can be set with a function', async () => { 57 | let badResource = new CustomUserResourceDefinedWithFunction({ phone: badPhoneNumber }) 58 | let errors = badResource.validate() 59 | expect(errors.length).to.equal(1) 60 | expect(errors[0].message).to.contain('Phone number is invalid') 61 | }) 62 | 63 | it('raises exceptions', async () => { 64 | let phoneErrors = failsPhoneValidation.validate() 65 | phoneErrors.forEach((e) => { 66 | expect(ValidationErrorOriginal.name === 'ValidationError').to.be.true 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /examples/todos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | REST Resource Example 5 | 6 | 7 | 8 |
9 |

Todos:

10 |

Loading...

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | 29 |
 TitleAuthor
  20 | 21 | {{ todo.title }} 22 | 23 | 24 | {{ todo.title }} 25 | 26 | {{ todo.author }}
30 |
31 | 32 | 33 | 82 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultClient, AxiosRequestConfig, RequestConfig } from '../src/client' 2 | import Resource from '../src/index' 3 | export const TEST_PORT = process.env.TEST_PORT || 8099 4 | 5 | export function axiosRequestLogger(request: AxiosRequestConfig) { 6 | return new Promise((resolve) => { 7 | console.log(`${request.method.toUpperCase()} ${request.url}`) 8 | if (request.data) { 9 | console.log(`${JSON.stringify(request.data, null, ' ')}`) 10 | } 11 | resolve(request) 12 | }) 13 | } 14 | 15 | export function createRequestTracker(client: TestingClient) { 16 | return function(request: AxiosRequestConfig) { 17 | return new Promise((resolve) => { 18 | client.requestTracker[request.url] = (client.requestTracker[request.url] || 0) + 1 19 | resolve(request) 20 | }) 21 | } 22 | } 23 | 24 | export class TestingClient extends DefaultClient { 25 | requestTracker: any = {} 26 | logging: boolean = false 27 | 28 | constructor(baseURL: string, options?: RequestConfig) { 29 | super(baseURL, options) 30 | this.axios.interceptors.request.use(createRequestTracker(this)) 31 | if(this.logging) { 32 | this.axios.interceptors.request.use(axiosRequestLogger) 33 | } 34 | } 35 | } 36 | 37 | export const BaseTestingResource = Resource.extend({ 38 | client: new TestingClient(`http://localhost:${TEST_PORT}`) 39 | }) 40 | 41 | export const UserResource = BaseTestingResource.extend({ 42 | endpoint: '/users' 43 | }) 44 | 45 | export const TodoResource = BaseTestingResource.extend({ 46 | endpoint: '/todos', 47 | related: { 48 | user: UserResource 49 | } 50 | }) 51 | 52 | export const PostResource = BaseTestingResource.extend({ 53 | endpoint: '/posts', 54 | related: { 55 | user: UserResource 56 | } 57 | }) 58 | 59 | export const GroupResource = BaseTestingResource.extend({ 60 | endpoint: '/groups', 61 | related: { 62 | owner: UserResource, 63 | users: UserResource, 64 | todos: TodoResource 65 | } 66 | }) 67 | 68 | export const CommentResource = BaseTestingResource.extend({ 69 | endpoint: '/comments', 70 | related: { 71 | post: PostResource, 72 | user: UserResource 73 | } 74 | }) 75 | 76 | export const CommentMeta = BaseTestingResource.extend({ 77 | endpoint: '/commentmeta', 78 | related: { 79 | comment: CommentResource 80 | } 81 | }) 82 | 83 | export const CommentMetaMeta = BaseTestingResource.extend({ 84 | endpoint: '/commentmetameta', 85 | related: { 86 | commentmeta: CommentMeta 87 | } 88 | }) 89 | 90 | export const CommentMetaMetaMeta = BaseTestingResource.extend({ 91 | endpoint: '/commentmetametameta', 92 | related: { 93 | commentmetameta: CommentMetaMeta 94 | } 95 | }) 96 | 97 | export const CommentMetaMetaMetaMeta = BaseTestingResource.extend({ 98 | endpoint: '/commentmetametametameta', 99 | related: { 100 | commentmetametameta: CommentMetaMetaMeta 101 | } 102 | }) 103 | -------------------------------------------------------------------------------- /tests/specs/004_normalization.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { UserResource } from '..' 3 | import { BaseNormalizer, NumberNormalizer, CurrencyNormalizer, BooleanNormalizer } from '../../src/helpers/normalization' 4 | 5 | describe('Normalization', () => { 6 | 7 | class CustomUserResource extends UserResource { 8 | static normalization = { 9 | followers: new NumberNormalizer() 10 | } 11 | } 12 | 13 | it('normalizers work with Resources', async () => { 14 | let resource = new CustomUserResource({ 15 | followers: '5' // Normalizes to a number 16 | }) 17 | 18 | expect(typeof resource.attributes.followers).to.equal('number') 19 | resource.attributes.followers = '6' 20 | expect(typeof resource.attributes.followers).to.equal('number') 21 | expect(resource.changes.followers).to.equal(6) 22 | }) 23 | 24 | it('base normalizer normalizes correctly', async () => { 25 | let normalizer = new BaseNormalizer() 26 | expect(normalizer.normalize(123)).to.equal('123') 27 | expect(normalizer.normalize({ id: 123 })).to.equal('123') 28 | expect(normalizer.normalize(false)).to.equal('') 29 | expect(normalizer.normalize(true)).to.equal('true') 30 | expect(normalizer.normalize(undefined)).to.equal(undefined) 31 | expect(normalizer.normalize(null)).to.equal(null) 32 | }) 33 | 34 | it('non nullable normalizer normalizes undefined and null', async () => { 35 | let normalizer = new BaseNormalizer() 36 | normalizer.nullable = false 37 | expect(normalizer.normalize(undefined)).to.equal('') 38 | expect(normalizer.normalize(null)).to.equal('') 39 | }) 40 | 41 | it('normalizer: numbers', async () => { 42 | let normalizer = new NumberNormalizer() 43 | expect(normalizer.normalize('123')).to.equal(123) 44 | expect(normalizer.normalize('-123')).to.equal(-123) 45 | expect(normalizer.normalize('-123.456')).to.equal(-123.456) 46 | expect(normalizer.normalize({ id: '123' })).to.equal(123) 47 | expect(normalizer.normalize(undefined)).to.equal(0) 48 | normalizer.nullable = true 49 | expect(normalizer.normalize(undefined)).to.equal(undefined) 50 | }) 51 | 52 | it('normalizer: currency', async () => { 53 | let normalizer = new CurrencyNormalizer() 54 | expect(normalizer.normalize('123')).to.equal('123.00') 55 | expect(normalizer.normalize('123.4567')).to.equal('123.46') 56 | expect(normalizer.normalize(['123.4567', '000000'])).to.eql(['123.46', '0.00']) 57 | expect(normalizer.normalize('')).to.equal('0.00') 58 | expect(normalizer.normalize(null)).to.equal('0.00') 59 | expect(normalizer.normalize(undefined)).to.equal('0.00') 60 | }) 61 | 62 | it('normalizer: boolean', async () => { 63 | let normalizer = new BooleanNormalizer() 64 | expect(normalizer.normalize('123')).to.equal(true) 65 | expect(normalizer.normalize([1, '1', 0, false, true])).to.eql([true, true, false, false, true]) 66 | expect(normalizer.normalize(undefined)).to.equal(false) 67 | expect(normalizer.normalize(null)).to.equal(false) 68 | }) 69 | 70 | }) 71 | -------------------------------------------------------------------------------- /dist/client.d.ts: -------------------------------------------------------------------------------- 1 | import Resource, { ListResponse } from './index'; 2 | import { AxiosPromise, AxiosRequestConfig, AxiosResponse, AxiosError, AxiosInstance } from 'axios'; 3 | export * from 'axios'; 4 | export interface RequestConfig extends AxiosRequestConfig { 5 | useCache?: boolean; 6 | query?: any; 7 | } 8 | export interface ResourceResponse extends Record { 9 | response: AxiosResponse; 10 | resources: T[]; 11 | count?: () => number; 12 | pages?: () => number; 13 | currentPage?: () => number; 14 | perPage?: () => number; 15 | next?: () => Promise>; 16 | previous?: () => Promise>; 17 | } 18 | export declare type ExtractorFunction = (result: ResourceResponse['response']) => ResourceResponse; 19 | export declare class BaseClient { 20 | axios: AxiosInstance; 21 | config: AxiosRequestConfig; 22 | constructor(baseURL: string, config?: AxiosRequestConfig); 23 | hostname: string; 24 | static extend(this: U, classProps: T): U & T; 25 | negotiateContent(ResourceClass: T): ExtractorFunction>; 26 | /** 27 | * Client.prototype.list() and Client.prototype.detail() are the primary purpose of defining these here. Simply runs a GET on the list route path (eg. /users) and negotiates the content 28 | * @param ResourceClass 29 | * @param options 30 | */ 31 | list(ResourceClass: T, options?: RequestConfig): ListResponse; 32 | /** 33 | * Client.prototype.detail() and Client.prototype.list() are the primary purpose of defining these here. Simply runs a GET on the detail route path (eg. /users/123) and negotiates the content 34 | * @param ResourceClass 35 | * @param options 36 | */ 37 | detail(ResourceClass: T, id: string, options?: RequestConfig): Promise, any>>; 38 | get(path: string, options?: AxiosRequestConfig): AxiosPromise; 39 | put(path: string, body?: any, options?: AxiosRequestConfig): AxiosPromise; 40 | post(path: string, body?: any, options?: AxiosRequestConfig): AxiosPromise; 41 | patch(path: string, body?: any, options?: AxiosRequestConfig): AxiosPromise; 42 | delete(path: string, options?: AxiosRequestConfig): AxiosPromise; 43 | head(path: string, options?: AxiosRequestConfig): AxiosPromise; 44 | options(path: string, options?: AxiosRequestConfig): AxiosPromise; 45 | bindMethodsToPath(relativePath: string): { 46 | get: (options?: AxiosRequestConfig) => AxiosPromise<{}>; 47 | post: (body?: any, options?: AxiosRequestConfig) => AxiosPromise<{}>; 48 | put: (body?: any, options?: AxiosRequestConfig) => AxiosPromise<{}>; 49 | patch: (body?: any, options?: AxiosRequestConfig) => AxiosPromise<{}>; 50 | head: (options?: AxiosRequestConfig) => AxiosPromise<{}>; 51 | options: (options?: AxiosRequestConfig) => AxiosPromise<{}>; 52 | delete: (options?: AxiosRequestConfig) => AxiosPromise<{}>; 53 | }; 54 | onError(exception: Error | AxiosError): any; 55 | } 56 | export declare class DefaultClient extends BaseClient { 57 | } 58 | export declare class JWTBearerClient extends BaseClient { 59 | token: string; 60 | constructor(baseURL: string, token?: string, options?: RequestConfig); 61 | getTokenPayload(): any; 62 | tokenIsExpired(): boolean; 63 | tokenIsValid(): boolean; 64 | } 65 | -------------------------------------------------------------------------------- /dist/helpers/normalization.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var index_1 = tslib_1.__importDefault(require("../index")); 5 | function normalizerFactory(name, options) { 6 | if (options === void 0) { options = {}; } 7 | try { 8 | return new exports[name](options); 9 | } 10 | catch (e) { 11 | if (e instanceof TypeError) { 12 | throw new Error(name + " is not a valid normalizer instance. Please see " + __filename + " for valid choices"); 13 | } 14 | else { 15 | throw e; 16 | } 17 | } 18 | } 19 | exports.normalizerFactory = normalizerFactory; 20 | var BaseNormalizer = /** @class */ (function () { 21 | function BaseNormalizer(_a) { 22 | var _b = (_a === void 0 ? {} : _a).uniqueKey, uniqueKey = _b === void 0 ? 'id' : _b; 23 | this.normalizeTo = String; 24 | this.uniqueKey = 'id'; 25 | this.nullable = true; 26 | this.uniqueKey = uniqueKey; 27 | } 28 | BaseNormalizer.prototype.getType = function (value) { 29 | if (value === null || value === undefined) { 30 | return false; 31 | } 32 | return value.constructor; 33 | }; 34 | BaseNormalizer.prototype.normalize = function (value) { 35 | var _this = this; 36 | var Ctor = this.getType(value); 37 | if (!Ctor && !this.nullable) { 38 | return this.normalizeTo(); 39 | } 40 | else if (!Ctor) { 41 | return value; 42 | } 43 | if (Ctor === this.normalizeTo) { 44 | return value; 45 | } 46 | else if (Ctor === index_1.default) { 47 | return value.id; 48 | } 49 | else if (Ctor === Object && Ctor !== null) { 50 | return this.normalize(value[this.uniqueKey]); 51 | } 52 | else if (Ctor === Array) { 53 | return value.map(function (item) { return _this.normalize(item); }); 54 | } 55 | else if (Ctor === Boolean) { 56 | return Boolean(value) ? 'true' : ''; 57 | } 58 | else { 59 | return this.normalizeTo(value); 60 | } 61 | }; 62 | return BaseNormalizer; 63 | }()); 64 | exports.BaseNormalizer = BaseNormalizer; 65 | var StringNormalizer = /** @class */ (function (_super) { 66 | tslib_1.__extends(StringNormalizer, _super); 67 | function StringNormalizer() { 68 | return _super !== null && _super.apply(this, arguments) || this; 69 | } 70 | return StringNormalizer; 71 | }(BaseNormalizer)); 72 | exports.StringNormalizer = StringNormalizer; 73 | var NumberNormalizer = /** @class */ (function (_super) { 74 | tslib_1.__extends(NumberNormalizer, _super); 75 | function NumberNormalizer() { 76 | var _this = _super !== null && _super.apply(this, arguments) || this; 77 | _this.nullable = false; 78 | _this.normalizeTo = Number; 79 | return _this; 80 | } 81 | return NumberNormalizer; 82 | }(BaseNormalizer)); 83 | exports.NumberNormalizer = NumberNormalizer; 84 | var BooleanNormalizer = /** @class */ (function (_super) { 85 | tslib_1.__extends(BooleanNormalizer, _super); 86 | function BooleanNormalizer() { 87 | var _this = _super !== null && _super.apply(this, arguments) || this; 88 | _this.nullable = false; 89 | _this.normalizeTo = Boolean; 90 | return _this; 91 | } 92 | return BooleanNormalizer; 93 | }(StringNormalizer)); 94 | exports.BooleanNormalizer = BooleanNormalizer; 95 | var CurrencyNormalizer = /** @class */ (function (_super) { 96 | tslib_1.__extends(CurrencyNormalizer, _super); 97 | function CurrencyNormalizer() { 98 | return _super !== null && _super.apply(this, arguments) || this; 99 | } 100 | CurrencyNormalizer.prototype.normalize = function (value) { 101 | var superVal = _super.prototype.normalize.call(this, value); 102 | var intermediateVal = [].concat(superVal).map(function (val) { return Number(val).toFixed(2); }); 103 | return Array.isArray(superVal) ? intermediateVal : intermediateVal.shift(); 104 | }; 105 | return CurrencyNormalizer; 106 | }(NumberNormalizer)); 107 | exports.CurrencyNormalizer = CurrencyNormalizer; 108 | //# sourceMappingURL=normalization.js.map -------------------------------------------------------------------------------- /dist/related.d.ts: -------------------------------------------------------------------------------- 1 | import Resource, { DetailOpts } from './index'; 2 | export declare type RelatedObjectValue = string | string[] | number | number[] | Record | Record[]; 3 | export declare type CollectionValue = Record[]; 4 | export default class RelatedManager { 5 | to: T; 6 | value: RelatedObjectValue; 7 | many: boolean; 8 | /** 9 | * Is `true` when `resolve()` is called and first page of results loads up to `this.batchSize` objects 10 | */ 11 | resolved: boolean; 12 | /** 13 | * Deferred promises when `this.resolve()` hits the max requests in `this.batchSize` 14 | */ 15 | deferred: (() => Promise>)[]; 16 | /** 17 | * List of stringified Primary Keys, even if `this.value` is a list of objects, or Resource instances 18 | */ 19 | primaryKeys: string[]; 20 | /** 21 | * When sending `this.resolve()`, only send out the first `n` requests where `n` is `this.batchSize`. You 22 | * can call `this.all()` to recursively get all objects 23 | */ 24 | batchSize: number; 25 | _resources: Record>; 26 | constructor(to: T, value: RelatedObjectValue); 27 | /** 28 | * Check if values exist on manager 29 | */ 30 | hasValues(): boolean; 31 | canAutoResolve(): boolean; 32 | /** 33 | * Return a constructor so we can guess the content type. For example, if an object literal 34 | * is passed, this function should return `Object`, and it's likely one single object literal representing attributes. 35 | * If the constructor is an `Array`, then all we know is that there are many of these sub items (in which case, we're 36 | * taking the first node of that array and using that node to guess). If it's a `Number`, then it's likely 37 | * that it's just a primary key. If it's a `Resource` instance, it should return `Resource`. Etc. 38 | * @returns Function 39 | */ 40 | getValueContentType(): any; 41 | /** 42 | * Get the current value and the content type and turn it into a list of primary keys 43 | * @returns String 44 | */ 45 | getPrimaryKeys(): string[]; 46 | /** 47 | * Get unique key property from object literal and turn it into a string 48 | * @param object Object 49 | */ 50 | getIdFromObject(object: any): string; 51 | /** 52 | * Get unique key from resource instance 53 | * @param resource Resource 54 | */ 55 | getIdFromResource(resource: InstanceType): string; 56 | /** 57 | * Get a single resource from the endpoint given an ID 58 | * @param id String | Number 59 | */ 60 | getOne(id: string | number, options?: DetailOpts): Promise>; 61 | /** 62 | * Same as getOne but allow lookup by index 63 | * @param index Number 64 | */ 65 | getOneAtIndex(index: number): Promise>; 66 | /** 67 | * Get all loaded resources relevant to this relation 68 | * Like manager.resources getter except it won't throw an AttributeError and will return with any loaded resources if its ID is listed in `this.primaryKeys` 69 | */ 70 | getAllLoaded(): InstanceType[]; 71 | /** 72 | * Primary function of the RelatedManager -- get some objects (`this.primaryKeys`) related to some 73 | * other Resource (`this.to` instance). Load the first n objects (`this.batchSize`) and set `this.resolved = true`. 74 | * Subsequent calls may be required to get all objects in `this.primaryKeys` because there is an inherent 75 | * limit to how many requests that can be made at one time. If you want to remove this limit, set `this.batchSize` to `Infinity` 76 | * @param options DetailOpts 77 | */ 78 | resolve(options?: DetailOpts): Promise[]>; 79 | next(options?: DetailOpts): Promise[]>; 80 | /** 81 | * Calls pending functions in `this.deferred` until it's empty. Runs `this.resolve()` first if it hasn't been ran yet 82 | * @param options DetailOpts 83 | */ 84 | all(options?: DetailOpts): Promise[]>; 85 | resolveFromObjectValue(): boolean; 86 | /** 87 | * Add a resource to the manager 88 | * @param resource Resource instance 89 | */ 90 | add(resource: InstanceType): void; 91 | /** 92 | * Create a copy of `this` except with new value(s) 93 | * @param value 94 | */ 95 | fromValue(this: InstanceType, value: any): InstanceType; 96 | /** 97 | * Getter -- get `this._resources` but make sure we've actually retrieved the objects first 98 | * Throws AttributeError if `this.resolve()` hasn't finished 99 | */ 100 | readonly resources: InstanceType[]; 101 | /** 102 | * Getter -- Same as manager.resources except returns first node 103 | */ 104 | readonly resource: InstanceType; 105 | readonly length: number; 106 | toString(): string; 107 | toJSON(): any; 108 | } 109 | -------------------------------------------------------------------------------- /dist/client.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /tests/specs/002_related.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { GroupResource, TodoResource, PostResource, UserResource, CommentResource, CommentMetaMetaMetaMeta } from '..' 3 | 4 | describe('Related', () => { 5 | it('managers next() all() and resolve() work correctly', async () => { 6 | const group = await GroupResource.detail('2') 7 | const todosManager = group.managers.todos 8 | const originalBatchSize = todosManager.batchSize 9 | const thisBatchSize = 20 10 | const expectedTodosInGroup = 90 11 | const compareIDOfTodoOnFirstPage = String(thisBatchSize + 1) 12 | const compareIDOfTodoOnLastPage = String(expectedTodosInGroup) 13 | todosManager.batchSize = thisBatchSize 14 | expect(todosManager.length).to.equal(expectedTodosInGroup) // If this returns false, make sure the group ID 2 listed in fixtures.json has exactly 90 IDs in it! 15 | expect(GroupResource.client.requestTracker[TodoResource.getDetailRoutePath(compareIDOfTodoOnFirstPage)]).to.be.undefined 16 | // Calling resolve() should only get the first page 17 | await todosManager.resolve() 18 | expect(GroupResource.client.requestTracker[TodoResource.getDetailRoutePath(compareIDOfTodoOnFirstPage)]).to.equal(1) 19 | expect(GroupResource.client.requestTracker[TodoResource.getDetailRoutePath(compareIDOfTodoOnLastPage)]).to.be.undefined 20 | await todosManager.next() 21 | expect(GroupResource.client.requestTracker[TodoResource.getDetailRoutePath(compareIDOfTodoOnLastPage)]).to.be.undefined 22 | await todosManager.next() 23 | expect(GroupResource.client.requestTracker[TodoResource.getDetailRoutePath(compareIDOfTodoOnLastPage)]).to.be.undefined 24 | // Now that we've retrieved a few pages, compareIDOfTodoOnLastPage should still not've been retrieved. Now we'll use all() to get the rest 25 | await todosManager.all() 26 | // ...and assert that compareIDOfTodoOnLastPage is now loaded 27 | expect(GroupResource.client.requestTracker[TodoResource.getDetailRoutePath(compareIDOfTodoOnLastPage)]).to.equal(1) 28 | // Reset batch size for later tests 29 | todosManager.batchSize = originalBatchSize 30 | }) 31 | 32 | it('related object lookup has correct progression', async () => { 33 | let post = await PostResource.detail('40') 34 | expect(post.get('user')).to.be.string 35 | let samePost = await PostResource.detail('40', { resolveRelated: true }) 36 | expect(samePost.get('user')).to.be.instanceOf(UserResource) 37 | }) 38 | 39 | it('correctly gets related objects and managers', async () => { 40 | let post = await PostResource.detail('1') 41 | let group = await GroupResource.detail('1') 42 | expect(post.get('user')).to.be.instanceOf(PostResource.RelatedManagerClass) 43 | await post.resolveRelated() 44 | await group.resolveRelated() 45 | expect(post.get('user')).to.be.instanceOf(UserResource) 46 | expect(post.managers.user).to.be.instanceOf(PostResource.RelatedManagerClass) 47 | expect(group.managers.users).to.be.instanceOf(GroupResource.RelatedManagerClass) 48 | expect(group.get('users')).to.be.instanceOf(GroupResource.RelatedManagerClass) 49 | expect(group.managers.users.many).to.be.true 50 | expect(group.managers.users.resolved).to.be.true 51 | expect(group.managers.users.primaryKeys.length).to.equal(3) 52 | expect(group.get('name')).to.equal('Test group') 53 | expect(group.get('users.name')).to.be.instanceOf(Array) 54 | expect(group.get('users').resources[0]).to.be.instanceOf(UserResource) 55 | }) 56 | 57 | it('can get related objects recursively', async () => { 58 | const requestTracker = CommentResource.client.requestTracker 59 | // No requests to /comments yet -- so let's ensure request count is undefined 60 | expect(requestTracker[CommentResource.getDetailRoutePath('250')]).to.be.undefined 61 | // GET comment, but recursively get related objects too -- Comment #250 should be post #50 which should be user #5 (all of which shouldn't have been retrieved yet) 62 | let comment = await CommentResource.detail('250', { resolveRelatedDeep: true }) 63 | let post = comment.get('post') 64 | let user = post.get('user') 65 | expect(post).to.be.instanceOf(PostResource) 66 | expect(user).to.be.instanceOf(UserResource) 67 | }) 68 | 69 | it('can define Resource.related with a function', async () => { 70 | let relatedFuncRan = false 71 | const CustomTodoResource = TodoResource.extend({ 72 | _cache: {}, 73 | related() { 74 | relatedFuncRan = true 75 | return { 76 | user: UserResource, 77 | } 78 | }, 79 | }) 80 | 81 | const todo = await CustomTodoResource.detail(55) 82 | expect(relatedFuncRan).to.be.true 83 | expect(await todo.resolveAttribute('user.username')).to.equal('Samantha') 84 | }) 85 | 86 | it('auto-resolves nested objects (single)', async () => { 87 | // Copy the GroupResource and redefine related.owner 88 | const CustomGroupResource = GroupResource.extend({ 89 | related: Object.assign(GroupResource.related, { 90 | owner: { 91 | to: UserResource, 92 | nested: true, // This is what we're testing 93 | }, 94 | }), 95 | }) 96 | 97 | let group = await CustomGroupResource.detail(1) 98 | expect(group.get('owner.username')).to.equal('Bret') 99 | expect(group.managers.owner).to.exist 100 | expect(group.managers.owner.resolved).to.be.true 101 | 102 | try { 103 | new CustomGroupResource({ 104 | id: 1, 105 | name: 'Test group', 106 | owner: { 107 | // No ID here 108 | name: 'Leanne Graham', 109 | }, 110 | }) 111 | } catch (e) { 112 | // Make sure error is thrown 113 | expect(e.name).to.contain('AssertionError') 114 | } 115 | }) 116 | 117 | it('auto-resolves nested objects (many)', async () => { 118 | const CustomGroupResource = GroupResource.extend({ 119 | _cache: {}, 120 | related: Object.assign(GroupResource.related, { 121 | todos: { 122 | to: TodoResource, 123 | nested: true, 124 | }, 125 | }), 126 | }) 127 | 128 | let group = await CustomGroupResource.detail(1) 129 | expect(group.get('todos.id')).to.eql([1, 2]) 130 | }) 131 | 132 | it('can resolve really really deeply nested attributes', async () => { 133 | let meta = await CommentMetaMetaMetaMeta.detail(1) 134 | expect('Leanne Graham' === await meta.resolveAttribute('commentmetametameta.commentmetameta.commentmeta.comment.post.user.name')) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /dist/related.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"related.js","sourceRoot":"","sources":["../src/related.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import Resource, { ListResponse } from './index' 2 | import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse, AxiosError, AxiosInstance } from 'axios' 3 | 4 | export * from 'axios' 5 | 6 | export interface RequestConfig extends AxiosRequestConfig { 7 | useCache?: boolean 8 | query?: any 9 | } 10 | 11 | export interface ResourceResponse extends Record { 12 | response: AxiosResponse 13 | resources: T[] 14 | count?: () => number 15 | pages?: () => number 16 | currentPage?: () => number 17 | perPage?: () => number 18 | next?: () => Promise> 19 | previous?: () => Promise> 20 | } 21 | 22 | export type ExtractorFunction = (result: ResourceResponse['response']) => ResourceResponse 23 | 24 | export class BaseClient { 25 | axios: AxiosInstance 26 | config: AxiosRequestConfig = {} 27 | 28 | constructor(baseURL: string, config: AxiosRequestConfig = {}) { 29 | this.config = Object.assign({ baseURL }, config) 30 | this.axios = axios.create(this.config) 31 | } 32 | 33 | get hostname() { 34 | return this.config.baseURL 35 | } 36 | 37 | set hostname(value: string) { 38 | this.config.baseURL = value 39 | this.axios = axios.create(this.config) 40 | } 41 | 42 | static extend(this: U, classProps: T): U & T { 43 | // @todo Figure out typings here -- this works perfectly but typings are not happy 44 | // @ts-ignore 45 | return Object.assign(class extends this {}, classProps) 46 | } 47 | 48 | negotiateContent(ResourceClass: T): ExtractorFunction> { 49 | // Should always return a function 50 | return (response: ResourceResponse>['response']) => { 51 | let objects: InstanceType[] = [] 52 | if (Array.isArray(response.data)) { 53 | response.data.forEach((attributes) => objects.push(new ResourceClass(attributes) as InstanceType)) 54 | } else { 55 | objects.push(new ResourceClass(response.data) as InstanceType) 56 | } 57 | 58 | return { 59 | response, 60 | resources: objects, 61 | count: () => response.headers['Pagination-Count'], 62 | pages: () => Math.ceil(response.headers['Pagination-Count'] / response.headers['Pagination-Limit']), 63 | currentPage: () => response.headers['Pagination-Page'], 64 | perPage: () => response.headers['Pagination-Limit'], 65 | } as ResourceResponse> 66 | } 67 | } 68 | 69 | /** 70 | * Client.prototype.list() and Client.prototype.detail() are the primary purpose of defining these here. Simply runs a GET on the list route path (eg. /users) and negotiates the content 71 | * @param ResourceClass 72 | * @param options 73 | */ 74 | list(ResourceClass: T, options: RequestConfig = {}): ListResponse { 75 | return this.get(ResourceClass.getListRoutePath(options.query), options).then(this.negotiateContent(ResourceClass)) 76 | } 77 | 78 | /** 79 | * Client.prototype.detail() and Client.prototype.list() are the primary purpose of defining these here. Simply runs a GET on the detail route path (eg. /users/123) and negotiates the content 80 | * @param ResourceClass 81 | * @param options 82 | */ 83 | detail(ResourceClass: T, id: string, options: RequestConfig = {}) { 84 | return this.get(ResourceClass.getDetailRoutePath(id, options.query), options).then(this.negotiateContent(ResourceClass)) 85 | } 86 | 87 | get(path: string, options: AxiosRequestConfig = {}): AxiosPromise { 88 | return this.axios.get(path, options).catch((e: Error) => this.onError(e)) 89 | } 90 | 91 | put(path: string, body: any = {}, options: AxiosRequestConfig = {}): AxiosPromise { 92 | return this.axios.put(path, body, options).catch((e: Error) => this.onError(e)) 93 | } 94 | post(path: string, body: any = {}, options: AxiosRequestConfig = {}): AxiosPromise { 95 | return this.axios.post(path, body, options).catch((e: Error) => this.onError(e)) 96 | } 97 | 98 | patch(path: string, body: any = {}, options: AxiosRequestConfig = {}): AxiosPromise { 99 | return this.axios.patch(path, body, options).catch((e: Error) => this.onError(e)) 100 | } 101 | 102 | delete(path: string, options: AxiosRequestConfig = {}): AxiosPromise { 103 | return this.axios.delete(path, options).catch((e: Error) => this.onError(e)) 104 | } 105 | 106 | head(path: string, options: AxiosRequestConfig = {}): AxiosPromise { 107 | return this.axios.head(path, options).catch((e: Error) => this.onError(e)) 108 | } 109 | 110 | options(path: string, options: AxiosRequestConfig = {}): AxiosPromise { 111 | // @ts-ignore -- Axios forgot to add options to AxiosInstance interface 112 | return this.axios.options(path, options).catch((e: Error) => this.onError(e)) 113 | } 114 | 115 | bindMethodsToPath(relativePath: string) { 116 | return { 117 | get: this.get.bind(this, relativePath), 118 | post: this.post.bind(this, relativePath), 119 | put: this.put.bind(this, relativePath), 120 | patch: this.patch.bind(this, relativePath), 121 | head: this.head.bind(this, relativePath), 122 | options: this.options.bind(this, relativePath), 123 | delete: this.delete.bind(this, relativePath), 124 | } 125 | } 126 | 127 | // Optionally catch all errors in client class (default: rethrow) 128 | onError(exception: Error | AxiosError): any { 129 | throw exception 130 | } 131 | } 132 | 133 | export class DefaultClient extends BaseClient {} 134 | 135 | export class JWTBearerClient extends BaseClient { 136 | token: string 137 | // This is just a basic client except we're including a token in the requests 138 | constructor(baseURL: string, token: string = '', options: RequestConfig = {}) { 139 | let headers = Object.assign( 140 | { 141 | Authorization: `Bearer ${token}`, 142 | }, 143 | options.headers 144 | ) 145 | options.headers = headers 146 | super(baseURL, options) 147 | this.token = token 148 | } 149 | 150 | getTokenPayload(): any { 151 | try { 152 | let jwtPieces = this.token.split('.') 153 | let payloadBase64 = jwtPieces[1] 154 | let payloadBuffer = Buffer.from(payloadBase64, 'base64').toString() 155 | return JSON.parse(payloadBuffer.toString()) 156 | } catch (e) { 157 | return undefined 158 | } 159 | } 160 | 161 | tokenIsExpired(): boolean { 162 | try { 163 | let payload = this.getTokenPayload() 164 | let nowInSeconds = Math.floor(Date.now() / 1000) 165 | return payload.exp < nowInSeconds 166 | } catch (e) { 167 | return true 168 | } 169 | } 170 | 171 | tokenIsValid(): boolean { 172 | return this.token && !this.tokenIsExpired() 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /dist/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var tslib_1 = require("tslib"); 4 | var axios_1 = tslib_1.__importDefault(require("axios")); 5 | tslib_1.__exportStar(require("axios"), exports); 6 | var BaseClient = /** @class */ (function () { 7 | function BaseClient(baseURL, config) { 8 | if (config === void 0) { config = {}; } 9 | this.config = {}; 10 | this.config = Object.assign({ baseURL: baseURL }, config); 11 | this.axios = axios_1.default.create(this.config); 12 | } 13 | Object.defineProperty(BaseClient.prototype, "hostname", { 14 | get: function () { 15 | return this.config.baseURL; 16 | }, 17 | set: function (value) { 18 | this.config.baseURL = value; 19 | this.axios = axios_1.default.create(this.config); 20 | }, 21 | enumerable: true, 22 | configurable: true 23 | }); 24 | BaseClient.extend = function (classProps) { 25 | // @todo Figure out typings here -- this works perfectly but typings are not happy 26 | // @ts-ignore 27 | return Object.assign(/** @class */ (function (_super) { 28 | tslib_1.__extends(class_1, _super); 29 | function class_1() { 30 | return _super !== null && _super.apply(this, arguments) || this; 31 | } 32 | return class_1; 33 | }(this)), classProps); 34 | }; 35 | BaseClient.prototype.negotiateContent = function (ResourceClass) { 36 | // Should always return a function 37 | return function (response) { 38 | var objects = []; 39 | if (Array.isArray(response.data)) { 40 | response.data.forEach(function (attributes) { return objects.push(new ResourceClass(attributes)); }); 41 | } 42 | else { 43 | objects.push(new ResourceClass(response.data)); 44 | } 45 | return { 46 | response: response, 47 | resources: objects, 48 | count: function () { return response.headers['Pagination-Count']; }, 49 | pages: function () { return Math.ceil(response.headers['Pagination-Count'] / response.headers['Pagination-Limit']); }, 50 | currentPage: function () { return response.headers['Pagination-Page']; }, 51 | perPage: function () { return response.headers['Pagination-Limit']; }, 52 | }; 53 | }; 54 | }; 55 | /** 56 | * Client.prototype.list() and Client.prototype.detail() are the primary purpose of defining these here. Simply runs a GET on the list route path (eg. /users) and negotiates the content 57 | * @param ResourceClass 58 | * @param options 59 | */ 60 | BaseClient.prototype.list = function (ResourceClass, options) { 61 | if (options === void 0) { options = {}; } 62 | return this.get(ResourceClass.getListRoutePath(options.query), options).then(this.negotiateContent(ResourceClass)); 63 | }; 64 | /** 65 | * Client.prototype.detail() and Client.prototype.list() are the primary purpose of defining these here. Simply runs a GET on the detail route path (eg. /users/123) and negotiates the content 66 | * @param ResourceClass 67 | * @param options 68 | */ 69 | BaseClient.prototype.detail = function (ResourceClass, id, options) { 70 | if (options === void 0) { options = {}; } 71 | return this.get(ResourceClass.getDetailRoutePath(id, options.query), options).then(this.negotiateContent(ResourceClass)); 72 | }; 73 | BaseClient.prototype.get = function (path, options) { 74 | var _this = this; 75 | if (options === void 0) { options = {}; } 76 | return this.axios.get(path, options).catch(function (e) { return _this.onError(e); }); 77 | }; 78 | BaseClient.prototype.put = function (path, body, options) { 79 | var _this = this; 80 | if (body === void 0) { body = {}; } 81 | if (options === void 0) { options = {}; } 82 | return this.axios.put(path, body, options).catch(function (e) { return _this.onError(e); }); 83 | }; 84 | BaseClient.prototype.post = function (path, body, options) { 85 | var _this = this; 86 | if (body === void 0) { body = {}; } 87 | if (options === void 0) { options = {}; } 88 | return this.axios.post(path, body, options).catch(function (e) { return _this.onError(e); }); 89 | }; 90 | BaseClient.prototype.patch = function (path, body, options) { 91 | var _this = this; 92 | if (body === void 0) { body = {}; } 93 | if (options === void 0) { options = {}; } 94 | return this.axios.patch(path, body, options).catch(function (e) { return _this.onError(e); }); 95 | }; 96 | BaseClient.prototype.delete = function (path, options) { 97 | var _this = this; 98 | if (options === void 0) { options = {}; } 99 | return this.axios.delete(path, options).catch(function (e) { return _this.onError(e); }); 100 | }; 101 | BaseClient.prototype.head = function (path, options) { 102 | var _this = this; 103 | if (options === void 0) { options = {}; } 104 | return this.axios.head(path, options).catch(function (e) { return _this.onError(e); }); 105 | }; 106 | BaseClient.prototype.options = function (path, options) { 107 | var _this = this; 108 | if (options === void 0) { options = {}; } 109 | // @ts-ignore -- Axios forgot to add options to AxiosInstance interface 110 | return this.axios.options(path, options).catch(function (e) { return _this.onError(e); }); 111 | }; 112 | BaseClient.prototype.bindMethodsToPath = function (relativePath) { 113 | return { 114 | get: this.get.bind(this, relativePath), 115 | post: this.post.bind(this, relativePath), 116 | put: this.put.bind(this, relativePath), 117 | patch: this.patch.bind(this, relativePath), 118 | head: this.head.bind(this, relativePath), 119 | options: this.options.bind(this, relativePath), 120 | delete: this.delete.bind(this, relativePath), 121 | }; 122 | }; 123 | // Optionally catch all errors in client class (default: rethrow) 124 | BaseClient.prototype.onError = function (exception) { 125 | throw exception; 126 | }; 127 | return BaseClient; 128 | }()); 129 | exports.BaseClient = BaseClient; 130 | var DefaultClient = /** @class */ (function (_super) { 131 | tslib_1.__extends(DefaultClient, _super); 132 | function DefaultClient() { 133 | return _super !== null && _super.apply(this, arguments) || this; 134 | } 135 | return DefaultClient; 136 | }(BaseClient)); 137 | exports.DefaultClient = DefaultClient; 138 | var JWTBearerClient = /** @class */ (function (_super) { 139 | tslib_1.__extends(JWTBearerClient, _super); 140 | // This is just a basic client except we're including a token in the requests 141 | function JWTBearerClient(baseURL, token, options) { 142 | if (token === void 0) { token = ''; } 143 | if (options === void 0) { options = {}; } 144 | var _this = this; 145 | var headers = Object.assign({ 146 | Authorization: "Bearer " + token, 147 | }, options.headers); 148 | options.headers = headers; 149 | _this = _super.call(this, baseURL, options) || this; 150 | _this.token = token; 151 | return _this; 152 | } 153 | JWTBearerClient.prototype.getTokenPayload = function () { 154 | try { 155 | var jwtPieces = this.token.split('.'); 156 | var payloadBase64 = jwtPieces[1]; 157 | var payloadBuffer = Buffer.from(payloadBase64, 'base64').toString(); 158 | return JSON.parse(payloadBuffer.toString()); 159 | } 160 | catch (e) { 161 | return undefined; 162 | } 163 | }; 164 | JWTBearerClient.prototype.tokenIsExpired = function () { 165 | try { 166 | var payload = this.getTokenPayload(); 167 | var nowInSeconds = Math.floor(Date.now() / 1000); 168 | return payload.exp < nowInSeconds; 169 | } 170 | catch (e) { 171 | return true; 172 | } 173 | }; 174 | JWTBearerClient.prototype.tokenIsValid = function () { 175 | return this.token && !this.tokenIsExpired(); 176 | }; 177 | return JWTBearerClient; 178 | }(BaseClient)); 179 | exports.JWTBearerClient = JWTBearerClient; 180 | //# sourceMappingURL=client.js.map -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 57 |

rest-resource

58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |

Index

66 |
67 |
68 |
69 |

External modules

70 | 78 |
79 |
80 |
81 |
82 |
83 | 120 |
121 |
122 |
123 |
124 |

Legend

125 |
126 |
    127 |
  • Module
  • 128 |
  • Object literal
  • 129 |
  • Variable
  • 130 |
  • Function
  • 131 |
  • Function with type parameter
  • 132 |
  • Index signature
  • 133 |
  • Type alias
  • 134 |
  • Type alias with type parameter
  • 135 |
136 |
    137 |
  • Enumeration
  • 138 |
  • Enumeration member
  • 139 |
  • Property
  • 140 |
  • Method
  • 141 |
142 |
    143 |
  • Interface
  • 144 |
  • Interface with type parameter
  • 145 |
  • Constructor
  • 146 |
  • Property
  • 147 |
  • Method
  • 148 |
  • Index signature
  • 149 |
150 |
    151 |
  • Class
  • 152 |
  • Class with type parameter
  • 153 |
  • Constructor
  • 154 |
  • Property
  • 155 |
  • Method
  • 156 |
  • Accessor
  • 157 |
  • Index signature
  • 158 |
159 |
    160 |
  • Inherited constructor
  • 161 |
  • Inherited property
  • 162 |
  • Inherited method
  • 163 |
  • Inherited accessor
  • 164 |
165 |
    166 |
  • Protected property
  • 167 |
  • Protected method
  • 168 |
  • Protected accessor
  • 169 |
170 |
    171 |
  • Private property
  • 172 |
  • Private method
  • 173 |
  • Private accessor
  • 174 |
175 |
    176 |
  • Static property
  • 177 |
  • Static method
  • 178 |
179 |
180 |
181 |
182 |
183 |

Generated using TypeDoc

184 |
185 |
186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DefaultClient, RequestConfig, ResourceResponse } from './client'; 2 | import RelatedManager from './related'; 3 | import { NormalizerDict } from './helpers/normalization'; 4 | import * as exceptions from './exceptions'; 5 | export default class Resource { 6 | static endpoint: string; 7 | static cacheMaxAge: number; 8 | static client: DefaultClient; 9 | static queued: Record; 10 | static uniqueKey: string; 11 | static defaults: Record; 12 | static RelatedManagerClass: typeof RelatedManager; 13 | static validation: ValidatorDictOrFunction; 14 | static normalization: NormalizerDict; 15 | static fields: string[]; 16 | static related: RelatedDictOrFunction; 17 | static _cache: any; 18 | static _uuid: string; 19 | _attributes: Record; 20 | uuid: string; 21 | attributes: Record; 22 | managers: Record; 23 | changes: Record; 24 | constructor(attributes?: any, options?: any); 25 | /** 26 | * Cache getter 27 | */ 28 | static readonly cache: any; 29 | static readonly uuid: string; 30 | /** 31 | * Cache a resource onto this class' cache for cacheMaxAge seconds 32 | * @param resource 33 | * @param replace 34 | */ 35 | static cacheResource(this: T, resource: InstanceType, replace?: boolean): void; 36 | /** 37 | * Replace attributes on a cached resource onto this class' cache for cacheMaxAge seconds (useful for bubbling up changes to states that may be already rendered) 38 | * @param resource 39 | */ 40 | static replaceCache(resource: T): void; 41 | static clearCache(): void; 42 | /** 43 | * Get time delta in seconds of cache expiry 44 | */ 45 | static cacheDeltaSeconds(): number; 46 | /** 47 | * Get a cached resource by ID 48 | * @param id 49 | */ 50 | static getCached(this: T, id: string | number): CachedResource> | undefined; 51 | static getCachedAll(this: T): CachedResource>[]; 52 | /** 53 | * Backwards compatibility 54 | * Remove in next major release @todo 55 | */ 56 | /** 57 | * Backwards compatibility 58 | * Remove in next major release @todo 59 | */ 60 | static validators: any; 61 | /** 62 | * Get list route path (eg. /users) to be used with HTTP requests and allow a querystring object 63 | * @param query Querystring 64 | */ 65 | static getListRoutePath(query?: any): string; 66 | /** 67 | * Get detail route path (eg. /users/123) to be used with HTTP requests 68 | * @param id 69 | * @param query Querystring 70 | */ 71 | static getDetailRoutePath(id: string | number, query?: any): string; 72 | /** 73 | * HTTP Get of resource's list route--returns a promise 74 | * @param options Options object 75 | * @returns Promise 76 | */ 77 | static list(this: T, options?: ListOpts): ListResponse; 78 | static detail(this: T, id: string | number, options?: DetailOpts): Promise>; 79 | static wrap(relativePath: string, query?: any): { 80 | get: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 81 | post: (body?: any, options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 82 | put: (body?: any, options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 83 | patch: (body?: any, options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 84 | head: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 85 | options: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 86 | /** 87 | * Get a cached resource by ID 88 | * @param id 89 | */ 90 | delete: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 91 | }; 92 | static toResourceName(): string; 93 | static makeDefaultsObject(): any; 94 | /** 95 | * Unique resource hash key used for caching and organizing requests 96 | * @param resourceId 97 | */ 98 | static getResourceHashKey(resourceId: string | number): string; 99 | private static getRelatedClasses; 100 | private static getValidatorObject; 101 | static extend(this: U, classProps: T): U & T; 102 | /** 103 | * Set an attribute of Resource instance and apply getters/setters 104 | * Do not use Dot Notation here 105 | * @param key 106 | * @param value 107 | */ 108 | set(key: string, value: any): this; 109 | /** 110 | * Get an attribute of Resource instance 111 | * You can use dot notation here -- eg. `resource.get('user.username')` 112 | * You can also get all properties by not providing any arguments 113 | * @param? key 114 | */ 115 | get(key?: string): T; 116 | /** 117 | * Persist getting an attribute and get related keys until a key can be found (or not found) 118 | * TypeError in get() will be thrown, we're just doing the resolveRelated() work for you... 119 | * @param key 120 | */ 121 | resolveAttribute(key: string): Promise; 122 | /** 123 | * Alias of resource.resolveAttribute(key) 124 | * @param key 125 | */ 126 | getAsync(key: string): Promise; 127 | /** 128 | * Setter -- Translate new value into an internal value onto this._attributes[key] 129 | * Usually this is just setting a key/value but we want to be able to accept 130 | * anything -- another Resource instance for example. If a Resource instance is 131 | * provided, set the this.managers[key] as the new manager instance, then set the 132 | * this.attributes[key] field as just the primary key of the related Resource instance 133 | * @param key 134 | * @param value 135 | */ 136 | toInternalValue(key: string, value: any): any; 137 | /** 138 | * Like calling instance.constructor but safer: 139 | * changing objects down the line won't creep up the prototype chain and end up on native global objects like Function or Object 140 | */ 141 | getConstructor(): T; 142 | /** 143 | * Match all related values in `attributes[key]` where key is primary key of related instance defined in `Resource.related[key]` 144 | * @param options resolveRelatedDict 145 | */ 146 | resolveRelated({ deep, managers }?: ResolveRelatedOpts): Promise; 147 | /** 148 | * Same as `Resource.prototype.resolveRelated` except `options.deep` defaults to `true` 149 | * @param options 150 | */ 151 | resolveRelatedDeep(options?: ResolveRelatedOpts): Promise; 152 | /** 153 | * Get related manager class by key 154 | * @param key 155 | */ 156 | rel(key: string): RelatedManager; 157 | /** 158 | * Create a manager instance on based on current attributes 159 | * @param relatedKey 160 | */ 161 | createManagerFor(relatedKey: string): RelatedManager; 162 | /** 163 | * Saves the instance -- sends changes as a PATCH or sends whole object as a POST if it's new 164 | */ 165 | save(options?: SaveOptions): Promise>; 166 | /** 167 | * Validate attributes -- returns empty if no errors exist -- you should throw new errors here 168 | * @returns `Error[]` Array of Exceptions 169 | */ 170 | validate(): Error[]; 171 | update(this: T): Promise; 172 | delete(options?: RequestConfig): import("axios").AxiosPromise; 173 | cache(this: T, replace?: boolean): T; 174 | getCached(this: T): CachedResource; 175 | wrap(relativePath: string, query?: any): { 176 | get: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 177 | post: (body?: any, options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 178 | put: (body?: any, options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 179 | patch: (body?: any, options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 180 | head: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 181 | options: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 182 | /** 183 | * Get a cached resource by ID 184 | * @param id 185 | */ 186 | delete: (options?: import("axios").AxiosRequestConfig) => import("axios").AxiosPromise<{}>; 187 | }; 188 | isNew(): boolean; 189 | id: string; 190 | toString(): string; 191 | toResourceName(): string; 192 | toJSON(): any; 193 | } 194 | export declare type TypeOrFunctionReturningType = (() => T) | T; 195 | export declare type RelatedDict = Record; 196 | export declare type RelatedDictOrFunction = TypeOrFunctionReturningType; 197 | export interface RelatedLiteral { 198 | to: typeof Resource; 199 | nested?: boolean; 200 | } 201 | export declare type ValidatorFunc = (value?: any, resource?: Resource, validationExceptionClass?: typeof exceptions.ValidationError) => void; 202 | export declare type ValidatorDict = Record; 203 | export declare type ValidatorDictOrFunction = TypeOrFunctionReturningType; 204 | export interface CachedResource { 205 | expires: number; 206 | resource: T; 207 | } 208 | export interface SaveOptions { 209 | partial?: boolean; 210 | replaceCache?: boolean; 211 | force?: boolean; 212 | fields?: any; 213 | } 214 | export interface ResolveRelatedOpts { 215 | managers?: string[]; 216 | deep?: boolean; 217 | } 218 | export declare type ListOpts = RequestConfig & { 219 | resolveRelated?: boolean; 220 | resolveRelatedDeep?: boolean; 221 | }; 222 | export declare type ListResponse = Promise, any>>; 223 | export declare type DetailOpts = RequestConfig & { 224 | resolveRelated?: boolean; 225 | resolveRelatedDeep?: boolean; 226 | }; 227 | -------------------------------------------------------------------------------- /tests/specs/001_resource.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BaseTestingResource, PostResource, UserResource, GroupResource, TodoResource, CommentResource } from '..' 3 | 4 | describe('Resources', () => { 5 | it('correctly gets remote resource', async () => { 6 | let post = await PostResource.detail('1') 7 | expect(post.get('user')).to.exist 8 | let user = await UserResource.detail('1') 9 | expect(user.get('name')).to.exist 10 | }) 11 | 12 | it('creates and saves resources', async () => { 13 | let user = new UserResource({ 14 | name: 'Test User', 15 | username: 'testing123321', 16 | email: 'testuser@dsf.com', 17 | }) 18 | 19 | await user.save() 20 | expect(user).to.have.property('id') 21 | expect(user.id).to.exist 22 | expect(user._attributes.id).to.exist 23 | 24 | // Make sure save() only sends requested fields 25 | const CustomUserResource = UserResource.extend({ 26 | fields: ['username', 'email'], 27 | }) 28 | 29 | let user2 = new CustomUserResource({ 30 | name: 'Another Test User', 31 | username: 'testing54321', 32 | email: 'anothertest@dsf.com', 33 | }) 34 | 35 | let result = await user2.save() 36 | expect(user).to.have.property('id') 37 | expect(user.changes.id).to.exist 38 | expect(result.response.data.email).to.exist 39 | expect(result.response.data.username).to.exist 40 | expect(result.response.data.name).to.be.undefined 41 | 42 | // Make sure this also works with save({ fields: [...] } 43 | let user3 = new CustomUserResource({ 44 | name: 'Another Another Test User', 45 | username: 'testing3212', 46 | email: 'anothertest2@dsf.com', 47 | address: '123 Fake St', 48 | }) 49 | 50 | let result2 = await user3.save({ fields: ['address'] }) 51 | expect(result2.response.data.address).to.equal('123 Fake St') 52 | expect(result2.response.data.email).to.be.undefined 53 | expect(result2.response.data.username).to.be.undefined 54 | expect(result2.response.data.name).to.be.undefined 55 | 56 | // Make sure changes are unset after they're sent 57 | user3.set('city', 'San Francisco') 58 | expect(user3.changes).to.have.property('city') 59 | await user3.save({ fields: ['city'] }) 60 | expect(user3.changes.city).to.be.undefined 61 | }) 62 | 63 | it('gets/sets properties correctly (static)', async () => { 64 | let changingUser = new UserResource({ 65 | name: 'Test User', 66 | username: 'testing123321', 67 | email: 'testuser@dsf.com', 68 | }) 69 | 70 | expect(changingUser.get('name')).to.equal('Test User') 71 | changingUser.set('name', 'Test User (Changed)') 72 | expect(changingUser.get('name')).to.equal('Test User (Changed)') 73 | expect(changingUser.changes.name).to.exist 74 | expect(changingUser.changes.name).to.equal('Test User (Changed)') 75 | expect(changingUser.attributes.name).to.equal('Test User (Changed)') 76 | expect(typeof changingUser.get()).to.equal('object') 77 | changingUser.set('name', 'Test User (Changed again)') 78 | expect(changingUser.get('name')).to.equal('Test User (Changed again)') 79 | }) 80 | 81 | it('correctly gets a cached related item', async () => { 82 | let post = await PostResource.detail('1', { resolveRelated: true }) 83 | let cached = UserResource.getCached(post.get('user.id')) 84 | expect(cached).to.be.string 85 | expect(cached.resource).to.be.instanceOf(UserResource) 86 | expect(await PostResource.detail('1') === post).to.be.true 87 | // Repeatedly GET /posts/1 ... 88 | await PostResource.detail('1') 89 | await PostResource.detail('1') 90 | await PostResource.detail('1') 91 | // Then check how many requests it sent to it (should be only 1) 92 | expect(PostResource.client.requestTracker[PostResource.getDetailRoutePath('1')]).to.equal(1) 93 | }) 94 | 95 | it('should never allow extended classes to share the same cache', async () => { 96 | class A extends PostResource {} 97 | class B extends A {} 98 | class C extends B {} 99 | expect(C.cache === A.cache).to.be.false 100 | expect(C.cache === B.cache).to.be.false 101 | expect(C._cache === B._cache).to.be.false 102 | expect(C._cache === A._cache).to.be.false 103 | expect(C._cache === PostResource._cache).to.be.false 104 | }) 105 | 106 | it('gets properties correctly (async)', async () => { 107 | // This post should already be cached by this point 108 | let comment = await CommentResource.detail('90') 109 | // Make sure resolveAttribute works 110 | let userName = await comment.resolveAttribute('post.user.name') 111 | // Also make sure getAsync is an alias of resolveAttribute 112 | let userEmail = await comment.getAsync('post.user.email') 113 | expect(userName).to.equal('Ervin Howell') 114 | expect(userEmail).to.equal('Shanna@melissa.tv') 115 | expect(await comment.resolveAttribute('post.user.propDoesNotExist')).to.be.undefined 116 | try { 117 | await comment.resolveAttribute('post.user.nested.propDoesNotExist') 118 | } catch (e) { 119 | expect(e.name).to.equal('ImproperlyConfiguredError') 120 | } 121 | }) 122 | 123 | it('caching can be turned off and on again', async () => { 124 | PostResource.cacheMaxAge = -1 125 | PostResource.clearCache() 126 | await PostResource.detail('1') 127 | expect(PostResource.client.requestTracker[PostResource.getDetailRoutePath('1')]).to.equal(2) 128 | await PostResource.detail('1') 129 | expect(PostResource.client.requestTracker[PostResource.getDetailRoutePath('1')]).to.equal(3) 130 | await PostResource.detail('1') 131 | expect(PostResource.client.requestTracker[PostResource.getDetailRoutePath('1')]).to.equal(4) 132 | // Turn cache back on 133 | PostResource.cacheMaxAge = Infinity 134 | // At this point, the above resources aren't being cached, so one more request to the server (5 so far) 135 | await PostResource.detail('1') 136 | expect(PostResource.client.requestTracker[PostResource.getDetailRoutePath('1')]).to.equal(5) 137 | // Now the resource should be cached (still 5) 138 | await PostResource.detail('1') 139 | expect(PostResource.client.requestTracker[PostResource.getDetailRoutePath('1')]).to.equal(5) 140 | }) 141 | 142 | it('cross-relating resources reference single resource by cache key', async () => { 143 | let group = await GroupResource.detail('1', { resolveRelatedDeep: true }) 144 | // ...At this point, group has a cached user (ID 1) 145 | let user = await UserResource.detail('1', { resolveRelatedDeep: true }) 146 | // And getting the user again will yield the same exact user in memory stored at cache[cacheKey] address 147 | expect(group.managers.users.resources[0] === user).to.be.true 148 | }) 149 | 150 | it('handles empty values correctly', async () => { 151 | // Custom group resource 152 | const CustomGroupResource = BaseTestingResource.extend({ 153 | endpoint: '/groups', 154 | related: { 155 | todos: TodoResource, 156 | users: UserResource, 157 | owner: UserResource, 158 | }, 159 | }) 160 | // ...that is created with empty lists and some emptyish values on related field 161 | const someGroup = new CustomGroupResource({ 162 | name: 'Test Group', 163 | // This is what we're testing 164 | users: [], // empty list 165 | owner: null, // null 166 | todos: undefined, 167 | }) 168 | 169 | expect(someGroup.get('owner')).to.be.null 170 | expect(someGroup.get('users')).to.be.instanceOf(CustomGroupResource.RelatedManagerClass) 171 | expect(someGroup.get('users').resources).to.be.empty 172 | expect(someGroup.get('todos')).to.be.undefined 173 | }) 174 | 175 | it('correctly lists remote resources', async () => { 176 | const PostResourceDupe = PostResource.extend({}) 177 | let result = await PostResourceDupe.list() 178 | result.resources.forEach((post) => { 179 | expect(post instanceof PostResourceDupe).to.be.true 180 | expect(post.id).to.exist 181 | }) 182 | }) 183 | 184 | it('calls relative list routes correctly', async () => { 185 | let filteredPostResult = await PostResource.wrap('/1', { user: 1 }).get() 186 | expect(filteredPostResult.config.method.toLowerCase()).to.equal('get') 187 | expect(filteredPostResult.config.url).to.equal('/posts/1?user=1') 188 | let optionsResult = await PostResource.wrap('/1', { user: 1 }).get({ headers: { 'X-Taco': true } }) 189 | expect(optionsResult.config.headers['X-Taco']).to.be.true 190 | try { 191 | await PostResource.wrap('/does_not_____exist', { user: 1 }).post({ someBody: true }) 192 | } catch(e) { 193 | expect(String(e.response.status)).to.equal('404') 194 | expect(e.config.method.toLowerCase()).to.equal('post') 195 | expect(e.config.url).to.equal('/posts/does_not_____exist?user=1') 196 | expect(JSON.parse(e.config.data)).to.eql({ someBody: true }) 197 | } 198 | 199 | try { 200 | await PostResource.wrap('does_not_start_with_a_/').post() 201 | } catch(e) { 202 | expect(e.name).to.contain('AssertionError') 203 | } 204 | }) 205 | 206 | it('calls relative detail routes correctly', async () => { 207 | let post = await PostResource.detail(1) 208 | let postResult = await post.wrap('/comments').get() 209 | let data = postResult.data as any 210 | expect(data).to.exist 211 | expect(data[0].post).to.equal(1) 212 | try { 213 | // Remove ID and try to run 214 | post.attributes.id = undefined 215 | await post.wrap('/comments').get() 216 | } catch(e) { 217 | expect(e.name).to.contain('AssertionError') 218 | } 219 | }) 220 | 221 | it('can accept resource instances as value', async () => { 222 | let post = await PostResource.detail(80) 223 | let userDoesNotEqualPostUserId = post.attributes.user + 1 224 | let otherUser = await UserResource.detail(userDoesNotEqualPostUserId) 225 | post.set('user', otherUser) 226 | // Normalization should be turned off for PostResource, so newly set user should be an object 227 | expect('object' === typeof post.attributes.user).to.be.true 228 | expect(post.attributes.user).to.eql(otherUser.toJSON()) 229 | expect(post.rel('user').canAutoResolve()).to.be.false 230 | }) 231 | }) 232 | -------------------------------------------------------------------------------- /docs/modules/_exceptions_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "exceptions" | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

External module "exceptions"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Classes

73 | 80 |
81 |
82 |
83 |
84 |
85 | 137 |
138 |
139 |
140 |
141 |

Legend

142 |
143 |
    144 |
  • Module
  • 145 |
  • Object literal
  • 146 |
  • Variable
  • 147 |
  • Function
  • 148 |
  • Function with type parameter
  • 149 |
  • Index signature
  • 150 |
  • Type alias
  • 151 |
  • Type alias with type parameter
  • 152 |
153 |
    154 |
  • Enumeration
  • 155 |
  • Enumeration member
  • 156 |
  • Property
  • 157 |
  • Method
  • 158 |
159 |
    160 |
  • Interface
  • 161 |
  • Interface with type parameter
  • 162 |
  • Constructor
  • 163 |
  • Property
  • 164 |
  • Method
  • 165 |
  • Index signature
  • 166 |
167 |
    168 |
  • Class
  • 169 |
  • Class with type parameter
  • 170 |
  • Constructor
  • 171 |
  • Property
  • 172 |
  • Method
  • 173 |
  • Accessor
  • 174 |
  • Index signature
  • 175 |
176 |
    177 |
  • Inherited constructor
  • 178 |
  • Inherited property
  • 179 |
  • Inherited method
  • 180 |
  • Inherited accessor
  • 181 |
182 |
    183 |
  • Protected property
  • 184 |
  • Protected method
  • 185 |
  • Protected accessor
  • 186 |
187 |
    188 |
  • Private property
  • 189 |
  • Private method
  • 190 |
  • Private accessor
  • 191 |
192 |
    193 |
  • Static property
  • 194 |
  • Static method
  • 195 |
196 |
197 |
198 |
199 |
200 |

Generated using TypeDoc

201 |
202 |
203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /src/related.ts: -------------------------------------------------------------------------------- 1 | import Resource, { DetailOpts } from './index' 2 | import assert from 'assert' 3 | import { AttributeError } from './exceptions' 4 | import { getContentTypeWeak } from './util' 5 | export type RelatedObjectValue = string | string[] | number | number[] | Record | Record[] 6 | export type CollectionValue = Record[] 7 | 8 | export default class RelatedManager { 9 | to: T 10 | value: RelatedObjectValue 11 | many: boolean = false 12 | /** 13 | * Is `true` when `resolve()` is called and first page of results loads up to `this.batchSize` objects 14 | */ 15 | resolved: boolean = false 16 | /** 17 | * Deferred promises when `this.resolve()` hits the max requests in `this.batchSize` 18 | */ 19 | deferred: (() => Promise>)[] = [] 20 | /** 21 | * List of stringified Primary Keys, even if `this.value` is a list of objects, or Resource instances 22 | */ 23 | primaryKeys: string[] 24 | /** 25 | * When sending `this.resolve()`, only send out the first `n` requests where `n` is `this.batchSize`. You 26 | * can call `this.all()` to recursively get all objects 27 | */ 28 | batchSize: number = Infinity 29 | _resources: Record> = {} 30 | 31 | constructor(to: T, value: RelatedObjectValue) { 32 | assert(typeof to === 'function', `RelatedManager expected first parameter to be Resource class, received "${to}". Please double check related definitions on class.`) 33 | this.to = to 34 | this.value = value 35 | this.many = Array.isArray(value) 36 | this.primaryKeys = this.getPrimaryKeys() 37 | 38 | if (!this.value || (this.many && !Object.keys(this.value).length)) { 39 | this.resolved = true 40 | } 41 | } 42 | 43 | /** 44 | * Check if values exist on manager 45 | */ 46 | hasValues() { 47 | if (this.many) { 48 | return (this.value as any[]).length > 0 49 | } 50 | return Boolean(this.value) 51 | } 52 | 53 | canAutoResolve() { 54 | let value = this.value as any 55 | let isObject = Object === this.getValueContentType() 56 | let hasIds = this.primaryKeys.length > 0 57 | 58 | if (this.many) { 59 | return isObject && hasIds && this.primaryKeys.length === value.length 60 | } 61 | 62 | return isObject && hasIds 63 | } 64 | 65 | /** 66 | * Return a constructor so we can guess the content type. For example, if an object literal 67 | * is passed, this function should return `Object`, and it's likely one single object literal representing attributes. 68 | * If the constructor is an `Array`, then all we know is that there are many of these sub items (in which case, we're 69 | * taking the first node of that array and using that node to guess). If it's a `Number`, then it's likely 70 | * that it's just a primary key. If it's a `Resource` instance, it should return `Resource`. Etc. 71 | * @returns Function 72 | */ 73 | getValueContentType() { 74 | return getContentTypeWeak(this.value) 75 | } 76 | 77 | /** 78 | * Get the current value and the content type and turn it into a list of primary keys 79 | * @returns String 80 | */ 81 | getPrimaryKeys(): string[] { 82 | if (!Boolean(this.value) || (Array.isArray(this.value) && !this.value.length)) { 83 | return [] 84 | } 85 | 86 | let contentType = this.getValueContentType() 87 | let iterValue = this.value as any[] 88 | 89 | if (this.many) { 90 | if (contentType === Resource) { 91 | return iterValue.map((resource: InstanceType) => this.getIdFromResource(resource)) 92 | } else if (this.many && contentType === Object) { 93 | return iterValue.map((record) => this.getIdFromObject(record)) 94 | } else { 95 | return this.value as string[] 96 | } 97 | } else { 98 | if (contentType === Resource) { 99 | return [this.getIdFromResource(this.value as InstanceType)] 100 | } else if (contentType === Object) { 101 | return [this.getIdFromObject(this.value)] 102 | } else { 103 | return [this.value as string] 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Get unique key property from object literal and turn it into a string 110 | * @param object Object 111 | */ 112 | getIdFromObject(object: any): string { 113 | return String(object[this.to.uniqueKey]) 114 | } 115 | 116 | /** 117 | * Get unique key from resource instance 118 | * @param resource Resource 119 | */ 120 | getIdFromResource(resource: InstanceType) { 121 | return resource.id 122 | } 123 | 124 | /** 125 | * Get a single resource from the endpoint given an ID 126 | * @param id String | Number 127 | */ 128 | getOne(id: string | number, options?: DetailOpts): Promise> { 129 | return this.to.detail(id, options).then((resource: InstanceType) => { 130 | assert(resource.getConstructor().toResourceName() == this.to.toResourceName(), `Related class detail() returned invalid instance: ${resource.toResourceName()} (returned) !== ${this.to.toResourceName()} (expected)`) 131 | this._resources[resource.id] = resource 132 | return resource 133 | }) 134 | } 135 | 136 | /** 137 | * Same as getOne but allow lookup by index 138 | * @param index Number 139 | */ 140 | getOneAtIndex(index: number) { 141 | return this.getOne(this.primaryKeys[index]) 142 | } 143 | 144 | /** 145 | * Get all loaded resources relevant to this relation 146 | * Like manager.resources getter except it won't throw an AttributeError and will return with any loaded resources if its ID is listed in `this.primaryKeys` 147 | */ 148 | getAllLoaded(): InstanceType[] { 149 | try { 150 | return this.resources 151 | } catch (e) { 152 | // @ts-ignore See the reason why we do this in index file 153 | if (e.name === 'AttributeError') { 154 | // Some resources aren't loaded -- just return any cached resources 155 | let cachedObjects = [] 156 | for (let id of this.primaryKeys) { 157 | // Check relation cache 158 | let cached = this.to.getCached(id) 159 | // If cache is good, add it to the list of objects to respond wtih 160 | if (cached) { 161 | cachedObjects.push(cached.resource as InstanceType) 162 | } 163 | } 164 | return cachedObjects 165 | } else { 166 | throw e 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Primary function of the RelatedManager -- get some objects (`this.primaryKeys`) related to some 173 | * other Resource (`this.to` instance). Load the first n objects (`this.batchSize`) and set `this.resolved = true`. 174 | * Subsequent calls may be required to get all objects in `this.primaryKeys` because there is an inherent 175 | * limit to how many requests that can be made at one time. If you want to remove this limit, set `this.batchSize` to `Infinity` 176 | * @param options DetailOpts 177 | */ 178 | async resolve(options?: DetailOpts): Promise[]> { 179 | const promises: Promise[] = [] 180 | this.deferred = [] 181 | 182 | for (let i in this.primaryKeys) { 183 | let pk = this.primaryKeys[i] 184 | 185 | if (Number(i) > this.batchSize) { 186 | this.deferred.push(this.getOne.bind(this, pk, options)) 187 | } else { 188 | promises.push(this.getOne(pk, options)) 189 | } 190 | } 191 | 192 | await Promise.all(promises) 193 | this.resolved = true 194 | return Object.values(this.resources) 195 | } 196 | 197 | async next(options?: DetailOpts): Promise[]> { 198 | const promises: Promise>[] = [] 199 | 200 | if (!this.resolved) { 201 | return await this.resolve(options) 202 | } 203 | 204 | // Take 0 to n items from this.deferred where n is this.batchSize 205 | this.deferred.splice(0, this.batchSize).forEach((deferredFn) => { 206 | promises.push(deferredFn()) 207 | }) 208 | 209 | return await Promise.all(promises) 210 | } 211 | 212 | /** 213 | * Calls pending functions in `this.deferred` until it's empty. Runs `this.resolve()` first if it hasn't been ran yet 214 | * @param options DetailOpts 215 | */ 216 | async all(options?: DetailOpts): Promise[]> { 217 | await this.next(options) 218 | 219 | if (this.deferred.length) { 220 | // Still have some left 221 | return await this.all(options) 222 | } else { 223 | return Object.values(this.resources) 224 | } 225 | } 226 | 227 | resolveFromObjectValue() { 228 | let Ctor = this.to 229 | let value = this.value as any 230 | let contentType = this.getValueContentType() 231 | let newResources: Record> = {} 232 | 233 | assert(Object === contentType, `Expected RelatedResource.value to be an Object. Received ${contentType}`) 234 | 235 | try { 236 | if (this.many) { 237 | for (let i in value) { 238 | let resource = new Ctor(value[i]) as InstanceType 239 | assert(!!resource.id, `RelatedResource.value[${i}] does not have an ID.`) 240 | newResources[resource.id] = resource 241 | } 242 | } else { 243 | let resource = new Ctor(value) as InstanceType 244 | assert(!!resource.id, `RelatedResource value does not have an ID.`) 245 | newResources[this.getIdFromObject(value)] = new Ctor(value) as InstanceType 246 | } 247 | 248 | this.resolved = true 249 | Object.assign(this._resources, newResources) 250 | return true 251 | } catch (e) { 252 | throw e 253 | } 254 | } 255 | 256 | /** 257 | * Add a resource to the manager 258 | * @param resource Resource instance 259 | */ 260 | add(resource: InstanceType) { 261 | assert(this.many, `Related Manager "many" must be true to add()`) 262 | assert(resource.id, `Resource must be saved before adding to Related Manager`) 263 | assert(resource.getConstructor() === this.to, `Related Manager add() expected ${this.to.toResourceName()}, received ${resource.getConstructor().toResourceName()}`) 264 | const ContentCtor = this.getValueContentType() 265 | var value 266 | if (ContentCtor === Object) { 267 | value = resource.toJSON() 268 | } else if (ContentCtor === Number || ContentCtor === String) { 269 | value = resource.id 270 | } 271 | 272 | ;(this.value as any[]).push(value) 273 | this._resources[resource.id] = resource 274 | } 275 | 276 | /** 277 | * Create a copy of `this` except with new value(s) 278 | * @param value 279 | */ 280 | fromValue(this: InstanceType, value: any): InstanceType { 281 | let Ctor = this.constructor 282 | return new Ctor(this.to, value) as InstanceType 283 | } 284 | 285 | /** 286 | * Getter -- get `this._resources` but make sure we've actually retrieved the objects first 287 | * Throws AttributeError if `this.resolve()` hasn't finished 288 | */ 289 | get resources(): InstanceType[] { 290 | if (!this.resolved) { 291 | throw new AttributeError(`Can't read results of ${this.constructor.name}[resources], ${this.to.toResourceName()} must resolve() first`) 292 | } 293 | 294 | const allObjects = Object.values(this._resources) 295 | 296 | return allObjects 297 | } 298 | 299 | /** 300 | * Getter -- Same as manager.resources except returns first node 301 | */ 302 | get resource(): InstanceType { 303 | return this.resources[0] 304 | } 305 | 306 | get length(): number { 307 | return this.primaryKeys.length 308 | } 309 | 310 | toString() { 311 | return this.primaryKeys.join(', ') 312 | } 313 | 314 | toJSON() { 315 | return JSON.parse(JSON.stringify(this.value)) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /docs/modules/_related_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "related" | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

External module "related"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Classes

73 | 76 |
77 |
78 |

Type aliases

79 | 83 |
84 |
85 |
86 |
87 |
88 |

Type aliases

89 |
90 | 91 |

CollectionValue

92 |
CollectionValue: Record<string, any>[]
93 | 98 |
99 |
100 | 101 |

RelatedObjectValue

102 |
RelatedObjectValue: string | string[] | number | number[] | Record<string, any> | Record<string, any>[]
103 | 108 |
109 |
110 |
111 | 157 |
158 |
159 |
160 |
161 |

Legend

162 |
163 |
    164 |
  • Module
  • 165 |
  • Object literal
  • 166 |
  • Variable
  • 167 |
  • Function
  • 168 |
  • Function with type parameter
  • 169 |
  • Index signature
  • 170 |
  • Type alias
  • 171 |
  • Type alias with type parameter
  • 172 |
173 |
    174 |
  • Enumeration
  • 175 |
  • Enumeration member
  • 176 |
  • Property
  • 177 |
  • Method
  • 178 |
179 |
    180 |
  • Interface
  • 181 |
  • Interface with type parameter
  • 182 |
  • Constructor
  • 183 |
  • Property
  • 184 |
  • Method
  • 185 |
  • Index signature
  • 186 |
187 |
    188 |
  • Class
  • 189 |
  • Class with type parameter
  • 190 |
  • Constructor
  • 191 |
  • Property
  • 192 |
  • Method
  • 193 |
  • Accessor
  • 194 |
  • Index signature
  • 195 |
196 |
    197 |
  • Inherited constructor
  • 198 |
  • Inherited property
  • 199 |
  • Inherited method
  • 200 |
  • Inherited accessor
  • 201 |
202 |
    203 |
  • Protected property
  • 204 |
  • Protected method
  • 205 |
  • Protected accessor
  • 206 |
207 |
    208 |
  • Private property
  • 209 |
  • Private method
  • 210 |
  • Private accessor
  • 211 |
212 |
    213 |
  • Static property
  • 214 |
  • Static method
  • 215 |
216 |
217 |
218 |
219 |
220 |

Generated using TypeDoc

221 |
222 |
223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /docs/interfaces/_helpers_normalization_.basenormalizeroptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BaseNormalizerOptions | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 63 |

Interface BaseNormalizerOptions

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Hierarchy

72 |
    73 |
  • 74 | BaseNormalizerOptions 75 |
  • 76 |
77 |
78 |
79 |

Index

80 |
81 |
82 |
83 |

Properties

84 | 87 |
88 |
89 |
90 |
91 |
92 |

Properties

93 |
94 | 95 |

Optional uniqueKey

96 |
uniqueKey: string
97 | 102 |
103 |
104 |
105 | 181 |
182 |
183 |
184 |
185 |

Legend

186 |
187 |
    188 |
  • Module
  • 189 |
  • Object literal
  • 190 |
  • Variable
  • 191 |
  • Function
  • 192 |
  • Function with type parameter
  • 193 |
  • Index signature
  • 194 |
  • Type alias
  • 195 |
  • Type alias with type parameter
  • 196 |
197 |
    198 |
  • Enumeration
  • 199 |
  • Enumeration member
  • 200 |
  • Property
  • 201 |
  • Method
  • 202 |
203 |
    204 |
  • Interface
  • 205 |
  • Interface with type parameter
  • 206 |
  • Constructor
  • 207 |
  • Property
  • 208 |
  • Method
  • 209 |
  • Index signature
  • 210 |
211 |
    212 |
  • Class
  • 213 |
  • Class with type parameter
  • 214 |
  • Constructor
  • 215 |
  • Property
  • 216 |
  • Method
  • 217 |
  • Accessor
  • 218 |
  • Index signature
  • 219 |
220 |
    221 |
  • Inherited constructor
  • 222 |
  • Inherited property
  • 223 |
  • Inherited method
  • 224 |
  • Inherited accessor
  • 225 |
226 |
    227 |
  • Protected property
  • 228 |
  • Protected method
  • 229 |
  • Protected accessor
  • 230 |
231 |
    232 |
  • Private property
  • 233 |
  • Private method
  • 234 |
  • Private accessor
  • 235 |
236 |
    237 |
  • Static property
  • 238 |
  • Static method
  • 239 |
240 |
241 |
242 |
243 |
244 |

Generated using TypeDoc

245 |
246 |
247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /docs/classes/_exceptions_.cacheerror.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CacheError | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 63 |

Class CacheError

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Hierarchy

72 |
    73 |
  • 74 | BaseError 75 |
      76 |
    • 77 | CacheError 78 |
    • 79 |
    80 |
  • 81 |
82 |
83 |
84 |

Index

85 |
86 |
87 |
88 |

Properties

89 | 94 |
95 |
96 |
97 |
98 |
99 |

Properties

100 |
101 | 102 |

message

103 |
message: string
104 | 110 |
111 |
112 | 113 |

name

114 |
name: string = "CacheError"
115 | 121 |
122 |
123 | 124 |

Optional stack

125 |
stack: string
126 | 133 |
134 |
135 |
136 | 203 |
204 |
205 |
206 |
207 |

Legend

208 |
209 |
    210 |
  • Module
  • 211 |
  • Object literal
  • 212 |
  • Variable
  • 213 |
  • Function
  • 214 |
  • Function with type parameter
  • 215 |
  • Index signature
  • 216 |
  • Type alias
  • 217 |
  • Type alias with type parameter
  • 218 |
219 |
    220 |
  • Enumeration
  • 221 |
  • Enumeration member
  • 222 |
  • Property
  • 223 |
  • Method
  • 224 |
225 |
    226 |
  • Interface
  • 227 |
  • Interface with type parameter
  • 228 |
  • Constructor
  • 229 |
  • Property
  • 230 |
  • Method
  • 231 |
  • Index signature
  • 232 |
233 |
    234 |
  • Class
  • 235 |
  • Class with type parameter
  • 236 |
  • Constructor
  • 237 |
  • Property
  • 238 |
  • Method
  • 239 |
  • Accessor
  • 240 |
  • Index signature
  • 241 |
242 |
    243 |
  • Inherited constructor
  • 244 |
  • Inherited property
  • 245 |
  • Inherited method
  • 246 |
  • Inherited accessor
  • 247 |
248 |
    249 |
  • Protected property
  • 250 |
  • Protected method
  • 251 |
  • Protected accessor
  • 252 |
253 |
    254 |
  • Private property
  • 255 |
  • Private method
  • 256 |
  • Private accessor
  • 257 |
258 |
    259 |
  • Static property
  • 260 |
  • Static method
  • 261 |
262 |
263 |
264 |
265 |
266 |

Generated using TypeDoc

267 |
268 |
269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /docs/interfaces/_index_.relatedliteral.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RelatedLiteral | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 63 |

Interface RelatedLiteral

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Hierarchy

72 |
    73 |
  • 74 | RelatedLiteral 75 |
  • 76 |
77 |
78 |
79 |

Index

80 |
81 |
82 |
83 |

Properties

84 | 88 |
89 |
90 |
91 |
92 |
93 |

Properties

94 |
95 | 96 |

Optional nested

97 |
nested: boolean
98 | 103 |
104 |
105 | 106 |

to

107 | 108 | 113 |
114 |
115 |
116 | 207 |
208 |
209 |
210 |
211 |

Legend

212 |
213 |
    214 |
  • Module
  • 215 |
  • Object literal
  • 216 |
  • Variable
  • 217 |
  • Function
  • 218 |
  • Function with type parameter
  • 219 |
  • Index signature
  • 220 |
  • Type alias
  • 221 |
  • Type alias with type parameter
  • 222 |
223 |
    224 |
  • Enumeration
  • 225 |
  • Enumeration member
  • 226 |
  • Property
  • 227 |
  • Method
  • 228 |
229 |
    230 |
  • Interface
  • 231 |
  • Interface with type parameter
  • 232 |
  • Constructor
  • 233 |
  • Property
  • 234 |
  • Method
  • 235 |
  • Index signature
  • 236 |
237 |
    238 |
  • Class
  • 239 |
  • Class with type parameter
  • 240 |
  • Constructor
  • 241 |
  • Property
  • 242 |
  • Method
  • 243 |
  • Accessor
  • 244 |
  • Index signature
  • 245 |
246 |
    247 |
  • Inherited constructor
  • 248 |
  • Inherited property
  • 249 |
  • Inherited method
  • 250 |
  • Inherited accessor
  • 251 |
252 |
    253 |
  • Protected property
  • 254 |
  • Protected method
  • 255 |
  • Protected accessor
  • 256 |
257 |
    258 |
  • Private property
  • 259 |
  • Private method
  • 260 |
  • Private accessor
  • 261 |
262 |
    263 |
  • Static property
  • 264 |
  • Static method
  • 265 |
266 |
267 |
268 |
269 |
270 |

Generated using TypeDoc

271 |
272 |
273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /docs/classes/_exceptions_.attributeerror.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AttributeError | rest-resource 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 63 |

Class AttributeError

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Hierarchy

72 |
    73 |
  • 74 | BaseError 75 |
      76 |
    • 77 | AttributeError 78 |
    • 79 |
    80 |
  • 81 |
82 |
83 |
84 |

Index

85 |
86 |
87 |
88 |

Properties

89 | 94 |
95 |
96 |
97 |
98 |
99 |

Properties

100 |
101 | 102 |

message

103 |
message: string
104 | 110 |
111 |
112 | 113 |

name

114 |
name: string = "AttributeError"
115 | 121 |
122 |
123 | 124 |

Optional stack

125 |
stack: string
126 | 133 |
134 |
135 |
136 | 203 |
204 |
205 |
206 |
207 |

Legend

208 |
209 |
    210 |
  • Module
  • 211 |
  • Object literal
  • 212 |
  • Variable
  • 213 |
  • Function
  • 214 |
  • Function with type parameter
  • 215 |
  • Index signature
  • 216 |
  • Type alias
  • 217 |
  • Type alias with type parameter
  • 218 |
219 |
    220 |
  • Enumeration
  • 221 |
  • Enumeration member
  • 222 |
  • Property
  • 223 |
  • Method
  • 224 |
225 |
    226 |
  • Interface
  • 227 |
  • Interface with type parameter
  • 228 |
  • Constructor
  • 229 |
  • Property
  • 230 |
  • Method
  • 231 |
  • Index signature
  • 232 |
233 |
    234 |
  • Class
  • 235 |
  • Class with type parameter
  • 236 |
  • Constructor
  • 237 |
  • Property
  • 238 |
  • Method
  • 239 |
  • Accessor
  • 240 |
  • Index signature
  • 241 |
242 |
    243 |
  • Inherited constructor
  • 244 |
  • Inherited property
  • 245 |
  • Inherited method
  • 246 |
  • Inherited accessor
  • 247 |
248 |
    249 |
  • Protected property
  • 250 |
  • Protected method
  • 251 |
  • Protected accessor
  • 252 |
253 |
    254 |
  • Private property
  • 255 |
  • Private method
  • 256 |
  • Private accessor
  • 257 |
258 |
    259 |
  • Static property
  • 260 |
  • Static method
  • 261 |
262 |
263 |
264 |
265 |
266 |

Generated using TypeDoc

267 |
268 |
269 | 270 | 271 | 272 | --------------------------------------------------------------------------------