├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── .nvmrc ├── .vscode └── launch.json ├── README.md ├── package.json ├── sandbox-app ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ └── in-memory-data.service.ts │ │ └── entities │ │ │ ├── components │ │ │ └── entity-form │ │ │ │ ├── entity-form.component.css │ │ │ │ ├── entity-form.component.html │ │ │ │ ├── entity-form.component.spec.ts │ │ │ │ └── entity-form.component.ts │ │ │ ├── containers │ │ │ ├── entity-list │ │ │ │ ├── entity-list.component.css │ │ │ │ ├── entity-list.component.html │ │ │ │ ├── entity-list.component.spec.ts │ │ │ │ └── entity-list.component.ts │ │ │ └── entity │ │ │ │ ├── entity.component.css │ │ │ │ ├── entity.component.html │ │ │ │ ├── entity.component.spec.ts │ │ │ │ └── entity.component.ts │ │ │ ├── entities-routing.module.ts │ │ │ ├── entities.module.spec.ts │ │ │ └── entities.module.ts │ ├── assets │ │ └── .gitkeep │ ├── browserslist │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock ├── src ├── collection.json └── ngrx-entity │ ├── __files__ │ ├── __name@dasherize@if-flat__ │ │ ├── __name@dasherize__.actions.ts │ │ ├── __name@dasherize__.effects.spec.ts │ │ ├── __name@dasherize__.effects.ts │ │ ├── __name@dasherize__.model.ts │ │ ├── __name@dasherize__.reducer.spec.ts │ │ ├── __name@dasherize__.reducer.ts │ │ ├── __name@dasherize__.service.ts │ │ ├── index.spec.ts │ │ └── index.ts │ ├── app.interfaces.ts │ ├── app.reducer.ts │ ├── state-utils.ts │ └── state.module.ts │ ├── index.d.ts │ ├── index.ts │ ├── schema.d.ts │ ├── schema.json │ └── utility │ ├── dependencies.d.ts │ ├── dependencies.ts │ ├── json-utils.d.ts │ ├── json-utils.ts │ ├── parseName.d.ts │ ├── parseName.ts │ ├── util.d.ts │ └── util.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. 13 | 2. 14 | 3. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | - Angular CLI Version [e.g. 7.0.1] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | **/dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | **/node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage 30 | /libpeerconnection.log 31 | npm-debug.log 32 | yarn-error.log 33 | testem.log 34 | /typings 35 | 36 | # System Files 37 | .DS_Store 38 | Thumbs.db 39 | 40 | # Schematic Outputs 41 | src/**/*.js 42 | !src/**/__files__/**/*.js 43 | src/**/*.js.map 44 | src/**.d.ts 45 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignores TypeScript files, but keeps definitions. 2 | *.ts 3 | !*.d.ts 4 | 5 | *.js.map 6 | !src/**/__files__/**/*.js 7 | !src/**/__files__/**/*.ts 8 | 9 | *.vscode 10 | *.idea 11 | 12 | sandbox-app 13 | .editorconfig 14 | .nvmrc 15 | tsconfig.root.json 16 | tsconfig.schematic.json 17 | 18 | *.circleci 19 | *.github 20 | *.lock 21 | *.log 22 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.9 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach Schematic", 11 | "port": 9229, 12 | "restart": false, 13 | "protocol": "inspector" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgRx Entity Schematic 2 | 3 | An Angular schematic for quickly scaffolding NgRx Entities with actions, effects, reducer, model, service, and passing specs. 4 | 5 | - [How to use](#how-to-use) 6 | - [What it generates](#generated) 7 | - [Local development](#development) 8 | - [Schematic project architecture](#architecture) 9 | 10 | ## How to Use 11 | 12 | ### Install the necessary NgRx Entity libraries in your project 13 | 14 | yarn add @ngrx/{effects,entity,router-store,store,store-devtools} ngrx-store-freeze 15 | yarn add -D jasmine-marbles 16 | 17 | ### Run the schematic 18 | 19 | ng add @briebug/ngrx-entity-schematic 20 | 21 | This will add the schematic as a project dependency if not already and provide prompts for configuration. 22 | 23 | #### Entity name 24 | 25 | The `ENTITY` name provided should either be camel case or dasherized (`customerOrder` || `customer-order`) 26 | 27 | ### Optional - run the schematic with inline options 28 | 29 | ng add @briebug/ngrx-entity-schematic ENTITY 30 | 31 | #### Generate Entity files at a specific relative path: `--path` 32 | 33 | ng add @briebug/ngrx-entity-schematic ENTITY --path PATH/TO/WRITE 34 | 35 | #### Generate Entity files with NgRx setup files: `--init` 36 | 37 | ng add @briebug/ngrx-entity-schematic ENTITY --init --path PATH/TO/WRITE 38 | 39 | - `ENTITY`, `--path`, and `--init` flags can be used together. 40 | - `ENTITY` is **required** as the first argument after the schematic name 41 | 42 | ## What it generates 43 | 44 | This schematic accepts an entity name and scaffolds all the necessary files for utilizing the NgRx Entity Library. For example, if you run the schematic for the entity `customer`, you'll end up with the following: 45 | 46 | ng add @briebug/ngrx-entity-schematic customer --path app/state 47 | 48 | ```text 49 | app/ 50 | ├── state/ 51 | │ └── customer 52 | │ ├── customer.actions.ts 53 | │ ├── customer.effects.spec.ts 54 | │ ├── customer.effects.ts 55 | │ ├── customer.model.ts 56 | │ ├── customer.reducer.spec.ts 57 | │ ├── customer.reducer.ts 58 | │ ├── customer.service.ts 59 | │ ├── index.ts 60 | ``` 61 | 62 | the `--init` option provides 4 additional files 63 | 64 | ng add @briebug/ngrx-entity-schematic customer --init --path app/state 65 | 66 | ```text 67 | app/ 68 | ├── state/ 69 | │ └── customer 70 | │ ├── customer.actions.ts 71 | │ ├── customer.effects.spec.ts 72 | │ ├── customer.effects.ts 73 | │ ├── customer.model.ts 74 | │ ├── customer.reducer.spec.ts 75 | │ ├── customer.reducer.ts 76 | │ ├── customer.service.ts 77 | │ ├── index.ts 78 | │ ├── app.interfaces.ts * 79 | │ ├── app.reducer.ts * 80 | │ ├── state-utils.ts * 81 | │ ├── state.module.ts * 82 | ``` 83 | 84 | Continuing the example of `customer`, the following are included: 85 | 86 | | action | effect | reducer | 87 | | ------ | ------ | ------- | 88 | | `InsertCustomer` | ✅ | ✅ | 89 | | `InsertCustomerSuccess` | | ✅ | 90 | | `InsertCustomerFail` | | ✅ | 91 | | `SearchAllCustomerEntities` | ✅ | ✅ | 92 | | `SearchAllCustomerEntitiesSuccess` | | ✅ | 93 | | `SearchAllCustomerEntitiesFail` | | ✅ | 94 | | `LoadCustomerById` | ✅ | ✅ | 95 | | `LoadCustomerByIdSuccess` | | ✅ | 96 | | `LoadCustomerByIdFail` | | ✅ | 97 | | `UpdateCustomer` | ✅ | ✅ | 98 | | `UpdateCustomerSuccess` | | ✅ | 99 | | `UpdateCustomerFail` | | ✅ | 100 | | `DeleteCustomerById` | ✅ | ✅ | 101 | | `DeleteCustomerByIdSuccess` | | ✅ | 102 | | `DeleteCustomerByIdFail` | | ✅ | 103 | | `SetSearchQuery` | ✅ | ✅ | 104 | | `SelectCustomerById` | ✅ | ✅ | 105 | 106 | ### Other files: 107 | 108 | - `index.ts` exports all the selectors. 109 | - `customer.service.ts` is a provider for your entities - you will need to modify this service to make CRUD calls for your entity. Be aware that the effects expect the methods in this file. 110 | - `customer.model.ts` - you can safely replace this but the generated spec files uses exported methods to generate mocks. 111 | 112 | *Be sure to audit the files and tailor them to your project* 113 | 114 | ## Install and use globally 115 | 116 | Optionally, you can install the package globally 117 | 118 | yarn global add @briebug/ngrx-entity-schematic 119 | 120 | Then run the schematic in any project, assuming the angular/cli is installed and available. 121 | 122 | ng g @briebug/ngrx-entity-schematic:add 123 | 124 | ## Adding another entity 125 | 126 | The schematic does not yet support auto connecting the entity to the root store when running the schematic without the `--init` option. The following steps will be necessary to connect the entity to the store manually. 127 | 128 | The following example assumes that an entity named `briebug` was first added with the initialization files (`--init`), followed by another entity named `order` without the initialization files. 129 | 130 | 1. add to the entity state from the `entity.reducer.ts` to the `state/app.interface.ts`. 131 | 132 | ```ts 133 | export interface AppState { 134 | router: RouterReducerState; 135 | briebug: BriebugState; 136 | order: OrderState; 137 | } 138 | ``` 139 | 140 | 2. add the entity reducer to the parent/root reducer in `state/app.reducer.ts`. 141 | 142 | ```ts 143 | export const appReducer: ActionReducerMap = { 144 | briebug: briebugReducer, 145 | router: routerReducer, 146 | order: orderReducer 147 | }; 148 | ``` 149 | 150 | 3. add the effects class to the parent/root Effects module `state/state.module.ts` in the `EffectsModule.forRoot([])` array. 151 | 152 | ```ts 153 | EffectsModule.forRoot([BriebugEffects, OrderEffects]), 154 | ``` 155 | 156 | ## Local Development 157 | 158 | ### Link the schematic to the `sandbox-app` 159 | 160 | This will create a symlink in your global packages so that when this schematic package is requested in the sandbox-app, it executes this local directory. 161 | 162 | Effectively executing the `./src/ngrx-entity/index.ts` every time the schematic is run inside the `./sandbox-app`. 163 | 164 | yarn link:schematic 165 | 166 | ### Run schematic locally 167 | 168 | The most robust way to test schematic changes against the sandbox-app is to reset the sandbox to its version-controlled state, build the schematic code, and execute the schematic against the sandbox-app. Make changes and repeat. 169 | 170 | yarn clean:build:launch 171 | 172 | You can pass optionally pass arguments to this command 173 | 174 | yarn clean:build:launch customerOrders --init --path src/app/state 175 | 176 | There are more specific commands that allow for individually running the above workflow. Those scripts can be found in the `./package.json`. 177 | 178 | ### Test the schematic prompts 179 | 180 | run the launch command with any inline options 181 | 182 | yarn launch 183 | 184 | ### Test commands 185 | 186 | The test command expects an entity name of `briebug` to test how the schematic runs inside the sandbox-app. Changing this script should require changes to the sandbox-app and understanding of the consequences. 187 | 188 | "test:ci": "yarn clean:build:launch briebug --init && yarn test:sandbox && yarn clean" 189 | 190 | ## Schematic Project Architecture 191 | 192 | ### ./src 193 | 194 | This is the schematic code that's executed when running `ng add @briebug/ngrx-entity-schematic`. 195 | 196 | ### ./sandbox-app 197 | 198 | This is an application that's used for testing the schematic locally during development. This provides E2E like feedback. 199 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@briebug/ngrx-entity-schematic", 3 | "version": "0.2.0", 4 | "description": "An Angular schematic for quickly scaffolding NgRx Entities with actions, effects, reducer, model, service, and passing specs.", 5 | "author": "Kevin Schuchard ", 6 | "contributors": [ 7 | "Reid Villeneuve ", 8 | "Jamie Perkins " 9 | ], 10 | "main": "src/ngrx-entity/index.js", 11 | "schematics": "./src/collection.json", 12 | "license": "MIT", 13 | "scripts": { 14 | "build:schematic": "tsc -p tsconfig.json", 15 | "build:schematic:watch": "tsc -p tsconfig.schematic.json -watch", 16 | "clean": "git checkout HEAD -- sandbox-app && git clean -fd sandbox-app", 17 | "clean:launch": "yarn clean && yarn launch", 18 | "clean:build:launch": "yarn build:schematic && yarn clean:launch", 19 | "launch": "cd sandbox-app && ng g @briebug/ngrx-entity-schematic:add", 20 | "test": "yarn clean:build:launch briebug --init --path src/app/state && yarn test:sandbox && yarn clean", 21 | "test:ci": "yarn clean:build:launch briebug --init --path src/app/state && yarn test:sandbox && yarn clean", 22 | "test:sandbox": "cd sandbox-app && yarn lint && yarn test && yarn e2e && yarn build", 23 | "link:schematic": "yarn link && cd sandbox-app && yarn link @briebug/ngrx-entity-schematic", 24 | "release": "yarn clean && yarn build:schematic && yarn np" 25 | }, 26 | "dependencies": { 27 | "@angular-devkit/core": "^7.0.2", 28 | "@angular-devkit/schematics": "^7.0.2" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.10.2", 32 | "@angular/compiler-cli": "^7.0.0", 33 | "@angular/language-service": "^7.0.0", 34 | "@types/jasmine": "^3.3.12", 35 | "@types/node": "^10.12.0", 36 | "codelyzer": "~4.5.0", 37 | "jasmine-core": "~3.3", 38 | "jasmine-spec-reporter": "~4.2.1", 39 | "karma": "~3.1.1", 40 | "karma-chrome-launcher": "~2.2.0", 41 | "karma-coverage-istanbul-reporter": "~2.0.4", 42 | "karma-jasmine": "~1.1.2", 43 | "karma-jasmine-html-reporter": "^1.3.1", 44 | "np": "3.0.4", 45 | "protractor": "~5.4.1", 46 | "ts-node": "^7.0.1", 47 | "tslint": "~5.11.0", 48 | "typescript": "~3.1.3" 49 | }, 50 | "engines": { 51 | "node": ">=8.11.0" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/briebug/ngrx-entity-schematic" 56 | }, 57 | "bugs": "https://github.com/briebug/ngrx-entity-schematic/issues", 58 | "publishConfig": { 59 | "access": "public" 60 | }, 61 | "keywords": [ 62 | "angular", 63 | "schematic", 64 | "ngrx", 65 | "entity", 66 | "entities", 67 | "scaffold" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /sandbox-app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngrx-entity-generator": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "changeDetection": "OnPush" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ngrx-entity-generator", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.css" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "fileReplacements": [ 37 | { 38 | "replace": "src/environments/environment.ts", 39 | "with": "src/environments/environment.prod.ts" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "extractCss": true, 46 | "namedChunks": false, 47 | "aot": true, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true 51 | } 52 | } 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "options": { 57 | "browserTarget": "ngrx-entity-generator:build" 58 | }, 59 | "configurations": { 60 | "production": { 61 | "browserTarget": "ngrx-entity-generator:build:production" 62 | } 63 | } 64 | }, 65 | "extract-i18n": { 66 | "builder": "@angular-devkit/build-angular:extract-i18n", 67 | "options": { 68 | "browserTarget": "ngrx-entity-generator:build" 69 | } 70 | }, 71 | "test": { 72 | "builder": "@angular-devkit/build-angular:karma", 73 | "options": { 74 | "main": "src/test.ts", 75 | "polyfills": "src/polyfills.ts", 76 | "tsConfig": "src/tsconfig.spec.json", 77 | "karmaConfig": "src/karma.conf.js", 78 | "styles": [ 79 | "src/styles.css" 80 | ], 81 | "scripts": [], 82 | "assets": [ 83 | "src/favicon.ico", 84 | "src/assets" 85 | ] 86 | } 87 | }, 88 | "lint": { 89 | "builder": "@angular-devkit/build-angular:tslint", 90 | "options": { 91 | "tsConfig": [ 92 | "src/tsconfig.app.json", 93 | "src/tsconfig.spec.json" 94 | ], 95 | "exclude": [ 96 | "**/node_modules/**" 97 | ] 98 | } 99 | } 100 | } 101 | }, 102 | "ngrx-entity-generator-e2e": { 103 | "root": "e2e/", 104 | "projectType": "application", 105 | "architect": { 106 | "e2e": { 107 | "builder": "@angular-devkit/build-angular:protractor", 108 | "options": { 109 | "protractorConfig": "e2e/protractor.conf.js", 110 | "devServerTarget": "ngrx-entity-generator:serve" 111 | } 112 | }, 113 | "lint": { 114 | "builder": "@angular-devkit/build-angular:tslint", 115 | "options": { 116 | "tsConfig": "e2e/tsconfig.e2e.json", 117 | "exclude": [ 118 | "**/node_modules/**" 119 | ] 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "defaultProject": "ngrx-entity-generator" 126 | } 127 | -------------------------------------------------------------------------------- /sandbox-app/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /sandbox-app/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Test Entities'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /sandbox-app/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sandbox-app/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /sandbox-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-entity-generator", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --prod", 8 | "test": "ng test --watch=false", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^7.0.0", 15 | "@angular/common": "^7.0.0", 16 | "@angular/compiler": "^7.0.0", 17 | "@angular/core": "^7.0.0", 18 | "@angular/forms": "^7.0.0", 19 | "@angular/http": "^7.0.0", 20 | "@angular/platform-browser": "^7.0.0", 21 | "@angular/platform-browser-dynamic": "^7.0.0", 22 | "@angular/router": "^7.0.0", 23 | "@ngrx/effects": "7.4.0", 24 | "@ngrx/entity": "7.4.0", 25 | "@ngrx/router-store": "7.4.0", 26 | "@ngrx/store": "7.4.0", 27 | "angular-in-memory-web-api": "^0.6.1", 28 | "core-js": "^2.5.7", 29 | "rxjs": "^6.3.3", 30 | "zone.js": "^0.8.26" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.10.2", 34 | "@angular/cli": "~7.0.2", 35 | "@angular/compiler-cli": "^7.0.0", 36 | "@angular/language-service": "^7.0.0", 37 | "@ngrx/store-devtools": "7.4.0", 38 | "@types/jasmine": "~2.8.9", 39 | "@types/jasminewd2": "~2.0.5", 40 | "@types/node": "~10.12.0", 41 | "codelyzer": "~4.5.0", 42 | "jasmine-core": "~3.3", 43 | "jasmine-marbles": "0.5.0", 44 | "jasmine-spec-reporter": "~4.2.1", 45 | "karma": "~3.1.1", 46 | "karma-chrome-launcher": "~2.2.0", 47 | "karma-coverage-istanbul-reporter": "~2.0.4", 48 | "karma-jasmine": "~1.1.2", 49 | "karma-jasmine-html-reporter": "^1.3.1", 50 | "ngrx-store-freeze": "0.2.4", 51 | "protractor": "~5.4.1", 52 | "ts-node": "~7.0.1", 53 | "tslint": "~5.11.0", 54 | "typescript": "~3.1.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sandbox-app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | redirectTo: '/entities', 8 | pathMatch: 'full' 9 | }, 10 | { 11 | path: 'entities', 12 | loadChildren: 'src/app/entities/entities.module#EntitiesModule' 13 | }, 14 | { 15 | path: '**', 16 | redirectTo: '/entities', 17 | pathMatch: 'full' 18 | } 19 | ]; 20 | 21 | @NgModule({ 22 | imports: [RouterModule.forRoot(routes)], 23 | exports: [RouterModule] 24 | }) 25 | export class AppRoutingModule {} 26 | -------------------------------------------------------------------------------- /sandbox-app/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briebug/ngrx-entity-schematic/33958bba121c62fda692cf6d0ddba0769f4d5212/sandbox-app/src/app/app.component.css -------------------------------------------------------------------------------- /sandbox-app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sandbox-app/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | it('should create the app', async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | })); 21 | }); 22 | -------------------------------------------------------------------------------- /sandbox-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /sandbox-app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { AppRoutingModule } from './app-routing.module'; 8 | import { CoreModule } from './core/core.module'; 9 | import { InMemoryDataService } from './core/in-memory-data.service'; 10 | import { StateModule } from './state/state.module'; 11 | 12 | @NgModule({ 13 | declarations: [AppComponent], 14 | imports: [ 15 | AppRoutingModule, 16 | BrowserModule, 17 | CoreModule.forRoot(), 18 | InMemoryWebApiModule.forRoot(InMemoryDataService, { delay: 600 }), 19 | StateModule.forRoot() 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent] 23 | }) 24 | export class AppModule {} 25 | -------------------------------------------------------------------------------- /sandbox-app/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | 5 | import { BriebugService } from '@state/briebug/briebug.service'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, HttpClientModule], 9 | declarations: [], 10 | providers: [BriebugService] 11 | }) 12 | export class CoreModule { 13 | constructor( 14 | @Optional() 15 | @SkipSelf() 16 | parentModule: CoreModule 17 | ) { 18 | if (parentModule) { 19 | throw new Error('CoreModule is already loaded. Import it in the AppModule only'); 20 | } 21 | } 22 | 23 | static forRoot(): ModuleWithProviders { 24 | return { 25 | ngModule: CoreModule, 26 | providers: [] 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sandbox-app/src/app/core/in-memory-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Briebug } from '@state/briebug/briebug.model'; 2 | 3 | export class InMemoryDataService { 4 | createDb() { 5 | const briebug: Array = [ 6 | { 7 | id: 1, 8 | name: 'Some Name', 9 | description: 'Some Description' 10 | }, 11 | { 12 | id: 2, 13 | name: 'Better Name', 14 | description: 'Better Description' 15 | }, 16 | { 17 | id: 3, 18 | name: 'Oh yeah? Well I have the best name!', 19 | description: 'And the best description, too!' 20 | }, 21 | { 22 | id: 4, 23 | name: 'Well, agree to disagree, then.', 24 | description: 'And for the record, I disagree!' 25 | } 26 | ]; 27 | 28 | return { briebug }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/components/entity-form/entity-form.component.css: -------------------------------------------------------------------------------- 1 | label { 2 | margin-right: 10px; 3 | } 4 | 5 | input { 6 | margin-bottom: 10px; 7 | width: 250px; 8 | } 9 | 10 | .invalid { 11 | color: red; 12 | } 13 | 14 | button { 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/components/entity-form/entity-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 |
11 | Name is required 12 |
13 | 14 |
15 | 16 | 17 | 21 |
25 | Description is required 26 |
27 | 28 |
29 | 30 | 37 |
38 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/components/entity-form/entity-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { SimpleChange } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { BriebugFormComponent } from './entity-form.component'; 6 | import { generateBriebug } from '@state/briebug/briebug.model'; 7 | import { fromEvent } from 'rxjs'; 8 | 9 | describe('BreibugFormComponent', () => { 10 | let component: BriebugFormComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ReactiveFormsModule], 16 | declarations: [BriebugFormComponent] 17 | }).compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(BriebugFormComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | 30 | describe('ngOnChanges', () => { 31 | it('should patch changes into the entity', () => { 32 | component.briebug = generateBriebug(); 33 | 34 | component.ngOnChanges({ 35 | briebug: new SimpleChange(null, component.briebug, true) 36 | }); 37 | 38 | expect(component.formGroup.value).toEqual({ ...component.briebug }); 39 | }); 40 | }); 41 | 42 | describe('constructor', () => { 43 | it('should emit briebugChanged when the form changes', (done) => { 44 | const briebug = generateBriebug(); 45 | 46 | component.briebugChanged.subscribe((value) => { 47 | expect(value).toEqual({ 48 | briebug, 49 | valid: component.formGroup.valid 50 | }); 51 | done(); 52 | }); 53 | 54 | // Called twice because the first value is skipped: 55 | component.formGroup.patchValue(briebug); 56 | component.formGroup.patchValue(briebug); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/components/entity-form/entity-form.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | OnChanges, 7 | SimpleChanges, 8 | OnDestroy, 9 | ChangeDetectionStrategy 10 | } from '@angular/core'; 11 | 12 | import { Briebug } from '@state/briebug/briebug.model'; 13 | import { FormGroup, Validators, FormBuilder } from '@angular/forms'; 14 | import { Subject } from 'rxjs'; 15 | import { takeUntil, skip, debounceTime } from 'rxjs/operators'; 16 | 17 | @Component({ 18 | selector: 'app-briebug-form', 19 | templateUrl: './entity-form.component.html', 20 | styleUrls: ['./entity-form.component.css'], 21 | changeDetection: ChangeDetectionStrategy.OnPush 22 | }) 23 | export class BriebugFormComponent implements OnChanges, OnDestroy { 24 | formGroup: FormGroup; 25 | 26 | @Input() briebug: Briebug; 27 | @Input() disableFields: boolean; 28 | @Input() showErrors: boolean; 29 | @Output() submit = new EventEmitter(); 30 | @Output() briebugChanged = new EventEmitter<{ briebug: Briebug; valid: boolean }>(); 31 | 32 | private destroyed$ = new Subject(); 33 | 34 | constructor(private formBuilder: FormBuilder) { 35 | this.buildForm(); 36 | } 37 | 38 | ngOnChanges(changes: SimpleChanges) { 39 | if (changes.briebug && changes.briebug.currentValue) { 40 | this.formGroup.patchValue(this.briebug); 41 | } 42 | } 43 | 44 | ngOnDestroy() { 45 | this.destroyed$.next(); 46 | this.destroyed$.complete(); 47 | } 48 | 49 | private buildForm() { 50 | // FIXME: Fields are not disabling as expected. 51 | this.formGroup = this.formBuilder.group({ 52 | id: null, 53 | name: [{ value: '', disabled: this.disableFields }, Validators.required], 54 | description: [{ value: '', disabled: this.disableFields }, Validators.required] 55 | }); 56 | 57 | this.formGroup.valueChanges 58 | .pipe( 59 | takeUntil(this.destroyed$), 60 | skip(1), 61 | debounceTime(500) 62 | ) 63 | .subscribe((value) => { 64 | this.briebugChanged.emit({ 65 | briebug: value, 66 | valid: this.formGroup.valid 67 | }); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity-list/entity-list.component.css: -------------------------------------------------------------------------------- 1 | .error-message { 2 | color: red; 3 | } 4 | ul { 5 | list-style-type: none; 6 | } 7 | 8 | li { 9 | margin-bottom: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity-list/entity-list.component.html: -------------------------------------------------------------------------------- 1 |

Test Entities

2 | 3 |

Loading...

4 |

{{errorMessage$ | async}}

5 | 6 |
    7 |
  • 8 | Name: {{briebug.name}}
    9 | Description: {{briebug.description}}
    10 |   11 | 12 |
  • 13 |
14 | 15 | (Add new entity) 16 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity-list/entity-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { Store, StoreModule } from '@ngrx/store'; 5 | 6 | import { BriebugListComponent } from './entity-list.component'; 7 | import { appReducer } from '@state/app.reducer'; 8 | import { AppState } from '@state/app.interfaces'; 9 | import { 10 | SearchAllBriebugEntities, 11 | SearchAllBriebugEntitiesSuccess, 12 | SearchAllBriebugEntitiesFail 13 | } from '@state/briebug/briebug.actions'; 14 | import { generateBriebugArray } from '@state/briebug/briebug.model'; 15 | 16 | describe('BriebugListComponent', () => { 17 | let component: BriebugListComponent; 18 | let fixture: ComponentFixture; 19 | let store: Store; 20 | 21 | beforeEach(async(() => { 22 | TestBed.configureTestingModule({ 23 | declarations: [BriebugListComponent], 24 | imports: [RouterTestingModule, StoreModule.forRoot(appReducer)] 25 | }).compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(BriebugListComponent); 30 | component = fixture.componentInstance; 31 | store = TestBed.get(Store); 32 | fixture.detectChanges(); 33 | 34 | spyOn(store, 'dispatch').and.callThrough(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | 41 | it('should get briebug entities when available', (done) => { 42 | const entities = generateBriebugArray(); 43 | 44 | store.dispatch(new SearchAllBriebugEntitiesSuccess({ result: entities })); 45 | 46 | component.briebugEntities$.subscribe((result) => { 47 | expect(result).toEqual(entities); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should get the loading status when available', (done) => { 53 | // Used to set loading to true: 54 | store.dispatch(new SearchAllBriebugEntities()); 55 | 56 | component.isLoading$.subscribe((isLoading) => { 57 | expect(isLoading).toBe(true); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should get the error message when available', (done) => { 63 | const testError = 'Some Error Message'; 64 | store.dispatch(new SearchAllBriebugEntitiesFail({ error: testError })); 65 | 66 | component.errorMessage$.subscribe((errorResult) => { 67 | if (errorResult) { // Needed because the first errorResult is blank 68 | expect(errorResult).toContain(testError); 69 | // This done() ensures that this test isn't skipped as a result of the 70 | // if block - tests fail if done is never called. 71 | done(); 72 | } 73 | }); 74 | }); 75 | 76 | describe('ngOnInit', () => { 77 | it('should request all Briebug entities', () => { 78 | component.ngOnInit(); 79 | 80 | expect(store.dispatch).toHaveBeenCalledWith( 81 | new SearchAllBriebugEntities() 82 | ); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity-list/entity-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { select, Store } from '@ngrx/store'; 3 | 4 | import { Observable } from 'rxjs'; 5 | import { shareReplay } from 'rxjs/operators'; 6 | 7 | import { 8 | briebug, 9 | briebugLoading, 10 | briebugError 11 | } from '@state/briebug'; 12 | import { BriebugState } from '@state/briebug/briebug.reducer'; 13 | import { 14 | SearchAllBriebugEntities, 15 | DeleteBriebugById 16 | } from '@state/briebug/briebug.actions'; 17 | import { Briebug } from '@state/briebug/briebug.model'; 18 | 19 | @Component({ 20 | templateUrl: './entity-list.component.html', 21 | styleUrls: ['./entity-list.component.css'], 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class BriebugListComponent implements OnInit { 25 | briebugEntities$ = this.store.pipe(select(briebug)); 26 | isLoading$ = this.store.pipe(select(briebugLoading)); 27 | errorMessage$ = this.store.pipe( 28 | select(briebugError), 29 | // This allows us to use the async pipe twice without creating two subscriptions: 30 | shareReplay() 31 | ); 32 | 33 | constructor(private store: Store) {} 34 | 35 | ngOnInit() { 36 | this.store.dispatch(new SearchAllBriebugEntities()); 37 | } 38 | 39 | deleteBriebug(id) { 40 | this.store.dispatch(new DeleteBriebugById({ id })); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity/entity.component.css: -------------------------------------------------------------------------------- 1 | .error-message { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity/entity.component.html: -------------------------------------------------------------------------------- 1 |

Test Entity

2 | 3 |

Loading...

4 |

{{errorMessage$ | async}}

5 | 6 | 13 | 14 | Back to list 15 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity/entity.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveFormsModule } from '@angular/forms'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { ActivatedRoute, convertToParamMap } from '@angular/router'; 5 | 6 | import * as fromRoot from '@state/app.reducer'; 7 | import { Store, StoreModule } from '@ngrx/store'; 8 | import { Subject } from 'rxjs'; 9 | 10 | import { BriebugComponent } from './entity.component'; 11 | import { BriebugFormComponent } from '../../components/entity-form/entity-form.component'; 12 | import { AppState } from '@state/app.interfaces'; 13 | import { generateBriebug } from '@state/briebug/briebug.model'; 14 | import { 15 | LoadBriebugById, 16 | LoadBriebugByIdSuccess, 17 | SelectBriebugById, 18 | LoadBriebugByIdFail, 19 | UpdateBriebug, 20 | CreateBriebug 21 | } from '@state/briebug/briebug.actions'; 22 | import { skip } from 'rxjs/operators'; 23 | 24 | describe('BriebugComponent', () => { 25 | let component: BriebugComponent; 26 | let fixture: ComponentFixture; 27 | let store: Store; 28 | const paramMap = new Subject(); 29 | const url = new Subject(); 30 | 31 | beforeEach(async(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [ 34 | ReactiveFormsModule, 35 | RouterTestingModule, 36 | StoreModule.forRoot(fromRoot.appReducer) 37 | ], 38 | declarations: [BriebugComponent, BriebugFormComponent], 39 | providers: [ 40 | { 41 | provide: ActivatedRoute, 42 | useValue: { 43 | paramMap, 44 | url 45 | } 46 | } 47 | ] 48 | }).compileComponents(); 49 | })); 50 | 51 | beforeEach(() => { 52 | fixture = TestBed.createComponent(BriebugComponent); 53 | component = fixture.componentInstance; 54 | store = TestBed.get(Store); 55 | fixture.detectChanges(); 56 | 57 | spyOn(store, 'dispatch').and.callThrough(); 58 | }); 59 | 60 | it('should create', () => { 61 | expect(component).toBeTruthy(); 62 | }); 63 | 64 | it('should get the specified briebug entities if editing', (done) => { 65 | const entity = generateBriebug(); 66 | 67 | component.briebug$.subscribe((result) => { 68 | if (result && result.id) { 69 | expect(result).toEqual(entity); 70 | // This done() ensures that this test isn't skipped as a result of the 71 | // if block - tests fail if done is never called. 72 | done(); 73 | } 74 | }); 75 | 76 | paramMap.next(convertToParamMap({ id: entity.id })); 77 | url.next([{ path: '1' }]); 78 | store.dispatch(new LoadBriebugByIdSuccess({ result: entity })); 79 | }); 80 | 81 | // it('should select a null ID if adding', (done) => { 82 | // component.briebug$.subscribe((result) => { 83 | // expect(Object.keys(result).length).toBe(0); 84 | // expect(store.dispatch).toHaveBeenCalledWith( 85 | // new SelectBriebugById({ id: null }) 86 | // ); 87 | // done(); 88 | // }); 89 | 90 | // paramMap.next(convertToParamMap({})); 91 | // url.next([{ path: 'add' }]); 92 | // }); 93 | 94 | it('should get the error message when available', (done) => { 95 | const testError = 'Some Error Message'; 96 | store.dispatch(new LoadBriebugByIdFail({ error: testError })); 97 | 98 | component.errorMessage$.pipe(skip(1)).subscribe((errorResult) => { 99 | expect(errorResult).toContain(testError); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should get the loading status when available', (done) => { 105 | // Used to set loading to true: 106 | store.dispatch(new LoadBriebugById({ id: 1 })); 107 | 108 | component.isLoading$.subscribe((isLoading) => { 109 | if (isLoading) { 110 | // Protects against initial value of false 111 | expect(isLoading).toBe(true); 112 | // This done() ensures that this test isn't skipped as a result of the 113 | // if block - tests fail if done is never called. 114 | done(); 115 | } 116 | }); 117 | }); 118 | 119 | describe('ngOnInit', () => { 120 | it('should not show the form errors', () => { 121 | component.ngOnInit(); 122 | 123 | expect(component.showFormErrors).toBe(false); 124 | }); 125 | }); 126 | 127 | describe('onBriebugChanged', () => { 128 | it('should match the returned entity and valid values', () => { 129 | const briebug = generateBriebug(); 130 | const valid = true; 131 | 132 | component.onBriebugChanged({ briebug, valid }); 133 | 134 | expect(component.briebugEdits).toEqual(briebug); 135 | expect(component.valid).toBe(valid); 136 | }); 137 | }); 138 | 139 | describe('onSubmit', () => { 140 | it('should show the form errors', () => { 141 | component.showFormErrors = false; 142 | 143 | component.onSubmit(); 144 | 145 | expect(component.showFormErrors).toBe(true); 146 | }); 147 | 148 | it('should not dispatch anything if the form isn\'t valid', () => { 149 | component.valid = false; 150 | 151 | component.onSubmit(); 152 | 153 | expect(store.dispatch).not.toHaveBeenCalled(); 154 | }); 155 | 156 | it('should dispatch UpdateBriebug if an ID is present', () => { 157 | component.valid = true; 158 | component.briebugEdits = generateBriebug(); 159 | 160 | component.onSubmit(); 161 | 162 | expect(store.dispatch).toHaveBeenCalledWith( 163 | new UpdateBriebug({ briebug: component.briebugEdits }) 164 | ); 165 | }); 166 | 167 | it('should dispatch CreateBriebug if an ID is not present', () => { 168 | component.valid = true; 169 | component.briebugEdits = generateBriebug(); 170 | delete component.briebugEdits.id; 171 | 172 | component.onSubmit(); 173 | 174 | expect(store.dispatch).toHaveBeenCalledWith( 175 | new CreateBriebug({ briebug: component.briebugEdits }) 176 | ); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/containers/entity/entity.component.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRoute } from '@angular/router'; 2 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 3 | 4 | import { Observable } from 'rxjs'; 5 | import { select, Store } from '@ngrx/store'; 6 | import { 7 | tap, 8 | filter, 9 | map, 10 | switchMap, 11 | shareReplay, 12 | combineLatest 13 | } from 'rxjs/operators'; 14 | 15 | import { briebugLoading, currentBriebug, briebugError } from '@state/briebug'; 16 | import { Briebug } from '@state/briebug/briebug.model'; 17 | import { 18 | LoadBriebugById, 19 | CreateBriebug, 20 | UpdateBriebug, 21 | SelectBriebugById 22 | } from '@state/briebug/briebug.actions'; 23 | import { BriebugState } from '@state/briebug/briebug.reducer'; 24 | 25 | @Component({ 26 | templateUrl: './entity.component.html', 27 | styleUrls: ['./entity.component.css'], 28 | changeDetection: ChangeDetectionStrategy.OnPush 29 | }) 30 | export class BriebugComponent implements OnInit { 31 | briebug$ = this.activatedRoute.paramMap.pipe( 32 | combineLatest(this.activatedRoute.url), 33 | filter(([params, url]) => 34 | params.has('id') || url[0].path === 'add' 35 | ), 36 | map(([params]) => params.get('id')), 37 | tap((id) => { 38 | // If no ID is present, then the user is here to add a new entity. 39 | const BriebugAction = id ? LoadBriebugById : SelectBriebugById; 40 | this.store.dispatch(new BriebugAction({ id: +id || null })); 41 | }), 42 | switchMap(() => this.store.pipe(select(currentBriebug))), 43 | map((briebug) => ({ ...briebug })) 44 | ); 45 | // The following shareReplay calls allow us to use the async pipe multiple 46 | // times without creating multiple subscriptions: 47 | errorMessage$ = this.store.pipe( 48 | select(briebugError), 49 | shareReplay() 50 | ); 51 | isLoading$ = this.store.pipe( 52 | select(briebugLoading), 53 | shareReplay() 54 | ); 55 | 56 | briebugEdits: Briebug; 57 | showFormErrors: boolean; 58 | valid: boolean; 59 | 60 | constructor( 61 | private activatedRoute: ActivatedRoute, 62 | private store: Store 63 | ) {} 64 | 65 | ngOnInit() { 66 | this.showFormErrors = false; 67 | } 68 | 69 | onBriebugChanged({ briebug, valid }: { briebug: Briebug; valid: boolean }) { 70 | this.briebugEdits = briebug; 71 | this.valid = valid; 72 | } 73 | 74 | onSubmit() { 75 | this.showFormErrors = true; 76 | 77 | if (!this.valid) { 78 | return; 79 | } 80 | 81 | const BriebugAction = this.briebugEdits.id 82 | ? UpdateBriebug 83 | : CreateBriebug; 84 | this.store.dispatch(new BriebugAction({ briebug: this.briebugEdits })); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/entities-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { BriebugListComponent } from './containers/entity-list/entity-list.component'; 5 | import { BriebugComponent } from './containers/entity/entity.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: BriebugListComponent 11 | }, 12 | { 13 | path: 'add', 14 | component: BriebugComponent 15 | }, 16 | { 17 | path: ':id', 18 | component: BriebugComponent 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class EntitiesRoutingModule {} 27 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/entities.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntitiesModule } from './entities.module'; 2 | 3 | describe('EntitiesModule', () => { 4 | let entitiesModule: EntitiesModule; 5 | 6 | beforeEach(() => { 7 | entitiesModule = new EntitiesModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(entitiesModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /sandbox-app/src/app/entities/entities.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { BriebugComponent } from './containers/entity/entity.component'; 6 | import { BriebugListComponent } from './containers/entity-list/entity-list.component'; 7 | import { BriebugFormComponent } from './components/entity-form/entity-form.component'; 8 | import { EntitiesRoutingModule } from './entities-routing.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | ReactiveFormsModule, 14 | EntitiesRoutingModule 15 | ], 16 | declarations: [BriebugListComponent, BriebugComponent, BriebugFormComponent] 17 | }) 18 | export class EntitiesModule {} 19 | -------------------------------------------------------------------------------- /sandbox-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briebug/ngrx-entity-schematic/33958bba121c62fda692cf6d0ddba0769f4d5212/sandbox-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /sandbox-app/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /sandbox-app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /sandbox-app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /sandbox-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briebug/ngrx-entity-schematic/33958bba121c62fda692cf6d0ddba0769f4d5212/sandbox-app/src/favicon.ico -------------------------------------------------------------------------------- /sandbox-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NGRX Entity Generator Example App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sandbox-app/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /sandbox-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /sandbox-app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /sandbox-app/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /sandbox-app/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /sandbox-app/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /sandbox-app/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /sandbox-app/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sandbox-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "paths": { 6 | "@state/*": ["src/app/state/*"], 7 | "@core/*": ["src/app/core/*"] 8 | }, 9 | "outDir": "./dist/out-tsc", 10 | "sourceMap": true, 11 | "declaration": false, 12 | "moduleResolution": "node", 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "target": "es5", 16 | "typeRoots": [ 17 | "node_modules/@types" 18 | ], 19 | "lib": [ 20 | "es2017", 21 | "dom" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sandbox-app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | "**/*.json" 8 | ] 9 | }, 10 | "rules": { 11 | "arrow-return-shorthand": true, 12 | "callable-types": true, 13 | "class-name": true, 14 | "comment-format": [ 15 | true, 16 | "check-space" 17 | ], 18 | "curly": true, 19 | "deprecation": { 20 | "severity": "warn" 21 | }, 22 | "eofline": true, 23 | "forin": true, 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "import-spacing": true, 29 | "indent": [ 30 | true, 31 | "spaces" 32 | ], 33 | "interface-over-type-literal": true, 34 | "label-position": true, 35 | "max-line-length": [ 36 | true, 37 | 140 38 | ], 39 | "member-access": false, 40 | "member-ordering": [ 41 | true, 42 | { 43 | "order": [ 44 | "static-field", 45 | "instance-field", 46 | "static-method", 47 | "instance-method" 48 | ] 49 | } 50 | ], 51 | "no-arg": true, 52 | "no-bitwise": true, 53 | "no-console": [ 54 | true, 55 | "debug", 56 | "info", 57 | "time", 58 | "timeEnd", 59 | "trace" 60 | ], 61 | "no-construct": true, 62 | "no-debugger": true, 63 | "no-duplicate-super": true, 64 | "no-empty": false, 65 | "no-empty-interface": true, 66 | "no-eval": true, 67 | "no-inferrable-types": [ 68 | true, 69 | "ignore-params" 70 | ], 71 | "no-misused-new": true, 72 | "no-non-null-assertion": true, 73 | "no-shadowed-variable": true, 74 | "no-string-literal": false, 75 | "no-string-throw": true, 76 | "no-switch-case-fall-through": true, 77 | "no-trailing-whitespace": true, 78 | "no-unnecessary-initializer": true, 79 | "no-unused-expression": true, 80 | "no-use-before-declare": true, 81 | "no-var-keyword": true, 82 | "object-literal-sort-keys": false, 83 | "one-line": [ 84 | true, 85 | "check-open-brace", 86 | "check-catch", 87 | "check-else", 88 | "check-whitespace" 89 | ], 90 | "prefer-const": true, 91 | "quotemark": [ 92 | true, 93 | "single" 94 | ], 95 | "radix": true, 96 | "semicolon": [ 97 | true, 98 | "always" 99 | ], 100 | "triple-equals": [ 101 | true, 102 | "allow-null-check" 103 | ], 104 | "typedef-whitespace": [ 105 | true, 106 | { 107 | "call-signature": "nospace", 108 | "index-signature": "nospace", 109 | "parameter": "nospace", 110 | "property-declaration": "nospace", 111 | "variable-declaration": "nospace" 112 | } 113 | ], 114 | "unified-signatures": true, 115 | "variable-name": false, 116 | "whitespace": [ 117 | true, 118 | "check-branch", 119 | "check-decl", 120 | "check-operator", 121 | "check-separator", 122 | "check-type" 123 | ], 124 | "no-output-on-prefix": true, 125 | "use-input-property-decorator": true, 126 | "use-output-property-decorator": true, 127 | "use-host-property-decorator": true, 128 | "no-input-rename": true, 129 | "no-output-rename": true, 130 | "use-life-cycle-interface": true, 131 | "use-pipe-transform-interface": true, 132 | "component-class-suffix": true, 133 | "directive-class-suffix": true 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "ng-add": { 5 | "description": "Generates all NgRx Entity files for an application", 6 | "factory": "./ngrx-entity/index", 7 | "schema": "./ngrx-entity/schema.json" 8 | }, 9 | "add": { 10 | "description": "Generates all NgRx Entity files for an application", 11 | "factory": "./ngrx-entity/index", 12 | "schema": "./ngrx-entity/schema.json" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Update } from '@ngrx/entity'; 3 | import { <%= classify(name) %> } from './<%= dasherize(name) %>.model'; 4 | import { <%= classify(name) %>SearchQuery } from './<%= dasherize(name) %>.reducer'; 5 | 6 | export enum <%= classify(name) %>ActionTypes { 7 | Create<%= classify(name) %> = '[<%= classify(name) %>] Create', 8 | Create<%= classify(name) %>Success = '[<%= classify(name) %>] Insert Success', 9 | Create<%= classify(name) %>Fail = '[<%= classify(name) %>] Insert Fail', 10 | 11 | SearchAll<%= classify(name) %>Entities = '[<%= classify(name) %>] Search', 12 | SearchAll<%= classify(name) %>EntitiesSuccess = '[<%= classify(name) %>] Search Success', 13 | SearchAll<%= classify(name) %>EntitiesFail = '[<%= classify(name) %>] Search Fail', 14 | 15 | Load<%= classify(name) %>ById = '[<%= classify(name) %>] Load By ID', 16 | Load<%= classify(name) %>ByIdSuccess = '[<%= classify(name) %>] Load Success', 17 | Load<%= classify(name) %>ByIdFail = '[<%= classify(name) %>] Load Fail', 18 | 19 | Update<%= classify(name) %> = '[<%= classify(name) %>] Update', 20 | Update<%= classify(name) %>Success = '[<%= classify(name) %>] Update Success', 21 | Update<%= classify(name) %>Fail = '[<%= classify(name) %>] Update Fail', 22 | 23 | Delete<%= classify(name) %>ById = '[<%= classify(name) %>] Delete By ID', 24 | Delete<%= classify(name) %>ByIdSuccess = '[<%= classify(name) %>] Delete Success', 25 | Delete<%= classify(name) %>ByIdFail = '[<%= classify(name) %>] Delete Fail', 26 | 27 | SetSearchQuery = '[<%= classify(name) %>] Set Search Query', 28 | Select<%= classify(name) %>ById = '[<%= classify(name) %>] Select By ID' 29 | } 30 | 31 | // ========================================= CREATE 32 | 33 | export class Create<%= classify(name) %> implements Action { 34 | readonly type = <%= classify(name) %>ActionTypes.Create<%= classify(name) %>; 35 | constructor(public payload: { <%= name %>: <%= classify(name) %> }) {} 36 | } 37 | 38 | export class Create<%= classify(name) %>Success implements Action { 39 | readonly type = <%= classify(name) %>ActionTypes.Create<%= classify(name) %>Success; 40 | constructor(public payload: { result: <%= classify(name) %> }) {} 41 | } 42 | 43 | export class Create<%= classify(name) %>Fail implements Action { 44 | readonly type = <%= classify(name) %>ActionTypes.Create<%= classify(name) %>Fail; 45 | constructor(public payload: { error: string }) {} 46 | } 47 | 48 | // ========================================= SEARCH 49 | 50 | export class SearchAll<%= classify(name) %>Entities implements Action { 51 | readonly type = <%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>Entities; 52 | } 53 | 54 | export class SearchAll<%= classify(name) %>EntitiesSuccess implements Action { 55 | readonly type = <%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>EntitiesSuccess; 56 | constructor(public payload: { result: Array<<%= classify(name) %>> }) {} 57 | } 58 | 59 | export class SearchAll<%= classify(name) %>EntitiesFail implements Action { 60 | readonly type = <%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>EntitiesFail; 61 | constructor(public payload: { error: string }) {} 62 | } 63 | 64 | // ========================================= LOAD BY ID 65 | 66 | export class Load<%= classify(name) %>ById implements Action { 67 | readonly type = <%= classify(name) %>ActionTypes.Load<%= classify(name) %>ById; 68 | constructor(public payload: { id: number }) {} 69 | } 70 | 71 | export class Load<%= classify(name) %>ByIdSuccess implements Action { 72 | readonly type = <%= classify(name) %>ActionTypes.Load<%= classify(name) %>ByIdSuccess; 73 | constructor(public payload: { result: <%= classify(name) %> }) {} 74 | } 75 | 76 | export class Load<%= classify(name) %>ByIdFail implements Action { 77 | readonly type = <%= classify(name) %>ActionTypes.Load<%= classify(name) %>ByIdFail; 78 | constructor(public payload: { error: string }) {} 79 | } 80 | 81 | // ========================================= UPDATE 82 | 83 | export class Update<%= classify(name) %> implements Action { 84 | readonly type = <%= classify(name) %>ActionTypes.Update<%= classify(name) %>; 85 | constructor(public payload: { <%= name %>: <%= classify(name) %> }) {} 86 | } 87 | 88 | export class Update<%= classify(name) %>Success implements Action { 89 | readonly type = <%= classify(name) %>ActionTypes.Update<%= classify(name) %>Success; 90 | constructor(public payload: { update: Update<<%= classify(name) %>> }) {} 91 | } 92 | 93 | export class Update<%= classify(name) %>Fail implements Action { 94 | readonly type = <%= classify(name) %>ActionTypes.Update<%= classify(name) %>Fail; 95 | constructor(public payload: { error: string }) {} 96 | } 97 | 98 | // ========================================= DELETE 99 | 100 | export class Delete<%= classify(name) %>ById implements Action { 101 | readonly type = <%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ById; 102 | constructor(public payload: { id: number }) {} 103 | } 104 | 105 | export class Delete<%= classify(name) %>ByIdSuccess implements Action { 106 | readonly type = <%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ByIdSuccess; 107 | constructor(public payload: { id: number }) {} 108 | } 109 | 110 | export class Delete<%= classify(name) %>ByIdFail implements Action { 111 | readonly type = <%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ByIdFail; 112 | constructor(public payload: { error: string }) {} 113 | } 114 | 115 | // ========================================= QUERY 116 | 117 | export class SetSearchQuery implements Action { 118 | readonly type = <%= classify(name) %>ActionTypes.SetSearchQuery; 119 | constructor(public payload: Partial<<%= classify(name) %>SearchQuery>) {} 120 | } 121 | 122 | // ========================================= SELECTED ID 123 | 124 | export class Select<%= classify(name) %>ById implements Action { 125 | readonly type = <%= classify(name) %>ActionTypes.Select<%= classify(name) %>ById; 126 | constructor(public payload: { id: number }) {} 127 | } 128 | 129 | export type <%= classify(name) %>Actions = 130 | | Create<%= classify(name) %> 131 | | Create<%= classify(name) %>Success 132 | | Create<%= classify(name) %>Fail 133 | | SearchAll<%= classify(name) %>Entities 134 | | SearchAll<%= classify(name) %>EntitiesSuccess 135 | | SearchAll<%= classify(name) %>EntitiesFail 136 | | Load<%= classify(name) %>ById 137 | | Load<%= classify(name) %>ByIdSuccess 138 | | Load<%= classify(name) %>ByIdFail 139 | | Update<%= classify(name) %> 140 | | Update<%= classify(name) %>Success 141 | | Update<%= classify(name) %>Fail 142 | | Delete<%= classify(name) %>ById 143 | | Delete<%= classify(name) %>ByIdSuccess 144 | | Delete<%= classify(name) %>ByIdFail 145 | | SetSearchQuery 146 | | Select<%= classify(name) %>ById; 147 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { provideMockActions } from '@ngrx/effects/testing'; 3 | import { cold, hot } from 'jasmine-marbles'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { 7 | Create<%= classify(name) %>, 8 | Create<%= classify(name) %>Success, 9 | Create<%= classify(name) %>Fail, 10 | SearchAll<%= classify(name) %>Entities, 11 | SearchAll<%= classify(name) %>EntitiesSuccess, 12 | SearchAll<%= classify(name) %>EntitiesFail, 13 | Load<%= classify(name) %>ById, 14 | Load<%= classify(name) %>ByIdSuccess, 15 | Load<%= classify(name) %>ByIdFail, 16 | Update<%= classify(name) %>, 17 | Update<%= classify(name) %>Success, 18 | Update<%= classify(name) %>Fail, 19 | Delete<%= classify(name) %>ById, 20 | Delete<%= classify(name) %>ByIdSuccess, 21 | Delete<%= classify(name) %>ByIdFail 22 | } from './<%= dasherize(name) %>.actions'; 23 | import { generate<%= classify(name) %>, generate<%= classify(name) %>Array } from './<%= dasherize(name) %>.model'; 24 | // TODO: Change this path when you move your service file: 25 | import { <%= classify(name) %>Service } from './<%= dasherize(name) %>.service'; 26 | import { <%= classify(name) %>Effects } from './<%= dasherize(name) %>.effects'; 27 | 28 | describe('<%= classify(name) %>Effects', () => { 29 | let actions: Observable; 30 | let effects: <%= classify(name) %>Effects; 31 | let service; 32 | 33 | beforeEach(() => { 34 | TestBed.configureTestingModule({ 35 | providers: [ 36 | <%= classify(name) %>Effects, 37 | provideMockActions(() => actions), 38 | { 39 | provide: <%= classify(name) %>Service, 40 | useValue: jasmine.createSpyObj('service', [ 41 | 'create', 42 | 'search', 43 | 'getById', 44 | 'update', 45 | 'deleteById' 46 | ]) 47 | } 48 | ] 49 | }); 50 | 51 | effects = TestBed.get(<%= classify(name) %>Effects); 52 | service = TestBed.get(<%= classify(name) %>Service); 53 | }); 54 | 55 | it('should be constructed', () => { 56 | expect(effects).toBeTruthy(); 57 | }); 58 | 59 | describe('create', () => { 60 | it('should return Create<%= classify(name) %>Success action with entity on success', () => { 61 | const entity = generate<%= classify(name) %>(); 62 | const insertAction = new Create<%= classify(name) %>({ <%= name %>: entity }); 63 | const successAction = new Create<%= classify(name) %>Success({ result: entity }); 64 | 65 | actions = hot('a-', { a: insertAction }); 66 | service.create.and.returnValue(cold('-e|', { e: entity })); 67 | const expected = cold('-s', { s: successAction }); 68 | 69 | expect(effects.create).toBeObservable(expected); 70 | }); 71 | 72 | it('should return Create<%= classify(name) %>Fail with error object on failure', () => { 73 | const entity = generate<%= classify(name) %>(); 74 | const insertAction = new Create<%= classify(name) %>({ <%= name %>: entity }); 75 | const failAction = new Create<%= classify(name) %>Fail({ error: 'fail' }); 76 | 77 | actions = hot('i-', { i: insertAction }); 78 | service.create.and.returnValue(cold('-#|', {}, { message: 'fail'})); 79 | const expected = cold('-f', { f: failAction }); 80 | 81 | expect(effects.create).toBeObservable(expected); 82 | }); 83 | }); 84 | 85 | describe('search', () => { 86 | it('should return SearchAll<%= classify(name) %>EntitiesSuccess action with entities on success', () => { 87 | const entities = generate<%= classify(name) %>Array(); 88 | const searchAction = new SearchAll<%= classify(name) %>Entities(); 89 | const successAction = new SearchAll<%= classify(name) %>EntitiesSuccess({ result: entities }); 90 | 91 | actions = hot('a-', { a: searchAction }); 92 | service.search.and.returnValue(cold('-e|', { e: entities })); 93 | const expected = cold('-s', { s: successAction }); 94 | 95 | expect(effects.search).toBeObservable(expected); 96 | }); 97 | 98 | it('should return SearchAll<%= classify(name) %>EntitiesFail with error object on failure', () => { 99 | const searchAction = new SearchAll<%= classify(name) %>Entities(); 100 | const failAction = new SearchAll<%= classify(name) %>EntitiesFail({ error: 'fail' }); 101 | 102 | actions = hot('a-', { a: searchAction }); 103 | service.search.and.returnValue(cold('-#|', {}, { message: 'fail'})); 104 | const expected = cold('-f', { f: failAction }); 105 | 106 | expect(effects.search).toBeObservable(expected); 107 | }); 108 | }); 109 | 110 | describe('loadById', () => { 111 | it('should return Load<%= classify(name) %>ByIdSuccess action with entity on success', () => { 112 | const entity = generate<%= classify(name) %>(); 113 | const loadAction = new Load<%= classify(name) %>ById({ id: entity.id }); 114 | const successAction = new Load<%= classify(name) %>ByIdSuccess({ result: entity}); 115 | 116 | actions = hot('a-', { a: loadAction }); 117 | service.getById.and.returnValue(cold('-e|', { e: entity })); 118 | const expected = cold('-s', { s: successAction }); 119 | 120 | expect(effects.loadById).toBeObservable(expected); 121 | }); 122 | 123 | it('should return Load<%= classify(name) %>ByIdFail with error object on failure', () => { 124 | const entity = generate<%= classify(name) %>(); 125 | const loadAction = new Load<%= classify(name) %>ById({ id: entity.id }); 126 | const failAction = new Load<%= classify(name) %>ByIdFail({ error: 'fail' }); 127 | 128 | actions = hot('a-', { a: loadAction }); 129 | service.getById.and.returnValue(cold('-#|', {}, { message: 'fail'})); 130 | const expected = cold('-f', { f: failAction }); 131 | 132 | expect(effects.loadById).toBeObservable(expected); 133 | }); 134 | }); 135 | 136 | describe('update', () => { 137 | it('should return Update<%= classify(name) %>Success action with entity on success', () => { 138 | const entity = generate<%= classify(name) %>(); 139 | const updateAction = new Update<%= classify(name) %>({ <%= name %>: entity }); 140 | const successAction = new Update<%= classify(name) %>Success({ update: { 141 | id: entity.id, 142 | changes: entity 143 | }}); 144 | 145 | actions = hot('a-', { a: updateAction }); 146 | service.update.and.returnValue(cold('-e|', { e: entity })); 147 | const expected = cold('-s', { s: successAction }); 148 | 149 | expect(effects.update).toBeObservable(expected); 150 | }); 151 | 152 | it('should return Update<%= classify(name) %>Fail with error object on failure', () => { 153 | const entity = generate<%= classify(name) %>(); 154 | const updateAction = new Update<%= classify(name) %>({ <%= name %>: entity }); 155 | const failAction = new Update<%= classify(name) %>Fail({ error: 'fail' }); 156 | 157 | actions = hot('a-', { a: updateAction }); 158 | service.update.and.returnValue(cold('-#|', {}, { message: 'fail'})); 159 | const expected = cold('-f', { f: failAction }); 160 | 161 | expect(effects.update).toBeObservable(expected); 162 | }); 163 | }); 164 | 165 | describe('delete', () => { 166 | it('should return Delete<%= classify(name) %>ByIdSuccess action with entity ID on success', () => { 167 | const entity = generate<%= classify(name) %>(); 168 | const deleteAction = new Delete<%= classify(name) %>ById({ id: entity.id }); 169 | const successAction = new Delete<%= classify(name) %>ByIdSuccess({ id: entity.id }); 170 | 171 | actions = hot('a-', { a: deleteAction }); 172 | service.deleteById.and.returnValue(cold('-e|', { e: entity.id })); 173 | const expected = cold('-s', { s: successAction }); 174 | 175 | expect(effects.delete).toBeObservable(expected); 176 | }); 177 | 178 | it('should return Delete<%= classify(name) %>ByIdFail with error object on failure', () => { 179 | const entity = generate<%= classify(name) %>(); 180 | const deleteAction = new Delete<%= classify(name) %>ById({ id: entity.id }); 181 | const failAction = new Delete<%= classify(name) %>ByIdFail({ error: 'fail' }); 182 | 183 | actions = hot('a-', { a: deleteAction }); 184 | service.deleteById.and.returnValue(cold('-#|', {}, { message: 'fail'})); 185 | const expected = cold('-f', { f: failAction }); 186 | 187 | expect(effects.delete).toBeObservable(expected); 188 | }); 189 | }); 190 | 191 | }); 192 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Observable, of } from 'rxjs'; 4 | import { 5 | exhaustMap, 6 | map, 7 | catchError, 8 | tap, 9 | switchMap 10 | } from 'rxjs/operators'; 11 | import { Actions, Effect, ofType } from '@ngrx/effects'; 12 | import { Action } from '@ngrx/store'; 13 | import { Update } from '@ngrx/entity'; 14 | 15 | import { 16 | <%= classify(name) %>ActionTypes, 17 | Create<%= classify(name) %>, 18 | Create<%= classify(name) %>Success, 19 | Create<%= classify(name) %>Fail, 20 | SearchAll<%= classify(name) %>Entities, 21 | SearchAll<%= classify(name) %>EntitiesSuccess, 22 | SearchAll<%= classify(name) %>EntitiesFail, 23 | Load<%= classify(name) %>ById, 24 | Load<%= classify(name) %>ByIdSuccess, 25 | Load<%= classify(name) %>ByIdFail, 26 | Update<%= classify(name) %>, 27 | Update<%= classify(name) %>Success, 28 | Update<%= classify(name) %>Fail, 29 | Delete<%= classify(name) %>ById, 30 | Delete<%= classify(name) %>ByIdSuccess, 31 | Delete<%= classify(name) %>ByIdFail, 32 | SetSearchQuery, 33 | Select<%= classify(name) %>ById 34 | } from './<%= dasherize(name) %>.actions'; 35 | import { <%= classify(name) %> } from './<%= dasherize(name) %>.model'; 36 | import { <%= classify(name) %>Service } from './<%= dasherize(name) %>.service'; 37 | 38 | @Injectable() 39 | export class <%= classify(name) %>Effects { 40 | 41 | // ========================================= CREATE 42 | @Effect() 43 | create: Observable = this.actions$ 44 | .pipe( 45 | ofType>(<%= classify(name) %>ActionTypes.Create<%= classify(name) %>), 46 | exhaustMap((action) => 47 | this.service.create(action.payload.<%= name %>).pipe( 48 | map((<%= name %>: <%= classify(name) %>) => new Create<%= classify(name) %>Success({ result: <%= name %> })), 49 | catchError(({ message }) => 50 | of(new Create<%= classify(name) %>Fail({ error: message })) 51 | ) 52 | ) 53 | ) 54 | ); 55 | 56 | // ========================================= SEARCH 57 | @Effect() 58 | search: Observable = this.actions$ 59 | .pipe( 60 | ofTypeEntities>(<%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>Entities), 61 | // Use the state's filtering and pagination values in this search call 62 | // here if desired: 63 | exhaustMap(() => 64 | this.service.search().pipe( 65 | map((entities: Array<<%= classify(name) %>>) => 66 | new SearchAll<%= classify(name) %>EntitiesSuccess({ result: entities }) 67 | ), 68 | catchError(({ message }) => 69 | of(new SearchAll<%= classify(name) %>EntitiesFail({ error: message })) 70 | ) 71 | ) 72 | ) 73 | ); 74 | 75 | // ========================================= LOAD BY ID 76 | @Effect() 77 | loadById: Observable = this.actions$ 78 | .pipe( 79 | ofTypeById>(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>ById), 80 | switchMap((action) => 81 | this.service.getById(action.payload.id).pipe( 82 | map((<%= name %>: <%= classify(name) %>) => new Load<%= classify(name) %>ByIdSuccess({ result: <%= name %> }) 83 | ), 84 | catchError(({ message }) => 85 | of(new Load<%= classify(name) %>ByIdFail({ error: message })) 86 | ) 87 | ) 88 | ) 89 | ); 90 | 91 | // ========================================= UPDATE 92 | @Effect() 93 | update: Observable = this.actions$ 94 | .pipe( 95 | ofType>(<%= classify(name) %>ActionTypes.Update<%= classify(name) %>), 96 | exhaustMap((action) => 97 | this.service.update(action.payload.<%= name %>).pipe( 98 | map((<%= name %>: <%= classify(name) %>) => 99 | new Update<%= classify(name) %>Success({ 100 | update: { 101 | id: <%= name %>.id, 102 | changes: <%= name %> 103 | } as Update<<%= classify(name) %>> 104 | }) 105 | ), 106 | catchError(({ message }) => 107 | of(new Update<%= classify(name) %>Fail({ error: message })) 108 | ) 109 | ) 110 | ) 111 | ); 112 | 113 | // ========================================= DELETE 114 | @Effect() 115 | delete: Observable = this.actions$ 116 | .pipe( 117 | ofTypeById>(<%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ById), 118 | exhaustMap((action) => 119 | this.service.deleteById(action.payload.id).pipe( 120 | map((id: number) => new Delete<%= classify(name) %>ByIdSuccess({ id })), 121 | catchError(({ message }) => 122 | of(new Delete<%= classify(name) %>ByIdFail({ error: message })) 123 | ) 124 | ) 125 | ) 126 | ); 127 | 128 | // ========================================= QUERY 129 | @Effect({ 130 | dispatch: false 131 | }) 132 | paging: Observable = this.actions$ 133 | .pipe( 134 | ofType(<%= classify(name) %>ActionTypes.SetSearchQuery), 135 | tap((action) => { 136 | // do stuff with: action.payload.limit & action.payload.page 137 | }) 138 | ); 139 | 140 | // ========================================= SELECTED ID 141 | @Effect({ 142 | dispatch: false 143 | }) 144 | selectedId: Observable = this.actions$ 145 | .pipe( 146 | ofTypeById>(<%= classify(name) %>ActionTypes.Select<%= classify(name) %>ById), 147 | tap((action) => { 148 | // do stuff with: action.payload.id 149 | }) 150 | ); 151 | 152 | constructor(private actions$: Actions, private service: <%= classify(name) %>Service) {} 153 | } 154 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.model.ts: -------------------------------------------------------------------------------- 1 | export interface <%= classify(name) %> { 2 | id: number; 3 | name: string; 4 | description: string; 5 | } 6 | 7 | // for testing 8 | 9 | export const generate<%= classify(name) %> = (idOverride?: number): <%= classify(name) %> => ({ 10 | id: idOverride || (Math.floor(Math.random() * 100) + 1), 11 | name: 'Test name', 12 | description: 'Test description' 13 | }); 14 | 15 | export const generate<%= classify(name) %>Array = (count = 10): <%= classify(name) %>[] => 16 | // Overwrite random id generation to prevent duplicate IDs: 17 | Array.apply(null, Array(count)).map((value, index) => generate<%= classify(name) %>(index + 1)); 18 | 19 | export const generate<%= classify(name) %>Map = ( 20 | <%= name %>Array: Array<<%= classify(name) %>> = generate<%= classify(name) %>Array() 21 | ): { ids: Array, entities: any } => ({ 22 | entities: <%= name %>Array.reduce( 23 | (<%= name %>Map, <%= name %>) => ({ ...<%= name %>Map, [<%= name %>.id]: <%= name %> }), 24 | {} 25 | ), 26 | ids: <%= name %>Array.map(<%= name %> => <%= name %>.id) 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | <%= classify(name) %>, 3 | generate<%= classify(name) %>, 4 | generate<%= classify(name) %>Map, 5 | generate<%= classify(name) %>Array 6 | } from './<%= dasherize(name) %>.model'; 7 | import * as actions from './<%= dasherize(name) %>.actions'; 8 | import { 9 | <%= name %>Reducer, 10 | initial<%= classify(name)%>State, 11 | getSelectedId, 12 | getLoading, 13 | getError, 14 | getQuery 15 | } from './<%= dasherize(name) %>.reducer'; 16 | import { Update } from '@ngrx/entity'; 17 | 18 | const INITIAL_STATE_WITH_ERROR = { 19 | ...initial<%= classify(name)%>State, 20 | error: 'some error' 21 | }; 22 | const BLANK_ERROR_MESSAGE = ''; 23 | 24 | describe('<%= name %>Reducer', () => { 25 | describe('upon an undefined action', () => { 26 | it('should return the default state upon an undefined action', () => { 27 | const action = { type: 'NOT DEFINED' } as any; 28 | 29 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual(initial<%= classify(name)%>State); 30 | }); 31 | }); 32 | 33 | describe('upon Create<%= classify(name) %>', () => { 34 | it('should set loading to true and clear any error', () => { 35 | const action = new actions.Create<%= classify(name) %>({ <%= name %>: generate<%= classify(name) %>() }); 36 | 37 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 38 | ...initial<%= classify(name)%>State, 39 | loading: true, 40 | error: BLANK_ERROR_MESSAGE 41 | }); 42 | }); 43 | }); 44 | 45 | describe('upon Create<%= classify(name) %>Success', () => { 46 | it('should add the given <%= classify(name) %>, set loading to false, and clear any error', () => { 47 | const result = generate<%= classify(name) %>(); 48 | const action = new actions.Create<%= classify(name) %>Success({ result }); 49 | 50 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 51 | ...initial<%= classify(name)%>State, 52 | ...generate<%= classify(name) %>Map([result]), 53 | loading: false, 54 | error: BLANK_ERROR_MESSAGE 55 | }); 56 | }); 57 | }); 58 | 59 | describe('upon Create<%= classify(name) %>Fail', () => { 60 | it('should set loading to true and echo the error', () => { 61 | const error = 'test create error'; 62 | const action = new actions.Create<%= classify(name) %>Fail({ error }); 63 | 64 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual({ 65 | ...initial<%= classify(name)%>State, 66 | loading: false, 67 | error: `<%= classify(name) %> create failed: ${error}` 68 | }); 69 | }); 70 | }); 71 | 72 | describe('upon SearchAll<%= classify(name) %>Entities', () => { 73 | it('should remove <%= classify(name) %> entities, set loading to true, and clear any error', () => { 74 | const initial<%= classify(name)%>StateWith<%= classify(name) %>Entities = { 75 | ...INITIAL_STATE_WITH_ERROR, 76 | ...generate<%= classify(name) %>Map() 77 | }; 78 | const action = new actions.SearchAll<%= classify(name) %>Entities(); 79 | 80 | expect(<%= name %>Reducer(initial<%= classify(name)%>StateWith<%= classify(name) %>Entities, action)).toEqual({ 81 | ...initial<%= classify(name)%>State, 82 | loading: true, 83 | error: BLANK_ERROR_MESSAGE 84 | }); 85 | }); 86 | }); 87 | 88 | describe('upon SearchAll<%= classify(name) %>EntitiesSuccess', () => { 89 | it('should add <%= classify(name) %> entities, set loading to false, and clear any error', () => { 90 | const result = generate<%= classify(name) %>Array(); 91 | const action = new actions.SearchAll<%= classify(name) %>EntitiesSuccess({ result }); 92 | 93 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 94 | ...initial<%= classify(name)%>State, 95 | ...generate<%= classify(name) %>Map(result), 96 | loading: false, 97 | error: BLANK_ERROR_MESSAGE 98 | }); 99 | }); 100 | }); 101 | 102 | describe('upon SearchAll<%= classify(name) %>EntitiesFail', () => { 103 | it('should set loading to false and echo the error', () => { 104 | const error = 'test search error'; 105 | const action = new actions.SearchAll<%= classify(name) %>EntitiesFail({ error }); 106 | 107 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual({ 108 | ...initial<%= classify(name)%>State, 109 | loading: false, 110 | error: `<%= classify(name) %> search failed: ${error}` 111 | }); 112 | }); 113 | }); 114 | 115 | describe('upon Load<%= classify(name) %>ById', () => { 116 | it('should remove <%= name %> entities, set selected id, and clear any error', () => { 117 | const id = 8675309; 118 | const initial<%= classify(name)%>StateWith<%= classify(name) %>Entities = { 119 | ...INITIAL_STATE_WITH_ERROR, 120 | ...generate<%= classify(name) %>Map() 121 | }; 122 | const action = new actions.Load<%= classify(name) %>ById({ id }); 123 | 124 | expect(<%= name %>Reducer(initial<%= classify(name)%>StateWith<%= classify(name) %>Entities, action)).toEqual({ 125 | ...initial<%= classify(name)%>State, 126 | selectedId: id, 127 | loading: true, 128 | error: BLANK_ERROR_MESSAGE 129 | }); 130 | }); 131 | }); 132 | 133 | describe('upon Load<%= classify(name) %>ByIdSuccess', () => { 134 | it('should add the given <%= classify(name) %>, set loading to false, and clear any error', () => { 135 | const result = generate<%= classify(name) %>(); 136 | const action = new actions.Load<%= classify(name) %>ByIdSuccess({ result }); 137 | 138 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 139 | ...initial<%= classify(name)%>State, 140 | ...generate<%= classify(name) %>Map([result]), 141 | loading: false, 142 | error: BLANK_ERROR_MESSAGE 143 | }); 144 | }); 145 | }); 146 | 147 | describe('upon Load<%= classify(name) %>ByIdFail', () => { 148 | it('should set loading to false and echo the error', () => { 149 | const error = 'test load by id error'; 150 | const action = new actions.Load<%= classify(name) %>ByIdFail({ error }); 151 | 152 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual({ 153 | ...initial<%= classify(name)%>State, 154 | loading: false, 155 | error: `<%= classify(name) %> load failed: ${error}` 156 | }); 157 | }); 158 | }); 159 | 160 | describe('upon Update<%= classify(name) %>', () => { 161 | it('should set loading to true and clear any errior', () => { 162 | const <%= name %> = generate<%= classify(name) %>(); 163 | const action = new actions.Update<%= classify(name) %>({ <%= name %> }); 164 | 165 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 166 | ...initial<%= classify(name)%>State, 167 | loading: true, 168 | error: BLANK_ERROR_MESSAGE 169 | }); 170 | }); 171 | }); 172 | 173 | describe('upon Update<%= classify(name) %>Success', () => { 174 | it('should add the given <%= classify(name) %>, set loading to false, and clear any error', () => { 175 | const <%= name %> = generate<%= classify(name) %>(); 176 | const initial<%= classify(name)%>StateWith<%= classify(name) %> = { 177 | ...INITIAL_STATE_WITH_ERROR, 178 | ...generate<%= classify(name) %>Map([<%= name %>]) 179 | }; 180 | const updated<%= classify(name) %> = { 181 | ...<%= name %>, 182 | name: <%= name %>.name + ' EDITED', 183 | description: <%= name %>.description + ' EDITED' 184 | }; 185 | const update = { 186 | id: updated<%= classify(name) %>.id, 187 | changes: updated<%= classify(name) %> 188 | } as Update<<%= classify(name) %>>; 189 | const action = new actions.Update<%= classify(name) %>Success({ update }); 190 | 191 | expect(<%= name %>Reducer(initial<%= classify(name)%>StateWith<%= classify(name) %>, action)).toEqual({ 192 | ...initial<%= classify(name)%>StateWith<%= classify(name) %>, 193 | ...generate<%= classify(name) %>Map([updated<%= classify(name) %>]), 194 | loading: false, 195 | error: BLANK_ERROR_MESSAGE 196 | }); 197 | }); 198 | }); 199 | 200 | describe('upon Update<%= classify(name) %>Fail', () => { 201 | it('should set loading to false and echo the error', () => { 202 | const error = 'test update error'; 203 | const action = new actions.Update<%= classify(name) %>Fail({ error }); 204 | 205 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual({ 206 | ...initial<%= classify(name)%>State, 207 | loading: false, 208 | error: `<%= classify(name) %> update failed: ${error}` 209 | }); 210 | }); 211 | }); 212 | 213 | describe('upon Delete<%= classify(name) %>ById', () => { 214 | it('should set the id, set loading to true, and clear any error', () => { 215 | const id = 4815162342; 216 | const action = new actions.Delete<%= classify(name) %>ById({ id }); 217 | 218 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 219 | ...initial<%= classify(name)%>State, 220 | selectedId: id, 221 | loading: true, 222 | error: BLANK_ERROR_MESSAGE 223 | }); 224 | }); 225 | }); 226 | 227 | describe('upon Delete<%= classify(name) %>ByIdSuccess', () => { 228 | it('should remove the id-given <%= name %>, set loading to false, and clear any error', () => { 229 | const id = 18009453669; 230 | const <%= name %>ToBeRemoved = generate<%= classify(name) %>(id); 231 | const expected<%= classify(name) %>Entities = generate<%= classify(name) %>Array(); 232 | const <%= name %>EntitiesWith<%= classify(name) %>ToBeRemoved = [ 233 | ...expected<%= classify(name) %>Entities, 234 | <%= name %>ToBeRemoved 235 | ]; 236 | const initial<%= classify(name)%>StateWithAll<%= classify(name) %>Entities = { 237 | ...INITIAL_STATE_WITH_ERROR, 238 | ...generate<%= classify(name) %>Map(<%= name %>EntitiesWith<%= classify(name) %>ToBeRemoved) 239 | }; 240 | const action = new actions.Delete<%= classify(name) %>ByIdSuccess({ id }); 241 | 242 | expect( 243 | <%= name %>Reducer(initial<%= classify(name)%>StateWithAll<%= classify(name) %>Entities, action) 244 | ).toEqual({ 245 | ...initial<%= classify(name)%>StateWithAll<%= classify(name) %>Entities, 246 | ...generate<%= classify(name) %>Map(expected<%= classify(name) %>Entities), 247 | loading: false, 248 | error: BLANK_ERROR_MESSAGE 249 | }); 250 | }); 251 | }); 252 | 253 | describe('upon Delete<%= classify(name) %>ByIdFail', () => { 254 | it('should set loading to false and echo the error', () => { 255 | const error = 'test delete error'; 256 | const action = new actions.Delete<%= classify(name) %>ByIdFail({ error }); 257 | 258 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual({ 259 | ...initial<%= classify(name)%>State, 260 | loading: false, 261 | error: `<%= classify(name) %> delete failed: ${error}` 262 | }); 263 | }); 264 | }); 265 | 266 | describe('upon SetSearchQuery', () => { 267 | it('should set the query', () => { 268 | const query = { 269 | filter: 'someFilter', 270 | sorting: 'someSort', 271 | limit: 1000000000000, 272 | page: 888888 273 | }; 274 | const action = new actions.SetSearchQuery(query); 275 | 276 | expect(<%= name %>Reducer(initial<%= classify(name)%>State, action)).toEqual({ 277 | ...initial<%= classify(name)%>State, 278 | query 279 | }); 280 | }); 281 | }); 282 | 283 | describe('upon Select<%= classify(name) %>ById', () => { 284 | it('should set the id and clear any error', () => { 285 | const id = 73; 286 | const action = new actions.Select<%= classify(name) %>ById({ id }); 287 | 288 | expect(<%= name %>Reducer(INITIAL_STATE_WITH_ERROR, action)).toEqual({ 289 | ...initial<%= classify(name)%>State, 290 | selectedId: id, 291 | error: BLANK_ERROR_MESSAGE 292 | }); 293 | }); 294 | }); 295 | }); 296 | 297 | describe('getters', () => { 298 | describe('getSelectedId', () => { 299 | it('should return the selected id', () => { 300 | expect(getSelectedId(initial<%= classify(name)%>State)).toEqual(initial<%= classify(name)%>State.selectedId); 301 | }); 302 | }); 303 | describe('getLoading', () => { 304 | it('should return the selected id', () => { 305 | expect(getLoading(initial<%= classify(name)%>State)).toEqual(initial<%= classify(name)%>State.loading); 306 | }); 307 | }); 308 | describe('getError', () => { 309 | it('should return the selected id', () => { 310 | expect(getError(INITIAL_STATE_WITH_ERROR)) 311 | .toEqual(INITIAL_STATE_WITH_ERROR.error); 312 | }); 313 | }); 314 | describe('getQuery', () => { 315 | it('should return the selected id', () => { 316 | expect(getQuery(initial<%= classify(name)%>State)) 317 | .toEqual(initial<%= classify(name)%>State.query); 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 2 | import { <%= classify(name) %> } from './<%= dasherize(name) %>.model'; 3 | import { <%= classify(name) %>Actions, <%= classify(name) %>ActionTypes } from './<%= dasherize(name) %>.actions'; 4 | 5 | export interface <%= classify(name) %>SearchQuery { 6 | filter: string; 7 | sorting: string; 8 | limit: number; 9 | page: number; 10 | } 11 | 12 | export interface <%= classify(name) %>State extends EntityState<<%= classify(name) %>> { 13 | // additional entities state properties 14 | selectedId: number; 15 | loading: boolean; 16 | error: string; 17 | query: <%= classify(name) %>SearchQuery; 18 | } 19 | 20 | export const <%= name %>Adapter: EntityAdapter<<%= classify(name) %>> = createEntityAdapter<<%= classify(name) %>>(); 21 | 22 | export const initial<%= classify(name)%>State: <%= classify(name) %>State = <%= name %>Adapter.getInitialState({ 23 | // additional <%= name %> state properties 24 | selectedId: null, 25 | loading: false, 26 | error: '', 27 | query: { 28 | filter: '', 29 | sorting: '', 30 | limit: 999, 31 | page: 1 32 | } 33 | }); 34 | 35 | export function <%= name %>Reducer(state = initial<%= classify(name)%>State, action: <%= classify(name) %>Actions): <%= classify(name) %>State { 36 | switch (action.type) { 37 | case <%= classify(name) %>ActionTypes.Create<%= classify(name) %>: 38 | return { 39 | ...state, 40 | loading: true, 41 | error: '' 42 | }; 43 | 44 | case <%= classify(name) %>ActionTypes.Create<%= classify(name) %>Success: 45 | return { 46 | ...<%= name %>Adapter.addOne(action.payload.result, state), 47 | loading: false, 48 | error: '' 49 | }; 50 | 51 | case <%= classify(name) %>ActionTypes.Create<%= classify(name) %>Fail: 52 | return { 53 | ...state, 54 | loading: false, 55 | error: '<%= classify(name) %> create failed: ' + action.payload.error 56 | }; 57 | 58 | case <%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>Entities: 59 | return { 60 | ...<%= name %>Adapter.removeAll(state), 61 | loading: true, 62 | error: '' 63 | }; 64 | 65 | case <%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>EntitiesSuccess: 66 | return { 67 | ...<%= name %>Adapter.addAll(action.payload.result, state), 68 | loading: false, 69 | error: '' 70 | }; 71 | 72 | case <%= classify(name) %>ActionTypes.SearchAll<%= classify(name) %>EntitiesFail: 73 | return { 74 | ...state, 75 | loading: false, 76 | error: '<%= classify(name) %> search failed: ' + action.payload.error 77 | }; 78 | 79 | case <%= classify(name) %>ActionTypes.Load<%= classify(name) %>ById: 80 | return { 81 | ...<%= name %>Adapter.removeAll(state), 82 | selectedId: action.payload.id, 83 | loading: true, 84 | error: '' 85 | }; 86 | 87 | case <%= classify(name) %>ActionTypes.Load<%= classify(name) %>ByIdSuccess: 88 | return { 89 | ...<%= name %>Adapter.addOne(action.payload.result, state), 90 | loading: false, 91 | error: '' 92 | }; 93 | 94 | case <%= classify(name) %>ActionTypes.Load<%= classify(name) %>ByIdFail: 95 | return { 96 | ...state, 97 | loading: false, 98 | error: '<%= classify(name) %> load failed: ' + action.payload.error 99 | }; 100 | 101 | case <%= classify(name) %>ActionTypes.Update<%= classify(name) %>: 102 | return { 103 | ...state, 104 | loading: true, 105 | error: '' 106 | }; 107 | 108 | case <%= classify(name) %>ActionTypes.Update<%= classify(name) %>Success: 109 | return { 110 | ...<%= name %>Adapter.updateOne(action.payload.update, state), 111 | loading: false, 112 | error: '' 113 | }; 114 | 115 | case <%= classify(name) %>ActionTypes.Update<%= classify(name) %>Fail: 116 | return { 117 | ...state, 118 | loading: false, 119 | error: '<%= classify(name) %> update failed: ' + action.payload.error 120 | }; 121 | 122 | case <%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ById: 123 | return { 124 | ...state, 125 | selectedId: action.payload.id, 126 | loading: true, 127 | error: '' 128 | }; 129 | 130 | case <%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ByIdSuccess: 131 | return { 132 | ...<%= name %>Adapter.removeOne(action.payload.id, state), 133 | loading: false, 134 | error: '' 135 | }; 136 | 137 | case <%= classify(name) %>ActionTypes.Delete<%= classify(name) %>ByIdFail: 138 | return { 139 | ...state, 140 | loading: false, 141 | error: '<%= classify(name) %> delete failed: ' + action.payload.error 142 | }; 143 | 144 | case <%= classify(name) %>ActionTypes.SetSearchQuery: 145 | return { 146 | ...state, 147 | query: { 148 | ...state.query, 149 | ...action.payload 150 | } 151 | }; 152 | 153 | case <%= classify(name) %>ActionTypes.Select<%= classify(name) %>ById: 154 | return { 155 | ...state, 156 | selectedId: action.payload.id, 157 | error: '' 158 | }; 159 | 160 | default: 161 | return state; 162 | } 163 | } 164 | 165 | export const getSelectedId = (state: <%= classify(name) %>State) => state.selectedId; 166 | export const getLoading = (state: <%= classify(name) %>State) => state.loading; 167 | export const getError = (state: <%= classify(name) %>State) => state.error; 168 | export const getQuery = (state: <%= classify(name) %>State) => state.query; 169 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/__name@dasherize__.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * TODO: 3 | * This file should not remain in the state folder. Move it to somewhere within 4 | * your app code. 5 | */ 6 | 7 | import { Injectable } from '@angular/core'; 8 | import { HttpClient } from '@angular/common/http'; 9 | 10 | import { Observable, of } from 'rxjs'; 11 | import { switchMap } from 'rxjs/operators'; 12 | import { <%= classify(name) %> } from './<%= dasherize(name) %>.model'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class <%= classify(name) %>Service { 18 | BASE_URL = 'api/'; 19 | 20 | constructor(private httpClient: HttpClient) {} 21 | 22 | create(<%= name %>: <%= classify(name) %>): Observable<<%= classify(name) %>> { 23 | return this.httpClient.post<<%= classify(name) %>>(`${this.BASE_URL}<%= name %>`, { 24 | ...<%= name %>, 25 | // We clear out the ID to indicate that this should be a new entry: 26 | id: null 27 | }); 28 | } 29 | 30 | search(): Observable>> { 31 | // TODO: get based on state.paging (filter, sorting, page, limit) 32 | return this.httpClient.get>>(`${this.BASE_URL}<%= name %>`); 33 | } 34 | 35 | getById(id: number): Observable<<%= classify(name) %>> { 36 | return this.httpClient.get<<%= classify(name) %>>(`${this.BASE_URL}<%= name %>/${id}`); 37 | } 38 | 39 | update(<%= name %>: <%= classify(name) %>): Observable<<%= classify(name) %>> { 40 | return this.httpClient 41 | .put<<%= classify(name) %>>(`${this.BASE_URL}<%= name %>/${<%= name %>.id}`, <%= name %>) 42 | // The following pipe can be removed if your backend service returns the 43 | // edited value: 44 | .pipe(switchMap(() => of(<%= name %>))); 45 | } 46 | 47 | deleteById(id: number): Observable { 48 | return this.httpClient.delete(`${this.BASE_URL}<%= name %>/${id}`) 49 | // The following pipe can be removed if your backend service returns the 50 | // ID or body of the deleted entity: 51 | .pipe(switchMap(() => of(id))); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | current<%= classify(name) %>Id, 3 | current<%= classify(name) %>, 4 | <%= name %>Loading, 5 | <%= name %>Error, 6 | <%= name %>Query 7 | } from './index'; 8 | import { <%= classify(name) %> } from './<%= name %>.model'; 9 | 10 | const create<%= classify(name) %> = ({ id = 0, name = '', description = '' } = {}): <%= classify(name) %> => ({ 11 | id: id, 12 | name: name || 'name', 13 | description: description || `description` 14 | }); 15 | 16 | // State Factory 17 | const create<%= classify(name) %>sState = ({ 18 | entities = { 19 | '1': create<%= classify(name) %>({ id: 1, name: 'Bob' }), 20 | '2': create<%= classify(name) %>({ id: 2, name: 'Sue' }), 21 | '3': create<%= classify(name) %>({ id: 3, name: 'Mary' }) 22 | }, 23 | ids = ['1', '2', '3'], 24 | selectedId = 1, 25 | loading = false, 26 | error = '', 27 | query = null 28 | } = {}) => ({ 29 | <%= name %>: { 30 | ids, 31 | entities, 32 | selectedId, 33 | loading, 34 | error, 35 | query 36 | } 37 | }); 38 | 39 | let state; 40 | 41 | describe('<%= name %>Selectors', () => { 42 | beforeEach(() => { 43 | state = create<%= classify(name) %>sState(); 44 | }); 45 | 46 | it('current<%= classify(name) %>Id', () => { 47 | expect(current<%= classify(name) %>Id(state)).toEqual(1); 48 | }); 49 | 50 | it('current<%= classify(name) %>', () => { 51 | expect(current<%= classify(name) %>(state)).toEqual(state.<%= name %>.entities[1]); 52 | }); 53 | 54 | it('<%= name %>Loading', () => { 55 | state.<%= name %>.loading = true; 56 | expect(<%= name %>Loading(state)).toEqual(state.<%= name %>.loading); 57 | }); 58 | 59 | it('<%= name %>Error', () => { 60 | state.<%= name %>.error = 'error loading <%= name %>s'; 61 | expect(<%= name %>Error(state)).toEqual(state.<%= name %>.error); 62 | }); 63 | 64 | it('<%= name %>Query', () => { 65 | state.<%= name %>.query = 'page=2'; 66 | expect(<%= name %>Query(state)).toEqual(state.<%= name %>.query); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/__name@dasherize@if-flat__/index.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createFeatureSelector } from '@ngrx/store'; 2 | 3 | import { 4 | <%= name %>Adapter, 5 | getSelectedId, 6 | getLoading, 7 | getError, 8 | getQuery 9 | } from './<%= dasherize(name) %>.reducer'; 10 | import { <%= classify(name) %>State } from './<%= dasherize(name) %>.reducer'; 11 | 12 | export const get<%= classify(name) %>State = createFeatureSelector<<%= classify(name) %>State>('<%= name %>'); 13 | 14 | export const { 15 | selectIds: <%= name %>Ids, 16 | selectEntities: <%= name %>Entities, 17 | selectAll: <%= name %>, 18 | selectTotal: <%= name %>Count 19 | } = <%= name %>Adapter.getSelectors(get<%= classify(name) %>State); 20 | 21 | export const current<%= classify(name) %>Id = createSelector( 22 | get<%= classify(name) %>State, 23 | getSelectedId 24 | ); 25 | 26 | export const current<%= classify(name) %> = createSelector( 27 | current<%= classify(name) %>Id, 28 | <%= name %>Entities, 29 | (selected<%= classify(name) %>Id, entities) => 30 | selected<%= classify(name) %>Id && entities[selected<%= classify(name) %>Id] 31 | ); 32 | 33 | export const <%= name %>Loading = createSelector( // TODO: Need to pluraliae name 34 | get<%= classify(name) %>State, 35 | getLoading 36 | ); 37 | 38 | export const <%= name %>Error = createSelector( 39 | get<%= classify(name) %>State, 40 | getError 41 | ); 42 | 43 | export const <%= name %>Query = createSelector( 44 | get<%= classify(name) %>State, 45 | getQuery 46 | ); 47 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/app.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { RouterReducerState } from '@ngrx/router-store'; 2 | import { RouterStateUrl } from './state-utils'; 3 | import { <%= classify(name) %>State } from './<%= dasherize(name) %>/<%= dasherize(name) %>.reducer'; 4 | 5 | export interface AppState { 6 | router: RouterReducerState; 7 | <%= name %>: <%= classify(name) %>State; 8 | } 9 | 10 | export type State = AppState; 11 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/app.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap, MetaReducer } from '@ngrx/store'; 2 | import { routerReducer } from '@ngrx/router-store'; 3 | 4 | import { storeFreeze } from 'ngrx-store-freeze'; 5 | 6 | import { AppState } from './app.interfaces'; 7 | import { environment } from '../../environments/environment'; 8 | import { <%= name %>Reducer } from './<%= dasherize(name) %>/<%= dasherize(name) %>.reducer'; 9 | 10 | export const appReducer: ActionReducerMap = { 11 | <%= name %>: <%= name %>Reducer, 12 | router: routerReducer 13 | }; 14 | 15 | export const appMetaReducers: MetaReducer[] = !environment.production 16 | ? [storeFreeze] 17 | : []; 18 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/state-utils.ts: -------------------------------------------------------------------------------- 1 | import { Params, RouterStateSnapshot } from '@angular/router'; 2 | import { RouterStateSerializer } from '@ngrx/router-store'; 3 | 4 | /** 5 | * The RouterStateSerializer takes the current RouterStateSnapshot 6 | * and returns any pertinent information needed. The snapshot contains 7 | * all information about the state of the router at the given point in time. 8 | * The entire snapshot is complex and not always needed. In this case, you only 9 | * need the URL and query parameters from the snapshot in the store. Other items could be 10 | * returned such as route parameters and static route data. 11 | */ 12 | 13 | export interface RouterStateUrl { 14 | url: string; 15 | queryParams: Params; 16 | } 17 | 18 | export class CustomRouterStateSerializer 19 | implements RouterStateSerializer { 20 | serialize(routerState: RouterStateSnapshot): RouterStateUrl { 21 | const { url } = routerState; 22 | const queryParams = routerState.root.queryParams; 23 | return { url, queryParams }; 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/ngrx-entity/__files__/state.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 4 | import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; 5 | 6 | import { EffectsModule } from '@ngrx/effects'; 7 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 8 | import { StoreModule } from '@ngrx/store'; 9 | 10 | import { appMetaReducers, appReducer } from './app.reducer'; 11 | import { CustomRouterStateSerializer } from './state-utils'; 12 | import { environment } from '../../environments/environment'; 13 | import { <%= classify(name) %>Effects } from './<%= dasherize(name) %>/<%= dasherize(name) %>.effects'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | CommonModule, 18 | HttpClientModule, 19 | StoreRouterConnectingModule, 20 | StoreModule.forRoot(appReducer, { metaReducers: appMetaReducers }), 21 | EffectsModule.forRoot([<%= classify(name) %>Effects]), 22 | !environment.production ? StoreDevtoolsModule.instrument() : [] 23 | ], 24 | declarations: [] 25 | }) 26 | export class StateModule { 27 | static forRoot(): ModuleWithProviders { 28 | return { 29 | ngModule: StateModule, 30 | providers: [ 31 | /** 32 | * The `RouterStateSnapshot` provided by the `Router` is a large complex structure. 33 | * A custom RouterStateSerializer is used to parse the `RouterStateSnapshot` provided 34 | * by `@ngrx/router-store` to include only the desired pieces of the snapshot. 35 | */ 36 | { provide: RouterStateSerializer, useClass: CustomRouterStateSerializer } 37 | ] 38 | }; 39 | } 40 | 41 | constructor( 42 | @Optional() 43 | @SkipSelf() 44 | parentModule: StateModule 45 | ) { 46 | if (parentModule) { 47 | throw new Error('StateModule is already loaded. Import it in the AppModule only'); 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/ngrx-entity/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from "@angular-devkit/schematics"; 2 | import { NgRxOptions } from "./utility/util"; 3 | export default function (options: NgRxOptions): Rule; 4 | -------------------------------------------------------------------------------- /src/ngrx-entity/index.ts: -------------------------------------------------------------------------------- 1 | import { strings } from "@angular-devkit/core"; 2 | import { NodePackageInstallTask } from "@angular-devkit/schematics/tasks"; 3 | import { 4 | Rule, 5 | SchematicContext, 6 | Tree, 7 | chain, 8 | url, 9 | apply, 10 | move, 11 | template, 12 | filter, 13 | noop, 14 | SchematicsException, 15 | mergeWith 16 | } from "@angular-devkit/schematics"; 17 | import { 18 | NgRxOptions, 19 | getLatestNodeVersion, 20 | NodePackage 21 | } from "./utility/util"; 22 | import { parseName } from "./utility/parseName"; 23 | import { of, Observable, concat } from "rxjs"; 24 | import { map, concatMap } from "rxjs/operators"; 25 | import { 26 | NodeDependencyType, 27 | addPackageJsonDependency 28 | } from "./utility/dependencies"; 29 | 30 | export default function(options: NgRxOptions): Rule { 31 | return (tree: Tree, context: SchematicContext) => { 32 | return chain([addNgRxFiles(options), updateDependencies()])(tree, context); 33 | }; 34 | } 35 | 36 | function updateDependencies(): Rule { 37 | return (tree: Tree, context: SchematicContext): Observable => { 38 | context.logger.debug("Updating dependencies..."); 39 | context.addTask(new NodePackageInstallTask()); 40 | 41 | const addDependencies = of( 42 | "@ngrx/store", 43 | "@ngrx/entity", 44 | "@ngrx/effects", 45 | "@ngrx/router-store" 46 | ).pipe( 47 | concatMap((packageName: string) => getLatestNodeVersion(packageName)), 48 | map((packageFromRegistry: NodePackage) => { 49 | const { name, version } = packageFromRegistry; 50 | context.logger.debug( 51 | `Adding ${name}:${version} to ${NodeDependencyType.Default}` 52 | ); 53 | 54 | addPackageJsonDependency(tree, { 55 | type: NodeDependencyType.Default, 56 | name, 57 | version 58 | }); 59 | 60 | return tree; 61 | }) 62 | ); 63 | 64 | const addDevDependencies = of( 65 | "@ngrx/store-devtools", 66 | "ngrx-store-freeze", 67 | "jasmine-marbles" 68 | ).pipe( 69 | concatMap((packageName: string) => getLatestNodeVersion(packageName)), 70 | map((packageFromRegistry: NodePackage) => { 71 | const { name, version } = packageFromRegistry; 72 | context.logger.debug( 73 | `Adding ${name}:${version} to ${NodeDependencyType.Dev}` 74 | ); 75 | 76 | addPackageJsonDependency(tree, { 77 | type: NodeDependencyType.Dev, 78 | name, 79 | version 80 | }); 81 | 82 | return tree; 83 | }) 84 | ); 85 | 86 | return concat(addDependencies, addDevDependencies); 87 | }; 88 | } 89 | 90 | function addNgRxFiles(options: NgRxOptions): Rule { 91 | return (tree: Tree, context: SchematicContext) => { 92 | if (!options.name) { 93 | throw new SchematicsException("Entity name is required"); 94 | } 95 | 96 | if (!options.path) { 97 | // todo: determine default based on current working dir 98 | options.path = `./src/app/state`; 99 | console.log(`No Entity path specified, adding files to ${options.path}`); 100 | } 101 | 102 | context.logger.debug(`adding NgRX files to ${options.path} dir`); 103 | 104 | const parsedPath = parseName(options.path, options.name); 105 | options.name = parsedPath.name; 106 | options.path = parsedPath.path; 107 | 108 | const templateSource = apply(url("./__files__"), [ 109 | options.init && !tree.exists(`${parsedPath.path}/app.interfaces.ts`) 110 | ? noop() 111 | : filter(path => !path.endsWith("app.interfaces.ts")), 112 | options.init && !tree.exists(`${parsedPath.path}/app.reducer.ts`) 113 | ? noop() 114 | : filter(path => !path.endsWith("app.reducer.ts")), 115 | options.init && !tree.exists(`${parsedPath.path}/state-utils.ts`) 116 | ? noop() 117 | : filter(path => !path.endsWith("state-utils.ts")), 118 | options.init && !tree.exists(`${parsedPath.path}/state.module.ts`) 119 | ? noop() 120 | : filter(path => !path.endsWith("state.module.ts")), 121 | template({ 122 | ...strings, 123 | "if-flat": (s: string) => (options.flat ? "" : s), 124 | ...options 125 | }), 126 | move(options.path) 127 | ]); 128 | 129 | return chain([mergeWith(templateSource)])( 130 | tree, 131 | context 132 | ); 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/ngrx-entity/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | /** 3 | * The name of the Entity. 4 | */ 5 | name: string; 6 | /** 7 | * The path to create the entity files. 8 | */ 9 | path?: string; 10 | /** 11 | * Should setup NgRx. 12 | */ 13 | init?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /src/ngrx-entity/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "BriebugSchematicsNgRxEntity", 4 | "title": "NgRx Generator Options Schema", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "The name of the entity.", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "minLength": 1, 15 | "x-prompt": "Enter the entity name (camelCase || dasherized)" 16 | }, 17 | "path": { 18 | "type": "string", 19 | "format": "path", 20 | "description": "The path to create the entity files", 21 | "visible": false, 22 | "alias": "p", 23 | "x-prompt": "Enter the path at which to create the entity files" 24 | }, 25 | "init": { 26 | "type": "boolean", 27 | "description": "Flag to setup NgRx.", 28 | "default": false, 29 | "x-prompt": "Add initalization files?" 30 | } 31 | }, 32 | "required": [] 33 | } 34 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/dependencies.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | * https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/dependencies.ts 8 | */ 9 | import { Tree } from "@angular-devkit/schematics"; 10 | export declare enum pkgJson { 11 | Path = "/package.json" 12 | } 13 | export declare enum NodeDependencyType { 14 | Default = "dependencies", 15 | Dev = "devDependencies", 16 | Peer = "peerDependencies", 17 | Optional = "optionalDependencies" 18 | } 19 | export interface NodeDependency { 20 | type: NodeDependencyType; 21 | name: string; 22 | version: string; 23 | overwrite?: boolean; 24 | } 25 | export interface DeleteNodeDependency { 26 | type: NodeDependencyType; 27 | name: string; 28 | } 29 | export declare function addPackageJsonDependency(tree: Tree, dependency: NodeDependency): void; 30 | export declare function getPackageJsonDependency(tree: Tree, name: string): NodeDependency | null; 31 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/dependencies.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | * https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/dependencies.ts 8 | */ 9 | 10 | import { Tree } from "@angular-devkit/schematics"; 11 | import { 12 | appendPropertyInAstObject, 13 | findPropertyInAstObject, 14 | insertPropertyInAstObjectInOrder 15 | } from "./json-utils"; 16 | import { parseJsonAtPath } from "./util"; 17 | 18 | export enum pkgJson { 19 | Path = "/package.json" 20 | } 21 | 22 | export enum NodeDependencyType { 23 | Default = "dependencies", 24 | Dev = "devDependencies", 25 | Peer = "peerDependencies", 26 | Optional = "optionalDependencies" 27 | } 28 | 29 | export interface NodeDependency { 30 | type: NodeDependencyType; 31 | name: string; 32 | version: string; 33 | overwrite?: boolean; 34 | } 35 | 36 | export interface DeleteNodeDependency { 37 | type: NodeDependencyType; 38 | name: string; 39 | } 40 | 41 | export function addPackageJsonDependency( 42 | tree: Tree, 43 | dependency: NodeDependency 44 | ): void { 45 | const packageJsonAst = parseJsonAtPath(tree, pkgJson.Path); 46 | const depsNode = findPropertyInAstObject(packageJsonAst, dependency.type); 47 | const recorder = tree.beginUpdate(pkgJson.Path); 48 | 49 | if (!depsNode) { 50 | // Haven't found the dependencies key, add it to the root of the package.json. 51 | appendPropertyInAstObject( 52 | recorder, 53 | packageJsonAst, 54 | dependency.type, 55 | { 56 | [dependency.name]: dependency.version 57 | }, 58 | 4 59 | ); 60 | } else if (depsNode.kind === "object") { 61 | // check if package already added 62 | const depNode = findPropertyInAstObject(depsNode, dependency.name); 63 | 64 | if (!depNode) { 65 | // Package not found, add it. 66 | insertPropertyInAstObjectInOrder( 67 | recorder, 68 | depsNode, 69 | dependency.name, 70 | dependency.version, 71 | 4 72 | ); 73 | 74 | } else if (dependency.overwrite) { 75 | // Package found, update version if overwrite. 76 | const { end, start } = depNode; 77 | recorder.remove(start.offset, end.offset - start.offset); 78 | recorder.insertRight(start.offset, JSON.stringify(dependency.version)); 79 | } 80 | } 81 | 82 | tree.commitUpdate(recorder); 83 | } 84 | 85 | export function getPackageJsonDependency( 86 | tree: Tree, 87 | name: string 88 | ): NodeDependency | null { 89 | const packageJson = parseJsonAtPath(tree, pkgJson.Path); 90 | let dep: NodeDependency | null = null; 91 | [ 92 | NodeDependencyType.Default, 93 | NodeDependencyType.Dev, 94 | NodeDependencyType.Optional, 95 | NodeDependencyType.Peer 96 | ].forEach(depType => { 97 | if (dep !== null) { 98 | return; 99 | } 100 | const depsNode = findPropertyInAstObject(packageJson, depType); 101 | if (depsNode !== null && depsNode.kind === "object") { 102 | const depNode = findPropertyInAstObject(depsNode, name); 103 | if (depNode !== null && depNode.kind === "string") { 104 | const version = depNode.value; 105 | dep = { 106 | type: depType, 107 | name: name, 108 | version: version 109 | }; 110 | } 111 | } 112 | }); 113 | 114 | return dep; 115 | } 116 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/json-utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | * https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/json-utils.ts 8 | */ 9 | import { JsonAstArray, JsonAstNode, JsonAstObject, JsonValue } from "@angular-devkit/core"; 10 | import { UpdateRecorder } from "@angular-devkit/schematics"; 11 | export declare function appendPropertyInAstObject(recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue, indent: number): void; 12 | export declare function insertPropertyInAstObjectInOrder(recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue, indent: number): void; 13 | export declare function appendValueInAstArray(recorder: UpdateRecorder, node: JsonAstArray, value: JsonValue, indent?: number): void; 14 | export declare function findPropertyInAstObject(node: JsonAstObject, propertyName: string): JsonAstNode | null; 15 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/json-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | * https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/json-utils.ts 8 | */ 9 | import { 10 | JsonAstArray, 11 | JsonAstKeyValue, 12 | JsonAstNode, 13 | JsonAstObject, 14 | JsonValue 15 | } from "@angular-devkit/core"; 16 | import { UpdateRecorder } from "@angular-devkit/schematics"; 17 | 18 | export function appendPropertyInAstObject( 19 | recorder: UpdateRecorder, 20 | node: JsonAstObject, 21 | propertyName: string, 22 | value: JsonValue, 23 | indent: number 24 | ) { 25 | const indentStr = _buildIndent(indent); 26 | 27 | if (node.properties.length > 0) { 28 | // Insert comma. 29 | const last = node.properties[node.properties.length - 1]; 30 | recorder.insertRight( 31 | last.start.offset + last.text.replace(/\s+$/, "").length, 32 | "," 33 | ); 34 | } 35 | 36 | recorder.insertLeft( 37 | node.end.offset - 1, 38 | " " + 39 | `"${propertyName}": ${JSON.stringify(value, null, 2).replace( 40 | /\n/g, 41 | indentStr 42 | )}` + 43 | indentStr.slice(0, -2) 44 | ); 45 | } 46 | 47 | export function insertPropertyInAstObjectInOrder( 48 | recorder: UpdateRecorder, 49 | node: JsonAstObject, 50 | propertyName: string, 51 | value: JsonValue, 52 | indent: number 53 | ) { 54 | console.log(`Package not found, add it. Adding ${propertyName}:${value}`); 55 | 56 | if (node.properties.length === 0) { 57 | console.log("node.properties.length === 0"); 58 | appendPropertyInAstObject(recorder, node, propertyName, value, indent); 59 | 60 | return; 61 | } 62 | 63 | // Find insertion info. 64 | let insertAfterProp: JsonAstKeyValue | null = null; 65 | let prev: JsonAstKeyValue | null = null; 66 | let isLastProp = false; 67 | const last = node.properties[node.properties.length - 1]; 68 | for (const prop of node.properties) { 69 | if (prop.key.value > propertyName) { 70 | if (prev) { 71 | insertAfterProp = prev; 72 | } 73 | break; 74 | } 75 | if (prop === last) { 76 | isLastProp = true; 77 | insertAfterProp = last; 78 | } 79 | prev = prop; 80 | } 81 | 82 | if (isLastProp) { 83 | console.log("isLastProp"); 84 | appendPropertyInAstObject(recorder, node, propertyName, value, indent); 85 | 86 | return; 87 | } 88 | 89 | const indentStr = _buildIndent(indent); 90 | 91 | const insertIndex = 92 | insertAfterProp === null 93 | ? node.start.offset + 1 94 | : insertAfterProp.end.offset + 1; 95 | 96 | recorder.insertRight( 97 | insertIndex, 98 | `${indentStr}` + 99 | `"${propertyName}": ${JSON.stringify(value, null, 2).replace( 100 | /\n/g, 101 | indentStr 102 | )}` + 103 | "," 104 | ); 105 | } 106 | 107 | export function appendValueInAstArray( 108 | recorder: UpdateRecorder, 109 | node: JsonAstArray, 110 | value: JsonValue, 111 | indent = 4 112 | ) { 113 | const indentStr = _buildIndent(indent); 114 | 115 | if (node.elements.length > 0) { 116 | // Insert comma. 117 | const last = node.elements[node.elements.length - 1]; 118 | recorder.insertRight( 119 | last.start.offset + last.text.replace(/\s+$/, "").length, 120 | "," 121 | ); 122 | } 123 | 124 | recorder.insertLeft( 125 | node.end.offset - 1, 126 | " " + 127 | JSON.stringify(value, null, 2).replace(/\n/g, indentStr) + 128 | indentStr.slice(0, -2) 129 | ); 130 | } 131 | 132 | export function findPropertyInAstObject( 133 | node: JsonAstObject, 134 | propertyName: string 135 | ): JsonAstNode | null { 136 | let maybeNode: JsonAstNode | null = null; 137 | for (const property of node.properties) { 138 | if (property.key.value == propertyName) { 139 | maybeNode = property.value; 140 | } 141 | } 142 | 143 | return maybeNode; 144 | } 145 | 146 | function _buildIndent(count: number): string { 147 | return "\n" + new Array(count + 1).join(" "); 148 | } 149 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/parseName.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | * source: https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/parse-name.ts 8 | */ 9 | import { Path } from '@angular-devkit/core'; 10 | export interface Location { 11 | name: string; 12 | path: Path; 13 | } 14 | export declare function parseName(path: string, name: string): Location; 15 | export declare const camelize: (word: string, splitBy?: string) => string; 16 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/parseName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | * source: https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/parse-name.ts 8 | */ 9 | // import { relative, Path } from "../../../angular_devkit/core/src/virtual-fs"; 10 | import { Path, basename, dirname, normalize } from '@angular-devkit/core'; 11 | 12 | export interface Location { 13 | name: string; 14 | path: Path; 15 | } 16 | 17 | export function parseName(path: string, name: string): Location { 18 | const nameWithoutPath = basename(name as Path); 19 | const namePath = dirname((path + '/' + name) as Path); 20 | 21 | return { 22 | // entity name input expects camelCase or dasherized name (customerOrder || customer-order) 23 | name: camelize(nameWithoutPath, '-'), 24 | path: normalize('/' + namePath), 25 | }; 26 | } 27 | 28 | export const camelize = (word: string, splitBy: string = '-') => { 29 | const parts = word.split(splitBy); 30 | 31 | return !parts.length 32 | ? word 33 | : parts[0] + 34 | parts 35 | .slice(1) 36 | .map((part) => part[0].toUpperCase() + part.slice(1)) 37 | .join(''); 38 | }; 39 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/util.d.ts: -------------------------------------------------------------------------------- 1 | import { experimental, JsonAstObject } from "@angular-devkit/core"; 2 | import { Tree, SchematicContext } from "@angular-devkit/schematics"; 3 | import { DeleteNodeDependency } from "./dependencies"; 4 | export interface NodePackage { 5 | name: string; 6 | version: string; 7 | } 8 | export declare enum Paths { 9 | AngularJson = "./angular.json" 10 | } 11 | export declare enum Configs { 12 | JsonIndentLevel = 4 13 | } 14 | export declare type WorkspaceSchema = experimental.workspace.WorkspaceSchema; 15 | export interface JestOptions { 16 | updateTests?: boolean; 17 | project?: string; 18 | config?: "file" | "packagejson" | string; 19 | overwrite?: boolean; 20 | __version__: number; 21 | } 22 | export declare function getAngularVersion(tree: Tree): number; 23 | export declare function getWorkspacePath(host: Tree): string; 24 | export declare function getWorkspace(host: Tree): WorkspaceSchema; 25 | export declare function getSourcePath(tree: Tree, options: any): String; 26 | export declare function removePackageJsonDependency(tree: Tree, dependency: DeleteNodeDependency): void; 27 | export declare function safeFileDelete(tree: Tree, path: string): boolean; 28 | export declare function addPropertyToPackageJson(tree: Tree, context: SchematicContext, propertyName: string, propertyValue: { 29 | [key: string]: string; 30 | }): void; 31 | export declare function getWorkspaceConfig(tree: Tree, options: JestOptions): { 32 | projectProps: any; 33 | workspacePath: string; 34 | workspace: experimental.workspace.WorkspaceSchema; 35 | projectName: any; 36 | }; 37 | /** 38 | * Angular5 (angular-cli.json) config is formatted into an array of applications vs Angular6's (angular.json) object mapping 39 | * multi-app Angular5 apps are currently not supported. 40 | * 41 | * @param tree 42 | * @param options 43 | */ 44 | export declare function isMultiAppV5(tree: Tree, options: JestOptions): boolean; 45 | /** 46 | * Attempt to retrieve the latest package version from NPM 47 | * Return an optional "latest" version in case of error 48 | * @param packageName 49 | */ 50 | export declare function getLatestNodeVersion(packageName: string): Promise; 51 | export declare function parseJsonAtPath(tree: Tree, path: string): JsonAstObject; 52 | export interface NgRxOptions { 53 | name: string; 54 | path?: string; 55 | init?: boolean; 56 | flat?: boolean; 57 | } 58 | -------------------------------------------------------------------------------- /src/ngrx-entity/utility/util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonParseMode, 3 | experimental, 4 | parseJson, 5 | join, 6 | Path, 7 | parseJsonAst, 8 | JsonAstObject 9 | } from "@angular-devkit/core"; 10 | 11 | import { 12 | SchematicsException, 13 | Tree, 14 | SchematicContext 15 | } from "@angular-devkit/schematics"; 16 | 17 | import { 18 | pkgJson, 19 | DeleteNodeDependency, 20 | getPackageJsonDependency 21 | } from "./dependencies"; 22 | 23 | import { 24 | findPropertyInAstObject, 25 | appendPropertyInAstObject, 26 | insertPropertyInAstObjectInOrder 27 | } from "./json-utils"; 28 | 29 | import { get } from "http"; 30 | 31 | export interface NodePackage { 32 | name: string; 33 | version: string; 34 | } 35 | 36 | export enum Paths { 37 | AngularJson = "./angular.json" 38 | } 39 | 40 | export enum Configs { 41 | JsonIndentLevel = 4 42 | } 43 | 44 | export type WorkspaceSchema = experimental.workspace.WorkspaceSchema; 45 | 46 | export interface JestOptions { 47 | updateTests?: boolean; 48 | project?: string; 49 | config?: "file" | "packagejson" | string; 50 | overwrite?: boolean; 51 | __version__: number; 52 | } 53 | 54 | export function getAngularVersion(tree: Tree): number { 55 | const packageNode = getPackageJsonDependency(tree, "@angular/core"); 56 | 57 | const version = 58 | packageNode && 59 | packageNode.version.split("").find(char => !!parseInt(char, 10)); 60 | 61 | return version ? +version : 0; 62 | } 63 | 64 | export function getWorkspacePath(host: Tree): string { 65 | const possibleFiles = [ 66 | "/angular.json", 67 | "/.angular.json", 68 | "/angular-cli.json" 69 | ]; 70 | const path = possibleFiles.filter(path => host.exists(path))[0]; 71 | 72 | return path; 73 | } 74 | 75 | export function getWorkspace(host: Tree): WorkspaceSchema { 76 | const path = getWorkspacePath(host); 77 | const configBuffer = host.read(path); 78 | if (configBuffer === null) { 79 | throw new SchematicsException(`Could not find (${path})`); 80 | } 81 | const content = configBuffer.toString(); 82 | 83 | return (parseJson(content, JsonParseMode.Loose) as {}) as WorkspaceSchema; 84 | } 85 | 86 | export function getSourcePath(tree: Tree, options: any): String { 87 | const workspace = getWorkspace(tree); 88 | 89 | if (!options.project) { 90 | throw new SchematicsException('Option "project" is required.'); 91 | } 92 | 93 | const project = workspace.projects[options.project]; 94 | 95 | if (project.projectType !== "application") { 96 | throw new SchematicsException( 97 | `AddJest requires a project type of "application".` 98 | ); 99 | } 100 | 101 | // const assetPath = join(project.root as Path, 'src', 'assets'); 102 | const sourcePath = join(project.root as Path, "src"); 103 | 104 | return sourcePath; 105 | } 106 | 107 | // modified version from utility/dependencies/getPackageJsonDependency 108 | export function removePackageJsonDependency( 109 | tree: Tree, 110 | dependency: DeleteNodeDependency 111 | ): void { 112 | const packageJsonAst = parseJsonAtPath(tree, pkgJson.Path); 113 | const depsNode = findPropertyInAstObject(packageJsonAst, dependency.type); 114 | const recorder = tree.beginUpdate(pkgJson.Path); 115 | 116 | if (!depsNode) { 117 | // Haven't found the dependencies key. 118 | new SchematicsException("Could not find the package.json dependency"); 119 | } else if (depsNode.kind === "object") { 120 | const fullPackageString = depsNode.text.split("\n").filter(pkg => { 121 | return pkg.includes(`"${dependency.name}"`); 122 | })[0]; 123 | 124 | const commaDangle = 125 | fullPackageString && fullPackageString.trim().slice(-1) === "," ? 1 : 0; 126 | 127 | const packageAst = depsNode.properties.find(node => { 128 | return node.key.value.toLowerCase() === dependency.name.toLowerCase(); 129 | }); 130 | 131 | // TODO: does this work for the last dependency? 132 | const newLineIndentation = 5; 133 | 134 | if (packageAst) { 135 | // Package found, remove it. 136 | const end = packageAst.end.offset + commaDangle; 137 | 138 | recorder.remove( 139 | packageAst.key.start.offset, 140 | end - packageAst.start.offset + newLineIndentation 141 | ); 142 | } 143 | } 144 | 145 | tree.commitUpdate(recorder); 146 | } 147 | 148 | export function safeFileDelete(tree: Tree, path: string): boolean { 149 | if (tree.exists(path)) { 150 | tree.delete(path); 151 | return true; 152 | } else { 153 | return false; 154 | } 155 | } 156 | 157 | export function addPropertyToPackageJson( 158 | tree: Tree, 159 | context: SchematicContext, 160 | propertyName: string, 161 | propertyValue: { [key: string]: string } 162 | ) { 163 | const packageJsonAst = parseJsonAtPath(tree, pkgJson.Path); 164 | const pkgNode = findPropertyInAstObject(packageJsonAst, propertyName); 165 | const recorder = tree.beginUpdate(pkgJson.Path); 166 | 167 | if (!pkgNode) { 168 | // outer node missing, add key/value 169 | appendPropertyInAstObject( 170 | recorder, 171 | packageJsonAst, 172 | propertyName, 173 | propertyValue, 174 | Configs.JsonIndentLevel 175 | ); 176 | } else if (pkgNode.kind === "object") { 177 | // property exists, update values 178 | for (let [key, value] of Object.entries(propertyValue)) { 179 | const innerNode = findPropertyInAstObject(pkgNode, key); 180 | 181 | if (!innerNode) { 182 | // script not found, add it 183 | context.logger.debug(`creating ${key} with ${value}`); 184 | 185 | insertPropertyInAstObjectInOrder( 186 | recorder, 187 | pkgNode, 188 | key, 189 | value, 190 | Configs.JsonIndentLevel 191 | ); 192 | } else { 193 | // script found, overwrite value 194 | context.logger.debug(`overwriting ${key} with ${value}`); 195 | 196 | const { end, start } = innerNode; 197 | 198 | recorder.remove(start.offset, end.offset - start.offset); 199 | recorder.insertRight(start.offset, JSON.stringify(value)); 200 | } 201 | } 202 | } 203 | 204 | tree.commitUpdate(recorder); 205 | } 206 | 207 | export function getWorkspaceConfig(tree: Tree, options: JestOptions) { 208 | const workspace = getWorkspace(tree); 209 | const workspacePath = getWorkspacePath(tree); 210 | let projectName; 211 | let projectProps; 212 | 213 | if (options.__version__ >= 6) { 214 | projectName = options.project || workspace.defaultProject || ""; 215 | projectProps = workspace.projects[projectName]; 216 | } else if (options.__version__ < 6) { 217 | projectName = (workspace as any).project.name || ""; 218 | projectProps = (workspace as any).apps[0]; 219 | } 220 | 221 | return { projectProps, workspacePath, workspace, projectName }; 222 | } 223 | 224 | /** 225 | * Angular5 (angular-cli.json) config is formatted into an array of applications vs Angular6's (angular.json) object mapping 226 | * multi-app Angular5 apps are currently not supported. 227 | * 228 | * @param tree 229 | * @param options 230 | */ 231 | export function isMultiAppV5(tree: Tree, options: JestOptions) { 232 | const config = getWorkspaceConfig(tree, options); 233 | 234 | return options.__version__ < 6 && (config.workspace as any).apps.length > 1; 235 | } 236 | 237 | /** 238 | * Attempt to retrieve the latest package version from NPM 239 | * Return an optional "latest" version in case of error 240 | * @param packageName 241 | */ 242 | export function getLatestNodeVersion( 243 | packageName: string 244 | ): Promise { 245 | const DEFAULT_VERSION = "latest"; 246 | 247 | return new Promise(resolve => { 248 | return get(`http://registry.npmjs.org/${packageName}`, res => { 249 | let rawData = ""; 250 | res.on("data", chunk => (rawData += chunk)); 251 | res.on("end", () => { 252 | try { 253 | const response = JSON.parse(rawData); 254 | const version = (response && response["dist-tags"]) || {}; 255 | 256 | resolve(buildPackage(packageName, version.latest)); 257 | } catch (e) { 258 | resolve(buildPackage(packageName)); 259 | } 260 | }); 261 | }).on("error", () => resolve(buildPackage(packageName))); 262 | }); 263 | 264 | function buildPackage( 265 | name: string, 266 | version: string = DEFAULT_VERSION 267 | ): NodePackage { 268 | return { name, version }; 269 | } 270 | } 271 | 272 | export function parseJsonAtPath(tree: Tree, path: string): JsonAstObject { 273 | const buffer = tree.read(path); 274 | 275 | if (buffer === null) { 276 | throw new SchematicsException("Could not read package.json."); 277 | } 278 | 279 | const content = buffer.toString(); 280 | 281 | const json = parseJsonAst(content, JsonParseMode.Strict); 282 | if (json.kind != "object") { 283 | throw new SchematicsException( 284 | "Invalid package.json. Was expecting an object" 285 | ); 286 | } 287 | 288 | return json; 289 | } 290 | 291 | export interface NgRxOptions { 292 | name: string; 293 | path?: string; 294 | init?: boolean; 295 | flat?: boolean; 296 | } 297 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "lib": ["es2017", "dom"], 5 | "declaration": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noUnusedParameters": false, 13 | "noUnusedLocals": false, 14 | "rootDir": "src/", 15 | "skipDefaultLibCheck": true, 16 | "skipLibCheck": true, 17 | "inlineSourceMap": true, 18 | "strictNullChecks": true, 19 | "target": "es6", 20 | "types": ["node", "jasmine"] 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules", "src/*/__files__/**/*"] 24 | } 25 | --------------------------------------------------------------------------------