UNPKG

18 kBJavaScriptView Raw
1"use strict";
2var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3 var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4 if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5 else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6 return c > 3 && r && Object.defineProperty(target, key, r), r;
7};
8var __metadata = (this && this.__metadata) || function (k, v) {
9 if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10};
11var __param = (this && this.__param) || function (paramIndex, decorator) {
12 return function (target, key) { decorator(target, key, paramIndex); }
13};
14var core_1 = require('@angular/core');
15var http_1 = require('@angular/http');
16var Observable_1 = require('rxjs/Observable');
17require('rxjs/add/operator/delay');
18var http_status_codes_1 = require('./http-status-codes');
19/**
20* Seed data for in-memory database
21* Must implement InMemoryDbService.
22*/
23exports.SEED_DATA = new core_1.OpaqueToken('seedData');
24/**
25* InMemoryBackendService configuration options
26* Usage:
27* provide(InMemoryBackendConfig, {useValue: {delay:600}}),
28*/
29var InMemoryBackendConfig = (function () {
30 function InMemoryBackendConfig(config) {
31 if (config === void 0) { config = {}; }
32 Object.assign(this, {
33 defaultResponseOptions: new http_1.BaseResponseOptions(),
34 delay: 500,
35 delete404: false
36 }, config);
37 }
38 return InMemoryBackendConfig;
39}());
40exports.InMemoryBackendConfig = InMemoryBackendConfig;
41exports.isSuccess = function (status) { return (status >= 200 && status < 300); };
42/**
43 * Simulate the behavior of a RESTy web api
44 * backed by the simple in-memory data store provided by the injected InMemoryDataService service.
45 * Conforms mostly to behavior described here:
46 * http://www.restapitutorial.com/lessons/httpmethods.html
47 *
48 * ### Usage
49 *
50 * Create InMemoryDataService class the implements InMemoryDataService.
51 * Register both this service and the seed data as in:
52 * ```
53 * // other imports
54 * import { HTTPPROVIDERS, XHRBackend } from 'angular2/http';
55 * import { InMemoryBackendConfig, InMemoryBackendService, SEEDDATA } from '../in-memory-backend/in-memory-backend.service';
56 * import { InMemoryStoryService } from '../api/in-memory-story.service';
57 *
58 * @Component({
59 * selector: ...,
60 * templateUrl: ...,
61 * providers: [
62 * HTTPPROVIDERS,
63 * provide(XHRBackend, { useClass: InMemoryBackendService }),
64 * provide(SEEDDATA, { useClass: InMemoryStoryService }),
65 * provide(InMemoryBackendConfig, { useValue: { delay: 600 } }),
66 * ]
67 * })
68 * export class AppComponent { ... }
69 * ```
70 */
71var InMemoryBackendService = (function () {
72 function InMemoryBackendService(seedData, config) {
73 this.seedData = seedData;
74 this.config = new InMemoryBackendConfig();
75 this.resetDb();
76 var loc = this.getLocation('./');
77 this.config.host = loc.host;
78 this.config.rootPath = loc.pathname;
79 Object.assign(this.config, config);
80 }
81 InMemoryBackendService.prototype.createConnection = function (req) {
82 var res = this.handleRequest(req);
83 var response = new Observable_1.Observable(function (responseObserver) {
84 if (exports.isSuccess(res.status)) {
85 responseObserver.next(res);
86 responseObserver.complete();
87 }
88 else {
89 responseObserver.error(res);
90 }
91 return function () { }; // unsubscribe function
92 });
93 response = response.delay(this.config.delay || 500);
94 return {
95 readyState: http_1.ReadyState.Done,
96 request: req,
97 response: response
98 };
99 };
100 //// protected /////
101 /**
102 * Process Request and return an Http Response object
103 * in the manner of a RESTy web api.
104 *
105 * Expect URI pattern in the form :base/:collectionName/:id?
106 * Examples:
107 * // for store with a 'characters' collection
108 * GET api/characters // all characters
109 * GET api/characters/42 // the character with id=42
110 * GET api/characters?name=^j // 'j' is a regex; returns characters whose name contains 'j' or 'J'
111 * GET api/characters.json/42 // ignores the ".json"
112 *
113 * POST commands/resetDb // resets the "database"
114 */
115 InMemoryBackendService.prototype.handleRequest = function (req) {
116 var _a = this.parseUrl(req.url), base = _a.base, collectionName = _a.collectionName, id = _a.id, resourceUrl = _a.resourceUrl, query = _a.query;
117 var collection = this.db[collectionName];
118 var reqInfo = {
119 req: req,
120 base: base,
121 collection: collection,
122 collectionName: collectionName,
123 headers: new http_1.Headers({ 'Content-Type': 'application/json' }),
124 id: this.parseId(collection, id),
125 query: query,
126 resourceUrl: resourceUrl
127 };
128 var options;
129 try {
130 if ('commands' === reqInfo.base.toLowerCase()) {
131 options = this.commands(reqInfo);
132 }
133 else if (reqInfo.collection) {
134 switch (req.method) {
135 case http_1.RequestMethod.Get:
136 options = this.get(reqInfo);
137 break;
138 case http_1.RequestMethod.Post:
139 options = this.post(reqInfo);
140 break;
141 case http_1.RequestMethod.Put:
142 options = this.put(reqInfo);
143 break;
144 case http_1.RequestMethod.Delete:
145 options = this.delete(reqInfo);
146 break;
147 default:
148 options = this.createErrorResponse(http_status_codes_1.STATUS.METHOD_NOT_ALLOWED, 'Method not allowed');
149 break;
150 }
151 }
152 else {
153 options = this.createErrorResponse(http_status_codes_1.STATUS.NOT_FOUND, "Collection '" + collectionName + "' not found");
154 }
155 }
156 catch (error) {
157 var err = error.message || error;
158 options = this.createErrorResponse(http_status_codes_1.STATUS.INTERNAL_SERVER_ERROR, "" + err);
159 }
160 options = this.setStatusText(options);
161 if (this.config.defaultResponseOptions) {
162 options = this.config.defaultResponseOptions.merge(options);
163 }
164 return new http_1.Response(options);
165 };
166 /**
167 * Apply query/search parameters as a filter over the collection
168 * This impl only supports RegExp queries on string properties of the collection
169 * ANDs the conditions together
170 */
171 InMemoryBackendService.prototype.applyQuery = function (collection, query) {
172 // extract filtering conditions - {propertyName, RegExps) - from query/search parameters
173 var conditions = [];
174 query.paramsMap.forEach(function (value, name) {
175 value.forEach(function (v) { return conditions.push({ name: name, rx: new RegExp(decodeURI(v), 'i') }); });
176 });
177 var len = conditions.length;
178 if (!len) {
179 return collection;
180 }
181 // AND the RegExp conditions
182 return collection.filter(function (row) {
183 var ok = true;
184 var i = len;
185 while (ok && i) {
186 i -= 1;
187 var cond = conditions[i];
188 ok = cond.rx.test(row[cond.name]);
189 }
190 return ok;
191 });
192 };
193 InMemoryBackendService.prototype.clone = function (data) {
194 return JSON.parse(JSON.stringify(data));
195 };
196 /**
197 * When the `base`="commands", the `collectionName` is the command
198 * Example URLs:
199 * commands/resetdb // Reset the "database" to its original state
200 * commands/config (GET) // Return this service's config object
201 * commands/config (!GET) // Update the config (e.g. delay)
202 *
203 * Usage:
204 * http.post('commands/resetdb', null);
205 * http.get('commands/config');
206 * http.post('commands/config', '{"delay":1000}');
207 */
208 InMemoryBackendService.prototype.commands = function (reqInfo) {
209 var command = reqInfo.collectionName.toLowerCase();
210 var method = reqInfo.req.method;
211 var options;
212 switch (command) {
213 case 'resetdb':
214 this.resetDb();
215 options = new http_1.ResponseOptions({ status: http_status_codes_1.STATUS.OK });
216 break;
217 case 'config':
218 if (method === http_1.RequestMethod.Get) {
219 options = new http_1.ResponseOptions({
220 body: this.clone(this.config),
221 status: http_status_codes_1.STATUS.OK
222 });
223 }
224 else {
225 // Be nice ... any other method is a config update
226 var body = JSON.parse(reqInfo.req.text() || '{}');
227 Object.assign(this.config, body);
228 options = new http_1.ResponseOptions({ status: http_status_codes_1.STATUS.NO_CONTENT });
229 }
230 break;
231 default:
232 options = this.createErrorResponse(http_status_codes_1.STATUS.INTERNAL_SERVER_ERROR, "Unknown command \"" + command + "\"");
233 }
234 return options;
235 };
236 InMemoryBackendService.prototype.createErrorResponse = function (status, message) {
237 return new http_1.ResponseOptions({
238 body: { 'error': "" + message },
239 headers: new http_1.Headers({ 'Content-Type': 'application/json' }),
240 status: status
241 });
242 };
243 InMemoryBackendService.prototype.delete = function (_a) {
244 var id = _a.id, collection = _a.collection, collectionName = _a.collectionName, headers = _a.headers;
245 if (!id) {
246 return this.createErrorResponse(http_status_codes_1.STATUS.NOT_FOUND, "Missing \"" + collectionName + "\" id");
247 }
248 var exists = this.removeById(collection, id);
249 return new http_1.ResponseOptions({
250 headers: headers,
251 status: (exists || !this.config.delete404) ? http_status_codes_1.STATUS.NO_CONTENT : http_status_codes_1.STATUS.NOT_FOUND
252 });
253 };
254 InMemoryBackendService.prototype.findById = function (collection, id) {
255 return collection.find(function (item) { return item.id === id; });
256 };
257 InMemoryBackendService.prototype.genId = function (collection) {
258 // assumes numeric ids
259 var maxId = 0;
260 collection.reduce(function (prev, item) {
261 maxId = Math.max(maxId, typeof item.id === 'number' ? item.id : maxId);
262 }, null);
263 return maxId + 1;
264 };
265 InMemoryBackendService.prototype.get = function (_a) {
266 var id = _a.id, query = _a.query, collection = _a.collection, collectionName = _a.collectionName, headers = _a.headers;
267 var data = collection;
268 if (id) {
269 data = this.findById(collection, id);
270 }
271 else if (query) {
272 data = this.applyQuery(collection, query);
273 }
274 if (!data) {
275 return this.createErrorResponse(http_status_codes_1.STATUS.NOT_FOUND, "'" + collectionName + "' with id='" + id + "' not found");
276 }
277 return new http_1.ResponseOptions({
278 body: { data: this.clone(data) },
279 headers: headers,
280 status: http_status_codes_1.STATUS.OK
281 });
282 };
283 InMemoryBackendService.prototype.getLocation = function (href) {
284 var l = document.createElement('a');
285 l.href = href;
286 return l;
287 };
288 ;
289 InMemoryBackendService.prototype.indexOf = function (collection, id) {
290 return collection.findIndex(function (item) { return item.id === id; });
291 };
292 // tries to parse id as number if collection item.id is a number.
293 // returns the original param id otherwise.
294 InMemoryBackendService.prototype.parseId = function (collection, id) {
295 if (!id) {
296 return null;
297 }
298 var isNumberId = collection[0] && typeof collection[0].id === 'number';
299 if (isNumberId) {
300 var idNum = parseFloat(id);
301 return isNaN(idNum) ? id : idNum;
302 }
303 return id;
304 };
305 InMemoryBackendService.prototype.parseUrl = function (url) {
306 try {
307 var loc = this.getLocation(url);
308 var drop = this.config.rootPath.length;
309 var urlRoot = '';
310 if (loc.host !== this.config.host) {
311 // url for a server on a different host!
312 // assume it's collection is actually here too.
313 drop = 1; // the leading slash
314 urlRoot = loc.protocol + '//' + loc.host + '/';
315 }
316 var path = loc.pathname.substring(drop);
317 var _a = path.split('/'), base = _a[0], collectionName = _a[1], id = _a[2];
318 var resourceUrl = urlRoot + base + '/' + collectionName + '/';
319 collectionName = collectionName.split('.')[0]; // ignore anything after the '.', e.g., '.json'
320 var query = loc.search && new http_1.URLSearchParams(loc.search.substr(1));
321 return { base: base, id: id, collectionName: collectionName, resourceUrl: resourceUrl, query: query };
322 }
323 catch (err) {
324 var msg = "unable to parse url '" + url + "'; original error: " + err.message;
325 throw new Error(msg);
326 }
327 };
328 InMemoryBackendService.prototype.post = function (_a) {
329 var collection = _a.collection, headers = _a.headers, id = _a.id, req = _a.req, resourceUrl = _a.resourceUrl;
330 var item = JSON.parse(req.text());
331 if (!item.id) {
332 item.id = id || this.genId(collection);
333 }
334 // ignore the request id, if any. Alternatively,
335 // could reject request if id differs from item.id
336 id = item.id;
337 var existingIx = this.indexOf(collection, id);
338 if (existingIx > -1) {
339 collection[existingIx] = item;
340 return new http_1.ResponseOptions({
341 headers: headers,
342 status: http_status_codes_1.STATUS.NO_CONTENT
343 });
344 }
345 else {
346 collection.push(item);
347 headers.set('Location', resourceUrl + '/' + id);
348 return new http_1.ResponseOptions({
349 headers: headers,
350 body: { data: this.clone(item) },
351 status: http_status_codes_1.STATUS.CREATED
352 });
353 }
354 };
355 InMemoryBackendService.prototype.put = function (_a) {
356 var id = _a.id, collection = _a.collection, collectionName = _a.collectionName, headers = _a.headers, req = _a.req;
357 var item = JSON.parse(req.text());
358 if (!id) {
359 return this.createErrorResponse(http_status_codes_1.STATUS.NOT_FOUND, "Missing '" + collectionName + "' id");
360 }
361 if (id !== item.id) {
362 return this.createErrorResponse(http_status_codes_1.STATUS.BAD_REQUEST, "\"" + collectionName + "\" id does not match item.id");
363 }
364 var existingIx = this.indexOf(collection, id);
365 if (existingIx > -1) {
366 collection[existingIx] = item;
367 return new http_1.ResponseOptions({
368 headers: headers,
369 status: http_status_codes_1.STATUS.NO_CONTENT // successful; no content
370 });
371 }
372 else {
373 collection.push(item);
374 return new http_1.ResponseOptions({
375 body: { data: this.clone(item) },
376 headers: headers,
377 status: http_status_codes_1.STATUS.CREATED
378 });
379 }
380 };
381 InMemoryBackendService.prototype.removeById = function (collection, id) {
382 var ix = this.indexOf(collection, id);
383 if (ix > -1) {
384 collection.splice(ix, 1);
385 return true;
386 }
387 return false;
388 };
389 /**
390 * Reset the "database" to its original state
391 */
392 InMemoryBackendService.prototype.resetDb = function () {
393 this.db = this.seedData.createDb();
394 };
395 InMemoryBackendService.prototype.setStatusText = function (options) {
396 try {
397 var statusCode = http_status_codes_1.STATUS_CODE_INFO[options.status];
398 options['statusText'] = statusCode ? statusCode.text : 'Unknown Status';
399 return options;
400 }
401 catch (err) {
402 return new http_1.ResponseOptions({
403 status: http_status_codes_1.STATUS.INTERNAL_SERVER_ERROR,
404 statusText: 'Invalid Server Operation'
405 });
406 }
407 };
408 InMemoryBackendService = __decorate([
409 __param(0, core_1.Inject(exports.SEED_DATA)),
410 __param(1, core_1.Inject(InMemoryBackendConfig)),
411 __param(1, core_1.Optional()),
412 __metadata('design:paramtypes', [Object, Object])
413 ], InMemoryBackendService);
414 return InMemoryBackendService;
415}());
416exports.InMemoryBackendService = InMemoryBackendService;
417//# sourceMappingURL=in-memory-backend.service.js.map
\No newline at end of file