├── .gitignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── builder.js ├── cancel.js ├── cast.js ├── contentTypes.js ├── index.js ├── request.js ├── resource.js ├── router.js ├── transform.js ├── transforms │ ├── collection.js │ ├── dataCollection.js │ └── model.js └── utils.js ├── tests ├── feature │ ├── __snapshots__ │ │ └── crud.spec.js.snap │ ├── cancel.spec.js │ ├── cast.spec.js │ ├── crud.spec.js │ ├── media.spec.js │ ├── pagination.spec.js │ └── query.spec.js ├── models │ ├── cast.js │ ├── comment.js │ ├── like.js │ ├── media.js │ └── post.js └── setup │ ├── client.js │ ├── resource.js │ └── router.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elodo 2 | 3 | Communicate with any REST API in an elegant and object oriented way. 4 | 5 | ## TOC 6 | 7 | [Quick preview](#quick-preview) 8 | 9 | [Installation](#installation) 10 | 11 | [Setup](#setup) 12 | 13 | [Fetching resources](#fetching-resources) 14 | 15 | [Persisting resources](#persisting-resources) 16 | 17 | [Relationships](#relationships) 18 | 19 | [Cancel requests](#cancel-requests) 20 | 21 | [Cast attributes](#cast-attributes) 22 | 23 | [File uploads](#file-uploads) 24 | 25 | [Options](#options) 26 | 27 | ## Quick preview 28 | 29 | ```js 30 | const post = Post.$create({ 31 | title: 'What is Elodo?', 32 | body: 'Elodo is an easy way to use resource models in front-end applications!', 33 | }); 34 | 35 | // POST request to "http://api.com/api/posts" 36 | await post.$store(); 37 | ``` 38 | 39 | ```js 40 | post.title = 'What is Elodo? Amazing!'; 41 | 42 | // PUT request to "http://api.com/api/posts/1" 43 | await post.$update(); 44 | ``` 45 | 46 | ```js 47 | // GET request to "http://api.com/api/posts?include=comments,author" 48 | const posts = await Post 49 | .$request() 50 | .$include('comments', 'author') 51 | .$index(); 52 | 53 | // Each post is tranformed to an instance of Post 54 | const post = posts[0]; 55 | await post.$update(); 56 | ``` 57 | 58 | ## Installation 59 | 60 | ``` 61 | npm install elodo 62 | ``` 63 | 64 | ``` 65 | npm install axios 66 | ``` 67 | 68 | ## Setup 69 | 70 | ### Structure 71 | 72 | ``` 73 | /src 74 | ├── /api 75 | | ├── /resources 76 | | | ├── post.js 77 | | | └── comment.js 78 | | └── client.js 79 | | └── resource.js 80 | | └── router.js 81 | ├── ... 82 | └── app.js 83 | ``` 84 | 85 | ### Resource 86 | 87 | `api/resource.js` 88 | 89 | ```js 90 | import { createResource } from 'elodo'; 91 | import { client } from './client'; 92 | import { router } from './router'; 93 | 94 | export const Resource = createResource({ 95 | client, 96 | router, 97 | }); 98 | ``` 99 | 100 | `api/router.js` 101 | 102 | ```js 103 | import { createRouter } from 'elodo'; 104 | 105 | export const router = createRouter(); 106 | 107 | router.prefix('http://api.com/api/v1/', function (router) { 108 | // Register each crud action 109 | router.index('posts', () => `posts`); 110 | router.store('posts', () => `posts`); 111 | router.show('posts', (post) => `posts/${post.id}`); 112 | router.update('posts', (post) => `posts/${post.id}`); 113 | router.destroy('posts', (post) => `posts/${post.id}`); 114 | 115 | // Or register all crud actions at once 116 | // $index GET /posts 117 | // $store POST /posts 118 | // $show GET /posts/{post.id} 119 | // $store POST /posts/{post.id} 120 | // $update PUT /posts/{post.id} 121 | // $destroy DELETE /posts/{post.id} 122 | router.resource('posts'); 123 | }); 124 | ``` 125 | 126 | `api/client.js` 127 | 128 | ```js 129 | import Axios from 'axios'; 130 | 131 | export const client = function () { 132 | return Axios.create(); 133 | }; 134 | ``` 135 | 136 | `api/resources/post.js` 137 | 138 | ```js 139 | import { Resource } from '../resource'; 140 | 141 | export class Post extends Resource { 142 | get _attributes () { 143 | return { 144 | id: null, 145 | title: null, 146 | body: null, 147 | }; 148 | } 149 | 150 | get _route () { 151 | return 'posts'; // Used to create the route path 152 | } 153 | } 154 | ``` 155 | 156 | Now you can use the post resource in `app.js` or any other file 157 | 158 | ```js 159 | import { Post } from './api/resources/post'; 160 | 161 | const posts = await Post.$index(); // GET request to "/posts" 162 | 163 | const post = await Post.$find(1); // GET request to "/posts/1" 164 | 165 | await post.$show(); // GET request to "/posts/1" 166 | 167 | await post.$store(); // POST request to "/posts" 168 | 169 | await post.$update(); // PUT request to "/posts/1" 170 | 171 | await post.$destroy(); // DELETE request to "/posts/1" 172 | ``` 173 | 174 | ## Fetching resources 175 | 176 | [Fetch list of resources](#fetch-list-of-resources) 177 | 178 | [Fetch single resource](#fetch-single-resource) 179 | 180 | [Fetch filtered list of resources](#fetch-filtered-list-of-resources) 181 | 182 | [Fetch list of resources sorted by an attribute](#fetch-list-of-resources-sorted-by-an-attribute) 183 | 184 | [Fetch list of resources with relationships](#fetch-list-of-resources-with-relationships) 185 | 186 | [Fetch list of resources with selected fields](#fetch-list-of-resources-with-selected-fields) 187 | 188 | [Fetch list of resources with appended fields](#fetch-list-of-resources-with-appended-fields) 189 | 190 | [Fetch list of resources with specific params](#fetch-list-of-resources-with-specific-params) 191 | 192 | [Fetch paginated list of resources](#fetch-paginated-list-of-resources) 193 | 194 | ### Fetch list of resources 195 | 196 | ```js 197 | // GET /posts 198 | const posts = await Post.$index(); 199 | ``` 200 | 201 | ### Fetch single resource 202 | 203 | Find a resource by primary id 204 | 205 | ```js 206 | // GET /posts/1 207 | const post = await Post.$find(1); 208 | ``` 209 | 210 | Show a resource by its attributes 211 | 212 | ```js 213 | // GET /posts/1 214 | const post = Post.$create({ id: 1 }); 215 | await post.$show(); 216 | ``` 217 | 218 | You can also use the `$refresh` alias 219 | 220 | ```js 221 | // GET /posts/1 222 | const post = Post.$create({ id: 1 }); 223 | await post.$refresh(); 224 | ``` 225 | 226 | ### Fetch filtered list of resources 227 | 228 | ```js 229 | // GET /posts?filter[title]=Hello 230 | const posts = await Post 231 | .$request() 232 | .$filter('title', 'Hello') 233 | .$index(); 234 | ``` 235 | 236 | You can also use the `where` alias 237 | 238 | ```js 239 | // GET /posts?filter[title]=Hello 240 | const posts = await Post 241 | .$request() 242 | .$where('title', 'Hello') 243 | .$index(); 244 | ``` 245 | 246 | ### Fetch list of resources sorted by an attribute 247 | 248 | ```js 249 | // GET /posts?sort=title 250 | const posts = await Post 251 | .$request() 252 | .$sort('title') 253 | .$index(); 254 | ``` 255 | 256 | Sort descending 257 | 258 | ```js 259 | // GET /posts?sort=-title 260 | const posts = await Post 261 | .$request() 262 | .$sortDesc('title') 263 | .$index(); 264 | ``` 265 | 266 | Combine multiple sorts 267 | 268 | ```js 269 | // GET /posts?sort=id,-name 270 | const posts = await Post 271 | .$request() 272 | .$sort('id') 273 | .$sortDesc('name') 274 | .$index(); 275 | ``` 276 | 277 | ### Fetch list of resources with relationships 278 | 279 | ```js 280 | // GET /posts?include=comments,author 281 | const posts = await Post 282 | .$request() 283 | .$include('comments', 'author') 284 | .$index(); 285 | ``` 286 | 287 | ### Fetch list of resources with selected fields 288 | 289 | ```js 290 | // GET /posts?fields[posts]=id,title 291 | const posts = await Post 292 | .$request() 293 | .$fields({ 'posts': ['id', 'title'] }) 294 | .$index(); 295 | ``` 296 | 297 | ### Fetch list of resources with appended fields 298 | 299 | ```js 300 | // GET /posts?append=published_at 301 | const posts = await Post 302 | .$request() 303 | .$append('published_at') 304 | .$index(); 305 | ``` 306 | 307 | ### Fetch list of resources with limited resultes 308 | 309 | ```js 310 | // GET /posts?limit=15 311 | const posts = await Post 312 | .$request() 313 | .$limit(15) 314 | .$index(); 315 | ``` 316 | 317 | ### Fetch list of resources with specific params 318 | 319 | ```js 320 | // GET /posts?param=value 321 | const posts = await Post 322 | .$request() 323 | .$param('param', 'value') 324 | .$index(); 325 | ``` 326 | 327 | ### Fetch paginated list of resources 328 | 329 | ```js 330 | // GET /posts?page[size]=15&page[number]=1 331 | const pagination = await Post 332 | .$request() 333 | .$page({ 334 | size: 15, 335 | number: 1, 336 | }) 337 | .$index(); 338 | 339 | // Pagination data is tranformed to instances of Post 340 | const post = pagination.data[0]; 341 | await post.$update(); 342 | ``` 343 | 344 | Set the page directly 345 | 346 | ```js 347 | // GET /posts?page=1 348 | await Post 349 | .$request() 350 | .$page(1) 351 | .$index(); 352 | ``` 353 | 354 | Use with the limit param 355 | 356 | ```js 357 | // GET /posts?page=1&limit=15 358 | await Post 359 | .$request() 360 | .$page(1) 361 | .$limit(15) 362 | .$index(); 363 | ``` 364 | 365 | ## Persisting resources 366 | 367 | [Store resource](#store-resource) 368 | 369 | [Update resource](#update-resource) 370 | 371 | [Delete resource](#delete-resource) 372 | 373 | ### Store resource 374 | 375 | ```js 376 | // POST /posts 377 | const post = Post.$create({ title: 'Hello' }); 378 | await post.$store(); 379 | ``` 380 | 381 | Or use the `$save` alias 382 | 383 | ```js 384 | // POST /posts 385 | const post = Post.$create({ title: 'Hello' }); 386 | await post.$save(); 387 | ``` 388 | 389 | ### Update resource 390 | 391 | ```js 392 | // Put /posts/1 393 | const post = await Post.$find(1); 394 | post.title = 'Updated title'; 395 | await post.$update(); 396 | ``` 397 | 398 | ### Delete resource 399 | 400 | ```js 401 | // DELETE /posts/1 402 | const post = await Post.$find(1); 403 | await post.$destroy(); 404 | ``` 405 | 406 | Or use the `$delete` alias 407 | 408 | ```js 409 | // DELETE /posts/1 410 | const post = await Post.$find(1); 411 | await post.$delete(); 412 | ``` 413 | 414 | ## Relationships 415 | 416 | Start with defining your relationship routes 417 | 418 | ```js 419 | import { createRouter } from 'elodo'; 420 | 421 | export const router = createRouter(); 422 | 423 | router.prefix('http://api.com/api/v1/', function (router) { 424 | // $index GET /posts 425 | // $store POST /posts 426 | // $show GET /posts/{post.id} 427 | // $store POST /posts/{post.id} 428 | // $update PUT /posts/{post.id} 429 | // $destroy DELETE /posts/{post.id} 430 | router.resource('posts'); 431 | 432 | // $index GET /posts/{post.id}/comments 433 | // $store POST /posts/{post.id}/comments 434 | // $show GET /posts/{post.id}/comments/{comment.id} 435 | // $store POST /posts/{post.id}/comments/{comment.id} 436 | // $update PUT /posts/{post.id}/comments/{comment.id} 437 | // $destroy DELETE /posts/{post.id}/comments/{comment.id} 438 | router.resource('posts.comments'); 439 | }); 440 | ``` 441 | 442 | ```js 443 | const post = Post.$find(1); 444 | const comment = Comment.$create({ body: 'Hello' }); 445 | 446 | // POST /posts/1/comments 447 | await comment.$parent(post).$store(); 448 | 449 | // POST /comments 450 | await comment.$store(); 451 | ``` 452 | 453 | ## Cancel requests 454 | 455 | ```js 456 | import { Post } from './resources/post'; 457 | import { createSource, isCancel } from 'elodo'; 458 | 459 | const source = createSource(); 460 | 461 | Post.$source(source) 462 | .$index() 463 | .then((posts) => { 464 | ... 465 | }) 466 | .catch((error) => { 467 | if (isCancel(error)) { 468 | // Request was canceled 469 | } 470 | }); 471 | 472 | source.cancel(); 473 | ``` 474 | 475 | Cancel any crud action 476 | 477 | ```js 478 | import { Post } from './resources/post'; 479 | import { createSource, isCancel } from 'elodo'; 480 | 481 | const source = createSource(); 482 | const post = Post.$create(); 483 | 484 | post.$source(source) 485 | .$store() 486 | .then((posts) => { 487 | // Render posts 488 | }) 489 | .catch((error) => { 490 | if (isCancel(error)) { 491 | // Request was canceled 492 | } 493 | }); 494 | 495 | source.cancel(); 496 | ``` 497 | 498 | ## Cast attributes 499 | 500 | [Cast nested properties](#cast-nested-properties) 501 | 502 | [Custom cast](#custom-cast) 503 | 504 | [Cast to relationship](#cast-to-relationship) 505 | 506 | The cast property allows you to convert attributes coming from the server. 507 | 508 | Build in cast types are: `number`, `float`, `int`, `bigint`, `boolean`, `string`, `date`, `json`, and `json.parse`. 509 | 510 | ```js 511 | import { Resource } from '../resource'; 512 | 513 | export class Post extends Resource { 514 | get _attributes () { 515 | return { 516 | id: null, 517 | title: null, 518 | published_at: null, 519 | }; 520 | } 521 | 522 | get _casts () { 523 | return { 524 | id: 'int', 525 | published_at: 'date', 526 | }; 527 | } 528 | 529 | get _route () { 530 | return 'posts'; 531 | } 532 | } 533 | ``` 534 | 535 | ### Cast nested properties 536 | 537 | ```js 538 | import { Resource } from '../resource'; 539 | 540 | export class Post extends Resource { 541 | get _attributes () { 542 | return { 543 | object: { 544 | prop: null, 545 | }, 546 | }; 547 | } 548 | 549 | get _casts () { 550 | return { 551 | 'object.prop': 'boolean', 552 | }; 553 | } 554 | 555 | get _route () { 556 | return 'posts'; 557 | } 558 | } 559 | ``` 560 | 561 | ### Custom cast 562 | 563 | Add a function to that returns the transformed value. 564 | 565 | ```js 566 | import { Resource } from '../resource'; 567 | 568 | export class Post extends Resource { 569 | get _attributes () { 570 | return { 571 | id: null, 572 | title: null, 573 | published_at: null, 574 | }; 575 | } 576 | 577 | get _casts () { 578 | return { 579 | published_at: (value) => new Date(value), 580 | }; 581 | } 582 | 583 | get _route () { 584 | return 'posts'; 585 | } 586 | } 587 | ``` 588 | 589 | ### Cast to relationship 590 | 591 | ```js 592 | import { Resource } from '../resource'; 593 | import { Comment } from './comment'; 594 | 595 | export class Post extends Resource { 596 | get _attributes () { 597 | return { 598 | id: null, 599 | title: null, 600 | comments: null, 601 | }; 602 | } 603 | 604 | get _casts () { 605 | return { 606 | comments: (value) => Comment.$create(value), 607 | }; 608 | } 609 | 610 | get _route () { 611 | return 'posts'; 612 | } 613 | } 614 | ``` 615 | 616 | ## File uploads 617 | 618 | Change the content type of the resource to `formdata` 619 | 620 | `./api/resources/post.js` 621 | 622 | ```js 623 | import { Resource } from '../resource'; 624 | 625 | export class Post extends Resource { 626 | get _attributes () { 627 | return { 628 | id: null, 629 | thumbnail: null, 630 | }; 631 | } 632 | 633 | get _contentType () { 634 | return 'formdata'; 635 | } 636 | 637 | get _route () { 638 | return 'posts'; 639 | } 640 | } 641 | ``` 642 | 643 | In `./app.js` or any other file 644 | 645 | ```js 646 | import { Post } from './api/resources/post'; 647 | 648 | const fileInput = document.querySelector('#fileInput'); 649 | const file = fileInput.files[0]; 650 | 651 | const post = Post.$create({ file }); 652 | 653 | // POST request to "/posts" with file in formdata 654 | await post.$store(); 655 | ``` 656 | 657 | ## Options 658 | 659 | To add or override options you can create a base resource 660 | 661 | ```js 662 | import { createResource } from 'elodo'; 663 | import { client } from './client'; 664 | import { router } from './router'; 665 | 666 | const BaseResource = createResource({ 667 | client, 668 | router, 669 | }); 670 | 671 | export class Resource extends BaseResource { 672 | /** 673 | * Default attributes 674 | */ 675 | get _attributes () { 676 | return { 677 | id: null, 678 | }; 679 | } 680 | 681 | /** 682 | * Default route 683 | */ 684 | get _route () { 685 | return 'default.route'; 686 | } 687 | 688 | /** 689 | * Default primary key 690 | */ 691 | get _primaryKey () { 692 | return 'id'; 693 | } 694 | 695 | /** 696 | * Default casts 697 | */ 698 | get _casts () { 699 | return {}; 700 | } 701 | 702 | /** 703 | * Default content type 704 | */ 705 | get _contentType () { 706 | return 'json'; 707 | } 708 | 709 | /** 710 | * Custom $latest function 711 | */ 712 | static $latest () { 713 | return this.$request() 714 | .$limit(5) 715 | .$sortDesc('created_at') 716 | .$index() 717 | ; 718 | } 719 | } 720 | ``` 721 | 722 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | const production = { 3 | presets: [ 4 | [ 5 | '@babel/preset-env' 6 | ], 7 | ], 8 | }; 9 | 10 | const test = { 11 | presets: [ 12 | [ 13 | '@babel/preset-env', 14 | { 15 | targets: { 16 | node: 'current', 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | 23 | return api.env('test') ? test : production; 24 | } 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: false, 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elodo", 3 | "version": "2.1.0", 4 | "main": "dist/index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/MaximVanhove/elodo" 8 | }, 9 | "keywords": [ 10 | "elodo", 11 | "rest", 12 | "restfull", 13 | "api", 14 | "resource", 15 | "resources" 16 | ], 17 | "scripts": { 18 | "test": "NODE_ENV=test jest", 19 | "test:watch": "NODE_ENV=test jest --watchAll", 20 | "dev": "NODE_ENV=development webpack --config webpack.dev.js", 21 | "watch": "NODE_ENV=development webpack --config webpack.dev.js --watch", 22 | "build": "NODE_ENV=production webpack --config=webpack.prod.js" 23 | }, 24 | "files": [ 25 | "/dist", 26 | "/src" 27 | ], 28 | "author": "Maxim Vanhove", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@babel/core": "^7.10.2", 32 | "@babel/polyfill": "^7.10.1", 33 | "@babel/preset-env": "^7.10.2", 34 | "axios": "^0.19.0", 35 | "babel-jest": "^24.9.0", 36 | "babel-loader": "^8.1.0", 37 | "jest": "^24.9.0", 38 | "lodash": "^4.17.15", 39 | "moxios": "^0.4.0", 40 | "qs": "^6.9.3", 41 | "uglifyjs-webpack-plugin": "^2.2.0", 42 | "webpack": "^4.43.0", 43 | "webpack-cli": "^3.3.11", 44 | "webpack-merge": "^4.2.2" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/MaximVanhove/elodo/issues" 48 | }, 49 | "homepage": "https://github.com/MaximVanhove/elodo#readme" 50 | } 51 | -------------------------------------------------------------------------------- /src/builder.js: -------------------------------------------------------------------------------- 1 | export const useBuilder = function () { 2 | var map = new Map(); 3 | 4 | const get = function (key, fallback) { 5 | if (!map.has(key)) { 6 | return fallback; 7 | } 8 | 9 | return map.get(key) 10 | }; 11 | 12 | const tap = function (key, callback, fallback) { 13 | map.set(key, callback(get(key, fallback))); 14 | }; 15 | 16 | const set = function () { 17 | return map.set(...arguments) 18 | }; 19 | 20 | const has = function () { 21 | return map.has(...arguments); 22 | } 23 | 24 | return { 25 | get, 26 | set, 27 | has, 28 | tap, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/cancel.js: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | 3 | export const createSource = () => { 4 | return Axios.CancelToken.source(); 5 | }; 6 | 7 | export const isCancel = (error) => { 8 | return Axios.isCancel(error); 9 | }; 10 | -------------------------------------------------------------------------------- /src/cast.js: -------------------------------------------------------------------------------- 1 | import { get, has, set } from './utils'; 2 | 3 | export const Cast = { 4 | cast(model, attribute, cast) { 5 | if (!has(model, attribute)) { 6 | return model; 7 | } 8 | 9 | if (typeof cast === 'function') { 10 | const callback = cast; 11 | return this.transform(model, attribute, callback); 12 | } 13 | 14 | const [key, argument] = cast.split(':'); 15 | 16 | if (!has(this, key)) { 17 | throw new Error(`Cast '${key}' does not exist`); 18 | } 19 | 20 | const value = get(model, attribute); 21 | const castedValue = this[key]({ key, value, argument, model }); 22 | 23 | set(model, attribute, castedValue); 24 | 25 | return model; 26 | }, 27 | 28 | transform (model, attribute, callback) { 29 | const value = get(model, attribute); 30 | const castedValue = callback(value); 31 | 32 | set(model, attribute, castedValue); 33 | 34 | return model; 35 | }, 36 | 37 | 'float' ({ value }) { 38 | return parseFloat(value); 39 | }, 40 | 41 | 'int' ({ value }) { 42 | return parseInt(value); 43 | }, 44 | 45 | 'integer' ({ value }) { 46 | return parseInt(value); 47 | }, 48 | 49 | 'number' ({ value }) { 50 | return Number(value); 51 | }, 52 | 53 | 'bigint' ({ value }) { 54 | return BigInt(value); 55 | }, 56 | 57 | 'bool' ({ value }) { 58 | return Boolean(value); 59 | }, 60 | 61 | 'boolean' ({ value }) { 62 | return Boolean(value); 63 | }, 64 | 65 | 'string' ({ value }) { 66 | return String(value); 67 | }, 68 | 69 | 'date' ({ value }) { 70 | return new Date(value); 71 | }, 72 | 73 | 'json' () { 74 | return this['json.stringify'](...arguments); 75 | }, 76 | 77 | 'json.parse' ({ value }) { 78 | if (typeof value !== 'string') { 79 | return value; 80 | } 81 | 82 | return JSON.parse(value); 83 | }, 84 | 85 | 'json.stringify' ({ value }) { 86 | return JSON.stringify(value); 87 | }, 88 | 89 | 'relationship' ({ value, argument, model }) { 90 | if (!model._relationships.hasOwnProperty(argument)) { 91 | throw new Error(`Relationship '${argument}' does not exist`); 92 | } 93 | 94 | const repository = model._relationships[argument]; 95 | 96 | return repository.$create(value); 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /src/contentTypes.js: -------------------------------------------------------------------------------- 1 | export const ContentTypes = { 2 | json: 'application/json', 3 | formdata: 'multipart/form-data', 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from './router'; 2 | import { createResource } from './resource'; 3 | import { createSource, isCancel } from './cancel'; 4 | 5 | export { 6 | isCancel, 7 | createRouter, 8 | createSource, 9 | createResource, 10 | }; 11 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import { stringify } from './utils'; 2 | import { transformResponse, transformError } from './transform'; 3 | import { useBuilder } from './builder'; 4 | import { ContentTypes } from './contentTypes'; 5 | 6 | class RequestBuilder { 7 | constructor(model) { 8 | this.builder = useBuilder(); 9 | 10 | this.builder.set('model', model); 11 | this.builder.set('route', model._route); 12 | this.builder.set('router', model._router); 13 | this.builder.set('client', model._client); 14 | this.builder.set('primaryKey', model._primaryKey); 15 | this.builder.set('headers.contentType', model._contentType); 16 | this.builder.set('parents', []); 17 | this.builder.set('params', {}); 18 | 19 | return this; 20 | } 21 | 22 | static create () { 23 | return new this(...arguments); 24 | } 25 | 26 | $route (route) { 27 | this.builder.set('route', route); 28 | 29 | return this; 30 | } 31 | 32 | $parents (...parents) { 33 | this.builder.set('parents', parents); 34 | 35 | return this; 36 | } 37 | 38 | $parent () { 39 | return this.$parents(...arguments); 40 | } 41 | 42 | $source (source) { 43 | this.builder.set('source', source); 44 | 45 | return this; 46 | } 47 | 48 | $contentType (type) { 49 | this.builder.set('headers.contentType', type); 50 | 51 | return this; 52 | } 53 | 54 | $param (name, value) { 55 | if (typeof name === 'object') { 56 | return this.$params(...arguments); 57 | } 58 | 59 | this.builder.tap('params', function (params) { 60 | params[name] = value; 61 | return params; 62 | }); 63 | 64 | return this; 65 | } 66 | 67 | $params (params) { 68 | this.builder.tap('params', function (value) { 69 | return Object.assign(value, params); 70 | }); 71 | 72 | return this; 73 | } 74 | 75 | $filters (filters) { 76 | this.builder.tap('params', function (params) { 77 | params.filter = params.filter || {}; 78 | params.filter = Object.assign({}, filters); 79 | return params; 80 | }); 81 | 82 | return this; 83 | } 84 | 85 | $filter (key, value) { 86 | if (typeof key === 'object') { 87 | return this.$filters(key); 88 | } 89 | 90 | return this.$filters({ [key]: value }); 91 | } 92 | 93 | $where () { 94 | return this.$filter(...arguments); 95 | } 96 | 97 | $include (...values) { 98 | this.builder.tap('params.include', (value) => { 99 | value.push(...values); 100 | return value; 101 | }, []); 102 | 103 | this.builder.tap('params', (value) => { 104 | value.include = this.builder.get('params.include').join(','); 105 | return value; 106 | }); 107 | 108 | return this; 109 | } 110 | 111 | $sort (...values) { 112 | this.builder.tap('params.sort', (value) => { 113 | value.push(...values); 114 | return value; 115 | }, []); 116 | 117 | this.builder.tap('params', (value) => { 118 | value.sort = this.builder.get('params.sort').join(','); 119 | return value; 120 | }); 121 | 122 | return this; 123 | } 124 | 125 | $orderBy () { 126 | return this.$sort(...arguments); 127 | } 128 | 129 | $sortDesc(...values) { 130 | const valuesDesc = values.map(function (value) { 131 | return '-' + value; 132 | }); 133 | 134 | return this.$sort(valuesDesc); 135 | } 136 | 137 | $orderByDesc () { 138 | return this.$sortDesc(...arguments); 139 | } 140 | 141 | $fields (fields) { 142 | this.builder.tap('params.fields', (value) => { 143 | return Object.assign(value, fields); 144 | }, {}); 145 | 146 | this.builder.tap('params', (params) => { 147 | params.fields = params.fields || {}; 148 | 149 | const resources = this.builder.get('params.fields'); 150 | for (const resource in resources) { 151 | params.fields[resource] = resources[resource].join(','); 152 | } 153 | 154 | return params; 155 | }); 156 | 157 | return this; 158 | } 159 | 160 | $append (...values) { 161 | this.builder.tap('params.append', (value) => { 162 | value.push(...values); 163 | return value; 164 | }, []); 165 | 166 | this.builder.tap('params', (value) => { 167 | value.append = this.builder.get('params.append').join(','); 168 | return value; 169 | }); 170 | 171 | return this; 172 | } 173 | 174 | $page (page) { 175 | this.builder.tap('params', function (params) { 176 | params.page = page; 177 | return params; 178 | }); 179 | 180 | return this; 181 | } 182 | 183 | $limit (limit) { 184 | this.builder.tap('params', function (params) { 185 | params.limit = limit; 186 | return params; 187 | }); 188 | 189 | return this; 190 | } 191 | 192 | $select () { 193 | return this.$fields(...arguments); 194 | } 195 | 196 | $find (id) { 197 | const key = this.$getPrimaryKey(); 198 | const attributes = { [key]: id }; 199 | 200 | this.$getModel().$fill(attributes); 201 | 202 | return this.$show(); 203 | } 204 | 205 | $index (options = {}) { 206 | const method = options.method || 'get'; 207 | const data = options.data || null; 208 | const url = options.url || this.$getUrl('index'); 209 | 210 | return this.$request({ 211 | method, 212 | data, 213 | url, 214 | }); 215 | } 216 | 217 | $show (options = {}) { 218 | const method = options.method || 'get'; 219 | const data = options.data || null; 220 | const url = options.url || this.$getUrl('show'); 221 | 222 | return this.$request({ 223 | method, 224 | data, 225 | url, 226 | }); 227 | } 228 | 229 | $refresh () { 230 | return this.$show(...arguments); 231 | } 232 | 233 | $store (options = {}) { 234 | const method = options.method || 'post'; 235 | const data = options.data || this.$getData(); 236 | const url = options.url || this.$getUrl('store'); 237 | 238 | return this.$request({ 239 | method, 240 | data, 241 | url, 242 | }); 243 | } 244 | 245 | $save () { 246 | return this.$store(...arguments); 247 | } 248 | 249 | $update (options = {}) { 250 | const method = options.method || 'put'; 251 | const data = options.data || this.$getData(); 252 | const url = options.url || this.$getUrl('update'); 253 | 254 | return this.$request({ 255 | method, 256 | data, 257 | url, 258 | }); 259 | } 260 | 261 | $destroy (options = {}) { 262 | const method = options.method || 'delete'; 263 | const data = options.data || null; 264 | const url = options.url || this.$getUrl('destroy'); 265 | 266 | return this.$request({ 267 | method, 268 | data, 269 | url, 270 | }); 271 | } 272 | 273 | $delete () { 274 | return this.$destroy(...arguments); 275 | } 276 | 277 | $request (options) { 278 | const model = this.$getModel(); 279 | const client = this.$getClient(); 280 | const config = this.$getConfig(options); 281 | 282 | return client() 283 | .request(config) 284 | .then(response => transformResponse(response, model)) 285 | .catch(error => transformError(error, model)); 286 | } 287 | 288 | $getConfig (options = {}) { 289 | const config = Object.assign(options, { 290 | headers: this.$getHeaders(), 291 | params: this.$getParams(), 292 | paramsSerializer: this.$getParamsSerializer(), 293 | }); 294 | 295 | if (this.builder.has('source')) { 296 | config.cancelToken = this.builder.get('source').token; 297 | } 298 | 299 | return config; 300 | } 301 | 302 | $getHeaders () { 303 | return { 304 | 'Accept': this.$getHeadersAccept(), 305 | 'Content-Type': this.$getHeadersContentType(), 306 | }; 307 | } 308 | 309 | $getHeadersAccept () { 310 | return 'application/json, text/plain, */*'; 311 | } 312 | 313 | $getHeadersContentType () { 314 | const type = this.builder.get('headers.contentType'); 315 | return ContentTypes[type] || ContentTypes.json; 316 | } 317 | 318 | $getUrl (action) { 319 | const route = `${this.builder.get('route')}.${action}`; 320 | const models = [this.builder.get('parents'), this.builder.get('model')].flat(); 321 | 322 | return this.$getRouter().route(route, ...models); 323 | } 324 | 325 | $getData () { 326 | return this.builder.get('headers.contentType') === 'formdata' ? 327 | this.$getModel().$formdata(): 328 | this.$getModel().$data(); 329 | } 330 | 331 | $getModel () { 332 | return this.builder.get('model'); 333 | } 334 | 335 | $getRouter () { 336 | return this.builder.get('router'); 337 | } 338 | 339 | $getPrimaryKey () { 340 | return this.builder.get('primaryKey'); 341 | } 342 | 343 | $getClient () { 344 | return this.builder.get('client'); 345 | } 346 | 347 | $getParams () { 348 | return this.builder.get('params'); 349 | } 350 | 351 | $getParamsSerializer () { 352 | return function (params) { 353 | return stringify(params, { 354 | arrayFormat: 'brackets', 355 | encode: false, 356 | }); 357 | }; 358 | } 359 | } 360 | 361 | /** 362 | * Build resource request 363 | * @param {object} model 364 | */ 365 | export const request = function (model) { 366 | return new RequestBuilder(model); 367 | }; 368 | -------------------------------------------------------------------------------- /src/resource.js: -------------------------------------------------------------------------------- 1 | import { Cast } from './cast'; 2 | import { request } from './request'; 3 | 4 | export const createResource = function (options) { 5 | const { 6 | client, 7 | router, 8 | } = options; 9 | 10 | return class Resource { 11 | /** 12 | * Construct model 13 | */ 14 | constructor (attributes = {}) { 15 | Object.assign(this, this._attributes); 16 | this.$fill(attributes); 17 | } 18 | 19 | /** 20 | * Default client 21 | */ 22 | get _client () { 23 | return client; 24 | } 25 | 26 | /** 27 | * Default attributes 28 | */ 29 | get _attributes () { 30 | throw new Error('The attributes are not defined'); 31 | } 32 | 33 | /** 34 | * Default route 35 | */ 36 | get _route () { 37 | throw new Error('The route is not defined'); 38 | } 39 | 40 | /** 41 | * Default primary key 42 | */ 43 | get _primaryKey () { 44 | return 'id'; 45 | } 46 | 47 | /** 48 | * Primary key value 49 | */ 50 | get _primaryValue () { 51 | return this[this._primaryKey]; 52 | } 53 | 54 | /** 55 | * Get relationships 56 | */ 57 | get _relationships () { 58 | return {}; 59 | } 60 | 61 | /** 62 | * Get response casts 63 | */ 64 | get _casts () { 65 | return {}; 66 | } 67 | 68 | /** 69 | * Get router 70 | */ 71 | get _router () { 72 | return router; 73 | } 74 | 75 | /** 76 | * Get content type 77 | */ 78 | get _contentType () { 79 | return 'json'; 80 | } 81 | 82 | /** 83 | * Create a new model 84 | */ 85 | static $create (attributesOrCollection) { 86 | if (Array.isArray(attributesOrCollection)) { 87 | return attributesOrCollection.map(data => { 88 | return new this(data); 89 | }); 90 | } 91 | 92 | return new this(attributesOrCollection); 93 | } 94 | 95 | static $request () { 96 | return this.$create().$request(...arguments); 97 | } 98 | 99 | static $route () { 100 | return this.$request().$route(...arguments); 101 | } 102 | 103 | static $source () { 104 | return this.$request().$source(...arguments); 105 | } 106 | 107 | static $index () { 108 | return this.$request().$index(...arguments); 109 | } 110 | 111 | static $find () { 112 | return this.$request().$find(...arguments); 113 | } 114 | 115 | $request () { 116 | return request(this); 117 | } 118 | 119 | $route () { 120 | return this.$request().$route(...arguments); 121 | } 122 | 123 | $source () { 124 | return this.$request().$source(...arguments); 125 | } 126 | 127 | $parents (...parents) { 128 | const route = [parents, this].flat().map((model) => model._route).join('.'); 129 | 130 | return this.$route(route).$parents(...parents); 131 | } 132 | 133 | $parent () { 134 | return this.$parents(...arguments); 135 | } 136 | 137 | $index () { 138 | return this.$request().$index(...arguments); 139 | } 140 | 141 | $find () { 142 | return this.$request().$find(...arguments); 143 | } 144 | 145 | $show () { 146 | return this.$request().$show(...arguments); 147 | } 148 | 149 | $refresh () { 150 | return this.$show(...arguments); 151 | } 152 | 153 | $store () { 154 | return this.$request().$store(...arguments); 155 | } 156 | 157 | $save () { 158 | return this.$store(...arguments); 159 | } 160 | 161 | $update () { 162 | return this.$request().$update(...arguments); 163 | } 164 | 165 | $destroy () { 166 | return this.$request().$destroy(...arguments); 167 | } 168 | 169 | $delete () { 170 | return this.$destroy(...arguments); 171 | } 172 | 173 | /** 174 | * Create a new model 175 | */ 176 | $create (attributesOrCollection) { 177 | if (Array.isArray(attributesOrCollection)) { 178 | return attributesOrCollection.map(data => { 179 | return new this.constructor(data); 180 | }); 181 | } 182 | 183 | return new this.constructor(attributesOrCollection); 184 | } 185 | 186 | /** 187 | * Fill attributes 188 | */ 189 | $fill (attributes) { 190 | Object.assign(this, attributes); 191 | 192 | const model = this; 193 | const casts = this._casts; 194 | for (const attribute in casts) { 195 | const cast = casts[attribute]; 196 | Cast.cast(model, attribute, cast); 197 | } 198 | 199 | return this; 200 | } 201 | 202 | /** 203 | * Transform attributes to data 204 | */ 205 | $data () { 206 | return Object.assign({}, this); 207 | } 208 | 209 | /** 210 | * Transform attributes to form data 211 | */ 212 | $formdata () { 213 | const data = this.$data() 214 | const formdata = new FormData(); 215 | 216 | for (const attribute in data) { 217 | formdata.append(attribute, data[attribute]); 218 | } 219 | 220 | return formdata; 221 | } 222 | } 223 | }; 224 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | class Router { 2 | constructor (options = {}) { 3 | const routes = new Map(); 4 | const prefix = options.prefix || ''; 5 | 6 | this.set = function (name, callback) { 7 | const createUrl = function () { 8 | return prefix + callback(...arguments); 9 | } 10 | 11 | routes.set(name, createUrl); 12 | } 13 | 14 | this.get = function (name) { 15 | return routes.get(name); 16 | } 17 | 18 | this.entries = function () { 19 | return routes.entries(); 20 | } 21 | } 22 | 23 | index (name, callback) { 24 | this.set(`${name}.index`, callback); 25 | } 26 | 27 | store (name, callback) { 28 | this.set(`${name}.store`, callback); 29 | } 30 | 31 | show (name, callback) { 32 | this.set(`${name}.show`, callback); 33 | } 34 | 35 | update (name, callback) { 36 | this.set(`${name}.update`, callback); 37 | } 38 | 39 | destroy (name, callback) { 40 | this.set(`${name}.destroy`, callback); 41 | } 42 | 43 | indexStore (name, callback) { 44 | this.index(name, callback); 45 | this.store(name, callback); 46 | } 47 | 48 | showUpdateDestroy (name, callback) { 49 | this.show(name, callback); 50 | this.update(name, callback); 51 | this.destroy(name, callback); 52 | } 53 | 54 | resource (resource) { 55 | function createRoutes (resource, models) { 56 | return resource 57 | .split('.') 58 | .reduce(function (routes, route, index) { 59 | routes.push(route); 60 | routes.push(models[index]._primaryValue); 61 | 62 | return routes; 63 | }, []) 64 | ; 65 | } 66 | 67 | function indexCallback (...models) { 68 | const routes = createRoutes(resource, models); 69 | routes.pop(); 70 | 71 | return routes.join('/'); 72 | }; 73 | 74 | function singleCallback (...models) { 75 | const routes = createRoutes(resource, models); 76 | 77 | return routes.join('/'); 78 | }; 79 | 80 | this.index(resource, indexCallback); 81 | this.store(resource, indexCallback); 82 | this.show(resource, singleCallback); 83 | this.update(resource, singleCallback); 84 | this.destroy(resource, singleCallback); 85 | } 86 | 87 | prefix (prefix, callback) { 88 | const router = new this.constructor({ prefix }); 89 | 90 | callback(router); 91 | 92 | for (const entry of router.entries()) { 93 | this.set(...entry); 94 | } 95 | } 96 | 97 | route (name, ...models) { 98 | const route = this.get(name); 99 | 100 | if (!route) { 101 | throw new Error(`Route '${name}' is not registered`); 102 | } 103 | 104 | return route(...models); 105 | } 106 | } 107 | 108 | export const createRouter = function () { 109 | return new Router(); 110 | } 111 | -------------------------------------------------------------------------------- /src/transform.js: -------------------------------------------------------------------------------- 1 | import CollectionTransform from './transforms/collection'; 2 | import DataCollectionTransform from './transforms/dataCollection'; 3 | import ModelTransform from './transforms/model'; 4 | 5 | export const transformResponse = function (response, model) { 6 | const transforms = [ 7 | DataCollectionTransform, 8 | CollectionTransform, 9 | ]; 10 | 11 | for (let index = 0; index < transforms.length; index++) { 12 | const transform = transforms[index]; 13 | 14 | if (transform.test(response, model)) { 15 | return transform.resolve(response, model); 16 | } 17 | } 18 | 19 | return ModelTransform.resolve(response, model); 20 | }; 21 | 22 | export const transformError = function (error) { 23 | return Promise.reject(error); 24 | }; 25 | -------------------------------------------------------------------------------- /src/transforms/collection.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test (response) { 3 | return Array.isArray(response.data); 4 | }, 5 | 6 | resolve (response, model) { 7 | return model.$create(response.data); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/transforms/dataCollection.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test (response, repository) { 3 | if (!response.data) { 4 | return false; 5 | } 6 | 7 | if (!Array.isArray(response.data.data)) { 8 | return false; 9 | } 10 | 11 | return !response.data.hasOwnProperty(repository._primaryKey); 12 | }, 13 | 14 | resolve (response, model) { 15 | const resource = response.data; 16 | 17 | resource.data = model.$create(resource.data); 18 | 19 | return resource; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/transforms/model.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test (response, model) { 3 | if (!response.data) { 4 | return false; 5 | } 6 | 7 | return response.data.hasOwnProperty(model._primaryKey); 8 | }, 9 | 10 | resolve (response, model) { 11 | return model.$fill(response.data); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get'; 2 | import has from 'lodash/has'; 3 | import set from 'lodash/set'; 4 | import Qs from 'qs'; 5 | 6 | const stringify = function () { return Qs.stringify(...arguments) }; 7 | 8 | export { 9 | get, 10 | has, 11 | set, 12 | stringify, 13 | }; 14 | -------------------------------------------------------------------------------- /tests/feature/__snapshots__/crud.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`crud it can store custom data 1`] = `"{\\"ids\\":[1,2,3]}"`; 4 | -------------------------------------------------------------------------------- /tests/feature/cancel.spec.js: -------------------------------------------------------------------------------- 1 | import Moxios from 'moxios'; 2 | import { Post } from '../models/post'; 3 | import { createSource, isCancel } from '../../src'; 4 | 5 | describe('post', () => { 6 | beforeEach(function () { 7 | Moxios.install(); 8 | }); 9 | 10 | afterEach(function () { 11 | Moxios.uninstall(); 12 | }); 13 | 14 | test('it can cancel request for queries', () => { 15 | expect.assertions(1); 16 | Moxios.stubRequest(/.*/, { status: 200 }); 17 | 18 | const errorHandler = (error) => { 19 | expect(isCancel(error)).toEqual(true); 20 | } 21 | 22 | const source = createSource(); 23 | const promise = Post.$source(source).$index().catch(errorHandler); 24 | source.cancel(); 25 | 26 | return promise; 27 | }); 28 | 29 | test('it can cancel request for crud actions', () => { 30 | expect.assertions(1); 31 | Moxios.stubRequest(/.*/, { status: 200 }); 32 | 33 | const errorHandler = (error) => { 34 | expect(isCancel(error)).toEqual(true); 35 | } 36 | 37 | const post = Post.$create(); 38 | const source = createSource(); 39 | const promise = post.$source(source).$store().catch(errorHandler); 40 | source.cancel(); 41 | 42 | return promise; 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/feature/cast.spec.js: -------------------------------------------------------------------------------- 1 | import { Cast } from '../models/cast'; 2 | import { Comment } from '../models/comment'; 3 | 4 | describe('cast', () => { 5 | test('it can transform response attributes', async () => { 6 | const response = { 7 | 'to_integer': "1", 8 | 'to_float': '1.1', 9 | 'to_boolean': 0, 10 | 'to_object': '{ "object": [1] }', 11 | 'to_date': '2020-04-13T14:13:12.000000Z', 12 | 'to_comments': [{ id: 2, body: 'Hello' }], 13 | }; 14 | 15 | const cast = await Cast.$create(response); 16 | 17 | expect(cast.to_integer).toEqual(1); 18 | expect(cast.to_float).toEqual(1.1); 19 | expect(cast.to_boolean).toEqual(false); 20 | expect(cast.to_object).toEqual({ 'object': [1] }); 21 | expect(cast.to_date).toBeInstanceOf(Date); 22 | expect(cast.to_comments[0]).toBeInstanceOf(Comment); 23 | }); 24 | 25 | test('it can transform nested response attributes', async () => { 26 | const response = { 27 | nested: { 28 | to_integer: "1", 29 | }, 30 | }; 31 | 32 | const cast = await Cast.$create(response); 33 | 34 | expect(cast.nested.to_integer).toEqual(1); 35 | }); 36 | 37 | test('it can with custom callback', async () => { 38 | const response = { 39 | to_callback: 'notcalled', 40 | }; 41 | 42 | const cast = await Cast.$create(response); 43 | 44 | expect(cast.to_callback).toEqual('called'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/feature/crud.spec.js: -------------------------------------------------------------------------------- 1 | import Moxios from 'moxios'; 2 | import { Post } from '../models/post'; 3 | import { Comment } from '../models/comment'; 4 | 5 | describe('crud', () => { 6 | beforeEach(function () { 7 | Moxios.install(); 8 | }); 9 | 10 | afterEach(function () { 11 | Moxios.uninstall(); 12 | }); 13 | 14 | test('it can find a post by primary key', async () => { 15 | const url = 'posts/1'; 16 | const response = { 17 | id: 1, 18 | title: 'Hello', 19 | }; 20 | 21 | Moxios.stubRequest(/.*/, { response }); 22 | 23 | const post = await Post.$find(1); 24 | 25 | const request = Moxios.requests.mostRecent(); 26 | expect(request.url).toEqual(url); 27 | expect(post).toBeInstanceOf(Post); 28 | expect(post.id).toEqual(response.id); 29 | expect(post.title).toEqual(response.title); 30 | }); 31 | 32 | test('it can show a post', async () => { 33 | const url = 'posts/1'; 34 | const response = { 35 | id: 1, 36 | title: 'Hello Updated', 37 | }; 38 | 39 | Moxios.stubRequest(/.*/, { response }); 40 | 41 | const post = Post.$create({ id: 1 }); 42 | await post.$show(); 43 | 44 | const request = Moxios.requests.mostRecent(); 45 | expect(request.url).toEqual(url); 46 | expect(post).toBeInstanceOf(Post); 47 | expect(post.id).toEqual(response.id); 48 | expect(post.title).toEqual(response.title); 49 | }); 50 | 51 | test('it can save a new post', async () => { 52 | const url = 'posts'; 53 | const response = { 54 | id: 1, 55 | title: 'Hello', 56 | }; 57 | 58 | Moxios.stubRequest(/.*/, { response }); 59 | 60 | const post = Post.$create({ title: 'Hello' }); 61 | await post.$store(); 62 | 63 | const request = Moxios.requests.mostRecent(); 64 | expect(request.url).toEqual(url); 65 | expect(request.config.data).toEqual("{\"id\":null,\"title\":\"Hello\",\"thumbnail\":null}"); 66 | expect(request.config.method).toEqual('post'); 67 | expect(post.id).toEqual(response.id); 68 | expect(post.title).toEqual(response.title); 69 | }); 70 | 71 | test('it can update a new post', async () => { 72 | const url = 'posts/1'; 73 | const response = { 74 | id: 1, 75 | title: 'Hello', 76 | }; 77 | 78 | Moxios.stubRequest(/.*/, { response }); 79 | 80 | const post = Post.$create({ id: 1, title: 'Hello' }); 81 | await post.$update(); 82 | 83 | const request = Moxios.requests.mostRecent(); 84 | expect(request.url).toEqual(url); 85 | expect(request.config.data).toEqual("{\"id\":1,\"title\":\"Hello\",\"thumbnail\":null}"); 86 | expect(request.config.method).toEqual('put'); 87 | expect(post.id).toEqual(response.id); 88 | expect(post.title).toEqual(response.title); 89 | }); 90 | 91 | test('it can delete a post', async () => { 92 | const url = 'posts/1'; 93 | const response = { 94 | id: 1, 95 | title: 'Hello', 96 | }; 97 | 98 | Moxios.stubRequest(/.*/, { response }); 99 | 100 | const post = Post.$create({ id: 1, title: 'Hello' }); 101 | await post.$destroy(); 102 | 103 | const request = Moxios.requests.mostRecent(); 104 | expect(request.url).toEqual(url); 105 | expect(request.config.method).toEqual('delete'); 106 | }); 107 | 108 | test('it can fetch a relationship', async () => { 109 | const url = 'posts/1/comments'; 110 | const response = { 111 | id: 1, 112 | body: 'Hello', 113 | }; 114 | 115 | Moxios.stubRequest(/.*/, { response }); 116 | 117 | const post = Post.$create({ id: 1 }); 118 | const comment = Comment.$create({ body: 'Hello' }); 119 | 120 | await comment.$parent(post).$store(); 121 | 122 | const request = Moxios.requests.mostRecent(); 123 | expect(request.url).toEqual(url); 124 | expect(request.config.method).toEqual('post'); 125 | expect(comment.id).toEqual(response.id); 126 | expect(comment.body).toEqual(response.body); 127 | }); 128 | 129 | test('it can fetch a custom route', async () => { 130 | const url = 'posts/last'; 131 | const response = { 132 | id: 1, 133 | body: 'Hello', 134 | }; 135 | 136 | Moxios.stubRequest(/.*/, { response }); 137 | 138 | const post = await Post.$route('posts.last').$show(); 139 | 140 | const request = Moxios.requests.mostRecent(); 141 | expect(request.url).toEqual(url); 142 | expect(request.config.method).toEqual('get'); 143 | expect(post.id).toEqual(response.id); 144 | expect(post.body).toEqual(response.body); 145 | }); 146 | 147 | test('it can store custom data', async () => { 148 | const url = 'posts/1/sync'; 149 | const data = { 150 | ids: [1, 2, 3], 151 | }; 152 | 153 | Moxios.stubRequest(/.*/, {}); 154 | 155 | const post = Post.$create({ id: 1 }); 156 | await post.$route('posts.sync').$store({ data }); 157 | 158 | const request = Moxios.requests.mostRecent(); 159 | expect(request.url).toEqual(url); 160 | expect(request.config.method).toEqual('post'); 161 | expect(request.config.data).toMatchSnapshot(); 162 | }); 163 | 164 | test('it can catch validation errors', () => { 165 | expect.assertions(1); 166 | 167 | const response = { 168 | messages: { 169 | title: ['The title field is required'], 170 | }, 171 | }; 172 | 173 | Moxios.stubRequest(/.*/, { status: 422, response }); 174 | 175 | const post = Post.$create({ 176 | id: '1', 177 | }); 178 | 179 | const promise = post.$store(); 180 | 181 | return expect(promise).rejects.toBeInstanceOf(Error); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /tests/feature/media.spec.js: -------------------------------------------------------------------------------- 1 | import Moxios from 'moxios'; 2 | import { Media } from '../models/media'; 3 | 4 | describe('media', () => { 5 | beforeEach(function () { 6 | Moxios.install(); 7 | }); 8 | 9 | afterEach(function () { 10 | Moxios.uninstall(); 11 | }); 12 | 13 | test('it can store a file', async () => { 14 | Moxios.stubRequest(/.*/, { status: 200 }); 15 | 16 | const file = new Blob([''], {type: 'text/plain'}); 17 | const media = Media.$create({ file }); 18 | 19 | await media.$store(); 20 | 21 | const request = Moxios.requests.mostRecent(); 22 | expect(request.config.method).toEqual('post'); 23 | expect(request.config.data).toBeInstanceOf(FormData); 24 | expect(request.config.data.has('file')).toBe(true); 25 | expect(request.headers['Content-Type']).toEqual('multipart/form-data'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/feature/pagination.spec.js: -------------------------------------------------------------------------------- 1 | import Moxios from 'moxios'; 2 | import { Post } from '../models/post'; 3 | 4 | describe('post', () => { 5 | beforeEach(function () { 6 | Moxios.install(); 7 | }); 8 | 9 | afterEach(function () { 10 | Moxios.uninstall(); 11 | }); 12 | 13 | test('it can paginate a query', async () => { 14 | const url = 'posts?page[size]=15&page[number]=1'; 15 | const method = 'get'; 16 | const response = { 17 | "current_page": 1, 18 | "data": [ 19 | { 20 | "id": 1, 21 | "title": "Soluta libero illo eligendi.", 22 | "body": "Nesciunt a eum porro. Rem dolorem dolore unde similique molestias. Commodi est error quo quod.", 23 | }, 24 | { 25 | "id": 2, 26 | "title": "Molestiae sit consequuntur neque dolores eum dolorum.", 27 | "body": "Totam voluptatem dolor nam magnam et adipisci dolorem. Qui ad quo enim aperiam earum et ut. Vitae expedita aut qui error. Quia aut reprehenderit vel nesciunt.", 28 | }, 29 | ], 30 | "from": 1, 31 | "last_page": 1, 32 | "next_page_url": null, 33 | "per_page": 15, 34 | "to": 5, 35 | "total": 5 36 | }; 37 | 38 | Moxios.stubRequest(/.*/, { response }); 39 | 40 | const pagination = await Post 41 | .$request() 42 | .$page({ 43 | size: 15, 44 | number: 1, 45 | }) 46 | .$index(); 47 | 48 | const request = Moxios.requests.mostRecent(); 49 | expect(request.url).toEqual(url); 50 | expect(request.config.method).toEqual(method); 51 | expect(pagination.data[0]).toBeInstanceOf(Post); 52 | }); 53 | 54 | test('it can paginate a query in laravel', async () => { 55 | const url = 'posts?page=1'; 56 | const response = { 57 | "current_page": 1, 58 | "data": [ 59 | { 60 | "id": 1, 61 | "title": "Soluta libero illo eligendi.", 62 | "body": "Nesciunt a eum porro. Rem dolorem dolore unde similique molestias. Commodi est error quo quod.", 63 | }, 64 | { 65 | "id": 2, 66 | "title": "Molestiae sit consequuntur neque dolores eum dolorum.", 67 | "body": "Totam voluptatem dolor nam magnam et adipisci dolorem. Qui ad quo enim aperiam earum et ut. Vitae expedita aut qui error. Quia aut reprehenderit vel nesciunt.", 68 | }, 69 | ], 70 | "from": 1, 71 | "last_page": 1, 72 | "next_page_url": null, 73 | "per_page": 15, 74 | "to": 5, 75 | "total": 5 76 | }; 77 | 78 | Moxios.stubRequest(/.*/, { response }); 79 | 80 | await Post 81 | .$request() 82 | .$page(1) 83 | .$index(); 84 | 85 | const request = Moxios.requests.mostRecent(); 86 | expect(request.url).toEqual(url); 87 | }); 88 | 89 | test('it can limit a query', async () => { 90 | const url = 'posts?limit=15'; 91 | 92 | Moxios.stubRequest(/.*/, {}); 93 | 94 | await Post 95 | .$request() 96 | .$limit(15) 97 | .$index(); 98 | 99 | const request = Moxios.requests.mostRecent(); 100 | expect(request.url).toEqual(url); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /tests/feature/query.spec.js: -------------------------------------------------------------------------------- 1 | import Moxios from 'moxios'; 2 | import { Post } from '../models/post'; 3 | 4 | describe('query', () => { 5 | beforeEach(function () { 6 | Moxios.install(); 7 | }); 8 | 9 | afterEach(function () { 10 | Moxios.uninstall(); 11 | }); 12 | 13 | test('it can query for all posts', async () => { 14 | const url = 'posts'; 15 | const response = [ 16 | { id: 1, title: 'Title' }, 17 | { id: 2, title: 'Title two' }, 18 | ]; 19 | 20 | Moxios.stubRequest(/.*/, { response }); 21 | 22 | const posts = await Post.$index(); 23 | 24 | const request = Moxios.requests.mostRecent(); 25 | expect(request.url).toEqual(url); 26 | expect(request.config.method).toEqual('get'); 27 | 28 | expect(posts[0]).toBeInstanceOf(Post); 29 | }); 30 | 31 | test('it can filter', async () => { 32 | const url = 'posts?filter[title]=Hello'; 33 | const response = [{ 34 | id: 1, 35 | title: 'Hello', 36 | }]; 37 | 38 | Moxios.stubRequest(/.*/, { response }); 39 | 40 | const posts = await Post 41 | .$request() 42 | .$filter('title', 'Hello') 43 | .$index(); 44 | 45 | const request = Moxios.requests.mostRecent(); 46 | expect(request.url).toEqual(url); 47 | expect(request.config.method).toEqual('get'); 48 | 49 | expect(posts[0]).toBeInstanceOf(Post); 50 | }); 51 | 52 | test('it can filter with the where alias', async () => { 53 | const url = 'posts?filter[title]=Hello'; 54 | const response = [{ 55 | id: 1, 56 | title: 'Hello', 57 | }]; 58 | 59 | Moxios.stubRequest(/.*/, { response }); 60 | 61 | const posts = await Post 62 | .$request() 63 | .$where('title', 'Hello') 64 | .$index(); 65 | 66 | const request = Moxios.requests.mostRecent(); 67 | expect(request.url).toEqual(url); 68 | expect(request.config.method).toEqual('get'); 69 | 70 | expect(posts[0]).toBeInstanceOf(Post); 71 | }); 72 | 73 | test('it can filter multiple', async () => { 74 | const url = 'posts?filter[id]=1&filter[title]=Hello'; 75 | const response = [{ 76 | id: 1, 77 | title: 'Hello', 78 | }]; 79 | 80 | Moxios.stubRequest(/.*/, { response }); 81 | 82 | const posts = await Post 83 | .$request() 84 | .$filter({ id: 1, title: 'Hello' }) 85 | .$index(); 86 | 87 | const request = Moxios.requests.mostRecent(); 88 | expect(request.url).toEqual(url); 89 | expect(request.config.method).toEqual('get'); 90 | 91 | expect(posts[0]).toBeInstanceOf(Post); 92 | }); 93 | 94 | test('it can use a specific parameter', async () => { 95 | const url = 'posts?param=value'; 96 | const response = [{ 97 | id: 1, 98 | title: 'Hello', 99 | }]; 100 | 101 | Moxios.stubRequest(/.*/, { response }); 102 | 103 | const posts = await Post 104 | .$request() 105 | .$param('param', 'value') 106 | .$index(); 107 | 108 | const request = Moxios.requests.mostRecent(); 109 | expect(request.url).toEqual(url); 110 | expect(posts[0].id).toEqual(response[0].id); 111 | expect(posts[0].title).toEqual(response[0].title); 112 | expect(posts[0]).toBeInstanceOf(Post); 113 | }); 114 | 115 | test('it can use a multiple specific parameters', async () => { 116 | const url = 'posts?paramOne=valueOne¶mTwo=valueTwo'; 117 | const response = [{ 118 | id: 1, 119 | title: 'Hello', 120 | }]; 121 | 122 | Moxios.stubRequest(/.*/, { response }); 123 | 124 | const posts = await Post 125 | .$request() 126 | .$param({ 'paramOne': 'valueOne', 'paramTwo': 'valueTwo' }) 127 | .$index(); 128 | 129 | const request = Moxios.requests.mostRecent(); 130 | expect(request.url).toEqual(url); 131 | expect(posts[0].id).toEqual(response[0].id); 132 | expect(posts[0].title).toEqual(response[0].title); 133 | expect(posts[0]).toBeInstanceOf(Post); 134 | }); 135 | 136 | test('it can get posts with comments', async () => { 137 | const url = 'posts?include=comments'; 138 | const response = [{ 139 | id: 1, 140 | title: 'Hello', 141 | comments: [ 142 | { 143 | id: 2, 144 | body: 'Nice', 145 | }, 146 | ], 147 | }]; 148 | 149 | Moxios.stubRequest(/.*/, { response }); 150 | 151 | const posts = await Post 152 | .$request() 153 | .$include('comments') 154 | .$index(); 155 | 156 | const request = Moxios.requests.mostRecent(); 157 | expect(request.url).toEqual(url); 158 | expect(posts[0].id).toEqual(response[0].id); 159 | expect(posts[0].title).toEqual(response[0].title); 160 | }); 161 | 162 | test('it can get posts with comments and author', async () => { 163 | const url = 'posts?include=comments,author'; 164 | const response = [{ 165 | id: 1, 166 | title: 'Hello', 167 | author: { 168 | id: 3, 169 | name: 'Maxim', 170 | }, 171 | comments: [ 172 | { 173 | id: 2, 174 | body: 'Nice', 175 | }, 176 | ], 177 | }]; 178 | 179 | Moxios.stubRequest(/.*/, { response }); 180 | 181 | const posts = await Post 182 | .$request() 183 | .$include('comments', 'author') 184 | .$index(); 185 | 186 | const request = Moxios.requests.mostRecent(); 187 | expect(request.url).toEqual(url); 188 | expect(posts[0].id).toEqual(response[0].id); 189 | expect(posts[0].title).toEqual(response[0].title); 190 | }); 191 | 192 | test('it can sort posts by name', async () => { 193 | const url = 'posts?sort=name'; 194 | const response = [ 195 | { 196 | id: 2, 197 | title: 'Alpha', 198 | }, 199 | { 200 | id: 1, 201 | title: 'Beta', 202 | }, 203 | ]; 204 | 205 | Moxios.stubRequest(/.*/, { response }); 206 | 207 | const posts = await Post 208 | .$request() 209 | .$sort('name') 210 | .$index(); 211 | 212 | const request = Moxios.requests.mostRecent(); 213 | expect(request.url).toEqual(url); 214 | expect(posts[0].id).toEqual(response[0].id); 215 | expect(posts[0].title).toEqual(response[0].title); 216 | }); 217 | 218 | test('it can sort posts by name descending', async () => { 219 | const url = 'posts?sort=-name'; 220 | const response = [ 221 | { 222 | id: 1, 223 | title: 'Beta', 224 | }, 225 | { 226 | id: 2, 227 | title: 'Alpha', 228 | }, 229 | ]; 230 | 231 | Moxios.stubRequest(/.*/, { response }); 232 | 233 | const posts = await Post 234 | .$request() 235 | .$sortDesc('name') 236 | .$index(); 237 | 238 | const request = Moxios.requests.mostRecent(); 239 | expect(request.url).toEqual(url); 240 | expect(posts[0].id).toEqual(response[0].id); 241 | expect(posts[0].title).toEqual(response[0].title); 242 | }); 243 | 244 | test('it can sort posts by name descending and id descending', async () => { 245 | const url = 'posts?sort=-name,-id'; 246 | const response = [ 247 | { 248 | id: 1, 249 | title: 'Beta', 250 | }, 251 | { 252 | id: 2, 253 | title: 'Alpha', 254 | }, 255 | ]; 256 | 257 | Moxios.stubRequest(/.*/, { response }); 258 | 259 | const posts = await Post 260 | .$request() 261 | .$sortDesc('name', 'id') 262 | .$index(); 263 | 264 | const request = Moxios.requests.mostRecent(); 265 | expect(request.url).toEqual(url); 266 | expect(posts[0].id).toEqual(response[0].id); 267 | expect(posts[0].title).toEqual(response[0].title); 268 | }); 269 | 270 | test('it can sort posts by id ascending and name descending', async () => { 271 | const url = 'posts?sort=id,-name'; 272 | const response = [ 273 | { 274 | id: 1, 275 | title: 'Beta', 276 | }, 277 | { 278 | id: 2, 279 | title: 'Alpha', 280 | }, 281 | ]; 282 | 283 | Moxios.stubRequest(/.*/, { response }); 284 | 285 | const posts = await Post 286 | .$request() 287 | .$sort('id') 288 | .$sortDesc('name') 289 | .$index(); 290 | 291 | const request = Moxios.requests.mostRecent(); 292 | expect(request.url).toEqual(url); 293 | expect(posts[0].id).toEqual(response[0].id); 294 | expect(posts[0].title).toEqual(response[0].title); 295 | }); 296 | 297 | test('it can select the fields of a post', async () => { 298 | const url = 'posts?fields[posts]=id,title'; 299 | const response = [{ 300 | id: 1, 301 | title: 'Hello', 302 | }]; 303 | 304 | Moxios.stubRequest(/.*/, { response }); 305 | 306 | const posts = await Post 307 | .$request() 308 | .$fields({ 'posts': ['id', 'title'] }) 309 | .$index(); 310 | 311 | const request = Moxios.requests.mostRecent(); 312 | expect(request.url).toEqual(url); 313 | expect(posts[0].id).toEqual(response[0].id); 314 | expect(posts[0].title).toEqual(response[0].title); 315 | }); 316 | 317 | test('it can append a field of a post', async () => { 318 | const url = 'posts?append=date'; 319 | const response = [{ 320 | id: 1, 321 | title: 'Hello', 322 | date: 'now', 323 | }]; 324 | 325 | Moxios.stubRequest(/.*/, { response }); 326 | 327 | const posts = await Post 328 | .$request() 329 | .$append('date') 330 | .$index(); 331 | 332 | const request = Moxios.requests.mostRecent(); 333 | expect(request.url).toEqual(url); 334 | expect(posts[0].id).toEqual(response[0].id); 335 | expect(posts[0].title).toEqual(response[0].title); 336 | }); 337 | 338 | test('it can append multiple fields of a post', async () => { 339 | const url = 'posts?append=title,date'; 340 | const response = [{ 341 | id: 1, 342 | title: 'Hello', 343 | date: 'now', 344 | }]; 345 | 346 | Moxios.stubRequest(/.*/, { response }); 347 | 348 | const posts = await Post 349 | .$request() 350 | .$append('title', 'date') 351 | .$index(); 352 | 353 | const request = Moxios.requests.mostRecent(); 354 | expect(request.url).toEqual(url); 355 | expect(posts[0].id).toEqual(response[0].id); 356 | expect(posts[0].title).toEqual(response[0].title); 357 | }); 358 | }); 359 | -------------------------------------------------------------------------------- /tests/models/cast.js: -------------------------------------------------------------------------------- 1 | import { Resource } from '../setup/resource'; 2 | import { Comment } from './comment'; 3 | 4 | /** 5 | * Cast resource 6 | * 7 | * @class Cast 8 | */ 9 | export class Cast extends Resource { 10 | get _attributes () { 11 | return { 12 | 'id': null, 13 | 'to_integer': null, 14 | 'to_float': null, 15 | 'to_boolean': null, 16 | 'to_object': null, 17 | 'to_date': null, 18 | 'to_callback': null, 19 | 'nested': { 20 | 'to_integer': null, 21 | }, 22 | }; 23 | } 24 | 25 | get _casts () { 26 | return { 27 | 'to_integer': 'int', 28 | 'to_float': 'float', 29 | 'to_boolean': 'boolean', 30 | 'to_object': 'json.parse', 31 | 'to_date': 'date', 32 | 'to_comments': 'relationship:comments', 33 | 'nested.to_integer': 'int', 34 | 'to_callback': (value) => ('called'), 35 | }; 36 | } 37 | 38 | get _relationships () { 39 | return { 40 | comments: Comment, 41 | }; 42 | } 43 | 44 | get _route () { 45 | return 'casts'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/models/comment.js: -------------------------------------------------------------------------------- 1 | import { Resource } from '../setup/resource'; 2 | import { Like } from './like'; 3 | 4 | /** 5 | * Comment resource 6 | * 7 | * @class Comment 8 | */ 9 | export class Comment extends Resource { 10 | get _attributes () { 11 | return { 12 | id: null, 13 | body: null, 14 | }; 15 | } 16 | 17 | get _route () { 18 | return 'comments'; 19 | } 20 | 21 | get _relationships () { 22 | return { 23 | likes: Like, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/models/like.js: -------------------------------------------------------------------------------- 1 | import { Resource } from '../setup/resource'; 2 | 3 | /** 4 | * Like resource 5 | * 6 | * @class Like 7 | */ 8 | export class Like extends Resource { 9 | get _attributes () { 10 | return { 11 | id: null, 12 | user: null, 13 | }; 14 | } 15 | 16 | get _route () { 17 | return 'likes'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/models/media.js: -------------------------------------------------------------------------------- 1 | import { Resource } from '../setup/resource'; 2 | 3 | /** 4 | * Media resource 5 | * 6 | * @class Media 7 | */ 8 | export class Media extends Resource { 9 | get _attributes () { 10 | return { 11 | id: null, 12 | file: null, 13 | }; 14 | } 15 | 16 | get _route () { 17 | return 'media'; 18 | } 19 | 20 | get _contentType () { 21 | return 'formdata'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/models/post.js: -------------------------------------------------------------------------------- 1 | import { Resource } from '../setup/resource'; 2 | import { Comment } from './comment'; 3 | import { Like } from './like'; 4 | 5 | /** 6 | * Post resource 7 | * 8 | * @class Post 9 | */ 10 | export class Post extends Resource { 11 | get _attributes () { 12 | return { 13 | id: null, 14 | title: null, 15 | thumbnail: null, 16 | }; 17 | } 18 | 19 | get _casts () { 20 | return { 21 | 'comments': 'relationship:comments', 22 | }; 23 | } 24 | 25 | get _route () { 26 | return 'posts'; 27 | } 28 | 29 | get _relationships () { 30 | return { 31 | comments: Comment, 32 | likes: Like, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/setup/client.js: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import Moxios from 'moxios'; 3 | 4 | export const client = function () { 5 | const instance = Axios.create(); 6 | Moxios.install(instance); 7 | 8 | return instance; 9 | }; 10 | -------------------------------------------------------------------------------- /tests/setup/resource.js: -------------------------------------------------------------------------------- 1 | import { createResource } from '../../src'; 2 | import { client } from '../setup/client'; 3 | import { router } from '../setup/router'; 4 | 5 | export const Resource = createResource({ 6 | client, 7 | router, 8 | }); 9 | -------------------------------------------------------------------------------- /tests/setup/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter } from '../../src'; 2 | 3 | export const router = createRouter(); 4 | 5 | router.resource('casts'); 6 | router.resource('comments'); 7 | router.resource('likes'); 8 | router.resource('media'); 9 | router.resource('posts'); 10 | router.resource('posts.comments'); 11 | router.resource('posts.comments.likes'); 12 | 13 | router.show('posts.last', () => 'posts/last'); 14 | router.store('posts.sync', (post) => `posts/${post.id}/sync`); 15 | 16 | router.prefix('http://api.com/api/v1/', function (router) { 17 | router.resource('authors'); 18 | }); 19 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | filename: 'index.js', 7 | library: 'Elodo', 8 | libraryTarget: 'umd', 9 | path: path.resolve(__dirname, 'dist') 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'inline-source-map' 7 | }) 8 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 3 | const common = require('./webpack.common.js') 4 | 5 | module.exports = merge(common, { 6 | mode: 'production', 7 | module: { 8 | rules: [ 9 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } 10 | ], 11 | }, 12 | plugins: [ 13 | new UglifyJSPlugin() 14 | ] 15 | }) 16 | --------------------------------------------------------------------------------