├── .babelrc
├── .prettierrc
├── .eslintrc
├── dev
├── modules
│ ├── settings.js
│ ├── xhr.js
│ ├── rest-client.js
│ └── base-model.js
├── common
│ └── helper.js
└── main.js
├── webpack.config.js
├── LICENSE
├── .gitignore
├── package.json
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "bracketSpacing": true
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "prettier/prettier": "error"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/dev/modules/settings.js:
--------------------------------------------------------------------------------
1 | const settings = {
2 | timeout: 0,
3 | endpoints: {},
4 | defaultEndpoint: '',
5 | apiPaths: {
6 | default: '',
7 | },
8 | defaultApiPath: '',
9 | headers: {
10 | accept: 'application/json',
11 | 'Content-Type': 'application/json',
12 | },
13 | setHeader: (key, value) => {
14 | settings.headers[key] = value;
15 | },
16 | modelHeaders: {},
17 | beforeEveryRequest: () => {},
18 | afterEveryRequest: () => {},
19 | };
20 | module.exports = settings;
21 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // const webpack = require('webpack');
2 | const path = require('path');
3 |
4 | const BUILD_DIR = path.resolve(__dirname, 'dist');
5 | const APP_DIR = path.resolve(__dirname, 'dev');
6 | const prod = process.argv.indexOf('-p') !== -1;
7 |
8 | const config = {
9 | entry: path.resolve(APP_DIR, 'main.js'),
10 | output: {
11 | path: BUILD_DIR,
12 | filename: prod ? 'rest-in-model.min.js' : 'rest-in-model.js',
13 | library: 'rest-in-model',
14 | libraryTarget: 'umd',
15 | umdNamedDefine: true,
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.js?/,
21 | include: APP_DIR,
22 | use: 'babel-loader',
23 | },
24 | ],
25 | },
26 | };
27 |
28 | module.exports = config;
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 engin üstün
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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | test_project
61 | dist
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rest-in-model",
3 | "version": "0.0.0",
4 | "description": "model based REST consumer library.",
5 | "main": "dist/rest-in-model.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "webpack --progress --mode=development",
9 | "build:prod": "webpack -p --progress --mode=production"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/enginustun/rest-in-model.git"
14 | },
15 | "keywords": [],
16 | "author": "engin üstün",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/enginustun/rest-in-model/issues"
20 | },
21 | "homepage": "https://github.com/enginustun/rest-in-model#readme",
22 | "readme": "https://github.com/enginustun/rest-in-model/blob/master/README.md",
23 | "devDependencies": {
24 | "@babel/cli": "^7.0.0",
25 | "@babel/core": "^7.0.0",
26 | "@babel/plugin-proposal-class-properties": "^7.0.0",
27 | "@babel/plugin-proposal-decorators": "^7.0.0",
28 | "@babel/plugin-proposal-do-expressions": "^7.0.0",
29 | "@babel/plugin-proposal-export-default-from": "^7.0.0",
30 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
31 | "@babel/plugin-proposal-function-bind": "^7.0.0",
32 | "@babel/plugin-proposal-function-sent": "^7.0.0",
33 | "@babel/plugin-proposal-json-strings": "^7.0.0",
34 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
35 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
36 | "@babel/plugin-proposal-numeric-separator": "^7.0.0",
37 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
38 | "@babel/plugin-proposal-optional-chaining": "^7.0.0",
39 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0",
40 | "@babel/plugin-proposal-throw-expressions": "^7.0.0",
41 | "@babel/plugin-syntax-dynamic-import": "^7.0.0",
42 | "@babel/plugin-syntax-import-meta": "^7.0.0",
43 | "@babel/preset-env": "^7.0.0",
44 | "babel-loader": "^8.0.0",
45 | "babel-plugin-transform-object-rest-spread": "^7.0.0-beta.3",
46 | "eslint": "^4.16.0",
47 | "eslint-plugin-import": "^2.8.0",
48 | "webpack": "^4.16.5",
49 | "webpack-cli": "^3.1.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/dev/modules/xhr.js:
--------------------------------------------------------------------------------
1 | import settings from './settings';
2 |
3 | const isValid = value => {
4 | return value || value === undefined;
5 | };
6 |
7 | class XHR {
8 | constructor() {
9 | this.xhr = new XMLHttpRequest();
10 | this.xhr.timeout = settings.timeout;
11 | this.method = 'GET';
12 | this.async = true;
13 | this.url = '';
14 | this.data = {};
15 | this.headers = {};
16 | }
17 |
18 | setHeader(name, value) {
19 | this.headers[name] = value;
20 | }
21 |
22 | exec({ beforeRequest = () => {}, afterRequest = () => {} } = {}) {
23 | return new Promise((resolve, reject) => {
24 | this.xhr.open(this.method, this.url, this.async);
25 | const headerKeys = Object.keys(this.headers);
26 | for (let i = 0; i < headerKeys.length; i += 1) {
27 | this.xhr.setRequestHeader(headerKeys[i], this.headers[headerKeys[i]]);
28 | }
29 | this.xhr.onreadystatechange = () => {
30 | if (this.xhr.readyState === 4) {
31 | settings.afterEveryRequest(this.xhr);
32 | afterRequest(this.xhr);
33 | if (this.xhr.status >= 200 && this.xhr.status < 300) {
34 | try {
35 | const responseType = this.xhr.responseType.toLowerCase();
36 | let data = this.xhr.response;
37 | if (['', 'text'].includes(responseType)) {
38 | const contentType = (
39 | this.xhr.getResponseHeader('Content-Type') || ''
40 | ).toLowerCase();
41 | if (data && contentType.includes('application/json')) {
42 | data = JSON.parse(this.xhr.responseText);
43 | }
44 | }
45 | resolve(data);
46 | } catch (error) {
47 | reject(error);
48 | }
49 | } else {
50 | try {
51 | reject(JSON.parse(this.xhr.responseText || this.xhr.statusText));
52 | } catch (error) {
53 | reject(new Error(this.xhr.statusText));
54 | }
55 | }
56 | }
57 | };
58 | isValid(settings.beforeEveryRequest(this.xhr)) &&
59 | isValid(beforeRequest(this.xhr)) &&
60 | this.xhr.send(this.data);
61 | });
62 | }
63 | }
64 |
65 | export default XHR;
66 |
--------------------------------------------------------------------------------
/dev/common/helper.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | isObject: val => Object.prototype.toString.call(val) === '[object Object]',
3 |
4 | isArray: val => Object.prototype.toString.call(val) === '[object Array]',
5 |
6 | isFunction: val =>
7 | Object.prototype.toString.call(val) === '[object Function]',
8 |
9 | defaultFunction: () => {},
10 |
11 | pathJoin: (...paths) => {
12 | const pathArray = Array.prototype.slice.call(paths);
13 | let resultPath = '';
14 | for (let i = 0; i < pathArray.length; i += 1) {
15 | let pathItem = pathArray[i];
16 | if (pathItem) {
17 | const pathItemLength = pathItem.length;
18 | if (pathItemLength > 0) {
19 | if (pathItem[pathItemLength - 1] === '/') {
20 | pathItem = pathItem.substr(0, pathItemLength - 1);
21 | }
22 | if (pathItem[0] === '/') {
23 | pathItem = pathItem.substr(1, pathItemLength - 1);
24 | }
25 | }
26 | resultPath += (resultPath.length > 0 && pathItem ? '/' : '') + pathItem;
27 | }
28 | }
29 | return resultPath;
30 | },
31 |
32 | replaceUrlParamsWithValues: (url, paramValues = {}) => {
33 | const paramKeys = Object.keys(paramValues);
34 | let newurl = url;
35 | for (let i = 0; i < paramKeys.length; i += 1) {
36 | const paramKey = paramKeys[i];
37 | newurl = newurl.replace(
38 | `{${paramKey}}`,
39 | encodeURIComponent(paramValues[paramKey])
40 | );
41 | }
42 | return newurl;
43 | },
44 |
45 | appendQueryParamsToUrl: (url, queryParams) => {
46 | let newurl = url;
47 | if (queryParams) {
48 | const paramKeys = Object.keys(queryParams);
49 | for (let i = 0; i < paramKeys.length; i += 1) {
50 | const paramKey = paramKeys[i];
51 | if (
52 | queryParams[paramKey] !== undefined &&
53 | queryParams[paramKey] !== null
54 | ) {
55 | newurl += `${
56 | newurl.indexOf('?') === -1 ? '?' : ''
57 | }${paramKey}=${encodeURIComponent(queryParams[paramKey])}${
58 | i < paramKeys.length - 1 ? '&' : ''
59 | }`;
60 | }
61 | }
62 | }
63 | return newurl;
64 | },
65 |
66 | getFormData: object => {
67 | let formData = new FormData();
68 | for (const key in object) {
69 | if (object.hasOwnProperty(key)) {
70 | if (object[key] !== undefined && object[key] !== null) {
71 | formData.append(key, object[key]);
72 | }
73 | }
74 | }
75 | return formData;
76 | },
77 | };
78 |
--------------------------------------------------------------------------------
/dev/modules/rest-client.js:
--------------------------------------------------------------------------------
1 | import settings from './settings';
2 | import XHR from './xhr';
3 | import helper from '../common/helper';
4 |
5 | const setHeaders = (request, headers) => {
6 | if (request instanceof XHR) {
7 | const modelHeaderKeys = Object.keys(headers);
8 | for (let i = 0; i < modelHeaderKeys.length; i += 1) {
9 | const headerKey = modelHeaderKeys[i];
10 | if (
11 | headerKey.toLowerCase() !== 'content-type' ||
12 | !headers[headerKey].toLowerCase().includes('form-data')
13 | ) {
14 | request.setHeader(headerKey, headers[headerKey]);
15 | }
16 | }
17 | }
18 | };
19 |
20 | const getFormData = (contentType, data) => {
21 | let formData = data;
22 | if (contentType.includes('form-data')) {
23 | formData = helper.getFormData(data);
24 | } else if (contentType.includes('application/json')) {
25 | formData = JSON.stringify(formData);
26 | } else if (contentType.includes('x-www-form-urlencoded')) {
27 | formData = helper.appendQueryParamsToUrl('', formData);
28 | formData = formData.substr(1, formData.length);
29 | }
30 | return formData;
31 | };
32 |
33 | class RestClient {
34 | constructor(_settings) {
35 | this.settings = {};
36 | if (helper.isObject(_settings)) {
37 | this.settings.endpoint =
38 | settings.endpoints[_settings.endpointName || settings.defaultEndpoint];
39 | this.settings.apiPath =
40 | settings.apiPaths[_settings.apiPathName || settings.defaultApiPath];
41 | }
42 | }
43 |
44 | sendRequest({ method = 'GET', service, data = null, headers = {} }) {
45 | const request = new XHR();
46 | headers = {
47 | ...settings.headers,
48 | ...headers
49 | }
50 | setHeaders(request, headers);
51 | request.method = method;
52 | request.url = helper.pathJoin(
53 | this.settings.endpoint,
54 | this.settings.apiPath,
55 | service
56 | );
57 | if (data) {
58 | request.data = getFormData(
59 | (headers['Content-Type'] || '').toLowerCase(),
60 | data
61 | );
62 | } else {
63 | delete request.data;
64 | }
65 | return request;
66 | }
67 |
68 | get(service, headers) {
69 | return this.sendRequest({ service, headers });
70 | }
71 |
72 | post(service, data, headers) {
73 | return this.sendRequest({ method: 'POST', service, data, headers });
74 | }
75 |
76 | put(service, data, headers) {
77 | return this.sendRequest({ method: 'PUT', service, data, headers });
78 | }
79 |
80 | patch(service, data, headers) {
81 | return this.sendRequest({ method: 'PATCH', service, data, headers });
82 | }
83 |
84 | delete(service, data, headers) {
85 | return this.sendRequest({ method: 'DELETE', service, data, headers });
86 | }
87 | }
88 |
89 | export default RestClient;
90 |
--------------------------------------------------------------------------------
/dev/main.js:
--------------------------------------------------------------------------------
1 | import RestClient from './modules/rest-client';
2 | import RestBaseModel from './modules/base-model';
3 | import _settings from './modules/settings';
4 | import helper from './common/helper';
5 |
6 | const settings = {
7 | addEndpoint: endpoint => {
8 | if (helper.isObject(endpoint) && endpoint.name && endpoint.value) {
9 | _settings.endpoints[endpoint.name] = endpoint.value;
10 | if (endpoint.default) {
11 | if (_settings.defaultEndpoint) {
12 | throw new Error('There can be only one default endpoint');
13 | }
14 | _settings.defaultEndpoint = endpoint.name;
15 | }
16 | } else if (helper.isArray(endpoint)) {
17 | endpoint.map(item => {
18 | if (item.name && item.value) {
19 | _settings.endpoints[item.name] = item.value;
20 | if (item.default) {
21 | if (_settings.defaultEndpoint) {
22 | throw new Error('There can be only one default endpoint');
23 | }
24 | _settings.defaultEndpoint = item.name;
25 | }
26 | }
27 | });
28 | } else {
29 | throw new Error(
30 | 'Endpoint provided is not valid or its format is wrong. Correct format is { name = "", value = "" }.'
31 | );
32 | }
33 | },
34 | addApiPath: apiPath => {
35 | if (helper.isObject(apiPath) && apiPath.name && apiPath.value) {
36 | _settings.apiPaths[apiPath.name] = apiPath.value;
37 | } else if (helper.isArray(apiPath)) {
38 | apiPath.map(item => {
39 | if (item.name && item.value) {
40 | _settings.apiPaths[item.name] = item.value;
41 | if (item.default) {
42 | if (_settings.defaultApiPath) {
43 | throw new Error('There can be only one default api path');
44 | }
45 | _settings.defaultApiPath = item.name;
46 | }
47 | }
48 | });
49 | } else {
50 | throw new Error(
51 | 'ApiPaths provided is not valid or its format is wrong. Correct format is { name = "", value = "" }.'
52 | );
53 | }
54 | },
55 | setDefaultEndpoint: endpointName => {
56 | if (endpointName && typeof endpointName === 'string') {
57 | if (_settings.endpoints[endpointName]) {
58 | _settings.defaultEndpoint = endpointName;
59 | } else {
60 | throw new Error(
61 | 'Endpoint name provided must be added to endpoints before.'
62 | );
63 | }
64 | } else {
65 | throw new Error(
66 | 'Default endpoint name must be provided and its type must be string.'
67 | );
68 | }
69 | },
70 | setDefaultApiPath: apiPathName => {
71 | if (apiPathName && typeof apiPathName === 'string') {
72 | if (_settings.apiPaths[apiPathName]) {
73 | _settings.defaultApiPath = apiPathName;
74 | } else {
75 | throw new Error(
76 | 'API path name provided must be added to ApiPaths before.'
77 | );
78 | }
79 | } else {
80 | throw new Error(
81 | 'Default api path name must be provided and its type must be string.'
82 | );
83 | }
84 | },
85 | setTimeout: duration => {
86 | _settings.timeout = duration;
87 | },
88 | setHeader: _settings.setHeader,
89 | set beforeEveryRequest(func = helper.defaultFunction) {
90 | _settings.beforeEveryRequest = func;
91 | },
92 | set afterEveryRequest(func = helper.defaultFunction) {
93 | _settings.afterEveryRequest = func;
94 | },
95 | set: (configs = {}) => {
96 | configs.endpoints && settings.addEndpoint(configs.endpoints);
97 | configs.apiPaths && settings.addApiPath(configs.apiPaths);
98 | configs.defaultEndpoint &&
99 | settings.setDefaultEndpoint(configs.defaultEndpoint);
100 | configs.defaultApiPath &&
101 | settings.setDefaultApiPath(configs.defaultApiPath);
102 | configs.timeout && settings.setTimeout(configs.timeout);
103 | helper.isObject(configs.headers) &&
104 | Object.entries(configs.headers).map(config =>
105 | settings.setHeader(config[0], config[1])
106 | );
107 | if (configs.beforeEveryRequest) {
108 | settings.beforeEveryRequest = beforeEveryRequest;
109 | }
110 | if (configs.afterEveryRequest) {
111 | settings.afterEveryRequest = afterEveryRequest;
112 | }
113 | },
114 | };
115 |
116 | export { RestClient, RestBaseModel, settings };
117 |
--------------------------------------------------------------------------------
/dev/modules/base-model.js:
--------------------------------------------------------------------------------
1 | import RestClient from './rest-client';
2 | import helper from '../common/helper';
3 |
4 | const getConsumerOptions = (opt, config) => ({
5 | endpointName: opt.endpointName || config.endpointName,
6 | apiPathName: opt.apiPathName || config.apiPathName,
7 | });
8 |
9 | class RestBaseModel {
10 | constructor(...[_model, ...args]) {
11 | const model = _model || {};
12 | const { constructor } = this;
13 | const config = this.getConfig();
14 | const { fields } = config;
15 |
16 | if (helper.isObject(fields)) {
17 | const fieldKeys = Object.keys(fields);
18 | const serializers = {};
19 | for (let i = 0; i < fieldKeys.length; i += 1) {
20 | const fieldKey = fieldKeys[i];
21 | if (helper.isFunction(fields[fieldKey].serializer)) {
22 | serializers[fieldKey] = fields[fieldKey].serializer;
23 | } else {
24 | const ModelClass = fields[fieldKey].modelClass;
25 | const fieldValue =
26 | model[fields[fieldKey].map] === undefined
27 | ? model[fieldKey]
28 | : model[fields[fieldKey].map];
29 | if (ModelClass && ModelClass.prototype instanceof RestBaseModel) {
30 | this[fieldKey] = new ModelClass(fieldValue, ...args);
31 | } else {
32 | this[fieldKey] = fieldValue;
33 | }
34 | if (this[fieldKey] === undefined && fields[fieldKey]) {
35 | if (helper.isArray(fields[fieldKey].default)) {
36 | this[fieldKey] = [];
37 | } else if (helper.isObject(fields[fieldKey].default)) {
38 | this[fieldKey] = {};
39 | } else if (fields[fieldKey].default !== undefined) {
40 | this[fieldKey] = fields[fieldKey].default;
41 | }
42 | }
43 | }
44 | }
45 | Object.entries(serializers).forEach(([fieldKey, serializer]) => {
46 | this[fieldKey] = serializer(...args);
47 | });
48 | }
49 |
50 | // define REST consumer
51 | if (!constructor.consumer) {
52 | Object.defineProperty(constructor, 'consumer', {
53 | value: new RestClient(getConsumerOptions({}, config)),
54 | writable: true,
55 | });
56 | }
57 | }
58 |
59 | getConfig() {
60 | return {
61 | fields: {},
62 | };
63 | }
64 |
65 | getIdField() {
66 | const { prototype } = this.constructor;
67 | const config = prototype.getConfig();
68 | if (!prototype.hasOwnProperty('_idField')) {
69 | const { fields } = config;
70 | Object.defineProperty(prototype, '_idField', {
71 | value: Object.keys(fields).find(fieldKey => {
72 | return fields[fieldKey].primary;
73 | }),
74 | enumerable: false,
75 | configurable: false,
76 | writable: false,
77 | });
78 | }
79 | return prototype._idField;
80 | }
81 |
82 | save(options) {
83 | return this.constructor.save({ ...options, model: this });
84 | }
85 |
86 | static save(options) {
87 | const opt = options || {};
88 | if (!(opt.model instanceof this)) {
89 | throw Error('model must be instance of RestBaseModel');
90 | }
91 |
92 | const { prototype } = this;
93 | const _idField = prototype.getIdField();
94 | const config = prototype.getConfig();
95 | const id = opt.model[_idField];
96 | const { fields } = config;
97 | const consumer = new RestClient(getConsumerOptions(opt, config));
98 | const path = opt.path || 'default';
99 |
100 | return new Promise((resolve, reject) => {
101 | let request;
102 | const isPost = !id;
103 | const isPatch = Array.isArray(opt.patch);
104 | const isPut = !isPost && !isPatch;
105 | const requestType = isPost ? 'post' : isPatch ? 'patch' : 'put';
106 | const requestData = {};
107 | const fieldKeys = opt.patch || Object.keys(opt.model);
108 | for (let i = 0; i < fieldKeys.length; i += 1) {
109 | const key = fieldKeys[i];
110 | requestData[(fields[key] || {}).map || key] = opt.model[key];
111 | }
112 | (isPost || isPut) && delete requestData[_idField];
113 |
114 | request = consumer[requestType](
115 | helper.replaceUrlParamsWithValues(
116 | opt.disableAutoAppendedId
117 | ? config.paths[path]
118 | : helper.pathJoin(config.paths[path], encodeURIComponent(id || '')),
119 | opt.pathData
120 | ),
121 | opt.data || requestData,
122 | config.headers || {}
123 | );
124 | if (opt.generateOnly) {
125 | resolve({ requestURL: request.url });
126 | } else {
127 | request
128 | .exec(opt)
129 | .then(response => {
130 | if (isPost) {
131 | opt.model[_idField] =
132 | response[(fields[_idField] || {}).map || _idField];
133 | }
134 | resolve({ response, request: request.xhr });
135 | })
136 | .catch(response => {
137 | reject({ response, request: request.xhr });
138 | });
139 | }
140 | });
141 | }
142 |
143 | static get(options) {
144 | const opt = options || {};
145 | const config = this.prototype.getConfig();
146 | const consumer = new RestClient(getConsumerOptions(opt, config));
147 | const path = opt.path || 'default';
148 | opt.pathData = opt.pathData || {};
149 | opt.resultField = opt.resultField || config.resultField;
150 |
151 | return new Promise((resolve, reject) => {
152 | let resultPath = config.paths[path];
153 | const { id } = opt;
154 | if (id && !opt.disableAutoAppendedId) {
155 | // if there is no pathData.id it should be set
156 | opt.pathData.id = opt.pathData.id || id;
157 | if (path === 'default') {
158 | resultPath = helper.pathJoin(config.paths[path], '{id}');
159 | }
160 | }
161 | // replace url parameters and append query parameters
162 | resultPath = helper.appendQueryParamsToUrl(
163 | helper.replaceUrlParamsWithValues(resultPath, opt.pathData),
164 | opt.queryParams
165 | );
166 | const request = consumer.get(resultPath, config.headers || {});
167 | if (opt.generateOnly) {
168 | resolve({ requestURL: request.url });
169 | } else {
170 | request
171 | .exec(opt)
172 | .then(response => {
173 | let model;
174 | if (helper.isObject(response)) {
175 | if (helper.isFunction(opt.resultField)) {
176 | model = opt.resultField(response);
177 | } else {
178 | model = new this(
179 | opt.resultField && response[opt.resultField]
180 | ? response[opt.resultField]
181 | : response,
182 | ...(opt.constructorArgs || [])
183 | );
184 | }
185 | }
186 | resolve({ model, response, request: request.xhr });
187 | })
188 | .catch(response => {
189 | reject({ response, request: request.xhr });
190 | });
191 | }
192 | });
193 | }
194 |
195 | static all(options) {
196 | const config = this.prototype.getConfig();
197 | const opt = options || {};
198 | const consumer = new RestClient(getConsumerOptions(opt, config));
199 | const path = opt.path || 'default';
200 | opt.pathData = opt.pathData || {};
201 | opt.resultListField = opt.resultListField || config.resultListField;
202 |
203 | return new Promise((resolve, reject) => {
204 | let resultPath = helper.replaceUrlParamsWithValues(
205 | config.paths[path],
206 | opt.pathData
207 | );
208 | // replace url parameters and append query parameters
209 | resultPath = helper.appendQueryParamsToUrl(
210 | helper.replaceUrlParamsWithValues(resultPath, opt.pathData),
211 | opt.queryParams
212 | );
213 | const request = consumer.get(resultPath, config.headers || {});
214 | if (opt.generateOnly) {
215 | resolve({ requestURL: request.url });
216 | } else {
217 | request
218 | .exec(opt)
219 | .then(response => {
220 | if (!helper.isArray(opt.resultList)) {
221 | opt.resultList = [];
222 | }
223 | let list;
224 | if (helper.isFunction(opt.resultListField)) {
225 | list = opt.resultListField(response);
226 | } else {
227 | list =
228 | opt.resultListField &&
229 | helper.isArray(response[opt.resultListField])
230 | ? response[opt.resultListField]
231 | : response;
232 | }
233 | opt.resultList.length = 0;
234 | if (helper.isArray(list)) {
235 | for (let i = 0; i < list.length; i += 1) {
236 | const item = list[i];
237 | const ClassName =
238 | opt.resultListItemType &&
239 | opt.resultListItemType.prototype instanceof RestBaseModel
240 | ? opt.resultListItemType
241 | : this;
242 | helper.isObject(item) &&
243 | opt.resultList.push(
244 | new ClassName(item, ...(opt.constructorArgs || []))
245 | );
246 | }
247 | }
248 | resolve({
249 | resultList: opt.resultList,
250 | response,
251 | request: request.xhr,
252 | });
253 | })
254 | .catch(response => {
255 | reject({ response, request: request.xhr });
256 | });
257 | }
258 | });
259 | }
260 |
261 | delete(options) {
262 | const opt = options || {};
263 | const { prototype } = this.constructor;
264 | const _idField = prototype.getIdField();
265 | const id = opt.id || this[_idField];
266 | return this.constructor.delete({ ...opt, id });
267 | }
268 |
269 | static delete(options) {
270 | const opt = options || {};
271 | const { prototype } = this;
272 | const config = prototype.getConfig();
273 | const { id } = opt;
274 |
275 | const consumer = new RestClient(getConsumerOptions(opt, config));
276 | const path = opt.path || 'default';
277 |
278 | return new Promise((resolve, reject) => {
279 | try {
280 | const request = consumer.delete(
281 | helper.replaceUrlParamsWithValues(
282 | opt.disableAutoAppendedId
283 | ? config.paths[path]
284 | : helper.pathJoin(
285 | config.paths[path],
286 | encodeURIComponent(id || '')
287 | ),
288 | opt.pathData
289 | ),
290 | opt.data,
291 | config.headers || {}
292 | );
293 | if (opt.generateOnly) {
294 | resolve({ requestURL: request.url });
295 | } else {
296 | request
297 | .exec(opt)
298 | .then(response => {
299 | resolve({ response, request: request.xhr });
300 | })
301 | .catch(response => {
302 | reject({ response, request: request.xhr });
303 | });
304 | }
305 | } catch (error) {
306 | reject(error);
307 | }
308 | });
309 | }
310 | }
311 |
312 | export default RestBaseModel;
313 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rest-in-model
2 |
3 | model based REST consumer library.
4 |
5 | ## Installing
6 |
7 | `npm install rest-in-model`
8 |
9 | ## Usage
10 | ### Imports
11 | ``` javascript
12 | // full import
13 | import RestInModel from 'rest-in-model';
14 |
15 | // or you can import by destructuring
16 | import { RestClient, RestBaseModel, settings } from 'rest-in-model';
17 | ```
18 | ### Settings
19 | ##### Endpoint configuration
20 | ``` javascript
21 | // Add single endpoint
22 | settings.addEndpoint({ name: 'project', value: 'https://jsonplaceholder.typicode.com/' });
23 | // Add multiple endpoint. And in this case can be only one default endpoint, otherwise throw error
24 | settings.addEndpoint([
25 | {
26 | name: 'api',
27 | value: 'https://jsonplaceholder.typicode.com/',
28 | default: true,
29 | },
30 | {
31 | name: 'projects',
32 | value: 'https://jsonplaceholder.typicode.com/projects',
33 | },
34 | ]);
35 |
36 | // set default endpoint uses that cases: if either not given default endpoint or desired to change default endpoint
37 | settings.setDefaultEndpoint('api');
38 | ```
39 | ##### Api Paths configurations
40 | ``` javascript
41 | // Add single api path
42 | settings.addApiPath({ name: 'auth', value: '/auth' });
43 | settings.addApiPath({ name: 'api', value: '/serve' });
44 | // Add multiple api path. And in this case can be only one default api path, otherwise throw error
45 | // true
46 | settings.addApiPath([
47 | {
48 | name: 'auth',
49 | value: '/auth',
50 | default: true,
51 | },
52 | {
53 | name: 'api',
54 | value: '/serve',
55 | },
56 | ]);
57 | // false
58 | settings.addApiPath([
59 | {
60 | name: 'auth',
61 | value: '/auth',
62 | default: true,
63 | },
64 | {
65 | name: 'api',
66 | value: '/serve',
67 | default: true, // Not Correct
68 | },
69 | ]);
70 | // set default api path uses that cases: if either not given default api path or desired to change default api path
71 | settings.setDefaultApiPath('api');
72 | ```
73 |
74 | ##### Set constant header configuration for each requests
75 | ``` javascript
76 | // set common headers for all models with setHeader method of settings
77 | settings.setHeader('Authorization', 'JWT xxxxxxxxxxxxxxxxxxxx...');
78 | ```
79 |
80 | > You can add additional model-specific headers to each model in `getConfig` method while defining model config. See `User` model definition below:
81 |
82 | ### Model Definition
83 |
84 | **User** model:
85 |
86 | ``` javascript
87 | import { RestBaseModel } from 'rest-in-model';
88 |
89 | class User extends RestBaseModel {
90 | getConfig() {
91 | return {
92 | // you can add additional headers
93 | headers: {
94 | 'Content-Type': 'multipart/form-data'
95 | },
96 | fields: {
97 | id: { primary: true },
98 | name: {},
99 | username: { /*map: 'user_name'*/ },
100 | email: {},
101 | company: {},
102 | phone: { /*default: null*/ },
103 | website: {},
104 | address: {}
105 | }
106 |
107 | // Normally you don't need to do this. But sometimes back-end doesn't/can't give what you want...
108 | // You can get any child from response as result list to convert if you need.
109 | resultListField: (response) => response.result.contents,
110 |
111 | paths: {
112 | default: 'users',
113 | posts: 'users/{userId}/posts'
114 | },
115 |
116 | //endpointName: '',
117 | //apiPathName: '',
118 | };
119 | }
120 | }
121 |
122 | module.exports = User;
123 | ```
124 |
125 | **Post** model:
126 |
127 | ``` javascript
128 | import { RestBaseModel } from 'rest-in-model';
129 |
130 | class Post extends RestBaseModel {
131 | getConfig() {
132 | return {
133 | fields: {
134 | id: { primary: true },
135 | title: {},
136 | body: {},
137 | userId: {}
138 | },
139 |
140 | paths: {
141 | default: 'posts',
142 | userPosts: 'users/{userId}/posts'
143 | },
144 |
145 | //endpointName: '',
146 | //apiPathName: '',
147 | }
148 | }
149 | }
150 |
151 | module.exports = Post;
152 | ```
153 |
154 | ### Methods
155 |
156 | ```javascript
157 | import User from '{model_folder}/user';
158 |
159 | const userInstance = new User();
160 |
161 | // all returns Promise
162 | userInstance.save(options);
163 | User.save(options);
164 |
165 | userInstance.delete(options);
166 | User.delete(options);
167 |
168 | User.get(options);
169 | User.all(options);
170 | ```
171 |
172 | ### Detailed Explanation
173 |
174 | **userInstance.save(options);**
175 |
176 | `options: { path, patch }`
177 |
178 | |Property|Description|Type|Default Value|
179 | |--------|-----------|----|--------|
180 | |path(optional)|one of the path attribute name in paths object defined in model|`string`|default|
181 | |patch(optional)|array of model fields that need to be updated with patch request|`string[]`|-|
182 | |generateOnly(optional)|if you want to generate only request url, use this property|`boolean`|`false`|
183 |
184 | ```javascript
185 | userInstance.save(); // userInstance.id === undefined
186 | // XHR finished loading: POST "https://jsonplaceholder.typicode.com/users"
187 |
188 | userInstance.save(); // userInstance.id !== undefined
189 | // XHR finished loading: PUT "https://jsonplaceholder.typicode.com/users/(:userId)"
190 |
191 | userInstance.save({ patch: ['name', 'lastname'] }); // userInstance.id !== undefined
192 | // XHR finished loading: PATCH "https://jsonplaceholder.typicode.com/users/(:userId)"
193 | ```
194 |
195 | ---
196 | **User.save(options);**
197 |
198 | `options: { model, path, patch }`
199 |
200 | |Property|Description|Type|Default Value|
201 | |--------|-----------|----|--------|
202 | |model(required)|instance of Model extended from RestBaseModel|`instance of RestBaseModel`|-|
203 | |path(optional)|one of the path attribute name in paths object defined in model|`string`|default|
204 | |patch(optional)|array of model fields that need to be updated with patch request|`string[]`|-|
205 | |generateOnly(optional)|if you want to generate only request url, use this property|`boolean`|`false`|
206 |
207 | ``` javascript
208 | const userInstance = new User({
209 | name: 'engin üstün',
210 | username: 'enginustun',
211 | email: 'enginustun@outlook.com',
212 | company: '-',
213 | phone: '-',
214 | website: '-',
215 | address: '-'
216 | });
217 |
218 | User.save({ model: userInstance }).then((response) => {
219 | // response is original server response
220 | // userInstance.id === id of saved record
221 | });
222 | // XHR finished loading: POST "https://jsonplaceholder.typicode.com/users"
223 | ```
224 |
225 | ---
226 | **userInstance.delete(options);**
227 |
228 | `options: { path }`
229 |
230 | |Property|Description|Type|Default Value|
231 | |--------|-----------|----|--------|
232 | |path(optional)|one of the path attribute name in paths object defined in model|`string`|default|
233 | |generateOnly(optional)|if you want to generate only request url, use this property|`boolean`|`false`|
234 |
235 | ``` javascript
236 | userInstance.delete(); // userInstance.id !== undefined
237 | // XHR finished loading: DELETE "https://jsonplaceholder.typicode.com/users/(:userId)"
238 |
239 | userInstance.delete({ id: 4 }); // userInstance.id doesn't matter
240 | // XHR finished loading: DELETE "https://jsonplaceholder.typicode.com/users/4"
241 | ```
242 |
243 | ---
244 | **User.delete(options);**
245 |
246 | `options: { id, path }`
247 |
248 | |Property|Description|Type|Default Value|
249 | |--------|-----------|----|--------|
250 | |id(required)|required id parameter of model will be deleted. if it is not provided, there will be an error thrown|`number\|string`|-|
251 | |path(optional)|one of the path attribute name in paths object defined in model|`string`|default|
252 | |generateOnly(optional)|if you want to generate only request url, use this property|`boolean`|`false`|
253 |
254 | ``` javascript
255 | User.delete(); // throws an error
256 |
257 | User.delete({ id: 4 });
258 | // XHR finished loading: DELETE "https://jsonplaceholder.typicode.com/users/4"
259 | ```
260 |
261 | ---
262 | **User.get(options);**
263 |
264 | `options: { id, path, pathData, queryParams }`
265 |
266 | |Property|Description|Type|Default Value|
267 | |--------|-----------|----|--------|
268 | |id(required)|required id parameter of model will be requested from server. if it is not provided, there will be an error thrown|`number\|string`|-|
269 | |resultField(optional)|**Better to define as config in model class for related field(s)**
for string: if the response is an object, result will be converted based on this name from the response. if this property is not provided or 'response[resultField]' is a falsy value, the response will be assumed as model itself and result will be converted from the response directly.
for function: you can return any of child/sub-child of given response object.|`string\|(response) => response.what.you.need`|-|
270 | |path(optional)|one of the path attribute name in paths object defined in model|`string`|default|
271 | |pathData(optional)|object that contains values of variables in path specified|`object`|-|
272 | |queryParams(optional)|object that contains keys and values of query parameters|`object`|-|
273 | |generateOnly(optional)|if you want to generate only request url, use this property|`boolean`|`false`|
274 |
275 | ``` javascript
276 | User.get(); // throws an error
277 |
278 | User.get({ id: 2 }).then(({ model, response }) => {
279 | // 'model' parameter is an instance of User that automatically converted from the server's response.
280 | // 'response' parameter is original response which is coming from server.
281 | });
282 | // XHR finished loading: GET "https://jsonplaceholder.typicode.com/users/2"
283 |
284 | // see User.all() function for usage of pathData property.
285 | ```
286 |
287 | ---
288 | **User.all(options);**
289 |
290 | `options: { resultList, resultListField, resultListItemType, path, pathData, queryParams }`
291 |
292 | |Property|Description|Type|Default Value|
293 | |--------|-----------|----|--------|
294 | |resultList(optional)|array object that will be filled models into it|`[]` reference|-|
295 | |resultListField(optional)|**Better to define as config in model class for related field(s)**
for string: if the response is an object, result list will be converted based on this name from the response. if this property is not provided or 'response[resultListField]' is not an array, the response will be assumed as model list itself and result will be converted from the response directly.
for function: you can return any of child/sub-child of given response object.|`string\|(response) => response.what.you.need`|-|
296 | |resultListItemType(optional)|it needs to be provided if the result type is different from class type itself.|a model class that is inherited from 'RestBaseModel'|itself|
297 | |path(optional)|one of the path attribute name in paths object defined in model|`string`|default|
298 | |pathData(optional)|object that contains values of variables in path specified|`object`|-|
299 | |queryParams(optional)|object that contains keys and values of query parameters|`object`|-|
300 | |generateOnly(optional)|if you want to generate only request url, use this property|`boolean`|`false`|
301 |
302 | ``` javascript
303 | User.all().then(({ resultList, response }) => {
304 | // 'resultList' parameter is array of model which is converted from response
305 | // 'response' parameter is original server response
306 | });
307 | // XHR finished loading: GET "https://jsonplaceholder.typicode.com/users"
308 |
309 | // recommended usage is above
310 | var list = [];
311 | User.all({ resultList: list }).then(() => {
312 | // 'list' array will be filled with models which are received from server's response.
313 | });
314 | // XHR finished loading: GET "https://jsonplaceholder.typicode.com/users"
315 |
316 | // path, pathData and resultListItemType usage
317 | User.all({ path: 'posts', pathData: { userId: 2 }, resultListItemType: Post }).then(({ resultList, response }) => {
318 | // 'resultList' parameter is an array of Post instances which belongs to user which has id=2.
319 | });
320 | // XHR finished loading: GET "https://jsonplaceholder.typicode.com/users/2/posts"
321 | // it works properly but this way is not recommended.
322 |
323 | // best practice of this usage is below
324 | Post.all({ path: 'userPosts', pathData: { userId: 2 } }).then(({ resultList, response }) => {
325 | // 'resultList' parameter is an array of Post instances which belongs to user which has id=2.
326 | });
327 | // sends same request as above
328 | // XHR finished loading: GET "https://jsonplaceholder.typicode.com/users/2/posts"
329 | ```
330 |
--------------------------------------------------------------------------------