├── .codeclimate.yml
├── .coveralls.yml
├── .editorconfig
├── .eslintrc
├── .flowconfig
├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── documentation
├── client.md
├── error.md
├── resource-interning.md
└── resource.md
├── karma.local.conf.js
├── karma.saucelabs.conf.js
├── lib
├── Client.js
├── Resource.js
├── Transport.js
└── resourceCache.js
├── package.json
└── test
├── _testServer.js
├── authentication.js
├── promises.js
├── read.js
└── write.js
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | languages:
2 | JavaScript: true
3 | exclude_paths:
4 | - "dist/*.js"
5 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 | repo_token: xBt1HoQbbXn6Buk0fEXR5zv6kQS1JOzim
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | end_of_line = lf
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "globals": {
4 | },
5 | "env": {
6 | "node": true,
7 | "mocha": true
8 | },
9 | "rules": {
10 | "no-underscore-dangle": [ 0 ],
11 | "curly": [2, "multi-line"],
12 | "no-console": [ 0 ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [include]
2 | ./lib/
3 |
4 | [ignore]
5 | .*node_modules/phantomjs/node_modules/npmconf/.*
6 |
7 | [options]
8 | module.ignore_non_literal_requires=true
9 | munge_underscores=true
10 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | complexity
2 | coverage.html
3 | dist.js
4 | dist/
5 | node_modules
6 | npm-debug.log
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true
3 | }
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 4
4 | - 6
5 | - 7
6 | addons:
7 | firefox: "latest"
8 | before_script:
9 | - export DISPLAY=:99.0
10 | - sh -e /etc/init.d/xvfb start
11 | script: npm run ci
12 | notifications:
13 | email: false
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | - 2017-07-14 - v1.0.0
2 | - 2017-07-14 - Publish to NPM as scoped package `@holidayextras/jsonapi-client`.
3 | - 2017-07-14 - v0.8.1
4 | - 2016-07-14 - Test with latest `jsonapi-server` and update dependencies.
5 | - 2016-07-19 - v0.8.0
6 | - 2016-07-19 - Enable options in fetch requests
7 | - 2016-07-13 - v0.7.0
8 | - 2016-07-12 - Send id and type on PATCH requests
9 | - 2016-07-12 - Add flow and jscpd to Travis
10 | - 2016-07-12 - Use qs instead of perry for url formatting
11 | - 2016-07-12 - Don't send null id when creating a resource
12 | - 2016-06-22 - v0.6.0 Updating all dependencies
13 | - 2016-06-08 - v0.5.1 Fixes retrieval of many-to-one relationships
14 | - 2016-04-22 - v0.5.0 Safer error extraction, relationship deduplication bug, adding relationships to new resources
15 | - 2016-03-10 - v0.4.3 Error handling when fetching related resources that don't exist
16 | - 2016-02-10 - v0.4.2 Fix adding/removing resources to/from relationships
17 | - 2016-02-08 - v0.4.1 Handling resources with no relationships
18 | - 2015-12-17 - v0.4.0 Better handling of non-json:api payloads
19 | - 2015-12-17 - v0.3.0 Fixes to enable usage in web browsers
20 | - 2015-12-17 - v0.2.0 Better validation when changing relations
21 | - 2015-12-17 - v0.1.0 Adding authentication
22 | - 2015-11-07 - v0.0.1 Initial release
23 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Contributing
2 |
3 | We'd love for you to contribute to our source code and to make this project even better than it is today!
4 |
5 | #### Submitting an Issue
6 |
7 | If you've found an area whereby this project does not do what you think it should, then you can help us by submitting an issue to this repository. The more specific you can be, the better.
8 |
9 | #### Submitting a Pull Request
10 |
11 | If the bug/feature you are working on is in any way controversial, or makes assumptions about parts of the json:api spec that could be misconstrued, I'd encourage you to first open an issue in this repository to enable a discussion to take place before you start changing things. Nobody likes to waste their own time, or anybody elses!
12 |
13 | Every pull request that changes code in this project needs to have some form of regression test with it. That might take the shape of some additional asserts in key places within existing tests, it might involve new tests. We write tests to ensure features don't get lost - protect your feature by writing good tests!
14 |
15 | To verify all the code changes pass our style guidelines:
16 | ```
17 | npm run lint
18 | ```
19 |
20 | To verify everything still behaves as expected:
21 | ```
22 | npm test
23 | ```
24 |
25 | To check the code coverage is still great:
26 | ```
27 | npm run coverage
28 | google-chrome ./coverage.html
29 | ```
30 |
31 | To see code complexity statistics:
32 | ```
33 | npm run complexity
34 | google-chrome ./complexity/index.html
35 | ```
36 |
37 | If all of the above comes up good, go ahead and put in a pull request!
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Oliver Rumbelow. All rights reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 | IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://coveralls.io/github/holidayextras/jsonapi-client?branch=master)
2 | [](https://travis-ci.org/holidayextras/jsonapi-client)
3 | [](https://codeclimate.com/github/holidayextras/jsonapi-client)
4 | [](https://www.codacy.com/app/oliver-rumbelow-github/jsonapi-client)
5 | [](https://david-dm.org/holidayextras/jsonapi-client)
6 |
7 | # jsonapi-client
8 |
9 | A javascript module designed to make it really easy to consume a `json:api` service.
10 |
11 | ** ⚠ !! THIS PROJECT IS IN NPM AS `@holidayextras/jsonapi-client` !! **
12 |
13 | ```
14 | $ npm install --save @holidayextras/jsonapi-client
15 | ```
16 |
17 | note: this project requires a Node.js version of at least `4.5.0`.
18 |
19 | ### Motivation / Justification / Rationale
20 |
21 | Consuming a json:api service from within Javascript is a non-trivial affair. Setting up a transport mechanism, authentication, making requests to standardised HTTP routes, error handling, pagination and expanding an inclusion tree... All of these things represent barriers to consuming an API. This module takes away all the hassle and lets developers focus on interacting with a rich API without wasting developer time focusing on anything other than shipping valuable features.
22 |
23 | This module is tested against the example json:api server provided by [jsonapi-server](https://github.com/holidayextras/jsonapi-server).
24 |
25 | ### Full documentation
26 |
27 | - [Using the Client](documentation/client.md)
28 | - [Interacting with Resources](documentation/resource.md)
29 | - [Resource Interning](documentation/resource-interning.md)
30 |
31 | ### The tl;dr
32 |
33 | #### In a browser
34 | ```html
35 |
36 |
43 | ```
44 |
45 | #### Creating a new Client
46 | ```javascript
47 | var JsonapiClient = require("jsonapi-client");
48 | var client = new JsonapiClient("http://localhost:16006/rest", {
49 | header: {
50 | authToken: "2ad1d6f7-e1d0-480d-86b2-dfad8af4a5b3"
51 | }
52 | });
53 | ```
54 |
55 | #### Creating a new Resource
56 | ```javascript
57 | var article = client.create("articles");
58 | article.set("title", "foobar");
59 | article.sync(function(err) {
60 | console.log("Resource created");
61 | });
62 | ```
63 |
64 | #### Finding Resources
65 | ```javascript
66 | client.find("articles", function(err, resources) {
67 | resources.map(function(resource) {
68 | console.log(resource.toJSON());
69 | });
70 | });
71 | ```
72 |
73 | #### Getting a specific Resource
74 | ```javascript
75 | client.get("articles", 5, { include: [ "author" ] }, function(err, article) {
76 | console.log(article.toJSONTree());
77 | });
78 | ```
79 |
80 | #### Fetching a Resource's related Resource
81 | ```javascript
82 | article.fetch("author", function(err) {
83 | console.log(article.author.toJSON());
84 | });
85 | ```
86 |
87 | #### Updating a Resource's primary relationships
88 | ```javascript
89 | article.relationships("comments").add(comment);
90 | article.sync(function(err) {
91 | console.log("Resource's relation updated");
92 | });
93 | ```
94 |
95 | #### Deleting a Resource
96 | ```javascript
97 | article.delete(function(err) {
98 | console.log("Resource deleted");
99 | });
100 | ```
101 |
102 | #### A more complex example
103 | ```javascript
104 | }).then(function() {
105 | return client.create("articles")
106 | .set("title", "some fancy booklet")
107 | .set("content", "oh-la-la!")
108 | .relationships("tags").add(someTagResource)
109 | .sync();
110 | }).then(function(newlyCreatedArticle) {
111 | ```
112 |
--------------------------------------------------------------------------------
/documentation/client.md:
--------------------------------------------------------------------------------
1 |
2 | ## Using the Client
3 |
4 | - [Purpose of the Client](#purpose-of-the-client)
5 | - [Getting Started](#getting-started)
6 | - [Authentication](#authentication)
7 | - [Creating a Resource](#creating-a-resource)
8 | - [Finding Resources](#finding-resources)
9 | - [Getting a Resource](#getting-a-resource)
10 |
11 | #### Purpose of the Client
12 |
13 | 1. To enable consumption of multiple JSON:API services.
14 | 2. To be a factory for Resource objects.
15 | 1. When creating new Resources.
16 | 2. When retrieving Resources from an API.
17 |
18 | #### Getting Started
19 |
20 | First up, if you're working in a browser you'll need to drop the library onto your page:
21 |
22 | ```html
23 |
24 | ```
25 |
26 | From here on, the implementation (be it in NodeJS or the browser) is the same.
27 | Start by pulling in the library:
28 |
29 | ```javascript
30 | var JsonapiClient = require("jsonapi-client");
31 | ```
32 |
33 | Next, we create a new instance of jsonapi-client and tell it where our target API is located:
34 |
35 | ```javascript
36 | var client = new JsonapiClient("http://localhost:16006/rest");
37 | ```
38 |
39 | Now we're good to go!
40 |
41 | #### Authentication
42 |
43 | jsonapi-server currently supports two basic forms of authentication - cookies and headers.
44 |
45 | Custom headers:
46 | ```javascript
47 | var client = new JsonapiClient("http://localhost:16006/rest", {
48 | header: {
49 | myHeaderName: "my-header-value"
50 | }
51 | });
52 | ```
53 |
54 | Cookie:
55 | ```javascript
56 | var client = new JsonapiClient("http://localhost:16006/rest", {
57 | cookie: {
58 | myCookieName: "my-cookie-value"
59 | }
60 | });
61 | ```
62 |
63 | #### Creating a Resource
64 |
65 | Creating a resource begins with a synchronous call to `client.create` - it will immediately return a new (empty) instance of a [Resource](resource.md). Next up is an opportunity to set the attributes on your new resource, for example you'll need to assign values to all mandatory attributes before trying to push it up to the remote API.
66 |
67 | ```javascript
68 | var newResource = client.create("resource-type"); // synchronous
69 | ```
70 |
71 | Once the newResource is populated and ready to be pushed, it needs to be `sync`d. You can read more about `sync` in the [Resource documentation](resource.md).
72 |
73 | ```javascript
74 | newResource.sync(function(err) { /* accepts a callback */ });
75 | newResource.sync(); // without a callback, it returns a promise
76 | ```
77 |
78 | #### Finding Resources
79 |
80 | To search for resources, use `client.find`. The first argument must be the resource-type you want to search for. The second argument is optional, it represents any URL parameters you may want to include - filter, include, sort, etc. The third argument is an optional callback. If no callback is provided, a promise will be returned.
81 |
82 | The function will produce either an [error](error.md) or an array of [Resources](resource.md).
83 |
84 | ```javascript
85 | var optionalParams = { /* url-params go here */ };
86 | client.find("resource-type", optionalParams, function(err, resources) { });
87 | client.find("resource-type", optionalParams); // without a callback, it returns a promise
88 | ```
89 |
90 | #### Getting a Resource
91 |
92 | To find a specific resource, use `client.get`. The first argument must be the resource-type you want to search for. The second argument must be the resource-id of the resource you want to retrieve from the remote API. The third argument is optional, it represents any URL parameters you may want to include - filter, include, sort, etc. The fourth argument is an optional callback. If no callback is provided, a promise will be returned.
93 |
94 | The function will produce either an [error](error.md) or a single [Resource](resource.md).
95 |
96 | ```javascript
97 | var optionalParams = { /* url-params go here */ };
98 | client.get("resource-type", "resource-id", optionalParams, function(err, resources) { });
99 | var promise = client.get("resource-type", "resource-id", optionalParams);
100 | ```
101 |
--------------------------------------------------------------------------------
/documentation/error.md:
--------------------------------------------------------------------------------
1 |
2 | ## Errors
3 |
4 | - [Purpose of Errors](#purpose-of-errors)
5 | - [Error properties](#error-properties)
6 |
7 | #### Purpose of Errors
8 |
9 | 1. To represent remote Errors.
10 | 1. To enable graceful fallback.
11 | 1. To conform to Javascript standards.
12 |
13 | #### Error properties
14 |
15 | * `name` - A high level summary of what went wrong.
16 | * `message` - A more detailed message of specifically what went wrong.
17 | * `status` - The HTTP response code, useful for distinguishing between user errors or API bugs.
18 | * `code` - Useful for debugging the remote API, should assist development teams to track down bugs.
19 |
20 | ```javascript
21 | > console.log(someError);
22 | { [Requested resource does not exist: "There is no people with id 5e7b30d5-c8da-4936-9f81-4a6ea1153a5f"]
23 | name: 'Requested resource does not exist',
24 | status: '404',
25 | code: 'ENOTFOUND' }
26 | ```
27 |
--------------------------------------------------------------------------------
/documentation/resource-interning.md:
--------------------------------------------------------------------------------
1 |
2 | ### Resource Interning
3 |
4 | *Resource Interning ONLY applies when running within a web browser!*
5 |
6 | Suppose your application fetches a specific resource from the remote API, lets call that resource A. At some point further down the line, another part of your application does a broad search which results in a list of resources, whereby that list contains A. Resource interning ensures that all local resources representing the same unique remote resource are all the same object.
7 |
8 | Here's an example of it in action:
9 | ```javascript
10 | var person1, person2;
11 |
12 | // Get Mark's record
13 | client.get("people", "d850ea75-4427-4f81-8595-039990aeede5", { }, function(err1, personResource) {
14 | person1 = personResource;
15 | });
16 |
17 | // Find Mark's record
18 | client.get("people", { filter: { firtsname: "Mark" } }, function(err1, personResource) {
19 | person2 = personResource;
20 | });
21 |
22 | // Wait for both requests to complete
23 | setTimeout(function() {
24 | // both objects will contain the SAME reference
25 | assert.equal(person1, person2);
26 | }, 1000);
27 | ```
28 |
--------------------------------------------------------------------------------
/documentation/resource.md:
--------------------------------------------------------------------------------
1 |
2 | ### Interacting with Resources
3 |
4 | Foreword: All resource objects are [interned](resource-interning.md).
5 |
6 | - [Examining resource attributes](#examining-resource-attributes)
7 | - [Setting resource attributes](#setting-resource-attributes)
8 | - [Syncing a resource with the remote service](#syncing-a-resource-with-the-remote-service)
9 | - [Flattening a resource](#flattening-a-resource)
10 | - [Debugging nested resources](#debugging-nested-resources)
11 | - [Fetching related resources](#fetching-related-resources)
12 | - [Changing resource relations](#changing-resource-relations)
13 | - [Deleting a resource](#deleting-a-resource)
14 |
15 | #### Examining resource attributes
16 |
17 | Resource attributes should be accessed via `resource.get()`.
18 |
19 | ```javascript
20 | resource.get("attribute-name");
21 | ```
22 |
23 | ### Setting resource attributes
24 |
25 | Resource attributes should be modified via `resource.set()`.
26 |
27 | ```javascript
28 | resource.set("attribute-name", "new-value");
29 | ```
30 |
31 | ### Syncing a resource with the remote service
32 |
33 | To push either a new resource, or changes to an existing resource, up to the remote API, use `resource.sync()`.
34 |
35 | ```javascript
36 | newPerson.sync(function(err) { /* accepts a callback */ });
37 | newPerson.sync(); // without a callback, it returns a promise
38 | ```
39 |
40 | ### Flattening a resource
41 |
42 | Calling `resource.toJSON()` will flatten and merge a Resource's type+id, attributes and relations to give a clean view of the resource. Nested or known related resources will be excluded.
43 |
44 | ```javascript
45 | > photo.toJSON();
46 | {
47 | id: "aab14844-97e7-401c-98c8-0bd5ec922d93",
48 | type: "photos",
49 | title: "Matrix Code",
50 | url: "http://www.example.com/foobar",
51 | height: 1080,
52 | width: 1920,
53 | photographer: { type: "people", id: "ad3aa89e-9c5b-4ac9-a652-6670f9f27587" }
54 | }
55 | ```
56 |
57 | ### Debugging nested resources
58 |
59 | Calling `resource.toJSONTree()` will produce an extended version of `resource.toJSON()` whereby known nested resources will be included.
60 |
61 | ```javascript
62 | > client.find("people", { include: "articles", filter: { lastname: "Rumbelow"}}, function(err, people) {
63 | console.log(people[0].toJSONTree());
64 | });
65 | {
66 | "id": "cc5cca2e-0dd8-4b95-8cfc-a11230e73116",
67 | "type": "people",
68 | "firstname": "Oli",
69 | "lastname": "Rumbelow",
70 | "email": "oliver.rumbelow@example.com",
71 | "articles": [
72 | {
73 | "id": "de305d54-75b4-431b-adb2-eb6b9e546014",
74 | "type": "articles",
75 | "title": "NodeJS Best Practices",
76 | "content": "na",
77 | "author": "[Circular]",
78 | "tags": [
79 | {
80 | "type": "tags",
81 | "id": "7541a4de-4986-4597-81b9-cf31b6762486"
82 | }
83 | ],
84 | "photos": [],
85 | "comments": [
86 | {
87 | "type": "comments",
88 | "id": "3f1a89c2-eb85-4799-a048-6735db24b7eb"
89 | }
90 | ]
91 | }
92 | ],
93 | "photos": null
94 | }
95 | ```
96 |
97 | ### Fetching related resources
98 |
99 | To easily retrieve related resources, call `resource.fetch("relation-name")`. This does a couple of things:
100 |
101 | 1. Fetches related resources via the `related` link, as per the JSON:API spec.
102 | 2. Creates instances of Resource for each new resource.
103 | 3. Assigns forward links.
104 | 4. Assigns backward links.
105 |
106 | ```javascript
107 | people.fetch("relation-name", function(err, newResources) { /* accepts a callback */ });
108 | people.fetch("relation-name"); // without a callback, it returns a promise
109 |
110 | photo.fetch("photographer", function(err, person) {
111 | person instanceof Resource;
112 | photo.photographer == person;
113 | person.photos.indexOf(photo) !== -1;
114 | });
115 | ```
116 |
117 | ### Changing resource relations
118 |
119 | A resource's relations can be modified by calling `resource.relationships("relation-name")` and then calling `.add()`, `.remove()` or `.set()` to mutate the relation.
120 |
121 | ```javascript
122 | person.relationships("photos").add(someResource) // synchronous
123 | person.relationships("photos").remove(someResource) // synchronous
124 | person.relationships("photos").set(someResource) // synchronous
125 | ```
126 |
127 | After mutating the relation, the resource needs to be `.sync()`d to push the changes to the remote API.
128 |
129 | ### Deleting a resource
130 |
131 | To delete a resource from the remote API use `resource.delete()`. Calling delete will remove the id from the local resource, retaining all other resource state. A deleted resource can therefore be re-`sync()`d under a new resource id.
132 |
133 | ```javascript
134 | resource.delete(function(err) { /* accepts a callback */ });
135 | resource.delete(); // without a callback, it returns a promise
136 | ```
137 |
--------------------------------------------------------------------------------
/karma.local.conf.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function(config) {
4 | config.set({
5 | frameworks: [ "mocha" ],
6 | files: [ "dist/jsonapi-client-test.js" ],
7 | reporters: [ "spec" ],
8 | plugins: [ "karma-mocha", "karma-firefox-launcher", "karma-spec-reporter" ],
9 | port: 9876,
10 | colors: true,
11 | logLevel: config.LOG_INFO,
12 | autoWatch: false,
13 | browsers: [ "Firefox"/*, "PhantomJS"*/ ],
14 | singleRun: true,
15 | concurrency: 1,
16 | client: {
17 | captureConsole: true,
18 | timeout: 10000,
19 | mocha: {
20 | timeout: 10000
21 | }
22 | }
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/karma.saucelabs.conf.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var customLaunchers = {
4 | win10chrome: {
5 | base: "SauceLabs",
6 | browserName: "chrome",
7 | platform: "Windows 10"
8 | },
9 | androidChrome: {
10 | base: "SauceLabs",
11 | browserName: "android",
12 | platform: "Linux"
13 | },
14 | win10firefox: {
15 | base: "SauceLabs",
16 | browserName: "firefox",
17 | platform: "Windows 10"
18 | },
19 | osxSafari: {
20 | base: "SauceLabs",
21 | browserName: "safari",
22 | platform: "OS X 10.11"
23 | },
24 | iosSafari: {
25 | base: "SauceLabs",
26 | browserName: "iphone",
27 | platform: "OS X 10.10"
28 | },
29 | iosSafari92: {
30 | base: "SauceLabs",
31 | browserName: "iphone",
32 | platform: "OS X 10.10",
33 | version: "9.2"
34 | },
35 | win10ie11: {
36 | base: "SauceLabs",
37 | browserName: "internet explorer",
38 | platform: "Windows 10"
39 | },
40 | win7ie9: {
41 | base: "SauceLabs",
42 | browserName: "internet explorer",
43 | platform: "Windows 7",
44 | version: "9.0"
45 | }
46 | };
47 |
48 | module.exports = function(config) {
49 | config.set({
50 | sauceLabs: {
51 | testName: "jsonapi-client full stack tests"
52 | },
53 | customLaunchers: customLaunchers,
54 | browsers: Object.keys(customLaunchers),
55 | frameworks: [ "mocha" ],
56 | reporters: [ "spec", "saucelabs" ],
57 | plugins: [ "karma-mocha", "karma-sauce-launcher", "karma-spec-reporter" ],
58 | singleRun: true,
59 | autoWatch: false,
60 | files: [
61 | "https://cdn.polyfill.io/v2/polyfill.js?features=Promise",
62 | "dist/jsonapi-client-test.js"
63 | ],
64 | port: 9876,
65 | colors: true,
66 | logLevel: config.LOG_INFO,
67 | concurrency: 5,
68 | client: {
69 | captureConsole: true,
70 | timeout: 10000,
71 | mocha: {
72 | timeout: 10000
73 | }
74 | },
75 | captureTimeout: 300000,
76 | customHeaders: [{
77 | name: "Access-Control-Allow-Origin",
78 | value: "*"
79 | },
80 | {
81 | name: "Access-Control-Allow-Methods",
82 | value: "GET, POST, PATCH, DELETE, OPTIONS"
83 | }],
84 | startConnect: true,
85 | connectOptions: {
86 | verbose: false,
87 | verboseDebugging: false
88 | },
89 | browserNoActivityTimeout: 30000
90 | });
91 | };
92 |
--------------------------------------------------------------------------------
/lib/Client.js:
--------------------------------------------------------------------------------
1 | /* @flow weak */
2 | "use strict";
3 | var Client = module.exports = function(path, auth, options) {
4 | if (!path) {
5 | throw new Error("API path is needed to construct an instanceof jsonapi-client");
6 | }
7 | auth = auth || { };
8 | options = options || {};
9 |
10 | this._construct(path, auth, options);
11 | };
12 |
13 | if (typeof window !== "undefined") {
14 | window.JsonapiClient = Client; // eslint-disable-line
15 | }
16 |
17 | var Promise = require("promise");
18 | // Promise.denodeify = function(a) { return a; };
19 | var Resource = require("./Resource");
20 | Client.Resource = Resource;
21 | var Transport = require("./Transport");
22 | var createResourceCache = require("./resourceCache");
23 |
24 | Client.prototype._construct = function(path, auth, options) {
25 | this._resourceCache = createResourceCache(options);
26 | this._transport = new Transport(path, auth);
27 | };
28 |
29 | Client.prototype.find = Promise.denodeify(function(type, options, callback) {
30 | var self = this;
31 |
32 | if (typeof options === "function") {
33 | callback = options;
34 | options = { };
35 | }
36 |
37 | this._transport.search(type, options, function(err, rawResponse, rawResources, rawIncludes) {
38 | if (err) return callback(err);
39 |
40 | var resources = rawResources.map(function(someRawResource) {
41 | return new Resource(someRawResource, self);
42 | });
43 | if (rawIncludes) {
44 | rawIncludes.forEach(function(someRawResource) {
45 | return new Resource(someRawResource, self);
46 | });
47 | }
48 |
49 | return callback(null, resources);
50 | });
51 | });
52 |
53 | Client.prototype.get = Promise.denodeify(function(type, id, options, callback) {
54 | var self = this;
55 |
56 | if (typeof options === "function") {
57 | callback = options;
58 | options = { };
59 | }
60 |
61 | this._transport.find(type, id, options, function(err, rawResponse, rawResource, rawIncludes) {
62 | if (err) return callback(err);
63 |
64 | var resource = new Resource(rawResource, self);
65 | if (rawIncludes) {
66 | rawIncludes.forEach(function(someRawResource) {
67 | return new Resource(someRawResource, self);
68 | });
69 | }
70 |
71 | return callback(null, resource);
72 | });
73 | });
74 |
75 | Client.prototype._getRelated = function(resource, relation, options, callback) {
76 | var self = this;
77 | this._transport.fetch(resource, relation, options, function(err, rawResponse, rawResources, rawIncludes) {
78 | if (err) return callback(err);
79 |
80 | if (rawIncludes) {
81 | rawIncludes.forEach(function(someRawResource) {
82 | return new Resource(someRawResource, self);
83 | });
84 | }
85 |
86 | if (!(rawResources instanceof Array)) {
87 | var rawResource = new Resource(rawResources, self);
88 | return callback(null, rawResource);
89 | }
90 |
91 | var resources = rawResources.map(function(someRawResource) {
92 | return new Resource(someRawResource, self);
93 | });
94 |
95 | return callback(null, resources);
96 | });
97 | };
98 |
99 | Client.prototype.create = function(type, properties) {
100 | var newResource = new Resource({
101 | id: null,
102 | type: type,
103 | attributes: properties || { },
104 | relationships: { }
105 | }, this);
106 | return newResource;
107 | };
108 |
109 | Client.prototype._remoteCreate = function(resource, callback) {
110 | this._transport.create(resource, callback);
111 | };
112 |
113 | Client.prototype._update = function(resource, callback) {
114 | this._transport.update(resource, callback);
115 | };
116 |
117 | Client.prototype._delete = function(resource, callback) {
118 | this._transport.delete(resource, callback);
119 | };
120 |
--------------------------------------------------------------------------------
/lib/Resource.js:
--------------------------------------------------------------------------------
1 | /* @flow weak */
2 | "use strict";
3 | var Resource = module.exports = function(rawResource, client) {
4 | rawResource = rawResource || { };
5 | return this._construct(rawResource, client);
6 | };
7 |
8 | var Promise = require("promise");
9 | // Promise.denodeify = function(a) { return a; };
10 | var assign = require("object-assign");
11 |
12 | function getMeta(obj) {
13 | var metaDefaults = { relation: "primary" };
14 | return obj.meta || metaDefaults;
15 | }
16 |
17 | Resource.prototype._construct = function(rawResource, client) {
18 | this._raw = rawResource;
19 | this._base = {
20 | id: rawResource.id,
21 | type: rawResource.type
22 | };
23 | this._client = client;
24 | this._changed = [ ];
25 |
26 | if (!this._base.id) return this;
27 |
28 | var fromCache = client._resourceCache.get(this);
29 | if (fromCache) return fromCache;
30 |
31 | client._resourceCache.set(this);
32 | return this;
33 | };
34 |
35 | Resource.prototype._checkIsResource = function(resource) {
36 | if (resource instanceof Resource) {
37 | if (!resource._base.id) throw new Error("Resource has not been created remotely, it can't be assigned to a relationship!");
38 | return;
39 | }
40 |
41 |
42 | var type = typeof resource;
43 | if (resource.constructor) type = resource.constructor.name;
44 |
45 | throw new Error("Expected Resource, got " + type);
46 | };
47 |
48 | Resource.prototype._getBase = function() {
49 | return this._base;
50 | };
51 |
52 | Resource.prototype._getRaw = function() {
53 | return this._raw;
54 | };
55 |
56 | Resource.prototype._getDelta = function() {
57 | var primaryRelations = { };
58 | var relationships = this._getRaw().relationships;
59 | Object.keys(relationships).forEach(function(i) {
60 | if (getMeta(relationships[i]).relation === "foreign") return;
61 | primaryRelations[i] = relationships[i];
62 | });
63 |
64 | return {
65 | id: this._base.id,
66 | type: this._base.type,
67 | attributes: this._getRaw().attributes,
68 | relationships: primaryRelations
69 | };
70 | };
71 |
72 | Resource.prototype._getUid = function() {
73 | return this._base.id;
74 | };
75 |
76 | Resource.prototype._getUidString = function() {
77 | return this._base.id + "/" + this._base.type;
78 | };
79 |
80 | Resource.prototype._getPathFor = function(relation) {
81 | return this._getRaw().relationships[relation].links.related;
82 | };
83 |
84 | Resource.prototype._matches = function(other) {
85 | return (this._base.id === other.id) && (this._base.type === other.type);
86 | };
87 |
88 | Resource.prototype._associateWithAll = function(resources) {
89 | var self = this;
90 | resources.forEach(function(resource) {
91 | self._associateWith(resource);
92 | resource._associateWith(self);
93 | });
94 | };
95 |
96 | Resource.prototype._relationHasResource = function(relationName, resource) {
97 | var relation = this[relationName];
98 |
99 | if (relation instanceof Array) {
100 | relation = relation.filter(function(i) {
101 | return (i._base === resource._base);
102 | }).pop();
103 | }
104 |
105 | return !!relation;
106 | };
107 |
108 | Resource._rawRelationHasMany = function(rawRelation) {
109 | var meta = getMeta(rawRelation);
110 | if (meta.relation === "primary") return (rawRelation.data instanceof Array);
111 | return meta.many;
112 | };
113 |
114 | Resource.prototype._associateWith = function(resource) {
115 | var self = this;
116 | var relationships = self._getRaw().relationships;
117 | if (!relationships) return;
118 | Object.keys(relationships).forEach(function(relationName) {
119 | var rawRelation = relationships[relationName];
120 | var otherRelation = resource;
121 | var rawRelationData = rawRelation.data;
122 | var rawRelationMeta = getMeta(rawRelation);
123 | if (rawRelationMeta.relation === "foreign") {
124 | if (rawRelationMeta.belongsTo !== resource._getBase().type) {
125 | return;
126 | }
127 | otherRelation = self;
128 | rawRelationData = resource._getRaw().relationships[rawRelationMeta.as].data;
129 | }
130 |
131 | var match = !![].concat(rawRelationData).filter(function(dataItem) {
132 | // should null ever get here?
133 | return dataItem && otherRelation._matches(dataItem);
134 | }).pop();
135 | if (!match) return;
136 |
137 | if (Resource._rawRelationHasMany(rawRelation)) {
138 | if (self._relationHasResource(relationName, resource)) return;
139 | self[relationName] = self[relationName] || [ ];
140 | self[relationName].push(resource);
141 | } else {
142 | self[relationName] = resource;
143 | }
144 | });
145 | };
146 |
147 | Resource.prototype.relationships = function(relationName) {
148 | var self = this;
149 | var rawRelation = this._raw.relationships[relationName];
150 |
151 | if (!self._base.id) {
152 | rawRelation = this._raw.relationships[relationName] = {
153 | meta: { _trust: true },
154 | links: { },
155 | data: null
156 | };
157 | }
158 |
159 | if (!rawRelation) {
160 | throw new Error("Relationship " + relationName + " on " + this._raw.type + " does not exist!");
161 | }
162 |
163 | if (getMeta(rawRelation).relation === "foreign") {
164 | throw new Error("Relationship " + relationName + " on " + this._raw.type + " is a foreign relation and cannot be updated from here!");
165 | }
166 |
167 | return {
168 | add: function(resource) {
169 | return self._addRelation(relationName, rawRelation, resource);
170 | },
171 | set: function(resource) {
172 | return self._setRelation(relationName, rawRelation, resource);
173 | },
174 | remove: function(resource) {
175 | return self._removeRelation(relationName, rawRelation, resource);
176 | }
177 | };
178 | };
179 | Resource.prototype._addRelation = function(relationName, rawRelation, resource) {
180 | this._checkIsResource(resource);
181 | if (rawRelation.meta && rawRelation.meta._trust) {
182 | delete rawRelation.meta;
183 | rawRelation.data = [ ];
184 | }
185 | if (!Resource._rawRelationHasMany(rawRelation)) {
186 | throw new Error("Relationship " + relationName + " on " + this._raw.type + " is a not a MANY relationship and cannot be added to!");
187 | }
188 | if (this._relationHasResource(relationName, resource)) return this;
189 | rawRelation.data.push(resource._getBase());
190 | this[relationName] = this[relationName] || [];
191 | this[relationName].push(resource);
192 | resource._tweakLinksTo("add", this);
193 | return this;
194 | };
195 | Resource.prototype._removeRelation = function(relationName, rawRelation, resource) {
196 | this._checkIsResource(resource);
197 | if (rawRelation.meta && rawRelation.meta._trust) {
198 | delete rawRelation.meta;
199 | rawRelation.data = [ ];
200 | }
201 | if (Resource._rawRelationHasMany(rawRelation)) {
202 | rawRelation.data = rawRelation.data.filter(function(id) {
203 | return !resource._matches(id);
204 | });
205 | this[relationName] = this[relationName].filter(function(id) {
206 | return !resource._matches(id);
207 | });
208 | resource._tweakLinksTo("remove", this);
209 | } else {
210 | rawRelation.data = undefined;
211 | this[relationName] = undefined;
212 | }
213 | resource._tweakLinksTo("remove", this);
214 | return this;
215 | };
216 | Resource.prototype._setRelation = function(relationName, rawRelation, resource) {
217 | this._checkIsResource(resource);
218 | rawRelation.data = resource._getBase();
219 | this[relationName] = resource;
220 | resource._tweakLinksTo("add", this);
221 | return this;
222 | };
223 |
224 | Resource.prototype.toJSON = function() {
225 | var theirs = this._raw;
226 | var theirResource = assign({
227 | id: theirs.id,
228 | type: theirs.type
229 | }, theirs.attributes);
230 | for (var i in theirs.relationships) {
231 | theirResource[i] = theirs.relationships[i].data;
232 | }
233 | return theirResource;
234 | };
235 |
236 | Resource.prototype.toJSONTree = function(seen) {
237 | seen = seen || [ ];
238 | if (seen.indexOf(this) !== -1) return "[Circular]";
239 | seen.push(this);
240 | var theirs = this._raw;
241 | var theirResource = assign({
242 | id: theirs.id,
243 | type: theirs.type
244 | }, theirs.attributes);
245 | for (var i in theirs.relationships) {
246 | if (this[i] instanceof Array) {
247 | theirResource[i] = this[i].map(function(j) { // eslint-disable-line
248 | return j.toJSONTree(seen);
249 | });
250 | } else if (this[i] instanceof Resource) {
251 | theirResource[i] = this[i].toJSONTree(seen);
252 | } else {
253 | theirResource[i] = theirs.relationships[i].data || null;
254 | }
255 | }
256 | return theirResource;
257 | };
258 |
259 | Resource.prototype.get = function(attibuteName) {
260 | return this._raw.attributes[attibuteName];
261 | };
262 |
263 | Resource.prototype.set = function(attributeName, value) {
264 | this._raw.attributes[attributeName] = value;
265 | this._changed.push(attributeName);
266 | return this;
267 | };
268 |
269 | Resource.prototype.fetch = Promise.denodeify(function(relationName, options, callback) {
270 | var self = this;
271 |
272 | if (typeof options === "function") {
273 | callback = options;
274 | options = { };
275 | }
276 |
277 | self._client._getRelated(this, relationName, options, function(err, newResources) {
278 | if (!newResources) return callback("No related resources found");
279 |
280 | if (!(newResources instanceof Array)) {
281 | self._raw.relationships[relationName].data = newResources._getBase();
282 | return callback(err, newResources);
283 | }
284 |
285 | self._raw.relationships[relationName].data = newResources.map(function(someResource) {
286 | return someResource._getBase();
287 | });
288 |
289 | return callback(err, newResources);
290 | });
291 | });
292 |
293 | Resource.prototype.sync = Promise.denodeify(function(callback) {
294 | var self = this;
295 | var target = self._client._update;
296 | if (!self._getBase().id) {
297 | target = self._client._remoteCreate;
298 | }
299 |
300 | target.call(self._client, self, function(err, rawResponse, rawResource) {
301 | if (err) return callback(err);
302 | self._construct(rawResource, self._client);
303 | return callback(null, self);
304 | });
305 | });
306 |
307 | Resource.prototype.delete = Promise.denodeify(function(callback) {
308 | var self = this;
309 | self._client._delete(self, function(err) {
310 | if (err) return callback(err);
311 |
312 | self._client._resourceCache.removeFromCache(self);
313 | self._getBase().id = null;
314 | self._getRaw().id = null;
315 |
316 | return callback();
317 | });
318 | });
319 |
320 | Resource.prototype._tweakLinksTo = function(method, resource) {
321 | var self = this;
322 | if (!self._raw.relationships) return;
323 | Object.keys(self._raw.relationships).forEach(function(i) {
324 | var someRawRelationship = self._raw.relationships[i];
325 | var someRawRelationshipMeta = getMeta(someRawRelationship);
326 | if (someRawRelationshipMeta.relation !== "foreign") return;
327 | if (someRawRelationshipMeta.belongsTo !== resource._raw.type) return;
328 |
329 | self["_" + method + "LinksTo"](i, resource, someRawRelationshipMeta.many);
330 | });
331 | };
332 |
333 | Resource.prototype._removeLinksTo = function(i, resource) {
334 | if (this[i] instanceof Array) {
335 | this[i] = this[i].filter(function(j) {
336 | return j !== resource;
337 | });
338 | } else {
339 | if (this[i] === resource) {
340 | this[i] = undefined;
341 | }
342 | }
343 | };
344 |
345 | Resource.prototype._addLinksTo = function(i, resource, many) {
346 | if (this[i] instanceof Array) {
347 | this[i].push(resource);
348 | } else {
349 | if (many) {
350 | this[i] = [resource];
351 | } else {
352 | this[i] = resource;
353 | }
354 | }
355 | };
356 |
--------------------------------------------------------------------------------
/lib/Transport.js:
--------------------------------------------------------------------------------
1 | /* @flow weak */
2 | "use strict";
3 | var Transport = module.exports = function(path, auth) {
4 | auth = auth || { };
5 | this._construct(path, auth);
6 | };
7 |
8 | var request = require("superagent");
9 | var qs = require("qs");
10 |
11 | Transport.prototype._construct = function(path, auth) {
12 | this._path = path;
13 | this._auth = {
14 | cookie: auth.cookie,
15 | header: auth.header
16 | };
17 | };
18 |
19 | Transport.prototype._attachAuthToRequest = function(someRequest) {
20 | if (this._auth.cookie) {
21 | var cookieName = Object.keys(this._auth.cookie)[0];
22 | someRequest.set("Cookie", cookieName + "=" + this._auth.cookie[cookieName]);
23 | }
24 |
25 | if (this._auth.header) {
26 | var headerName = Object.keys(this._auth.header)[0];
27 | someRequest.set(headerName, this._auth.header[headerName]);
28 | }
29 | };
30 |
31 | Transport._defaultError = function(response) {
32 | return {
33 | status: "500",
34 | code: "EUNKNOWN",
35 | title: "An unknown error has occured",
36 | detail: response
37 | };
38 | };
39 |
40 | Transport.prototype._action = function(method, url, data, callback) {
41 | // console.log(method, url, JSON.stringify(data, null, 2));
42 | var someRequest = request[method](url);
43 | someRequest.set("Content-Type", "application/vnd.api+json");
44 | this._attachAuthToRequest(someRequest);
45 | if (method === "get") {
46 | someRequest = someRequest.query(qs.stringify(data));
47 | } else {
48 | someRequest = someRequest.send(data || {});
49 | }
50 | someRequest.end(function(err, payload) {
51 | // console.log("=>", err, payload);
52 | if (err) {
53 | // console.log(err.response.text);
54 | if (err.status === 401) {
55 | return callback(new Error("401 Unauthorized"));
56 | }
57 |
58 | var response;
59 | try {
60 | response = JSON.parse(err.response.text);
61 | } catch(e) {
62 | console.error("Transport Error: " + JSON.stringify(err));
63 | return callback(Transport._defaultError(response));
64 | }
65 |
66 | if (!Array.isArray(response.errors)) {
67 | console.error("Invalid Error payload!", response);
68 | return callback(Transport._defaultError(response));
69 | }
70 |
71 | var realErrors = response.errors.map(function(apiError) {
72 | var someError = new Error(JSON.stringify(apiError.detail));
73 | someError.name = apiError.title;
74 | // $FlowFixMe: Technically "status" isn't a field on Error
75 | someError.status = apiError.status;
76 | // $FlowFixMe: Technically "code" isn't a field on Error
77 | someError.code = apiError.code;
78 |
79 | return someError;
80 | });
81 |
82 | return callback(realErrors[0]);
83 | }
84 |
85 | if (!payload.body) {
86 | try {
87 | payload.body = JSON.parse(payload.text);
88 | } catch(e) {
89 | return callback(Transport._defaultError(payload));
90 | }
91 | }
92 |
93 | if (!payload.body) {
94 | return callback(Transport._defaultError(payload));
95 | }
96 |
97 | return callback(null, payload.body, payload.body.data, payload.body.included);
98 | });
99 | };
100 |
101 | Transport.prototype.search = function(type, options, callback) {
102 | var url = this._path + "/" + type;
103 | this._action("get", url, options, callback);
104 | };
105 |
106 | Transport.prototype.find = function(type, id, options, callback) {
107 | var url = this._path + "/" + type + "/" + id;
108 | this._action("get", url, options, callback);
109 | };
110 |
111 | Transport.prototype.fetch = function(resource, relation, options, callback) {
112 | var url = resource._getPathFor(relation);
113 | this._action("get", url, options, callback);
114 | };
115 |
116 | Transport.prototype.create = function(resource, callback) {
117 | var url = this._path + "/" + resource._getBase().type;
118 | var postData = resource._getRaw();
119 | delete postData.id;
120 | this._action("post", url, {
121 | data: resource._getRaw()
122 | }, callback);
123 | };
124 |
125 | Transport.prototype.update = function(resource, callback) {
126 | var url = this._path + "/" + resource._getBase().type + "/" + resource._getBase().id;
127 | this._action("patch", url, {
128 | data: resource._getDelta()
129 | }, callback);
130 | };
131 |
132 | Transport.prototype.delete = function(resource, callback) {
133 | var url = this._path + "/" + resource._getBase().type + "/" + resource._getBase().id;
134 | this._action("del", url, null, callback);
135 | };
136 |
--------------------------------------------------------------------------------
/lib/resourceCache.js:
--------------------------------------------------------------------------------
1 | /* @flow weak */
2 | "use strict";
3 | module.exports = function createResourceCache(options) {
4 | options = options || { };
5 | var resourceCache = {};
6 |
7 | resourceCache._cacheDuration = options.cacheDuration || 5000;
8 |
9 | if (typeof module === undefined) {
10 | resourceCache._cacheDuration = null;
11 | }
12 | resourceCache._cache = { };
13 | resourceCache._cacheList = [ ];
14 |
15 | resourceCache.get = function(someResource) {
16 | var key = someResource._getUidString();
17 | return resourceCache._cache[key];
18 | };
19 |
20 | resourceCache.set = function(someResource) {
21 | someResource._associateWithAll(resourceCache._cacheList);
22 |
23 | var key = someResource._getUidString();
24 | resourceCache._cache[key] = someResource;
25 | resourceCache._cacheList.push(someResource);
26 |
27 | if (!resourceCache._cacheDuration) return;
28 |
29 | setTimeout(function() {
30 | resourceCache.removeFromCache(someResource);
31 | }, resourceCache._cacheDuration);
32 | };
33 |
34 | resourceCache.removeFromCache = function(someResource) {
35 | var key = someResource._getUidString();
36 | resourceCache._cache[key] = undefined;
37 | var i = resourceCache._cacheList.indexOf(someResource);
38 | resourceCache._cacheList.splice(i, 1);
39 | };
40 |
41 | return resourceCache;
42 | };
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@holidayextras/jsonapi-client",
3 | "version": "1.0.0",
4 | "description": "A clientside module designed to make it really easy to consume a json:api service.",
5 | "keywords": [
6 | "jsonapi",
7 | "json:api",
8 | "api"
9 | ],
10 | "main": "lib/Client.js",
11 | "author": "Oliver Rumbelow",
12 | "license": "MIT",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/holidayextras/jsonapi-client.git"
16 | },
17 | "dependencies": {
18 | "async": "2.5.0",
19 | "object-assign": "4.1.1",
20 | "promise": "8.0.1",
21 | "qs": "^6.2.0",
22 | "superagent": "3.5.2"
23 | },
24 | "devDependencies": {
25 | "blanket": "1.2.3",
26 | "browserify": "14.4.0",
27 | "coveralls": "2.13.1",
28 | "eslint": "4.2.0",
29 | "eslint-config-eslint": "4.0.0",
30 | "flow-bin": "0.50.0",
31 | "jscpd": "^0.6.1",
32 | "jsonapi-server": "3.0.2",
33 | "karma": "1.7.0",
34 | "karma-chrome-launcher": "2.2.0",
35 | "karma-firefox-launcher": "1.0.1",
36 | "karma-mocha": "1.3.0",
37 | "karma-phantomjs-launcher": "1.0.4",
38 | "karma-sauce-launcher": "1.1.0",
39 | "karma-spec-reporter": "0.0.31",
40 | "mocha": "3.4.2",
41 | "mocha-lcov-reporter": "1.3.0",
42 | "phantomjs-prebuilt": "2.1.14",
43 | "plato": "1.7.0",
44 | "uglify-js": "3.0.24"
45 | },
46 | "engines": {
47 | "node": ">=4.5"
48 | },
49 | "scripts": {
50 | "build": "mkdir -p dist && ./node_modules/.bin/browserify lib/Client.js | ./node_modules/.bin/uglifyjs -cm -o dist/jsonapi-client.min.js 2> /dev/null && echo '✔ Assets built!'",
51 | "build:test": "npm run build && ./node_modules/.bin/browserify -i jsonapi-server/example/server.js test/*.js -o dist/jsonapi-client-test.js && echo '✔ Test assets built!'",
52 | "mocha": "./node_modules/mocha/bin/mocha -R spec ./test/*.js",
53 | "prekarmalocal": "DEBUG=* node ./node_modules/jsonapi-server/example/server.js &",
54 | "karmalocal": "sleep 1 && npm run build:test && ./node_modules/karma/bin/karma start ./karma.local.conf.js",
55 | "postkarmalocal": "killall node",
56 | "prekarmaremote": "DEBUG=* node ./node_modules/jsonapi-server/example/server.js &",
57 | "karmaremote": "sleep 1 && npm run build:test && ./node_modules/karma/bin/karma start ./karma.saucelabs.conf.js || true",
58 | "postkarmaremote": "killall node",
59 | "karma": "npm run karmalocal && npm run karmaremote",
60 | "test": "npm run mocha && npm run karma",
61 | "coveralls": "./node_modules/mocha/bin/mocha --require blanket --reporter mocha-lcov-reporter ./test/*.js | ./node_modules/coveralls/bin/coveralls.js",
62 | "coverage": "./node_modules/mocha/bin/mocha --require blanket --reporter html-cov ./test/*.js > coverage.html",
63 | "complexity": "./node_modules/plato/bin/plato -r -d complexity lib",
64 | "lint": "./node_modules/.bin/eslint ./lib/* ./test/*.js --quiet && echo '✔ All good!'",
65 | "flow": "node ./node_modules/flow-bin/cli.js && echo '✔ All good!'",
66 | "jscpd": "jscpd --blame -p ./lib/ || echo 'Finished!'",
67 | "ci": "npm run lint && npm run flow && npm run jscpd && npm run test"
68 | },
69 | "config": {
70 | "blanket": {
71 | "pattern": "/jsonapi-client/lib/",
72 | "data-cover-never": [
73 | "node_modules",
74 | "test",
75 | "example"
76 | ]
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/test/_testServer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var jsonApiTestServer = require("jsonapi-server/example/server.js");
4 | if (!jsonApiTestServer.start) return;
5 |
6 | jsonApiTestServer.getExpressServer().use(function (req, res, next) {
7 | res.header("Access-Control-Allow-Origin", "*");
8 | res.header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS");
9 | res.header("Access-Control-Allow-Headers", "Content-Type");
10 | next();
11 | });
12 |
13 | before(function() {
14 | jsonApiTestServer.start();
15 | });
16 | after(function() {
17 | jsonApiTestServer.close();
18 | });
19 |
--------------------------------------------------------------------------------
/test/authentication.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var assert = require("assert");
3 | var Client = require("../.");
4 | require("./_testServer.js");
5 |
6 | describe("Testing jsonapi-client", function() {
7 | describe("authentication", function() {
8 |
9 | it("is denied access with the blockme header", function(done) {
10 | var client = new Client("http://localhost:16006/rest", {
11 | header: {
12 | blockme: true
13 | }
14 | });
15 |
16 | client.get("people", "ad3aa89e-9c5b-4ac9-a652-6670f9f27587", function(err) {
17 | assert.equal(err.message, "401 Unauthorized");
18 | done();
19 | });
20 | });
21 |
22 | xit("is denied access with the blockMe cookie", function(done) {
23 | var client = new Client("http://localhost:16006/rest", {
24 | cookie: {
25 | blockMe: true
26 | }
27 | });
28 |
29 | client.get("people", "ad3aa89e-9c5b-4ac9-a652-6670f9f27587", function(err) {
30 | assert.equal(err.message, "401 Unauthorized");
31 | done();
32 | });
33 | });
34 |
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/promises.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var assert = require("assert");
3 | var Client = require("../.");
4 | require("./_testServer.js");
5 |
6 | var client = new Client("http://localhost:16006/rest");
7 |
8 | describe("Testing jsonapi-client", function() {
9 |
10 | context("supports promises", function() {
11 |
12 | it("client.find", function(done) {
13 | client.find("people", { }).then(function(people) {
14 | assert.ok(people[0] instanceof Client.Resource);
15 | }).then(done, done);
16 | });
17 |
18 | it("client.get", function(done) {
19 | client.get("people", "d850ea75-4427-4f81-8595-039990aeede5", { }).then(function(person) {
20 | assert.ok(person instanceof Client.Resource);
21 | }).then(done, done);
22 | });
23 |
24 | it("client.fetch", function(done) {
25 | client.get("people", "d850ea75-4427-4f81-8595-039990aeede5", { }).then(function(person) {
26 | return person.fetch("articles", { include: "photos" });
27 | }).then(function(articles) {
28 | assert.ok(articles[0] instanceof Client.Resource);
29 | assert.ok(articles[0].photos[0] instanceof Client.Resource);
30 | }).then(done, done);
31 | });
32 |
33 | it("create-sync-delete", function(done) {
34 | var newPerson = client.create("people");
35 | var uuid;
36 | newPerson.sync().then(function() {
37 | uuid = newPerson._getUid();
38 | return newPerson.delete();
39 | }).then(function() {
40 | return client.get("people", uuid, { });
41 | }).then(function() {
42 | throw new Error("Should have errored!");
43 | }, function(err) {
44 | assert.ok(err.message.match(/There is no people with id /));
45 | done();
46 | });
47 | });
48 |
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/test/read.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var assert = require("assert");
3 | var Client = require("../.");
4 | require("./_testServer.js");
5 |
6 | var client = new Client("http://localhost:16006/rest");
7 |
8 | describe("Testing jsonapi-client", function() {
9 |
10 | context("searches for resources", function() {
11 | it("resource.toJSONTree() should provide developer-friendly debug info", function(done) {
12 | client.find("people", { include: "articles", filter: { lastname: "Rumbelow"}}, function(err, people) {
13 | assert.equal(err, null);
14 |
15 | var treeView = people.map(function(person) {
16 | return person.toJSONTree();
17 | });
18 |
19 | assert.deepEqual(treeView, [
20 | {
21 | "id": "cc5cca2e-0dd8-4b95-8cfc-a11230e73116",
22 | "type": "people",
23 | "firstname": "Oli",
24 | "lastname": "Rumbelow",
25 | "email": "oliver.rumbelow@example.com",
26 | "articles": [
27 | {
28 | "id": "de305d54-75b4-431b-adb2-eb6b9e546014",
29 | "type": "articles",
30 | "title": "NodeJS Best Practices",
31 | "status": "published",
32 | "content": "na",
33 | "author": "[Circular]",
34 | "created": "2016-01-05",
35 | "views": "10",
36 | "tags": [
37 | {
38 | "type": "tags",
39 | "id": "7541a4de-4986-4597-81b9-cf31b6762486"
40 | }
41 | ],
42 | "photos": [],
43 | "comments": [
44 | {
45 | "type": "comments",
46 | "id": "3f1a89c2-eb85-4799-a048-6735db24b7eb"
47 | }
48 | ]
49 | }
50 | ],
51 | "photos": null
52 | }
53 | ]);
54 |
55 | done();
56 | });
57 | });
58 |
59 | it("finds resources", function(done) {
60 | client.find("articles", { }, function(err, people) {
61 | assert.equal(err, null);
62 | assert.equal(people.length, 4);
63 |
64 | done();
65 | });
66 | });
67 |
68 | it("filters resources", function(done) {
69 | client.find("articles", { filter: { title: "