From e406099e1872a7917075533627074d8a7d147c43 Mon Sep 17 00:00:00 2001 From: Jan Molak Date: Fri, 3 Jul 2020 15:42:26 +0100 Subject: [PATCH] fix(playground): added a REST API and a JSON DB to store todos in --- angular.json | 5 +- e2e/protractor.conf.js | 4 +- e2e/src/playground.spec.ts | 40 +- e2e/src/screenplay/Actors.ts | 12 +- e2e/src/screenplay/playground/Start.ts | 11 +- e2e/tsconfig.json | 3 +- karma.conf.js | 56 +- package-lock.json | 776 +++++++++++++++++- package.json | 233 +++--- src/api/api.ts | 114 ++- src/cli/commands/start.ts | 24 +- src/index.ts | 7 +- src/start.ts | 6 +- src/webapp/app/app.component.spec.ts | 3 +- src/webapp/app/app.module.ts | 7 +- .../todo/storage/in-memory-storage.service.ts | 39 +- src/webapp/app/todo/storage/index.ts | 1 + .../app/todo/storage/local-storage.service.ts | 44 +- .../app/todo/storage/rest-storage.service.ts | 69 ++ .../app/todo/storage/storage.service.ts | 19 +- src/webapp/app/todo/todo.component.ts | 75 +- src/webapp/environments/environment.prod.ts | 3 +- src/webapp/environments/environment.ts | 4 +- tsconfig.webapp.spec.json | 2 +- 24 files changed, 1247 insertions(+), 310 deletions(-) create mode 100644 src/webapp/app/todo/storage/rest-storage.service.ts diff --git a/angular.json b/angular.json index 44be4ac..2892792 100644 --- a/angular.json +++ b/angular.json @@ -26,7 +26,10 @@ "styles": [ "src/webapp/styles.css" ], - "scripts": [] + "scripts": [], + "allowedCommonJsDependencies": [ + "tiny-types" + ] }, "configurations": { "production": { diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js index d98501f..ea44839 100644 --- a/e2e/protractor.conf.js +++ b/e2e/protractor.conf.js @@ -29,11 +29,11 @@ exports.config = { ], serenity: { - runner: 'jasmine', + runner: 'mocha', crew: [ ArtifactArchiver.storingArtifactsAt('./target/site/serenity'), ConsoleReporter.forDarkTerminals(), - Photographer.whoWill(TakePhotosOfInteractions), // or Photographer.whoWill(TakePhotosOfInteractions), + Photographer.whoWill(TakePhotosOfFailures), // or Photographer.whoWill(TakePhotosOfInteractions), new SerenityBDDReporter(), ] }, diff --git a/e2e/src/playground.spec.ts b/e2e/src/playground.spec.ts index 4980c96..8d4a58c 100644 --- a/e2e/src/playground.spec.ts +++ b/e2e/src/playground.spec.ts @@ -3,14 +3,13 @@ import { containAtLeastOneItemThat, Ensure, equals, - isGreaterThan, not, property, } from '@serenity-js/assertions'; import { actorCalled, actorInTheSpotlight, engage, Log, Note, TakeNote } from '@serenity-js/core'; import { LocalServer, StartLocalServer, StopLocalServer } from '@serenity-js/local-server'; import { Browser } from '@serenity-js/protractor'; -import { logging } from 'protractor'; +import { logging, protractor } from 'protractor'; import { Actors, @@ -22,31 +21,34 @@ import { Start, ToggleItem, } from './screenplay'; -import { ChangeApiUrl, GetRequest, LastResponse, Send } from '@serenity-js/rest'; +import { GetRequest, LastResponse, Send } from '@serenity-js/rest'; -describe('Playground Todo App', () => { +describe('Playground Todo App', function() { - beforeAll(() => engage(new Actors())); + this.timeout(30000); - beforeAll(() => - actorCalled('Adam').attemptsTo( - StartLocalServer.onOneOfThePreferredPorts([3000]), - Log.the(LocalServer.url()), - Send.a(GetRequest.to(LocalServer.url())), - Ensure.that(LastResponse.status(), equals(200)), - TakeNote.of(LocalServer.url()), - )); - - afterAll(() => - actorCalled('Adam').attemptsTo( - StopLocalServer.ifRunning(), - )); + before(() => engage(new Actors(protractor.browser.baseUrl))); describe('actor', () => { + before(() => + actorCalled('Adam').attemptsTo( + StartLocalServer.onOneOfThePreferredPorts([3000]), + Log.the(LocalServer.url()), + Ensure.that(LocalServer.url(), equals('http://127.0.0.1:3000')), + + Send.a(GetRequest.to('/api/health')), + Ensure.that(LastResponse.status(), equals(200)), + TakeNote.of(LocalServer.url()), + )); + + after(() => + actorCalled('Adam').attemptsTo( + StopLocalServer.ifRunning(), + )); + it('can record new items', () => actorCalled('Jasmine').attemptsTo( - ChangeApiUrl.to(Note.of(LocalServer.url())), Start.withAnEmptyList(), RecordItem.called('Walk a dog'), Ensure.that(RecordedItems(), contain('Walk a dog')), diff --git a/e2e/src/screenplay/Actors.ts b/e2e/src/screenplay/Actors.ts index f5ade2b..ad31121 100644 --- a/e2e/src/screenplay/Actors.ts +++ b/e2e/src/screenplay/Actors.ts @@ -2,26 +2,32 @@ import { Actor, Cast, TakeNotes } from '@serenity-js/core'; import { ManageALocalServer } from '@serenity-js/local-server'; import { BrowseTheWeb } from '@serenity-js/protractor'; +import path = require('path'); import { protractor } from 'protractor'; import { server } from '../../../src'; import { CallAnApi } from '@serenity-js/rest'; export class Actors implements Cast { + constructor(private readonly baseUrl: string) { + } + prepare(actor: Actor): Actor { switch (actor.name) { case 'Adam': return actor.whoCan( TakeNotes.usingASharedNotepad(), BrowseTheWeb.using(protractor.browser), // todo: fixme, remove - ManageALocalServer.runningAHttpListener(server), // todo: `server` should be parametrised - CallAnApi.at('http://localhost'), + ManageALocalServer.runningAHttpListener(server( + path.join(__dirname, '../../../target/todos.json') + )), + CallAnApi.at(this.baseUrl), ); case 'Jasmine': default: return actor.whoCan( TakeNotes.usingASharedNotepad(), - CallAnApi.at(protractor.browser.baseUrl), + CallAnApi.at(this.baseUrl), BrowseTheWeb.using(protractor.browser), ); } diff --git a/e2e/src/screenplay/playground/Start.ts b/e2e/src/screenplay/playground/Start.ts index 3f3667a..f81b39f 100644 --- a/e2e/src/screenplay/playground/Start.ts +++ b/e2e/src/screenplay/playground/Start.ts @@ -1,13 +1,15 @@ import { Ensure, equals } from '@serenity-js/assertions'; -import { Task } from '@serenity-js/core'; +import { Note, Task } from '@serenity-js/core'; import { Navigate, Website } from '@serenity-js/protractor'; import { RecordItem } from './RecordItem'; -import { GetRequest, LastResponse, Send } from '@serenity-js/rest'; +import { ChangeApiUrl, DeleteRequest, GetRequest, LastResponse, Send } from '@serenity-js/rest'; +import { LocalServer } from '@serenity-js/local-server'; export class Start { static withAnEmptyList = () => Task.where(`#actor starts with an empty list`, CheckIfTheServerIsUp(), + ClearTheDatabase(), Navigate.to('/'), Ensure.that(Website.title(), equals('Serenity/JS Playground')), ); @@ -24,3 +26,8 @@ const CheckIfTheServerIsUp = () => Send.a(GetRequest.to('/api/health/')), Ensure.that(LastResponse.status(), equals(200)), ); + +const ClearTheDatabase = () => + Task.where(`#actor clears the database`, + Send.a(DeleteRequest.to('/api/todos')), + ); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index c25b761..8623603 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -5,8 +5,7 @@ "module": "commonjs", "target": "es5", "types": [ - "jasmine", - "jasminewd2", + "mocha", "node" ] } diff --git a/karma.conf.js b/karma.conf.js index 2c41d6a..831664d 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -2,31 +2,33 @@ // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, './coverage/playground'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true - }); + config.set({ + basePath: '', + frameworks: ['mocha', '@angular-devkit/build-angular'], + plugins: [ + require('karma-mocha'), + require('karma-chrome-launcher'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + mocha: { + reporter: 'html', + ui: 'bdd', + } + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, './coverage/playground'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); }; diff --git a/package-lock.json b/package-lock.json index dd74911..c2ddb8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2488,7 +2488,8 @@ "version": "2.11.1", "resolved": "https://registry.npmjs.org/@serenity-js/jasmine/-/jasmine-2.11.1.tgz", "integrity": "sha512-vIisq8cFjp8js9PR6oq+dJCNV+t3+G/pYNmdEfXnraT634CqmatNFNSCQXXabYBTlKiN3KxMb6KddK6kO3+stw==", - "dev": true + "dev": true, + "optional": true }, "@serenity-js/local-server": { "version": "2.11.1", @@ -2504,8 +2505,7 @@ "version": "2.11.1", "resolved": "https://registry.npmjs.org/@serenity-js/mocha/-/mocha-2.11.1.tgz", "integrity": "sha512-Cf8yLMHaeke3cDNWXXL4PrleXz4FxNBLbOJK7JvlzTbW5xThiUWQCJkBpg4VnXgKIR0T29zOdRrV/c6UKu4imw==", - "dev": true, - "optional": true + "dev": true }, "@serenity-js/protractor": { "version": "2.11.1", @@ -2639,6 +2639,12 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz", + "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -2693,12 +2699,6 @@ "@types/node": "*" } }, - "@types/jasmine": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.11.tgz", - "integrity": "sha512-fg1rOd/DehQTIJTifGqGVY6q92lDgnLfs7C6t1ccSwQrMyoTGSoH6wWzhJDZb6ezhsdwAX4EIBLe8w5fXWmEng==", - "dev": true - }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", @@ -2723,6 +2723,21 @@ "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", "dev": true }, + "@types/mocha": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", + "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "dev": true + }, + "@types/nanoid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/nanoid/-/nanoid-2.1.0.tgz", + "integrity": "sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "12.12.47", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.47.tgz", @@ -3309,6 +3324,18 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", @@ -3370,6 +3397,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -3831,6 +3864,12 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -4162,6 +4201,20 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4179,6 +4232,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "chokidar": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", @@ -5754,6 +5813,15 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -6447,6 +6515,35 @@ "string.prototype.trimstart": "^1.0.1" } }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -7203,6 +7300,23 @@ "resolve-dir": "^1.0.1" } }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + } + } + }, "flatted": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", @@ -7359,6 +7473,12 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -7509,7 +7629,12 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "handle-thing": { @@ -7706,6 +7831,12 @@ "minimalistic-assert": "^1.0.1" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -8381,6 +8512,12 @@ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8432,6 +8569,11 @@ "isobject": "^3.0.1" } }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "is-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", @@ -8447,12 +8589,24 @@ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-svg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", @@ -8682,22 +8836,22 @@ "istanbul-lib-report": "^3.0.0" } }, - "jasmine": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", - "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", + "dev": true + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", "dev": true, "requires": { - "glob": "^7.1.4", - "jasmine-core": "~3.5.0" + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" } }, - "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", - "dev": true - }, "jasminewd2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", @@ -8920,21 +9074,15 @@ "minimatch": "^3.0.4" } }, - "karma-jasmine": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.3.1.tgz", - "integrity": "sha512-Nxh7eX9mOQMyK0VSsMxdod+bcqrR/ikrmEiWj5M6fwuQ7oI+YEF1FckaDsWfs6TIpULm9f0fTKMjF7XcrvWyqQ==", + "karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", "dev": true, "requires": { - "jasmine-core": "^3.5.0" + "minimist": "^1.2.3" } }, - "karma-jasmine-html-reporter": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.4.tgz", - "integrity": "sha512-PtilRLno5O6wH3lDihRnz0Ba8oSn0YUJqKjjux1peoYGwo0AQqrWRbdWk/RLzcGlb+onTyXAnHl6M+Hu3UxG/Q==", - "dev": true - }, "karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -9091,8 +9239,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.capitalize": { "version": "4.2.1", @@ -9209,6 +9356,25 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "requires": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9872,20 +10038,304 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.0.1.tgz", + "integrity": "sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.3.1", + "debug": "3.2.6", + "diff": "4.0.2", + "escape-string-regexp": "1.0.5", + "find-up": "4.1.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "object.assign": "4.1.0", + "promise.allsettled": "1.0.2", + "serialize-javascript": "3.0.0", + "strip-json-comments": "3.0.1", + "supports-color": "7.1.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.0", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "chokidar": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", + "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.3.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "readdirp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", + "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.7" + } + }, + "serialize-javascript": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", + "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, "modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -9975,6 +10425,11 @@ "dev": true, "optional": true }, + "nanoid": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.10.tgz", + "integrity": "sha512-iZFMXKeXWkxzlfmMfM91gw7YhN2sdJtixY+eZh9V6QWJWTOiurhpKhBMgr82pfzgSqglQgqYSCowEYsz8D++6w==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -13709,6 +14164,12 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "npm-failsafe": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/npm-failsafe/-/npm-failsafe-0.4.1.tgz", + "integrity": "sha512-/Q7pgrupOjgVW/+ECl22VPHAUpZrzLrLHoWLBO3EjToh05u0qQOEF2hzDkF9IXh7MhMXB3y19xXfb4LZww97hg==", + "dev": true + }, "npm-install-checks": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", @@ -14558,6 +15019,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "pbkdf2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", @@ -15393,6 +15860,19 @@ } } }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "dev": true, + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + } + }, "protoduck": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", @@ -17666,6 +18146,14 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "requires": { + "graceful-fs": "^4.1.3" + } + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -18464,6 +18952,12 @@ "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", @@ -19773,6 +20267,48 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "windows-release": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.1.tgz", @@ -19840,6 +20376,12 @@ } } }, + "workerpool": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", + "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "dev": true + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -20019,6 +20561,150 @@ "decamelize": "^1.2.0" } }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/package.json b/package.json index 5321238..89346d5 100644 --- a/package.json +++ b/package.json @@ -1,114 +1,125 @@ { - "name": "@serenity-js/playground", - "description": "", - "version": "0.0.0-development", - "bin": { - "playground": "bin/playground" - }, - "author": { - "name": "Jan Molak", - "email": "jan.molak@smartcodeltd.co.uk", - "url": "https://janmolak.com" - }, - "funding": { - "url": "https://github.com/sponsors/serenity-js" - }, - "homepage": "https://serenity-js.org", - "license": "Apache-2.0", - "main": "lib/index.js", - "typings": "lib/index.d.ts", - "keywords": [ - "bdd", - "tdd", - "test", - "testing", - "serenity", - "screenplay", - "playground", - "learning" - ], - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/serenity-js/playground.git" - }, - "bugs": { - "url": "https://github.com/serenity-js/playground/issues" - }, - "engines": { - "node": ">= 10", - "npm": ">= 6" - }, - "scripts": { - "ng": "ng", - "start": "npm run start:api & npm run start:webapp", - "start:webapp": "ng serve", - "start:api": "ts-node src/start.ts", - "build": "npm run build:webapp && npm run build:server", - "build:webapp": "ng build", - "build:server": "tsc --project tsconfig.json", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e", - "commit": "git-cz", - "semantic-release": "semantic-release" - }, - "dependencies": { - "express": "^4.17.1", - "yargs": "^15.3.1" - }, - "devDependencies": { - "@angular/animations": "^10.0.2", - "@angular/common": "^10.0.2", - "@angular/compiler": "^10.0.2", - "@angular/core": "^10.0.2", - "@angular/forms": "^10.0.2", - "@angular/platform-browser": "^10.0.2", - "@angular/platform-browser-dynamic": "^10.0.2", - "@angular/router": "^10.0.2", - "@angular-devkit/build-angular": "^0.1000.1", - "@angular/cli": "^10.0.1", - "@angular/compiler-cli": "^10.0.2", - "@serenity-js/assertions": "^2.11.1", - "@serenity-js/core": "^2.11.1", - "@serenity-js/console-reporter": "^2.11.1", - "@serenity-js/local-server": "^2.11.1", - "@serenity-js/jasmine": "^2.11.1", - "@serenity-js/protractor": "^2.11.1", - "@serenity-js/serenity-bdd": "^2.11.1", - "@serenity-js/rest": "^2.11.1", - "@types/node": "^12.12.47", - "@types/jasmine": "^3.5.11", - "@types/express": "^4.17.6", - "chromedriver": "^83.0.0", - "codelyzer": "^5.2.2", - "commitizen": "^4.1.2", - "cz-conventional-changelog": "^3.2.0", - "cross-env": "^7.0.2", - "is-ci": "^2.0.0", - "jasmine": "^3.5.0", - "karma": "^5.1.0", - "karma-chrome-launcher": "^3.1.0", - "karma-coverage-istanbul-reporter": "^3.0.3", - "karma-jasmine": "^3.3.1", - "karma-jasmine-html-reporter": "^1.5.4", - "protractor": "^7.0.0", - "rxjs": "^6.5.5", - "tiny-types": "^1.14.1", - "todomvc-app-css": "^2.3.0", - "todomvc-common": "^1.0.5", - "tslib": "^2.0.0", - "ts-node": "^8.10.2", - "tslint": "^6.1.2", - "typescript": "^3.9.6", - "zone.js": "^0.10.3", - "semantic-release": "^17.1.1" - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" + "name": "@serenity-js/playground", + "description": "", + "version": "0.0.0-development", + "bin": { + "playground": "bin/playground" + }, + "author": { + "name": "Jan Molak", + "email": "jan.molak@smartcodeltd.co.uk", + "url": "https://janmolak.com" + }, + "funding": { + "url": "https://github.com/sponsors/serenity-js" + }, + "homepage": "https://serenity-js.org", + "license": "Apache-2.0", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "keywords": [ + "bdd", + "tdd", + "test", + "testing", + "serenity", + "screenplay", + "playground", + "learning" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/serenity-js/playground.git" + }, + "bugs": { + "url": "https://github.com/serenity-js/playground/issues" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6" + }, + "scripts": { + "clean": "rimraf lib target", + "ng": "ng", + "start": "npm run start:api & npm run start:webapp", + "start:webapp": "ng serve", + "start:api": "ts-node src/start.ts", + "build": "npm run build:webapp && npm run build:server", + "build:webapp": "ng build", + "build:server": "tsc --project tsconfig.json", + "test": "ng test", + "lint": "ng lint", + "pree2e": "serenity-bdd update --ignoreSSL", + "e2e": "failsafe clean build e2e:execute e2e:report", + "e2e:execute": "ng e2e", + "e2e:report": "serenity-bdd run", + "commit": "git-cz", + "semantic-release": "semantic-release" + }, + "dependencies": { + "body-parser": "^1.19.0", + "express": "^4.17.1", + "lowdb": "^1.0.0", + "nanoid": "^3.1.10", + "yargs": "^15.3.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^0.1000.1", + "@angular/animations": "^10.0.2", + "@angular/cli": "^10.0.1", + "@angular/common": "^10.0.2", + "@angular/compiler": "^10.0.2", + "@angular/compiler-cli": "^10.0.2", + "@angular/core": "^10.0.2", + "@angular/forms": "^10.0.2", + "@angular/platform-browser": "^10.0.2", + "@angular/platform-browser-dynamic": "^10.0.2", + "@angular/router": "^10.0.2", + "@serenity-js/assertions": "^2.11.1", + "@serenity-js/console-reporter": "^2.11.1", + "@serenity-js/core": "^2.11.1", + "@serenity-js/local-server": "^2.11.1", + "@serenity-js/mocha": "^2.11.1", + "@serenity-js/protractor": "^2.11.1", + "@serenity-js/rest": "^2.11.1", + "@serenity-js/serenity-bdd": "^2.11.1", + "@types/chai": "^4.2.11", + "@types/express": "^4.17.6", + "@types/nanoid": "^2.1.0", + "@types/node": "^12.12.47", + "@types/mocha": "^7.0.2", + "chai": "^4.2.0", + "chromedriver": "^83.0.0", + "codelyzer": "^5.2.2", + "commitizen": "^4.1.2", + "cross-env": "^7.0.2", + "cz-conventional-changelog": "^3.2.0", + "npm-failsafe": "^0.4.1", + "is-ci": "^2.0.0", + "mocha": "^8.0.1", + "karma": "^5.1.0", + "karma-chrome-launcher": "^3.1.0", + "karma-coverage-istanbul-reporter": "^3.0.3", + "karma-mocha": "^2.0.1", + "protractor": "^7.0.0", + "rimraf": "^3.0.2", + "rxjs": "^6.5.5", + "semantic-release": "^17.1.1", + "tiny-types": "^1.14.1", + "todomvc-app-css": "^2.3.0", + "todomvc-common": "^1.0.5", + "ts-node": "^8.10.2", + "tslib": "^2.0.0", + "tslint": "^6.1.2", + "typescript": "^3.9.6", + "zone.js": "^0.10.3" + }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } } - } } diff --git a/src/api/api.ts b/src/api/api.ts index 1bd7c57..732a97e 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,21 +1,107 @@ +import bodyParser = require('body-parser'); import express = require('express'); +import fs = require('fs'); +import path = require('path'); +import FileSync = require('lowdb/adapters/FileSync'); +import { nanoid } from 'nanoid'; +import { Todo } from '../domain'; +const lowDB = require('lowdb'); function errorHandler(err, req, res, next) { console.error(err.stack); // tslint:disable-line:no-console next(err); } -export const api = express() - .use(errorHandler) - .get('/api/health', (req: express.Request, res: express.Response) => { - res.status(200).send({ - uptime: Math.floor(process.uptime()), - }); - }) - .get('/api/config', (req: express.Request, res: express.Response) => { - res.send({ - // storage: 'LocalStorageService', - storage: 'InMemoryStorageService', - }); - }) -; +export function api (pathToDbJson: string) { + + fs.mkdirSync(path.dirname(pathToDbJson), { recursive: true }); + + const db = lowDB(new FileSync(pathToDbJson)); + + db.defaults({ + todos: [], + }).write(); + + return express() + .use(errorHandler) + .use(bodyParser.json()) + .get('/api/health', (req: express.Request, res: express.Response) => { + res.status(200).send({ + uptime: Math.floor(process.uptime()), + }); + }) + .get('/api/config', (req: express.Request, res: express.Response) => { + res.send({ + storage: 'RestStorageService', + // storage: 'LocalStorageService', + // storage: 'InMemoryStorageService', + }); + }) + // get all items + .get('/api/todos', (req: express.Request, res: express.Response) => { + res.json(db.get('todos').value()); + }) + // create an item + .post('/api/todos', (req: express.Request, res: express.Response) => { + const serialised = Todo.fromJSON({ ...req.body, id: nanoid() }).toJSON(); + + db.get('todos') + .push(serialised) + .write(); + + res.json(serialised); + }) + // update an item + .put('/api/todos/:id', (req: express.Request, res: express.Response) => { + + const serialised = Todo.fromJSON(req.body).toJSON(); + + db.get('todos') + .find({ id: req.params.id }) + .assign(serialised) + .write(); + + res.json(serialised); + }) + // remove an item + .delete('/api/todos/:id', (req: express.Request, res: express.Response) => { + + db.get('todos') + .remove({ id: req.params.id }) + .write(); + + res.status(200).send(); + }) + // change status of all todos + .patch('/api/todos', (req: express.Request, res: express.Response) => { + + db.get('todos') + .each(item => { + item.completed = req.body.completed + }) + .write(); + + res.json(db.get('todos').value()); + }) + // remove all todos + .delete('/api/todos/', (req: express.Request, res: express.Response) => { + + function filter() { + switch (req.query.completed) { + case 'true': + return { completed: true }; + case 'false': + return { completed: false }; + default: + return undefined; + } + } + + db.get('todos') + .remove(filter()) + .write(); + + res.json(db.get('todos').value()); + }) + ; +} diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 0f5257d..e6bf2e6 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -1,3 +1,4 @@ +import path = require('path'); import { Argv } from '../Argv'; import { server } from '../../index'; @@ -9,12 +10,29 @@ export = { default: 3000, describe: 'The port to start the web server on', }, + db: { + default: path.join(process.cwd(), 'todos.json'), + describe: 'Location of JSON file where the server will store its data' + } }, handler: (argv: Argv) => new Promise((resolve, reject) => { - server.on('error', reject); - server.listen(parseInt(argv.port, 10), '127.0.0.1', () => { + const instance = server(argv.db); + + instance.on('error', reject); + instance.listen(parseInt(argv.port, 10), '127.0.0.1', () => { // tslint:disable-next-line:no-console - console.log(`Serenity/JS Playground started on http://localhost:${ argv.port }`) + console.log(` + Serenity/JS Playground started! + - User Interface - http://localhost:${ argv.port } + + API: + - Health check - GET http://localhost:${ argv.port }/api/health + - List items - GET http://localhost:${ argv.port }/api/todos + - Add an item - POST http://localhost:${ argv.port }/api/todos { title: string, completed: boolean } + - Update an item - PUT http://localhost:${ argv.port }/api/todos { title: string, completed: boolean } + - Remove an item - DELETE http://localhost:${ argv.port }/api/todos/:id + - Remove all - DELETE http://localhost:${ argv.port }/api/todos + `); }); }), }; diff --git a/src/index.ts b/src/index.ts index 7e4ec3c..392cdc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { api } from './api'; import express = require('express'); import { resolve } from 'path'; -api.use(express.static(resolve(__dirname, './webapp'))) - -export const server: express.Express = api; +export function server(pathToDbJson: string): express.Express { + return api(pathToDbJson) + .use(express.static(resolve(__dirname, './webapp'))); +} diff --git a/src/start.ts b/src/start.ts index 6058c2c..edb8ad6 100644 --- a/src/start.ts +++ b/src/start.ts @@ -1,5 +1,7 @@ +import path = require('path'); import { api } from './api'; -const port = 30000; +const port = 3000; // tslint:disable-next-line:no-console -api.listen(port, () => console.log(`Serenity/JS Playground API started on http://localhost:${port}`)); +api(path.join(__dirname, '../target/todos.json')) + .listen(port, () => console.log(`Serenity/JS Playground API started on http://localhost:${port}`)); diff --git a/src/webapp/app/app.component.spec.ts b/src/webapp/app/app.component.spec.ts index d345d31..accad1d 100644 --- a/src/webapp/app/app.component.spec.ts +++ b/src/webapp/app/app.component.spec.ts @@ -1,5 +1,6 @@ import { TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { expect } from 'chai'; import { AppComponent } from './app.component'; describe('AppComponent', () => { @@ -17,6 +18,6 @@ describe('AppComponent', () => { it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app).toBeTruthy(); + expect(app).to.be.ok; }); }); diff --git a/src/webapp/app/app.module.ts b/src/webapp/app/app.module.ts index 15a66e6..b914675 100644 --- a/src/webapp/app/app.module.ts +++ b/src/webapp/app/app.module.ts @@ -7,7 +7,7 @@ import { TodoComponent } from './todo/todo.component'; import { AppComponent } from './app.component'; import { ConfigService } from './todo/config.service'; import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { InMemoryStorageService, LocalStorageService, StorageService } from './todo/storage'; +import { InMemoryStorageService, LocalStorageService, RestStorageService, StorageService } from './todo/storage'; const routes: Routes = [ { path: '', component: TodoComponent, pathMatch: 'full' }, @@ -18,8 +18,6 @@ const appInitializerFn = (config: ConfigService) => () => config.load(); export function storageServiceFactory(config: ConfigService, http: HttpClient) { - console.log({http}); - switch (config.get('storage')) { case 'LocalStorageService': return new LocalStorageService(); @@ -27,6 +25,9 @@ export function storageServiceFactory(config: ConfigService, http: HttpClient) { case 'InMemoryStorageService': return new InMemoryStorageService(); + case 'RestStorageService': + return new RestStorageService(http); + default: throw new Error(`"${ config.get('storage') }" is not defined. Maybe a configuration error?`); } diff --git a/src/webapp/app/todo/storage/in-memory-storage.service.ts b/src/webapp/app/todo/storage/in-memory-storage.service.ts index cef5335..5266b9d 100644 --- a/src/webapp/app/todo/storage/in-memory-storage.service.ts +++ b/src/webapp/app/todo/storage/in-memory-storage.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; import { Todo } from '../../../../domain'; import { StorageService } from './storage.service'; @@ -13,7 +14,7 @@ export class InMemoryStorageService extends StorageService { super(); } - create(todo: string): Todo { + create(todo: string): Observable { todo = todo.trim(); if (todo.length === 0) { return; @@ -22,45 +23,43 @@ export class InMemoryStorageService extends StorageService { const newTodo = new Todo(++this.lastInsertId, todo); this.todos.push(newTodo); - return newTodo; + return of(newTodo); } - findAll(): Todo[] { - return this.todos; + findAll(): Observable { + return of(this.todos); } - update(todo: Todo): void { + update(todo: Todo): Observable { todo.title = todo.title.trim(); if (todo.title.length === 0) { this.delete(todo); } + + return of(todo); } - delete(todo: Todo): void { + delete(todo: Todo): Observable { this.todos = this.todos.filter((t) => t !== todo); + + return of(null); } - toggle(todo: Todo): void { + toggle(todo: Todo): Observable { todo.completed = ! todo.completed; + + return of(todo); } - toggleAll(completed: boolean): void { + toggleAll(completed: boolean): Observable { this.todos.forEach((t) => t.completed = completed); - } - clearCompleted(): void { - this.todos = this.todos.filter((t) => !t.completed); + return of(this.todos); } - remaining(): number { - return this.todos - .filter(t => !t.completed) - .length; - } + clearCompleted(): Observable { + this.todos = this.todos.filter((t) => !t.completed); - completed(): number { - return this.todos - .filter(t => t.completed) - .length; + return of(this.todos); } } diff --git a/src/webapp/app/todo/storage/index.ts b/src/webapp/app/todo/storage/index.ts index c591bfd..5ce53d1 100644 --- a/src/webapp/app/todo/storage/index.ts +++ b/src/webapp/app/todo/storage/index.ts @@ -1,3 +1,4 @@ export * from './in-memory-storage.service'; export * from './local-storage.service'; +export * from './rest-storage.service'; export * from './storage.service'; diff --git a/src/webapp/app/todo/storage/local-storage.service.ts b/src/webapp/app/todo/storage/local-storage.service.ts index 8c4eeb5..952ed24 100644 --- a/src/webapp/app/todo/storage/local-storage.service.ts +++ b/src/webapp/app/todo/storage/local-storage.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Todo } from '../../../../domain'; import { StorageService } from './storage.service'; +import { Observable, of } from 'rxjs'; @Injectable() export class LocalStorageService extends StorageService { @@ -20,7 +21,7 @@ export class LocalStorageService extends StorageService { } } - create(todo: string): Todo { + create(todo: string): Observable { todo = todo.trim(); if (todo.length === 0) { return; @@ -30,51 +31,54 @@ export class LocalStorageService extends StorageService { this.todos.push(newTodo); this.save(); - return newTodo; + return of(newTodo); } - findAll(): Todo[] { - return this.todos; + findAll(): Observable { + return of(this.todos); } - update(todo: Todo): void { + update(todo: Todo): Observable { todo.title = todo.title.trim(); if (todo.title.length === 0) { this.delete(todo); } + this.save(); + + return of(todo); } - delete(todo: Todo): void { + delete(todo: Todo): Observable { this.todos = this.todos.filter((t) => t !== todo); + this.save(); + + return of(null); } - toggle(todo: Todo): void { + toggle(todo: Todo): Observable { todo.completed = ! todo.completed; + this.save(); + + return of(todo); } - toggleAll(completed: boolean): void { + toggleAll(completed: boolean): Observable { this.todos.forEach((t) => t.completed = completed); + this.save(); + + return of(this.todos); } - clearCompleted(): void { + clearCompleted(): Observable { this.todos = this.todos.filter((t) => !t.completed); - this.save(); - } - remaining(): number { - return this.todos - .filter(t => !t.completed) - .length; - } + this.save(); - completed(): number { - return this.todos - .filter(t => t.completed) - .length; + return of(this.todos); } private loadTodos(): Todo[] { diff --git a/src/webapp/app/todo/storage/rest-storage.service.ts b/src/webapp/app/todo/storage/rest-storage.service.ts new file mode 100644 index 0000000..232a6a1 --- /dev/null +++ b/src/webapp/app/todo/storage/rest-storage.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; + +import { environment } from '../../../environments/environment'; +import { Todo } from '../../../../domain'; +import { StorageService } from './storage.service'; +import { map } from 'rxjs/operators'; +import { Serialised } from 'tiny-types'; + +@Injectable() +export class RestStorageService extends StorageService { + + constructor(private readonly http: HttpClient) { + super(); + } + + create(todo: string): Observable { + todo = todo.trim(); + if (todo.length === 0) { + return; + } + + const newTodo = new Todo(undefined, todo); + + return this.http.post(`${ environment.apiUrl }/todos`, newTodo.toJSON()) + .pipe(map(Todo.fromJSON)); + } + + findAll(): Observable { + return (this.http.get(`${ environment.apiUrl }/todos`) as Observable>>) + .pipe(map(items => items.map(Todo.fromJSON))); + } + + update(todo: Todo): Observable { + todo.title = todo.title.trim(); + + if (todo.title.length === 0) { + return this.delete(todo); + } + + return this.http.put(`${ environment.apiUrl }/todos/${ todo.id }`, todo.toJSON()) + .pipe(map(Todo.fromJSON)); + } + + delete(todo: Todo): Observable { + return this.http.delete(`${ environment.apiUrl }/todos/${ todo.id }`) + .pipe(map(() => todo)); + } + + toggle(todo: Todo): Observable { + todo.completed = ! todo.completed; + + return this.http.put(`${ environment.apiUrl }/todos/${ todo.id }`, todo) + .pipe(map(Todo.fromJSON)); + } + + toggleAll(completed: boolean): Observable { + return (this.http.patch(`${ environment.apiUrl }/todos`, { completed }) as Observable>>) + .pipe(map(items => items.map(Todo.fromJSON))); + } + + clearCompleted(): Observable { + return (this.http.delete(`${ environment.apiUrl }/todos`, { + params: { completed: 'true' } + }) as unknown as Observable>>) + .pipe(map(items => items.map(Todo.fromJSON))); + } +} diff --git a/src/webapp/app/todo/storage/storage.service.ts b/src/webapp/app/todo/storage/storage.service.ts index a0df54e..e1f5e44 100644 --- a/src/webapp/app/todo/storage/storage.service.ts +++ b/src/webapp/app/todo/storage/storage.service.ts @@ -1,25 +1,22 @@ import { Injectable } from '@angular/core'; import { Todo } from '../../../../domain'; +import { Observable } from 'rxjs'; @Injectable() export abstract class StorageService { - abstract create(todo: string): Todo; + abstract create(todo: string): Observable; - abstract findAll(): Todo[]; + abstract findAll(): Observable; - abstract update(todo: Todo): void; + abstract update(todo: Todo): Observable; - abstract delete(todo: Todo): void; + abstract delete(todo: Todo): Observable; - abstract toggle(todo: Todo): void; + abstract toggle(todo: Todo): Observable; - abstract toggleAll(completed: boolean): void; + abstract toggleAll(completed: boolean): Observable; - abstract clearCompleted(): void; - - abstract remaining(): number; - - abstract completed(): number; + abstract clearCompleted(): Observable; } diff --git a/src/webapp/app/todo/todo.component.ts b/src/webapp/app/todo/todo.component.ts index eb0088f..fc2cdba 100644 --- a/src/webapp/app/todo/todo.component.ts +++ b/src/webapp/app/todo/todo.component.ts @@ -9,7 +9,7 @@ import { StorageService } from './storage/storage.service'; @Component({ selector: 'app-todo', templateUrl: './todo.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, + // changeDetection: ChangeDetectionStrategy.OnPush, }) export class TodoComponent implements OnInit, DoCheck, OnDestroy { @@ -20,11 +20,7 @@ export class TodoComponent implements OnInit, DoCheck, OnDestroy { snapshot: Todo; filter = Filter.default(); - todos: Todo[]; - filteredTodos: Todo[]; - completed: number; - remaining: number; - allCompleted: boolean; + todos: Todo[] = []; constructor( private storageService: StorageService, @@ -32,20 +28,48 @@ export class TodoComponent implements OnInit, DoCheck, OnDestroy { ) { } + get filteredTodos(): Todo[] { + return this.todos.filter(todo => this.filter.allows(todo)) + } + + get remaining(): number { + return this.todos + .filter(t => ! t.completed) + .length; + } + + get completed(): number { + return this.todos + .filter(t => t.completed) + .length; + } + + get allCompleted(): boolean { + return this.todos.length === this.completed; + } + // ~ lifecycle ngOnInit() { this.routeSubscription = this.route.params.subscribe(params => { this.filter = Filter.fromString(params['filter']); }); + + this.storageService.findAll() + .subscribe(todos => { + this.todos = todos; + }); } ngDoCheck() { - this.todos = this.storageService.findAll(); - this.filteredTodos = this.todos.filter(todo => this.filter.allows(todo)); - this.remaining = this.completed = 0; - this.todos.forEach(t => t.completed ? this.completed++ : this.remaining++); - this.allCompleted = this.todos.length === this.completed; + // this.storageService.findAll() + // .subscribe(todos => { + // this.todos = todos; + // this.filteredTodos = this.todos.filter(todo => this.filter.allows(todo)); + // this.remaining = this.completed = 0; + // this.todos.forEach(t => t.completed ? this.completed++ : this.remaining++); + // this.allCompleted = this.todos.length === this.completed; + // }); } ngOnDestroy(): void { @@ -55,14 +79,19 @@ export class TodoComponent implements OnInit, DoCheck, OnDestroy { // ~ crud create(todo: string) { + console.log('Component::create todo', todo) if (todo.trim().length === 0) { return; } - this.storageService.create(todo); - this.newTodo = ''; + this.storageService.create(todo).subscribe(newTodo => { + console.log('Component::create todo', newTodo) + this.todos = this.todos.concat(newTodo); + this.newTodo = ''; + }); } edit(todo: Todo) { + console.log('EDIT', todo); this.currentTodo = todo; this.snapshot = todo.clone(); } @@ -75,22 +104,32 @@ export class TodoComponent implements OnInit, DoCheck, OnDestroy { update(todo: Todo) { this.currentTodo = null; this.snapshot = null; - this.storageService.update(todo); + this.storageService.update(todo).subscribe(updatedTodo => { + this.todos = this.todos.map(item => item.id === updatedTodo.id ? updatedTodo : item); + }); } delete(todo: Todo) { - this.storageService.delete(todo); + this.storageService.delete(todo).subscribe(() => { + this.todos = this.todos.filter(item => item.id !== todo.id); + }); } toggle(todo: Todo) { - this.storageService.toggle(todo); + this.storageService.toggle(todo).subscribe(toggled => { + this.todos.map(item => item.id === toggled.id ? toggled : item) + }); } toggleAll(completed: boolean) { - this.storageService.toggleAll(completed); + this.storageService.toggleAll(completed).subscribe(todos => { + this.todos = todos; + }); } clearCompleted() { - this.storageService.clearCompleted(); + this.storageService.clearCompleted().subscribe(todos => { + this.todos = todos; + }); } } diff --git a/src/webapp/environments/environment.prod.ts b/src/webapp/environments/environment.prod.ts index 3612073..6bd55b4 100644 --- a/src/webapp/environments/environment.prod.ts +++ b/src/webapp/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + apiUrl: '/api', }; diff --git a/src/webapp/environments/environment.ts b/src/webapp/environments/environment.ts index 7b4f817..c33f742 100644 --- a/src/webapp/environments/environment.ts +++ b/src/webapp/environments/environment.ts @@ -3,7 +3,9 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + + apiUrl: '/api' }; /* diff --git a/tsconfig.webapp.spec.json b/tsconfig.webapp.spec.json index eb7767d..4cd4bf0 100644 --- a/tsconfig.webapp.spec.json +++ b/tsconfig.webapp.spec.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ - "jasmine", + "mocha", "node" ] },