UNPKG

17.3 kBMarkdownView Raw
1# Angular in-memory-web-api
2[![Build Status][travis-badge]][travis-badge-url]
3
4An in-memory web api for Angular demos and tests
5that emulates CRUD operations over a RESTy API.
6
7It intercepts Angular `Http` and `HttpClient` requests that would otherwise go to the remote server and redirects them to an in-memory data store that you control.
8
9See [Austin McDaniel's article](https://medium.com/@amcdnl/mocking-with-angular-more-than-just-unit-testing-cbb7908c9fcc)
10for a quick introduction.
11
12### _It used to work and now it doesn't :-(_
13
14Perhaps you installed a new version of this library? Check the
15[CHANGELOG.md](https://github.com/angular/in-memory-web-api/blob/master/CHANGELOG.md)
16for breaking changes that may have affected your app.
17
18If that doesn't explain it, create an
19[issue on github](https://github.com/angular/in-memory-web-api/issues),
20preferably with a small repro.
21
22## Use cases
23
24* Demo apps that need to simulate CRUD data persistence operations without a real server.
25You won't have to build and start a test server.
26
27* Whip up prototypes and proofs of concept.
28
29* Share examples with the community in a web coding environment such as Plunker or CodePen.
30Create Angular issues and StackOverflow answers supported by live code.
31
32* Simulate operations against data collections that aren't yet implemented on your dev/test server.
33You can pass requests thru to the dev/test server for collections that are supported.
34
35* Write unit test apps that read and write data.
36Avoid the hassle of intercepting multiple http calls and manufacturing sequences of responses.
37The in-memory data store resets for each test so there is no cross-test data pollution.
38
39* End-to-end tests. If you can toggle the app into test mode
40using the in-memory web api, you won't disturb the real database.
41This can be especially useful for CI (continuous integration) builds.
42
43
44>**LIMITATIONS**
45>
46>The _in-memory-web-api_ exists primarily to support the
47[Angular documentation](https://angular.io/docs/ts/latest/ "Angular documentation web site").
48It is not supposed to emulate every possible real world web API and is not intended for production use.
49>
50>Most importantly, it is ***always experimental***.
51We will make breaking changes and we won't feel bad about it
52because this is a development tool, not a production product.
53We do try to tell you about such changes in the `CHANGELOG.md`
54and we fix bugs as fast as we can.
55
56## HTTP request handling
57This in-memory web api service processes an HTTP request and
58returns an `Observable` of HTTP `Response` object
59in the manner of a RESTy web api.
60It natively handles URI patterns in the form `:base/:collectionName/:id?`
61
62Examples:
63```ts
64 // for requests to an `api` base URL that gets heroes from a 'heroes' collection
65 GET api/heroes // all heroes
66 GET api/heroes/42 // the hero with id=42
67 GET api/heroes?name=^j // 'j' is a regex; returns heroes whose name starting with 'j' or 'J'
68 GET api/heroes.json/42 // ignores the ".json"
69```
70
71The in-memory web api service processes these requests against a "database" - a set of named collections - that you define during setup.
72
73## Basic setup
74
75<a id="createDb"></a>
76
77Create an `InMemoryDataService` class that implements `InMemoryDbService`.
78
79At minimum it must implement `createDb` which
80creates a "database" hash whose keys are collection names
81and whose values are arrays of collection objects to return or update.
82For example:
83```ts
84import { InMemoryDbService } from 'angular-in-memory-web-api';
85
86export class InMemHeroService implements InMemoryDbService {
87 createDb() {
88 let heroes = [
89 { id: 1, name: 'Windstorm' },
90 { id: 2, name: 'Bombasto' },
91 { id: 3, name: 'Magneta' },
92 { id: 4, name: 'Tornado' }
93 ];
94 return {heroes};
95 }
96}
97```
98
99**Notes**
100
101* The in-memory web api library _currently_ assumes that every collection has a primary key called `id`.
102
103* The `createDb` method can be synchronous or asynchronous.
104It would have to be asynchronous if you initialized your in-memory database service from a JSON file.
105Return the database _object_, an _observable_ of that object, or a _promise_ of that object. The tests include an example of all three.
106
107* The in-memory web api calls your `InMemoryDbService` data service class's `createDb` method on two occasions.
108
109 1. when it handles the _first_ HTTP request
110 1. when it receives a `resetdb` [command](#commands).
111
112 In the command case, the service passes in a `RequestInfo` object,
113 enabling the `createDb` logic to adjust its behavior per the client request. See the tests for examples.
114
115### Import the in-memory web api module
116
117Register your data store service implementation with the `HttpClientInMemoryWebApiModule`
118in your root `AppModule.imports`
119calling the `forRoot` static method with this service class and an optional configuration object:
120```ts
121import { HttpClientModule } from '@angular/common/http';
122import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
123
124import { InMemHeroService } from '../app/hero.service';
125
126@NgModule({
127 imports: [
128 HttpClientModule,
129 HttpClientInMemoryWebApiModule.forRoot(InMemHeroService),
130 ...
131 ],
132 ...
133})
134export class AppModule { ... }
135```
136
137**_Notes_**
138
139* Always import the `HttpClientInMemoryWebApiModule` _after_ the `HttpClientModule`
140to ensure that the in-memory backend provider supersedes the Angular version.
141
142* You can setup the in-memory web api within a lazy loaded feature module by calling the `.forFeature` method as you would `.forRoot`.
143
144* In production, you want HTTP requests to go to the real server and probably have no need for the _in-memory_ provider.
145CLI-based apps can exclude the provider in production builds like this:
146 ```ts
147 imports: [
148 HttpClientModule,
149 environment.production ?
150 [] : HttpClientInMemoryWebApiModule.forRoot(InMemHeroService)
151 ...
152 ]
153 ```
154
155# Examples
156The tests (`src/app/*.spec.ts` files) in the
157[github repository](https://github.com/angular/in-memory-web-api/tree/master/src/app)
158are a good place to learn how to setup and use this in-memory web api library.
159
160See also the example source code in the official Angular.io documentation such as the
161[HttpClient](https://angular.io/guide/http) guide and the
162[Tour of Heroes](https://angular.io/tutorial/toh-pt6).
163
164# Advanced Features
165Some features are not readily apparent in the basic usage described above.
166
167## Configuration arguments
168
169The `InMemoryBackendConfigArgs` defines a set of options. Add them as the second `forRoot` argument:
170```ts
171 InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 500 }),
172```
173
174**Read the `InMemoryBackendConfigArgs` interface to learn about these options**.
175
176
177## Request evaluation order
178This service can evaluate requests in multiple ways depending upon the configuration.
179Here's how it reasons:
1801. If it looks like a [command](#commands), process as a command
1812. If the [HTTP method is overridden](#method-override), try the override.
1823. If the resource name (after the api base path) matches one of the configured collections, process that
1834. If not but the `Config.passThruUnknownUrl` flag is `true`, try to [pass the request along to a real _XHR_](#passthru).
1845. Return a 404.
185
186See the `handleRequest` method implementation for details.
187
188## Default delayed response
189
190By default this service adds a 500ms delay
191to all data requests to simulate round-trip latency.
192
193>[Command requests](#commands) have zero added delay as they concern
194in-memory service configuration and do not emulate real data requests.
195
196You can change or eliminate the latency by setting a different `delay` value:
197```ts
198 InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 0 }), // no delay
199 InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 1500 }), // 1.5 second delay
200```
201
202## Simple query strings
203Pass custom filters as a regex pattern via query string.
204The query string defines which property and value to match.
205
206Format: `/app/heroes/?propertyName=regexPattern`
207
208The following example matches all names start with the letter 'j' or 'J' in the heroes collection.
209
210`/app/heroes/?name=^j`
211
212>Search pattern matches are case insensitive by default.
213Set `config.caseSensitiveSearch = true` if needed.
214
215<a id="passthru"></a>
216## Pass thru to a live server
217
218If an existing, running remote server should handle requests for collections
219that are not in the in-memory database, set `Config.passThruUnknownUrl: true`.
220Then this service will forward unrecognized requests to the remote server
221via the Angular default `XHR` backend (it depends on whether your using `Http` or `HttpClient`).
222
223<a id="commands"></a>
224## Commands
225
226The client may issue a command request to get configuration state
227from the in-memory web api service, reconfigure it,
228or reset the in-memory database.
229
230When the last segment of the _api base path_ is "commands", the `collectionName` is treated as the _command_.
231
232Example URLs:
233```sh
234 commands/resetdb // Reset the "database" to its original state
235 commands/config // Get or update this service's config object
236```
237
238Usage:
239```sh
240 http.post('commands/resetdb', undefined);
241 http.get('commands/config');
242 http.post('commands/config', '{"delay":1000}');
243```
244
245Command requests do not simulate real remote data access.
246They ignore the latency delay and respond as quickly as possible.
247
248The `resetDb` command
249calls your `InMemoryDbService` data service's [`createDb` method](#createDb) with the `RequestInfo` object,
250enabling the `createDb` logic to adjust its behavior per the client request.
251
252In the following example, the client includes a reset option in the command request body:
253```ts
254http
255 // Reset the database collections with the `clear` option
256 .post('commands/resetDb', { clear: true }))
257
258 // when command finishes, get heroes
259 .concatMap(
260 ()=> http.get<Data>('api/heroes')
261 .map(data => data.data as Hero[])
262 )
263
264 // execute the request sequence and
265 // do something with the heroes
266 .subscribe(...)
267```
268
269See the tests for other examples.
270
271## _parseRequestUrl_
272
273The `parseRequestUrl` parses the request URL into a `ParsedRequestUrl` object.
274`ParsedRequestUrl` is a public interface whose properties guide the in-memory web api
275as it processes the request.
276
277### Default _parseRequestUrl_
278
279Default parsing depends upon certain values of `config`: `apiBase`, `host`, and `urlRoot`.
280Read the source code for the complete story.
281
282Configuring the `apiBase` yields the most interesting changes to `parseRequestUrl` behavior:
283
284* For `apiBase=undefined` and `url='http://localhost/api/customers/42'`
285 ```ts
286 {apiBase: 'api/', collectionName: 'customers', id: '42', ...}
287 ```
288
289* For `apiBase='some/api/root/'` and `url='http://localhost/some/api/root/customers'`
290 ```ts
291 { apiBase: 'some/api/root/', collectionName: 'customers', id: undefined, ... }
292 ```
293
294* For `apiBase='/'` and `url='http://localhost/customers'`
295 ```ts
296 { apiBase: '/', collectionName: 'customers', id: undefined, ... }
297 ```
298
299**The actual api base segment values are ignored**. Only the number of segments matters.
300The following api base strings are considered identical: 'a/b' ~ 'some/api/' ~ `two/segments'
301
302This means that URLs that work with the in-memory web api may be rejected by the real server.
303
304### Custom _parseRequestUrl_
305
306You can override the default parser by implementing a `parseRequestUrl` method in your `InMemoryDbService`.
307
308The service calls your method with two arguments.
3091. `url` - the request URL string
3101. `requestInfoUtils` - utility methods in a `RequestInfoUtilities` object, including the default parser.
311Note that some values have not yet been set as they depend on the outcome of parsing.
312
313Your method must either return a `ParsedRequestUrl` object or `null`|`undefined`,
314in which case the service uses the default parser.
315In this way you can intercept and parse some URLs and leave the others to the default parser.
316
317## Custom _genId_
318
319Collection items are presumed to have a primary key property called `id`.
320
321You can specify the `id` while adding a new item.
322The service will blindly use that `id`; it does not check for uniqueness.
323
324If you do not specify the `id`, the service generates one via the `genId` method.
325
326You can override the default id generator with a method called `genId` in your `InMemoryDbService`.
327Your method receives the new item's collection and collection name.
328It should return the generated id.
329If your generator returns `null`|`undefined`, the service uses the default generator.
330
331## _responseInterceptor_
332
333You can change the response returned by the service's default HTTP methods.
334A typical reason to intercept is to add a header that your application is expecting.
335
336To intercept responses, add a `responseInterceptor` method to your `InMemoryDbService` class.
337The service calls your interceptor like this:
338```ts
339responseOptions = this.responseInterceptor(responseOptions, requestInfo);
340```
341
342<a id="method-override"></a>
343## HTTP method interceptors
344
345You may have HTTP requests that the in-memory web api can't handle properly.
346
347You can override any HTTP method by implementing a method
348of that name in your `InMemoryDbService`.
349
350Your method's name must be the same as the HTTP method name but **all lowercase**.
351The in-memory web api calls it with a `RequestInfo` object that contains request data and utility methods.
352
353For example, if you implemented a `get` method, the web api would be called like this:
354`yourInMemDbService["get"](requestInfo)`.
355
356Your custom HTTP method must return either:
357
358* `Observable<Response>` - you handled the request and the response is available from this
359observable. It _should be "cold"_.
360
361* `null`/`undefined` - you decided not to intervene,
362perhaps because you wish to intercept only certain paths for the given HTTP method.
363The service continues with its default processing of the HTTP request.
364
365The `RequestInfo` is an interface defined in `src/in-mem/interfaces.ts`.
366Its members include:
367```ts
368req: Request; // the request object from the client
369collectionName: string; // calculated from the request url
370collection: any[]; // the corresponding collection (if found)
371id: any; // the item `id` (if specified)
372url: string; // the url in the request
373utils: RequestInfoUtilities; // helper functions
374```
375The functions in `utils` can help you analyze the request
376and compose a response.
377
378## In-memory Web Api Examples
379
380The [github repository](https://github.com/angular/in-memory-web-api/tree/master/src/app)
381demonstrates library usage with tested examples.
382
383The `HeroInMemDataService` class (in `src/app/hero-in-mem-data.service.ts`) is a Hero-oriented `InMemoryDbService`
384such as you might see in an HTTP sample in the Angular documentation.
385
386The `HeroInMemDataOverrideService` class (in `src/app/hero-in-mem-data-override.service.ts`)
387demonstrates a few ways to override methods of the base `HeroInMemDataService`.
388
389The tests ([see below](#testing)) exercise these examples.
390
391# Build Instructions
392
393Follow these steps for updating the library.
394
395- `gulp bump` - up the package version number.
396
397- update `CHANGELOG.md` to record the change. Call out _breaking changes_.
398
399- update `README.md` if usage or interfaces change.
400
401- consider updating the dependency versions in `package.json`.
402
403- `npm install` the new package(s) if you did.
404
405- `npm list --depth=0` to make sure they really did install!
406
407- `gulp clean` to delete all generated files.
408
409- `npm test` to dev-build and run tests (see ["Testing"](#testing) below).
410
411- `gulp build` to build for distribution.
412
413- git add, commit, and push.
414
415- `npm publish`
416
417- Confirm that angular.io docs samples still work
418
419- Add two tags to the release commit in github
420 - the version number
421 - 'latest'
422
423[travis-badge]: https://travis-ci.org/angular/in-memory-web-api.svg?branch=master
424[travis-badge-url]: https://travis-ci.org/angular/in-memory-web-api
425
426## Testing
427
428The "app" for this repo is not a real app.
429It's an Angular data service (`HeroService`) and a bunch of tests.
430
431>Note that the `tsconfig.json` produces a `commonjs` module.
432That's what _Angular specs require_.
433But when building for an app, it should be a `es2015` module,
434as is the `tsconfig-ngc.json` for AOT-ready version of this library.
435
436These tests are a work-in-progress, as tests often are.
437
438The `src/` folder is divided into
439- `app/` - the test "app" and its tests
440- `in-mem/` - the source code for the in-memory web api library
441
442>A real app would reference the in-memory web api node module;
443these tests reference the library source files.
444
445The `karma-test-shim.js` adds the `in-mem` folder to the list of folders that SystemJS should resolve.
446
447## Rollup
448
449The gulp "umd" task runs rollup for tree-shaking.
450
451I don't remember if it ever worked without a lot of warnings.
452The `v0.4.x` release updated to `rollup@0.49` which required updates to the `rollup.config.js`.
453
454Still weirdly runs `cjs` rollup config first that I can’t find (which produces numerous warnings) before doing the right thing and running the `umd` config.
455
456Also does not work if you follow instructions and use the `output` property of `rollup.config.js`; does work when configure it “wrong” and put the options in the root.
457
458Ignoring these issues for now.
459