Comparing version 3.5.0 to 4.0.0-beta.0
{ | ||
"name": "fakerest", | ||
"version": "3.5.0", | ||
"version": "4.0.0-beta.0", | ||
"repository": "https://github.com/marmelab/FakeRest", | ||
"description": "Patch XMLHttpRequest to fake a REST server based on JSON data. ", | ||
"scripts": { | ||
"test": "jest" | ||
"dev": "vite", | ||
"build": "vite build && vite build -c vite.config.min.ts", | ||
"format": "biome format --write src", | ||
"lint": "biome lint --apply src", | ||
"test": "vitest", | ||
"prepare": "husky" | ||
}, | ||
"main": "dist/FakeRest.min.js", | ||
"type": "module", | ||
"main": "dist/fakerest.min.umd.cjs", | ||
"module": "./dist/fakerest.min.js", | ||
"types": "./dist/FakeRest.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./dist/fakerest.min.js", | ||
"require": "./dist/fakerest.umd.cjs" | ||
} | ||
}, | ||
"author": "François Zaninotto <fzaninotto@gmail.com>", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"@babel/preset-env": "^7.12.13", | ||
"babel-core": "^6.26.3", | ||
"babel-loader": "^8.2.2", | ||
"babel-plugin-add-module-exports": "^1.0.4", | ||
"babel-plugin-transform-runtime": "^6.23.0", | ||
"babel-polyfill": "^6.26.0", | ||
"babel-runtime": "^6.26.0", | ||
"@biomejs/biome": "1.7.0", | ||
"@types/lodash": "^4.17.0", | ||
"@types/sinon": "^17.0.3", | ||
"@vitejs/plugin-react": "^4.2.1", | ||
"fetch-mock": "^9.11.0", | ||
"jest": "^26.6.3", | ||
"sinon": "~1.14.1", | ||
"terser-webpack-plugin": "^5.1.1", | ||
"webpack": "^5.20.0", | ||
"webpack-cli": "^4.5.0" | ||
"happy-dom": "^14.7.1", | ||
"husky": "^9.0.11", | ||
"lint-staged": "^15.2.2", | ||
"msw": "^2.2.14", | ||
"ra-data-simple-rest": "^4.16.15", | ||
"react": "^17.0.2", | ||
"react-admin": "^4.16.15", | ||
"react-dom": "^17.0.2", | ||
"sinon": "~18.0.0", | ||
"typescript": "^5.4.5", | ||
"vite": "^5.2.9", | ||
"vite-plugin-dts": "^3.8.3", | ||
"vitest": "^1.5.0" | ||
}, | ||
"dependencies": { | ||
"babel-runtime": "^6.26.0", | ||
"lodash": "^4.17.21" | ||
}, | ||
"browserslist": "> 0.25%, not dead" | ||
} | ||
"browserslist": "> 0.25%, not dead", | ||
"lint-staged": { | ||
"*.{js,jsx,ts,tsx}": [ | ||
"biome lint --apply", | ||
"biome format --write" | ||
] | ||
}, | ||
"msw": { | ||
"workerDirectory": [ | ||
"public" | ||
] | ||
} | ||
} |
983
README.md
@@ -1,373 +0,793 @@ | ||
# FakeRest [![Build Status](https://travis-ci.org/marmelab/FakeRest.svg?branch=master)](https://travis-ci.org/marmelab/FakeRest) | ||
# FakeRest | ||
Intercept AJAX calls to fake a REST server based on JSON data. Use it on top of [Sinon.js](http://sinonjs.org/) (for `XMLHTTPRequest`) or [fetch-mock](https://github.com/wheresrhys/fetch-mock) (for `fetch`) to test JavaScript REST clients on the browser side (e.g. single page apps) without a server. | ||
A browser library that intercepts AJAX calls to mock a REST server based on JSON data. | ||
Use it in conjunction with [MSW](https://mswjs.io/), [fetch-mock](https://www.wheresrhys.co.uk/fetch-mock/), or [Sinon.js](https://sinonjs.org/releases/v18/fake-xhr-and-server/) to test JavaScript REST clients on the client side (e.g. single page apps) without a server. | ||
See it in action in the [react-admin](https://marmelab.com/react-admin/) [demo](https://marmelab.com/react-admin-demo) ([source code](https://github.com/marmelab/react-admin/tree/master/examples/demo)). | ||
## Installation | ||
```sh | ||
npm install fakerest --save-dev | ||
``` | ||
## Usage | ||
### Fake XMLHTTPRequest | ||
FakeRest lets you create a handler function that you can pass to an API mocking library. FakeRest supports [MSW](https://mswjs.io/), [fetch-mock](https://www.wheresrhys.co.uk/fetch-mock/), and [Sinon](https://sinonjs.org/releases/v18/fake-xhr-and-server/). If you have the choice, we recommend using MSW, as it will allow you to inspect requests as you usually do in the dev tools network tab. | ||
```html | ||
<script src="/path/to/FakeRest.min.js"></script> | ||
<script src="/path/to/sinon.js"></script> | ||
<script type="text/javascript"> | ||
var data = { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
}; | ||
// initialize fake REST server | ||
var restServer = new FakeRest.Server(); | ||
restServer.init(data); | ||
### MSW | ||
// use sinon.js to monkey-patch XmlHttpRequest | ||
var server = sinon.fakeServer.create(); | ||
server.respondWith(restServer.getHandler()); | ||
</script> | ||
Install [MSW](https://mswjs.io/) and initialize it: | ||
```sh | ||
npm install msw@latest --save-dev | ||
npx msw init <PUBLIC_DIR> # eg: public | ||
``` | ||
### Fake fetch | ||
Then configure an MSW worker: | ||
```js | ||
import fetchMock from 'fetch-mock'; | ||
import FakeRest from 'fakerest'; | ||
var data = { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
}; | ||
const restServer = new FakeRest.FetchServer('http://localhost:3000'); | ||
restServer.init(data); | ||
fetchMock.mock('begin:http://localhost:3000', restServer.getHandler()); | ||
// in ./src/fakeServer.js | ||
import { setupWorker } from "msw/browser"; | ||
import { getMswHandler } from "fakerest"; | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://localhost:3000', | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
} | ||
}); | ||
export const worker = setupWorker(handler); | ||
``` | ||
FakeRest will now intercept every `XmlHttpRequest` to the REST server. The handled routes for collections of items are: | ||
Finally, call the `worker.start()` method before rendering your application. For instance, in a Vite React application: | ||
```js | ||
import React from "react"; | ||
import ReactDom from "react-dom"; | ||
import { App } from "./App"; | ||
import { worker } from "./fakeServer"; | ||
worker.start({ | ||
quiet: true, // Instruct MSW to not log requests in the console | ||
onUnhandledRequest: 'bypass', // Instruct MSW to ignore requests we don't handle | ||
}).then(() => { | ||
ReactDom.render(<App />, document.getElementById("root")); | ||
}); | ||
``` | ||
GET /:resource | ||
POST /:resource | ||
GET /:resource/:id | ||
PUT /:resource/:id | ||
PATCH /:resource/:id | ||
DELETE /:resource/:id | ||
``` | ||
The handled routes for single items are: | ||
FakeRest will now intercept every `fetch` request to the REST server. | ||
### fetch-mock | ||
Install [fetch-mock](https://www.wheresrhys.co.uk/fetch-mock/): | ||
```sh | ||
npm install fetch-mock --save-dev | ||
``` | ||
GET /:resource | ||
PUT /:resource | ||
PATCH /:resource | ||
``` | ||
You can then create a handler and pass it to fetch-mock: | ||
Let's see an example: | ||
```js | ||
// Query the fake REST server | ||
var req = new XMLHttpRequest(); | ||
req.open("GET", "/authors", false); | ||
req.send(null); | ||
console.log(req.responseText); | ||
// [ | ||
// {"id":0,"first_name":"Leo","last_name":"Tolstoi"}, | ||
// {"id":1,"first_name":"Jane","last_name":"Austen"} | ||
// ] | ||
import fetchMock from 'fetch-mock'; | ||
import { getFetchMockHandler } from "fakerest"; | ||
var req = new XMLHttpRequest(); | ||
req.open("GET", "/books/3", false); | ||
req.send(null); | ||
console.log(req.responseText); | ||
// {"id":3,"author_id":1,"title":"Sense and Sensibility"} | ||
const handler = getFetchMockHandler({ | ||
baseUrl: 'http://localhost:3000', | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
} | ||
}); | ||
var req = new XMLHttpRequest(); | ||
req.open("GET", "/settings", false); | ||
req.send(null); | ||
console.log(req.responseText); | ||
// {"language:"english","preferred_format":"hardback"} | ||
fetchMock.mock('begin:http://localhost:3000', handler); | ||
``` | ||
var req = new XMLHttpRequest(); | ||
req.open("POST", "/books", false); | ||
req.send(JSON.stringify({ author_id: 1, title: 'Emma' })); | ||
console.log(req.responseText); | ||
// {"author_id":1,"title":"Emma","id":4} | ||
FakeRest will now intercept every `fetch` request to the REST server. | ||
// restore native XHR constructor | ||
server.restore(); | ||
``` | ||
### Sinon | ||
*Tip*: The `fakerServer` provided by Sinon.js is [available as a standalone library](http://sinonjs.org/docs/#server), without the entire stubbing framework. Simply add the following bower dependency: | ||
Install [Sinon](https://sinonjs.org/releases/v18/fake-xhr-and-server/): | ||
```sh | ||
npm install sinon --save-dev | ||
``` | ||
devDependencies: { | ||
"sinon-server": "http://sinonjs.org/releases/sinon-server-1.14.1.js" | ||
} | ||
``` | ||
## Installation | ||
Then, configure a Sinon server: | ||
FakeRest is available through npm and Bower: | ||
```js | ||
import sinon from 'sinon'; | ||
import { getSinonHandler } from "fakerest"; | ||
```sh | ||
# If you use Bower | ||
bower install fakerest --save-dev | ||
# If you use npm | ||
npm install fakerest --save-dev | ||
const handler = getSinonHandler({ | ||
baseUrl: 'http://localhost:3000', | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
}, | ||
}); | ||
// use sinon.js to monkey-patch XmlHttpRequest | ||
const sinonServer = sinon.fakeServer.create(); | ||
// this is required when doing asynchronous XmlHttpRequest | ||
sinonServer.autoRespond = true; | ||
sinonServer.respondWith(handler); | ||
``` | ||
## REST Flavor | ||
FakeRest will now intercept every `XMLHttpRequest` request to the REST server. | ||
FakeRest defines a REST flavor, described below. It is inspired by commonly used ways how to handle aspects like filtering and sorting. | ||
## REST Syntax | ||
* `GET /foo` returns a JSON array. It accepts three query parameters: `filter`, `sort`, and `range`. It responds with a status 200 if there is no pagination, or 206 if the list of items is paginated. The response contains a mention of the total count in the `Content-Range` header. | ||
FakeRest uses a simple REST syntax described below. | ||
GET /books?filter={"author_id":1}&embed=["author"]&sort=["title","desc"]&range=[0-9] | ||
### Get A Collection of records | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }, | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
] | ||
`GET /[name]` returns an array of records in the `name` collection. It accepts 4 query parameters: `filter`, `sort`, `range`, and `embed`. It responds with a status 200 if there is no pagination, or 206 if the list of items is paginated. The response mentions the total count in the `Content-Range` header. | ||
The `filter` param must be a serialized object literal describing the criteria to apply to the search query. | ||
GET /books?filter={"author_id":1}&embed=["author"]&sort=["title","desc"]&range=[0-9] | ||
GET /books?filter={"author_id":1} // return books where author_id is equal to 1 | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility" } | ||
] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }, | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
] | ||
// array values are possible | ||
GET /books?filter={"id":[2,3]} // return books where id is in [2,3] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility" } | ||
] | ||
The `filter` param must be a serialized object literal describing the criteria to apply to the search query. See the [supported filters](#supported-filters) for more details. | ||
// use the special "q" filter to make a full-text search on all text fields | ||
GET /books?filter={"q":"and"} // return books where any of the book properties contains the string 'and' | ||
GET /books?filter={"author_id":1} // return books where author_id is equal to 1 | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility" } | ||
] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-2/3 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 1, "author_id": 0, "title": "War and Peace" }, | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility" } | ||
] | ||
// array values are possible | ||
GET /books?filter={"id":[2,3]} // return books where id is in [2,3] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility" } | ||
] | ||
// use _gt, _gte, _lte, _lt, or _neq suffix on filter names to make range queries | ||
GET /books?filter={"price_lte":20} // return books where price is less than or equal to 20 | ||
GET /books?filter={"price_gt":20} // return books where price is greater than 20 | ||
// use the special "q" filter to make a full-text search on all text fields | ||
GET /books?filter={"q":"and"} // return books where any of the book properties contains the string 'and' | ||
// when the filter object contains more than one property, the criteria combine with an AND logic | ||
GET /books?filter={"published_at_gte":"2015-06-12","published_at_lte":"2015-06-15"} // return books published between two dates | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-2/3 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 1, "author_id": 0, "title": "War and Peace" }, | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility" } | ||
] | ||
The `embed` param sets the related objects or collections to be embedded in the response. | ||
// use _gt, _gte, _lte, _lt, or _neq suffix on filter names to make range queries | ||
GET /books?filter={"price_lte":20} // return books where the price is less than or equal to 20 | ||
GET /books?filter={"price_gt":20} // return books where the price is greater than 20 | ||
// embed author in books | ||
GET /books?embed=["author"] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-3/4 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 0, "author_id": 0, "title": "Anna Karenina", "author": { "id": 0, "first_name": "Leo", "last_name": "Tolstoi" } }, | ||
{ "id": 1, "author_id": 0, "title": "War and Peace", "author": { "id": 0, "first_name": "Leo", "last_name": "Tolstoi" } }, | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
] | ||
// when the filter object contains more than one property, the criteria combine with an AND logic | ||
GET /books?filter={"published_at_gte":"2015-06-12","published_at_lte":"2015-06-15"} // return books published between two dates | ||
// embed books in author | ||
GET /authors?embed=["books"] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi', books: [{ id: 0, author_id: 0, title: 'Anna Karenina' }, { id: 1, author_id: 0, title: 'War and Peace' }] }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen', books: [{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, { id: 3, author_id: 1, title: 'Sense and Sensibility' }] } | ||
] | ||
The `sort` param must be a serialized array literal defining first the property used for sorting, then the sorting direction. | ||
// you can embed several objects | ||
GET /authors?embed=["books","country"] | ||
GET /author?sort=["date_of_birth","asc"] // return authors, the oldest first | ||
GET /author?sort=["date_of_birth","desc"] // return authors, the youngest first | ||
The `sort` param must be a serialized array literal defining first the property used for sorting, then the sorting direction. | ||
The `range` param defines the number of results by specifying the rank of the first and last results. The first result is #0. | ||
GET /author?sort=["date_of_birth","asc"] // return authors, the oldest first | ||
GET /author?sort=["date_of_birth","desc"] // return authors, the youngest first | ||
GET /books?range=[0-9] // return the first 10 books | ||
GET /books?range=[10-19] // return the 10 next books | ||
The `range` param defines the number of results by specifying the rank of the first and last result. The first result is #0. | ||
The `embed` param sets the related objects or collections to be embedded in the response. | ||
GET /books?range=[0-9] // return the first 10 books | ||
GET /books?range=[10-19] // return the 10 next books | ||
// embed author in books | ||
GET /books?embed=["author"] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-3/4 | ||
Content-Type: application/json | ||
[ | ||
{ "id": 0, "author_id": 0, "title": "Anna Karenina", "author": { "id": 0, "first_name": "Leo", "last_name": "Tolstoi" } }, | ||
{ "id": 1, "author_id": 0, "title": "War and Peace", "author": { "id": 0, "first_name": "Leo", "last_name": "Tolstoi" } }, | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }, | ||
{ "id": 3, "author_id": 1, "title": "Sense and Sensibility", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
] | ||
* `POST /foo` returns a status 201 with a `Location` header for the newly created resource, and the new resource in the body. | ||
// embed books in author | ||
GET /authors?embed=["books"] | ||
HTTP 1.1 200 OK | ||
Content-Range: items 0-1/2 | ||
Content-Type: application/json | ||
[ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi', books: [{ id: 0, author_id: 0, title: 'Anna Karenina' }, { id: 1, author_id: 0, title: 'War and Peace' }] }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen', books: [{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, { id: 3, author_id: 1, title: 'Sense and Sensibility' }] } | ||
] | ||
POST /books | ||
{ "author_id": 1, "title": "Emma" } | ||
// you can embed several objects | ||
GET /authors?embed=["books","country"] | ||
HTTP 1.1 201 Created | ||
Location: /books/4 | ||
Content-Type: application/json | ||
{ "author_id": 1, "title": "Emma", "id": 4 } | ||
### Get A Single Record | ||
* `GET /foo/:id` returns a JSON object, and a status 200, unless the resource doesn't exist | ||
`GET /[name]/:id` returns a JSON object, and a status 200, unless the resource doesn't exist. | ||
GET /books/2 | ||
GET /books/2 | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" } | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" } | ||
The `embed` param sets the related objects or collections to be embedded in the response. | ||
The `embed` param sets the related objects or collections to be embedded in the response. | ||
GET /books/2?embed=['author'] | ||
GET /books/2?embed=['author'] | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
* `PUT /foo/:id` returns the modified JSON object, and a status 200, unless the resource doesn't exist | ||
* `DELETE /foo/:id` returns the deleted JSON object, and a status 200, unless the resource doesn't exist | ||
### Create A Record | ||
If the REST flavor you want to simulate differs from the one chosen for FakeRest, no problem: request and response interceptors will do the conversion (see below). | ||
`POST /[name]` returns a status 201 with a `Location` header for the newly created resource, and the new resource in the body. | ||
Note that all of the above apply only to collections. Single objects respond to `GET /bar`, `PUT /bar` and `PATCH /bar` in a manner identical to those operations for `/foo/:id`, including embedding. `POST /bar` and `DELETE /bar` are not enabled. | ||
POST /books | ||
{ "author_id": 1, "title": "Emma" } | ||
## Supported Filters | ||
HTTP 1.1 201 Created | ||
Location: /books/4 | ||
Content-Type: application/json | ||
{ "author_id": 1, "title": "Emma", "id": 4 } | ||
Operators are specified as suffixes on each filtered field. For instance, applying the `_lte` operator on the `price` field for the `books` resource is done by like this: | ||
GET /books?filter={"price_lte":20} // return books where price is less than or equal to 20 | ||
### Update A Record | ||
`PUT /[name]/:id` returns the modified JSON object, and a status 200, unless the resource doesn't exist. | ||
PUT /books/2 | ||
{ "author_id": 1, "title": "Pride and Prejudice" } | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" } | ||
### Delete A Single Record | ||
`DELETE /[name]/:id` returns the deleted JSON object, and a status 200, unless the resource doesn't exist. | ||
DELETE /books/2 | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice" } | ||
### Supported Filters | ||
Operators are specified as suffixes on each filtered field. For instance, applying the `_lte` operator on the `price` field for the `books` resource is done like this: | ||
GET /books?filter={"price_lte":20} // return books where the price is less than or equal to 20 | ||
- `_eq`: check for equality on simple values: | ||
GET /books?filter={"price_eq":20} // return books where price is equal to 20 | ||
GET /books?filter={"price_eq":20} // return books where the price is equal to 20 | ||
- `_neq`: check for inequality on simple values | ||
GET /books?filter={"price_neq":20} // return books where price is not equal to 20 | ||
GET /books?filter={"price_neq":20} // return books where the price is not equal to 20 | ||
- `_eq_any`: check for equality on any passed values | ||
GET /books?filter={"price_eq_any":[20, 30]} // return books where price is equal to 20 or 30 | ||
GET /books?filter={"price_eq_any":[20, 30]} // return books where the price is equal to 20 or 30 | ||
- `_neq_any`: check for inequality on any passed values | ||
GET /books?filter={"price_neq_any":[20, 30]} // return books where price is not equal to 20 nor 30 | ||
GET /books?filter={"price_neq_any":[20, 30]} // return books where the price is not equal to 20 nor 30 | ||
- `_inc_any`: check for items that includes any of the passed values | ||
- `_inc_any`: check for items that include any of the passed values | ||
GET /books?filter={"authors_inc_any":['William Gibson', 'Pat Cadigan']} // return books where authors includes either 'William Gibson' or 'Pat Cadigan' or both | ||
GET /books?filter={"authors_inc_any":['William Gibson', 'Pat Cadigan']} // return books where authors include either 'William Gibson' or 'Pat Cadigan' or both | ||
- `_q`: check for items that contains the provided text | ||
- `_q`: check for items that contain the provided text | ||
GET /books?filter={"author_q":['Gibson']} // return books where author includes 'Gibson' not considering the other fields | ||
GET /books?filter={"author_q":['Gibson']} // return books where the author includes 'Gibson' not considering the other fields | ||
- `_lt`: check for items that has a value lower than the provided value | ||
- `_lt`: check for items that have a value lower than the provided value | ||
GET /books?filter={"price_lte":100} // return books that have a price lower that 100 | ||
GET /books?filter={"price_lte":100} // return books that have a price lower that 100 | ||
- `_lte`: check for items that has a value lower or equal than the provided value | ||
- `_lte`: check for items that have a value lower than or equal to the provided value | ||
GET /books?filter={"price_lte":100} // return books that have a price lower or equal to 100 | ||
GET /books?filter={"price_lte":100} // return books that have a price lower or equal to 100 | ||
- `_gt`: check for items that has a value greater than the provided value | ||
- `_gt`: check for items that have a value greater than the provided value | ||
GET /books?filter={"price_gte":100} // return books that have a price greater that 100 | ||
GET /books?filter={"price_gte":100} // return books that have a price greater that 100 | ||
- `_gte`: check for items that has a value greater or equal than the provided value | ||
- `_gte`: check for items that have a value greater than or equal to the provided value | ||
GET /books?filter={"price_gte":100} // return books that have a price greater or equal to 100 | ||
GET /books?filter={"price_gte":100} // return books that have a price greater or equal to 100 | ||
## Usage and Configuration | ||
### Single Elements | ||
FakeRest allows you to define a single element, such as a user profile or global settings, that can be fetched, updated, or deleted. | ||
GET /settings | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "language": "english", "preferred_format": "hardback" } | ||
PUT /settings | ||
{ "language": "french", "preferred_format": "paperback" } | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "language": "french", "preferred_format": "paperback" } | ||
DELETE /settings | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "language": "french", "preferred_format": "paperback" } | ||
## Middlewares | ||
Middlewares let you intercept requests and simulate server features such as: | ||
- authentication checks | ||
- server-side validation | ||
- server dynamically generated values | ||
- simulate response delays | ||
You can define middlewares on all handlers, by passing a `middlewares` option: | ||
```js | ||
// initialize a rest server with a custom base URL | ||
var restServer = new FakeRest.Server('http://my.custom.domain'); // // only URLs starting with my.custom.domain will be intercepted | ||
restServer.toggleLogging(); // logging is off by default, enable it to see network calls in the console | ||
// Set all JSON data at once - only if identifier name is 'id' | ||
restServer.init(json); | ||
// modify the request before FakeRest handles it, using a request interceptor | ||
// request is { | ||
// url: '...', | ||
// headers: [...], | ||
// requestBody: '...', | ||
// json: ..., // parsed JSON body | ||
// queryString: '...', | ||
// params: {...} // parsed query string | ||
// } | ||
restServer.addRequestInterceptor(function(request) { | ||
var start = (request.params._start - 1) || 0; | ||
var end = request.params._end !== undefined ? (request.params._end - 1) : 19; | ||
request.params.range = [start, end]; | ||
return request; // always return the modified input | ||
import { getMswHandler } from 'fakerest'; | ||
import { data } from './data'; | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
middlewares: [ | ||
async (context, next) => { | ||
if (context.headers.Authorization === undefined) { | ||
return { | ||
status: 401, | ||
headers: {}, | ||
}; | ||
} | ||
return next(context); | ||
}, | ||
withDelay(300), | ||
], | ||
}); | ||
// modify the response before FakeRest sends it, using a response interceptor | ||
// response is { | ||
// status: ..., | ||
// headers: [...], | ||
// body: {...} | ||
// } | ||
restServer.addResponseInterceptor(function(response) { | ||
response.body = { data: response.body, status: response.status }; | ||
return response; // always return the modified input | ||
``` | ||
A middleware is a function that receives 2 parameters: | ||
- The FakeRest `context`, an object containing the data extracted from the request that FakeRest uses to build the response. It has the following properties: | ||
- `method`: The request method as a string (`GET`, `POST`, `PATCH` or `PUT`) | ||
- `url`: The request URL as a string | ||
- `headers`: The request headers as an object where keys are header names | ||
- `requestBody`: The parsed request data if any | ||
- `params`: The request parameters from the URL search (e.g. the identifier of the requested record) | ||
- `collection`: The name of the targeted [collection](#collection) (e.g. `posts`) | ||
- `single`: The name of the targeted [single](#single) (e.g. `settings`) | ||
- A `next` function to call the next middleware in the chain, to which you must pass the `context` | ||
A middleware must return a FakeRest response either by returning the result of the `next` function or by returning its own response. A FakeRest response is an object with the following properties: | ||
- `status`: The response status as a number (e.g. `200`) | ||
- `headers`: The response HTTP headers as an object where keys are header names | ||
- `body`: The response body which will be stringified | ||
### Authentication Checks | ||
Here's how to implement an authentication check: | ||
```js | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
middlewares: [ | ||
async (context, next) => { | ||
if (context.headers.Authorization === undefined) { | ||
return { status: 401, headers: {} }; | ||
} | ||
return next(context); | ||
} | ||
] | ||
}); | ||
// set default query, e.g. to force embeds or filters | ||
restServer.setDefaultQuery(function(resourceName) { | ||
if (resourceName == 'authors') return { embed: ['books'] } | ||
if (resourceName == 'books') return { filter: { published: true } } | ||
return {}; | ||
}) | ||
// enable batch request handler, i.e. allow API clients to query several resourecs into a single request | ||
// see [Facebook's Batch Requests philosophy](https://developers.facebook.com/docs/graph-api/making-multiple-requests) for more details. | ||
restServer.setBatchUrl('/batch'); | ||
``` | ||
// you can create more than one fake server to listen to several domains | ||
var restServer2 = new FakeRest.Server('http://my.other.domain'); | ||
// Set data collection by collection - allows to customize the identifier name | ||
var authorsCollection = new FakeRest.Collection([], '_id'); | ||
authorsCollection.addOne({ first_name: 'Leo', last_name: 'Tolstoi' }); // { _id: 0, first_name: 'Leo', last_name: 'Tolstoi' } | ||
authorsCollection.addOne({ first_name: 'Jane', last_name: 'Austen' }); // { _id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
// collections have autoincremented identifier but accept identifiers already set | ||
authorsCollection.addOne({ _id: 3, first_name: 'Marcel', last_name: 'Proust' }); // { _id: 3, first_name: 'Marcel', last_name: 'Proust' } | ||
restServer2.addCollection('authors', authorsCollection); | ||
// collections are mutable | ||
authorsCollection.updateOne(1, { last_name: 'Doe' }); // { _id: 1, first_name: 'Jane', last_name: 'Doe' } | ||
authorsCollection.removeOne(3); // { _id: 3, first_name: 'Marcel', last_name: 'Proust' } | ||
### Server-Side Validation | ||
var server = sinon.fakeServer.create(); | ||
server.autoRespond = true; | ||
server.respondWith(restServer.getHandler()); | ||
server.respondWith(restServer2.getHandler()); | ||
Here's how to implement server-side validation: | ||
```js | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
middlewares: [ | ||
async (context, next) => { | ||
if ( | ||
context.collection === "books" && | ||
request.method === "POST" && | ||
!context.requestBody?.title | ||
) { | ||
return { | ||
status: 400, | ||
headers: {}, | ||
body: { | ||
errors: { | ||
title: 'An article with this title already exists. The title must be unique.', | ||
}, | ||
}, | ||
}; | ||
} | ||
return next(context); | ||
} | ||
] | ||
}); | ||
``` | ||
### Dynamically Generated Values | ||
Here's how to implement dynamically generated values on creation: | ||
```js | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
middlewares: [ | ||
async (context, next) => { | ||
if ( | ||
context.collection === 'books' && | ||
context.method === 'POST' | ||
) { | ||
const response = await next(context); | ||
response.body.updatedAt = new Date().toISOString(); | ||
return response; | ||
} | ||
return next(context); | ||
} | ||
] | ||
}); | ||
``` | ||
### Simulate Response Delays | ||
Here's how to simulate response delays: | ||
```js | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
middlewares: [ | ||
async (context, next) => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
resolve(next(context)); | ||
}, 500); | ||
}); | ||
} | ||
] | ||
}); | ||
``` | ||
This is so common FakeRest provides the `withDelay` function for that: | ||
```js | ||
import { getMswHandler, withDelay } from 'fakerest'; | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
middlewares: [ | ||
withDelay(500), // delay in ms | ||
] | ||
}); | ||
``` | ||
## Configuration | ||
All handlers can be customized to accommodate your API structure. | ||
### Identifiers | ||
By default, FakeRest assumes all records have a unique `id` field. | ||
Some databases such as [MongoDB](https://www.mongodb.com) use `_id` instead of `id` for collection identifiers. | ||
You can customize FakeRest to do the same by setting the `identifierName` option: | ||
```js | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
identifierName: '_id' | ||
}); | ||
``` | ||
You can also specify that on a per-collection basis: | ||
```js | ||
import { MswAdapter, Collection } from 'fakerest'; | ||
const adapter = new MswAdapter({ baseUrl: 'http://my.custom.domain', data }); | ||
const authorsCollection = new Collection({ items: [], identifierName: '_id' }); | ||
adapter.server.addCollection('authors', authorsCollection); | ||
const handler = adapter.getHandler(); | ||
``` | ||
### Primary Keys | ||
By default, FakeRest uses an auto-incremented sequence for the item identifiers. | ||
If you'd rather use UUIDs for instance but would like to avoid providing them when you insert new items, you can provide your own function: | ||
```js | ||
import { getMswHandler } from 'fakerest'; | ||
import uuid from 'uuid'; | ||
const handler = new getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
getNewId: () => uuid.v5() | ||
}); | ||
``` | ||
You can also specify that on a per-collection basis: | ||
```js | ||
import { MswAdapter, Collection } from 'fakerest'; | ||
import uuid from 'uuid'; | ||
const adapter = new MswAdapter({ baseUrl: 'http://my.custom.domain', data }); | ||
const authorsCollection = new Collection({ items: [], getNewId: () => uuid.v5() }); | ||
adapter.server.addCollection('authors', authorsCollection); | ||
const handler = adapter.getHandler(); | ||
``` | ||
### Default Queries | ||
Some APIs might enforce some parameters on queries. For instance, an API might always include an [embed](#embed) or enforce a query filter. | ||
You can simulate this using the `defaultQuery` parameter: | ||
```js | ||
import { getMswHandler } from 'fakerest'; | ||
import uuid from 'uuid'; | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://my.custom.domain', | ||
data, | ||
defaultQuery: (collection) => { | ||
if (resourceName == 'authors') return { embed: ['books'] } | ||
if (resourceName == 'books') return { filter: { published: true } } | ||
return {}; | ||
} | ||
}); | ||
``` | ||
## Architecture | ||
Behind a simple API (`getXXXHandler`), FakeRest uses a modular architecture that lets you combine different components to build a fake REST server that fits your needs. | ||
### Mocking Adapter | ||
`getXXXHandler` is a shortcut to an object-oriented API of adapter classes: | ||
```js | ||
export const getMswHandler = (options: MswAdapterOptions) => { | ||
const server = new MswAdapter(options); | ||
return server.getHandler(); | ||
}; | ||
``` | ||
FakeRest provides 3 adapter classes: | ||
- `MswAdapter`: Based on [MSW](https://mswjs.io/) | ||
- `FetchMockAdapter`: Based on [`fetch-mock`](https://www.wheresrhys.co.uk/fetch-mock/) | ||
- `SinonAdapter`: Based on [Sinon](https://sinonjs.org/releases/v18/fake-xhr-and-server/) | ||
You can use the adapter class directly, e.g. if you want to make the adapter instance available in the global scope for debugging purposes: | ||
```js | ||
import { MsWAdapter } from 'fakerest'; | ||
const adapter = new MswAdapter({ | ||
baseUrl: 'http://my.custom.domain', | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
} | ||
}); | ||
window.fakerest = adapter; | ||
const handler = adapter.getHandler(); | ||
``` | ||
### REST Server | ||
Adapters transform requests to a normalized format, pass them to a server object, and transform the normalized server response into the format expected by the mocking library. | ||
The server object implements the REST syntax. It takes a normalized request and exposes a `handle` method that returns a normalized response. FakeRest currently provides only one server implementation: `SimpleRestServer`. | ||
You can specify the server to use in an adapter by passing the `server` option: | ||
```js | ||
const server = new SimpleRestServer({ | ||
baseUrl: 'http://my.custom.domain', | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
} | ||
}); | ||
const adapter = new MswAdapter({ server }); | ||
const handler = adapter.getHandler(); | ||
``` | ||
You can provide an alternative server implementation. This class must implement the `APIServer` type: | ||
```ts | ||
export type APIServer = { | ||
baseUrl?: string; | ||
handle: (context: FakeRestContext) => Promise<BaseResponse>; | ||
}; | ||
export type BaseResponse = { | ||
status: number; | ||
body?: Record<string, any> | Record<string, any>[]; | ||
headers: { [key: string]: string }; | ||
}; | ||
export type FakeRestContext = { | ||
url?: string; | ||
headers?: Headers; | ||
method?: string; | ||
collection?: string; | ||
single?: string; | ||
requestBody: Record<string, any> | undefined; | ||
params: { [key: string]: any }; | ||
}; | ||
``` | ||
The `FakerRestContext` type describes the normalized request. It's usually the adapter's job to transform the request from the mocking library to this format. | ||
### Database | ||
The querying logic is implemented in a class called `Database`, which is independent of the server. It contains [collections](#collections) and [single](#single). | ||
You can specify the database used by a server by setting its `database` property: | ||
```js | ||
const database = new Database({ | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
} | ||
}); | ||
const server = new SimpleRestServer({ baseUrl: 'http://my.custom.domain', database }); | ||
``` | ||
You can even use the database object if you want to manipulate the data: | ||
```js | ||
database.updateOne('authors', 0, { first_name: 'Lev' }); | ||
``` | ||
### Collections & Singles | ||
The Database may contain collections and singles. In the following example, `authors` and `books` are collections, and `settings` is a single. | ||
```js | ||
const handler = getMswHandler({ | ||
baseUrl: 'http://localhost:3000', | ||
data: { | ||
'authors': [ | ||
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' }, | ||
{ id: 1, first_name: 'Jane', last_name: 'Austen' } | ||
], | ||
'books': [ | ||
{ id: 0, author_id: 0, title: 'Anna Karenina' }, | ||
{ id: 1, author_id: 0, title: 'War and Peace' }, | ||
{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, | ||
{ id: 3, author_id: 1, title: 'Sense and Sensibility' } | ||
], | ||
'settings': { | ||
language: 'english', | ||
preferred_format: 'hardback', | ||
} | ||
} | ||
}); | ||
``` | ||
A collection is the equivalent of a classic database table. It supports filtering and direct access to records by their identifier. | ||
A single represents an API endpoint that returns a single entity. It's useful for things such as user profile routes (`/me`) or global settings (`/settings`). | ||
### Embeds | ||
FakeRest supports embedding other resources in a main resource query result. For instance, embedding the author of a book. | ||
GET /books/2?embed=['author'] | ||
HTTP 1.1 200 OK | ||
Content-Type: application/json | ||
{ "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } | ||
Embeds are defined by the query, they require no setup in the database. | ||
## Development | ||
@@ -378,6 +798,15 @@ | ||
make install | ||
# Watch source files and recompile dist/FakeRest.js when anything is modified | ||
make watch | ||
# Run the demo with MSW | ||
make run-msw | ||
# Run the demo with fetch-mock | ||
make run-fetch-mock | ||
# Run the demo with sinon | ||
make run-sinon | ||
# Run tests | ||
make test | ||
# Build minified version | ||
@@ -387,4 +816,6 @@ make build | ||
You can sign-in to the demo with `janedoe` and `password` | ||
## License | ||
FakeRest is licensed under the [MIT Licence](LICENSE), sponsored by [marmelab](http://marmelab.com). | ||
FakeRest is licensed under the [MIT License](LICENSE), sponsored by [marmelab](http://marmelab.com). |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2206118
1
64
17783
820
Yes
18
2
3
- Removedbabel-runtime@^6.26.0
- Removedbabel-runtime@6.26.0(transitive)
- Removedcore-js@2.6.12(transitive)
- Removedregenerator-runtime@0.11.1(transitive)