├── .babelrc ├── .gitignore ├── FullArchitecture.svg ├── FullArchitectureLessMongo.svg ├── FullArchitectureLessNode.svg ├── LICENSE ├── README.md ├── business_logic ├── commands │ ├── createProductCommand.js │ ├── deleteOrderCommand.js │ ├── deleteProductCommand.js │ └── shipOrderItemCommand.js ├── rules │ ├── canDeleteCategoryRule.js │ ├── canDeleteCustomerRule.js │ ├── canDeleteProductRule.js │ ├── canShipOrderItemRule.js │ ├── canSubmitOrderItemRule.js │ ├── fieldLengthRule.js │ ├── fieldRequiredRule.js │ ├── fieldTypeRule.js │ ├── orderItemAmountValidityRule.js │ ├── orderItemPriceValidityRule.js │ ├── validOrderItemStatusForDeleteRule.js │ ├── validOrderItemStatusForUpdateRule.js │ └── validOrderStatusForUpdateRule.js ├── services │ ├── baseService.js │ ├── categoryService.js │ ├── clientOrderItemService.js │ ├── clientOrderService.js │ ├── clientProductService.js │ ├── customerService.js │ ├── inventoryItemService.js │ ├── orderItemService.js │ ├── orderService.js │ └── productService.js └── shared │ ├── concurrencyError.js │ ├── notFoundError.js │ └── utils.js ├── client ├── angular │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.e2e.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app-routing.module.spec.ts │ │ ├── app-routing.module.ts │ │ ├── app │ │ │ ├── SocketManager.ts │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── category │ │ │ │ ├── category-detail │ │ │ │ │ ├── category-detail-viewmodel.ts │ │ │ │ │ ├── category-detail.component.css │ │ │ │ │ ├── category-detail.component.html │ │ │ │ │ ├── category-detail.component.spec.ts │ │ │ │ │ └── category-detail.component.ts │ │ │ │ └── category-list │ │ │ │ │ ├── category-list-viewmodel.ts │ │ │ │ │ ├── category-list.component.css │ │ │ │ │ ├── category-list.component.html │ │ │ │ │ ├── category-list.component.spec.ts │ │ │ │ │ └── category-list.component.ts │ │ │ ├── component-base.ts │ │ │ ├── contracts.ts │ │ │ ├── customer │ │ │ │ ├── customer-detail │ │ │ │ │ ├── customer-detail-viewmodel.ts │ │ │ │ │ ├── customer-detail.component.css │ │ │ │ │ ├── customer-detail.component.html │ │ │ │ │ ├── customer-detail.component.spec.ts │ │ │ │ │ └── customer-detail.component.ts │ │ │ │ ├── customer-list │ │ │ │ │ ├── customer-list-viewmodel.ts │ │ │ │ │ ├── customer-list.component.css │ │ │ │ │ ├── customer-list.component.html │ │ │ │ │ ├── customer-list.component.spec.ts │ │ │ │ │ └── customer-list.component.ts │ │ │ │ └── custtest │ │ │ │ │ ├── custtest.component.css │ │ │ │ │ ├── custtest.component.html │ │ │ │ │ ├── custtest.component.spec.ts │ │ │ │ │ └── custtest.component.ts │ │ │ ├── data-proxies │ │ │ │ ├── cache │ │ │ │ │ ├── cache-data-proxy-base.ts │ │ │ │ │ ├── category-cache-data-proxy.ts │ │ │ │ │ ├── customer-cache-data-proxy.ts │ │ │ │ │ ├── inventory-cache-data-proxy.ts │ │ │ │ │ ├── order-cache-data-proxy.ts │ │ │ │ │ ├── order-item-cache-data-proxy.ts │ │ │ │ │ └── product-cache-data-proxy.ts │ │ │ │ ├── data-proxy-factory.ts │ │ │ │ └── http │ │ │ │ │ ├── category-data-proxy.ts │ │ │ │ │ ├── customer-data-proxy.ts │ │ │ │ │ ├── http-data-proxy-base.ts │ │ │ │ │ ├── inventory-data-proxy.ts │ │ │ │ │ ├── order-data-proxy.ts │ │ │ │ │ ├── order-item-data-proxy.ts │ │ │ │ │ └── product-data-proxy.ts │ │ │ ├── entity-view-model-base.ts │ │ │ ├── event-aggregators │ │ │ │ ├── category-event-aggregator.ts │ │ │ │ ├── customer-event-aggregator.ts │ │ │ │ ├── event-aggregator.ts │ │ │ │ ├── event-emitter.ts │ │ │ │ ├── inventory-event-aggregator.ts │ │ │ │ ├── order-event-aggregator.ts │ │ │ │ ├── order-item-event-aggregator.ts │ │ │ │ └── product-event-aggregator.ts │ │ │ ├── inventory │ │ │ │ ├── inventory-detail │ │ │ │ │ ├── inventory-detail-viewmodel.ts │ │ │ │ │ ├── inventory-detail.component.css │ │ │ │ │ ├── inventory-detail.component.html │ │ │ │ │ ├── inventory-detail.component.spec.ts │ │ │ │ │ └── inventory-detail.component.ts │ │ │ │ └── inventory-list │ │ │ │ │ ├── inventory-list-viewmodel.ts │ │ │ │ │ ├── inventory-list.component.css │ │ │ │ │ ├── inventory-list.component.html │ │ │ │ │ ├── inventory-list.component.spec.ts │ │ │ │ │ └── inventory-list.component.ts │ │ │ ├── list-view-model-base.ts │ │ │ ├── notification-messenger.ts │ │ │ ├── order-item │ │ │ │ ├── order-item-detail │ │ │ │ │ ├── order-item-detail-viewmodel.ts │ │ │ │ │ ├── order-item-detail.component.css │ │ │ │ │ ├── order-item-detail.component.html │ │ │ │ │ ├── order-item-detail.component.spec.ts │ │ │ │ │ └── order-item-detail.component.ts │ │ │ │ ├── order-item-list │ │ │ │ │ ├── order-item-list-viewmodel.ts │ │ │ │ │ ├── order-item-list.component.css │ │ │ │ │ ├── order-item-list.component.html │ │ │ │ │ ├── order-item-list.component.spec.ts │ │ │ │ │ └── order-item-list.component.ts │ │ │ │ └── order-item-viewmodel.ts │ │ │ ├── order │ │ │ │ ├── order-detail │ │ │ │ │ ├── order-detail-viewmodel.ts │ │ │ │ │ ├── order-detail.component.css │ │ │ │ │ ├── order-detail.component.html │ │ │ │ │ ├── order-detail.component.spec.ts │ │ │ │ │ └── order-detail.component.ts │ │ │ │ └── order-list │ │ │ │ │ ├── order-list-viewmodel.ts │ │ │ │ │ ├── order-list.component.css │ │ │ │ │ ├── order-list.component.html │ │ │ │ │ ├── order-list.component.spec.ts │ │ │ │ │ └── order-list.component.ts │ │ │ ├── product │ │ │ │ ├── product-detail │ │ │ │ │ ├── product-detail-viewmodel.ts │ │ │ │ │ ├── product-detail.component.css │ │ │ │ │ ├── product-detail.component.html │ │ │ │ │ ├── product-detail.component.spec.ts │ │ │ │ │ └── product-detail.component.ts │ │ │ │ └── product-list │ │ │ │ │ ├── product-list-viewmodel.ts │ │ │ │ │ ├── product-list.component.css │ │ │ │ │ ├── product-list.component.html │ │ │ │ │ ├── product-list.component.spec.ts │ │ │ │ │ └── product-list.component.ts │ │ │ ├── rules │ │ │ │ ├── canDeleteCategoryRule.ts │ │ │ │ ├── canDeleteCustomerRule.ts │ │ │ │ ├── canSubmitOrderRule.ts │ │ │ │ ├── fieldLengthRule.ts │ │ │ │ ├── fieldRequiredRule.ts │ │ │ │ ├── fieldTypeRule.ts │ │ │ │ ├── name-rule.ts │ │ │ │ ├── orderItemAmountValidityRule.ts │ │ │ │ ├── orderItemPriceValidityRule.ts │ │ │ │ └── validOrderItemStatusForUpdateRule.ts │ │ │ ├── services │ │ │ │ ├── category.service.spec.ts │ │ │ │ ├── category.service.ts │ │ │ │ ├── customer.service.spec.ts │ │ │ │ ├── customer.service.ts │ │ │ │ ├── inventory.service.spec.ts │ │ │ │ ├── inventory.service.ts │ │ │ │ ├── order-item.service.spec.ts │ │ │ │ ├── order-item.service.ts │ │ │ │ ├── order.service.spec.ts │ │ │ │ ├── order.service.ts │ │ │ │ ├── product.service.spec.ts │ │ │ │ └── product.service.ts │ │ │ ├── stores │ │ │ │ ├── category-store.ts │ │ │ │ ├── customer-store.ts │ │ │ │ ├── inventory-store.ts │ │ │ │ ├── order-item-store.ts │ │ │ │ ├── order-store.ts │ │ │ │ ├── product-store.ts │ │ │ │ ├── store-manager.ts │ │ │ │ └── store.ts │ │ │ └── view-model-base.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 └── react │ ├── .babelrc │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── actions │ │ ├── ActionsBase.js │ │ ├── asyncStatusActions.js │ │ ├── categoryActions.js │ │ ├── customerActions.js │ │ ├── inventoryItemActions.js │ │ ├── orderActions.js │ │ ├── orderItemActions.js │ │ └── productActions.js │ ├── businessLogic.js │ ├── commandInvoker.js │ ├── components │ │ ├── App.js │ │ ├── category │ │ │ ├── CategoriesView.js │ │ │ ├── CategoryForm.js │ │ │ └── ManageCategory.js │ │ ├── common │ │ │ ├── Header.js │ │ │ ├── ListViewBase.js │ │ │ ├── ManageEntityBase.js │ │ │ ├── SelectInput.js │ │ │ └── TextInput.js │ │ ├── customer │ │ │ ├── CustomerForm.js │ │ │ ├── CustomersView.js │ │ │ └── ManageCustomer.js │ │ ├── inventory │ │ │ ├── InventoryItemForm.js │ │ │ ├── InventoryItemsView.js │ │ │ └── ManageInventoryItem.js │ │ ├── order │ │ │ ├── ManageOrder.js │ │ │ ├── OrderForm.js │ │ │ └── OrdersView.js │ │ ├── orderItem │ │ │ ├── ManageOrderItem.js │ │ │ ├── OrderItemForm.js │ │ │ └── OrderItemsView.js │ │ └── product │ │ │ ├── ManageProduct.js │ │ │ ├── ProductForm.js │ │ │ └── ProductsView.js │ ├── constants.js │ ├── index.css │ ├── index.html │ ├── index.js │ ├── reducers │ │ ├── asyncStatusReducer.js │ │ ├── categoryReducer.js │ │ ├── customerReducer.js │ │ ├── index.js │ │ ├── inventoryItemReducer.js │ │ ├── orderItemReducer.js │ │ ├── orderReducer.js │ │ └── productReducer.js │ ├── routes.js │ ├── serviceWorker.js │ ├── store │ │ └── configureStore.js │ └── viewModels │ │ ├── categoryViewModel.js │ │ ├── customerViewModel.js │ │ ├── inventoryItemViewModel.js │ │ ├── orderItemLineItemViewModel.js │ │ ├── orderItemViewModel.js │ │ ├── orderLineItemViewModel.js │ │ ├── orderViewModel.js │ │ ├── productViewModel.js │ │ └── viewModelBase.js │ └── webpack.config.js ├── data_proxies ├── http │ ├── categoryDataProxy.js │ ├── customerDataProxy.js │ ├── httpDataProxy.js │ ├── httpDataProxyFactory.js │ ├── inventoryItemDataProxy.js │ ├── orderDataProxy.js │ ├── orderItemDataProxy.js │ └── productDataProxy.js ├── in-memory │ ├── inMemoryDataProxy.js │ ├── inMemoryDataProxyFactory.js │ ├── inventoryItemDataProxy.js │ ├── orderDataProxy.js │ ├── orderItemDataProxy.js │ └── productDataProxy.js └── mongo │ ├── categoryDataProxy.js │ ├── customerDataProxy.js │ ├── inventoryItemDataProxy.js │ ├── mongoDataProxy.js │ ├── mongoDataProxyFactory.js │ ├── orderDataProxy.js │ ├── orderItemDataProxy.js │ └── productDataProxy.js ├── dist ├── favicon.ico ├── img │ ├── angular.png │ ├── express.png │ ├── mongo.png │ ├── node.png │ ├── peasy.png │ ├── plain.png │ └── react.png └── index.html ├── package-lock.json ├── package.json ├── screenflow.gif ├── server ├── applyMiddleware.js ├── applySettings.js ├── routeHelper.js ├── server.js └── wireUpRoutes.js ├── spec ├── business_logic │ ├── commands │ │ ├── createProductCommandSpec.js │ │ ├── deleteOrderCommandSpec.js │ │ ├── deleteProductCommandSpec.js │ │ └── shipOrderItemCommandSpec.js │ ├── rules │ │ ├── canDeleteCategoryRuleSpec.js │ │ ├── canDeleteCustomerRuleSpec.js │ │ ├── canDeleteProductRuleSpec.js │ │ ├── canShipOrderItemRuleSpec.js │ │ ├── canSubmitOrderItemRuleSpec.js │ │ ├── fieldLengthRuleSpec.js │ │ ├── fieldRequiredRuleSpec.js │ │ ├── fieldTypeRuleSpec.js │ │ ├── orderItemAmountValidityRuleSpec.js │ │ ├── orderItemPriceValidityRuleSpec.js │ │ ├── validOrderItemStatusForDeleteRuleSpec.js │ │ ├── validOrderItemStatusForUpdateRuleSpec.js │ │ └── validOrderStatusForUpdateRuleSpec.js │ └── services │ │ ├── baseServiceSpec.js │ │ ├── categoryServiceSpec.js │ │ ├── customerServiceSpec.js │ │ ├── inventoryItemServiceSpec.js │ │ ├── orderItemServiceSpec.js │ │ ├── orderServiceSpec.js │ │ └── productServiceSpec.js └── support │ └── jasmine.json └── test.sh /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | client/angular/node_modules 3 | client/react/node_modules 4 | dist/angular 5 | dist/react 6 | .DS_Store 7 | npm-debug.log 8 | lib/ 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Aaron Hanusa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /business_logic/commands/deleteProductCommand.js: -------------------------------------------------------------------------------- 1 | var Command = require('peasy-js').Command; 2 | var CanDeleteProductRule = require('../rules/canDeleteProductRule'); 3 | 4 | var DeleteProductCommand = Command.extend({ 5 | params: ['productId', 'productDataProxy', 'orderService', 'inventoryItemService', 'eventPublisher'], 6 | functions: { 7 | _getRules: function(productId, productDataProxy, orderService, inventoryItemService, eventPublisher, context, done) { 8 | done(null, new CanDeleteProductRule(this.productId, this.orderService)); 9 | }, 10 | _onValidationSuccess: function(productId, productDataProxy, orderService, inventoryItemService, eventPublisher, context, done) { 11 | var inventoryItemService = this.inventoryItemService; 12 | var productDataProxy = this.productDataProxy; 13 | var productId = this.productId; 14 | var eventPublisher = this.eventPublisher || { publish: () => {} }; 15 | inventoryItemService.getByProductCommand(this.productId).execute(function(err, result) { 16 | if (err) { return done(err); } 17 | inventoryItemService.destroyCommand(result.value.id).execute(function(err, result) { 18 | if (err) { return done(err); } 19 | productDataProxy.destroy(productId, function(err, result) { 20 | if (err) { return done(err); } 21 | eventPublisher.publish({ 22 | type: 'destroy', 23 | data: { id: productId } 24 | }); 25 | done(); 26 | }); 27 | }); 28 | }); 29 | } 30 | } 31 | }); 32 | 33 | module.exports = DeleteProductCommand; 34 | -------------------------------------------------------------------------------- /business_logic/rules/canDeleteCategoryRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var CanDeleteCategoryRule = Rule.extend({ 4 | params: ['categoryId', 'productService'], 5 | functions: { 6 | _onValidate: function(categoryId, productService, done) { 7 | var self = this; 8 | productService.getByCategoryCommand(categoryId).execute(function(err, result) { 9 | if (err) { return done(err); } 10 | if (result.value && result.value.length > 0) { 11 | self._invalidate("This category is associated with one or more products and cannot be deleted"); 12 | } 13 | done(); 14 | }); 15 | } 16 | } 17 | }); 18 | 19 | module.exports = CanDeleteCategoryRule; 20 | -------------------------------------------------------------------------------- /business_logic/rules/canDeleteCustomerRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var CanDeleteCustomerRule = Rule.extend({ 4 | params: ['customerId', 'orderService'], 5 | functions: { 6 | _onValidate: function(customerId, orderService, done) { 7 | var self = this; 8 | orderService.getByCustomerCommand(customerId).execute(function(err, result) { 9 | if (err) { return done(err); } 10 | if (result.value && result.value.length > 0) { 11 | self._invalidate("This customer is associated with one or more orders and cannot be deleted"); 12 | } 13 | done(result.errors); 14 | }); 15 | } 16 | } 17 | }); 18 | 19 | module.exports = CanDeleteCustomerRule; 20 | -------------------------------------------------------------------------------- /business_logic/rules/canDeleteProductRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var CanDeleteProductRule = Rule.extend({ 4 | params: ['productId', 'orderService'], 5 | functions: { 6 | _onValidate: function(productId, orderService, done) { 7 | var self = this; 8 | this.orderService.getByProductCommand(this.productId).execute(function(err, result) { 9 | if (err) { return done(err); } 10 | if (result.value && result.value.length > 0) { 11 | self._invalidate("This product is associated with one or more orders and cannot be deleted"); 12 | } 13 | done(result.errors); 14 | }); 15 | } 16 | } 17 | }); 18 | 19 | module.exports = CanDeleteProductRule; 20 | -------------------------------------------------------------------------------- /business_logic/rules/canShipOrderItemRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var CanShipOrderItemRule = Rule.extend({ 4 | params: ['orderItem'], 5 | functions: { 6 | _onValidate: function(orderItem, done) { 7 | var validStatuses = ["SUBMITTED", "BACKORDERED"]; 8 | if (validStatuses.indexOf(orderItem.status) === -1) { 9 | this._invalidate(`Order item ${orderItem.id} is not in a shippable state`); 10 | } 11 | done(); 12 | } 13 | } 14 | }); 15 | 16 | module.exports = CanShipOrderItemRule; 17 | -------------------------------------------------------------------------------- /business_logic/rules/canSubmitOrderItemRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var CanSubmitOrderItemRule = Rule.extend({ 4 | params: ['orderItem'], 5 | functions: { 6 | _onValidate: function(orderItem, done) { 7 | if (orderItem.status !== "PENDING") { 8 | this._invalidate(`Order item ${orderItem.id} must be in a pending state to be submitted`); 9 | } 10 | done(); 11 | } 12 | } 13 | }); 14 | 15 | module.exports = CanSubmitOrderItemRule; 16 | -------------------------------------------------------------------------------- /business_logic/rules/fieldLengthRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var FieldLengthRule = Rule.extend({ 4 | params: ['field', 'value', 'length'], 5 | functions: { 6 | _onValidate: function(field, value, length, done) { 7 | if (this.value && this.value.length > this.length) { 8 | this.association = this.field; 9 | this._invalidate(this.field + " accepts a max length of " + this.length); 10 | } 11 | done(); 12 | } 13 | } 14 | }); 15 | 16 | module.exports = FieldLengthRule; 17 | -------------------------------------------------------------------------------- /business_logic/rules/fieldRequiredRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var FieldRequiredRule = Rule.extend({ 4 | params: ['field', 'data'], 5 | functions: { 6 | _onValidate: function(field, data, done) { 7 | this.data = this.data || {}; 8 | if (!this.data[this.field]) { 9 | this.association = this.field; 10 | this._invalidate(this.field + " is required"); 11 | } 12 | done(); 13 | } 14 | } 15 | }); 16 | 17 | module.exports = FieldRequiredRule; 18 | -------------------------------------------------------------------------------- /business_logic/rules/fieldTypeRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var FieldTypeRule = Rule.extend({ 4 | params: ['field', 'value', 'type'], 5 | functions: { 6 | _onValidate: function(field, value, type, done) { 7 | // if (isNaN(this.value)) { this.value = null; } 8 | if (this.value && typeof this.value !== this.type) { 9 | this.association = this.field; 10 | this._invalidate(`Invalid type supplied for ${this.field}, expected ${this.type}`); 11 | } 12 | done(); 13 | } 14 | } 15 | }); 16 | 17 | module.exports = FieldTypeRule; 18 | -------------------------------------------------------------------------------- /business_logic/rules/orderItemAmountValidityRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var OrderItemAmountValidityRule = Rule.extend({ 4 | association: "amount", 5 | params: ['orderItem', 'product'], 6 | functions: { 7 | _onValidate: function(orderItem, product, done) { 8 | if (orderItem.amount !== product.price * orderItem.quantity) { 9 | this._invalidate(`The amount for the ${product.name} order item does not equal the quantity multiplied by the current price in our system`); 10 | } 11 | done(); 12 | } 13 | } 14 | }); 15 | 16 | module.exports = OrderItemAmountValidityRule; 17 | -------------------------------------------------------------------------------- /business_logic/rules/orderItemPriceValidityRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var OrderItemPriceValidityRule = Rule.extend({ 4 | association: "price", 5 | params: ['orderItem', 'product'], 6 | functions: { 7 | _onValidate: function(orderItem, product, done) { 8 | if (orderItem.price !== product.price) { 9 | this._invalidate(`The price for ${product.name} no longer reflects the current price in our system`); 10 | } 11 | done(); 12 | } 13 | } 14 | }); 15 | 16 | module.exports = OrderItemPriceValidityRule; 17 | -------------------------------------------------------------------------------- /business_logic/rules/validOrderItemStatusForDeleteRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var ValidOrderItemStatusForDeleteRule = Rule.extend({ 4 | params: ['orderItem'], 5 | functions: { 6 | _onValidate: function(orderItem, done) { 7 | if (this.orderItem.status.toUpperCase() === "SHIPPED") { 8 | this._invalidate('Shipped items cannot be deleted'); 9 | } 10 | done(); 11 | } 12 | } 13 | }); 14 | 15 | module.exports = ValidOrderItemStatusForDeleteRule; 16 | -------------------------------------------------------------------------------- /business_logic/rules/validOrderItemStatusForUpdateRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var ValidOrderItemStatusForUpdateRule = Rule.extend({ 4 | params: ['orderItem'], 5 | functions: { 6 | _onValidate: function(orderItem, done) { 7 | if (orderItem.status.toUpperCase() === "BACKORDERED") { 8 | this._invalidate('Backordered items cannot be changed'); 9 | } else if (orderItem.status.toUpperCase() === "SHIPPED") { 10 | this._invalidate('Shipped items cannot be changed'); 11 | } 12 | done(); 13 | } 14 | } 15 | }); 16 | 17 | module.exports = ValidOrderItemStatusForUpdateRule; 18 | -------------------------------------------------------------------------------- /business_logic/rules/validOrderStatusForUpdateRule.js: -------------------------------------------------------------------------------- 1 | var Rule = require('peasy-js').Rule; 2 | 3 | var ValidOrderStatusForUpdateRule = Rule.extend({ 4 | params: ['orderId', 'orderItemService'], 5 | functions: { 6 | _onValidate: function(orderId, orderItemService, done) { 7 | var self = this; 8 | self.orderItemService.getByOrderCommand(self.orderId).execute(function(err, result) { 9 | if (err) { return done(err); } 10 | if (result.value) { 11 | var shippedItems = result.value.some(function(item) { return item.status === "SHIPPED" }); 12 | if (shippedItems) { 13 | self._invalidate('This order cannot change because it contains items that have been shipped'); 14 | } 15 | } 16 | done(); 17 | }); 18 | } 19 | } 20 | }); 21 | 22 | module.exports = ValidOrderStatusForUpdateRule; 23 | -------------------------------------------------------------------------------- /business_logic/services/categoryService.js: -------------------------------------------------------------------------------- 1 | var BusinessService = require('peasy-js').BusinessService; 2 | var FieldRequiredRule = require('../rules/fieldRequiredRule'); 3 | var utils = require('../shared/utils'); 4 | var stripAllFieldsFrom = utils.stripAllFieldsFrom; 5 | var BaseService = require('../services/baseService'); 6 | var CanDeleteCategoryRule = require('../rules/canDeleteCategoryRule'); 7 | 8 | var CategoryService = BusinessService.extendService(BaseService, { 9 | params: ['dataProxy', 'productService', 'eventPublisher'], 10 | functions: { 11 | _onInsertCommandInitialization: function(category, context, done) { 12 | stripAllFieldsFrom(category).except(['name', 'parentid']); 13 | done(); 14 | }, 15 | _getRulesForInsertCommand: function(category, context, done) { 16 | done(null, new FieldRequiredRule("name", category)); 17 | }, 18 | _onUpdateCommandInitialization: function(category, context, done) { 19 | stripAllFieldsFrom(category).except(['id', 'name', 'parentid']); 20 | done(); 21 | }, 22 | _getRulesForUpdateCommand: function(category, context, done) { 23 | done(null, [ 24 | new FieldRequiredRule('id', category), 25 | new FieldRequiredRule("name", category) 26 | ]); 27 | }, 28 | _getRulesForDestroyCommand: function(categoryId, context, done) { 29 | done(null, new CanDeleteCategoryRule(categoryId, this.productService)); 30 | } 31 | } 32 | }).service; 33 | 34 | module.exports = CategoryService; 35 | -------------------------------------------------------------------------------- /business_logic/services/clientOrderItemService.js: -------------------------------------------------------------------------------- 1 | var OrderItemService = require('./orderItemService'); 2 | var Command = require('peasy-js').Command; 3 | 4 | class ClientOrderItemService extends OrderItemService { 5 | constructor(dataProxy, productDataProxy, inventoryItemService) { 6 | super(dataProxy, productDataProxy, inventoryItemService); 7 | } 8 | 9 | submitCommand(orderItemId) { 10 | return new SubmitCommand(orderItemId, this.dataProxy); 11 | } 12 | 13 | shipCommand(orderItemId) { 14 | return new ShipCommand(orderItemId, this.dataProxy); 15 | } 16 | } 17 | 18 | class SubmitCommand extends Command { 19 | constructor(orderItemId, dataProxy) { 20 | super(); 21 | this.orderItemId = orderItemId; 22 | this.dataProxy = dataProxy; 23 | } 24 | 25 | _onValidationSuccess(context, done) { 26 | this.dataProxy.submit(this.orderItemId, function(err, result) { 27 | if (err) { return done(err); } 28 | done(null, result); 29 | }); 30 | } 31 | } 32 | 33 | class ShipCommand extends Command { 34 | constructor(orderItemId, dataProxy) { 35 | super(); 36 | this.orderItemId = orderItemId; 37 | this.dataProxy = dataProxy; 38 | } 39 | 40 | _onValidationSuccess(context, done) { 41 | this.dataProxy.ship(this.orderItemId, function(err, result) { 42 | if (err) { return done(err); } 43 | done(null, result); 44 | }); 45 | } 46 | } 47 | 48 | module.exports = ClientOrderItemService; -------------------------------------------------------------------------------- /business_logic/services/clientOrderService.js: -------------------------------------------------------------------------------- 1 | var Command = require('peasy-js').Command; 2 | var OrderService = require('../services/orderService'); 3 | 4 | class ClientOrderService extends OrderService { 5 | constructor(dataProxy, orderItemService) { 6 | super(dataProxy, orderItemService); 7 | } 8 | 9 | destroyCommand(orderId) { 10 | return new DestroyCommand(orderId, this.dataProxy); 11 | } 12 | } 13 | 14 | class DestroyCommand extends Command { 15 | constructor(orderId, dataProxy) { 16 | super(); 17 | this.orderId = orderId; 18 | this.dataProxy = dataProxy; 19 | } 20 | 21 | _onValidationSuccess(context, done) { 22 | this.dataProxy.destroy(this.orderId, done); 23 | } 24 | } 25 | 26 | module.exports = ClientOrderService; -------------------------------------------------------------------------------- /business_logic/shared/concurrencyError.js: -------------------------------------------------------------------------------- 1 | var ConcurrencyError = function(message) { 2 | this.message = message; 3 | } 4 | 5 | ConcurrencyError.prototype = new Error(); 6 | 7 | module.exports = ConcurrencyError; 8 | -------------------------------------------------------------------------------- /business_logic/shared/notFoundError.js: -------------------------------------------------------------------------------- 1 | var NotFoundError = function(message) { 2 | this.message = message; 3 | } 4 | 5 | NotFoundError.prototype = new Error(); 6 | 7 | module.exports = NotFoundError; 8 | -------------------------------------------------------------------------------- /business_logic/shared/utils.js: -------------------------------------------------------------------------------- 1 | 2 | function stripAllFieldsFrom(entity) { 3 | entity = entity || {}; 4 | return { 5 | except: function(allowableFields) { 6 | if (!Array.isArray(allowableFields)) { 7 | allowableFields = [allowableFields]; 8 | } 9 | allowableFields.forEach((field, index) => { 10 | allowableFields[index] = field.toLowerCase(); 11 | }); 12 | Object.keys(entity).forEach(function(field) { 13 | if (allowableFields.indexOf(field.toLowerCase()) === -1) { 14 | delete entity[field]; 15 | } 16 | }); 17 | } 18 | } 19 | } 20 | 21 | function convert(value, prop) { 22 | function toFloat() { 23 | var parsed = parseFloat(value[prop]); 24 | if (parsed) { 25 | value[prop] = parsed; 26 | } 27 | } 28 | 29 | function toInt() { 30 | var parsed = parseInt(value[prop], 10); 31 | if (parsed) { 32 | value[prop] = parsed; 33 | } 34 | } 35 | 36 | return { 37 | toFloat: toFloat, 38 | toInt: toInt 39 | }; 40 | } 41 | 42 | module.exports = { 43 | stripAllFieldsFrom: stripAllFieldsFrom, 44 | convert: convert 45 | } 46 | -------------------------------------------------------------------------------- /client/angular/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | -------------------------------------------------------------------------------- /client/angular/.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 | # profiling files 12 | chrome-profiler-events.json 13 | speed-measure-plugin.json 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # IDE - VSCode 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | .history/* 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | yarn-error.log 39 | testem.log 40 | /typings 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /client/angular/README.md: -------------------------------------------------------------------------------- 1 | # Orders 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /client/angular/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 | }; -------------------------------------------------------------------------------- /client/angular/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to orders!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | })); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/angular/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/angular/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 | } -------------------------------------------------------------------------------- /client/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orders", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.2.0", 15 | "@angular/common": "~7.2.0", 16 | "@angular/compiler": "~7.2.0", 17 | "@angular/core": "~7.2.0", 18 | "@angular/forms": "~7.2.0", 19 | "@angular/platform-browser": "~7.2.0", 20 | "@angular/platform-browser-dynamic": "~7.2.0", 21 | "@angular/router": "~7.2.0", 22 | "axios": "^0.18.0", 23 | "core-js": "^2.5.4", 24 | "ngx-toastr": "^9.1.1", 25 | "peasy-js": "^2.1.2", 26 | "rxjs": "~6.3.3", 27 | "socket.io-client": "^2.2.0", 28 | "tslib": "^1.9.0", 29 | "zone.js": "~0.8.26" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.13.0", 33 | "@angular/cli": "~7.3.0", 34 | "@angular/compiler-cli": "~7.2.0", 35 | "@angular/language-service": "~7.2.0", 36 | "@types/node": "~8.9.4", 37 | "@types/jasmine": "~2.8.8", 38 | "@types/jasminewd2": "~2.0.3", 39 | "codelyzer": "~4.5.0", 40 | "jasmine-core": "~2.99.1", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~3.1.1", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~1.1.2", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "protractor": "~5.4.0", 48 | "ts-node": "~7.0.0", 49 | "tslint": "~5.11.0", 50 | "typescript": "~3.2.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/angular/src/app-routing.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { AppRoutingModule } from './app-routing.module'; 2 | 3 | describe('AppRoutingModule', () => { 4 | let appRoutingModule: AppRoutingModule; 5 | 6 | beforeEach(() => { 7 | appRoutingModule = new AppRoutingModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(appRoutingModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /client/angular/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/app.component.css -------------------------------------------------------------------------------- /client/angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 21 |
22 | 23 |
-------------------------------------------------------------------------------- /client/angular/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /client/angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { SocketManager } from './SocketManager'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent { 11 | 12 | private socket; 13 | 14 | constructor(private router: Router, private socketManager: SocketManager) { 15 | } 16 | 17 | isActive(path) { 18 | return this.router.url.indexOf(path) > -1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/angular/src/app/category/category-detail/category-detail-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '../../contracts'; 2 | import { EntityViewModelBase } from '../../entity-view-model-base'; 3 | import { CategoryService } from '../../services/category.service'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class CategoryDetailViewModel extends EntityViewModelBase { 8 | 9 | constructor(service: CategoryService) { 10 | super(service); 11 | } 12 | 13 | get name(): string { 14 | return this.CurrentEntity.name; 15 | } 16 | 17 | set name(value: string) { 18 | this.setValue('name', value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/angular/src/app/category/category-detail/category-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/category/category-detail/category-detail.component.css -------------------------------------------------------------------------------- /client/angular/src/app/category/category-detail/category-detail.component.html: -------------------------------------------------------------------------------- 1 |

Manage Category

2 | 3 |
4 | 5 | 6 |
{{viewModel.getErrorMessageFor('name')}}
7 |
8 | 9 |
10 | 16 | 21 |
-------------------------------------------------------------------------------- /client/angular/src/app/category/category-detail/category-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CategoryDetailComponent } from './category-detail.component'; 4 | 5 | describe('CategoryDetailComponent', () => { 6 | let component: CategoryDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CategoryDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CategoryDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/category/category-detail/category-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Location } from '@angular/common'; 4 | import { CategoryDetailViewModel } from './category-detail-viewmodel'; 5 | import { Category, ViewModelArgs } from '../../contracts'; 6 | import { NotificationMessenger } from '../../notification-messenger'; 7 | 8 | @Component({ 9 | selector: 'app-category-detail', 10 | templateUrl: './category-detail.component.html', 11 | styleUrls: ['./category-detail.component.css'] 12 | }) 13 | export class CategoryDetailComponent implements OnInit { 14 | 15 | constructor( 16 | private route: ActivatedRoute, 17 | private location: Location, 18 | public viewModel: CategoryDetailViewModel, 19 | private notificationMessenger: NotificationMessenger) { } 20 | 21 | public async ngOnInit(): Promise { 22 | let categoryId = this.route.snapshot.params['id']; 23 | if (categoryId.toLowerCase() === 'new') { categoryId = null; } 24 | this.viewModel.loadData({ 25 | entityID: categoryId 26 | } as ViewModelArgs); 27 | } 28 | 29 | public goBack(): void { 30 | this.location.back(); 31 | } 32 | 33 | public async save(): Promise { 34 | if (await this.viewModel.save()) { 35 | this.notificationMessenger.info('Save successful'); 36 | this.goBack(); 37 | } else { 38 | this.notificationMessenger.error('Save failed. Please try again.'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/angular/src/app/category/category-list/category-list-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '../../contracts'; 2 | import { ListViewModelBase } from '../../list-view-model-base'; 3 | import { CategoryService } from '../../services/category.service'; 4 | import { Injectable } from '@angular/core'; 5 | import { CategoryEventAggregator } from '../../event-aggregators/category-event-aggregator'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class CategoryListViewModel extends ListViewModelBase { 9 | constructor(protected service: CategoryService, categoryEventAggregator: CategoryEventAggregator) { 10 | super(service); 11 | categoryEventAggregator.insert.subscribe(() => this.loadData()); 12 | categoryEventAggregator.update.subscribe(() => this.loadData()); 13 | categoryEventAggregator.delete.subscribe(() => this.loadData()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/angular/src/app/category/category-list/category-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/category/category-list/category-list.component.css -------------------------------------------------------------------------------- /client/angular/src/app/category/category-list/category-list.component.html: -------------------------------------------------------------------------------- 1 |

Categories

2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 |
6 | 7 | Create New 8 | 9 | Name
{{ category.name }} 19 | 24 |
-------------------------------------------------------------------------------- /client/angular/src/app/category/category-list/category-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CategoryListComponent } from './category-list.component'; 4 | 5 | describe('CategoryListComponent', () => { 6 | let component: CategoryListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CategoryListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CategoryListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/category/category-list/category-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CategoryListViewModel } from './category-list-viewmodel'; 3 | import { NotificationMessenger } from 'src/app/notification-messenger'; 4 | 5 | @Component({ 6 | selector: 'app-category-list', 7 | templateUrl: './category-list.component.html', 8 | styleUrls: ['./category-list.component.css'] 9 | }) 10 | export class CategoryListComponent implements OnInit { 11 | 12 | public viewModel: CategoryListViewModel; 13 | 14 | constructor(vm: CategoryListViewModel, 15 | private notificationMessenger: NotificationMessenger) { 16 | this.viewModel = vm; 17 | } 18 | 19 | public async ngOnInit() { 20 | this.viewModel.loadData(); 21 | } 22 | 23 | public async destroy(id: string): Promise { 24 | if (!await this.viewModel.destroy(id)) { 25 | this.notificationMessenger.error(this.viewModel.errors[0]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/angular/src/app/component-base.ts: -------------------------------------------------------------------------------- 1 | import { OnInit, OnDestroy } from '@angular/core'; 2 | import { ViewModelBase } from './view-model-base'; 3 | 4 | export abstract class ComponentBase implements OnInit, OnDestroy { 5 | 6 | constructor(protected viewModel: ViewModelBase) { 7 | } 8 | 9 | public async ngOnInit(): Promise { 10 | this.viewModel.listen(); 11 | } 12 | 13 | public async ngOnDestroy(): Promise { 14 | this.viewModel.dispose(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-detail/customer-detail-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { Customer } from '../../contracts'; 2 | import { EntityViewModelBase } from '../../entity-view-model-base'; 3 | import { CustomerService } from '../../services/customer.service'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class CustomerDetailViewModel extends EntityViewModelBase { 8 | 9 | constructor(service: CustomerService) { 10 | super(service); 11 | } 12 | 13 | get name(): string { 14 | return this.CurrentEntity.name; 15 | } 16 | 17 | set name(value: string) { 18 | this.setValue('name', value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-detail/customer-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/customer/customer-detail/customer-detail.component.css -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-detail/customer-detail.component.html: -------------------------------------------------------------------------------- 1 |

Manage Customer

2 | 3 |
4 | 5 | 6 |
{{viewModel.getErrorMessageFor('name')}}
7 |
8 | 9 | 15 | 16 |
17 | 20 | 23 |
-------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-detail/customer-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomerDetailComponent } from './customer-detail.component'; 4 | 5 | describe('CustomerDetailComponent', () => { 6 | let component: CustomerDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CustomerDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CustomerDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-detail/customer-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Location } from '@angular/common'; 4 | import { CustomerDetailViewModel } from './customer-detail-viewmodel'; 5 | import { Customer, ViewModelArgs } from '../../contracts'; 6 | import { NotificationMessenger } from '../../notification-messenger'; 7 | import { CustomerService } from '../../services/customer.service'; 8 | 9 | @Component({ 10 | selector: 'app-customer-detail', 11 | templateUrl: './customer-detail.component.html', 12 | styleUrls: ['./customer-detail.component.css'], 13 | // providers: [CustomerService, CustomerDetailViewModel] 14 | }) 15 | export class CustomerDetailComponent implements OnInit { 16 | 17 | constructor( 18 | private route: ActivatedRoute, 19 | private location: Location, 20 | public viewModel: CustomerDetailViewModel, 21 | private notificationMessenger: NotificationMessenger) { 22 | } 23 | 24 | public async ngOnInit(): Promise { 25 | let customerId = this.route.snapshot.params['id']; 26 | if (customerId.toLowerCase() === 'new') { customerId = null; } 27 | this.viewModel.loadData({ 28 | entityID: customerId 29 | } as ViewModelArgs); 30 | } 31 | 32 | public goBack(): void { 33 | this.location.back(); 34 | } 35 | 36 | public async save(): Promise { 37 | if (await this.viewModel.save()) { 38 | this.notificationMessenger.info('Save successful'); 39 | this.goBack(); 40 | } else { 41 | this.notificationMessenger.error('Save failed. Please try again.'); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-list/customer-list-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { Customer } from '../../contracts'; 2 | import { ListViewModelBase } from '../../list-view-model-base'; 3 | import { Injectable } from '@angular/core'; 4 | import { CustomerService } from '../../services/customer.service'; 5 | import { CustomerEventAggregator } from '../../event-aggregators/customer-event-aggregator'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class CustomerListViewModel extends ListViewModelBase { 9 | constructor(protected service: CustomerService, private customerEventAggregator: CustomerEventAggregator) { 10 | super(service); 11 | } 12 | 13 | public listen(): void { 14 | this.customerEventAggregator.update.subscribe(() => super.loadData()); 15 | this.customerEventAggregator.insert.subscribe(() => super.loadData()); 16 | this.customerEventAggregator.delete.subscribe(() => super.loadData()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-list/customer-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/customer/customer-list/customer-list.component.css -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-list/customer-list.component.html: -------------------------------------------------------------------------------- 1 |

Customers

2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 |
6 | 9 | Create New 10 | 11 | Name
{{ customer.name }} 21 | 26 |
-------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-list/customer-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomerListComponent } from './customer-list.component'; 4 | 5 | describe('CustomerListComponent', () => { 6 | let component: CustomerListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CustomerListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CustomerListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/customer-list/customer-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CustomerListViewModel } from './customer-list-viewmodel'; 3 | import { NotificationMessenger } from 'src/app/notification-messenger'; 4 | 5 | @Component({ 6 | selector: 'app-customer-list', 7 | templateUrl: './customer-list.component.html', 8 | styleUrls: ['./customer-list.component.css'] 9 | }) 10 | export class CustomerListComponent implements OnInit { 11 | 12 | public viewModel: CustomerListViewModel; 13 | 14 | constructor(vm: CustomerListViewModel, 15 | private notificationMessenger: NotificationMessenger) { 16 | this.viewModel = vm; 17 | } 18 | 19 | public async ngOnInit() { 20 | this.viewModel.loadData(); 21 | } 22 | 23 | public async destroy(id: string): Promise { 24 | if (!await this.viewModel.destroy(id)) { 25 | this.notificationMessenger.error(this.viewModel.errors[0]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/custtest/custtest.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/customer/custtest/custtest.component.css -------------------------------------------------------------------------------- /client/angular/src/app/customer/custtest/custtest.component.html: -------------------------------------------------------------------------------- 1 |

2 | custtest works! 3 | 4 |

5 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/custtest/custtest.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CusttestComponent } from './custtest.component'; 4 | 5 | describe('CusttestComponent', () => { 6 | let component: CusttestComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CusttestComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CusttestComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/customer/custtest/custtest.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CustomerDetailViewModel } from '../customer-detail/customer-detail-viewmodel'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { ViewModelArgs, Customer } from '../../contracts'; 5 | import { CustomerEventAggregator } from '../../event-aggregators/customer-event-aggregator'; 6 | 7 | @Component({ 8 | selector: 'app-custtest', 9 | templateUrl: './custtest.component.html', 10 | styleUrls: ['./custtest.component.css'], 11 | providers: [CustomerDetailViewModel] 12 | }) 13 | export class CusttestComponent implements OnInit { 14 | 15 | private updateSubscription; 16 | 17 | constructor( 18 | private route: ActivatedRoute, 19 | private eventAggregator: CustomerEventAggregator, 20 | public viewModel: CustomerDetailViewModel) { 21 | this.onCustomerChanged = this.onCustomerChanged.bind(this); 22 | this.updateSubscription = this.eventAggregator.update.subscribe(this.onCustomerChanged); 23 | } 24 | 25 | private onCustomerChanged(customer: Customer): void { 26 | this.viewModel.loadData({ 27 | entityID: customer.id 28 | } as ViewModelArgs); 29 | } 30 | 31 | ngOnInit() { 32 | let customerId = this.route.snapshot.params['id']; 33 | if (customerId.toLowerCase() === 'new') { customerId = null; } 34 | this.viewModel.loadData({ 35 | entityID: customerId 36 | } as ViewModelArgs); 37 | } 38 | 39 | // tslint:disable-next-line:use-life-cycle-interface 40 | public async ngOnDestroy(): Promise { 41 | this.updateSubscription.unsubscribe(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/cache/category-cache-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Category, ICategoryDataProxy } from '../../contracts'; 3 | import { CacheDataProxy } from './cache-data-proxy-base'; 4 | import { CategoryEventAggregator } from '../../event-aggregators/category-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class CategoryCacheDataProxy 8 | extends CacheDataProxy 9 | implements ICategoryDataProxy { 10 | 11 | constructor(protected dataProxy: ICategoryDataProxy, protected eventAggregator: CategoryEventAggregator) { 12 | super(dataProxy, eventAggregator); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/cache/customer-cache-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Customer, ICustomerDataProxy } from '../../contracts'; 3 | import { CacheDataProxy } from './cache-data-proxy-base'; 4 | import { CustomerEventAggregator } from '../../event-aggregators/customer-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class CustomerCacheDataProxy 8 | extends CacheDataProxy 9 | implements ICustomerDataProxy { 10 | 11 | constructor(protected dataProxy: ICustomerDataProxy, protected eventAggregator: CustomerEventAggregator) { 12 | super(dataProxy, eventAggregator); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/cache/inventory-cache-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { InventoryItem, IInventoryDataProxy, OrderItem } from '../../contracts'; 3 | import { CacheDataProxy } from './cache-data-proxy-base'; 4 | import { InventoryEventAggregator } from '../../event-aggregators/inventory-event-aggregator'; 5 | import { OrderItemEventAggregator } from '../../event-aggregators/order-item-event-aggregator'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class InventoryCacheDataProxy 9 | extends CacheDataProxy 10 | implements IInventoryDataProxy { 11 | 12 | constructor( 13 | protected dataProxy: IInventoryDataProxy, 14 | protected eventAggregator: InventoryEventAggregator, 15 | protected orderItemEventAggregator: OrderItemEventAggregator) { 16 | super(dataProxy, eventAggregator); 17 | this.orderItemEventAggregator.update.subscribe(this.handleOrderItemUpdate.bind(this)); 18 | } 19 | 20 | private async handleOrderItemUpdate(orderItem: OrderItem) { 21 | this.getByProduct(orderItem.productId); 22 | } 23 | 24 | public async getByProduct(productId: string): Promise { 25 | const result = await this.dataProxy.getByProduct(productId); 26 | this._data.set(result.id, Object.assign({}, result)); 27 | return result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/cache/order-cache-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { IOrderDataProxy, Order } from '../../contracts'; 3 | import { CacheDataProxy } from './cache-data-proxy-base'; 4 | import { OrderEventAggregator } from '../../event-aggregators/order-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class OrderCacheDataProxy 8 | extends CacheDataProxy 9 | implements IOrderDataProxy { 10 | 11 | constructor(protected dataProxy: IOrderDataProxy, protected eventAggregator: OrderEventAggregator) { 12 | super(dataProxy, eventAggregator); 13 | } 14 | 15 | public getByCustomer(customerId: string): Promise { 16 | return this.dataProxy.getByCustomer(customerId); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/cache/order-item-cache-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { OrderItem, IOrderItemDataProxy } from '../../contracts'; 3 | import { CacheDataProxy } from './cache-data-proxy-base'; 4 | import { OrderItemEventAggregator } from '../../event-aggregators/order-item-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class OrderItemCacheDataProxy 8 | extends CacheDataProxy 9 | implements IOrderItemDataProxy { 10 | 11 | constructor(protected dataProxy: IOrderItemDataProxy, protected eventAggregator: OrderItemEventAggregator) { 12 | super(dataProxy, eventAggregator); 13 | } 14 | 15 | public getByOrder(orderId: string): Promise { 16 | return this.dataProxy.getByOrder(orderId); 17 | } 18 | 19 | public async submit(itemId: string): Promise { 20 | const result = await this.dataProxy.submit(itemId); 21 | this._data.set(result.id, Object.assign({}, result)); 22 | this.eventAggregator.update.publish(result); 23 | return result; 24 | } 25 | 26 | public async ship(itemId: string): Promise { 27 | const result = await this.dataProxy.ship(itemId); 28 | this._data.set(result.id, Object.assign({}, result)); 29 | this.eventAggregator.update.publish(result); 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/cache/product-cache-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Product, IProductDataProxy } from '../../contracts'; 3 | import { CacheDataProxy } from './cache-data-proxy-base'; 4 | import { ProductEventAggregator } from '../../event-aggregators/product-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class ProductCacheDataProxy 8 | extends CacheDataProxy 9 | implements IProductDataProxy { 10 | 11 | constructor(protected dataProxy: IProductDataProxy, protected eventAggreator: ProductEventAggregator) { 12 | super(dataProxy, eventAggreator); 13 | } 14 | 15 | public getByCategory(categoryId: string): Promise { 16 | return this.dataProxy.getByCategory(categoryId); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/category-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpDataProxy } from './http-data-proxy-base'; 3 | import { Category } from '../../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class CategoryDataProxy extends HttpDataProxy { 7 | protected baseUri = '/categories'; 8 | } 9 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/customer-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpDataProxy } from './http-data-proxy-base'; 3 | import { Customer } from '../../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class CustomerDataProxy extends HttpDataProxy { 7 | protected baseUri = '/customers'; 8 | } 9 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/http-data-proxy-base.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../contracts'; 2 | import { IDataProxy, ServiceException } from 'peasy-js'; 3 | import axios from 'axios'; 4 | 5 | export abstract class HttpDataProxy implements IDataProxy { 6 | 7 | private httpStatusCodes = { 8 | BAD_REQUEST: 400, 9 | CONFLICT: 409, 10 | NOT_FOUND: 404, 11 | NOT_IMPLEMENTED: 501 12 | }; 13 | 14 | protected abstract baseUri: string; 15 | 16 | getAll(): Promise { 17 | return axios.get(this.baseUri).then(result => result.data); 18 | } 19 | 20 | getById(id: string): Promise { 21 | return axios.get(`${this.baseUri}/${id}`).then(result => result.data); 22 | } 23 | 24 | protected async getAllById(url: string): Promise { 25 | try { 26 | const data = await axios.get(url).then(result => result.data); 27 | return data; 28 | } catch (err) { 29 | if (err.response && err.response.status === this.httpStatusCodes.NOT_FOUND) { 30 | return []; 31 | } 32 | throw err; 33 | } 34 | } 35 | 36 | insert(data: T): Promise { 37 | return axios.post(this.baseUri, data).then(result => result.data); 38 | } 39 | 40 | update(data: T): Promise { 41 | return axios.put(`${this.baseUri}/${data.id}`, data).then(result => result.data); 42 | } 43 | 44 | destroy(id: string): Promise { 45 | return axios.delete(`${this.baseUri}/${id}`).then(result => result.data); 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/inventory-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpDataProxy } from './http-data-proxy-base'; 3 | import { InventoryItem, IInventoryDataProxy } from '../../contracts'; 4 | import axios from 'axios'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class InventoryDataProxy 8 | extends HttpDataProxy 9 | implements IInventoryDataProxy { 10 | 11 | protected baseUri = '/inventoryitems'; 12 | 13 | public getByProduct(productId: string): Promise { 14 | return axios.get(`${this.baseUri}?productid=${productId}`) 15 | .then(result => result.data); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/order-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpDataProxy } from './http-data-proxy-base'; 3 | import { Order, IOrderDataProxy } from '../../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class OrderDataProxy extends HttpDataProxy implements IOrderDataProxy { 7 | protected baseUri = '/orders'; 8 | 9 | public getByCustomer(customerId: string): Promise { 10 | return this.getAllById(`${this.baseUri}?customerid=${customerId}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/order-item-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpDataProxy } from './http-data-proxy-base'; 3 | import { OrderItem, IOrderItemDataProxy } from '../../contracts'; 4 | import axios from 'axios'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class OrderItemDataProxy 8 | extends HttpDataProxy 9 | implements IOrderItemDataProxy { 10 | 11 | protected baseUri = '/orderitems'; 12 | 13 | public getByOrder(orderId: string): Promise { 14 | return axios.get(`${this.baseUri}?orderid=${orderId}`) 15 | .then(result => result.data) 16 | .catch(e => { 17 | if (e.response.status === 404) { 18 | return Promise.resolve([]); 19 | } 20 | return Promise.reject(e); 21 | }); 22 | } 23 | 24 | public submit(itemId: string): Promise { 25 | return axios.post(`${this.baseUri}/${itemId}/submit`).then(result => result.data); 26 | } 27 | 28 | public ship(itemId: string): Promise { 29 | return axios.post(`${this.baseUri}/${itemId}/ship`).then(result => result.data); 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /client/angular/src/app/data-proxies/http/product-data-proxy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpDataProxy } from './http-data-proxy-base'; 3 | import { Product, IProductDataProxy } from '../../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class ProductDataProxy extends HttpDataProxy implements IProductDataProxy { 7 | protected baseUri = '/products'; 8 | 9 | getByCategory(categoryid: string): Promise { 10 | return this.getAllById(`${this.baseUri}?categoryid=${categoryid}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/category-event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventAggregator } from './event-aggregator'; 3 | import { Category } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class CategoryEventAggregator extends EventAggregator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/customer-event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventAggregator } from './event-aggregator'; 3 | import { Customer } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class CustomerEventAggregator extends EventAggregator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventEmitter } from './event-emitter'; 3 | import { Entity } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class EventAggregator { 7 | 8 | public insert: EventEmitter; 9 | public update: EventEmitter; 10 | public delete: EventEmitter; 11 | public remoteInsert: EventEmitter; 12 | public remoteUpdate: EventEmitter; 13 | public remoteDelete: EventEmitter; 14 | 15 | constructor() { 16 | this.insert = new EventEmitter(); 17 | this.update = new EventEmitter(); 18 | this.delete = new EventEmitter(); 19 | this.remoteInsert = new EventEmitter(); 20 | this.remoteUpdate = new EventEmitter(); 21 | this.remoteDelete = new EventEmitter(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { ISubscription } from '../contracts'; 2 | 3 | export class EventEmitter { 4 | 5 | private callbacks = []; 6 | 7 | public subscribe(callback): ISubscription { 8 | this.callbacks.push(callback); 9 | return { 10 | unsubscribe: () => { 11 | this.unsubscribe(callback); 12 | } 13 | } as ISubscription; 14 | } 15 | 16 | public unsubscribe(callback) { 17 | const index = this.callbacks.indexOf(callback); 18 | this.callbacks.splice(index, 1); 19 | } 20 | 21 | public publish(data: T) { 22 | this.callbacks.forEach(cb => cb(data)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/inventory-event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventAggregator } from './event-aggregator'; 3 | import { InventoryItem } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class InventoryEventAggregator extends EventAggregator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/order-event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventAggregator } from './event-aggregator'; 3 | import { Order } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class OrderEventAggregator extends EventAggregator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/order-item-event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventAggregator } from './event-aggregator'; 3 | import { OrderItem } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class OrderItemEventAggregator extends EventAggregator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/event-aggregators/product-event-aggregator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EventAggregator } from './event-aggregator'; 3 | import { Product } from '../contracts'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class ProductEventAggregator extends EventAggregator { 7 | constructor() { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-detail/inventory-detail-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { EntityViewModelBase } from '../../entity-view-model-base'; 2 | import { ViewModelArgs, InventoryItem } from '../../contracts'; 3 | import { ProductListViewModel } from '../../product/product-list/product-list-viewmodel'; 4 | import { InventoryService } from '../../services/inventory.service'; 5 | import { Injectable } from '@angular/core'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class InventoryDetailViewModel extends EntityViewModelBase { 9 | 10 | private _productName: string; 11 | 12 | constructor(service: InventoryService, private productsVM: ProductListViewModel) { 13 | super(service); 14 | } 15 | 16 | loadData(args: ViewModelArgs): any { 17 | this._productName = null; 18 | super.loadData(args); 19 | this.productsVM.loadData(); 20 | } 21 | 22 | get isBusy() { 23 | return super['isBusy'] || this.productsVM.isBusy; 24 | } 25 | 26 | get quantityOnHand(): number { 27 | return this.CurrentEntity.quantityOnHand; 28 | } 29 | 30 | set quantityOnHand(amount: number) { 31 | this.setValue('quantityOnHand', amount); 32 | } 33 | 34 | get name(): string { 35 | const products = this.productsVM.data; 36 | if (!this._productName && this.CurrentEntity.id) { 37 | if (products) { 38 | this._productName = products.find(p => p.id === this.CurrentEntity.productId).name; 39 | } 40 | } 41 | return this._productName; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-detail/inventory-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/inventory/inventory-detail/inventory-detail.component.css -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-detail/inventory-detail.component.html: -------------------------------------------------------------------------------- 1 |

Manage Inventory Item

2 | 3 |
4 | {{viewModel.name}} 5 |
6 | 7 |
8 | 9 | 10 |
{{viewModel.getErrorMessageFor('quantityOnHand')}}
11 |
12 | 13 |
14 | 17 | 20 |
-------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-detail/inventory-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { InventoryDetailComponent } from './inventory-detail.component'; 4 | 5 | describe('InventoryDetailComponent', () => { 6 | let component: InventoryDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ InventoryDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(InventoryDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-detail/inventory-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Location } from '@angular/common'; 4 | import { ViewModelArgs, InventoryItem } from '../../contracts'; 5 | import { InventoryDetailViewModel } from './inventory-detail-viewmodel'; 6 | import { NotificationMessenger } from '../../notification-messenger'; 7 | 8 | @Component({ 9 | selector: 'app-inventory-detail', 10 | templateUrl: './inventory-detail.component.html', 11 | styleUrls: ['./inventory-detail.component.css'] 12 | }) 13 | export class InventoryDetailComponent implements OnInit { 14 | 15 | constructor( 16 | private route: ActivatedRoute, 17 | private location: Location, 18 | public viewModel: InventoryDetailViewModel, 19 | private notificationMessenger: NotificationMessenger) { } 20 | 21 | public async ngOnInit(): Promise { 22 | let inventoryItemId = this.route.snapshot.params['id']; 23 | if (inventoryItemId.toLowerCase() === 'new') { inventoryItemId = null; } 24 | this.viewModel.loadData({ 25 | entityID: inventoryItemId 26 | } as ViewModelArgs); 27 | } 28 | 29 | public goBack(): void { 30 | this.location.back(); 31 | } 32 | 33 | public async save(): Promise { 34 | if (await this.viewModel.save()) { 35 | this.notificationMessenger.info('Save successful'); 36 | this.goBack(); 37 | } else { 38 | this.notificationMessenger.error('Save failed. Please try again.'); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-list/inventory-list-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { ListViewModelBase } from '../../list-view-model-base'; 2 | import { InventoryItem } from '../../contracts'; 3 | import { InventoryService } from '../../services/inventory.service'; 4 | import { ProductListViewModel } from '../../product/product-list/product-list-viewmodel'; 5 | import { Injectable } from '@angular/core'; 6 | import { InventoryEventAggregator } from '../../event-aggregators/inventory-event-aggregator'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class InventoryListViewModel extends ListViewModelBase { 10 | 11 | constructor( 12 | protected service: InventoryService, 13 | private productListVM: ProductListViewModel, 14 | private inventoryEventAggregator: InventoryEventAggregator) { 15 | super(service); 16 | inventoryEventAggregator.insert.subscribe(() => this.loadData()); 17 | inventoryEventAggregator.update.subscribe(() => this.loadData()); 18 | inventoryEventAggregator.delete.subscribe(() => this.loadData()); 19 | } 20 | 21 | get isBusy() { 22 | return super['isBusy'] || this.productListVM.isBusy; 23 | } 24 | 25 | async loadData(): Promise { 26 | const results = await Promise.all([ 27 | super.loadData(), 28 | this.productListVM.loadData() 29 | ]); 30 | return results.every(r => r === true); 31 | } 32 | 33 | getProductNameFor(productId: string): string { 34 | const products = this.productListVM.data; 35 | if (products) { 36 | return products.find(p => p.id === productId).name; 37 | } 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-list/inventory-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/inventory/inventory-list/inventory-list.component.css -------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-list/inventory-list.component.html: -------------------------------------------------------------------------------- 1 |

Inventory

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
NameQuantity
{{ viewModel.getProductNameFor(item.productId) }}{{ item.quantityOnHand }}
-------------------------------------------------------------------------------- /client/angular/src/app/inventory/inventory-list/inventory-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { InventoryListComponent } from './inventory-list.component'; 4 | 5 | describe('InventoryListComponent', () => { 6 | let component: InventoryListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ InventoryListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(InventoryListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/list-view-model-base.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from './contracts'; 2 | import { BusinessService } from 'peasy-js'; 3 | import { ViewModelBase } from './view-model-base'; 4 | 5 | export class ListViewModelBase extends ViewModelBase { 6 | 7 | public data: T[] = []; 8 | 9 | constructor(protected service: BusinessService) { 10 | super(); 11 | } 12 | 13 | public loadData(): Promise { 14 | return this.handle(() => this.service.getAllCommand()); 15 | } 16 | 17 | protected async handle(command: any): Promise { 18 | let success = true; 19 | this.loadStarted(); 20 | try { 21 | const result = await command().execute(); 22 | if (result.success) { 23 | this.data = result.value || this.data; 24 | } else { 25 | success = false; 26 | result.errors.forEach(e => { 27 | this._errors.push(e.message); 28 | }); 29 | } 30 | } catch (e) { 31 | success = false; 32 | if (Array.isArray(e)) { 33 | this._errors = e; 34 | } else { 35 | this._errors.push(e); 36 | } 37 | } 38 | this.loadCompleted(); 39 | return success; 40 | } 41 | 42 | async destroy(id: string): Promise { 43 | const result = await this.handle(() => this.service.destroyCommand(id)); 44 | if (result) { 45 | this.data = this.data.filter(entity => entity.id !== id); 46 | } 47 | return result; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /client/angular/src/app/notification-messenger.ts: -------------------------------------------------------------------------------- 1 | import { INotificationMessenger } from './contracts'; 2 | import { ToastrService } from 'ngx-toastr'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class NotificationMessenger implements INotificationMessenger { 7 | 8 | constructor(private toastr: ToastrService) { 9 | } 10 | 11 | info(message: string): void { 12 | this.toastr.success(message); 13 | } 14 | 15 | warning(message: string): void { 16 | this.toastr.warning(message); 17 | } 18 | 19 | error(message: string): void { 20 | this.toastr.error(message); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-detail/order-item-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/order-item/order-item-detail/order-item-detail.component.css -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-detail/order-item-detail.component.html: -------------------------------------------------------------------------------- 1 |

Manage Order Item

2 | 3 |
4 | 5 | 8 |
{{viewModel.getErrorMessageFor('categoryId')}}
9 |
10 | 11 |
12 | 13 | 16 |
{{viewModel.getErrorMessageFor('productId')}}
17 |
18 | 19 |
20 | {{ viewModel.inStock }} 21 |
22 | 23 |
24 | {{ viewModel.price | currency }} 25 |
26 | 27 |
28 | 29 | 30 |
{{viewModel.getErrorMessageFor('quantity')}}
31 |
32 | 33 |
34 | {{ viewModel.amount | currency }} 35 |
36 | 37 |
38 | 41 | 44 |
45 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-detail/order-item-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderItemDetailComponent } from './order-item-detail.component'; 4 | 5 | describe('OrderItemDetailComponent', () => { 6 | let component: OrderItemDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OrderItemDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderItemDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-detail/order-item-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Location } from '@angular/common'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { OrderItemDetailViewModel } from './order-item-detail-viewmodel'; 5 | import { ViewModelArgs, OrderItem } from '../../contracts'; 6 | import { NotificationMessenger } from '../../notification-messenger'; 7 | 8 | @Component({ 9 | selector: 'app-order-item-detail', 10 | templateUrl: './order-item-detail.component.html', 11 | styleUrls: ['./order-item-detail.component.css'] 12 | }) 13 | export class OrderItemDetailComponent implements OnInit { 14 | 15 | constructor( 16 | private route: ActivatedRoute, 17 | private location: Location, 18 | public viewModel: OrderItemDetailViewModel, 19 | private notificationMessenger: NotificationMessenger) { } 20 | 21 | public async ngOnInit() { 22 | const orderId = this.route.snapshot.params['id']; 23 | let orderItemId = this.route.snapshot.params['itemid']; 24 | if (orderItemId.toLowerCase() === 'new') { orderItemId = null; } 25 | await this.viewModel.loadData({ entityID: orderItemId, } as ViewModelArgs); 26 | this.viewModel.orderId = orderId; 27 | } 28 | 29 | public goBack(): void { 30 | this.location.back(); 31 | } 32 | 33 | public async save(): Promise { 34 | if (await this.viewModel.save()) { 35 | this.notificationMessenger.info('Save successful'); 36 | this.goBack(); 37 | } else { 38 | this.notificationMessenger.error('Save failed. Please try again.'); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-list/order-item-list-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { ListViewModelBase } from '../../list-view-model-base'; 2 | import { OrderItem } from '../../contracts'; 3 | import { OrderItemService } from '../../services/order-item.service'; 4 | import { ProductListViewModel } from '../../product/product-list/product-list-viewmodel'; 5 | import { Injectable } from '@angular/core'; 6 | import { OrderItemViewModel } from '../order-item-viewmodel'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class OrderItemListViewModel extends ListViewModelBase { 10 | 11 | constructor( 12 | protected service: OrderItemService, private productsVM: ProductListViewModel) { 13 | super(service); 14 | } 15 | 16 | public items: OrderItemViewModel[]; 17 | 18 | public get isBusy() { 19 | return super['isBusy'] || 20 | this.productsVM.isBusy || 21 | this.items.some(vm => vm.isBusy); 22 | } 23 | 24 | public async loadDataFor(orderId: string): Promise { 25 | const results = await Promise.all 26 | ([ 27 | super.handle(() => this.service.getByOrderCommand(orderId)), 28 | this.productsVM.loadData() 29 | ]); 30 | this.items = this.data.map(i => new OrderItemViewModel(this.service, this.productsVM.data, i)); 31 | return results.every(r => r === true); 32 | } 33 | 34 | public async destroy(id: string): Promise { 35 | const result = await super.destroy(id); 36 | if (result) { 37 | this.items = [...this.items.filter(vm => vm.id !== id)]; 38 | } 39 | return result; 40 | } 41 | 42 | public async submitAllSubmittable(): Promise { 43 | const submittableItems = this.items.filter(vm => vm.canSubmit); 44 | const results = await Promise.all(submittableItems.map(vm => vm.submit())); 45 | return results.every(result => result === true); 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-list/order-item-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/order-item/order-item-list/order-item-list.component.css -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-list/order-item-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 46 | 47 | 48 |
5 | 8 | Add Item 9 | 10 | ProductPriceQuantityAmountStatusSubmitted OnShipped On
24 | 30 | {{ item.productName }}{{ item.CurrentEntity.price | currency }}{{ item.CurrentEntity.quantity }}{{ item.CurrentEntity.amount | currency }}{{ item.CurrentEntity.status }}{{ item.CurrentEntity.submittedOn | date:'short' }}{{ item.CurrentEntity.shippedOn | date:'short'}} 39 | 45 |
49 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-list/order-item-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderItemListComponent } from './order-item-list.component'; 4 | 5 | describe('OrderItemListComponent', () => { 6 | let component: OrderItemListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OrderItemListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderItemListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-list/order-item-list.component.ts: -------------------------------------------------------------------------------- 1 | import { OnInit, Component, Input, EventEmitter, Output } from '@angular/core'; 2 | import { OrderItemListViewModel } from './order-item-list-viewmodel'; 3 | import { OrderItem } from '../../contracts'; 4 | 5 | @Component({ 6 | selector: 'app-order-item-list', 7 | templateUrl: './order-item-list.component.html', 8 | styleUrls: ['./order-item-list.component.css'] 9 | }) 10 | export class OrderItemListComponent implements OnInit { 11 | 12 | @Input() 13 | public orderId: string; 14 | 15 | @Input() 16 | public viewModel: OrderItemListViewModel; 17 | 18 | @Output() 19 | destroyClicked = new EventEmitter(); 20 | 21 | public async ngOnInit() { 22 | } 23 | 24 | public onDestroyClicked(orderItem: OrderItem): void { 25 | this.destroyClicked.emit(orderItem); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/angular/src/app/order-item/order-item-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { EntityViewModelBase } from '../entity-view-model-base'; 2 | import { Product, OrderItem } from '../contracts'; 3 | import { Injectable } from '@angular/core'; 4 | import { OrderItemService } from '../services/order-item.service'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class OrderItemViewModel extends EntityViewModelBase { 8 | 9 | constructor( 10 | private orderItemService: OrderItemService, 11 | private products: Product[], 12 | orderItem: OrderItem) { 13 | super(orderItemService); 14 | this.CurrentEntity = orderItem; 15 | } 16 | 17 | public get canDelete(): boolean { 18 | return this.orderItemService.canDelete(this.CurrentEntity); 19 | } 20 | 21 | public get canSubmit(): boolean { 22 | return this.orderItemService.canSubmit(this.CurrentEntity); 23 | } 24 | 25 | public get canShip(): boolean { 26 | return this.orderItemService.canShip(this.CurrentEntity); 27 | } 28 | 29 | public submit(): Promise { 30 | if (this.canSubmit) { 31 | return this.handle( 32 | this.orderItemService.submitCommand(this.CurrentEntity.id) 33 | ); 34 | } 35 | } 36 | 37 | public ship(): Promise { 38 | if (this.canShip) { 39 | return this.handle( 40 | this.orderItemService.shipCommand(this.CurrentEntity.id) 41 | ); 42 | } 43 | } 44 | 45 | public get productName(): string { 46 | if (this.products && !this.isNew) { 47 | return this.products.find(p => p.id === this.CurrentEntity.productId).name; 48 | } 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/angular/src/app/order/order-detail/order-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/order/order-detail/order-detail.component.css -------------------------------------------------------------------------------- /client/angular/src/app/order/order-detail/order-detail.component.html: -------------------------------------------------------------------------------- 1 |

Manage Order

2 | 3 | 4 |
5 | {{ viewModel.id }} 6 |
7 | 8 |
9 | 10 | 13 |
{{viewModel.getErrorMessageFor('customerId')}}
14 |
15 | 16 |
17 | 21 | 22 |
23 | 24 |
25 | {{ viewModel.orderTotal | currency }} 26 |
27 | 28 |
29 | 32 | 35 | 38 |
-------------------------------------------------------------------------------- /client/angular/src/app/order/order-detail/order-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderDetailComponent } from './order-detail.component'; 4 | 5 | describe('OrderDetailComponent', () => { 6 | let component: OrderDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OrderDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/order/order-list/order-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/order/order-list/order-list.component.css -------------------------------------------------------------------------------- /client/angular/src/app/order/order-list/order-list.component.html: -------------------------------------------------------------------------------- 1 |

Orders

2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 |
6 | 9 | Create New 10 | 11 | IdOrder DateCustomerTotalStatus
{{ item.id }}{{ item.orderDate }}{{ viewModel.getCustomerNameFor(item.customerId) }}{{ viewModel.getTotalFor(item.id) | currency }}{{ viewModel.getStatusFor(item.id) }} 29 | 34 |
38 | -------------------------------------------------------------------------------- /client/angular/src/app/order/order-list/order-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderListComponent } from './order-list.component'; 4 | 5 | describe('OrderListComponent', () => { 6 | let component: OrderListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OrderListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-detail/product-detail-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { EntityViewModelBase } from '../../entity-view-model-base'; 2 | import { Product, ViewModelArgs, Category } from '../../contracts'; 3 | import { CategoryListViewModel } from '../../category/category-list/category-list-viewmodel'; 4 | import { ProductService } from '../../services/product.service'; 5 | import { Injectable } from '@angular/core'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class ProductDetailViewModel extends EntityViewModelBase { 9 | 10 | constructor(productService: ProductService, private categoryListVM: CategoryListViewModel) { 11 | super(productService); 12 | } 13 | 14 | async loadData(args: ViewModelArgs): Promise { 15 | super.loadData(args), 16 | this.categoryListVM.loadData(); 17 | } 18 | 19 | get isBusy() { 20 | return super['isBusy'] || this.categoryListVM.isBusy; 21 | } 22 | 23 | get name(): string { 24 | return this.CurrentEntity.name; 25 | } 26 | 27 | set name(value: string) { 28 | this.setValue('name', value); 29 | } 30 | 31 | get price(): number { 32 | return this.CurrentEntity.price; 33 | } 34 | 35 | set price(value: number) { 36 | this.setValue('price', value); 37 | } 38 | 39 | get categoryId(): string { 40 | return this.CurrentEntity.categoryId || ''; 41 | } 42 | 43 | set categoryId(value: string) { 44 | this.setValue('categoryId', value); 45 | } 46 | 47 | get categories(): Category[] { 48 | const defaultItem = { name: 'Select Category ...', id: '' } as Category; 49 | return [defaultItem, ...this.categoryListVM.data]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-detail/product-detail.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/product/product-detail/product-detail.component.css -------------------------------------------------------------------------------- /client/angular/src/app/product/product-detail/product-detail.component.html: -------------------------------------------------------------------------------- 1 |

Manage Product

2 | 3 |
4 | 5 | 6 |
{{viewModel.getErrorMessageFor('name')}}
7 |
8 | 9 |
10 | 11 | 12 |
{{viewModel.getErrorMessageFor('price')}}
13 |
14 | 15 |
16 | 17 | 20 |
{{viewModel.getErrorMessageFor('categoryId')}}
21 |
22 | 23 |
24 | 27 | 30 |
31 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-detail/product-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductDetailComponent } from './product-detail.component'; 4 | 5 | describe('ProductDetailComponent', () => { 6 | let component: ProductDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-detail/product-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Location } from '@angular/common'; 4 | import { ProductDetailViewModel } from './product-detail-viewmodel'; 5 | import { Product, ViewModelArgs } from '../../contracts'; 6 | import { NotificationMessenger } from '../../notification-messenger'; 7 | 8 | @Component({ 9 | selector: 'app-product-detail', 10 | templateUrl: './product-detail.component.html', 11 | styleUrls: ['./product-detail.component.css'] 12 | }) 13 | export class ProductDetailComponent implements OnInit { 14 | 15 | constructor( 16 | private route: ActivatedRoute, 17 | private location: Location, 18 | public viewModel: ProductDetailViewModel, 19 | private notificationMessenger: NotificationMessenger) { } 20 | 21 | public async ngOnInit(): Promise { 22 | let productId = this.route.snapshot.params['id']; 23 | if (productId.toLowerCase() === 'new') { productId = null; } 24 | this.viewModel.loadData({ 25 | entityID: productId 26 | } as ViewModelArgs); 27 | } 28 | 29 | public goBack(): void { 30 | this.location.back(); 31 | } 32 | 33 | public async save(): Promise { 34 | if (await this.viewModel.save()) { 35 | this.notificationMessenger.info('Save successful'); 36 | this.goBack(); 37 | } else { 38 | this.notificationMessenger.error('Save failed. Please try again.'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-list/product-list-viewmodel.ts: -------------------------------------------------------------------------------- 1 | import { ListViewModelBase } from '../../list-view-model-base'; 2 | import { Product } from '../../contracts'; 3 | import { ProductService } from '../../services/product.service'; 4 | import { Injectable } from '@angular/core'; 5 | import { ProductEventAggregator } from '../../event-aggregators/product-event-aggregator'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class ProductListViewModel extends ListViewModelBase { 9 | constructor(protected service: ProductService, productEventAggregator: ProductEventAggregator) { 10 | super(service); 11 | productEventAggregator.insert.subscribe(() => this.loadData()); 12 | productEventAggregator.update.subscribe(() => this.loadData()); 13 | productEventAggregator.delete.subscribe(() => this.loadData()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-list/product-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/app/product/product-list/product-list.component.css -------------------------------------------------------------------------------- /client/angular/src/app/product/product-list/product-list.component.html: -------------------------------------------------------------------------------- 1 |

Products

2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 |
6 | 7 | Create New 8 | 9 | Name
{{ product.name }} 19 | 24 |
-------------------------------------------------------------------------------- /client/angular/src/app/product/product-list/product-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductListComponent } from './product-list.component'; 4 | 5 | describe('ProductListComponent', () => { 6 | let component: ProductListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/angular/src/app/product/product-list/product-list.component.ts: -------------------------------------------------------------------------------- 1 | import { OnInit, Component } from '@angular/core'; 2 | import { ProductListViewModel } from './product-list-viewmodel'; 3 | 4 | @Component({ 5 | selector: 'app-product-list', 6 | templateUrl: './product-list.component.html', 7 | styleUrls: ['./product-list.component.css'] 8 | }) 9 | export class ProductListComponent implements OnInit { 10 | 11 | constructor(private viewModel: ProductListViewModel) { 12 | } 13 | 14 | public ngOnInit(): void { 15 | this.viewModel.loadData(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/canDeleteCategoryRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | import { ProductService } from '../services/product.service'; 3 | 4 | export class CanDeleteCategoryRule extends Rule { 5 | 6 | constructor(private categoryId: string, private productService: ProductService) { 7 | super(); 8 | } 9 | 10 | protected async _onValidate(): Promise { 11 | const result = await this.productService.getByCategoryCommand(this.categoryId).execute(); 12 | if (result.value && result.value.length > 0) { 13 | super._invalidate('This category is associated with one or more products and cannot be deleted'); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/canDeleteCustomerRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | import { OrderService } from '../services/order.service'; 3 | 4 | export class CanDeleteCustomerRule extends Rule { 5 | 6 | constructor(private customerId: string, private orderService: OrderService) { 7 | super(); 8 | } 9 | 10 | protected async _onValidate(): Promise { 11 | const result = await this.orderService.getByCustomerCommand(this.customerId).execute(); 12 | if (result.value && result.value.length > 0) { 13 | super._invalidate('This customer is associated with one or more orders and cannot be deleted'); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/canSubmitOrderRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | import { OrderItem } from '../contracts'; 3 | 4 | export class CanSubmitOrderItemRule extends Rule { 5 | 6 | constructor(private orderItem: OrderItem) { 7 | super(); 8 | } 9 | 10 | protected _onValidate(): Promise { 11 | if (this.orderItem.status !== 'PENDING') { 12 | super._invalidate(`Order item ${this.orderItem.id} must be in a pending state to be submitted`); 13 | } 14 | return Promise.resolve(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/fieldLengthRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | 3 | export class FieldLengthRule extends Rule { 4 | 5 | constructor(private field: string, private value: string, private length) { 6 | super(); 7 | } 8 | 9 | protected _onValidate(): Promise { 10 | if (this.value && this.value.length > this.length) { 11 | this.association = this.field; 12 | this._invalidate(this.field + ' accepts a max length of ' + this.length); 13 | } 14 | return Promise.resolve(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/fieldRequiredRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | 3 | export class FieldRequiredRule extends Rule { 4 | 5 | constructor(private field: string, private data: object, private fieldDisplay?: string) { 6 | super(); 7 | } 8 | 9 | protected _onValidate(): Promise { 10 | if (!this.data[this.field]) { 11 | this.association = this.field; 12 | this._invalidate((this.fieldDisplay || this.field) + ' is required'); 13 | } 14 | return Promise.resolve(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/fieldTypeRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | 3 | export class FieldTypeRule extends Rule { 4 | 5 | constructor(private field: string, private value: any, private type, private fieldDisplay?: string) { 6 | super(); 7 | } 8 | 9 | protected _onValidate(): Promise { 10 | if (this.value && typeof this.value !== this.type) { 11 | this.association = this.field; 12 | this._invalidate(`Invalid type supplied for ${this.fieldDisplay || this.field}, expected ${this.type}`); 13 | } 14 | return Promise.resolve(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/name-rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | 3 | export class NameRule extends Rule { 4 | 5 | constructor(private name: string) { 6 | super(); 7 | } 8 | 9 | protected _onValidate(): Promise { 10 | if (this.name === 'aaron han') { 11 | super._invalidate('Name cannot be aaron han'); 12 | } 13 | return Promise.resolve(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/orderItemAmountValidityRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | import { OrderItem, Product } from '../contracts'; 3 | 4 | export class OrderItemAmountValidityRule extends Rule { 5 | 6 | constructor(private orderItem: OrderItem, private product: Product) { 7 | super(); 8 | } 9 | 10 | protected _onValidate(): Promise { 11 | if (this.orderItem.amount !== this.product.price * this.orderItem.quantity) { 12 | this._invalidate(`The amount for the ${this.product.name} order item does 13 | not equal the quantity multiplied by the current price in our system` 14 | ); 15 | } 16 | return Promise.resolve(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/orderItemPriceValidityRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | import { OrderItem, Product } from '../contracts'; 3 | 4 | export class OrderItemPriceValidityRule extends Rule { 5 | 6 | constructor(private orderItem: OrderItem, private product: Product) { 7 | super(); 8 | } 9 | 10 | protected _onValidate(): Promise { 11 | if (this.orderItem.price !== this.product.price) { 12 | this._invalidate(`The price for ${this.product.name} no longer reflects the current price in our system`); 13 | } 14 | return Promise.resolve(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/rules/validOrderItemStatusForUpdateRule.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'peasy-js'; 2 | import { OrderItem } from '../contracts'; 3 | 4 | export class ValidOrderItemStatusForUpdateRule extends Rule { 5 | 6 | constructor(private orderItem: OrderItem) { 7 | super(); 8 | } 9 | 10 | protected _onValidate(): Promise { 11 | if (this.orderItem.status.toUpperCase() === 'BACKORDERED') { 12 | this._invalidate('Backordered items cannot be changed'); 13 | } else if (this.orderItem.status.toUpperCase() === 'SHIPPED') { 14 | this._invalidate('Shipped items cannot be changed'); 15 | } 16 | return Promise.resolve(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/angular/src/app/services/category.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { CategoryService } from './category.service'; 4 | 5 | describe('CategoryService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [CategoryService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([CategoryService], (service: CategoryService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/angular/src/app/services/category.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Category } from '../contracts'; 3 | import { BusinessService, IRule } from 'peasy-js'; 4 | import { DataProxyFactory } from '../data-proxies/data-proxy-factory'; 5 | import { stripAllFieldsFrom } from '../../../../../business_logic/shared/utils'; 6 | import { FieldRequiredRule } from '../rules/fieldRequiredRule'; 7 | import { CanDeleteCategoryRule } from '../rules/canDeleteCategoryRule'; 8 | import { ProductService } from './product.service'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class CategoryService extends BusinessService { 12 | 13 | constructor(proxyFactory: DataProxyFactory, private productService: ProductService) { 14 | super(proxyFactory.categoryDataProxy); 15 | } 16 | 17 | _onInsertCommandInitialization(category: Category, context: Object): Promise { 18 | stripAllFieldsFrom(category).except(['name', 'parentid']); 19 | return Promise.resolve(); 20 | } 21 | 22 | _getRulesForInsertCommand(category: Category, context: Object): Promise { 23 | return Promise.resolve([ 24 | new FieldRequiredRule('name', category) 25 | ]); 26 | } 27 | 28 | _onUpdateCommandInitialization(category: Category, context: Object): Promise { 29 | stripAllFieldsFrom(category).except(['id', 'name', 'parentid']); 30 | return Promise.resolve(); 31 | } 32 | 33 | _getRulesForUpdateCommand(category: Category, context: Object): Promise { 34 | return Promise.resolve([ 35 | new FieldRequiredRule('id', category), 36 | new FieldRequiredRule('name', category) 37 | ]); 38 | } 39 | 40 | _getRulesForDestroyCommand(id: string, context: Object): Promise { 41 | return Promise.resolve([ 42 | new CanDeleteCategoryRule(id, this.productService) 43 | ]); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /client/angular/src/app/services/customer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { CustomerService } from './customer.service'; 4 | 5 | describe('CustomerService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [CustomerService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([CustomerService], (service: CustomerService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/angular/src/app/services/inventory.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { InventoryService } from './inventory.service'; 4 | 5 | describe('Inventory.ServiceService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [InventoryService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([InventoryService], (service: InventoryService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/angular/src/app/services/inventory.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { InventoryItem } from '../contracts'; 3 | import { BusinessService, IRule } from 'peasy-js'; 4 | import { DataProxyFactory } from '../data-proxies/data-proxy-factory'; 5 | import { FieldRequiredRule } from '../rules/fieldRequiredRule'; 6 | import { FieldTypeRule } from '../rules/fieldTypeRule'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class InventoryService extends BusinessService { 10 | 11 | constructor(proxyFactory: DataProxyFactory) { 12 | super(proxyFactory.inventoryDataProxy); 13 | } 14 | 15 | _getRulesForInsertCommand(item: InventoryItem, context: Object): Promise { 16 | return Promise.resolve([ 17 | new FieldRequiredRule('quantityOnHand', item, 'quantity') 18 | .ifValidThenValidate(new FieldTypeRule('quantityOnHand', item.quantityOnHand, 'number', 'quantity')) 19 | ]); 20 | } 21 | 22 | _getRulesForUpdateCommand(item: InventoryItem, context: Object): Promise { 23 | return Promise.resolve([ 24 | new FieldRequiredRule('quantityOnHand', item, 'quantity') 25 | .ifValidThenValidate(new FieldTypeRule('quantityOnHand', item.quantityOnHand, 'number', 'quantity')) 26 | ]); 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /client/angular/src/app/services/order-item.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { OrderItemService } from './order-item.service'; 4 | 5 | describe('OrderItemService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [OrderItemService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([OrderItemService], (service: OrderItemService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/angular/src/app/services/order.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { OrderService } from './order.service'; 4 | 5 | describe('OrderService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [OrderService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([OrderService], (service: OrderService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/angular/src/app/services/product.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ProductServiceService } from './product-service.service'; 4 | 5 | describe('ProductServiceService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ProductServiceService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ProductServiceService], (service: ProductServiceService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/category-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from './store'; 3 | import { Category } from '../contracts'; 4 | import { CategoryEventAggregator } from '../event-aggregators/category-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class CategoryStore extends Store { 8 | constructor(protected eventAggregator: CategoryEventAggregator) { 9 | super(eventAggregator); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/customer-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from './store'; 3 | import { Customer } from '../contracts'; 4 | import { CustomerEventAggregator } from '../event-aggregators/customer-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class CustomerStore extends Store { 8 | constructor(protected eventAggregator: CustomerEventAggregator) { 9 | super(eventAggregator); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/inventory-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from './store'; 3 | import { OrderItemEventAggregator } from '../event-aggregators/order-item-event-aggregator'; 4 | import { InventoryItem, OrderItem } from '../contracts'; 5 | import { InventoryEventAggregator } from '../event-aggregators/inventory-event-aggregator'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class InventoryStore extends Store { 9 | 10 | constructor( 11 | protected eventAggregator: InventoryEventAggregator, 12 | protected orderItemEventAggregator: OrderItemEventAggregator) { 13 | super(eventAggregator); 14 | this.orderItemEventAggregator.update.subscribe(this.onOrderItemUpdated.bind(this)); 15 | } 16 | 17 | private onOrderItemUpdated(orderItem: OrderItem) { 18 | this._data.clear(); 19 | // this.getById() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/order-item-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from './store'; 3 | import { OrderItem } from '../contracts'; 4 | import { OrderItemEventAggregator } from '../event-aggregators/order-item-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class OrderItemStore extends Store { 8 | constructor(protected eventAggregator: OrderItemEventAggregator) { 9 | super(eventAggregator); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/order-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from './store'; 3 | import { Order } from '../contracts'; 4 | import { OrderEventAggregator } from '../event-aggregators/order-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class OrderStore extends Store { 8 | constructor(protected eventAggregator: OrderEventAggregator) { 9 | super(eventAggregator); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/product-store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from './store'; 3 | import { Product } from '../contracts'; 4 | import { ProductEventAggregator } from '../event-aggregators/product-event-aggregator'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class ProductStore extends Store { 8 | constructor(protected eventAggregator: ProductEventAggregator) { 9 | super(eventAggregator); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/store-manager.ts: -------------------------------------------------------------------------------- 1 | import { OrderItem } from '../contracts'; 2 | import { OrderItemStore } from './order-item-store'; 3 | import { OrderItemEventAggregator } from '../event-aggregators/order-item-event-aggregator'; 4 | import { Injectable } from '@angular/core'; 5 | import { InventoryStore } from './inventory-store'; 6 | import { InventoryEventAggregator } from '../event-aggregators/inventory-event-aggregator'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class StoreManager { 10 | 11 | constructor( 12 | private orderItemsStore: OrderItemStore, 13 | private orderItemEventAggregator: OrderItemEventAggregator, 14 | private inventoryStore: InventoryStore, 15 | private inventoryAggregator: InventoryEventAggregator) { 16 | // orderItemEventAggregator.update.subscribe(this.onOrderItemChanged); 17 | } 18 | 19 | private onOrderItemChanged(orderItem: OrderItem): void { 20 | // delete associated item in inventory store 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/angular/src/app/stores/store.ts: -------------------------------------------------------------------------------- 1 | import { EventAggregator } from '../event-aggregators/event-aggregator'; 2 | import { Entity } from '../contracts'; 3 | 4 | export abstract class Store { 5 | 6 | constructor(protected eventAggregator: EventAggregator) { } 7 | 8 | protected _data: Map = new Map(); 9 | 10 | public getAll(): T[] { 11 | return Array.from(this._data.values(), i => { 12 | return Object.assign({}, i); 13 | }); 14 | } 15 | 16 | public getById(id: string): T { 17 | const data = this._data.get(id); 18 | if (data) { 19 | return Object.assign({}, data); 20 | } 21 | return null; 22 | } 23 | 24 | public insertBulk(data: T[]) { 25 | data.forEach(i => this._data.set(i.id, Object.assign({}, i))); 26 | } 27 | 28 | public insert(data: T): void { 29 | this._data.set(data.id, Object.assign({}, data)); 30 | this.eventAggregator.insert.publish(data); 31 | } 32 | 33 | public destroy(id: string): void { 34 | const data = this._data.get(id); 35 | this._data.delete(id); 36 | this.eventAggregator.delete.publish(data); 37 | } 38 | 39 | public update(data: T): void { 40 | this._data.set(data.id, Object.assign({}, data)); 41 | this.eventAggregator.update.publish(data); 42 | } 43 | } 44 | 45 | // @Injectable({ providedIn: 'root' }) 46 | // export class EventEmitter { 47 | 48 | // private _functions = {}; 49 | 50 | // public publish(data) { 51 | // Object.keys(this._functions).forEach(key => this._functions[key](data)); 52 | // } 53 | 54 | // public subscribe(func): number { 55 | // const id = Object.keys(this._functions).length + 1; 56 | // this._functions[id] = func; 57 | // return id; 58 | // } 59 | 60 | // public unsubscribe(subscriptionId: number) { 61 | // delete this._functions[subscriptionId]; 62 | // } 63 | // } 64 | 65 | -------------------------------------------------------------------------------- /client/angular/src/app/view-model-base.ts: -------------------------------------------------------------------------------- 1 | import { ISubscription } from './contracts'; 2 | 3 | export abstract class ViewModelBase { 4 | 5 | protected _isDirty: boolean; 6 | protected _busyCount: number; 7 | protected _errors: any[] = []; 8 | protected _subscriptions: ISubscription[] = []; 9 | 10 | constructor() { 11 | this._busyCount = 0; 12 | } 13 | 14 | public get isDirty(): boolean { 15 | return this._isDirty; 16 | } 17 | 18 | public get isBusy(): boolean { 19 | return this._busyCount > 0; 20 | } 21 | 22 | protected loadStarted(): void { 23 | this._busyCount += 1; 24 | } 25 | 26 | protected loadCompleted(): void { 27 | this._busyCount -= 1; 28 | } 29 | 30 | public get errors(): any[] { 31 | return this._errors; 32 | } 33 | 34 | public getErrorMessageFor(field: string): string { 35 | const error = this._errors.find(e => e.association === field); 36 | return error ? error.message : null; 37 | } 38 | 39 | protected addSubscription(subscription: ISubscription): void { 40 | this._subscriptions.push(subscription); 41 | } 42 | 43 | public listen(): void { 44 | } 45 | 46 | public dispose(): void { 47 | this._subscriptions.forEach(s => s.unsubscribe()); 48 | this._subscriptions = []; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/angular/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/angular/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 -------------------------------------------------------------------------------- /client/angular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/angular/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 | -------------------------------------------------------------------------------- /client/angular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/client/angular/src/favicon.ico -------------------------------------------------------------------------------- /client/angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/angular/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 | }; -------------------------------------------------------------------------------- /client/angular/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 | -------------------------------------------------------------------------------- /client/angular/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .normalfont { 3 | font-weight: normal; 4 | } 5 | 6 | .main { 7 | padding: 10px; 8 | } 9 | 10 | .form-label { 11 | font-weight: bold; 12 | } 13 | 14 | .button-grouping button { 15 | margin-right: 5px; 16 | margin-top: 20px; 17 | } 18 | 19 | .small-button { 20 | font-size: 75%; 21 | padding: .25em .4em; 22 | } 23 | 24 | .field-error { 25 | padding-left: 5px; 26 | font-style: italic; 27 | } 28 | 29 | #toast-container > div { 30 | opacity:1; 31 | } -------------------------------------------------------------------------------- /client/angular/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 | -------------------------------------------------------------------------------- /client/angular/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 | -------------------------------------------------------------------------------- /client/angular/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 | -------------------------------------------------------------------------------- /client/angular/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 | -------------------------------------------------------------------------------- /client/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /client/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-peasy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "build": "webpack --mode development" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.3.3", 14 | "@babel/preset-env": "^7.3.1", 15 | "@babel/preset-react": "^7.0.0", 16 | "babel-loader": "^8.0.5", 17 | "css-loader": "^2.1.1", 18 | "html-loader": "^0.5.5", 19 | "html-webpack-plugin": "^3.2.0", 20 | "style-loader": "^0.23.1", 21 | "webpack": "^4.29.5", 22 | "webpack-cli": "^3.2.3" 23 | }, 24 | "dependencies": { 25 | "axios": "^0.18.0", 26 | "bootstrap": "^4.3.1", 27 | "jquery": "^3.3.1", 28 | "peasy-js": "^2.1.2", 29 | "react": "^16.8.3", 30 | "react-dom": "^16.8.3", 31 | "react-redux": "^6.0.1", 32 | "react-router": "^4.3.1", 33 | "react-router-dom": "^4.3.1", 34 | "redux": "^4.0.1", 35 | "redux-immutable-state-invariant": "^2.1.0", 36 | "redux-thunk": "^2.3.0", 37 | "toastr": "^2.1.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/react/src/actions/asyncStatusActions.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export function beginAsyncInvocation() { 4 | return { type: constants.actions.BEGIN_ASYNC_INVOCATION }; 5 | } 6 | 7 | export function endAsyncInvocation() { 8 | return { type: constants.actions.END_ASYNC_INVOCATION }; 9 | } -------------------------------------------------------------------------------- /client/react/src/actions/categoryActions.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | import ordersDotCom from '../businessLogic'; 3 | import ActionsBase from './ActionsBase'; 4 | 5 | class CategoryActions extends ActionsBase { 6 | 7 | service() { 8 | return ordersDotCom.services.categoryService; 9 | } 10 | 11 | getAllAction(data) { 12 | return { type: constants.actions.LOAD_CATEGORIES_SUCCESS, categories: data }; 13 | } 14 | 15 | insertAction(data) { 16 | return { type: constants.actions.INSERT_CATEGORY_SUCCESS, category: data }; 17 | } 18 | 19 | updateAction(data) { 20 | return { type: constants.actions.UPDATE_CATEGORY_SUCCESS, category: data }; 21 | } 22 | 23 | destroyAction(id) { 24 | return { type: constants.actions.DESTROY_CATEGORY_SUCCESS, id: id }; 25 | } 26 | } 27 | 28 | export default CategoryActions; -------------------------------------------------------------------------------- /client/react/src/actions/customerActions.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | import ordersDotCom from '../businessLogic'; 3 | import ActionsBase from './ActionsBase'; 4 | 5 | class CustomerActions extends ActionsBase { 6 | 7 | service() { 8 | return ordersDotCom.services.customerService; 9 | } 10 | 11 | getAllAction(data) { 12 | return { type: constants.actions.LOAD_CUSTOMERS_SUCCESS, customers: data }; 13 | } 14 | 15 | insertAction(data) { 16 | return { type: constants.actions.INSERT_CUSTOMER_SUCCESS, customer: data }; 17 | } 18 | 19 | updateAction(data) { 20 | return { type: constants.actions.UPDATE_CUSTOMER_SUCCESS, customer: data }; 21 | } 22 | 23 | destroyAction(id) { 24 | return { type: constants.actions.DESTROY_CUSTOMER_SUCCESS, id: id }; 25 | } 26 | } 27 | 28 | export default CustomerActions; -------------------------------------------------------------------------------- /client/react/src/actions/inventoryItemActions.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | import ordersDotCom from '../businessLogic'; 3 | import ActionsBase from './ActionsBase'; 4 | 5 | class InventoryItemActions extends ActionsBase { 6 | 7 | service() { 8 | return ordersDotCom.services.inventoryItemService; 9 | } 10 | 11 | getByIdAction(data) { 12 | return { type: constants.actions.GET_INVENTORY_ITEM_SUCCESS, inventoryItem: data }; 13 | } 14 | 15 | getAllAction(data) { 16 | return { type: constants.actions.LOAD_INVENTORY_ITEMS_SUCCESS, inventoryItems: data }; 17 | } 18 | 19 | updateAction(data) { 20 | return { type: constants.actions.UPDATE_INVENTORY_ITEM_SUCCESS, inventoryItem: data }; 21 | } 22 | 23 | destroyAction(id) { 24 | return { type: constants.actions.DESTROY_INVENTORY_ITEM_SUCCESS, id: id }; 25 | } 26 | 27 | destroy(id) { 28 | var self = this; 29 | return function(dispatch, getState) { 30 | return dispatch(self.destroyAction(id)); 31 | } 32 | } 33 | } 34 | 35 | export default InventoryItemActions; -------------------------------------------------------------------------------- /client/react/src/actions/orderActions.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | import ordersDotCom from '../businessLogic'; 3 | import ActionsBase from './ActionsBase'; 4 | import OrderItemActions from './orderItemActions'; 5 | 6 | let orderItemActions = new OrderItemActions(); 7 | 8 | class OrderActions extends ActionsBase { 9 | 10 | service() { 11 | return ordersDotCom.services.orderService; 12 | } 13 | 14 | getAllAction(data) { 15 | return { type: constants.actions.LOAD_ORDERS_SUCCESS, orders: data }; 16 | } 17 | 18 | insertAction(data) { 19 | return { type: constants.actions.INSERT_ORDER_SUCCESS, order: data }; 20 | } 21 | 22 | updateAction(data) { 23 | return { type: constants.actions.UPDATE_ORDER_SUCCESS, order: data }; 24 | } 25 | 26 | destroyAction(id) { 27 | return { type: constants.actions.DESTROY_ORDER_SUCCESS, id: id }; 28 | } 29 | 30 | submitOrder(id) { 31 | var self = this; 32 | return function(dispatch, getState) { 33 | var orderItems = getState().orderItems; 34 | var submittableItems = orderItems.filter(i => { 35 | return i.orderId === id && i.status === "PENDING" 36 | }) 37 | var foo = submittableItems.map(i => { 38 | return dispatch(orderItemActions.submitOrderItem(i.id)); 39 | }); 40 | return Promise.all(foo); 41 | } 42 | } 43 | } 44 | 45 | export default OrderActions; -------------------------------------------------------------------------------- /client/react/src/actions/productActions.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | import ordersDotCom from '../businessLogic'; 3 | import ActionsBase from './ActionsBase'; 4 | 5 | class ProductActions extends ActionsBase { 6 | 7 | service() { 8 | return ordersDotCom.services.productService; 9 | } 10 | 11 | getAllAction(data) { 12 | return { type: constants.actions.LOAD_PRODUCTS_SUCCESS, products: data }; 13 | } 14 | 15 | insertAction(data) { 16 | return { type: constants.actions.INSERT_PRODUCT_SUCCESS, product: data }; 17 | } 18 | 19 | updateAction(data) { 20 | return { type: constants.actions.UPDATE_PRODUCT_SUCCESS, product: data }; 21 | } 22 | 23 | destroyAction(id) { 24 | return { type: constants.actions.DESTROY_PRODUCT_SUCCESS, id: id }; 25 | } 26 | } 27 | 28 | export default ProductActions; -------------------------------------------------------------------------------- /client/react/src/commandInvoker.js: -------------------------------------------------------------------------------- 1 | import {beginAsyncInvocation, endAsyncInvocation} from './actions/asyncStatusActions'; 2 | 3 | class CommandInvoker { 4 | 5 | constructor(dispatch, logger = new Logger()) { 6 | this._dispatch = dispatch; 7 | this._logger = logger; 8 | } 9 | 10 | invoke(command, successAction) { 11 | this._dispatch(beginAsyncInvocation()); 12 | return new Promise((resolve, reject) => { 13 | command.execute((err, result) => { 14 | this._dispatch(endAsyncInvocation()); 15 | if (err) return reject(err); 16 | if (result.success) { 17 | this._dispatch(successAction(result.value)); 18 | } 19 | resolve(result); 20 | }); 21 | }); 22 | } 23 | } 24 | 25 | class Logger { 26 | logError(message) { 27 | console.log("LOG:ERROR:", message); 28 | } 29 | } 30 | 31 | export default CommandInvoker; -------------------------------------------------------------------------------- /client/react/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import Header from './common/Header'; 3 | import {connect} from 'react-redux'; 4 | import { withRouter } from 'react-router-dom' 5 | 6 | class App extends React.Component { 7 | getStyle() { 8 | var style = this.props.isBusy ? {} : { display: 'none' }; 9 | return style; 10 | } 11 | 12 | render() { 13 | console.log('FOOOOOOOOOOOOOO', this.props.children) 14 | return ( 15 |
16 |
17 | {this.props.children} 18 |
19 | ); 20 | } 21 | } 22 | 23 | // App.propTypes = { 24 | // children: PropTypes.object.isRequired 25 | // }; 26 | 27 | function mapStateToProps(state, ownProps) { 28 | return { 29 | isBusy: state.asyncInvocationsInProgress > 0 30 | }; 31 | } 32 | 33 | export default withRouter(connect(mapStateToProps)(App)); -------------------------------------------------------------------------------- /client/react/src/components/category/CategoryForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | 4 | const CategoryForm = ({viewModel, onSave, onChange, saving, errors, onCancel}) => { 5 | return ( 6 |
7 | 13 | 14 |
15 | 19 | 20 | 24 |
25 | 26 | 27 | ); 28 | }; 29 | 30 | export default CategoryForm; -------------------------------------------------------------------------------- /client/react/src/components/category/ManageCategory.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import CategoryActions from '../../actions/categoryActions'; 5 | import CategoryForm from './CategoryForm'; 6 | import ManageEntityBase from '../common/ManageEntityBase'; 7 | import constants from '../../constants'; 8 | import CategoryViewModel from '../../viewModels/categoryViewModel'; 9 | 10 | let categoryActions = new CategoryActions(); 11 | 12 | class ManageCategory extends ManageEntityBase { 13 | 14 | _saveAction(viewModel) { 15 | return categoryActions.save(viewModel.entity); 16 | } 17 | 18 | _redirectUri(savedEntity) { 19 | return constants.routes.CATEGORIES; 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |

Manage Category

26 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | function mapStateToProps(state, ownProps) { 39 | return { 40 | viewModel: new CategoryViewModel(ownProps.match.params.id, state.categories) 41 | }; 42 | } 43 | 44 | export default connect(mapStateToProps)(ManageCategory); -------------------------------------------------------------------------------- /client/react/src/components/common/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import constants from '../../constants'; 4 | 5 | let routes = constants.routes; 6 | 7 | const Header = () => { 8 | return ( 9 | 28 | ); 29 | }; 30 | 31 | export default Header; -------------------------------------------------------------------------------- /client/react/src/components/common/ListViewBase.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import toastr from 'toastr'; 3 | 4 | class ListViewBase extends React.Component { 5 | 6 | constructor(props, context) { 7 | super(props, context); 8 | } 9 | 10 | _destroyAction(id) {} 11 | 12 | destroy(id) { 13 | var self = this; 14 | return function() { 15 | return self.props.dispatch(self._destroyAction(id)) 16 | .then(result => { 17 | if (!result.success) self.handleErrors(result.errors); 18 | }); 19 | } 20 | } 21 | 22 | handleErrors(errors) { 23 | if (Array.isArray(errors)) { 24 | toastr.error(errors[0].message); 25 | } else { 26 | toastr.error(errors.message); 27 | } 28 | } 29 | } 30 | 31 | export default ListViewBase; 32 | -------------------------------------------------------------------------------- /client/react/src/components/common/SelectInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SelectInput = ({name, label, onChange, defaultOption, value, errors, options}) => { 4 | 5 | let wrapperClass = 'form-group'; 6 | if (getError()) { 7 | wrapperClass += ' has-error'; 8 | } 9 | 10 | function getError() { 11 | if (!errors) return; 12 | var error = errors.find(e => e.association === name); 13 | if (error) { 14 | return error.message; 15 | } 16 | } 17 | 18 | function errorDisplay() { 19 | var error = getError(); 20 | if (error) { 21 | return ( 22 |
{error}
23 | ); 24 | } 25 | return null; 26 | } 27 | 28 | return ( 29 |
30 | 31 |
32 | 44 | {errorDisplay()} 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default SelectInput; -------------------------------------------------------------------------------- /client/react/src/components/common/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TextInput = ({name, label, onChange, placeholder, value, errors }) => { 4 | 5 | let wrapperClass = 'form-group'; 6 | if (getError()) { 7 | wrapperClass += ' has-error'; 8 | } 9 | 10 | function getError() { 11 | if (!errors) return; 12 | var error = errors.find(e => e.association === name); 13 | if (error) { 14 | return error.message; 15 | } 16 | } 17 | 18 | function errorDisplay() { 19 | var error = getError(); 20 | if (error) { 21 | return ( 22 |
{error}
23 | ); 24 | } 25 | return null; 26 | } 27 | 28 | return ( 29 |
30 | 31 |
32 | 39 | {errorDisplay()} 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default TextInput; -------------------------------------------------------------------------------- /client/react/src/components/customer/CustomerForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | 4 | const CustomerForm = ({ viewModel, onSave, onChange, saving, errors, onCancel }) => { 5 | return ( 6 |
7 | 13 | 14 |
15 | 19 | 20 | 24 | 25 |
26 | 27 | 28 | ); 29 | }; 30 | 31 | export default CustomerForm; -------------------------------------------------------------------------------- /client/react/src/components/customer/ManageCustomer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import CustomerActions from '../../actions/customerActions'; 4 | import CustomerForm from './CustomerForm'; 5 | import ManageEntityBase from '../common/ManageEntityBase'; 6 | import constants from '../../constants'; 7 | import CustomerViewModel from '../../viewModels/customerViewModel'; 8 | 9 | let customerActions = new CustomerActions(); 10 | 11 | class ManageCustomer extends ManageEntityBase { 12 | 13 | _saveAction(viewModel) { 14 | return customerActions.save(viewModel.entity); 15 | } 16 | 17 | _redirectUri(savedEntity) { 18 | return constants.routes.CUSTOMERS; 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |

Manage Customer

25 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | function mapStateToProps(state, ownProps) { 38 | return { 39 | viewModel: new CustomerViewModel(ownProps.match.params.id, state.customers) 40 | }; 41 | 42 | } 43 | 44 | export default connect(mapStateToProps)(ManageCustomer); -------------------------------------------------------------------------------- /client/react/src/components/inventory/InventoryItemForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | 4 | const InventoryItemForm = ({viewModel, onSave, onChange, saving, errors, onCancel}) => { 5 | return ( 6 |
7 | 8 |
9 | 10 |
{viewModel.associatedProduct.name}
11 |
12 | 13 | 19 | 20 |
21 | 25 | 26 | 30 |
31 | 32 | 33 | ); 34 | }; 35 | 36 | export default InventoryItemForm; -------------------------------------------------------------------------------- /client/react/src/components/inventory/InventoryItemsView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import constants from '../../constants'; 5 | 6 | class InventoryItemsView extends React.Component { 7 | 8 | constructor(props, context) { 9 | super(props, context); 10 | this.inventoryItemRow = this.inventoryItemRow.bind(this); 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 |

Inventory Items

17 | {this.InventoryItemsList()} 18 |
19 | ); 20 | } 21 | 22 | InventoryItemsList() { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {this.props.inventoryItems.map(this.inventoryItemRow)} 33 | 34 |
NameQuantity
35 | ); 36 | } 37 | 38 | inventoryItemRow(inventoryItem, index) { 39 | return ( 40 | 41 | 42 | {inventoryItem.name} 43 | 44 | 45 | {inventoryItem.quantityOnHand} 46 | 47 | 48 | ); 49 | } 50 | 51 | } 52 | 53 | function mapStateToProps(state, ownProps) { 54 | var items = state.inventoryItems.map(i => { 55 | var associatedProduct = state.products.find(p => p.id === i.productId); 56 | return Object.assign({}, i, { name: associatedProduct.name }); 57 | }); 58 | return { 59 | inventoryItems: items 60 | }; 61 | } 62 | 63 | export default connect(mapStateToProps)(InventoryItemsView); -------------------------------------------------------------------------------- /client/react/src/components/inventory/ManageInventoryItem.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import InventoryItemActions from '../../actions/inventoryItemActions'; 4 | import InventoryItemForm from './InventoryItemForm'; 5 | import ManageEntityBase from '../common/ManageEntityBase'; 6 | import constants from '../../constants'; 7 | import InventoryItemViewModel from '../../viewModels/inventoryItemViewModel'; 8 | 9 | let inventoryItemActions = new InventoryItemActions(); 10 | 11 | class ManageInventoryItem extends ManageEntityBase { 12 | 13 | _saveAction(viewModel) { 14 | return inventoryItemActions.save(viewModel.entity); 15 | } 16 | 17 | _redirectUri() { 18 | return constants.routes.INVENTORY_ITEMS; 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |

Manage Inventory Item

25 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | function mapStateToProps(state, ownProps) { 38 | return { 39 | viewModel: new InventoryItemViewModel(ownProps.match.params.id, state.inventoryItems, state.products) 40 | }; 41 | } 42 | 43 | export default connect(mapStateToProps)(ManageInventoryItem); -------------------------------------------------------------------------------- /client/react/src/components/order/OrderForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SelectInput from '../common/SelectInput'; 3 | import OrderItemsView from '../orderItem/orderItemsView'; 4 | 5 | const OrderForm = ({viewModel, onSave, onChange, onSubmitOrder, saving, errors, onCancel, dispatch}) => { 6 | return ( 7 |
8 | 9 |
10 | {viewModel.entity.id} 11 |
12 | 13 | 21 | 22 | 25 | 26 |
27 | 31 | 32 | 35 | 36 | 43 | 44 |
45 | 46 | 47 | ); 48 | 49 | function getStyle() { 50 | return viewModel.hasPendingItems ? {} : { visibility: 'hidden' }; 51 | } 52 | }; 53 | 54 | 55 | export default OrderForm; -------------------------------------------------------------------------------- /client/react/src/components/orderItem/ManageOrderItem.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import OrderItemActions from '../../actions/orderItemActions'; 4 | import OrderItemForm from './orderItemForm'; 5 | import ManageEntityBase from '../common/ManageEntityBase'; 6 | import constants from '../../constants'; 7 | import OrderItemViewModel from '../../viewModels/orderItemViewModel'; 8 | 9 | let orderItemActions = new OrderItemActions(); 10 | 11 | class ManageOrderItem extends ManageEntityBase { 12 | 13 | _saveAction(viewModel) { 14 | return orderItemActions.save(viewModel.entity); 15 | } 16 | 17 | _redirectUri(savedEntity) { 18 | var currentOrder = savedEntity || this.props.viewModel.entity; 19 | return constants.routes.ORDERS + '/' + currentOrder.orderId; 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |

Manage Order Item

26 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | function mapStateToProps(state, ownProps) { 39 | var orderId = ownProps.match.params.id; 40 | var orderItemId = ownProps.match.params.itemid; 41 | return { 42 | viewModel: new OrderItemViewModel( 43 | orderId, 44 | orderItemId, 45 | state.orderItems, 46 | state.categories, 47 | state.products, 48 | state.inventoryItems 49 | ) 50 | } 51 | } 52 | 53 | export default connect(mapStateToProps)(ManageOrderItem); -------------------------------------------------------------------------------- /client/react/src/components/product/ManageProduct.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import ProductActions from '../../actions/productActions'; 4 | import InventoryItemActions from '../../actions/inventoryItemActions'; 5 | import ProductForm from './ProductForm'; 6 | import ManageEntityBase from '../common/ManageEntityBase'; 7 | import constants from '../../constants'; 8 | import ProductViewModel from '../../viewModels/productViewModel'; 9 | 10 | let productActions = new ProductActions(); 11 | let inventoryItemActions = new InventoryItemActions(); 12 | 13 | class ManageProduct extends ManageEntityBase { 14 | 15 | _saveAction(viewModel) { 16 | return productActions.save(viewModel.entity); 17 | } 18 | 19 | _redirectUri(savedEntity) { 20 | return constants.routes.PRODUCTS; 21 | } 22 | 23 | save(event) { 24 | var self = this; 25 | return super.save(event) 26 | .then((result) => { 27 | if (result && result.success) { 28 | return self.props.dispatch(inventoryItemActions.loadData()); 29 | } 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 |

Manage Product

37 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | function mapStateToProps(state, ownProps) { 50 | return { 51 | viewModel: new ProductViewModel(ownProps.match.params.id, state.products, state.categories) 52 | }; 53 | } 54 | 55 | export default connect(mapStateToProps)(ManageProduct); -------------------------------------------------------------------------------- /client/react/src/components/product/ProductForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | import SelectInput from '../common/SelectInput'; 4 | 5 | const ProductForm = ({viewModel, onSave, onChange, saving, errors, onCancel}) => { 6 | return ( 7 |
8 | 14 | 15 | 21 | 22 | 28 | 29 | 37 | 38 |
39 | 43 | 44 | 48 |
49 | 50 | 51 | ); 52 | }; 53 | 54 | export default ProductForm; -------------------------------------------------------------------------------- /client/react/src/index.css: -------------------------------------------------------------------------------- 1 | .main-content { 2 | padding: 10px; 3 | } 4 | 5 | .normalfont { 6 | font-weight: normal; 7 | } 8 | 9 | .form-label { 10 | font-weight: bold; 11 | } 12 | 13 | .button-grouping button { 14 | margin-right: 5px; 15 | margin-top: 20px; 16 | } 17 | 18 | .button-grouping input[type=button] { 19 | margin-right: 5px; 20 | margin-top: 20px; 21 | } 22 | 23 | .small-button { 24 | font-size: 75%; 25 | padding: .25em .4em; 26 | } 27 | 28 | .field-error { 29 | padding-left: 5px; 30 | font-style: italic; 31 | } 32 | -------------------------------------------------------------------------------- /client/react/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Orders.com 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/react/src/index.js: -------------------------------------------------------------------------------- 1 | // import 'babel-polyfill'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { Provider } from 'react-redux'; 6 | import routes from './routes'; 7 | import configureStore from './store/configureStore'; 8 | import ordersDotCom from './businessLogic'; 9 | import CustomerActions from './actions/customerActions'; 10 | import CategoryActions from './actions/categoryActions'; 11 | import OrderActions from './actions/orderActions'; 12 | import OrderItemActions from './actions/orderItemActions'; 13 | import ProductActions from './actions/productActions'; 14 | import InventoryItemActions from './actions/inventoryItemActions'; 15 | import Header from './components/common/Header'; 16 | import '../node_modules/toastr/build/toastr.min.css'; 17 | import './index.css'; 18 | 19 | const store = configureStore(); 20 | const customerActions = new CustomerActions(); 21 | const categoryActions = new CategoryActions(); 22 | const orderActions = new OrderActions(); 23 | const orderItemActions = new OrderItemActions(); 24 | const productActions = new ProductActions(); 25 | const inventoryItemActions = new InventoryItemActions(); 26 | 27 | store.dispatch(productActions.loadData()); 28 | store.dispatch(customerActions.loadData()); 29 | store.dispatch(categoryActions.loadData()); 30 | store.dispatch(inventoryItemActions.loadData()); 31 | store.dispatch(orderActions.loadData()); 32 | store.dispatch(orderItemActions.loadData()); 33 | 34 | render( 35 | 36 | 37 |
38 |
39 |
40 | { routes } 41 |
42 |
43 |
44 |
, document.getElementById('app') 45 | ); 46 | 47 | ( function( ordersDotCom ) { 48 | 49 | } )( ordersDotCom ); -------------------------------------------------------------------------------- /client/react/src/reducers/asyncStatusReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | function actionTypeEndsInSuccess(type) { 4 | return type.substring(type.length - 8) === "_SUCCESS"; 5 | } 6 | 7 | export default function asyncStatusReducer(state = 0, action) { 8 | if (action.type === constants.actions.BEGIN_ASYNC_INVOCATION) { 9 | return state + 1; 10 | } else if (action.type === constants.actions.END_ASYNC_INVOCATION) { 11 | return state - 1; 12 | } 13 | return state; 14 | }; -------------------------------------------------------------------------------- /client/react/src/reducers/categoryReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export default function categoryReducer(state = [], action) { 4 | switch (action.type) { 5 | case constants.actions.INSERT_CATEGORY_SUCCESS: 6 | return [...state, Object.assign({}, action.category)]; 7 | case constants.actions.UPDATE_CATEGORY_SUCCESS: 8 | return [...state.filter(category => category.id !== action.category.id), 9 | Object.assign({}, action.category) 10 | ]; 11 | case constants.actions.DESTROY_CATEGORY_SUCCESS: 12 | return [...state.filter(category => category.id !== action.id)]; 13 | case constants.actions.LOAD_CATEGORIES_SUCCESS: 14 | return action.categories; 15 | default: 16 | return state; 17 | } 18 | }; -------------------------------------------------------------------------------- /client/react/src/reducers/customerReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export default function customerReducer(state = [], action) { 4 | switch (action.type) { 5 | case constants.actions.INSERT_CUSTOMER_SUCCESS: 6 | return [...state, Object.assign({}, action.customer)]; 7 | case constants.actions.UPDATE_CUSTOMER_SUCCESS: 8 | return [...state.filter(customer => customer.id !== action.customer.id), 9 | Object.assign({}, action.customer) 10 | ]; 11 | case constants.actions.DESTROY_CUSTOMER_SUCCESS: 12 | return [...state.filter(customer => customer.id !== action.id)]; 13 | case constants.actions.LOAD_CUSTOMERS_SUCCESS: 14 | return action.customers; 15 | default: 16 | return state; 17 | } 18 | }; -------------------------------------------------------------------------------- /client/react/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import customers from './customerReducer'; 3 | import categories from './categoryReducer'; 4 | import inventoryItems from './inventoryItemReducer'; 5 | import orders from './orderReducer'; 6 | import orderItems from './orderItemReducer'; 7 | import products from './productReducer'; 8 | import asyncInvocationsInProgress from './asyncStatusReducer' 9 | 10 | const rootReducer = combineReducers({ 11 | categories, 12 | customers, 13 | inventoryItems, 14 | orders, 15 | orderItems, 16 | products, 17 | asyncInvocationsInProgress 18 | }); 19 | 20 | export default rootReducer; -------------------------------------------------------------------------------- /client/react/src/reducers/inventoryItemReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export default function inventoryItemReducer(state = [], action) { 4 | switch (action.type) { 5 | case constants.actions.GET_INVENTORY_ITEM_SUCCESS: 6 | return [...state.filter(inventoryItem => inventoryItem.id !== action.inventoryItem.id), 7 | Object.assign({}, action.inventoryItem) 8 | ]; 9 | case constants.actions.INSERT_INVENTORY_ITEM_SUCCESS: 10 | return [...state, Object.assign({}, action.inventoryItem)]; 11 | case constants.actions.UPDATE_INVENTORY_ITEM_SUCCESS: 12 | return [...state.filter(inventoryItem => inventoryItem.id !== action.inventoryItem.id), 13 | Object.assign({}, action.inventoryItem) 14 | ]; 15 | case constants.actions.DESTROY_INVENTORY_ITEM_SUCCESS: 16 | return [...state.filter(inventoryItem => inventoryItem.id !== action.id)]; 17 | case constants.actions.LOAD_INVENTORY_ITEMS_SUCCESS: 18 | return action.inventoryItems; 19 | default: 20 | return state; 21 | } 22 | }; -------------------------------------------------------------------------------- /client/react/src/reducers/orderItemReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export default function orderItemReducer(state = [], action) { 4 | switch (action.type) { 5 | case constants.actions.INSERT_ORDER_ITEM_SUCCESS: 6 | return [...state, Object.assign({}, action.orderItem)]; 7 | case constants.actions.UPDATE_ORDER_ITEM_SUCCESS: 8 | return [...state.filter(orderItem => orderItem.id !== action.orderItem.id), 9 | Object.assign({}, action.orderItem) 10 | ]; 11 | case constants.actions.SUBMIT_ORDER_ITEM_SUCCESS: 12 | return [...state.filter(orderItem => orderItem.id !== action.orderItem.id), 13 | Object.assign({}, action.orderItem) 14 | ]; 15 | case constants.actions.SHIP_ORDER_ITEM_SUCCESS: 16 | return [...state.filter(orderItem => orderItem.id !== action.orderItem.id), 17 | Object.assign({}, action.orderItem) 18 | ]; 19 | case constants.actions.DESTROY_ORDER_ITEM_SUCCESS: 20 | return [...state.filter(orderItem => orderItem.id !== action.id)]; 21 | case constants.actions.DESTROY_BY_ORDER_SUCCESS: 22 | var orderItemIds = action.orderItems.map(i => i.id); 23 | return [...state.filter(orderItem => !orderItemIds.includes(orderItem.id))]; 24 | case constants.actions.LOAD_ORDER_ITEMS_SUCCESS: 25 | return action.orderItems; 26 | default: 27 | return state; 28 | } 29 | }; -------------------------------------------------------------------------------- /client/react/src/reducers/orderReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export default function orderReducer(state = [], action) { 4 | switch (action.type) { 5 | case constants.actions.INSERT_ORDER_SUCCESS: 6 | return [...state, Object.assign({}, action.order)]; 7 | case constants.actions.UPDATE_ORDER_SUCCESS: 8 | return [...state.filter(order => order.id !== action.order.id), 9 | Object.assign({}, action.order) 10 | ]; 11 | case constants.actions.DESTROY_ORDER_SUCCESS: 12 | return [...state.filter(order => order.id !== action.id)]; 13 | case constants.actions.LOAD_ORDERS_SUCCESS: 14 | return action.orders; 15 | default: 16 | return state; 17 | } 18 | }; -------------------------------------------------------------------------------- /client/react/src/reducers/productReducer.js: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | export default function productReducer(state = [], action) { 4 | switch (action.type) { 5 | case constants.actions.INSERT_PRODUCT_SUCCESS: 6 | return [...state, Object.assign({}, action.product)]; 7 | case constants.actions.UPDATE_PRODUCT_SUCCESS: 8 | return [...state.filter(product => product.id !== action.product.id), 9 | Object.assign({}, action.product) 10 | ]; 11 | case constants.actions.DESTROY_PRODUCT_SUCCESS: 12 | return [...state.filter(product => product.id !== action.id)]; 13 | case constants.actions.LOAD_PRODUCTS_SUCCESS: 14 | return action.products; 15 | default: 16 | return state; 17 | } 18 | }; -------------------------------------------------------------------------------- /client/react/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | // import rootReducer from '../reducers/index'; 4 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant'; 5 | import thunk from 'redux-thunk'; 6 | 7 | export default function configureStore(initialState) { 8 | return createStore( 9 | rootReducer, 10 | initialState, 11 | applyMiddleware(thunk, reduxImmutableStateInvariant()) 12 | ); 13 | }; -------------------------------------------------------------------------------- /client/react/src/viewModels/categoryViewModel.js: -------------------------------------------------------------------------------- 1 | import ViewModelBase from './viewModelBase'; 2 | 3 | class CategoryViewModel extends ViewModelBase { 4 | constructor(categoryId, categories) { 5 | super(categoryId, categories); 6 | } 7 | 8 | set name(value) { 9 | this.entity.name = value; 10 | } 11 | 12 | get name() { 13 | return this.entity.name; 14 | } 15 | } 16 | 17 | export default CategoryViewModel; -------------------------------------------------------------------------------- /client/react/src/viewModels/customerViewModel.js: -------------------------------------------------------------------------------- 1 | import ViewModelBase from './viewModelBase'; 2 | 3 | class CustomerViewModel extends ViewModelBase { 4 | constructor(customerId, customers) { 5 | super(customerId, customers); 6 | } 7 | 8 | set name(value) { 9 | this.entity.name = value; 10 | } 11 | 12 | get name() { 13 | return this.entity.name; 14 | } 15 | } 16 | 17 | export default CustomerViewModel; -------------------------------------------------------------------------------- /client/react/src/viewModels/inventoryItemViewModel.js: -------------------------------------------------------------------------------- 1 | import ViewModelBase from './viewModelBase'; 2 | 3 | class InventoryItemViewModel extends ViewModelBase { 4 | constructor(itemId, inventoryItems, products) { 5 | super(itemId, inventoryItems); 6 | this._products = products; 7 | this._associatedProduct = null; 8 | } 9 | 10 | set quantityOnHand(value) { 11 | this.entity.quantityOnHand = value; 12 | } 13 | 14 | get associatedProduct() { 15 | if (!this._associatedProduct) { 16 | this._associatedProduct = {}; 17 | var productId = this.entity.productId; 18 | if (productId) { 19 | this._associatedProduct = this._products.find(p => p.id === productId); 20 | } 21 | } 22 | return this._associatedProduct; 23 | } 24 | 25 | } 26 | 27 | export default InventoryItemViewModel; -------------------------------------------------------------------------------- /client/react/src/viewModels/orderItemLineItemViewModel.js: -------------------------------------------------------------------------------- 1 | import ordersDotCom from '../businessLogic'; 2 | 3 | class OrderItemLineItemViewModel { 4 | 5 | constructor(orderItem, products) { 6 | this._orderItem = orderItem; 7 | this._products = products; 8 | this._associatedProduct = null; 9 | } 10 | 11 | get orderItem() { 12 | return this._orderItem; 13 | } 14 | 15 | get productName() { 16 | return this.product.name; 17 | } 18 | 19 | get product() { 20 | if (!this._associatedProduct) { 21 | this._associatedProduct = this._products.find(p => p.id === this._orderItem.productId); 22 | } 23 | return this._associatedProduct; 24 | } 25 | 26 | get amountFormatted() { 27 | if (this._orderItem.amount) { 28 | return this.formatDollars(this._orderItem.amount); 29 | } 30 | } 31 | 32 | get priceFormatted() { 33 | if (this._orderItem.price) { 34 | return this.formatDollars(this._orderItem.price); 35 | } 36 | } 37 | 38 | formatDollars(value) { 39 | return value.toLocaleString("en-US", {style: "currency", currency: "USD", minimumFractionDigits: 2}) 40 | } 41 | 42 | get submittedOnFormatted() { 43 | var submittedOn = this._orderItem.submittedOn; 44 | if (!submittedOn) return '-'; 45 | return new Date(submittedOn).toLocaleString(); 46 | } 47 | 48 | get shippedOnFormatted() { 49 | var shippedOn = this._orderItem.shippedOn; 50 | if (!shippedOn) return '-'; 51 | return new Date(shippedOn).toLocaleString(); 52 | } 53 | 54 | get canDelete() { 55 | return ordersDotCom.services.orderItemService.canDelete(this._orderItem); 56 | } 57 | 58 | get canShip() { 59 | return ordersDotCom.services.orderItemService.canShip(this._orderItem); 60 | } 61 | } 62 | 63 | export default OrderItemLineItemViewModel; -------------------------------------------------------------------------------- /client/react/src/viewModels/orderLineItemViewModel.js: -------------------------------------------------------------------------------- 1 | import ordersDotCom from '../businessLogic'; 2 | 3 | class OrderLineItemViewModel { 4 | 5 | constructor(order, orderItems, customers) { 6 | this._order = order; 7 | this._orderItems = orderItems; 8 | this._customers = customers; 9 | this._associatedCustomer = null; 10 | this._associatedOrderItems = null; 11 | } 12 | 13 | get orderId() { 14 | return this._order.id; 15 | } 16 | 17 | get orderDate() { 18 | return this._order.orderDate; 19 | } 20 | 21 | get orderDateFormatted() { 22 | return new Date(this.orderDate).toLocaleString(); 23 | } 24 | 25 | get customerName() { 26 | return this.customer.name; 27 | } 28 | 29 | get totalFormatted() { 30 | if (this.total) { 31 | return this.formatDollars(this.total); 32 | } 33 | } 34 | 35 | formatDollars(value) { 36 | return value.toLocaleString("en-US", {style: "currency", currency: "USD", minimumFractionDigits: 2}) 37 | } 38 | 39 | get total() { 40 | if (this.orderItems.length > 0) { 41 | return this.orderItems.map(i => i.amount).reduce((a = 0, b) => a + b); 42 | } 43 | } 44 | 45 | get status() { 46 | return ordersDotCom.services.orderService.status(this.orderItems); 47 | } 48 | 49 | get customer() { 50 | if (!this._associatedCustomer) { 51 | this._associatedCustomer = this._customers.find(c => c.id === this._order.customerId); 52 | } 53 | return this._associatedCustomer; 54 | } 55 | 56 | get orderItems() { 57 | if (!this._associatedOrderItems) { 58 | this._associatedOrderItems = this._orderItems.filter(i => i.orderId === this._order.id); 59 | } 60 | return this._associatedOrderItems; 61 | } 62 | } 63 | 64 | export default OrderLineItemViewModel; -------------------------------------------------------------------------------- /client/react/src/viewModels/productViewModel.js: -------------------------------------------------------------------------------- 1 | import ViewModelBase from './viewModelBase'; 2 | 3 | class ProductViewModel extends ViewModelBase { 4 | constructor(productId, products, categories) { 5 | super(productId, products); 6 | this._categories = categories; 7 | } 8 | 9 | set name(value) { 10 | this.entity.name = value; 11 | } 12 | 13 | set description(value) { 14 | this.entity.description = value; 15 | } 16 | 17 | set price(value) { 18 | this.entity.price = value; 19 | } 20 | 21 | set categoryId(value) { 22 | this.entity.categoryId = value; 23 | } 24 | 25 | get categorySelectValues() { 26 | return this._categories.map(c => { return { text: c.name, value: c.id }}); 27 | } 28 | } 29 | 30 | export default ProductViewModel; -------------------------------------------------------------------------------- /client/react/src/viewModels/viewModelBase.js: -------------------------------------------------------------------------------- 1 | class ViewModelBase { 2 | constructor(id, list) { 3 | this._id = id; 4 | this._list = list; 5 | } 6 | 7 | get entity() { 8 | if (!this._currentEntity) { 9 | this._currentEntity = {}; 10 | if (this._id) { 11 | if (this._list) { 12 | var entity = this._list.find(e => e.id === this._id) 13 | this._currentEntity = Object.assign({}, entity); 14 | } 15 | } 16 | } 17 | return this._currentEntity; 18 | } 19 | 20 | formatDollars(value) { 21 | return value.toLocaleString("en-US", {style: "currency", currency: "USD", minimumFractionDigits: 2}) 22 | } 23 | 24 | } 25 | 26 | export default ViewModelBase; -------------------------------------------------------------------------------- /client/react/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebPackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | filename: 'react.bundle.js', 8 | path: path.resolve(__dirname, '../../dist/react'), 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: "babel-loader" 17 | } 18 | }, 19 | { 20 | test: /\.html$/, 21 | use: [{ 22 | loader: "html-loader" 23 | }] 24 | }, 25 | { 26 | test:/\.css$/, 27 | use:['style-loader','css-loader'] 28 | } 29 | ] 30 | }, 31 | plugins: [ 32 | new HtmlWebPackPlugin({ 33 | template: "./src/index.html", 34 | filename: "./index.html" 35 | }) 36 | ] 37 | }; -------------------------------------------------------------------------------- /data_proxies/http/categoryDataProxy.js: -------------------------------------------------------------------------------- 1 | var HttpDataProxy = require('./httpDataProxy'); 2 | 3 | var CategoryDataProxy = function() { 4 | HttpDataProxy.call(this, 'categories'); 5 | }; 6 | 7 | CategoryDataProxy.prototype = new HttpDataProxy(); 8 | 9 | module.exports = CategoryDataProxy ; 10 | -------------------------------------------------------------------------------- /data_proxies/http/customerDataProxy.js: -------------------------------------------------------------------------------- 1 | var HttpDataProxy = require('./httpDataProxy'); 2 | 3 | var CustomerDataProxy = function() { 4 | HttpDataProxy.call(this, 'customers'); 5 | }; 6 | 7 | CustomerDataProxy.prototype = new HttpDataProxy(); 8 | 9 | module.exports = CustomerDataProxy; 10 | -------------------------------------------------------------------------------- /data_proxies/http/httpDataProxyFactory.js: -------------------------------------------------------------------------------- 1 | var CategoryDataProxy = require('./categoryDataProxy'); 2 | var CustomerDataProxy = require('./customerDataProxy'); 3 | var ProductDataProxy = require('./productDataProxy'); 4 | var InventoryItemDataProxy = require('./inventoryItemDataProxy'); 5 | var OrderDataProxy = require('./orderDataProxy.js'); 6 | var OrderItemDataProxy = require('./orderItemDataProxy.js'); 7 | 8 | module.exports = { 9 | categoryDataProxy: new CategoryDataProxy(), 10 | customerDataProxy: new CustomerDataProxy(), 11 | productDataProxy: new ProductDataProxy(), 12 | inventoryItemDataProxy: new InventoryItemDataProxy(), 13 | orderDataProxy: new OrderDataProxy(), 14 | orderItemDataProxy: new OrderItemDataProxy() 15 | }; 16 | -------------------------------------------------------------------------------- /data_proxies/http/inventoryItemDataProxy.js: -------------------------------------------------------------------------------- 1 | var HttpDataProxy = require('./httpDataProxy'); 2 | var axios = require('axios'); 3 | 4 | var InventoryItemDataProxy = function() { 5 | HttpDataProxy.call(this, 'inventoryitems'); 6 | }; 7 | 8 | InventoryItemDataProxy.prototype = new HttpDataProxy(); 9 | 10 | InventoryItemDataProxy.prototype.getByProduct = function(productId, done) { 11 | this._handleGetListByIdFrom(axios.get(`${this._url}?productid=${productId}`), done); 12 | }; 13 | 14 | module.exports = InventoryItemDataProxy; 15 | -------------------------------------------------------------------------------- /data_proxies/http/orderDataProxy.js: -------------------------------------------------------------------------------- 1 | var HttpDataProxy = require('./httpDataProxy'); 2 | var axios = require('axios'); 3 | 4 | var OrderDataProxy = function() { 5 | HttpDataProxy.call(this, 'orders'); 6 | }; 7 | 8 | OrderDataProxy.prototype = new HttpDataProxy(); 9 | 10 | OrderDataProxy.prototype.getByCustomer = function(customerId, done) { 11 | this._handleGetListByIdFrom(axios.get(`${this._url}?customerid=${customerId}`), done); 12 | }; 13 | 14 | OrderDataProxy.prototype.getByProduct = function(productId, done) { 15 | this._handleGetListByIdFrom(axios.get(`${this._url}?productid=${productId}`), done); 16 | }; 17 | 18 | module.exports = OrderDataProxy; 19 | -------------------------------------------------------------------------------- /data_proxies/http/orderItemDataProxy.js: -------------------------------------------------------------------------------- 1 | var HttpDataProxy = require('./httpDataProxy'); 2 | var axios = require('axios'); 3 | 4 | var OrderItemDataProxy = function() { 5 | HttpDataProxy.call(this, 'orderitems'); 6 | }; 7 | 8 | OrderItemDataProxy.prototype = new HttpDataProxy(); 9 | 10 | OrderItemDataProxy.prototype.getByOrder = function(orderId, done) { 11 | this._handleGetListByIdFrom(axios.get(`${this._url}?orderid=${orderId}`), done); 12 | }; 13 | 14 | OrderItemDataProxy.prototype.submit = function(itemId, done) { 15 | this._handleResponseFrom(axios.post(`${this._url}/${itemId}/submit`, itemId), done); 16 | }; 17 | 18 | OrderItemDataProxy.prototype.ship = function(itemId, done) { 19 | this._handleResponseFrom(axios.post(`${this._url}/${itemId}/ship`, itemId), done); 20 | }; 21 | 22 | module.exports = OrderItemDataProxy; 23 | -------------------------------------------------------------------------------- /data_proxies/http/productDataProxy.js: -------------------------------------------------------------------------------- 1 | var HttpDataProxy = require('./httpDataProxy'); 2 | var axios = require('axios'); 3 | 4 | var ProductDataProxy = function() { 5 | HttpDataProxy.call(this, 'products'); 6 | }; 7 | 8 | ProductDataProxy.prototype = new HttpDataProxy(); 9 | 10 | ProductDataProxy.prototype.getByCategory = function(categoryId, done) { 11 | this._handleGetListByIdFrom(axios.get(`${this._url}?categoryid=${categoryId}`), done); 12 | }; 13 | 14 | module.exports = ProductDataProxy; 15 | -------------------------------------------------------------------------------- /data_proxies/in-memory/inMemoryDataProxy.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var InMemoryDataProxy = function(data) { 4 | this._store = data || []; 5 | }; 6 | 7 | InMemoryDataProxy.prototype.getById = function(id, done) { 8 | var data = this._findBy(id); 9 | if (data) { 10 | data = Object.assign({}, data); 11 | } 12 | done(null, data); 13 | }; 14 | 15 | InMemoryDataProxy.prototype.getAll = function(done) { 16 | var all = this._store.map(function(item) { 17 | return Object.assign({}, item); 18 | }); 19 | done(null, all); 20 | }; 21 | 22 | InMemoryDataProxy.prototype.insert = function(data, done) { 23 | const newId = Math.max(...this._store.map(i => parseInt(i.id))); 24 | data.id = (newId + 1).toString(); 25 | this._store.push(Object.assign({}, data)); 26 | done(null, data); 27 | }; 28 | 29 | InMemoryDataProxy.prototype.update = function(data, done) { 30 | var existing = this._findBy(data.id); 31 | _.merge(existing, data); 32 | done(null, Object.assign({}, existing)); 33 | }; 34 | 35 | InMemoryDataProxy.prototype.destroy = function(id, done) { 36 | var data = this._findBy(id); 37 | var index = this._store.indexOf(data); 38 | if (index > -1) { 39 | this._store.splice(index, 1); 40 | } 41 | done(null); 42 | }; 43 | 44 | InMemoryDataProxy.prototype._findBy = function(id) { 45 | var data = this._store.filter((function(p) { 46 | return p.id === id; 47 | }))[0]; 48 | return data; 49 | }; 50 | 51 | if (module) { 52 | module.exports = InMemoryDataProxy; 53 | } 54 | -------------------------------------------------------------------------------- /data_proxies/in-memory/inMemoryDataProxyFactory.js: -------------------------------------------------------------------------------- 1 | var InMemoryDataProxy = require('./inMemoryDataProxy'); 2 | var ProductDataProxy = require('./productDataProxy'); 3 | var InventoryItemDataProxy = require('./inventoryItemDataProxy'); 4 | var OrderDataProxy = require('./orderDataProxy'); 5 | var OrderItemDataProxy = require('./orderItemDataProxy'); 6 | 7 | var orderItemDataProxy = new OrderItemDataProxy([{"quantity": 2, "amount": 5000, "price": 2250, "productId": "1", "orderId": "1", "status": "PENDING", "id": "1" }]); 8 | 9 | module.exports = { 10 | categoryDataProxy: new InMemoryDataProxy([{id: "1", name: "Musical Equipment"}, {id: "2", name: "Art Supplies"}]), 11 | customerDataProxy: new InMemoryDataProxy([{id: "1", name: "Jimi Hendrix"}]), 12 | productDataProxy: new ProductDataProxy([{id: "1", name: "PRS Hollow II", categoryId: "1", price: 2250}, {id: "2", name: "Pastelles", categoryId: "2", price: 10.5}]), 13 | inventoryItemDataProxy: new InventoryItemDataProxy([{id: "1", productId: "1", quantityOnHand: 1, version: 1}, {id: "2", productId: "2", quantityOnHand: 5, version: 1}]), 14 | orderDataProxy: new OrderDataProxy(orderItemDataProxy, [{id: "1", orderDate: new Date(), customerId: "1"}]), 15 | orderItemDataProxy: orderItemDataProxy 16 | }; 17 | -------------------------------------------------------------------------------- /data_proxies/in-memory/inventoryItemDataProxy.js: -------------------------------------------------------------------------------- 1 | var InMemoryDataProxy = require('./inMemoryDataProxy'); 2 | var ConcurrencyError = require('./../../business_logic/shared/concurrencyError'); 3 | var _ = require('lodash'); 4 | 5 | var InventoryItemDataProxy = function(defaults) { 6 | InMemoryDataProxy.call(this, defaults); 7 | }; 8 | 9 | InventoryItemDataProxy.prototype = new InMemoryDataProxy(); 10 | 11 | InventoryItemDataProxy.prototype.update = function(data, done) { 12 | var existing = this._findBy(data.id); 13 | if (existing.version !== data.version) { 14 | return done(new ConcurrencyError("This item has been modified, please try again with new version")); 15 | } 16 | data.version++; 17 | _.merge(existing, data); 18 | done(null, Object.assign({}, existing)); 19 | }; 20 | 21 | InventoryItemDataProxy.prototype.getByProduct = function(productId, done) { 22 | var items = this._store.filter(function(item) { 23 | return item.productId === productId; 24 | })[0]; 25 | done(null, items || []); 26 | }; 27 | 28 | module.exports = InventoryItemDataProxy; 29 | -------------------------------------------------------------------------------- /data_proxies/in-memory/orderDataProxy.js: -------------------------------------------------------------------------------- 1 | var InMemoryDataProxy = require('./inMemoryDataProxy'); 2 | 3 | var OrderDataProxy = function(orderItemDataProxy, defaults) { 4 | this._orderItemDataProxy = orderItemDataProxy; 5 | InMemoryDataProxy.call(this, defaults); 6 | }; 7 | 8 | OrderDataProxy.prototype = new InMemoryDataProxy(); 9 | 10 | OrderDataProxy.prototype.getByCustomer = function(customerId, done) { 11 | var orders = this._store.filter(function(order) { 12 | return order.customerId === customerId; 13 | }); 14 | done(null, orders); 15 | }; 16 | 17 | OrderDataProxy.prototype.getByProduct = function(productId, done) { 18 | var self = this; 19 | self._orderItemDataProxy.getAll(function(err, items) { 20 | var orderIds = items.filter(function(item) { 21 | return item.productId === productId; 22 | }).map(function(item) { 23 | return item.orderId; 24 | }); 25 | var orders = self._store.filter(function(order) { 26 | return orderIds.indexOf(order.id) > -1; 27 | }); 28 | done(null, orders); 29 | }); 30 | }; 31 | 32 | module.exports = OrderDataProxy; 33 | -------------------------------------------------------------------------------- /data_proxies/in-memory/orderItemDataProxy.js: -------------------------------------------------------------------------------- 1 | var InMemoryDataProxy = require('./inMemoryDataProxy'); 2 | 3 | var OrderItemDataProxy = function(defaults) { 4 | InMemoryDataProxy.call(this, defaults); 5 | }; 6 | 7 | OrderItemDataProxy.prototype = new InMemoryDataProxy(); 8 | 9 | OrderItemDataProxy.prototype.getByOrder = function(orderId, done) { 10 | var items = this._store.filter(function(item) { 11 | return item.orderId === orderId; 12 | }); 13 | done(null, items); 14 | }; 15 | 16 | module.exports = OrderItemDataProxy; 17 | -------------------------------------------------------------------------------- /data_proxies/in-memory/productDataProxy.js: -------------------------------------------------------------------------------- 1 | var InMemoryDataProxy = require('./inMemoryDataProxy'); 2 | 3 | var ProductDataProxy = function(defaults) { 4 | InMemoryDataProxy.call(this, defaults); 5 | }; 6 | 7 | ProductDataProxy.prototype = new InMemoryDataProxy(); 8 | 9 | ProductDataProxy.prototype.getByCategory = function(categoryId, done) { 10 | var products = this._store.filter(function(product) { 11 | return product.categoryId === categoryId; 12 | }); 13 | done(null, products); 14 | }; 15 | 16 | module.exports = ProductDataProxy; 17 | -------------------------------------------------------------------------------- /data_proxies/mongo/categoryDataProxy.js: -------------------------------------------------------------------------------- 1 | var MongoDataProxy = require('./mongoDataProxy'); 2 | 3 | var CategoryDataProxy = function() { 4 | MongoDataProxy.call(this, "categories"); 5 | }; 6 | 7 | CategoryDataProxy.prototype = new MongoDataProxy(); 8 | 9 | module.exports = CategoryDataProxy; 10 | -------------------------------------------------------------------------------- /data_proxies/mongo/customerDataProxy.js: -------------------------------------------------------------------------------- 1 | var MongoDataProxy = require('./mongoDataProxy'); 2 | 3 | var CustomerDataProxy = function() { 4 | MongoDataProxy.call(this, "customers"); 5 | }; 6 | 7 | CustomerDataProxy.prototype = new MongoDataProxy(); 8 | 9 | module.exports = CustomerDataProxy; 10 | -------------------------------------------------------------------------------- /data_proxies/mongo/mongoDataProxyFactory.js: -------------------------------------------------------------------------------- 1 | var CategoryDataProxy = require('./categoryDataProxy'); 2 | var CustomerDataProxy = require('./customerDataProxy'); 3 | var ProductDataProxy = require('./productDataProxy'); 4 | var InventoryItemDataProxy = require('./inventoryItemDataProxy'); 5 | var OrderDataProxy = require('./orderDataProxy.js'); 6 | var OrderItemDataProxy = require('./orderItemDataProxy.js'); 7 | 8 | module.exports = { 9 | categoryDataProxy: new CategoryDataProxy(), 10 | customerDataProxy: new CustomerDataProxy(), 11 | productDataProxy: new ProductDataProxy(), 12 | inventoryItemDataProxy: new InventoryItemDataProxy(), 13 | orderDataProxy: new OrderDataProxy(), 14 | orderItemDataProxy: new OrderItemDataProxy() 15 | }; 16 | -------------------------------------------------------------------------------- /data_proxies/mongo/orderDataProxy.js: -------------------------------------------------------------------------------- 1 | var MongoDataProxy = require('./mongoDataProxy'); 2 | var objectId = require('mongodb').ObjectID; 3 | 4 | var OrderDataProxy = function() { 5 | MongoDataProxy.call(this, "orders"); 6 | }; 7 | 8 | OrderDataProxy.prototype = new MongoDataProxy(); 9 | 10 | OrderDataProxy.prototype.getByCustomer = function(customerId, done) { 11 | var self = this; 12 | self._mongodb.connect(self.connectionString, function(err, db) { 13 | if (err) { done(err); } 14 | var collection = db.collection(self.collectionName); 15 | collection.find({customerId: customerId}).toArray(function(err, data) { 16 | data.forEach((item) => { 17 | item.id = item._id; 18 | delete item._id; 19 | }); 20 | db.close(); 21 | done(err, data); 22 | }); 23 | }); 24 | }; 25 | 26 | OrderDataProxy.prototype.getByProduct = function(productId, done) { 27 | var self = this; 28 | self._mongodb.connect(self.connectionString, function(err, db) { 29 | if (err) { done(err); } 30 | var collection = db.collection(self.collectionName); 31 | var orderItemsCollection = db.collection("orderItems"); 32 | orderItemsCollection.find({productId: productId}).toArray(function(err, data) { 33 | var orderIds = data.map((i) => new objectId(i.orderId)); 34 | if (orderIds.length > 0) { 35 | collection.find({_id: { $in: orderIds }}).toArray(function(err, data) { 36 | if (data) { 37 | data.forEach((item) => { 38 | item.id = item._id; 39 | delete item._id; 40 | }); 41 | db.close(); 42 | } 43 | done(err, data); 44 | }); 45 | } 46 | else { 47 | done(null, []); 48 | } 49 | }); 50 | }); 51 | }; 52 | 53 | module.exports = OrderDataProxy; 54 | -------------------------------------------------------------------------------- /data_proxies/mongo/orderItemDataProxy.js: -------------------------------------------------------------------------------- 1 | var MongoDataProxy = require('./mongoDataProxy'); 2 | 3 | var OrderItemDataProxy = function() { 4 | MongoDataProxy.call(this, "orderItems"); 5 | }; 6 | 7 | OrderItemDataProxy.prototype = new MongoDataProxy(); 8 | 9 | OrderItemDataProxy.prototype.getByOrder = function(orderId, done) { 10 | var self = this; 11 | self._mongodb.connect(self.connectionString, function(err, db) { 12 | if (err) { done(err); } 13 | var collection = db.collection(self.collectionName); 14 | collection.find({orderId: orderId}).toArray(function(err, data) { 15 | data.forEach((item) => { 16 | item.id = item._id; 17 | delete item._id; 18 | }); 19 | db.close(); 20 | done(err, data); 21 | }); 22 | }); 23 | }; 24 | 25 | module.exports = OrderItemDataProxy; 26 | -------------------------------------------------------------------------------- /data_proxies/mongo/productDataProxy.js: -------------------------------------------------------------------------------- 1 | var objectId = require('mongodb').ObjectID; 2 | var MongoDataProxy = require('./mongoDataProxy'); 3 | 4 | var ProductDataProxy = function() { 5 | MongoDataProxy.call(this, "products"); 6 | }; 7 | 8 | ProductDataProxy.prototype = new MongoDataProxy(); 9 | 10 | ProductDataProxy.prototype.getByCategory = function(categoryId, done) { 11 | var self = this; 12 | self._mongodb.connect(self.connectionString, function(err, db) { 13 | if (err) { done(err); } 14 | var collection = db.collection(self.collectionName); 15 | collection.find({categoryId: categoryId}).toArray(function(err, data) { 16 | data.forEach((item) => { 17 | item.id = item._id; 18 | delete item._id; 19 | }); 20 | db.close(); 21 | done(err, data); 22 | }); 23 | }); 24 | }; 25 | 26 | module.exports = ProductDataProxy; 27 | -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/favicon.ico -------------------------------------------------------------------------------- /dist/img/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/angular.png -------------------------------------------------------------------------------- /dist/img/express.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/express.png -------------------------------------------------------------------------------- /dist/img/mongo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/mongo.png -------------------------------------------------------------------------------- /dist/img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/node.png -------------------------------------------------------------------------------- /dist/img/peasy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/peasy.png -------------------------------------------------------------------------------- /dist/img/plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/plain.png -------------------------------------------------------------------------------- /dist/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/dist/img/react.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peasy-js-samples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build_angular": "cd client/angular && npm run build", 8 | "install_angular": "cd client/angular && npm install", 9 | "build_react": "cd client/react && npm run build", 10 | "install_react": "cd client/react && npm install", 11 | "server": "node server/server.js", 12 | "install_dependencies": "npm install && npm run install_angular && npm run install_react", 13 | "build_all": "npm run build_angular && npm run build_react", 14 | "install_server": "npm install", 15 | "install_all": "npm run install_server && npm run install_dependencies", 16 | "install_dependencies_and_build_projects": "npm run install_all && npm run build_all" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "axios": "^0.18.0", 22 | "body-parser": "^1.18.3", 23 | "cors": "^2.8.5", 24 | "ejs": "^2.6.1", 25 | "express": "^4.16.4", 26 | "lodash": "^4.17.11", 27 | "mongodb": "^3.1.13", 28 | "opn": "^5.4.0", 29 | "peasy-js": "^2.1.2", 30 | "socket.io": "^2.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /screenflow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js-samples/a579b27f68cda0fbbc2b9fdd4cd88cbe2ecf337e/screenflow.gif -------------------------------------------------------------------------------- /server/applyMiddleware.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const express = require('express'); 3 | const cors = require('cors') 4 | 5 | const applyMiddleware = function(app) { 6 | 7 | app.use(lowerCaseQueryParams); 8 | app.use(bodyParser.json()); 9 | app.use(bodyParser.urlencoded({ 10 | extended: true 11 | })); 12 | 13 | app.use(express.static('dist')); 14 | app.use(express.static('dist/angular')); 15 | app.use(express.static('dist/react')); 16 | app.use(cors()); 17 | }; 18 | 19 | function lowerCaseQueryParams(req, res, next) { 20 | for (var key in req.query) { 21 | req.query[key.toLowerCase()] = req.query[key]; 22 | } 23 | next(); 24 | } 25 | 26 | module.exports = applyMiddleware; 27 | -------------------------------------------------------------------------------- /server/applySettings.js: -------------------------------------------------------------------------------- 1 | const peasyConfig = require('peasy-js').Configuration; 2 | 3 | var applySettings = function(app) { 4 | peasyConfig.autoPromiseWrap = true; 5 | }; 6 | 7 | module.exports = applySettings; 8 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const applySettings = require('./applySettings'); 4 | const applyMiddleware = require('./applyMiddleware'); 5 | const wireUpRoutes = require('./wireUpRoutes'); 6 | const http = require('http').Server(app); 7 | const io = require('socket.io')(http); 8 | const opn = require('opn'); 9 | 10 | const port = 3000 || process.env.PORT; 11 | 12 | io.on('connection', function(socket){ 13 | console.log('a user connected'); 14 | io.emit('test', 'hello from server'); 15 | }); 16 | 17 | applySettings(app); 18 | applyMiddleware(app); 19 | wireUpRoutes(app, io); 20 | 21 | http.listen(port, () => { 22 | opn(`http://localhost:${port}`) 23 | console.log(`Listening on: http://localhost:${port}`); 24 | }); 25 | -------------------------------------------------------------------------------- /spec/business_logic/rules/canShipOrderItemRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("CanShipOrderItemRule", function() { 2 | var CanShipOrderItemRule = require('../../../business_logic/rules/canShipOrderItemRule'); 3 | 4 | it("does not invalidate submitted order items", () => { 5 | var orderItem = { 6 | status: "SUBMITTED" 7 | }; 8 | var rule = new CanShipOrderItemRule(orderItem); 9 | rule.validate(() => { 10 | expect(rule.valid).toBe(true); 11 | }); 12 | }); 13 | 14 | it("does not invalidate backordered order items", () => { 15 | var orderItem = { 16 | status: "BACKORDERED" 17 | }; 18 | var rule = new CanShipOrderItemRule(orderItem); 19 | rule.validate(() => { 20 | expect(rule.valid).toBe(true); 21 | }); 22 | }); 23 | 24 | it("invalidates everything else", () => { 25 | var orderItem = { 26 | status: "PENDING" 27 | }; 28 | var rule = new CanShipOrderItemRule(orderItem); 29 | rule.validate(() => { 30 | expect(rule.valid).toBe(false); 31 | }); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /spec/business_logic/rules/canSubmitOrderItemRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("CanSubmitOrderItemRule", function() { 2 | var CanSubmitOrderItemRule = require('../../../business_logic/rules/canSubmitOrderItemRule'); 3 | 4 | it("does not invalidate pending order items", () => { 5 | var orderItem = { 6 | status: "PENDING" 7 | }; 8 | var rule = new CanSubmitOrderItemRule(orderItem); 9 | rule.validate(() => { 10 | expect(rule.valid).toBe(true); 11 | }); 12 | }); 13 | 14 | it("invalidates everything else", () => { 15 | var orderItem = { 16 | status: "SHIPPED" 17 | }; 18 | var rule = new CanSubmitOrderItemRule(orderItem); 19 | rule.validate(() => { 20 | expect(rule.valid).toBe(false); 21 | }); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /spec/business_logic/rules/fieldLengthRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("FieldLengthRule", function() { 2 | var FieldLengthRule = require('../../../business_logic/rules/fieldLengthRule'); 3 | 4 | it("invalidates when the value exceeds length", () => { 5 | var rule = new FieldLengthRule("name", "hello", 4); 6 | rule.validate(() => { 7 | expect(rule.valid).toBe(false); 8 | }); 9 | }); 10 | 11 | it("invalidates with the expected association", () => { 12 | var rule = new FieldLengthRule("name", "hello", 4); 13 | rule.validate(() => { 14 | expect(rule.errors[0].association).toEqual("name"); 15 | }); 16 | }); 17 | 18 | it("invalidates with the expected message", () => { 19 | var rule = new FieldLengthRule("name", "hello", 4); 20 | rule.validate(() => { 21 | expect(rule.errors[0].message).toEqual("name accepts a max length of 4"); 22 | }); 23 | }); 24 | 25 | it("does not invalidate when the value equals length", () => { 26 | var rule = new FieldLengthRule("name", "hello", 5); 27 | rule.validate(() => { 28 | expect(rule.valid).toBe(true); 29 | }); 30 | }); 31 | 32 | it("does not invalidate when the value less than length", () => { 33 | var rule = new FieldLengthRule("name", "hello", 6); 34 | rule.validate(() => { 35 | expect(rule.valid).toBe(true); 36 | }); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /spec/business_logic/rules/fieldRequiredRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("FieldRequiredRule", function() { 2 | var FieldRequiredRule = require('../../../business_logic/rules/fieldRequiredRule'); 3 | 4 | it("invalidates as expected", () => { 5 | var rule = new FieldRequiredRule("name", { name: "" }); 6 | rule.validate(() => { 7 | expect(rule.valid).toBe(false); 8 | }); 9 | }); 10 | 11 | it("invalidates with the expected association", () => { 12 | var rule = new FieldRequiredRule("name"); 13 | rule.validate(() => { 14 | expect(rule.errors[0].association).toEqual("name"); 15 | }); 16 | }); 17 | 18 | it("invalidates with the expected message", () => { 19 | var rule = new FieldRequiredRule("name", {}); 20 | rule.validate(() => { 21 | expect(rule.errors[0].message).toEqual("name is required"); 22 | }); 23 | }); 24 | 25 | it("does not invalidate when a value is supplied", () => { 26 | var rule = new FieldRequiredRule("name", { name: "Jimi Hendrix" }); 27 | rule.validate(() => { 28 | expect(rule.valid).toBe(true); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/business_logic/rules/fieldTypeRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("FieldTypeRule", function() { 2 | var FieldTypeRule = require('../../../business_logic/rules/fieldTypeRule'); 3 | 4 | it("invalidates with the expected association", () => { 5 | var rule = new FieldTypeRule("name", 5, "string"); 6 | rule.validate(() => { 7 | expect(rule.errors[0].association).toEqual("name"); 8 | }); 9 | }); 10 | 11 | it("invalidates with the expected message", () => { 12 | var rule = new FieldTypeRule("name", 5, "string"); 13 | rule.validate(() => { 14 | expect(rule.errors[0].message).toEqual("Invalid type supplied for name, expected string"); 15 | }); 16 | }); 17 | 18 | it("does not invalidate when a value is supplied", () => { 19 | var rule = new FieldTypeRule("name", "5", "string"); 20 | rule.validate(() => { 21 | expect(rule.valid).toBe(true); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /spec/business_logic/rules/orderItemAmountValidityRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("OrderItemAmountValidityRule", function() { 2 | var OrderItemAmountValidityRule = require('../../../business_logic/rules/orderItemAmountValidityRule'); 3 | 4 | it("invalidates when order item's amount does not equal product price * order items's quantity", () => { 5 | var orderItem = { amount: 102, quantity: 5 }; 6 | var product = { price: 20.2 }; 7 | var rule = new OrderItemAmountValidityRule(orderItem, product); 8 | rule.validate(() => { 9 | expect(rule.valid).toBe(false); 10 | }); 11 | }); 12 | 13 | it("validates when order item's amount equals product price * order items's quantity", () => { 14 | var orderItem = { amount: 101, quantity: 5 }; 15 | var product = { price: 20.2 }; 16 | var rule = new OrderItemAmountValidityRule(orderItem, product); 17 | rule.validate(() => { 18 | expect(rule.valid).toBe(true); 19 | }); 20 | }); 21 | 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /spec/business_logic/rules/orderItemPriceValidityRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("OrderItemPriceValidityRule", function() { 2 | var OrderItemPriceValidityRule = require('../../../business_logic/rules/orderItemPriceValidityRule'); 3 | 4 | it("invalidates when order item's price does not equal product price", () => { 5 | var orderItem = { amount: 102, quantity: 5, price: 10 }; 6 | var product = { price: 20.2 }; 7 | var rule = new OrderItemPriceValidityRule(orderItem, product); 8 | rule.validate(() => { 9 | expect(rule.valid).toBe(false); 10 | }); 11 | }); 12 | 13 | it("validates when order item's price equals product price", () => { 14 | var orderItem = { amount: 101, quantity: 5, price: 20.2 }; 15 | var product = { price: 20.2 }; 16 | var rule = new OrderItemPriceValidityRule(orderItem, product); 17 | rule.validate(() => { 18 | expect(rule.valid).toBe(true); 19 | }); 20 | }); 21 | 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /spec/business_logic/rules/validOrderItemStatusForDeleteRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("ValidOrderItemStatusForDeleteRule", function() { 2 | var ValidOrderItemStatusForDeleteRule = require('../../../business_logic/rules/validOrderItemStatusForDeleteRule'); 3 | 4 | it("invalidates when order item's status is shipped", () => { 5 | var orderItem = { amount: 102, quantity: 5, price: 10, status: "SHIPPED" }; 6 | var rule = new ValidOrderItemStatusForDeleteRule(orderItem); 7 | rule.validate(() => { 8 | expect(rule.valid).toBe(false); 9 | }); 10 | }); 11 | 12 | it("validates when order item's status is anything else", () => { 13 | var orderItem = { amount: 101, quantity: 5, price: 20.2, status: "PENDING" }; 14 | var rule = new ValidOrderItemStatusForDeleteRule(orderItem); 15 | rule.validate(() => { 16 | expect(rule.valid).toBe(true); 17 | }); 18 | }); 19 | 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /spec/business_logic/rules/validOrderItemStatusForUpdateRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("ValidOrderItemStatusForUpdateRule", function() { 2 | var ValidOrderItemStatusForUpdateRule = require('../../../business_logic/rules/validOrderItemStatusForUpdateRule'); 3 | 4 | it("invalidates when order item's status is shipped", () => { 5 | var orderItem = { amount: 102, quantity: 5, price: 10, status: "SHIPPED" }; 6 | var rule = new ValidOrderItemStatusForUpdateRule(orderItem); 7 | rule.validate(() => { 8 | expect(rule.valid).toBe(false); 9 | expect(rule.errors[0].message).toEqual("Shipped items cannot be changed"); 10 | }); 11 | }); 12 | 13 | it("invalidates when order item's status is shipped", () => { 14 | var orderItem = { amount: 102, quantity: 5, price: 10, status: "BACKORDERED" }; 15 | var rule = new ValidOrderItemStatusForUpdateRule(orderItem); 16 | rule.validate(() => { 17 | expect(rule.valid).toBe(false); 18 | expect(rule.errors[0].message).toEqual("Backordered items cannot be changed"); 19 | }); 20 | }); 21 | 22 | it("validates when order item's status is anything else", () => { 23 | var orderItem = { amount: 102, quantity: 5, price: 10, status: "PENDING" }; 24 | var rule = new ValidOrderItemStatusForUpdateRule(orderItem); 25 | rule.validate(() => { 26 | expect(rule.valid).toBe(true); 27 | }); 28 | }); 29 | 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /spec/business_logic/rules/validOrderStatusForUpdateRuleSpec.js: -------------------------------------------------------------------------------- 1 | describe("ValidOrderStatusForUpdateRule", function() { 2 | var ValidOrderStatusForUpdateRule = require('../../../business_logic/rules/validOrderStatusForUpdateRule'); 3 | 4 | it("invalidates when order contains one or more shipped items", () => { 5 | var orderId = 1; 6 | var orderItemService = { 7 | getByOrderCommand: function(orderId) { 8 | return { 9 | execute: function(done) { 10 | done(null, { 11 | value: [ 12 | { id: 1, status: "SHIPPED" }, 13 | { id: 2, status: "PENDING" } 14 | ] 15 | }); 16 | } 17 | } 18 | } 19 | }; 20 | var rule = new ValidOrderStatusForUpdateRule(orderId, orderItemService); 21 | rule.validate(() => { 22 | expect(rule.valid).toBe(false); 23 | }); 24 | }); 25 | 26 | it("validates when order item's status is anything else", () => { 27 | var orderId = 1; 28 | var orderItemService = { 29 | getByOrderCommand: function(orderId) { 30 | return { 31 | execute: function(done) { 32 | done(null, { 33 | value: [ 34 | { id: 1, status: "BACKORDERED" }, 35 | { id: 2, status: "PENDING" } 36 | ] 37 | }); 38 | } 39 | } 40 | } 41 | }; 42 | var rule = new ValidOrderStatusForUpdateRule(orderId, orderItemService); 43 | rule.validate(() => { 44 | expect(rule.valid).toBe(true); 45 | }); 46 | }); 47 | 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | cd client/angular/orders 2 | node server.js --------------------------------------------------------------------------------