mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-09 15:08:12 -05:00
docs(examples): basic CRUD application
See also: https://socket.io/get-started/basic-crud-application/
This commit is contained in:
19
examples/basic-crud-application/README.md
Normal file
19
examples/basic-crud-application/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Basic CRUD application with Socket.IO
|
||||
|
||||
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
|
||||
|
||||
## Running the frontend
|
||||
|
||||
```
|
||||
cd angular-client
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### Running the server
|
||||
|
||||
```
|
||||
cd server
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
16
examples/basic-crud-application/angular-client/.editorconfig
Normal file
16
examples/basic-crud-application/angular-client/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
46
examples/basic-crud-application/angular-client/.gitignore
vendored
Normal file
46
examples/basic-crud-application/angular-client/.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
31
examples/basic-crud-application/angular-client/README.md
Normal file
31
examples/basic-crud-application/angular-client/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Angular TodoMVC + Socket.IO
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.0.4.
|
||||
|
||||
Inspired from the [TodoMVC](http://todomvc.com/) [angular example](https://github.com/tastejs/todomvc/tree/master/examples/angular2).
|
||||
|
||||

|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
128
examples/basic-crud-application/angular-client/angular.json
Normal file
128
examples/basic-crud-application/angular-client/angular.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-todomvc": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-todomvc",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular-todomvc:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular-todomvc:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "angular-todomvc:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "angular-todomvc:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular-todomvc"
|
||||
}
|
||||
BIN
examples/basic-crud-application/angular-client/assets/demo.gif
Normal file
BIN
examples/basic-crud-application/angular-client/assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
SELENIUM_PROMISE_MANAGER: false,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', async () => {
|
||||
await page.navigateTo();
|
||||
expect(await page.getTitleText()).toEqual('angular-todomvc app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
async navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl);
|
||||
}
|
||||
|
||||
async getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
44
examples/basic-crud-application/angular-client/karma.conf.js
Normal file
44
examples/basic-crud-application/angular-client/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// 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'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/angular-todomvc'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
46
examples/basic-crud-application/angular-client/package.json
Normal file
46
examples/basic-crud-application/angular-client/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "angular-todomvc",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.0.4",
|
||||
"@angular/common": "~11.0.4",
|
||||
"@angular/compiler": "~11.0.4",
|
||||
"@angular/core": "~11.0.4",
|
||||
"@angular/forms": "~11.0.4",
|
||||
"@angular/platform-browser": "~11.0.4",
|
||||
"@angular/platform-browser-dynamic": "~11.0.4",
|
||||
"@angular/router": "~11.0.4",
|
||||
"rxjs": "~6.6.0",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1100.4",
|
||||
"@angular/cli": "~11.0.4",
|
||||
"@angular/compiler-cli": "~11.0.4",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.1.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keyup.enter)="addTodo()">
|
||||
</header>
|
||||
<section class="main" *ngIf="todoStore.todos.length > 0">
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox" *ngIf="todoStore.todos.length" #toggleall [checked]="todoStore.allCompleted()" (click)="todoStore.setAllTo(toggleall.checked)">
|
||||
<ul class="todo-list">
|
||||
<li *ngFor="let todo of todoStore.todos" [class.completed]="todo.completed" [class.editing]="todo.editing">
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" (click)="toggleCompletion(todo)" [checked]="todo.completed">
|
||||
<label (dblclick)="editTodo(todo)">{{todo.title}}</label>
|
||||
<button class="destroy" (click)="remove(todo)"></button>
|
||||
</div>
|
||||
<input class="edit" *ngIf="todo.editing" [value]="todo.title" #editedtodo (blur)="stopEditing(todo, editedtodo.value)" (keyup.enter)="updateEditingTodo(todo, editedtodo.value)" (keyup.escape)="cancelEditingTodo(todo)">
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" *ngIf="todoStore.todos.length > 0">
|
||||
<span class="todo-count"><strong>{{todoStore.getRemaining().length}}</strong> {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left</span>
|
||||
<button class="clear-completed" *ngIf="todoStore.getCompleted().length > 0" (click)="removeCompleted()">Clear completed</button>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'angular-todomvc'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('angular-todomvc');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('angular-todomvc app is running!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TodoStore, Todo } from './store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
todoStore: TodoStore;
|
||||
newTodoText = '';
|
||||
|
||||
constructor(todoStore: TodoStore) {
|
||||
this.todoStore = todoStore;
|
||||
}
|
||||
|
||||
stopEditing(todo: Todo, editedTitle: string) {
|
||||
todo.title = editedTitle;
|
||||
todo.editing = false;
|
||||
}
|
||||
|
||||
cancelEditingTodo(todo: Todo) {
|
||||
todo.editing = false;
|
||||
}
|
||||
|
||||
updateEditingTodo(todo: Todo, editedTitle: string) {
|
||||
editedTitle = editedTitle.trim();
|
||||
todo.editing = false;
|
||||
|
||||
if (editedTitle.length === 0) {
|
||||
return this.todoStore.remove(todo);
|
||||
}
|
||||
|
||||
todo.title = editedTitle;
|
||||
}
|
||||
|
||||
editTodo(todo: Todo) {
|
||||
todo.editing = true;
|
||||
}
|
||||
|
||||
removeCompleted() {
|
||||
this.todoStore.removeCompleted();
|
||||
}
|
||||
|
||||
toggleCompletion(todo: Todo) {
|
||||
this.todoStore.toggleCompletion(todo);
|
||||
}
|
||||
|
||||
remove(todo: Todo){
|
||||
this.todoStore.remove(todo);
|
||||
}
|
||||
|
||||
addTodo() {
|
||||
if (this.newTodoText.trim().length) {
|
||||
this.todoStore.add(this.newTodoText);
|
||||
this.newTodoText = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TodoStore } from './store';
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [TodoStore],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
140
examples/basic-crud-application/angular-client/src/app/store.ts
Normal file
140
examples/basic-crud-application/angular-client/src/app/store.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { ClientEvents, ServerEvents } from "../../../server/lib/events";
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export interface Todo {
|
||||
id: string,
|
||||
title: string,
|
||||
completed: boolean,
|
||||
editing: boolean,
|
||||
synced: boolean
|
||||
}
|
||||
|
||||
const mapTodo = (todo: any) => {
|
||||
return {
|
||||
...todo,
|
||||
editing: false,
|
||||
synced: true
|
||||
}
|
||||
}
|
||||
|
||||
export class TodoStore {
|
||||
public todos: Array<Todo> = [];
|
||||
private socket: Socket<ServerEvents, ClientEvents>;
|
||||
|
||||
constructor() {
|
||||
this.socket = io(environment.serverUrl);
|
||||
|
||||
this.socket.on("connect", () => {
|
||||
this.socket.emit("todo:list", (res) => {
|
||||
if ("error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
this.todos = res.data.map(mapTodo);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on("todo:created", (todo) => {
|
||||
this.todos.push(mapTodo(todo));
|
||||
});
|
||||
|
||||
this.socket.on("todo:updated", (todo) => {
|
||||
const existingTodo = this.todos.find(t => {
|
||||
return t.id === todo.id
|
||||
});
|
||||
if (existingTodo) {
|
||||
existingTodo.title = todo.title;
|
||||
existingTodo.completed = todo.completed;
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("todo:deleted", (id) => {
|
||||
const index = this.todos.findIndex(t => {
|
||||
return t.id === id
|
||||
});
|
||||
if (index !== -1) {
|
||||
this.todos.splice(index, 1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getWithCompleted(completed: boolean) {
|
||||
return this.todos.filter((todo: Todo) => todo.completed === completed);
|
||||
}
|
||||
|
||||
allCompleted() {
|
||||
return this.todos.length === this.getCompleted().length;
|
||||
}
|
||||
|
||||
setAllTo(completed: boolean) {
|
||||
this.todos.forEach(todo => {
|
||||
todo.completed = completed;
|
||||
todo.synced = false;
|
||||
this.socket.emit("todo:update", todo, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
todo.synced = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeCompleted() {
|
||||
this.getCompleted().forEach((todo) => {
|
||||
this.socket.emit("todo:delete", todo.id, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
}
|
||||
});
|
||||
})
|
||||
this.todos = this.getRemaining();
|
||||
}
|
||||
|
||||
getRemaining() {
|
||||
return this.getWithCompleted(false);
|
||||
}
|
||||
|
||||
getCompleted() {
|
||||
return this.getWithCompleted(true);
|
||||
}
|
||||
|
||||
toggleCompletion(todo: Todo) {
|
||||
todo.completed = !todo.completed;
|
||||
todo.synced = false;
|
||||
this.socket.emit("todo:update", todo, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
todo.synced = true;
|
||||
})
|
||||
}
|
||||
|
||||
remove(todo: Todo) {
|
||||
this.todos.splice(this.todos.indexOf(todo), 1);
|
||||
this.socket.emit("todo:delete", todo.id, (res) => {
|
||||
if (res && "error" in res) {
|
||||
// handle the error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add(title: string) {
|
||||
this.socket.emit("todo:create", { title, completed: false }, (res) => {
|
||||
if ("error" in res) {
|
||||
// handle the error
|
||||
return;
|
||||
}
|
||||
this.todos.push({
|
||||
id: res.data,
|
||||
title,
|
||||
completed: false,
|
||||
editing: false,
|
||||
synced: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
serverUrl: "https://my-custom-domain.com"
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
serverUrl: "http://localhost:3000"
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
BIN
examples/basic-crud-application/angular-client/src/favicon.ico
Normal file
BIN
examples/basic-crud-application/angular-client/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Todo MVC</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
12
examples/basic-crud-application/angular-client/src/main.ts
Normal file
12
examples/basic-crud-application/angular-client/src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
381
examples/basic-crud-application/angular-client/src/styles.css
Normal file
381
examples/basic-crud-application/angular-client/src/styles.css
Normal file
@@ -0,0 +1,381 @@
|
||||
/* imported from node_modules/todomvc-app-css/index.css */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #111111;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -140px;
|
||||
width: 100%;
|
||||
font-size: 80px;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
color: #b83f45;
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
font-weight: 400;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #cdcdcd;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #4d4d4d;
|
||||
font-size: 11px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
25
examples/basic-crud-application/angular-client/src/test.ts
Normal file
25
examples/basic-crud-application/angular-client/src/test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
keys(): string[];
|
||||
<T>(id: string): T;
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
@@ -0,0 +1,15 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
29
examples/basic-crud-application/angular-client/tsconfig.json
Normal file
29
examples/basic-crud-application/angular-client/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
152
examples/basic-crud-application/angular-client/tslint.json
Normal file
152
examples/basic-crud-application/angular-client/tslint.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"align": {
|
||||
"options": [
|
||||
"parameters",
|
||||
"statements"
|
||||
]
|
||||
},
|
||||
"array-type": false,
|
||||
"arrow-return-shorthand": true,
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"eofline": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": {
|
||||
"options": [
|
||||
"spaces"
|
||||
]
|
||||
},
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"semicolon": {
|
||||
"options": [
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"space-before-function-paren": {
|
||||
"options": {
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always",
|
||||
"constructor": "never",
|
||||
"method": "never",
|
||||
"named": "never"
|
||||
}
|
||||
},
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature"
|
||||
],
|
||||
"typedef-whitespace": {
|
||||
"options": [
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
},
|
||||
"whitespace": {
|
||||
"options": [
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type",
|
||||
"check-typecast"
|
||||
]
|
||||
},
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
35
examples/basic-crud-application/server/lib/app.ts
Normal file
35
examples/basic-crud-application/server/lib/app.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Server as HttpServer } from "http";
|
||||
import { Server, ServerOptions } from "socket.io";
|
||||
import { ClientEvents, ServerEvents } from "./events";
|
||||
import { TodoRepository } from "./todo-management/todo.repository";
|
||||
import createTodoHandlers from "./todo-management/todo.handlers";
|
||||
|
||||
export interface Components {
|
||||
todoRepository: TodoRepository;
|
||||
}
|
||||
|
||||
export function createApplication(
|
||||
httpServer: HttpServer,
|
||||
components: Components,
|
||||
serverOptions: Partial<ServerOptions> = {}
|
||||
): Server<ClientEvents, ServerEvents> {
|
||||
const io = new Server<ClientEvents, ServerEvents>(httpServer, serverOptions);
|
||||
|
||||
const {
|
||||
createTodo,
|
||||
readTodo,
|
||||
updateTodo,
|
||||
deleteTodo,
|
||||
listTodo,
|
||||
} = createTodoHandlers(components);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("todo:create", createTodo);
|
||||
socket.on("todo:read", readTodo);
|
||||
socket.on("todo:update", updateTodo);
|
||||
socket.on("todo:delete", deleteTodo);
|
||||
socket.on("todo:list", listTodo);
|
||||
});
|
||||
|
||||
return io;
|
||||
}
|
||||
37
examples/basic-crud-application/server/lib/events.ts
Normal file
37
examples/basic-crud-application/server/lib/events.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Todo, TodoID } from "./todo-management/todo.repository";
|
||||
import { ValidationErrorItem } from "joi";
|
||||
|
||||
interface Error {
|
||||
error: string;
|
||||
errorDetails?: ValidationErrorItem[];
|
||||
}
|
||||
|
||||
interface Success<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type Response<T> = Error | Success<T>;
|
||||
|
||||
export interface ServerEvents {
|
||||
"todo:created": (todo: Todo) => void;
|
||||
"todo:updated": (todo: Todo) => void;
|
||||
"todo:deleted": (id: TodoID) => void;
|
||||
}
|
||||
|
||||
export interface ClientEvents {
|
||||
"todo:list": (callback: (res: Response<Todo[]>) => void) => void;
|
||||
|
||||
"todo:create": (
|
||||
payload: Omit<Todo, "id">,
|
||||
callback: (res: Response<TodoID>) => void
|
||||
) => void;
|
||||
|
||||
"todo:read": (id: TodoID, callback: (res: Response<Todo>) => void) => void;
|
||||
|
||||
"todo:update": (
|
||||
payload: Todo,
|
||||
callback: (res?: Response<void>) => void
|
||||
) => void;
|
||||
|
||||
"todo:delete": (id: TodoID, callback: (res?: Response<void>) => void) => void;
|
||||
}
|
||||
19
examples/basic-crud-application/server/lib/index.ts
Normal file
19
examples/basic-crud-application/server/lib/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createServer } from "http";
|
||||
import { createApplication } from "./app";
|
||||
import { InMemoryTodoRepository } from "./todo-management/todo.repository";
|
||||
|
||||
const httpServer = createServer();
|
||||
|
||||
createApplication(
|
||||
httpServer,
|
||||
{
|
||||
todoRepository: new InMemoryTodoRepository(),
|
||||
},
|
||||
{
|
||||
cors: {
|
||||
origin: ["http://localhost:4200"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
httpServer.listen(3000);
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { Components } from "../app";
|
||||
import Joi = require("joi");
|
||||
import { Todo, TodoID } from "./todo.repository";
|
||||
import { ClientEvents, Response, ServerEvents } from "../events";
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
const idSchema = Joi.string().guid({
|
||||
version: "uuidv4",
|
||||
});
|
||||
|
||||
const todoSchema = Joi.object({
|
||||
id: idSchema.alter({
|
||||
create: (schema) => schema.forbidden(),
|
||||
update: (schema) => schema.required(),
|
||||
}),
|
||||
title: Joi.string().max(256).required(),
|
||||
completed: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
export default function (components: Components) {
|
||||
const { todoRepository } = components;
|
||||
return {
|
||||
createTodo: async function (
|
||||
payload: Omit<Todo, "id">,
|
||||
callback: (res: Response<TodoID>) => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
const socket: Socket<ClientEvents, ServerEvents> = this;
|
||||
|
||||
// validate the payload
|
||||
const { error, value } = todoSchema.tailor("create").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
value.id = uuid();
|
||||
|
||||
// persist the entity
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
// acknowledge the creation
|
||||
callback({
|
||||
data: value.id,
|
||||
});
|
||||
|
||||
// notify the other users
|
||||
socket.broadcast.emit("todo:created", value);
|
||||
},
|
||||
|
||||
readTodo: async function (
|
||||
id: TodoID,
|
||||
callback: (res: Response<Todo>) => void
|
||||
) {
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const todo = await todoRepository.findById(id);
|
||||
callback({
|
||||
data: todo,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTodo: async function (
|
||||
payload: Todo,
|
||||
callback: (res?: Response<void>) => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
const socket: Socket<ClientEvents, ServerEvents> = this;
|
||||
|
||||
const { error, value } = todoSchema.tailor("update").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:updated", value);
|
||||
},
|
||||
|
||||
deleteTodo: async function (
|
||||
id: TodoID,
|
||||
callback: (res?: Response<void>) => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
const socket: Socket<ClientEvents, ServerEvents> = this;
|
||||
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.deleteById(id);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:deleted", id);
|
||||
},
|
||||
|
||||
listTodo: async function (callback: (res: Response<Todo[]>) => void) {
|
||||
try {
|
||||
callback({
|
||||
data: await todoRepository.findAll(),
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Errors } from "../util";
|
||||
|
||||
abstract class CrudRepository<T, ID> {
|
||||
abstract findAll(): Promise<T[]>;
|
||||
abstract findById(id: ID): Promise<T>;
|
||||
abstract save(entity: T): Promise<void>;
|
||||
abstract deleteById(id: ID): Promise<void>;
|
||||
}
|
||||
|
||||
export type TodoID = string;
|
||||
|
||||
export interface Todo {
|
||||
id: TodoID;
|
||||
completed: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export abstract class TodoRepository extends CrudRepository<Todo, TodoID> {}
|
||||
|
||||
export class InMemoryTodoRepository extends TodoRepository {
|
||||
private readonly todos: Map<TodoID, Todo> = new Map();
|
||||
|
||||
findAll(): Promise<Todo[]> {
|
||||
const entities = Array.from(this.todos.values());
|
||||
return Promise.resolve(entities);
|
||||
}
|
||||
|
||||
findById(id: TodoID): Promise<Todo> {
|
||||
if (this.todos.has(id)) {
|
||||
return Promise.resolve(this.todos.get(id)!);
|
||||
} else {
|
||||
return Promise.reject(Errors.ENTITY_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
save(entity: Todo): Promise<void> {
|
||||
this.todos.set(entity.id, entity);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
deleteById(id: TodoID): Promise<void> {
|
||||
const deleted = this.todos.delete(id);
|
||||
if (deleted) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(Errors.ENTITY_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
examples/basic-crud-application/server/lib/util.ts
Normal file
24
examples/basic-crud-application/server/lib/util.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ValidationErrorItem } from "joi";
|
||||
|
||||
export enum Errors {
|
||||
ENTITY_NOT_FOUND = "entity not found",
|
||||
INVALID_PAYLOAD = "invalid payload",
|
||||
}
|
||||
|
||||
const errorValues: string[] = Object.values(Errors);
|
||||
|
||||
export function sanitizeErrorMessage(message: string) {
|
||||
if (errorValues.includes(message)) {
|
||||
return message;
|
||||
} else {
|
||||
return "an unknown error has occurred";
|
||||
}
|
||||
}
|
||||
|
||||
export function mapErrorDetails(details: ValidationErrorItem[]) {
|
||||
return details.map((item) => ({
|
||||
message: item.message,
|
||||
path: item.path,
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
36
examples/basic-crud-application/server/package.json
Normal file
36
examples/basic-crud-application/server/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "basic-crud-server",
|
||||
"version": "0.0.1",
|
||||
"description": "Server for the Basic CRUD Socket.IO example",
|
||||
"main": "dist/lib/index.js",
|
||||
"scripts": {
|
||||
"start": "ts-node lib/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "nyc mocha --require ts-node/register test/**/*.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
},
|
||||
"homepage": "https://github.com/socketio/socket.io#readme",
|
||||
"dependencies": {
|
||||
"joi": "^17.4.0",
|
||||
"socket.io": "^4.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.16",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"chai": "^4.3.4",
|
||||
"mocha": "^8.3.2",
|
||||
"nyc": "^15.1.0",
|
||||
"socket.io-client": "^4.0.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { createApplication } from "../../lib/app";
|
||||
import { createServer, Server } from "http";
|
||||
import {
|
||||
InMemoryTodoRepository,
|
||||
TodoRepository,
|
||||
} from "../../lib/todo-management/todo.repository";
|
||||
import { AddressInfo } from "net";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { ClientEvents, ServerEvents } from "../../lib/events";
|
||||
import { expect } from "chai";
|
||||
|
||||
const createPartialDone = (count: number, done: () => void) => {
|
||||
let i = 0;
|
||||
return () => {
|
||||
if (++i === count) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe("todo management", () => {
|
||||
let httpServer: Server,
|
||||
socket: Socket<ServerEvents, ClientEvents>,
|
||||
otherSocket: Socket<ServerEvents, ClientEvents>,
|
||||
todoRepository: TodoRepository;
|
||||
|
||||
beforeEach((done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
|
||||
httpServer = createServer();
|
||||
todoRepository = new InMemoryTodoRepository();
|
||||
|
||||
createApplication(httpServer, {
|
||||
todoRepository,
|
||||
});
|
||||
|
||||
httpServer.listen(() => {
|
||||
const port = (httpServer.address() as AddressInfo).port;
|
||||
socket = io(`http://localhost:${port}`);
|
||||
socket.on("connect", partialDone);
|
||||
|
||||
otherSocket = io(`http://localhost:${port}`);
|
||||
otherSocket.on("connect", partialDone);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpServer.close();
|
||||
socket.disconnect();
|
||||
otherSocket.disconnect();
|
||||
});
|
||||
|
||||
describe("create todo", () => {
|
||||
it("should create a todo entity", (done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
|
||||
socket.emit(
|
||||
"todo:create",
|
||||
{
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
},
|
||||
async (res) => {
|
||||
if ("error" in res) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.data).to.be.a("string");
|
||||
|
||||
const storedEntity = await todoRepository.findById(res.data);
|
||||
expect(storedEntity).to.eql({
|
||||
id: res.data,
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
});
|
||||
|
||||
partialDone();
|
||||
}
|
||||
);
|
||||
|
||||
otherSocket.on("todo:created", (todo) => {
|
||||
expect(todo.id).to.be.a("string");
|
||||
expect(todo.title).to.eql("lorem ipsum");
|
||||
expect(todo.completed).to.eql(false);
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an invalid entity", (done) => {
|
||||
const incompleteTodo = {
|
||||
completed: "false",
|
||||
description: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
socket.emit("todo:create", incompleteTodo, (res) => {
|
||||
if (!("error" in res)) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.error).to.eql("invalid payload");
|
||||
expect(res.errorDetails).to.eql([
|
||||
{
|
||||
message: '"title" is required',
|
||||
path: ["title"],
|
||||
type: "any.required",
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
otherSocket.on("todo:created", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("read todo", () => {
|
||||
it("should return a todo entity", (done) => {
|
||||
todoRepository.save({
|
||||
id: "254dbf85-f5b9-4675-b913-acab5d600884",
|
||||
title: "lorem ipsum",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit(
|
||||
"todo:read",
|
||||
"254dbf85-f5b9-4675-b913-acab5d600884",
|
||||
(res) => {
|
||||
if ("error" in res) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.data.id).to.eql("254dbf85-f5b9-4675-b913-acab5d600884");
|
||||
expect(res.data.title).to.eql("lorem ipsum");
|
||||
expect(res.data.completed).to.eql(true);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail with an invalid ID", (done) => {
|
||||
socket.emit("todo:read", "123", (res) => {
|
||||
if ("error" in res) {
|
||||
expect(res.error).to.eql("entity not found");
|
||||
done();
|
||||
} else {
|
||||
done(new Error("should not happen"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an unknown entity", (done) => {
|
||||
socket.emit(
|
||||
"todo:read",
|
||||
"6edcf81e-7049-40e0-8497-9cdd52414f75",
|
||||
(res) => {
|
||||
if ("error" in res) {
|
||||
expect(res.error).to.eql("entity not found");
|
||||
done();
|
||||
} else {
|
||||
done(new Error("should not happen"));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update todo", () => {
|
||||
it("should update a todo entity", (done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
|
||||
todoRepository.save({
|
||||
id: "c7790b35-6bbb-45dd-8d67-a281ca407b43",
|
||||
title: "lorem ipsum",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit(
|
||||
"todo:update",
|
||||
{
|
||||
id: "c7790b35-6bbb-45dd-8d67-a281ca407b43",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
},
|
||||
async () => {
|
||||
const storedEntity = await todoRepository.findById(
|
||||
"c7790b35-6bbb-45dd-8d67-a281ca407b43"
|
||||
);
|
||||
expect(storedEntity).to.eql({
|
||||
id: "c7790b35-6bbb-45dd-8d67-a281ca407b43",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
});
|
||||
partialDone();
|
||||
}
|
||||
);
|
||||
|
||||
otherSocket.on("todo:updated", (todo) => {
|
||||
expect(todo.title).to.eql("dolor sit amet");
|
||||
expect(todo.completed).to.eql(true);
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an invalid entity", (done) => {
|
||||
const incompleteTodo = {
|
||||
id: "123",
|
||||
completed: "false",
|
||||
description: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
socket.emit("todo:update", incompleteTodo, (res) => {
|
||||
if (!(res && "error" in res)) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.error).to.eql("invalid payload");
|
||||
expect(res.errorDetails).to.eql([
|
||||
{
|
||||
message: '"id" must be a valid GUID',
|
||||
path: ["id"],
|
||||
type: "string.guid",
|
||||
},
|
||||
{
|
||||
message: '"title" is required',
|
||||
path: ["title"],
|
||||
type: "any.required",
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
otherSocket.on("todo:updated", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete todo", () => {
|
||||
it("should delete a todo entity", (done) => {
|
||||
const partialDone = createPartialDone(2, done);
|
||||
const id = "58960ab2-4e78-4ced-8079-134f12179d46";
|
||||
|
||||
todoRepository.save({
|
||||
id,
|
||||
title: "lorem ipsum",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit("todo:delete", id, async () => {
|
||||
try {
|
||||
await todoRepository.findById(id);
|
||||
} catch (e) {
|
||||
partialDone();
|
||||
}
|
||||
});
|
||||
|
||||
otherSocket.on("todo:deleted", (id) => {
|
||||
expect(id).to.eql("58960ab2-4e78-4ced-8079-134f12179d46");
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with an invalid ID", (done) => {
|
||||
socket.emit("todo:delete", "123", (res) => {
|
||||
if (!(res && "error" in res)) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.error).to.eql("entity not found");
|
||||
done();
|
||||
});
|
||||
|
||||
otherSocket.on("todo:deleted", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("list todo", () => {
|
||||
it("should return a list of entities", (done) => {
|
||||
todoRepository.save({
|
||||
id: "d445db6d-9d55-4ff2-88ae-bd1f81c299d2",
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
});
|
||||
|
||||
todoRepository.save({
|
||||
id: "5f56fb59-a887-4984-93bf-eb39b4170a35",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
});
|
||||
|
||||
socket.emit("todo:list", (res) => {
|
||||
if ("error" in res) {
|
||||
return done(new Error("should not happen"));
|
||||
}
|
||||
expect(res.data).to.eql([
|
||||
{
|
||||
id: "d445db6d-9d55-4ff2-88ae-bd1f81c299d2",
|
||||
title: "lorem ipsum",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "5f56fb59-a887-4984-93bf-eb39b4170a35",
|
||||
title: "dolor sit amet",
|
||||
completed: true,
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
11
examples/basic-crud-application/server/tsconfig.json
Normal file
11
examples/basic-crud-application/server/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"./lib/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user