├── .DS_Store ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── README.md ├── babel.config.js ├── dist ├── peasy.d.ts └── peasy.js ├── license.txt ├── package-lock.json ├── package.json ├── package ├── README.md ├── dist │ ├── peasy.d.ts │ └── peasy.js ├── license.txt └── package.json ├── peasy-js-uml.svg ├── peasy-js.svg ├── spec ├── businessServiceSpec.js ├── commandSpec.js ├── executionResultSpec.js ├── ruleSpec.js ├── rulesValidatorSpec.js └── support │ └── jasmine.json ├── src ├── businessService.js ├── command.js ├── configuration.js ├── executionResult.js ├── index.d.ts ├── index.js ├── rule.js ├── rulesValidator.js ├── sampleWithCallbacks.js ├── sampleWithPromises.js ├── serviceException.js └── utility.js ├── tsconfig.json └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peasy/peasy-js/804e852f4db8632454a9b0fe31e38a5109add04a/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .DS_Store 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "JasmineIndividualTest", 11 | "program": "${workspaceRoot}/node_modules/jasmine/bin/jasmine.js", 12 | "args": [ 13 | // "src/sampleWithCallbacks.js" 14 | "src/sampleWithPromises.js" 15 | // "spec/commandSpec.js" 16 | ], 17 | "env": { 18 | "NODE_PATH": "." 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![peasy-js](https://www.dropbox.com/s/2yajr2x9yevvzbm/peasy3.png?dl=0&raw=1) 2 | 3 |
4 |

A business logic micro-framework for javascript

5 |
6 | 7 |
8 | 9 | Gitter 10 | 11 | 12 | npm version 13 | 14 | 15 | travis 16 | 17 |
18 | 19 |

Write your business logic once and consume from everywhere!

20 |

21 | 22 |

23 | 24 | # What's a business logic micro-framework? 25 | 26 | A business logic framework is code that facilitates creating business logic in a consistent, predictable, reusable, extensible, maintainable, scalable, and testable manner. It promotes creating business logic that is completely decoupled from its consuming technologies and helps to ensure that separation of concerns ([SoC](https://en.wikipedia.org/wiki/Separation_of_concerns)) are adhered to. 27 | 28 | # Why peasy-js? 29 | 30 | Because the javascript ecosystem changes at a pace much more rapid than your business logic. UI frameworks change: Backbone one day, Angular the next day, React the following... Backend frameworks change: Express one day, Koa the next day, Hapi the next... Data frameworks and ORMS change... 31 | 32 | Why couple your code with technologies that are hot today and gone tomorrow? Why not focus on your business logic and abstract out everything else into truly reusable code that can be consumed by javascript in the browser, backend, or both, and by any UI or backend framework? 33 | 34 | peasy-js makes it trivial to whimsically swap out UI, backend, and data frameworks in your applications by creating your business logic in a composable, reusable, scalable, and testable manner. 35 | 36 | #### peasy-js offers/addresses the following: 37 | 38 | - [Business and validation rules](https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules) engine 39 | - [Asynchronous support](https://github.com/peasy/peasy-js/wiki/asynchronous-api) via promises or callbacks 40 | - ES5 and later support 41 | - [TypeScript](https://github.com/peasy/peasy-js/wiki/new-in-2.0#typescript-support) support 42 | - [Multiple client support](https://github.com/peasy/peasy-js/wiki/Multiple-Client-Support) 43 | - [Multiple deployment scenario support](https://github.com/peasy/peasy-js/wiki/Data-Proxy#multiple-deployment-scenarios) 44 | - Reusability (decouples business and validation logic from consuming code and frameworks) 45 | - [Scalability](https://github.com/peasy/peasy-js/wiki/Data-Proxy#scalability) 46 | - [Testability](https://github.com/peasy/peasy-js/wiki/Testing) 47 | 48 | # Where can I get it? 49 | 50 | - [Download the latest release](https://github.com/peasy/peasy-js/archive/master.zip) 51 | - Clone the repo: ```git clone https://github.com/peasy/peasy-js.git``` 52 | - Install with **npm**: ```npm install peasy-js``` 53 | - Install with **yarn**: ```yarn add peasy-js``` 54 | 55 | You can also download and add the [peasy.js](https://github.com/peasy/peasy-js/blob/master/dist/peasy.js) file to your project and reference it accordingly. 56 | 57 | # Getting started 58 | 59 | You can get started by reviewing the walk throughs below. 60 | 61 | - Run it in a [client](https://github.com/peasy/peasy-js/wiki/Browser-sample) (browser) 62 | - Run it on a [server](https://github.com/peasy/peasy-js/wiki/node.js-sample) (Node.js) 63 | - Run it with [TypeScript](https://github.com/peasy/peasy-js/wiki/typescript-node.js-sample) (Node.js) 64 | - [Sample application](https://github.com/peasy/peasy-js-samples): This sample application is an order entry / inventory management system written with peasy-js, react, angular (with TypeScript), mongoDB, nodejs, and express. 65 | - An additional sample can be viewed [using promises](https://github.com/peasy/peasy-js/blob/master/src/sampleWithPromises.js) or [using callbacks](https://github.com/peasy/peasy-js/blob/master/src/sampleWithCallbacks.js) that showcases creating a [business service](https://github.com/peasy/peasy-js/wiki/businessservice), custom [command](https://github.com/peasy/peasy-js/wiki/command), [business rules](https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules), and wiring them up. The sample also showcases how to consume the service. To see it in action, run one or both from a command line: 66 | 67 | - ```node src/sampleWithPromises.js``` 68 | - ```node src/sampleWithCallbacks.js``` 69 | 70 | # The main actors 71 | 72 | ### Business Service 73 | A [business service](https://github.com/peasy/peasy-js/wiki/BusinessService) implementation represents an entity (e.g. users, or projects) and is responsible for exposing business functionality via commands. These commands encapsulate CRUD and other business related logic. 74 | 75 | ### Command 76 | The [command](https://github.com/peasy/peasy-js/wiki/Command) is responsible for orchestrating the execution of initialization logic, business and validation rule execution, and other logic (data proxy invocations, workflow logic, etc.), respectively, via the command execution pipeline. 77 | 78 | ### Rule 79 | A [rule](https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules) can be created to represent a business rule (authorization, price validity, etc.) or a validation rule (field length, required, etc.). Rules are consumed by commands and can be chained, configured to execute based on a previous rule’s execution, etc. Rules can also be configured to invoke code based on the result of their execution. 80 | 81 | ### Data Proxy 82 | The [data proxy](https://github.com/peasy/peasy-js/wiki/Data-Proxy) is responsible for data storage and retrieval, and serves as an abstraction layer for data stores (database, web services, cache, etc.). 83 | 84 | ##### peasy-js actors at work 85 |

86 | 87 |

88 | 89 | # What's new in version 2.0? 90 | 91 | You can see all changes introduced with peasy-js 2.0 [here](https://github.com/peasy/peasy-js/wiki/new-in-2.0). 92 | 93 | # Contributing 94 | 95 | All contributions are welcome, from general framework improvements to sample client consumers, proxy implementations, and documentation updates. Want to get involved? Please hit us up with your ideas. Alternatively, you can make a pull request and we'll get to it ASAP. 96 | 97 | # Like what you see? 98 | 99 | Please consider showing your support by starring the project. 100 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env' 4 | ] 5 | }; -------------------------------------------------------------------------------- /dist/peasy.d.ts: -------------------------------------------------------------------------------- 1 | declare module Peasy { 2 | 3 | /** Represents a data store abstraction */ 4 | interface IDataProxy { 5 | 6 | /** Accepts the id of the object to be queried and returns it asynchronously. 7 | * @param id The id of the object to query by. 8 | * @returns A promise that when resolved, returns the queried object. */ 9 | getById(id: TKey): Promise 10 | 11 | /** Asynchronously returns all values from a data source and is especially useful for lookup data. 12 | * @returns A promise that when resolved, returns an array of all of the objects from a data source. */ 13 | getAll(): Promise 14 | 15 | /** Accepts an object and asynchronously inserts it into the data store.. 16 | * @param data The object to insert. 17 | * @returns A promise that when resolved, returns an updated version of the object as a result of the insert operation. */ 18 | insert(data: T): Promise 19 | 20 | /** Accepts an object and asynchronously updates it in the data store. 21 | * @param data The object to update. 22 | * @returns A promise that when resolved, returns an updated version of the object as a result of the update operation. */ 23 | update(data: T): Promise 24 | 25 | /** Accepts the id of the object to be deleted and asynchronously deletes it from the data store. 26 | * @param id The id of the object to delete. 27 | * @returns A resolvable promise. */ 28 | destroy(id: TKey): Promise 29 | } 30 | 31 | /** Represents a handled error */ 32 | class PeasyError { 33 | 34 | /** (Optional) The field that the message is associated with. */ 35 | association?: string; 36 | 37 | /** The error message. */ 38 | message: string; 39 | } 40 | 41 | /** Serves as optional arguments to the constructor of a Command */ 42 | class CommandArgs { 43 | 44 | /** (Optional) Used to perform initialization logic before rules are executed. 45 | * @returns An awaitable promise. 46 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 47 | _onInitialization?: (context: any) => Promise; 48 | 49 | /** (Optional) Used to return a list of rules whose execution outcome will determine whether or not to invoke _onValidateSuccess. 50 | * @returns An awaitable array of IRule. 51 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 52 | _getRules?: (context: any) => Promise; 53 | 54 | /** Primarily used to interact with data proxies, workflow logic, etc. 55 | * @returns An awaitable promise. 56 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 57 | _onValidationSuccess: (context: any) => Promise 58 | } 59 | 60 | /** Contains peasy-js configuration settings */ 61 | class Configuration { 62 | 63 | /** if true, will wrap command function results in promises */ 64 | static autoPromiseWrap: boolean; 65 | } 66 | 67 | /** Represents a business service abstraction */ 68 | interface IBusinessService { 69 | 70 | /** Accepts the id of the object to be queried and returns a command. 71 | * @param id The id of the object to query by. 72 | * @returns A command that when executed, retrieves the object associated with the supplied id argument upon successful rule validation. */ 73 | getByIdCommand(id: TKey): ICommand; 74 | 75 | /** Returns a command that delivers all values from a data source and is especially useful for lookup data. 76 | * @returns A command that when executed, retrieves all of the objects upon successful rule validation. */ 77 | getAllCommand(): ICommand; 78 | 79 | /** @param data The object to insert. 80 | * @returns A command that when executed, inserts the object upon successful rule validation. */ 81 | insertCommand(data: T): ICommand; 82 | 83 | /** @param data The object to update. 84 | * @returns A command that when executed, updates the object upon successful rule validation. */ 85 | updateCommand(data: T): ICommand; 86 | 87 | /** @param id The id of the object to delete. 88 | * @returns A command that when executed, deletes the object associated with the supplied id argument upon successful rule validation. */ 89 | destroyCommand(id: TKey): ICommand; 90 | } 91 | 92 | /** Base class for all business services */ 93 | abstract class BusinessService implements IBusinessService { 94 | 95 | protected dataProxy: IDataProxy; 96 | 97 | /** @param dataProxy The data store abstraction. */ 98 | constructor(dataProxy: IDataProxy); 99 | 100 | /** Accepts the id of the object to be queried and returns a command. 101 | * @param id The id of the object to query by. 102 | * @returns A command that when executed, retrieves the object associated with the supplied id argument upon successful rule validation. */ 103 | getByIdCommand(id: TKey): ICommand; 104 | 105 | /** Returns a command that delivers all values from a data source and is especially useful for lookup data. 106 | * @returns A command that when executed, retrieves all of the objects upon successful rule validation. */ 107 | getAllCommand(): ICommand; 108 | 109 | /** Accepts an object to be inserted into a data store and returns a command. 110 | * @param data The object to insert. 111 | * @returns A command that when executed, inserts the object upon successful rule validation. */ 112 | insertCommand(data: T): ICommand; 113 | 114 | /** Accepts an object to be updated within a data store and returns a command. 115 | * @param data The object to update. 116 | * @returns A command that when executed, updates the object upon successful rule validation. */ 117 | updateCommand(data: T): ICommand; 118 | 119 | /** Accepts the id of the object to be deleted from a data store and returns a command. 120 | * @param id The id of the object to delete. 121 | * @returns A command that when executed, deletes the object associated with the supplied id argument upon successful rule validation. */ 122 | destroyCommand(id: TKey): ICommand; 123 | 124 | /** Override this function to perform initialization logic before rule validations for getByIDCommand are performed. 125 | * @param id The id of the object to query by. 126 | * @param context An object that can be used as a property bag throughout the getByIdCommand execution pipeline. 127 | * @returns An awaitable promise. */ 128 | protected _onGetByIdCommandInitialization(id: TKey, context: any): Promise; 129 | 130 | /** Override this function to perform initialization logic before rule validations for getAllCommand are performed. 131 | * @param context An object that can be used as a property bag throughout the getAllCommand execution pipeline. 132 | * @returns An awaitable promise. */ 133 | protected _onGetAllCommandInitialization(context: any): Promise; 134 | 135 | /** Override this function to perform initialization logic before rule validations for insertCommand are performed. 136 | * @param data The object to save (insert). 137 | * @param context An object that can be used as a property bag throughout the insertCommand execution pipeline. 138 | * @returns An awaitable promise. */ 139 | protected _onInsertCommandInitialization(data: T, context: any): Promise; 140 | 141 | /** Override this function to perform initialization logic before rule validations for updateCommand are performed. 142 | * @param data The object to save (update). 143 | * @param context An object that can be used as a property bag throughout the updateCommand execution pipeline. 144 | * @returns An awaitable promise. */ 145 | protected _onUpdateCommandInitialization(data: T, context: any): Promise; 146 | 147 | /** Override this function to perform initialization logic before rule validations for destroyCommand are performed. 148 | * @param id The id of the object to delete. 149 | * @param context An object that can be used as a property bag throughout the destroyCommand execution pipeline. 150 | * @returns An awaitable promise. */ 151 | protected _onDestroyCommandInitialization(id: TKey, context: any): Promise; 152 | 153 | /** Override this function to supply custom business rules to getByIdCommand. 154 | * @param id The id of the object to query by. 155 | * @param context An object that can be used as a property bag throughout the getByIdCommand execution pipeline. 156 | * @returns An awaitable array of IRule. */ 157 | protected _getRulesForGetByIdCommand(id: TKey, context: any): Promise; 158 | 159 | /** Override this function to supply custom business rules to getAllCommand. 160 | * @param context An object that can be used as a property bag throughout the getAllCommand execution pipeline. 161 | * @returns An awaitable array of IRule. */ 162 | protected _getRulesForGetAllCommand(context: any): Promise; 163 | 164 | /** Override this function to supply custom business rules to insertCommand. 165 | * @param data The object to save (insert). 166 | * @param context An object that can be used as a property bag throughout the insertCommand execution pipeline. 167 | * @returns An awaitable array of IRule. */ 168 | protected _getRulesForInsertCommand(data: T, context: any): Promise; 169 | 170 | /** Override this function to supply custom business rules to updateCommand. 171 | * @param data The object to save (update). 172 | * @param context An object that can be used as a property bag throughout the updateCommand execution pipeline. 173 | * @returns An awaitable array of IRule. */ 174 | protected _getRulesForUpdateCommand(data: T, context: any): Promise; 175 | 176 | /** Override this function to supply custom business rules to destroyCommand. 177 | * @param id The id of the object to delete. 178 | * @param context An object that can be used as a property bag throughout the destroyCommand execution pipeline. 179 | * @returns An awaitable array of IRule. */ 180 | protected _getRulesForDestroyCommand(id: TKey, context: any): Promise; 181 | 182 | /** Invoked by the command returned from getByIdCommand() if validation and business rules execute successfully. 183 | * @param id The id of the object to query by. 184 | * @param context An object that has been passed through the getByIdCommand execution pipeline. 185 | * @returns An awaitable promise. */ 186 | protected _getById(id: TKey, context: any): Promise; 187 | 188 | /** Invoked by the command returned from getAllCommand() if validation and business rules execute successfully. 189 | * @param context An object that has been passed through the getAllCommand execution pipeline. 190 | * @returns An awaitable promise. */ 191 | protected _getAll(context: any): Promise; 192 | 193 | /** Invoked by the command returned from insertCommand() if validation and business rules execute successfully. 194 | * @param data The object to save (insert). 195 | * @param context An object that has been passed through the insertCommand execution pipeline. 196 | * @returns An awaitable promise. */ 197 | protected _insert(data: T, context: any): Promise; 198 | 199 | /** Invoked by the command returned from updateCommand() if validation and business rules execute successfully. 200 | * @param data The object to save (update). 201 | * @param context An object that has been passed through the updateCommand execution pipeline. 202 | * @returns An awaitable promise. */ 203 | protected _update(data: T, context: any): Promise; 204 | 205 | /** Invoked by the command returned from destroyCommand() if validation and business rules execute successfully. 206 | * @param id The id of the object to delete. 207 | * @param context An object that has been passed through the deleteCommand execution pipeline. 208 | * @returns An awaitable promise. */ 209 | protected _destroy(id: TKey, context: any): Promise; 210 | } 211 | 212 | /** Exceptions of this type are explicitly caught and handled by commands during execution. 213 | * If caught, the command will return a failed execution result with error messages. 214 | * ServiceException can be used in many situations, but is especially helpful to throw within data proxies 215 | * See https://github.com/peasy/peasy-js-samples for examples on usage */ 216 | class ServiceException { 217 | 218 | /** @param The error message to display. */ 219 | constructor(message: string); 220 | 221 | /** These errors will be added to a failed execution result's error collection. */ 222 | errors: PeasyError[]; 223 | 224 | /** The error message to display. */ 225 | readonly message: string; 226 | } 227 | 228 | /** Serves as the result of a command's execution. */ 229 | class ExecutionResult { 230 | 231 | /** @param success States whether the command execution was successful. 232 | * @param value Represents the data returned as a result of command execution. 233 | * @param errors Represents the errors returned from failed rules (if any). */ 234 | constructor(success: boolean, value?: T, errors?: PeasyError[]); 235 | 236 | /** Determines whether the command execution was successful. */ 237 | readonly success: boolean; 238 | 239 | /** Represents the data returned as a result of command execution. */ 240 | readonly value: T; 241 | 242 | /** Represents the errors returned from failed rules (if any). */ 243 | readonly errors: PeasyError[]; 244 | } 245 | 246 | /** Represents a command abstraction */ 247 | interface ICommand { 248 | 249 | /** Executes validation/business rule execution. 250 | * @returns An array of errors if validation fails. */ 251 | getErrors(): Promise; 252 | 253 | /** Executes initialization logic, validation/business rule execution, and command logic. 254 | * @returns An execution result. */ 255 | execute(): Promise>; 256 | } 257 | 258 | /** Responsible for orchestrating the execution of initialization logic, validation/business rule execution, and command logic */ 259 | class Command implements ICommand { 260 | 261 | /** Executes an array of commands and returns after all have completed. 262 | * @param commands An array of commands. 263 | * @returns An array of execution results. */ 264 | static executeAll(commands: Command[]): Promise[]>; 265 | 266 | /** @param args (Optional) Functions that an instance of this command will use. */ 267 | constructor(args?: CommandArgs); 268 | 269 | /** Executes validation/business rule execution. 270 | * @returns An array of errors if validation fails. */ 271 | getErrors(): Promise; 272 | 273 | /** Executes initialization logic, validation/business rule execution, and command logic. 274 | * @returns An execution result. */ 275 | execute(): Promise>; 276 | 277 | /** Used to perform initialization logic before rules are executed. 278 | * @returns An awaitable promise. */ 279 | protected _onInitialization(context: any): Promise; 280 | 281 | /** Used to return a list of rules whose execution outcome will determine whether or not to invoke _onValidateSuccess. 282 | * @returns An awaitable array of IRule. */ 283 | protected _getRules(context: any): Promise; 284 | 285 | /** Primarily used to interact with data proxies, workflow logic, etc. 286 | * @returns An awaitable promise. */ 287 | protected _onValidationSuccess(context: any): Promise; 288 | } 289 | 290 | class ifAllValidResult { 291 | /** @param func A function that when executed, returns an awaitable array of rules. 292 | * @returns A executable rule. */ 293 | thenGetRules(func: () => Promise): Rule 294 | } 295 | 296 | /** Represents a rule abstraction */ 297 | interface IRule { 298 | 299 | /** Associates an instance of the rule with a field. */ 300 | association: string; 301 | 302 | /** A list of errors resulting from failed validation. */ 303 | errors: PeasyError[]; 304 | 305 | /** Indicates whether the rule is successful after validation. */ 306 | valid: boolean; 307 | 308 | /** Invokes the rule. 309 | * @returns An awaitable promise. */ 310 | validate(): Promise; 311 | } 312 | 313 | /** Represents a container for business logic. */ 314 | abstract class Rule implements IRule { 315 | 316 | /** Extracts all rules from an array of commands. 317 | * @param commands An array of commands. 318 | * @returns An awaitable rule. */ 319 | static getAllRulesFrom(commands: Command[]): Promise; 320 | 321 | /** Extracts all rules from an array of commands. 322 | * @param commands An spread of commands. 323 | * @returns An awaitable rule. */ 324 | static getAllRulesFrom(...commands: Command[]): Promise; 325 | 326 | /** Returns a function that upon execution, will only return the next set of rules on successful validation of the initial set. 327 | * @param rules An array of rules. 328 | * @returns An ifAllValidResult. */ 329 | static ifAllValid(rules: IRule[]): ifAllValidResult; 330 | 331 | /** Returns a function that upon execution, will only return the next set of rules on successful validation of the initial set. 332 | * @param rules A spread of rules. 333 | * @returns An ifAllValidResult. */ 334 | static ifAllValid(...rules: IRule[]): ifAllValidResult; 335 | 336 | /** Override this function to perform the business/validation logic. Invoke _invalidate() if the logic fails validation. 337 | * @returns An awaitable promise. */ 338 | protected abstract _onValidate(): Promise; 339 | 340 | /** Override this function to gain more control as to how validation is performed. 341 | * @param message The validation failure error message. */ 342 | protected _invalidate(message: string): void; 343 | 344 | /** Associates an instance of the rule with a field. */ 345 | association: string; 346 | 347 | /** Indicates whether the rule is successful after validation. */ 348 | readonly valid: boolean; 349 | 350 | /** A list of errors resulting from failed validation. */ 351 | readonly errors: PeasyError[]; 352 | 353 | /** Invokes the rule. On completion, _valid_ and _errors_ (if applicable) will be set. 354 | * @returns An awaitable promise. */ 355 | validate(): Promise 356 | 357 | /** Invokes the supplied set of rules if the validation of this rule is successful. 358 | * @param rules A spread of rules. 359 | * @returns A reference to this rule. */ 360 | ifValidThenValidate(...rules: Rule[]): Rule; 361 | 362 | /** Invokes the supplied set of rules if the validation of this rule is successful. 363 | * @param rules An array of rules. 364 | * @returns A reference to this rule. */ 365 | ifValidThenValidate(rules: Rule[]): Rule; 366 | 367 | /** Invokes the supplied set of rules if the validation of this rule fails. 368 | * @param rules A spread of rules. 369 | * @returns A reference to this rule. */ 370 | ifInvalidThenValidate(...rules: Rule[]): Rule; 371 | 372 | /** Invokes the supplied set of rules if the validation of this rule fails. 373 | * @param rules An array of rules. 374 | * @returns A reference to this rule. */ 375 | ifInvalidThenValidate(rules: Rule[]): Rule; 376 | 377 | /** Invokes the supplied function if the validation of this rule is successful. 378 | * @param func A function to execute that receives a reference to the invoking rule 379 | * @returns A reference to this rule. */ 380 | ifValidThenExecute(func: (rule: Rule) => void): Rule 381 | 382 | /** Invokes the supplied function if the validation of this rule fails. 383 | * @param func A function to execute that receives a reference to the invoking rule 384 | * @returns A reference to this rule. */ 385 | ifInvalidThenExecute(func: (rule: Rule) => void): Rule 386 | 387 | /** Invokes the supplied function if the validation of this rule is successful. 388 | * @param func A function that returns an awaitable array of rules. 389 | * @returns A reference to this rule. */ 390 | ifValidThenGetRules(func: () => Promise): Rule 391 | } 392 | 393 | } 394 | 395 | export = Peasy; 396 | 397 | -------------------------------------------------------------------------------- /dist/peasy.js: -------------------------------------------------------------------------------- 1 | !function(n,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.peasy=t():n.peasy=t()}(this,(()=>{return n={891:(n,t,e)=>{var r=e(807),i=e(826),o=function(){"use strict";var n=function(t){if(!(this instanceof n))return new n(t);this.dataProxy=t};return n.extendService=function(t,e){return e.service=t,n.extend(e)},n.extend=function(t){(t=t||{}).params=t.params||["dataProxy"],t.functions=t.functions||{};var e=function(){var e=this;e.arguments=arguments,n.call(this),t.params.forEach((function(n,t){e[n]=e.arguments[t]}))},r=t.service||n;e.prototype=new r;var i=Object.keys(n.prototype);return Object.keys(t.functions).forEach((function(n){-1===i.indexOf(n)&&console.warn("The method: '"+n+"' is not an overridable method of BusinessService"),e.prototype[n]=t.functions[n]})),{createCommand:function t(r){return(r=r||{}).service=e,n.createCommand(r),{createCommand:t,service:e}},service:e}},n.createCommand=function(n){function t(n){return n.charAt(0).toUpperCase()+n.slice(1)}if(!(n=n||{}).name)throw new Error("A value for name must be supplied");if(!n.service)throw new Error("A function for the service argument must be supplied");var e=n.name,o="_on"+t(e)+"Initialization",a="_getRulesFor"+t(e),u="_"+e.replace("Command",""),s="_"+e+"Params",c=n.functions||{},l=n.service;return l.prototype[o]=c._onInitialization||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},l.prototype[a]=c._getRules||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null,[]):Promise.resolve([])},l.prototype[u]=c._onValidationSuccess||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},l.prototype[s]=n.params||[],l.prototype[e]=function(){var n=this,t=arguments,e=new r({_onInitialization:function(){var t=n[o].apply(this,arguments);return i.autoWrapInitializationResult(t)},_getRules:function(){var t=n[a].apply(this,arguments);return i.autoWrapRulesResult(t)},_onValidationSuccess:function(){var t=n[u].apply(this,arguments);return i.autoWrapValidationCompleteResult(t)}});return n[s].forEach((function(n,r){e[n]=t[r]})),Object.keys(n).forEach((function(t){e[t]=n[t]})),e.arguments=arguments,e},l},Object.defineProperty(n.prototype,"constructor",{enumerable:!1,value:n}),n.createCommand({name:"getByIdCommand",service:n,params:["id"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.getById(this.id,e):this.dataProxy.getById(this.id)}}}),n.createCommand({name:"getAllCommand",service:n,functions:{_onValidationSuccess:function(n,t){return t?this.dataProxy.getAll(t):this.dataProxy.getAll()}}}),n.createCommand({name:"insertCommand",service:n,params:["data"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.insert(n,e):this.dataProxy.insert(n)}}}),n.createCommand({name:"updateCommand",service:n,params:["data"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.update(n,e):this.dataProxy.update(n)}}}),n.createCommand({name:"destroyCommand",service:n,params:["id"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.destroy(n,e):this.dataProxy.destroy(n)}}}),n}();n.exports=o},807:(n,t,e)=>{var r=e(197),i=e(108),o=e(359),a=e(826),u=function(){"use strict";function n(n,t){return function(e){var r=this;return new Promise((function(e,i){n.apply(r,t.concat((function(n,t){if(n)return i(n);e(t)})))}))}}var t=function(n){if(n=n||{},!(this instanceof t))return new t(n.onInitialization,n.getRules,n.onValidationSuccess);this._onInitialization||(this._onInitialization=n._onInitialization||function(n,t){return t?t():Promise.resolve()}),this._getRules||(this._getRules=n._getRules||function(n,t){return t?t(null,[]):Promise.resolve([])}),this._onValidationSuccess||(this._onValidationSuccess=n._onValidationSuccess||function(n,t){return t?t():Promise.resolve()})};return t.prototype={constructor:t,getErrors:function(t,e){var r=this,i=r.arguments||{},u=Object.keys(i).map((function(n){return i[n]})).concat([e]),s=r._getRules.bind(r),c=r._onValidationSuccess.bind(r),l=function(n){return new o(n).validate()};t&&(s=n(s,u),c=n(c,u),l=function(n){return new Promise((function(t,e){new o(n).validate((function(r){if(r)return e(r);t(n)}))}))});var f,d=(f=s.apply(r,u),(f=a.autoWrapRulesResult(f)).then((function(n){return Array.isArray(n)||(n=[n]),n}))).then((function(n){return l(n)})).then((function(n){var t=n.filter((function(n){return!n.valid})).map((function(n){return n.errors}));return[].concat.apply([],t)})).then((function(n){return t?t(null,n):n})).catch((function(n){return t?t(n):Promise.reject(n)}));if(!t)return d},execute:function(t){var e=this,o={},u=e.arguments||{},s=Object.keys(u).map((function(n){return u[n]})).concat([o]),c=e._onInitialization.bind(e),l=e._onValidationSuccess.bind(e);t&&(c=n(c,s),l=n(l,s));var f,d=(f=c.apply(e,s),a.autoWrapInitializationResult(f)).then((function(){return t?new Promise((function(n,t){e.getErrors((function(t,e){return n(e)}),o)})):e.getErrors(null,o)})).then((function(n){if(n.length>0)return function(n){return Promise.resolve(new r(!1,null,n))}(n);try{var t=l.apply(e,s);return(t=a.autoWrapValidationCompleteResult(t)).then((function(n){return Promise.resolve(new r(!0,n,null))})).catch(h)}catch(n){return h(n)}})).then((function(n){return t?t(null,n):n})).catch((function(n){return t?t(n):Promise.reject(n)}));if(!t)return d;function h(n){return n instanceof i?Promise.resolve(new r(!1,null,n.errors)):Promise.reject(n)}}},t.extend=function(n){var e=(n=n||{}).params||[],r=n.functions||{},i=function(){var n=this;n.arguments=arguments,e.forEach((function(t,e){n[t]=n.arguments[e]}))};return(i.prototype=new t)._onInitialization=r._onInitialization||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},i.prototype._getRules=r._getRules||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null,[]):Promise.resolve([])},i.prototype._onValidationSuccess=r._onValidationSuccess||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},i},t.executeAll=function(n,t){Array.isArray(n)||(n=[n]);var e=n.length;if(e<1)return t?t():Promise.resolve();if(!t)return Promise.all(n.map((function(n){return n.execute()})));var r=0,i=[];function o(n,o){if(n)return t(n,i);r++,i.push(o),r===e&&t(null,i)}n.forEach((function(n){n.execute(o)}))},t}();n.exports=u},168:n=>{var t=function(){"use strict";var n=function(n){};return n.autoPromiseWrap=!1,n}();n.exports=t},197:n=>{var t=function(){"use strict";var n=function(t,e,r){if(!(this instanceof n))return new n(t,e,r);this.success=t,this.value=e,this.errors=r};return n}();n.exports=t},954:(n,t,e)=>{var r=e(891),i=e(807),o=e(197),a=e(758),u=e(108),s=e(168);n.exports={BusinessService:r,Command:i,ExecutionResult:o,Rule:a,ServiceException:u,Configuration:s}},758:(n,t,e)=>{var r=e(359),i=e(826),o=function(){"use strict";var n=function(t){if(!(this instanceof n))return new n;t=t||{},this.association=t.association||null,this.errors=[],this.ifInvalidThenFn=null,this.ifValidThenFn=null,this.ifValidThenGetRulesFn=null,this.validSuccessors=[],this.invalidSuccessors=[],this.valid=!0};return n.getAllRulesFrom=function(n,t){var e={};return t?function(n,t){Array.isArray(n)||(n=[n]);var r=n.length;if(r<1&&t)return t(null,[]);var i=0,o=[];function a(n,e){if(n)return t(n,o);Array.isArray(e)?e.forEach((function(n){o.push(n)})):o.push(e),++i===r&&t(null,o)}n.forEach((function(n){n._getRules(e,a)}))}(n,t):Promise.all(n.map((function(n){return n._getRules(e)}))).then((function(n){return[].concat.apply([],n)}))},n.ifAllValid=function(t){return{thenGetRules:function(e){var r=new n;return r._onValidate=function(n){return n?n():Promise.resolve()},r.validSuccessors=t,r.ifValidThenGetRulesFn=e,r}}},n.extend=function(t){if((t=t||{}).functions=t.functions||{},"function"!=typeof t.functions._onValidate)throw new Error("An onValidate method needs to be supplied to execute!");t.association=t.association||null,t.params=t.params||[];var e=function(){var e=this;e.arguments=arguments,n.call(e,{association:t.association}),t.params.forEach((function(n,t){e[n]=e.arguments[t]}))};return(e.prototype=new n)._onValidate=t.functions._onValidate,e},n.prototype={constructor:n,_invalidate:function(n){var t=this;this.valid=!1,Array.isArray(n)||(n=[n]),n.forEach((function(n){"string"==typeof n?t.errors.push({association:t.association,message:n}):t.errors.push(n)}))},_unInvalidate:function(){this.valid=!0,this.errors=[]},_onValidate:function(n){},validate:function(n){var t=this;t.errors=[];var e=t.arguments||{},o=Object.keys(e).map((function(n){return t.arguments[n]}));if(n)return this._onValidate.apply(t,o.concat((function(t){if(t)return n(t);u(n)})));var a=this._onValidate.apply(t,o);return(a=i.autoWrapValidationResult(a)).then(u);function u(n){if(t.valid){if(t.ifValidThenFn&&t.ifValidThenFn(t),t.validSuccessors.length>0)return function(n,t,e){return e?s(n,t,(function(){if(n.ifValidThenGetRulesFn)return c(n,t,e);e()})):s(n,t).then((function(){if(n.ifValidThenGetRulesFn)return c(n,t)}))}(t,t.validSuccessors,n);if(t.ifValidThenGetRulesFn)return c(t,t.validSuccessors,n)}else if(t.ifInvalidThenFn&&t.ifInvalidThenFn(t),t.invalidSuccessors.length>0)return s(t,t.invalidSuccessors,n);n&&n()}function s(n,t,e){return e?new r(t).validate((function(r){if(r)return e(r);l(n).ifAnyInvalid(t),e()})):new r(t).validate().then((function(){return l(n).ifAnyInvalid(t)}))}function c(n,t,e){var r=t.filter((function(n){return!n.valid}));return r.length>0?e?e():Promise.resolve():e?n.ifValidThenGetRulesFn((function(t,r){return Array.isArray(r)||(r=[r]),s(n,r,e)})):n.ifValidThenGetRulesFn().then((function(t){return Array.isArray(t)||(t=[t]),s(n,t)}))}function l(n){return{ifAnyInvalid:function(t){var e=t.filter((function(n){return!n.valid}));e.length>0?e.forEach((function(t){n._invalidate(t.errors)})):n._unInvalidate()}}}},ifValidThenValidate:function(n){return Array.isArray(n)||(n=[n]),this.validSuccessors=n,this},ifValidThenExecute:function(n){return this.ifValidThenFn=n,this},ifInvalidThenValidate:function(n){return Array.isArray(n)||(n=[n]),this.invalidSuccessors=n,this},ifInvalidThenExecute:function(n){return this.ifInvalidThenFn=n,this},ifValidThenGetRules:function(n){return this.ifValidThenGetRulesFn=n,this}},n}();n.exports=o},359:n=>{var t=function(){"use strict";var n=function(t){if(!(this instanceof n))return new n(t);this.rules=t};return n.prototype.validate=function(n){var t=this,e=t.rules.map((function(n){return n.validate.bind(n)}));n&&(e=e.map((function(n){return t=n,function(){return new Promise((function(n,e){t((function(t,r){if(t)return e(t);n(r)}))}))};var t})));var r=Promise.all(e.map((function(n){return n()}))).then((function(){return n?n(null,t.rules):Promise.resolve(t.rules)})).catch((function(t){return n?n(t):Promise.reject(t)}));if(!n)return r},n}();n.exports=t},108:n=>{var t=function(n){this.message=n,this.errors=[]};t.prototype=new Error,n.exports=t},826:(n,t,e)=>{var r=e(168);function i(n){return!r.autoPromiseWrap||void 0!==n&&"function"==typeof n.then?n:Promise.resolve(n)}var o={autoWrapInitializationResult:function(n){return i(n)},autoWrapRulesResult:function(n){return r.autoPromiseWrap&&(Array.isArray(n)&&(n=Promise.resolve(n)),void 0===n&&(n=Promise.resolve([])),"function"!=typeof n.then&&(n=Promise.resolve(n))),n},autoWrapValidationCompleteResult:function(n){return i(n)},autoWrapValidationResult:function(n){return i(n)},autoWrapRuleValidationCompleteResult:function(n){return i(n)}};n.exports=o}},t={},function e(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={exports:{}};return n[r](o,o.exports,e),o.exports}(954);var n,t})); -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peasy-js", 3 | "version": "2.3.0", 4 | "description": "A business logic micro-framework for javascript", 5 | "main": "dist/peasy.js", 6 | "types": "dist/peasy.d.ts", 7 | "files": [ 8 | "/dist" 9 | ], 10 | "scripts": { 11 | "test": "node ./node_modules/jasmine/bin/jasmine.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/peasy/peasy-js.git" 16 | }, 17 | "keywords": [ 18 | "middle tier", 19 | "middle", 20 | "tier", 21 | "framework", 22 | "business", 23 | "layer", 24 | "middle tier framework", 25 | "business layer", 26 | "business logic", 27 | "rules engine", 28 | "rules", 29 | "peasy-js", 30 | "peasy" 31 | ], 32 | "author": "Aaron Hanusa ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/peasy/peasy-js/issues" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.25.2", 39 | "@babel/preset-env": "^7.25.3", 40 | "babel-loader": "^9.1.3", 41 | "install": "^0.13.0", 42 | "jasmine": "^5.2.0", 43 | "npm": "^10.8.2", 44 | "terser-webpack-plugin": "^5.3.10", 45 | "webpack": "^5.93.0", 46 | "webpack-cli": "^5.1.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 | ![peasy-js](https://www.dropbox.com/s/2yajr2x9yevvzbm/peasy3.png?dl=0&raw=1) 2 | 3 |
4 |

A business logic micro-framework for javascript

5 |
6 | 7 | 18 | 19 |

Write your business logic once and consume from everywhere!

20 |

21 | 22 |

23 | 24 | # What's a business logic micro-framework? 25 | 26 | A business logic framework is code that facilitates creating business logic in a consistent, predictable, reusable, extensible, maintainable, scalable, and testable manner. It promotes creating business logic that is completely decoupled from its consuming technologies and helps to ensure that separation of concerns ([SoC](https://en.wikipedia.org/wiki/Separation_of_concerns)) are adhered to. 27 | 28 | # Why peasy-js? 29 | 30 | Because the javascript ecosystem changes at a pace much more rapid than your business logic. UI frameworks change: Backbone one day, Angular the next day, React the following... Backend frameworks change: Express one day, Koa the next day, Hapi the next... Data frameworks and ORMS change... 31 | 32 | Why couple your code with technologies that are hot today and gone tomorrow? Why not focus on your business logic and abstract out everything else into truly reusable code that can be consumed by javascript in the browser, backend, or both, and by any UI or backend framework? 33 | 34 | peasy-js makes it trivial to whimsically swap out UI, backend, and data frameworks in your applications by creating your business logic in a composable, reusable, scalable, and testable manner. 35 | 36 | #### peasy-js offers/addresses the following: 37 | 38 | - [Business and validation rules](https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules) engine 39 | - [Asynchronous support](https://github.com/peasy/peasy-js/wiki/asynchronous-api) via promises or callbacks 40 | - ES5 and later support 41 | - [TypeScript](https://github.com/peasy/peasy-js/wiki/new-in-2.0#typescript-support) support 42 | - [Multiple client support](https://github.com/peasy/peasy-js/wiki/Multiple-Client-Support) 43 | - [Multiple deployment scenario support](https://github.com/peasy/peasy-js/wiki/Data-Proxy#multiple-deployment-scenarios) 44 | - Reusability (decouples business and validation logic from consuming code and frameworks) 45 | - [Scalability](https://github.com/peasy/peasy-js/wiki/Data-Proxy#scalability) 46 | - [Testability](https://github.com/peasy/peasy-js/wiki/Testing) 47 | 48 | # Where can I get it? 49 | 50 | - [Download the latest release](https://github.com/peasy/peasy-js/archive/master.zip) 51 | - Clone the repo: ```git clone https://github.com/peasy/peasy-js.git``` 52 | - Install with **npm**: ```npm install peasy-js``` 53 | - Install with **yarn**: ```yarn add peasy-js``` 54 | 55 | You can also download and add the [peasy.js](https://github.com/peasy/peasy-js/blob/master/dist/peasy.js) file to your project and reference it accordingly. 56 | 57 | # Getting started 58 | 59 | You can get started by reviewing the walk throughs below. 60 | 61 | - Run it in a [client](https://github.com/peasy/peasy-js/wiki/Browser-sample) (browser) 62 | - Run it on a [server](https://github.com/peasy/peasy-js/wiki/node.js-sample) (Node.js) 63 | - Run it with [TypeScript](https://github.com/peasy/peasy-js/wiki/typescript-node.js-sample) (Node.js) 64 | - [Sample application](https://github.com/peasy/peasy-js-samples): This sample application is an order entry / inventory management system written with peasy-js, react, angular (with TypeScript), mongoDB, nodejs, and express. 65 | - An additional sample can be viewed [using promises](https://github.com/peasy/peasy-js/blob/master/src/sampleWithPromises.js) or [using callbacks](https://github.com/peasy/peasy-js/blob/master/src/sampleWithCallbacks.js) that showcases creating a [business service](https://github.com/peasy/peasy-js/wiki/businessservice), custom [command](https://github.com/peasy/peasy-js/wiki/command), [business rules](https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules), and wiring them up. The sample also showcases how to consume the service. To see it in action, run one or both from a command line: 66 | 67 | - ```node src/sampleWithPromises.js``` 68 | - ```node src/sampleWithCallbacks.js``` 69 | 70 | # The main actors 71 | 72 | ### Business Service 73 | A [business service](https://github.com/peasy/peasy-js/wiki/BusinessService) implementation represents an entity (e.g. users, or projects) and is responsible for exposing business functionality via commands. These commands encapsulate CRUD and other business related logic. 74 | 75 | ### Command 76 | The [command](https://github.com/peasy/peasy-js/wiki/Command) is responsible for orchestrating the execution of initialization logic, business and validation rule execution, and other logic (data proxy invocations, workflow logic, etc.), respectively, via the command execution pipeline. 77 | 78 | ### Rule 79 | A [rule](https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules) can be created to represent a business rule (authorization, price validity, etc.) or a validation rule (field length, required, etc.). Rules are consumed by commands and can be chained, configured to execute based on a previous rule’s execution, etc. Rules can also be configured to invoke code based on the result of their execution. 80 | 81 | ### Data Proxy 82 | The [data proxy](https://github.com/peasy/peasy-js/wiki/Data-Proxy) is responsible for data storage and retrieval, and serves as an abstraction layer for data stores (database, web services, cache, etc.). 83 | 84 | ##### peasy-js actors at work 85 |

86 | 87 |

88 | 89 | # What's new in version 2.0? 90 | 91 | You can see all changes introduced with peasy-js 2.0 [here](https://github.com/peasy/peasy-js/wiki/new-in-2.0). 92 | 93 | # Contributing 94 | 95 | All contributions are welcome, from general framework improvements to sample client consumers, proxy implementations, and documentation updates. Want to get involved? Please hit us up with your ideas. Alternatively, you can make a pull request and we'll get to it ASAP. 96 | 97 | # Like what you see? 98 | 99 | Please consider showing your support by starring the project. 100 | -------------------------------------------------------------------------------- /package/dist/peasy.d.ts: -------------------------------------------------------------------------------- 1 | declare module Peasy { 2 | 3 | /** Represents a data store abstraction */ 4 | interface IDataProxy { 5 | 6 | /** Accepts the id of the object to be queried and returns it asynchronously. 7 | * @param id The id of the object to query by. 8 | * @returns A promise that when resolved, returns the queried object. */ 9 | getById(id: TKey): Promise 10 | 11 | /** Asynchronously returns all values from a data source and is especially useful for lookup data. 12 | * @returns A promise that when resolved, returns an array of all of the objects from a data source. */ 13 | getAll(): Promise 14 | 15 | /** Accepts an object and asynchronously inserts it into the data store.. 16 | * @param data The object to insert. 17 | * @returns A promise that when resolved, returns an updated version of the object as a result of the insert operation. */ 18 | insert(data: T): Promise 19 | 20 | /** Accepts an object and asynchronously updates it in the data store. 21 | * @param data The object to update. 22 | * @returns A promise that when resolved, returns an updated version of the object as a result of the update operation. */ 23 | update(data: T): Promise 24 | 25 | /** Accepts the id of the object to be deleted and asynchronously deletes it from the data store. 26 | * @param id The id of the object to delete. 27 | * @returns A resolvable promise. */ 28 | destroy(id: TKey): Promise 29 | } 30 | 31 | /** Represents a handled error */ 32 | class PeasyError { 33 | 34 | /** (Optional) The field that the message is associated with. */ 35 | association?: string; 36 | 37 | /** The error message. */ 38 | message: string; 39 | } 40 | 41 | /** Serves as optional arguments to the constructor of a Command */ 42 | class CommandArgs { 43 | 44 | /** (Optional) Used to perform initialization logic before rules are executed. 45 | * @returns An awaitable promise. 46 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 47 | _onInitialization?: (context: any) => Promise; 48 | 49 | /** (Optional) Used to return a list of rules whose execution outcome will determine whether or not to invoke _onValidateSuccess. 50 | * @returns An awaitable array of IRule. 51 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 52 | _getRules?: (context: any) => Promise; 53 | 54 | /** Primarily used to interact with data proxies, workflow logic, etc. 55 | * @returns An awaitable promise. 56 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 57 | _onValidationSuccess: (context: any) => Promise 58 | } 59 | 60 | /** Contains peasy-js configuration settings */ 61 | class Configuration { 62 | 63 | /** if true, will wrap command function results in promises */ 64 | static autoPromiseWrap: boolean; 65 | } 66 | 67 | /** Represents a business service abstraction */ 68 | interface IBusinessService { 69 | 70 | /** Accepts the id of the object to be queried and returns a command. 71 | * @param id The id of the object to query by. 72 | * @returns A command that when executed, retrieves the object associated with the supplied id argument upon successful rule validation. */ 73 | getByIdCommand(id: TKey): ICommand; 74 | 75 | /** Returns a command that delivers all values from a data source and is especially useful for lookup data. 76 | * @returns A command that when executed, retrieves all of the objects upon successful rule validation. */ 77 | getAllCommand(): ICommand; 78 | 79 | /** @param data The object to insert. 80 | * @returns A command that when executed, inserts the object upon successful rule validation. */ 81 | insertCommand(data: T): ICommand; 82 | 83 | /** @param data The object to update. 84 | * @returns A command that when executed, updates the object upon successful rule validation. */ 85 | updateCommand(data: T): ICommand; 86 | 87 | /** @param id The id of the object to delete. 88 | * @returns A command that when executed, deletes the object associated with the supplied id argument upon successful rule validation. */ 89 | destroyCommand(id: TKey): ICommand; 90 | } 91 | 92 | /** Base class for all business services */ 93 | abstract class BusinessService implements IBusinessService { 94 | 95 | protected dataProxy: IDataProxy; 96 | 97 | /** @param dataProxy The data store abstraction. */ 98 | constructor(dataProxy: IDataProxy); 99 | 100 | /** Accepts the id of the object to be queried and returns a command. 101 | * @param id The id of the object to query by. 102 | * @returns A command that when executed, retrieves the object associated with the supplied id argument upon successful rule validation. */ 103 | getByIdCommand(id: TKey): ICommand; 104 | 105 | /** Returns a command that delivers all values from a data source and is especially useful for lookup data. 106 | * @returns A command that when executed, retrieves all of the objects upon successful rule validation. */ 107 | getAllCommand(): ICommand; 108 | 109 | /** Accepts an object to be inserted into a data store and returns a command. 110 | * @param data The object to insert. 111 | * @returns A command that when executed, inserts the object upon successful rule validation. */ 112 | insertCommand(data: T): ICommand; 113 | 114 | /** Accepts an object to be updated within a data store and returns a command. 115 | * @param data The object to update. 116 | * @returns A command that when executed, updates the object upon successful rule validation. */ 117 | updateCommand(data: T): ICommand; 118 | 119 | /** Accepts the id of the object to be deleted from a data store and returns a command. 120 | * @param id The id of the object to delete. 121 | * @returns A command that when executed, deletes the object associated with the supplied id argument upon successful rule validation. */ 122 | destroyCommand(id: TKey): ICommand; 123 | 124 | /** Override this function to perform initialization logic before rule validations for getByIDCommand are performed. 125 | * @param id The id of the object to query by. 126 | * @param context An object that can be used as a property bag throughout the getByIdCommand execution pipeline. 127 | * @returns An awaitable promise. */ 128 | protected _onGetByIdCommandInitialization(id: TKey, context: any): Promise; 129 | 130 | /** Override this function to perform initialization logic before rule validations for getAllCommand are performed. 131 | * @param context An object that can be used as a property bag throughout the getAllCommand execution pipeline. 132 | * @returns An awaitable promise. */ 133 | protected _onGetAllCommandInitialization(context: any): Promise; 134 | 135 | /** Override this function to perform initialization logic before rule validations for insertCommand are performed. 136 | * @param data The object to save (insert). 137 | * @param context An object that can be used as a property bag throughout the insertCommand execution pipeline. 138 | * @returns An awaitable promise. */ 139 | protected _onInsertCommandInitialization(data: T, context: any): Promise; 140 | 141 | /** Override this function to perform initialization logic before rule validations for updateCommand are performed. 142 | * @param data The object to save (update). 143 | * @param context An object that can be used as a property bag throughout the updateCommand execution pipeline. 144 | * @returns An awaitable promise. */ 145 | protected _onUpdateCommandInitialization(data: T, context: any): Promise; 146 | 147 | /** Override this function to perform initialization logic before rule validations for destroyCommand are performed. 148 | * @param id The id of the object to delete. 149 | * @param context An object that can be used as a property bag throughout the destroyCommand execution pipeline. 150 | * @returns An awaitable promise. */ 151 | protected _onDestroyCommandInitialization(id: TKey, context: any): Promise; 152 | 153 | /** Override this function to supply custom business rules to getByIdCommand. 154 | * @param id The id of the object to query by. 155 | * @param context An object that can be used as a property bag throughout the getByIdCommand execution pipeline. 156 | * @returns An awaitable array of IRule. */ 157 | protected _getRulesForGetByIdCommand(id: TKey, context: any): Promise; 158 | 159 | /** Override this function to supply custom business rules to getAllCommand. 160 | * @param context An object that can be used as a property bag throughout the getAllCommand execution pipeline. 161 | * @returns An awaitable array of IRule. */ 162 | protected _getRulesForGetAllCommand(context: any): Promise; 163 | 164 | /** Override this function to supply custom business rules to insertCommand. 165 | * @param data The object to save (insert). 166 | * @param context An object that can be used as a property bag throughout the insertCommand execution pipeline. 167 | * @returns An awaitable array of IRule. */ 168 | protected _getRulesForInsertCommand(data: T, context: any): Promise; 169 | 170 | /** Override this function to supply custom business rules to updateCommand. 171 | * @param data The object to save (update). 172 | * @param context An object that can be used as a property bag throughout the updateCommand execution pipeline. 173 | * @returns An awaitable array of IRule. */ 174 | protected _getRulesForUpdateCommand(data: T, context: any): Promise; 175 | 176 | /** Override this function to supply custom business rules to destroyCommand. 177 | * @param id The id of the object to delete. 178 | * @param context An object that can be used as a property bag throughout the destroyCommand execution pipeline. 179 | * @returns An awaitable array of IRule. */ 180 | protected _getRulesForDestroyCommand(id: TKey, context: any): Promise; 181 | 182 | /** Invoked by the command returned from getByIdCommand() if validation and business rules execute successfully. 183 | * @param id The id of the object to query by. 184 | * @param context An object that has been passed through the getByIdCommand execution pipeline. 185 | * @returns An awaitable promise. */ 186 | protected _getById(id: TKey, context: any): Promise; 187 | 188 | /** Invoked by the command returned from getAllCommand() if validation and business rules execute successfully. 189 | * @param context An object that has been passed through the getAllCommand execution pipeline. 190 | * @returns An awaitable promise. */ 191 | protected _getAll(context: any): Promise; 192 | 193 | /** Invoked by the command returned from insertCommand() if validation and business rules execute successfully. 194 | * @param data The object to save (insert). 195 | * @param context An object that has been passed through the insertCommand execution pipeline. 196 | * @returns An awaitable promise. */ 197 | protected _insert(data: T, context: any): Promise; 198 | 199 | /** Invoked by the command returned from updateCommand() if validation and business rules execute successfully. 200 | * @param data The object to save (update). 201 | * @param context An object that has been passed through the updateCommand execution pipeline. 202 | * @returns An awaitable promise. */ 203 | protected _update(data: T, context: any): Promise; 204 | 205 | /** Invoked by the command returned from destroyCommand() if validation and business rules execute successfully. 206 | * @param id The id of the object to delete. 207 | * @param context An object that has been passed through the deleteCommand execution pipeline. 208 | * @returns An awaitable promise. */ 209 | protected _destroy(id: TKey, context: any): Promise; 210 | } 211 | 212 | /** Exceptions of this type are explicitly caught and handled by commands during execution. 213 | * If caught, the command will return a failed execution result with error messages. 214 | * ServiceException can be used in many situations, but is especially helpful to throw within data proxies 215 | * See https://github.com/peasy/peasy-js-samples for examples on usage */ 216 | class ServiceException { 217 | 218 | /** @param The error message to display. */ 219 | constructor(message: string); 220 | 221 | /** These errors will be added to a failed execution result's error collection. */ 222 | errors: PeasyError[]; 223 | 224 | /** The error message to display. */ 225 | readonly message: string; 226 | } 227 | 228 | /** Serves as the result of a command's execution. */ 229 | class ExecutionResult { 230 | 231 | /** @param success States whether the command execution was successful. 232 | * @param value Represents the data returned as a result of command execution. 233 | * @param errors Represents the errors returned from failed rules (if any). */ 234 | constructor(success: boolean, value?: T, errors?: PeasyError[]); 235 | 236 | /** Determines whether the command execution was successful. */ 237 | readonly success: boolean; 238 | 239 | /** Represents the data returned as a result of command execution. */ 240 | readonly value: T; 241 | 242 | /** Represents the errors returned from failed rules (if any). */ 243 | readonly errors: PeasyError[]; 244 | } 245 | 246 | /** Represents a command abstraction */ 247 | interface ICommand { 248 | 249 | /** Executes validation/business rule execution. 250 | * @returns An array of errors if validation fails. */ 251 | getErrors(): Promise; 252 | 253 | /** Executes initialization logic, validation/business rule execution, and command logic. 254 | * @returns An execution result. */ 255 | execute(): Promise>; 256 | } 257 | 258 | /** Responsible for orchestrating the execution of initialization logic, validation/business rule execution, and command logic */ 259 | class Command implements ICommand { 260 | 261 | /** Executes an array of commands and returns after all have completed. 262 | * @param commands An array of commands. 263 | * @returns An array of execution results. */ 264 | static executeAll(commands: Command[]): Promise[]>; 265 | 266 | /** @param args (Optional) Functions that an instance of this command will use. */ 267 | constructor(args?: CommandArgs); 268 | 269 | /** Executes validation/business rule execution. 270 | * @returns An array of errors if validation fails. */ 271 | getErrors(): Promise; 272 | 273 | /** Executes initialization logic, validation/business rule execution, and command logic. 274 | * @returns An execution result. */ 275 | execute(): Promise>; 276 | 277 | /** Used to perform initialization logic before rules are executed. 278 | * @returns An awaitable promise. */ 279 | protected _onInitialization(context: any): Promise; 280 | 281 | /** Used to return a list of rules whose execution outcome will determine whether or not to invoke _onValidateSuccess. 282 | * @returns An awaitable array of IRule. */ 283 | protected _getRules(context: any): Promise; 284 | 285 | /** Primarily used to interact with data proxies, workflow logic, etc. 286 | * @returns An awaitable promise. */ 287 | protected _onValidationSuccess(context: any): Promise; 288 | } 289 | 290 | class ifAllValidResult { 291 | /** @param func A function that when executed, returns an awaitable array of rules. 292 | * @returns A executable rule. */ 293 | thenGetRules(func: () => Promise): Rule 294 | } 295 | 296 | /** Represents a rule abstraction */ 297 | interface IRule { 298 | 299 | /** Associates an instance of the rule with a field. */ 300 | association: string; 301 | 302 | /** A list of errors resulting from failed validation. */ 303 | errors: PeasyError[]; 304 | 305 | /** Indicates whether the rule is successful after validation. */ 306 | valid: boolean; 307 | 308 | /** Invokes the rule. 309 | * @returns An awaitable promise. */ 310 | validate(): Promise; 311 | } 312 | 313 | /** Represents a container for business logic. */ 314 | abstract class Rule implements IRule { 315 | 316 | /** Extracts all rules from an array of commands. 317 | * @param commands An array of commands. 318 | * @returns An awaitable rule. */ 319 | static getAllRulesFrom(commands: Command[]): Promise; 320 | 321 | /** Extracts all rules from an array of commands. 322 | * @param commands An spread of commands. 323 | * @returns An awaitable rule. */ 324 | static getAllRulesFrom(...commands: Command[]): Promise; 325 | 326 | /** Returns a function that upon execution, will only return the next set of rules on successful validation of the initial set. 327 | * @param rules An array of rules. 328 | * @returns An ifAllValidResult. */ 329 | static ifAllValid(rules: IRule[]): ifAllValidResult; 330 | 331 | /** Returns a function that upon execution, will only return the next set of rules on successful validation of the initial set. 332 | * @param rules A spread of rules. 333 | * @returns An ifAllValidResult. */ 334 | static ifAllValid(...rules: IRule[]): ifAllValidResult; 335 | 336 | /** Override this function to perform the business/validation logic. Invoke _invalidate() if the logic fails validation. 337 | * @returns An awaitable promise. */ 338 | protected abstract _onValidate(): Promise; 339 | 340 | /** Override this function to gain more control as to how validation is performed. 341 | * @param message The validation failure error message. */ 342 | protected _invalidate(message: string): void; 343 | 344 | /** Associates an instance of the rule with a field. */ 345 | association: string; 346 | 347 | /** Indicates whether the rule is successful after validation. */ 348 | readonly valid: boolean; 349 | 350 | /** A list of errors resulting from failed validation. */ 351 | readonly errors: PeasyError[]; 352 | 353 | /** Invokes the rule. On completion, _valid_ and _errors_ (if applicable) will be set. 354 | * @returns An awaitable promise. */ 355 | validate(): Promise 356 | 357 | /** Invokes the supplied set of rules if the validation of this rule is successful. 358 | * @param rules A spread of rules. 359 | * @returns A reference to this rule. */ 360 | ifValidThenValidate(...rules: Rule[]): Rule; 361 | 362 | /** Invokes the supplied set of rules if the validation of this rule is successful. 363 | * @param rules An array of rules. 364 | * @returns A reference to this rule. */ 365 | ifValidThenValidate(rules: Rule[]): Rule; 366 | 367 | /** Invokes the supplied set of rules if the validation of this rule fails. 368 | * @param rules A spread of rules. 369 | * @returns A reference to this rule. */ 370 | ifInvalidThenValidate(...rules: Rule[]): Rule; 371 | 372 | /** Invokes the supplied set of rules if the validation of this rule fails. 373 | * @param rules An array of rules. 374 | * @returns A reference to this rule. */ 375 | ifInvalidThenValidate(rules: Rule[]): Rule; 376 | 377 | /** Invokes the supplied function if the validation of this rule is successful. 378 | * @param func A function to execute that receives a reference to the invoking rule 379 | * @returns A reference to this rule. */ 380 | ifValidThenExecute(func: (rule: Rule) => void): Rule 381 | 382 | /** Invokes the supplied function if the validation of this rule fails. 383 | * @param func A function to execute that receives a reference to the invoking rule 384 | * @returns A reference to this rule. */ 385 | ifInvalidThenExecute(func: (rule: Rule) => void): Rule 386 | 387 | /** Invokes the supplied function if the validation of this rule is successful. 388 | * @param func A function that returns an awaitable array of rules. 389 | * @returns A reference to this rule. */ 390 | ifValidThenGetRules(func: () => Promise): Rule 391 | } 392 | 393 | } 394 | 395 | export = Peasy; 396 | 397 | -------------------------------------------------------------------------------- /package/dist/peasy.js: -------------------------------------------------------------------------------- 1 | !function(n,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.peasy=t():n.peasy=t()}(this,(()=>{return n={891:(n,t,e)=>{var r=e(807),i=e(826),o=function(){"use strict";var n=function(t){if(!(this instanceof n))return new n(t);this.dataProxy=t};return n.extendService=function(t,e){return e.service=t,n.extend(e)},n.extend=function(t){(t=t||{}).params=t.params||["dataProxy"],t.functions=t.functions||{};var e=function(){var e=this;e.arguments=arguments,n.call(this),t.params.forEach((function(n,t){e[n]=e.arguments[t]}))},r=t.service||n;e.prototype=new r;var i=Object.keys(n.prototype);return Object.keys(t.functions).forEach((function(n){-1===i.indexOf(n)&&console.warn("The method: '"+n+"' is not an overridable method of BusinessService"),e.prototype[n]=t.functions[n]})),{createCommand:function t(r){return(r=r||{}).service=e,n.createCommand(r),{createCommand:t,service:e}},service:e}},n.createCommand=function(n){function t(n){return n.charAt(0).toUpperCase()+n.slice(1)}if(!(n=n||{}).name)throw new Error("A value for name must be supplied");if(!n.service)throw new Error("A function for the service argument must be supplied");var e=n.name,o="_on"+t(e)+"Initialization",a="_getRulesFor"+t(e),u="_"+e.replace("Command",""),s="_"+e+"Params",c=n.functions||{},l=n.service;return l.prototype[o]=c._onInitialization||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},l.prototype[a]=c._getRules||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null,[]):Promise.resolve([])},l.prototype[u]=c._onValidationSuccess||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},l.prototype[s]=n.params||[],l.prototype[e]=function(){var n=this,t=arguments,e=new r({_onInitialization:function(){var t=n[o].apply(this,arguments);return i.autoWrapInitializationResult(t)},_getRules:function(){var t=n[a].apply(this,arguments);return i.autoWrapRulesResult(t)},_onValidationSuccess:function(){var t=n[u].apply(this,arguments);return i.autoWrapValidationCompleteResult(t)}});return n[s].forEach((function(n,r){e[n]=t[r]})),Object.keys(n).forEach((function(t){e[t]=n[t]})),e.arguments=arguments,e},l},Object.defineProperty(n.prototype,"constructor",{enumerable:!1,value:n}),n.createCommand({name:"getByIdCommand",service:n,params:["id"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.getById(this.id,e):this.dataProxy.getById(this.id)}}}),n.createCommand({name:"getAllCommand",service:n,functions:{_onValidationSuccess:function(n,t){return t?this.dataProxy.getAll(t):this.dataProxy.getAll()}}}),n.createCommand({name:"insertCommand",service:n,params:["data"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.insert(n,e):this.dataProxy.insert(n)}}}),n.createCommand({name:"updateCommand",service:n,params:["data"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.update(n,e):this.dataProxy.update(n)}}}),n.createCommand({name:"destroyCommand",service:n,params:["id"],functions:{_onValidationSuccess:function(n,t,e){return e?this.dataProxy.destroy(n,e):this.dataProxy.destroy(n)}}}),n}();n.exports=o},807:(n,t,e)=>{var r=e(197),i=e(108),o=e(359),a=e(826),u=function(){"use strict";function n(n,t){return function(e){var r=this;return new Promise((function(e,i){n.apply(r,t.concat((function(n,t){if(n)return i(n);e(t)})))}))}}var t=function(n){if(n=n||{},!(this instanceof t))return new t(n.onInitialization,n.getRules,n.onValidationSuccess);this._onInitialization||(this._onInitialization=n._onInitialization||function(n,t){return t?t():Promise.resolve()}),this._getRules||(this._getRules=n._getRules||function(n,t){return t?t(null,[]):Promise.resolve([])}),this._onValidationSuccess||(this._onValidationSuccess=n._onValidationSuccess||function(n,t){return t?t():Promise.resolve()})};return t.prototype={constructor:t,getErrors:function(t,e){var r=this,i=r.arguments||{},u=Object.keys(i).map((function(n){return i[n]})).concat([e]),s=r._getRules.bind(r),c=r._onValidationSuccess.bind(r),l=function(n){return new o(n).validate()};t&&(s=n(s,u),c=n(c,u),l=function(n){return new Promise((function(t,e){new o(n).validate((function(r){if(r)return e(r);t(n)}))}))});var f,d=(f=s.apply(r,u),(f=a.autoWrapRulesResult(f)).then((function(n){return Array.isArray(n)||(n=[n]),n}))).then((function(n){return l(n)})).then((function(n){var t=n.filter((function(n){return!n.valid})).map((function(n){return n.errors}));return[].concat.apply([],t)})).then((function(n){return t?t(null,n):n})).catch((function(n){return t?t(n):Promise.reject(n)}));if(!t)return d},execute:function(t){var e=this,o={},u=e.arguments||{},s=Object.keys(u).map((function(n){return u[n]})).concat([o]),c=e._onInitialization.bind(e),l=e._onValidationSuccess.bind(e);t&&(c=n(c,s),l=n(l,s));var f,d=(f=c.apply(e,s),a.autoWrapInitializationResult(f)).then((function(){return t?new Promise((function(n,t){e.getErrors((function(t,e){return n(e)}),o)})):e.getErrors(null,o)})).then((function(n){if(n.length>0)return function(n){return Promise.resolve(new r(!1,null,n))}(n);try{var t=l.apply(e,s);return(t=a.autoWrapValidationCompleteResult(t)).then((function(n){return Promise.resolve(new r(!0,n,null))})).catch(h)}catch(n){return h(n)}})).then((function(n){return t?t(null,n):n})).catch((function(n){return t?t(n):Promise.reject(n)}));if(!t)return d;function h(n){return n instanceof i?Promise.resolve(new r(!1,null,n.errors)):Promise.reject(n)}}},t.extend=function(n){var e=(n=n||{}).params||[],r=n.functions||{},i=function(){var n=this;n.arguments=arguments,e.forEach((function(t,e){n[t]=n.arguments[e]}))};return(i.prototype=new t)._onInitialization=r._onInitialization||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},i.prototype._getRules=r._getRules||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null,[]):Promise.resolve([])},i.prototype._onValidationSuccess=r._onValidationSuccess||function(){var n=arguments[Object.keys(arguments).length-1];return n&&"function"==typeof n?n(null):Promise.resolve()},i},t.executeAll=function(n,t){Array.isArray(n)||(n=[n]);var e=n.length;if(e<1)return t?t():Promise.resolve();if(!t)return Promise.all(n.map((function(n){return n.execute()})));var r=0,i=[];function o(n,o){if(n)return t(n,i);r++,i.push(o),r===e&&t(null,i)}n.forEach((function(n){n.execute(o)}))},t}();n.exports=u},168:n=>{var t=function(){"use strict";var n=function(n){};return n.autoPromiseWrap=!1,n}();n.exports=t},197:n=>{var t=function(){"use strict";var n=function(t,e,r){if(!(this instanceof n))return new n(t,e,r);this.success=t,this.value=e,this.errors=r};return n}();n.exports=t},954:(n,t,e)=>{var r=e(891),i=e(807),o=e(197),a=e(758),u=e(108),s=e(168);n.exports={BusinessService:r,Command:i,ExecutionResult:o,Rule:a,ServiceException:u,Configuration:s}},758:(n,t,e)=>{var r=e(359),i=e(826),o=function(){"use strict";var n=function(t){if(!(this instanceof n))return new n;t=t||{},this.association=t.association||null,this.errors=[],this.ifInvalidThenFn=null,this.ifValidThenFn=null,this.ifValidThenGetRulesFn=null,this.validSuccessors=[],this.invalidSuccessors=[],this.valid=!0};return n.getAllRulesFrom=function(n,t){var e={};return t?function(n,t){Array.isArray(n)||(n=[n]);var r=n.length;if(r<1&&t)return t(null,[]);var i=0,o=[];function a(n,e){if(n)return t(n,o);Array.isArray(e)?e.forEach((function(n){o.push(n)})):o.push(e),++i===r&&t(null,o)}n.forEach((function(n){n._getRules(e,a)}))}(n,t):Promise.all(n.map((function(n){return n._getRules(e)}))).then((function(n){return[].concat.apply([],n)}))},n.ifAllValid=function(t){return{thenGetRules:function(e){var r=new n;return r._onValidate=function(n){return n?n():Promise.resolve()},r.validSuccessors=t,r.ifValidThenGetRulesFn=e,r}}},n.extend=function(t){if((t=t||{}).functions=t.functions||{},"function"!=typeof t.functions._onValidate)throw new Error("An onValidate method needs to be supplied to execute!");t.association=t.association||null,t.params=t.params||[];var e=function(){var e=this;e.arguments=arguments,n.call(e,{association:t.association}),t.params.forEach((function(n,t){e[n]=e.arguments[t]}))};return(e.prototype=new n)._onValidate=t.functions._onValidate,e},n.prototype={constructor:n,_invalidate:function(n){var t=this;this.valid=!1,Array.isArray(n)||(n=[n]),n.forEach((function(n){"string"==typeof n?t.errors.push({association:t.association,message:n}):t.errors.push(n)}))},_unInvalidate:function(){this.valid=!0,this.errors=[]},_onValidate:function(n){},validate:function(n){var t=this;t.errors=[];var e=t.arguments||{},o=Object.keys(e).map((function(n){return t.arguments[n]}));if(n)return this._onValidate.apply(t,o.concat((function(t){if(t)return n(t);u(n)})));var a=this._onValidate.apply(t,o);return(a=i.autoWrapValidationResult(a)).then(u);function u(n){if(t.valid){if(t.ifValidThenFn&&t.ifValidThenFn(t),t.validSuccessors.length>0)return function(n,t,e){return e?s(n,t,(function(){if(n.ifValidThenGetRulesFn)return c(n,t,e);e()})):s(n,t).then((function(){if(n.ifValidThenGetRulesFn)return c(n,t)}))}(t,t.validSuccessors,n);if(t.ifValidThenGetRulesFn)return c(t,t.validSuccessors,n)}else if(t.ifInvalidThenFn&&t.ifInvalidThenFn(t),t.invalidSuccessors.length>0)return s(t,t.invalidSuccessors,n);n&&n()}function s(n,t,e){return e?new r(t).validate((function(r){if(r)return e(r);l(n).ifAnyInvalid(t),e()})):new r(t).validate().then((function(){return l(n).ifAnyInvalid(t)}))}function c(n,t,e){var r=t.filter((function(n){return!n.valid}));return r.length>0?e?e():Promise.resolve():e?n.ifValidThenGetRulesFn((function(t,r){return Array.isArray(r)||(r=[r]),s(n,r,e)})):n.ifValidThenGetRulesFn().then((function(t){return Array.isArray(t)||(t=[t]),s(n,t)}))}function l(n){return{ifAnyInvalid:function(t){var e=t.filter((function(n){return!n.valid}));e.length>0?e.forEach((function(t){n._invalidate(t.errors)})):n._unInvalidate()}}}},ifValidThenValidate:function(n){return Array.isArray(n)||(n=[n]),this.validSuccessors=n,this},ifValidThenExecute:function(n){return this.ifValidThenFn=n,this},ifInvalidThenValidate:function(n){return Array.isArray(n)||(n=[n]),this.invalidSuccessors=n,this},ifInvalidThenExecute:function(n){return this.ifInvalidThenFn=n,this},ifValidThenGetRules:function(n){return this.ifValidThenGetRulesFn=n,this}},n}();n.exports=o},359:n=>{var t=function(){"use strict";var n=function(t){if(!(this instanceof n))return new n(t);this.rules=t};return n.prototype.validate=function(n){var t=this,e=t.rules.map((function(n){return n.validate.bind(n)}));n&&(e=e.map((function(n){return t=n,function(){return new Promise((function(n,e){t((function(t,r){if(t)return e(t);n(r)}))}))};var t})));var r=Promise.all(e.map((function(n){return n()}))).then((function(){return n?n(null,t.rules):Promise.resolve(t.rules)})).catch((function(t){return n?n(t):Promise.reject(t)}));if(!n)return r},n}();n.exports=t},108:n=>{var t=function(n){this.message=n,this.errors=[]};t.prototype=new Error,n.exports=t},826:(n,t,e)=>{var r=e(168);function i(n){return!r.autoPromiseWrap||void 0!==n&&"function"==typeof n.then?n:Promise.resolve(n)}var o={autoWrapInitializationResult:function(n){return i(n)},autoWrapRulesResult:function(n){return r.autoPromiseWrap&&(Array.isArray(n)&&(n=Promise.resolve(n)),void 0===n&&(n=Promise.resolve([])),"function"!=typeof n.then&&(n=Promise.resolve(n))),n},autoWrapValidationCompleteResult:function(n){return i(n)},autoWrapValidationResult:function(n){return i(n)},autoWrapRuleValidationCompleteResult:function(n){return i(n)}};n.exports=o}},t={},function e(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={exports:{}};return n[r](o,o.exports,e),o.exports}(954);var n,t})); -------------------------------------------------------------------------------- /package/license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 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 | -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peasy-js", 3 | "version": "2.3.0", 4 | "description": "A business logic micro-framework for javascript", 5 | "main": "dist/peasy.js", 6 | "types": "dist/peasy.d.ts", 7 | "files": [ 8 | "/dist" 9 | ], 10 | "scripts": { 11 | "test": "node ./node_modules/jasmine/bin/jasmine.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/peasy/peasy-js.git" 16 | }, 17 | "keywords": [ 18 | "middle tier", 19 | "middle", 20 | "tier", 21 | "framework", 22 | "business", 23 | "layer", 24 | "middle tier framework", 25 | "business layer", 26 | "business logic", 27 | "rules engine", 28 | "rules", 29 | "peasy-js", 30 | "peasy" 31 | ], 32 | "author": "Aaron Hanusa ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/peasy/peasy-js/issues" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.25.2", 39 | "@babel/preset-env": "^7.25.3", 40 | "babel-loader": "^9.1.3", 41 | "install": "^0.13.0", 42 | "jasmine": "^5.2.0", 43 | "npm": "^10.8.2", 44 | "terser-webpack-plugin": "^5.3.10", 45 | "webpack": "^5.93.0", 46 | "webpack-cli": "^5.1.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /peasy-js-uml.svg: -------------------------------------------------------------------------------- 1 | 2 | OrderItemServicemore commands ....more rules ....CommandBusinessServiceDataProxyRulemore services ...HttpDataProxyMongoDbDataProxyRabbitMQDataProxyCreditCheckRuleValidAddressRuleAuthorizationRuleGetByIdCommandGetAllCommandInsertCommandUpdateCommandDestroyCommandOrderItemService
Extends
Extends
Extends
Extends
Extends
Extends
Use
Use
Use
Use
Extends
Extends
Extends
Extends
Extends
Extends
Use
Use
0 .. *
0 .. *
5 .. *
5 .. *
Business Service
Business Service
Command
Command
Rule
Rule
Data Proxy
Data Proxy
more http proxies ...OrderItemHttpProxymore mongo proxies ..OrderItemMongoProxymore rabbit proxies ..OrderItemRabbitProxy
1
1
Creates
Creates
Uses
[Not supported by viewer]
-------------------------------------------------------------------------------- /spec/commandSpec.js: -------------------------------------------------------------------------------- 1 | describe("Command", function() { 2 | var Command = require("../src/command"); 3 | var Rule = require("../src/rule"); 4 | var ServiceException = require("../src/serviceException"); 5 | var Configuration = require("../src/configuration"); 6 | 7 | Configuration.autoPromiseWrap = true; 8 | 9 | function promisify(command) { 10 | return { 11 | execute: function() { 12 | return new Promise((resolve, reject) => { 13 | command.execute(function (err, result) { 14 | if (err) return reject(err); 15 | resolve(result); 16 | }); 17 | }); 18 | } 19 | } 20 | } 21 | 22 | describe("constructor", () => { 23 | it("returns a new instance when invoked directly", function() { 24 | var command = Command(); 25 | expect(command instanceof Command).toBe(true); 26 | }); 27 | 28 | it("returns a new instance when instantiated", function() { 29 | var command = new Command(); 30 | expect(command instanceof Command).toBe(true); 31 | }); 32 | 33 | it("sets callbacks.onInitialization to a default if not supplied", () => { 34 | var command = new Command(); 35 | expect(typeof command._onInitialization).toEqual('function'); 36 | }); 37 | 38 | it("sets callbacks.getRules to a default if not supplied", () => { 39 | var command = new Command(); 40 | expect(typeof command._getRules).toEqual('function'); 41 | }); 42 | 43 | it("sets callbacks.onValidationSuccess to a default if not supplied", () => { 44 | var command = new Command(); 45 | expect(typeof command._onValidationSuccess).toEqual('function'); 46 | }); 47 | 48 | it("does not override existing functions if already exists (es6 inheritance support)", (onComplete) => { 49 | "use strict"; 50 | var val = 0 51 | class MyCommand extends Command { 52 | constructor() { 53 | super(); 54 | } 55 | _onInitialization(context, done) { 56 | val += 1; 57 | done(); 58 | } 59 | _getRules(context, done) { 60 | val += 1; 61 | done(null, []); 62 | } 63 | _onValidationSuccess(context, done) { 64 | val += 1; 65 | done(); 66 | } 67 | } 68 | var command = new MyCommand(); 69 | command.execute((e, r) => { 70 | expect(val).toEqual(3); 71 | onComplete(); 72 | }); 73 | }); 74 | }); 75 | 76 | describe("getErrors", () => { 77 | describe("when validation succeeds", () => { 78 | it("getErrors() returns an empty array", async () => { 79 | var TrueRule = Rule.extend({ 80 | functions: { 81 | _onValidate: function(context) { 82 | return Promise.resolve(); 83 | } 84 | } 85 | }); 86 | var command = new Command({ 87 | _getRules: (context) => { 88 | return Promise.resolve(new TrueRule()); 89 | } 90 | }); 91 | 92 | var results = await command.getErrors(); 93 | 94 | expect(results).toEqual([]); 95 | }); 96 | }); 97 | 98 | describe("when validation fails", () => { 99 | it("getErrors() returns the expected errors", async () => { 100 | var FalseRule = Rule.extend({ 101 | functions: { 102 | _onValidate: function(context) { 103 | this._invalidate("NOPE"); 104 | return Promise.resolve(); 105 | } 106 | } 107 | }); 108 | var command = new Command({ 109 | _getRules: (context) => { 110 | return Promise.resolve([new FalseRule(), new FalseRule()]); 111 | } 112 | }); 113 | 114 | var results = await command.getErrors(); 115 | 116 | expect(results.length).toEqual(2); 117 | expect(results[0].message).toEqual("NOPE"); 118 | expect(results[1].message).toEqual("NOPE"); 119 | }); 120 | }); 121 | }); 122 | 123 | describe("execute", () => { 124 | it("invokes the pipeline methods in the correct order", async () => { 125 | 126 | var command1 = new Command({ 127 | _onInitialization: (context, done) => { 128 | context.stuff = "1"; 129 | done(); 130 | }, 131 | _getRules: (context, done) => { 132 | context.stuff += "2"; 133 | done(null, []); 134 | }, 135 | _onValidationSuccess: (context, done) => { 136 | context.stuff += "3"; 137 | done(null, context); 138 | } 139 | }); 140 | 141 | var command2 = new Command({ 142 | _onInitialization: (context) => { 143 | context.stuff = "1"; 144 | return Promise.resolve(); 145 | }, 146 | _getRules: (context) => { 147 | context.stuff += "2"; 148 | return Promise.resolve([]); 149 | }, 150 | _onValidationSuccess: (context) => { 151 | context.stuff += "3"; 152 | return Promise.resolve(context); 153 | } 154 | }); 155 | 156 | var results = await Promise.all([ 157 | promisify(command1).execute(), 158 | command2.execute() 159 | ]); 160 | 161 | expect(results[0].value.stuff).toEqual("123"); 162 | expect(results[1].value.stuff).toEqual("123"); 163 | }); 164 | 165 | describe("execution results", () => { 166 | var TrueRule, FalseRule; 167 | beforeAll(() => { 168 | TrueRuleWithCallback = Rule.extend({ 169 | functions: { 170 | _onValidate: function(done) { 171 | done(); 172 | } 173 | } 174 | }); 175 | 176 | FalseRuleWithCallback = Rule.extend({ 177 | params: ['message'], 178 | functions: { 179 | _onValidate: function(message, done) { 180 | this._invalidate(this.message); 181 | done(); 182 | } 183 | } 184 | }); 185 | 186 | TrueRuleWithPromise = Rule.extend({ 187 | functions: { 188 | _onValidate: function() { 189 | return Promise.resolve(); 190 | } 191 | } 192 | }); 193 | 194 | FalseRuleWithPromise = Rule.extend({ 195 | params: ['message'], 196 | functions: { 197 | _onValidate: function() { 198 | this._invalidate(this.message); 199 | return Promise.resolve(); 200 | } 201 | } 202 | }); 203 | }); 204 | 205 | describe("when no rules configured", () => { 206 | it("returns the expected validation result", async () => { 207 | var returnValue = { id: 5, data: "abc" }; 208 | var functions1 = { 209 | _onValidationSuccess: (context, done) => { 210 | done(null, returnValue); 211 | } 212 | }; 213 | var functions2 = { 214 | _onValidationSuccess: (context) => { 215 | return Promise.resolve(returnValue); 216 | } 217 | }; 218 | 219 | var command1 = new Command(functions1); 220 | var command2 = new Command(functions2); 221 | 222 | var results = await Promise.all([ 223 | promisify(command1).execute(), 224 | command2.execute() 225 | ]); 226 | 227 | expect(results[0].success).toEqual(true); 228 | expect(results[0].value).toEqual(returnValue); 229 | expect(results[0].errors).toBeNull(); 230 | 231 | expect(results[1].success).toEqual(true); 232 | expect(results[1].value).toEqual(returnValue); 233 | expect(results[1].errors).toBeNull(); 234 | }); 235 | }); 236 | 237 | describe("when one rule configured", () => { 238 | it("supports single object literal argument as input to getRules callback", async () => { 239 | var returnValue = { id: 5, data: "abc" }; 240 | 241 | var command1 = new Command({ 242 | _getRules: (context, done) => { 243 | done(null, new TrueRuleWithCallback()); 244 | }, 245 | _onValidationSuccess: (context, done) => { 246 | done(null, returnValue); 247 | } 248 | }); 249 | 250 | var command2 = new Command({ 251 | _getRules: (context) => { 252 | return Promise.resolve(new TrueRuleWithPromise()); 253 | }, 254 | _onValidationSuccess: (context) => { 255 | return Promise.resolve(returnValue); 256 | } 257 | }); 258 | 259 | var results = await Promise.all([ 260 | promisify(command1).execute(), 261 | command2.execute() 262 | ]); 263 | 264 | expect(results[0].success).toEqual(true); 265 | expect(results[0].value).toEqual(returnValue); 266 | expect(results[0].errors).toBeNull(); 267 | 268 | expect(results[1].success).toEqual(true); 269 | expect(results[1].value).toEqual(returnValue); 270 | expect(results[1].errors).toBeNull(); 271 | }); 272 | 273 | describe("when validation succeeds", () => { 274 | it("returns the expected validation result", async () => { 275 | var returnValue = { id: 5, data: "abc" }; 276 | var command1 = new Command({ 277 | _getRules: (context, done) => { 278 | done(null, [new TrueRuleWithCallback()]); 279 | }, 280 | _onValidationSuccess: (context, done) => { 281 | done(null, returnValue); 282 | } 283 | }); 284 | var command2 = new Command({ 285 | _getRules: (context) => { 286 | return Promise.resolve([new TrueRuleWithPromise()]); 287 | }, 288 | _onValidationSuccess: (context) => { 289 | return Promise.resolve(returnValue); 290 | } 291 | }); 292 | 293 | var results = await Promise.all([ 294 | promisify(command1).execute(), 295 | command2.execute() 296 | ]); 297 | 298 | expect(results[0].success).toEqual(true); 299 | expect(results[0].value).toEqual(returnValue); 300 | expect(results[0].errors).toBeNull(); 301 | 302 | expect(results[1].success).toEqual(true); 303 | expect(results[1].value).toEqual(returnValue); 304 | expect(results[1].errors).toBeNull(); 305 | }); 306 | }); 307 | 308 | describe("when validation fails", () => { 309 | it("returns the expected validation result", async () => { 310 | var returnValue = { id: 5, data: "abc" }; 311 | var command1 = new Command({ 312 | _getRules: (context, done) => { 313 | done(null, [new FalseRuleWithCallback("a")]); 314 | }, 315 | _onValidationSuccess: (context, done) => { 316 | done(null, returnValue); 317 | } 318 | }); 319 | var command2 = new Command({ 320 | _getRules: (context) => { 321 | return Promise.resolve([new FalseRuleWithPromise("a")]); 322 | }, 323 | _onValidationSuccess: (context) => { 324 | return Promise.resolve(returnValue); 325 | } 326 | }); 327 | 328 | var results = await Promise.all([ 329 | promisify(command1).execute(), 330 | command2.execute() 331 | ]); 332 | 333 | expect(results[0].success).toEqual(false); 334 | expect(results[0].value).toBeNull(); 335 | expect(results[0].errors.length).toEqual(1); 336 | 337 | expect(results[1].success).toEqual(false); 338 | expect(results[1].value).toBeNull(); 339 | expect(results[1].errors.length).toEqual(1); 340 | }); 341 | }); 342 | }); 343 | 344 | describe("when multiple rules configured", () => { 345 | it("validates each rule", async () => { 346 | var command1 = new Command({ 347 | _getRules: (context, done) => { 348 | done(null, [ 349 | new FalseRuleWithCallback("a"), 350 | new TrueRuleWithCallback(), 351 | new FalseRuleWithCallback("b"), 352 | new TrueRuleWithCallback(), 353 | new FalseRuleWithCallback("c") 354 | ]); 355 | } 356 | }); 357 | var command2 = new Command({ 358 | _getRules: (context) => { 359 | return Promise.resolve([ 360 | new FalseRuleWithPromise("a"), 361 | new TrueRuleWithPromise(), 362 | new FalseRuleWithPromise("b"), 363 | new TrueRuleWithPromise(), 364 | new FalseRuleWithPromise("c") 365 | ]); 366 | } 367 | }); 368 | 369 | var results = await Promise.all([ 370 | promisify(command1).execute(), 371 | command2.execute() 372 | ]); 373 | 374 | expect(results[0].success).toEqual(false); 375 | expect(results[0].value).toBeNull(); 376 | expect(results[0].errors.length).toEqual(3); 377 | expect(results[0].errors[0].message).toEqual("a"); 378 | expect(results[0].errors[1].message).toEqual("b"); 379 | expect(results[0].errors[2].message).toEqual("c"); 380 | 381 | expect(results[1].success).toEqual(false); 382 | expect(results[1].value).toBeNull(); 383 | expect(results[1].errors.length).toEqual(3); 384 | expect(results[1].errors[0].message).toEqual("a"); 385 | expect(results[1].errors[1].message).toEqual("b"); 386 | expect(results[1].errors[2].message).toEqual("c"); 387 | }); 388 | 389 | }); 390 | 391 | describe("when a promise rejection is encountered", () => { 392 | 393 | describe("when the error is an instance of ServiceException", () => { 394 | it("returns the expected validation result", (onComplete) => { 395 | var functions = { 396 | _onValidationSuccess: (context) => { 397 | var ex = new ServiceException("404"); 398 | ex.errors.push({ association: "name", message: "name not supplied"}); 399 | return Promise.reject(ex); 400 | } 401 | } 402 | 403 | var command = new Command(functions); 404 | command.execute().then(result => { 405 | expect(result.success).toEqual(false); 406 | expect(result.value).toBeNull(); 407 | expect(result.errors.length).toEqual(1); 408 | expect(result.errors[0].message).toEqual("name not supplied"); 409 | onComplete(); 410 | }); 411 | }); 412 | }); 413 | 414 | describe("when the error is anything other than ServiceException", () => { 415 | it("returns a promise rejection", (onComplete) => { 416 | var functions = { 417 | _onValidationSuccess: (context) => { 418 | return Promise.reject(new Error("something unexpected happened")); 419 | } 420 | } 421 | 422 | var command = new Command(functions); 423 | command.execute().catch(err => { 424 | expect(err.message).toEqual("something unexpected happened"); 425 | onComplete(); 426 | }); 427 | }); 428 | }); 429 | 430 | describe("when an unhandled exception occurs", () => { 431 | 432 | describe("when the error is an instance of ServiceException", () => { 433 | it("returns the expected validation result when an exception is handled", (onComplete) => { 434 | var functions = { 435 | _onValidationSuccess: (context) => { 436 | var ex = new ServiceException("404"); 437 | ex.errors.push({ association: "name", message: "name not supplied"}); 438 | throw ex; 439 | } 440 | } 441 | 442 | var command = new Command(functions); 443 | command.execute().then(result => { 444 | expect(result.success).toEqual(false); 445 | expect(result.value).toBeNull(); 446 | expect(result.errors.length).toEqual(1); 447 | expect(result.errors[0].message).toEqual("name not supplied"); 448 | onComplete(); 449 | }); 450 | }); 451 | }); 452 | 453 | describe("when the error is anything other than ServiceException", () => { 454 | it("returns a promise rejection", (onComplete) => { 455 | var functions = { 456 | _onValidationSuccess: (context) => { 457 | throw new Error("something unexpected happened"); 458 | } 459 | } 460 | var command = new Command(functions); 461 | command.execute().catch(err => { 462 | expect(err.message).toEqual("something unexpected happened"); 463 | onComplete(); 464 | }); 465 | }); 466 | }); 467 | 468 | }); 469 | }); 470 | 471 | describe("when an error is received", () => { 472 | 473 | describe("when the error is an instance of ServiceException", () => { 474 | it("returns the expected validation result", (onComplete) => { 475 | var functions = { 476 | _onValidationSuccess: (context, done) => { 477 | var ex = new ServiceException("404"); 478 | ex.errors.push({ association: "name", message: "name not supplied"}); 479 | done(ex); 480 | } 481 | } 482 | 483 | var command = new Command(functions); 484 | command.execute((err, result) => { 485 | expect(result.success).toEqual(false); 486 | expect(result.value).toBeNull(); 487 | expect(result.errors.length).toEqual(1); 488 | expect(result.errors[0].message).toEqual("name not supplied"); 489 | onComplete(); 490 | }); 491 | }); 492 | }); 493 | 494 | describe("when the error is anything other than ServiceException", () => { 495 | it("returns the error in the callback", (onComplete) => { 496 | var functions = { 497 | _onValidationSuccess: (context, done) => { 498 | done(new Error("something unexpected happened")); 499 | } 500 | } 501 | 502 | var command = new Command(functions); 503 | command.execute((err, result) => { 504 | expect(err.message).toEqual("something unexpected happened"); 505 | onComplete(); 506 | }); 507 | 508 | }); 509 | }); 510 | 511 | }); 512 | 513 | describe("when an unhandled exception occurs", () => { 514 | it("returns the error in the callback", (onComplete) => { 515 | var functions = { 516 | _onValidationSuccess: (context, done) => { 517 | throw new Error("something unexpected happened"); 518 | } 519 | } 520 | 521 | var command = new Command(functions); 522 | command.execute((err, result) => { 523 | expect(err.message).toEqual("something unexpected happened"); 524 | onComplete(); 525 | }); 526 | }); 527 | }); 528 | }); 529 | }); 530 | 531 | describe("extend", () => { 532 | 533 | it("returns a constructor function that creates a new Command", () => { 534 | var TestCommand = Command.extend({}); 535 | var command = new TestCommand(); 536 | 537 | expect(command instanceof Command).toBe(true); 538 | }); 539 | 540 | it("creates different instances", () => { 541 | var Test1Command = Command.extend({}); 542 | var Test2Command = Command.extend({}); 543 | var test1Command = new Test1Command(); 544 | var test2Command = new Test2Command(); 545 | 546 | expect(test1Command._onInitialization).not.toEqual(test2Command._onInitialization); 547 | }) 548 | 549 | it("creates default functions if none supplied", () => { 550 | var TestCommand = Command.extend({}); 551 | var command = new TestCommand(); 552 | 553 | expect(command._onInitialization).toBeDefined(); 554 | expect(command._getRules).toBeDefined(); 555 | expect(command._onValidationSuccess).toBeDefined(); 556 | }); 557 | 558 | it("contains a reference to the supplied arguments", () => { 559 | var TestCommand = Command.extend({}); 560 | var command = new TestCommand(1, "my name is"); 561 | expect(command.arguments[0]).toEqual(1); 562 | expect(command.arguments[1]).toEqual("my name is"); 563 | }); 564 | 565 | it("correctly maps params to arguments", () => { 566 | var TestCommand = Command.extend({ 567 | params: ['id', 'name'] 568 | }); 569 | var command = new TestCommand(1, "my name is"); 570 | expect(command.id).toEqual(1); 571 | expect(command.name).toEqual("my name is"); 572 | }); 573 | 574 | it("creates instances that do not share state", () => { 575 | var TestCommand = Command.extend({}); 576 | var command1 = new TestCommand(1, "my name is"); 577 | var command2 = new TestCommand(2, "your name is"); 578 | expect(command1.arguments[0]).not.toEqual(command2.arguments[0]); 579 | expect(command1.arguments[1]).not.toEqual(command2.arguments[1]); 580 | }); 581 | 582 | it("does not override supplied functions", () => { 583 | function onInitialization(context, done) { } 584 | function getRules(context, done) { } 585 | function onValidationSuccess(context, done) { } 586 | 587 | var TestCommand = Command.extend({ 588 | functions: { 589 | _onInitialization: onInitialization, 590 | _getRules: getRules, 591 | _onValidationSuccess: onValidationSuccess 592 | } 593 | }); 594 | 595 | var command = new TestCommand(); 596 | expect(command._onInitialization).toEqual(onInitialization); 597 | expect(command._getRules).toEqual(getRules); 598 | expect(command._onValidationSuccess).toEqual(onValidationSuccess); 599 | }); 600 | 601 | it('passes constructor parameters to functions when params are not supplied in extend options', (onComplete) => { 602 | var Command1 = Command.extend({ 603 | functions: { 604 | _onInitialization: function(a, b, c, context, done) { 605 | context.a = a; context.b = b; context.c = c; 606 | done(); 607 | }, 608 | _getRules: function(a, b, c, context, done) { 609 | context.a += a; context.b += b; context.c += c; 610 | done(null, []); 611 | }, 612 | _onValidationSuccess: function(a, b, c, context, done) { 613 | context.a += a; context.b += b; context.c += c; 614 | done(null, context); 615 | } 616 | } 617 | }); 618 | 619 | var Command2 = Command.extend({ 620 | functions: { 621 | _onInitialization: function(a, b, c, context) { 622 | context.a = a; context.b = b; context.c = c; 623 | return Promise.resolve(); 624 | }, 625 | _getRules: function(a, b, c, context) { 626 | context.a += a; context.b += b; context.c += c; 627 | return Promise.resolve([]); 628 | }, 629 | _onValidationSuccess: function(a, b, c, context) { 630 | context.a += a; context.b += b; context.c += c; 631 | return Promise.resolve(context); 632 | } 633 | } 634 | }); 635 | 636 | var command = new Command1(2, 4, 6).execute((err, result) => { 637 | expect(result.value).toEqual({ a: 6, b: 12, c: 18}); 638 | var command2 = new Command2(2, 4, 6).execute().then(result => { 639 | expect(result.value).toEqual({ a: 6, b: 12, c: 18}); 640 | onComplete(); 641 | }) 642 | }); 643 | 644 | }); 645 | 646 | }); 647 | 648 | describe("executeAll", () => { 649 | 650 | it("invokes callback immediately if passed empty array", async () => { 651 | var results = await Promise.all([ 652 | new Promise((resolve, reject) => { 653 | Command.executeAll([], (err, result) => { 654 | resolve(result); 655 | }); 656 | }), 657 | Command.executeAll([]) 658 | ]); 659 | 660 | expect(results[0]).toBe(undefined); 661 | expect(results[1]).toBe(undefined); 662 | }); 663 | 664 | it("invokes all commands", async () => { 665 | var TestCommand1 = Command.extend({ 666 | params: ['val'], 667 | functions: { 668 | _onValidationSuccess: function(val, context, done) { 669 | done(null, this.val); 670 | } 671 | } 672 | }); 673 | 674 | var TestCommand2 = Command.extend({ 675 | params: ['val'], 676 | functions: { 677 | _onValidationSuccess: function(val, context) { 678 | return Promise.resolve(this.val); 679 | } 680 | } 681 | }); 682 | 683 | var commandsWithCallbacks = [ 684 | new TestCommand1(4), 685 | new TestCommand1(2) 686 | ]; 687 | 688 | var commandsWithPromises = [ 689 | new TestCommand2(4), 690 | new TestCommand2(2) 691 | ]; 692 | 693 | var results = await Promise.all([ 694 | new Promise((resolve, reject) => { 695 | Command.executeAll(commandsWithCallbacks, (err, result) => { 696 | if (err) return reject(err); 697 | resolve(result); 698 | }); 699 | }), 700 | Command.executeAll(commandsWithPromises) 701 | ]); 702 | 703 | expect(results[0][0].value).toEqual(4); 704 | expect(results[0][1].value).toEqual(2); 705 | expect(results[1][0].value).toEqual(4); 706 | expect(results[1][1].value).toEqual(2); 707 | }); 708 | }); 709 | 710 | describe('Configuration.autoPromiseWrap = true', () => { 711 | it("invokes each function without an explicit return of a promise", async () => { 712 | 713 | var command1 = new Command({ 714 | _onInitialization: function(context) { 715 | }, 716 | _getRules: function(context) { 717 | }, 718 | _onValidationSuccess: function(context) { 719 | return { stuff: "command1 result"}; 720 | } 721 | }); 722 | 723 | var Command2 = Command.extend({ 724 | functions: { 725 | _onInitialization: function(context) { 726 | }, 727 | _getRules: function(context) { 728 | }, 729 | _onValidationSuccess: function(context) { 730 | return { stuff: "command2 result"}; 731 | } 732 | } 733 | }); 734 | 735 | var Command3 = Command.extend({ 736 | functions: { 737 | _onInitialization: function(a, b, context) { 738 | }, 739 | _getRules: function(a, b, context) { 740 | }, 741 | _onValidationSuccess: function(a, b, context) { 742 | return { stuff: `${a} ${b}`}; 743 | } 744 | } 745 | }); 746 | 747 | class Command4 extends Command { 748 | _onInitialization() { 749 | } 750 | _getRules() { 751 | } 752 | _onValidationSuccess() { 753 | return { stuff: "command4 result"}; 754 | } 755 | } 756 | 757 | var command2 = new Command2(); 758 | var command3 = new Command3('command3', 'result'); 759 | var command4 = new Command4() 760 | 761 | var results = await Promise.all([ 762 | command1.execute(), 763 | command2.execute(), 764 | command3.execute(), 765 | command4.execute() 766 | ]); 767 | 768 | expect(results[0].value.stuff).toEqual("command1 result"); 769 | expect(results[1].value.stuff).toEqual("command2 result"); 770 | expect(results[2].value.stuff).toEqual("command3 result"); 771 | expect(results[3].value.stuff).toEqual("command4 result"); 772 | }); 773 | }); 774 | 775 | }); 776 | -------------------------------------------------------------------------------- /spec/executionResultSpec.js: -------------------------------------------------------------------------------- 1 | describe("ExecutionResult", () => { 2 | var ExecutionResult = require('../src/executionResult.js'); 3 | 4 | describe("constructor", () => { 5 | it("returns a new instance when invoked directly", () => { 6 | var result = ExecutionResult(); 7 | expect(result instanceof ExecutionResult).toBe(true); 8 | }); 9 | 10 | it("returns a new instance when instantiated", () => { 11 | var result = new ExecutionResult(); 12 | expect(result instanceof ExecutionResult).toBe(true); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /spec/rulesValidatorSpec.js: -------------------------------------------------------------------------------- 1 | describe("RulesValidator", () => { 2 | var RulesValidator = require("../src/rulesValidator"); 3 | var Rule = require("../src/rule"); 4 | 5 | describe("constructor", () => { 6 | it("returns a new instance when invoked directly", () => { 7 | var validator = RulesValidator(); 8 | expect(validator instanceof RulesValidator).toBe(true); 9 | }); 10 | 11 | it("returns a new instance when instantiated", () => { 12 | var validator = new RulesValidator(); 13 | expect(validator instanceof RulesValidator).toBe(true); 14 | }); 15 | }); 16 | 17 | describe("validate (callback)", () => { 18 | it("invokes done when all rules are complete", (onComplete) => { 19 | var TestRule = Rule.extend({ 20 | functions: { 21 | _onValidate: function(done) { 22 | done(); 23 | } 24 | } 25 | }); 26 | var rules = [ new TestRule(), new TestRule(), new TestRule() ]; 27 | var validator = new RulesValidator(rules); 28 | validator.validate((e, rules) => { 29 | expect(rules.every(r => r.valid)).toBeTruthy(); 30 | onComplete(); 31 | }); 32 | }); 33 | 34 | it("sets err when a errors occur in rule validations", (onComplete) => { 35 | var err = new Error("nope!"); 36 | var counter = 0; 37 | var TestRule = Rule.extend({ 38 | params: ['raiseError'], 39 | functions: { 40 | _onValidate: function(raiseError, done) { 41 | if (this.raiseError) { 42 | return done(err + ++counter); 43 | } 44 | done(); 45 | } 46 | } 47 | }); 48 | 49 | var rules = [new TestRule(true), new TestRule(false), new TestRule(true)]; 50 | var validator = new RulesValidator(rules); 51 | validator.validate((err, rules) => { 52 | expect(err).toBe('Error: nope!1'); 53 | onComplete(); 54 | }); 55 | }); 56 | }); 57 | 58 | describe("validate (promise)", () => { 59 | it("invokes done when all rules are complete", (onComplete) => { 60 | var TestRule = Rule.extend({ 61 | functions: { 62 | _onValidate: function(done) { 63 | return Promise.resolve(); 64 | } 65 | } 66 | }); 67 | var rules = [ new TestRule(), new TestRule(), new TestRule() ]; 68 | var validator = new RulesValidator(rules); 69 | validator.validate().then(rules => { 70 | expect(rules.every(r => r.valid)).toBeTruthy(); 71 | onComplete(); 72 | }); 73 | }); 74 | 75 | it("sets err when a errors occur in rule validations", (onComplete) => { 76 | var err = new Error("nope!"); 77 | var counter = 0; 78 | var TestRule = Rule.extend({ 79 | params: ['raiseError'], 80 | functions: { 81 | _onValidate: function(done) { 82 | if (this.raiseError) { 83 | return Promise.reject(err + ++counter); 84 | } 85 | return Promise.resolve(); 86 | } 87 | } 88 | }); 89 | 90 | var rules = [new TestRule(true), new TestRule(false), new TestRule(true)]; 91 | var validator = new RulesValidator(rules); 92 | validator.validate().catch(err => { 93 | expect(err).toBe('Error: nope!1'); 94 | onComplete(); 95 | }); 96 | }); 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/businessService.js: -------------------------------------------------------------------------------- 1 | var Command = require('./command'); 2 | var utility = require('./utility'); 3 | 4 | var BusinessService = (function() { 5 | 6 | "use strict"; 7 | 8 | // BUSINESS SERVICE 9 | 10 | var BusinessService = function(dataProxy) { 11 | if (this instanceof BusinessService) { 12 | this.dataProxy = dataProxy; 13 | } else { 14 | return new BusinessService(dataProxy); 15 | } 16 | }; 17 | 18 | BusinessService.extendService = function(service, options) { 19 | options.service = service; 20 | return BusinessService.extend(options); 21 | }; 22 | 23 | BusinessService.extend = function(options) { 24 | 25 | options = options || {}; 26 | options.params = options.params || ['dataProxy']; 27 | options.functions = options.functions || {}; 28 | 29 | var Extended = function() { 30 | var self = this; 31 | self.arguments = arguments; 32 | BusinessService.call(this); 33 | options.params.forEach(function(field, index) { 34 | self[field] = self.arguments[index]; 35 | }); 36 | }; 37 | 38 | var Service = options.service || BusinessService; 39 | Extended.prototype = new Service(); 40 | var keys = Object.keys(BusinessService.prototype); 41 | Object.keys(options.functions).forEach(function(key) { 42 | if (keys.indexOf(key) === -1) { 43 | console.warn("The method: '" + key + "' is not an overridable method of BusinessService"); 44 | } 45 | Extended.prototype[key] = options.functions[key]; 46 | }); 47 | 48 | function createCommand(options) { 49 | options = options || {}; 50 | options.service = Extended; 51 | BusinessService.createCommand(options); 52 | return { 53 | createCommand: createCommand, 54 | service: Extended 55 | }; 56 | } 57 | 58 | return { 59 | createCommand: createCommand, 60 | service: Extended 61 | }; 62 | }; 63 | 64 | BusinessService.createCommand = function(options) { 65 | 66 | function capitalize(value) { 67 | return value.charAt(0).toUpperCase() + value.slice(1); 68 | } 69 | 70 | options = options || {}; 71 | 72 | if (!options.name) { 73 | throw new Error('A value for name must be supplied'); 74 | } 75 | 76 | if (!options.service) { 77 | throw new Error('A function for the service argument must be supplied'); 78 | } 79 | 80 | var name = options.name; 81 | var onInitialization = '_on' + capitalize(name) + 'Initialization'; 82 | var getRules = '_getRulesFor' + capitalize(name); 83 | var onValidationSuccess = '_' + name.replace("Command", ""); 84 | var commandParams = '_' + name + 'Params'; 85 | var functions = options.functions || {}; 86 | var service = options.service; 87 | 88 | service.prototype[onInitialization] = functions._onInitialization || function() { 89 | var doneCallback = arguments[Object.keys(arguments).length -1]; 90 | if (doneCallback && typeof doneCallback === 'function') return doneCallback(null); 91 | return Promise.resolve(); 92 | }; 93 | 94 | service.prototype[getRules] = functions._getRules || function() { 95 | var doneCallback = arguments[Object.keys(arguments).length -1]; 96 | if (doneCallback && typeof doneCallback === 'function') return doneCallback(null, []); 97 | return Promise.resolve([]); 98 | }; 99 | 100 | service.prototype[onValidationSuccess] = functions._onValidationSuccess || function() { 101 | var doneCallback = arguments[Object.keys(arguments).length -1]; 102 | if (doneCallback && typeof doneCallback === 'function') return doneCallback(null); 103 | return Promise.resolve(); 104 | }; 105 | 106 | service.prototype[commandParams] = options.params || []; 107 | 108 | service.prototype[name] = function() { 109 | var serviceInstance = this; 110 | var constructorArgs = arguments; 111 | 112 | var command = new Command({ 113 | _onInitialization: function() { 114 | var result = serviceInstance[onInitialization].apply(this, arguments); 115 | return utility.autoWrapInitializationResult(result); 116 | }, 117 | _getRules: function() { 118 | var result = serviceInstance[getRules].apply(this, arguments); 119 | return utility.autoWrapRulesResult(result); 120 | }, 121 | _onValidationSuccess: function() { 122 | var result = serviceInstance[onValidationSuccess].apply(this, arguments); 123 | return utility.autoWrapValidationCompleteResult(result); 124 | } 125 | }); 126 | 127 | serviceInstance[commandParams].forEach(function(param, index) { 128 | command[param] = constructorArgs[index]; 129 | }); 130 | 131 | Object.keys(serviceInstance).forEach((key) => { 132 | command[key] = serviceInstance[key]; 133 | }); 134 | 135 | command['arguments'] = arguments; 136 | 137 | return command; 138 | }; 139 | 140 | return service; 141 | }; 142 | 143 | Object.defineProperty(BusinessService.prototype, "constructor", { 144 | enumerable: false, 145 | value: BusinessService 146 | }); 147 | 148 | BusinessService.createCommand({ 149 | name: "getByIdCommand", 150 | service: BusinessService, 151 | params: ["id"], 152 | functions: { 153 | _onValidationSuccess: function(id, context, done) { 154 | if (done) return this.dataProxy.getById(this.id, done); 155 | return this.dataProxy.getById(this.id); 156 | } 157 | } 158 | }); 159 | 160 | BusinessService.createCommand({ 161 | name: "getAllCommand", 162 | service: BusinessService, 163 | functions: { 164 | _onValidationSuccess: function(context, done) { 165 | if (done) return this.dataProxy.getAll(done); 166 | return this.dataProxy.getAll(); 167 | } 168 | } 169 | }); 170 | 171 | BusinessService.createCommand({ 172 | name: "insertCommand", 173 | service: BusinessService, 174 | params: ["data"], 175 | functions: { 176 | _onValidationSuccess: function(data, context, done) { 177 | if (done) return this.dataProxy.insert(data, done); 178 | return this.dataProxy.insert(data); 179 | } 180 | } 181 | }); 182 | 183 | BusinessService.createCommand({ 184 | name: "updateCommand", 185 | service: BusinessService, 186 | params: ["data"], 187 | functions: { 188 | _onValidationSuccess: function(data, context, done) { 189 | if (done) return this.dataProxy.update(data, done); 190 | return this.dataProxy.update(data); 191 | } 192 | } 193 | }); 194 | 195 | BusinessService.createCommand({ 196 | name: "destroyCommand", 197 | service: BusinessService, 198 | params: ["id"], 199 | functions: { 200 | _onValidationSuccess: function(id, context, done) { 201 | if (done) return this.dataProxy.destroy(id, done); 202 | return this.dataProxy.destroy(id); 203 | } 204 | } 205 | }); 206 | 207 | return BusinessService; 208 | 209 | })(); 210 | 211 | module.exports = BusinessService; 212 | -------------------------------------------------------------------------------- /src/command.js: -------------------------------------------------------------------------------- 1 | var ExecutionResult = require('./executionResult'); 2 | var ServiceException = require('./serviceException'); 3 | var RulesValidator = require('./rulesValidator'); 4 | var utility = require('./utility'); 5 | 6 | var Command = (function() { 7 | 8 | "use strict"; 9 | 10 | function wrap(fn, args) { 11 | return function(context) { 12 | return new Promise((resolve, reject) => { 13 | var callback = function(err, result) { 14 | if (err) return reject(err); 15 | resolve(result); 16 | } 17 | fn.apply(this, args.concat(callback)); 18 | }); 19 | } 20 | } 21 | 22 | var Command = function(callbacks) { 23 | callbacks = callbacks || {}; 24 | if (this instanceof Command) { 25 | 26 | if (!this._onInitialization) { // allow for inheritance (ES6) 27 | this._onInitialization = callbacks._onInitialization || function(context, done) { 28 | if (done) return done(); 29 | return Promise.resolve(); 30 | }; 31 | } 32 | 33 | if (!this._getRules) { // allow for inheritance (ES6) 34 | this._getRules = callbacks._getRules || function(context, done) { 35 | if (done) return done(null, []); 36 | return Promise.resolve([]); 37 | }; 38 | } 39 | 40 | if (!this._onValidationSuccess) { // allow for inheritance (ES6) 41 | this._onValidationSuccess = callbacks._onValidationSuccess || function(context, done) { 42 | if (done) return done(); 43 | return Promise.resolve(); 44 | }; 45 | } 46 | 47 | } else { 48 | return new Command( 49 | callbacks.onInitialization, 50 | callbacks.getRules, 51 | callbacks.onValidationSuccess 52 | ); 53 | } 54 | }; 55 | 56 | Command.prototype = { 57 | 58 | constructor: Command, 59 | 60 | getErrors: function(done, context) { 61 | var self = this; 62 | var constructorArgs = self.arguments || {}; 63 | var constructorValues = Object.keys(constructorArgs).map(key => constructorArgs[key]); 64 | var functionArgs = constructorValues.concat([context]); 65 | 66 | var rulesFunc = self._getRules.bind(self); 67 | var validationSuccessFunc = self._onValidationSuccess.bind(self); 68 | var validateRulesFunc = function(rules) { 69 | return new RulesValidator(rules).validate(); 70 | } 71 | 72 | if (done) { 73 | rulesFunc = wrap(rulesFunc, functionArgs); 74 | validationSuccessFunc = wrap(validationSuccessFunc, functionArgs); 75 | validateRulesFunc = function(rules) { 76 | return new Promise((resolve, reject) => { 77 | new RulesValidator(rules).validate(function(err) { 78 | if (err) return reject(err); 79 | resolve(rules); 80 | }); 81 | }); 82 | } 83 | } 84 | 85 | var promise = getRules() 86 | .then(validateRules) 87 | .then(parseErrorsFromRules) 88 | .then((result) => { 89 | if (done) return done(null, result); 90 | return result; 91 | }) 92 | .catch((e) => { 93 | if (done) return done(e); 94 | return Promise.reject(e); 95 | }); 96 | 97 | if (!done) return promise; 98 | 99 | function getRules() { 100 | var result = rulesFunc.apply(self, functionArgs); 101 | result = utility.autoWrapRulesResult(result); 102 | return result.then(rules => { 103 | if (!Array.isArray(rules)) { 104 | rules = [rules]; 105 | } 106 | return rules; 107 | }); 108 | } 109 | 110 | function validateRules(rules) { 111 | return validateRulesFunc(rules); 112 | } 113 | 114 | function parseErrorsFromRules(rules) { 115 | var errors = rules.filter(function(rule) { return !rule.valid; }) 116 | .map(function(rule) { return rule.errors; }); 117 | 118 | return [].concat.apply([], errors); // flatten array 119 | } 120 | }, 121 | 122 | execute: function(done) { 123 | var self = this; 124 | var context = {}; 125 | var constructorArgs = self.arguments || {}; 126 | var constructorValues = Object.keys(constructorArgs).map(key => constructorArgs[key]); 127 | var functionArgs = constructorValues.concat([context]); 128 | 129 | var initialization = self._onInitialization.bind(self); 130 | var validationSuccessFunc = self._onValidationSuccess.bind(self); 131 | var executionFailureFunc = function(errors) { 132 | return Promise.resolve(new ExecutionResult(false, null, errors)); 133 | }; 134 | 135 | if (done) { 136 | initialization = wrap(initialization, functionArgs); 137 | validationSuccessFunc = wrap(validationSuccessFunc, functionArgs); 138 | } 139 | 140 | var promise = performInitialization() 141 | .then(getErrors) 142 | .then(createExecutionResult) 143 | .then(returnResult) 144 | .catch((e) => { 145 | if (done) return done(e); 146 | return Promise.reject(e); 147 | }); 148 | 149 | if (!done) return promise; 150 | 151 | function performInitialization() { 152 | var result = initialization.apply(self, functionArgs); 153 | return utility.autoWrapInitializationResult(result); 154 | } 155 | 156 | function getErrors() { 157 | if (done) { 158 | return new Promise((resolve, reject) => { 159 | self.getErrors((r, v) => resolve(v), context) 160 | }); 161 | } 162 | return self.getErrors(null, context); 163 | } 164 | 165 | function createExecutionResult(errors) { 166 | if (errors.length > 0) return executionFailureFunc(errors); 167 | try { 168 | var result = validationSuccessFunc.apply(self, functionArgs); 169 | result = utility.autoWrapValidationCompleteResult(result); 170 | return result.then(result => { 171 | return Promise.resolve(new ExecutionResult(true, result, null)); 172 | }) 173 | .catch(handleError); 174 | } catch(err) { 175 | return handleError(err); 176 | } 177 | } 178 | 179 | function returnResult(result) { 180 | if (done) return done(null, result); 181 | return result; 182 | } 183 | 184 | function handleError(err) { 185 | if (err instanceof ServiceException) { 186 | return Promise.resolve(new ExecutionResult(false, null, err.errors)); 187 | } 188 | return Promise.reject(err); 189 | } 190 | } 191 | }; 192 | 193 | Command.extend = function(options) { 194 | options = options || {}; 195 | var params = options.params || []; 196 | var functions = options.functions || {}; 197 | 198 | var Extended = function() { 199 | var self = this; 200 | self.arguments = arguments; 201 | params.forEach(function(param, index) { 202 | self[param] = self.arguments[index]; 203 | }); 204 | }; 205 | 206 | Extended.prototype = new Command(); 207 | 208 | Extended.prototype._onInitialization = functions._onInitialization || function() { 209 | var doneCallback = arguments[Object.keys(arguments).length -1]; 210 | if (doneCallback && typeof doneCallback === 'function') return doneCallback(null); 211 | return Promise.resolve(); 212 | }; 213 | 214 | Extended.prototype._getRules = functions._getRules || function() { 215 | var doneCallback = arguments[Object.keys(arguments).length -1]; 216 | if (doneCallback && typeof doneCallback === 'function') return doneCallback(null, []); 217 | return Promise.resolve([]); 218 | }; 219 | 220 | Extended.prototype._onValidationSuccess = functions._onValidationSuccess || function() { 221 | var doneCallback = arguments[Object.keys(arguments).length -1]; 222 | if (doneCallback && typeof doneCallback === 'function') return doneCallback(null); 223 | return Promise.resolve(); 224 | }; 225 | 226 | return Extended; 227 | }; 228 | 229 | Command.executeAll = function(commands, done) { 230 | 231 | if (!Array.isArray(commands)) { 232 | commands = [commands]; 233 | } 234 | 235 | var count = commands.length; 236 | 237 | if (count < 1) { 238 | if (done) return done(); 239 | return Promise.resolve(); 240 | } 241 | 242 | if (!done) { 243 | return Promise.all(commands.map(c => c.execute())); 244 | } 245 | 246 | var current = 0; 247 | var results = []; 248 | 249 | commands.forEach(function(command) { 250 | command.execute(onComplete); 251 | }); 252 | 253 | function onComplete(err, result) { 254 | if (err) { return done(err, results); } 255 | current++; 256 | results.push(result); 257 | if (current === count) { 258 | done(null, results); 259 | } 260 | } 261 | }; 262 | 263 | return Command; 264 | 265 | })(); 266 | 267 | module.exports = Command; 268 | -------------------------------------------------------------------------------- /src/configuration.js: -------------------------------------------------------------------------------- 1 | var Configuration = (function() { 2 | 3 | "use strict"; 4 | 5 | var Configuration = function(callbacks) { 6 | }; 7 | 8 | Configuration.autoPromiseWrap = false; 9 | 10 | return Configuration; 11 | 12 | })(); 13 | 14 | module.exports = Configuration; 15 | -------------------------------------------------------------------------------- /src/executionResult.js: -------------------------------------------------------------------------------- 1 | var ExecutionResult = (function() { 2 | 3 | "use strict"; 4 | 5 | var ExecutionResult = function(success, value, errors) { 6 | if (this instanceof ExecutionResult) { 7 | this.success = success; 8 | this.value = value; 9 | this.errors = errors; 10 | } else { 11 | return new ExecutionResult(success, value, errors); 12 | } 13 | }; 14 | 15 | return ExecutionResult; 16 | 17 | })(); 18 | 19 | module.exports = ExecutionResult; 20 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module Peasy { 2 | 3 | /** Represents a data store abstraction */ 4 | interface IDataProxy { 5 | 6 | /** Accepts the id of the object to be queried and returns it asynchronously. 7 | * @param id The id of the object to query by. 8 | * @returns A promise that when resolved, returns the queried object. */ 9 | getById(id: TKey): Promise 10 | 11 | /** Asynchronously returns all values from a data source and is especially useful for lookup data. 12 | * @returns A promise that when resolved, returns an array of all of the objects from a data source. */ 13 | getAll(): Promise 14 | 15 | /** Accepts an object and asynchronously inserts it into the data store.. 16 | * @param data The object to insert. 17 | * @returns A promise that when resolved, returns an updated version of the object as a result of the insert operation. */ 18 | insert(data: T): Promise 19 | 20 | /** Accepts an object and asynchronously updates it in the data store. 21 | * @param data The object to update. 22 | * @returns A promise that when resolved, returns an updated version of the object as a result of the update operation. */ 23 | update(data: T): Promise 24 | 25 | /** Accepts the id of the object to be deleted and asynchronously deletes it from the data store. 26 | * @param id The id of the object to delete. 27 | * @returns A resolvable promise. */ 28 | destroy(id: TKey): Promise 29 | } 30 | 31 | /** Represents a handled error */ 32 | class PeasyError { 33 | 34 | /** (Optional) The field that the message is associated with. */ 35 | association?: string; 36 | 37 | /** The error message. */ 38 | message: string; 39 | } 40 | 41 | /** Serves as optional arguments to the constructor of a Command */ 42 | class CommandArgs { 43 | 44 | /** (Optional) Used to perform initialization logic before rules are executed. 45 | * @returns An awaitable promise. 46 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 47 | _onInitialization?: (context: any) => Promise; 48 | 49 | /** (Optional) Used to return a list of rules whose execution outcome will determine whether or not to invoke _onValidateSuccess. 50 | * @returns An awaitable array of IRule. 51 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 52 | _getRules?: (context: any) => Promise; 53 | 54 | /** Primarily used to interact with data proxies, workflow logic, etc. 55 | * @returns An awaitable promise. 56 | * @param context An object that can be used as a property bag throughout this command's execution pipeline. */ 57 | _onValidationSuccess: (context: any) => Promise 58 | } 59 | 60 | /** Contains peasy-js configuration settings */ 61 | class Configuration { 62 | 63 | /** if true, will wrap command function results in promises */ 64 | static autoPromiseWrap: boolean; 65 | } 66 | 67 | /** Represents a business service abstraction */ 68 | interface IBusinessService { 69 | 70 | /** Accepts the id of the object to be queried and returns a command. 71 | * @param id The id of the object to query by. 72 | * @returns A command that when executed, retrieves the object associated with the supplied id argument upon successful rule validation. */ 73 | getByIdCommand(id: TKey): ICommand; 74 | 75 | /** Returns a command that delivers all values from a data source and is especially useful for lookup data. 76 | * @returns A command that when executed, retrieves all of the objects upon successful rule validation. */ 77 | getAllCommand(): ICommand; 78 | 79 | /** @param data The object to insert. 80 | * @returns A command that when executed, inserts the object upon successful rule validation. */ 81 | insertCommand(data: T): ICommand; 82 | 83 | /** @param data The object to update. 84 | * @returns A command that when executed, updates the object upon successful rule validation. */ 85 | updateCommand(data: T): ICommand; 86 | 87 | /** @param id The id of the object to delete. 88 | * @returns A command that when executed, deletes the object associated with the supplied id argument upon successful rule validation. */ 89 | destroyCommand(id: TKey): ICommand; 90 | } 91 | 92 | /** Base class for all business services */ 93 | abstract class BusinessService implements IBusinessService { 94 | 95 | protected dataProxy: IDataProxy; 96 | 97 | /** @param dataProxy The data store abstraction. */ 98 | constructor(dataProxy: IDataProxy); 99 | 100 | /** Accepts the id of the object to be queried and returns a command. 101 | * @param id The id of the object to query by. 102 | * @returns A command that when executed, retrieves the object associated with the supplied id argument upon successful rule validation. */ 103 | getByIdCommand(id: TKey): ICommand; 104 | 105 | /** Returns a command that delivers all values from a data source and is especially useful for lookup data. 106 | * @returns A command that when executed, retrieves all of the objects upon successful rule validation. */ 107 | getAllCommand(): ICommand; 108 | 109 | /** Accepts an object to be inserted into a data store and returns a command. 110 | * @param data The object to insert. 111 | * @returns A command that when executed, inserts the object upon successful rule validation. */ 112 | insertCommand(data: T): ICommand; 113 | 114 | /** Accepts an object to be updated within a data store and returns a command. 115 | * @param data The object to update. 116 | * @returns A command that when executed, updates the object upon successful rule validation. */ 117 | updateCommand(data: T): ICommand; 118 | 119 | /** Accepts the id of the object to be deleted from a data store and returns a command. 120 | * @param id The id of the object to delete. 121 | * @returns A command that when executed, deletes the object associated with the supplied id argument upon successful rule validation. */ 122 | destroyCommand(id: TKey): ICommand; 123 | 124 | /** Override this function to perform initialization logic before rule validations for getByIDCommand are performed. 125 | * @param id The id of the object to query by. 126 | * @param context An object that can be used as a property bag throughout the getByIdCommand execution pipeline. 127 | * @returns An awaitable promise. */ 128 | protected _onGetByIdCommandInitialization(id: TKey, context: any): Promise; 129 | 130 | /** Override this function to perform initialization logic before rule validations for getAllCommand are performed. 131 | * @param context An object that can be used as a property bag throughout the getAllCommand execution pipeline. 132 | * @returns An awaitable promise. */ 133 | protected _onGetAllCommandInitialization(context: any): Promise; 134 | 135 | /** Override this function to perform initialization logic before rule validations for insertCommand are performed. 136 | * @param data The object to save (insert). 137 | * @param context An object that can be used as a property bag throughout the insertCommand execution pipeline. 138 | * @returns An awaitable promise. */ 139 | protected _onInsertCommandInitialization(data: T, context: any): Promise; 140 | 141 | /** Override this function to perform initialization logic before rule validations for updateCommand are performed. 142 | * @param data The object to save (update). 143 | * @param context An object that can be used as a property bag throughout the updateCommand execution pipeline. 144 | * @returns An awaitable promise. */ 145 | protected _onUpdateCommandInitialization(data: T, context: any): Promise; 146 | 147 | /** Override this function to perform initialization logic before rule validations for destroyCommand are performed. 148 | * @param id The id of the object to delete. 149 | * @param context An object that can be used as a property bag throughout the destroyCommand execution pipeline. 150 | * @returns An awaitable promise. */ 151 | protected _onDestroyCommandInitialization(id: TKey, context: any): Promise; 152 | 153 | /** Override this function to supply custom business rules to getByIdCommand. 154 | * @param id The id of the object to query by. 155 | * @param context An object that can be used as a property bag throughout the getByIdCommand execution pipeline. 156 | * @returns An awaitable array of IRule. */ 157 | protected _getRulesForGetByIdCommand(id: TKey, context: any): Promise; 158 | 159 | /** Override this function to supply custom business rules to getAllCommand. 160 | * @param context An object that can be used as a property bag throughout the getAllCommand execution pipeline. 161 | * @returns An awaitable array of IRule. */ 162 | protected _getRulesForGetAllCommand(context: any): Promise; 163 | 164 | /** Override this function to supply custom business rules to insertCommand. 165 | * @param data The object to save (insert). 166 | * @param context An object that can be used as a property bag throughout the insertCommand execution pipeline. 167 | * @returns An awaitable array of IRule. */ 168 | protected _getRulesForInsertCommand(data: T, context: any): Promise; 169 | 170 | /** Override this function to supply custom business rules to updateCommand. 171 | * @param data The object to save (update). 172 | * @param context An object that can be used as a property bag throughout the updateCommand execution pipeline. 173 | * @returns An awaitable array of IRule. */ 174 | protected _getRulesForUpdateCommand(data: T, context: any): Promise; 175 | 176 | /** Override this function to supply custom business rules to destroyCommand. 177 | * @param id The id of the object to delete. 178 | * @param context An object that can be used as a property bag throughout the destroyCommand execution pipeline. 179 | * @returns An awaitable array of IRule. */ 180 | protected _getRulesForDestroyCommand(id: TKey, context: any): Promise; 181 | 182 | /** Invoked by the command returned from getByIdCommand() if validation and business rules execute successfully. 183 | * @param id The id of the object to query by. 184 | * @param context An object that has been passed through the getByIdCommand execution pipeline. 185 | * @returns An awaitable promise. */ 186 | protected _getById(id: TKey, context: any): Promise; 187 | 188 | /** Invoked by the command returned from getAllCommand() if validation and business rules execute successfully. 189 | * @param context An object that has been passed through the getAllCommand execution pipeline. 190 | * @returns An awaitable promise. */ 191 | protected _getAll(context: any): Promise; 192 | 193 | /** Invoked by the command returned from insertCommand() if validation and business rules execute successfully. 194 | * @param data The object to save (insert). 195 | * @param context An object that has been passed through the insertCommand execution pipeline. 196 | * @returns An awaitable promise. */ 197 | protected _insert(data: T, context: any): Promise; 198 | 199 | /** Invoked by the command returned from updateCommand() if validation and business rules execute successfully. 200 | * @param data The object to save (update). 201 | * @param context An object that has been passed through the updateCommand execution pipeline. 202 | * @returns An awaitable promise. */ 203 | protected _update(data: T, context: any): Promise; 204 | 205 | /** Invoked by the command returned from destroyCommand() if validation and business rules execute successfully. 206 | * @param id The id of the object to delete. 207 | * @param context An object that has been passed through the deleteCommand execution pipeline. 208 | * @returns An awaitable promise. */ 209 | protected _destroy(id: TKey, context: any): Promise; 210 | } 211 | 212 | /** Exceptions of this type are explicitly caught and handled by commands during execution. 213 | * If caught, the command will return a failed execution result with error messages. 214 | * ServiceException can be used in many situations, but is especially helpful to throw within data proxies 215 | * See https://github.com/peasy/peasy-js-samples for examples on usage */ 216 | class ServiceException { 217 | 218 | /** @param The error message to display. */ 219 | constructor(message: string); 220 | 221 | /** These errors will be added to a failed execution result's error collection. */ 222 | errors: PeasyError[]; 223 | 224 | /** The error message to display. */ 225 | readonly message: string; 226 | } 227 | 228 | /** Serves as the result of a command's execution. */ 229 | class ExecutionResult { 230 | 231 | /** @param success States whether the command execution was successful. 232 | * @param value Represents the data returned as a result of command execution. 233 | * @param errors Represents the errors returned from failed rules (if any). */ 234 | constructor(success: boolean, value?: T, errors?: PeasyError[]); 235 | 236 | /** Determines whether the command execution was successful. */ 237 | readonly success: boolean; 238 | 239 | /** Represents the data returned as a result of command execution. */ 240 | readonly value: T; 241 | 242 | /** Represents the errors returned from failed rules (if any). */ 243 | readonly errors: PeasyError[]; 244 | } 245 | 246 | /** Represents a command abstraction */ 247 | interface ICommand { 248 | 249 | /** Executes validation/business rule execution. 250 | * @returns An array of errors if validation fails. */ 251 | getErrors(): Promise; 252 | 253 | /** Executes initialization logic, validation/business rule execution, and command logic. 254 | * @returns An execution result. */ 255 | execute(): Promise>; 256 | } 257 | 258 | /** Responsible for orchestrating the execution of initialization logic, validation/business rule execution, and command logic */ 259 | class Command implements ICommand { 260 | 261 | /** Executes an array of commands and returns after all have completed. 262 | * @param commands An array of commands. 263 | * @returns An array of execution results. */ 264 | static executeAll(commands: Command[]): Promise[]>; 265 | 266 | /** @param args (Optional) Functions that an instance of this command will use. */ 267 | constructor(args?: CommandArgs); 268 | 269 | /** Executes validation/business rule execution. 270 | * @returns An array of errors if validation fails. */ 271 | getErrors(): Promise; 272 | 273 | /** Executes initialization logic, validation/business rule execution, and command logic. 274 | * @returns An execution result. */ 275 | execute(): Promise>; 276 | 277 | /** Used to perform initialization logic before rules are executed. 278 | * @returns An awaitable promise. */ 279 | protected _onInitialization(context: any): Promise; 280 | 281 | /** Used to return a list of rules whose execution outcome will determine whether or not to invoke _onValidateSuccess. 282 | * @returns An awaitable array of IRule. */ 283 | protected _getRules(context: any): Promise; 284 | 285 | /** Primarily used to interact with data proxies, workflow logic, etc. 286 | * @returns An awaitable promise. */ 287 | protected _onValidationSuccess(context: any): Promise; 288 | } 289 | 290 | class ifAllValidResult { 291 | /** @param func A function that when executed, returns an awaitable array of rules. 292 | * @returns A executable rule. */ 293 | thenGetRules(func: () => Promise): Rule 294 | } 295 | 296 | /** Represents a rule abstraction */ 297 | interface IRule { 298 | 299 | /** Associates an instance of the rule with a field. */ 300 | association: string; 301 | 302 | /** A list of errors resulting from failed validation. */ 303 | errors: PeasyError[]; 304 | 305 | /** Indicates whether the rule is successful after validation. */ 306 | valid: boolean; 307 | 308 | /** Invokes the rule. 309 | * @returns An awaitable promise. */ 310 | validate(): Promise; 311 | } 312 | 313 | /** Represents a container for business logic. */ 314 | abstract class Rule implements IRule { 315 | 316 | /** Extracts all rules from an array of commands. 317 | * @param commands An array of commands. 318 | * @returns An awaitable rule. */ 319 | static getAllRulesFrom(commands: Command[]): Promise; 320 | 321 | /** Extracts all rules from an array of commands. 322 | * @param commands An spread of commands. 323 | * @returns An awaitable rule. */ 324 | static getAllRulesFrom(...commands: Command[]): Promise; 325 | 326 | /** Returns a function that upon execution, will only return the next set of rules on successful validation of the initial set. 327 | * @param rules An array of rules. 328 | * @returns An ifAllValidResult. */ 329 | static ifAllValid(rules: IRule[]): ifAllValidResult; 330 | 331 | /** Returns a function that upon execution, will only return the next set of rules on successful validation of the initial set. 332 | * @param rules A spread of rules. 333 | * @returns An ifAllValidResult. */ 334 | static ifAllValid(...rules: IRule[]): ifAllValidResult; 335 | 336 | /** Override this function to perform the business/validation logic. Invoke _invalidate() if the logic fails validation. 337 | * @returns An awaitable promise. */ 338 | protected abstract _onValidate(): Promise; 339 | 340 | /** Override this function to gain more control as to how validation is performed. 341 | * @param message The validation failure error message. */ 342 | protected _invalidate(message: string): void; 343 | 344 | /** Associates an instance of the rule with a field. */ 345 | association: string; 346 | 347 | /** Indicates whether the rule is successful after validation. */ 348 | readonly valid: boolean; 349 | 350 | /** A list of errors resulting from failed validation. */ 351 | readonly errors: PeasyError[]; 352 | 353 | /** Invokes the rule. On completion, _valid_ and _errors_ (if applicable) will be set. 354 | * @returns An awaitable promise. */ 355 | validate(): Promise 356 | 357 | /** Invokes the supplied set of rules if the validation of this rule is successful. 358 | * @param rules A spread of rules. 359 | * @returns A reference to this rule. */ 360 | ifValidThenValidate(...rules: Rule[]): Rule; 361 | 362 | /** Invokes the supplied set of rules if the validation of this rule is successful. 363 | * @param rules An array of rules. 364 | * @returns A reference to this rule. */ 365 | ifValidThenValidate(rules: Rule[]): Rule; 366 | 367 | /** Invokes the supplied set of rules if the validation of this rule fails. 368 | * @param rules A spread of rules. 369 | * @returns A reference to this rule. */ 370 | ifInvalidThenValidate(...rules: Rule[]): Rule; 371 | 372 | /** Invokes the supplied set of rules if the validation of this rule fails. 373 | * @param rules An array of rules. 374 | * @returns A reference to this rule. */ 375 | ifInvalidThenValidate(rules: Rule[]): Rule; 376 | 377 | /** Invokes the supplied function if the validation of this rule is successful. 378 | * @param func A function to execute that receives a reference to the invoking rule 379 | * @returns A reference to this rule. */ 380 | ifValidThenExecute(func: (rule: Rule) => void): Rule 381 | 382 | /** Invokes the supplied function if the validation of this rule fails. 383 | * @param func A function to execute that receives a reference to the invoking rule 384 | * @returns A reference to this rule. */ 385 | ifInvalidThenExecute(func: (rule: Rule) => void): Rule 386 | 387 | /** Invokes the supplied function if the validation of this rule is successful. 388 | * @param func A function that returns an awaitable array of rules. 389 | * @returns A reference to this rule. */ 390 | ifValidThenGetRules(func: () => Promise): Rule 391 | } 392 | 393 | } 394 | 395 | export = Peasy; 396 | 397 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var BusinessService = require('./businessService'); 2 | var Command = require('./command'); 3 | var ExecutionResult = require('./executionResult'); 4 | var Rule = require('./rule'); 5 | var ServiceException = require('./serviceException'); 6 | var Configuration = require('./configuration'); 7 | 8 | module.exports = { 9 | BusinessService: BusinessService, 10 | Command: Command, 11 | ExecutionResult: ExecutionResult, 12 | Rule: Rule, 13 | ServiceException: ServiceException, 14 | Configuration: Configuration 15 | } 16 | -------------------------------------------------------------------------------- /src/rule.js: -------------------------------------------------------------------------------- 1 | var RulesValidator = require('./rulesValidator'); 2 | var utility = require('./utility'); 3 | 4 | var Rule = function () { 5 | 6 | "use strict"; 7 | 8 | var Rule = function(options) { 9 | if (this instanceof Rule) { 10 | options = options || {}; 11 | this.association = options.association || null; 12 | this.errors = []; 13 | this.ifInvalidThenFn = null; 14 | this.ifValidThenFn = null; 15 | this.ifValidThenGetRulesFn = null; 16 | this.validSuccessors = []; 17 | this.invalidSuccessors = []; 18 | this.valid = true; 19 | } else { 20 | return new Rule(); 21 | } 22 | }; 23 | 24 | Rule.getAllRulesFrom = function(commands, done) { 25 | 26 | var context = {}; 27 | 28 | if (done) return(doWork(commands, done)); 29 | 30 | return Promise.all(commands.map(c => c._getRules(context))) 31 | .then(results => [].concat.apply([], results)); // flatten array 32 | 33 | function doWork(commands, done) { 34 | 35 | if (!Array.isArray(commands)) { 36 | commands = [commands]; 37 | } 38 | 39 | var count = commands.length; 40 | 41 | if (count < 1) { 42 | if (done) return done(null, []); 43 | }; 44 | 45 | var current = 0; 46 | var rules = []; 47 | 48 | commands.forEach(command => { 49 | command._getRules(context, onComplete); 50 | }); 51 | 52 | function onComplete(err, rule) { 53 | if (err) { return done(err, rules); } 54 | if (Array.isArray(rule)) { 55 | rule.forEach(function(r) { rules.push(r) }); 56 | } else { 57 | rules.push(rule); 58 | } 59 | current++; 60 | if (current === count) { 61 | done(null, rules); 62 | } 63 | } 64 | } 65 | }; 66 | 67 | Rule.ifAllValid = function(rules) { 68 | 69 | function thenGetRules(func) { 70 | var rule = new Rule(); 71 | rule._onValidate = function(done) { 72 | if (done) return done(); 73 | return Promise.resolve(); 74 | }; 75 | 76 | rule.validSuccessors = rules; 77 | rule.ifValidThenGetRulesFn = func; 78 | return rule; 79 | } 80 | 81 | return { 82 | thenGetRules: thenGetRules 83 | }; 84 | 85 | }; 86 | 87 | Rule.extend = function(options) { 88 | options = options || {}; 89 | options.functions = options.functions || {}; 90 | 91 | if (typeof options.functions._onValidate !== 'function') { 92 | throw new Error('An onValidate method needs to be supplied to execute!'); 93 | } 94 | 95 | options.association = options.association || null; 96 | options.params = options.params || []; 97 | 98 | var Extended = function() { 99 | var self = this; 100 | self.arguments = arguments; 101 | Rule.call(self, { association: options.association }); 102 | options.params.forEach(function(field, index) { 103 | self[field] = self.arguments[index]; 104 | }); 105 | }; 106 | 107 | Extended.prototype = new Rule(); 108 | Extended.prototype._onValidate = options.functions._onValidate; 109 | 110 | return Extended; 111 | }; 112 | 113 | Rule.prototype = { 114 | 115 | constructor: Rule, 116 | 117 | _invalidate: function(errors) { 118 | var self = this; 119 | this.valid = false; 120 | if (!Array.isArray(errors)) { 121 | errors = [errors]; 122 | } 123 | errors.forEach(function(err) { 124 | if (typeof err === "string") { 125 | self.errors.push({ association: self.association, message: err }); 126 | } else { 127 | self.errors.push(err); 128 | } 129 | }); 130 | }, 131 | 132 | /** 133 | * This set a invalid Rule to valid again. 134 | * which is used for the ifInvalidThenValidate Functionality. 135 | * @private 136 | */ 137 | _unInvalidate: function () { 138 | var self = this; 139 | this.valid = true; 140 | self.errors = []; 141 | }, 142 | 143 | _onValidate: function (done) {}, 144 | 145 | validate: function(done) { 146 | var self = this; 147 | self.errors = []; 148 | var args = self.arguments || {}; 149 | var argumentValues = Object.keys(args).map(key => self.arguments[key]); 150 | 151 | if (done) { 152 | var cb = (err) => { 153 | if (err) return done(err); 154 | validationComplete(done); 155 | }; 156 | return this._onValidate.apply(self, argumentValues.concat(cb)); 157 | } 158 | 159 | var result = this._onValidate.apply(self, argumentValues); 160 | result = utility.autoWrapValidationResult(result); 161 | 162 | return result.then(validationComplete); 163 | 164 | function validationComplete(onComplete) { 165 | if (self.valid) { 166 | if (self.ifValidThenFn) { 167 | self.ifValidThenFn(self); 168 | } 169 | if (self.validSuccessors.length > 0) { 170 | return invokeSuccessorsAndNextRules(self, self.validSuccessors, onComplete); 171 | } else { 172 | if (self.ifValidThenGetRulesFn) { 173 | return invokeNextRules(self, self.validSuccessors, onComplete); 174 | } 175 | } 176 | } else { 177 | if (self.ifInvalidThenFn) { 178 | self.ifInvalidThenFn(self); 179 | } 180 | if (self.invalidSuccessors.length > 0) { 181 | return invokeSuccessors(self, self.invalidSuccessors, onComplete); 182 | } 183 | } 184 | if (onComplete) onComplete(); 185 | } 186 | 187 | function invokeSuccessorsAndNextRules(rule, successors, onComplete) { 188 | if (onComplete) { 189 | return invokeSuccessors(rule, successors, () => { 190 | if (rule.ifValidThenGetRulesFn) { 191 | return invokeNextRules(rule, successors, onComplete); 192 | } 193 | onComplete(); 194 | }); 195 | } 196 | 197 | return invokeSuccessors(rule, successors).then(() => { 198 | if (rule.ifValidThenGetRulesFn) { 199 | return invokeNextRules(rule, successors); 200 | } 201 | }); 202 | } 203 | 204 | function invokeSuccessors(parent, rules, onComplete) { 205 | if (onComplete) { 206 | return new RulesValidator(rules).validate(function (err) { 207 | if (err) return onComplete(err); 208 | invalidate(parent).ifAnyInvalid(rules); 209 | onComplete(); 210 | }); 211 | } 212 | 213 | return new RulesValidator(rules).validate() 214 | .then(() => invalidate(parent).ifAnyInvalid(rules)); 215 | } 216 | 217 | function invokeNextRules(rule, rules, onComplete) { 218 | var failedRules = rules.filter(function(rule) { return !rule.valid; }); 219 | 220 | if (failedRules.length > 0) { 221 | if (onComplete) return onComplete(); 222 | return Promise.resolve(); 223 | } 224 | 225 | if (onComplete) { 226 | return rule.ifValidThenGetRulesFn(function(err, successors) { 227 | if (!Array.isArray(successors)) { 228 | successors = [successors]; 229 | } 230 | return invokeSuccessors(rule, successors, onComplete); 231 | }); 232 | } 233 | 234 | return rule.ifValidThenGetRulesFn().then(rules => { 235 | if (!Array.isArray(rules)) { 236 | rules = [rules]; 237 | } 238 | return invokeSuccessors(rule, rules); 239 | }); 240 | 241 | } 242 | 243 | function invalidate(rule) { 244 | 245 | function ifAnyInvalid(rules) { 246 | const invalidRules = rules.filter(function (r) { 247 | return !r.valid; 248 | }); 249 | // set the invalid-status if there are invalid rules 250 | if (invalidRules.length > 0) { 251 | invalidRules.forEach(function (r) { 252 | rule._invalidate(r.errors); 253 | }); 254 | } 255 | // otherwise set it valid 256 | else { 257 | rule._unInvalidate(); 258 | } 259 | 260 | } 261 | 262 | return { ifAnyInvalid: ifAnyInvalid }; 263 | } 264 | }, 265 | 266 | ifValidThenValidate: function(rules) { 267 | if (!Array.isArray(rules)) { 268 | rules = [rules]; 269 | } 270 | this.validSuccessors = rules; 271 | return this; 272 | }, 273 | 274 | ifValidThenExecute: function(funcToExecute) { 275 | this.ifValidThenFn = funcToExecute; 276 | return this; 277 | }, 278 | 279 | /** 280 | * If a rule is invalid then this function adds an alternative. 281 | * if the alternative is true, also the rule is. 282 | * 283 | * A special case apply by using an array of rules as alternative, 284 | * in this case, all rules in the array will be joined by an AND by default, 285 | * so that the alternative is valid, if ALL rules of the alternative are valid 286 | * 287 | * @param {Rule|Array} rules the alternative rule(s) that should be checked when the rule is invalid 288 | * @return {Rule} 289 | */ 290 | ifInvalidThenValidate: function (rules) { 291 | if (!Array.isArray(rules)) { 292 | rules = [rules]; 293 | } 294 | this.invalidSuccessors = rules; 295 | return this; 296 | }, 297 | 298 | ifInvalidThenExecute: function(funcToExecute) { 299 | this.ifInvalidThenFn = funcToExecute; 300 | return this; 301 | }, 302 | 303 | ifValidThenGetRules: function(funcToExecute) { 304 | this.ifValidThenGetRulesFn = funcToExecute; 305 | return this; 306 | } 307 | 308 | }; 309 | 310 | return Rule; 311 | }(); 312 | 313 | module.exports = Rule; 314 | -------------------------------------------------------------------------------- /src/rulesValidator.js: -------------------------------------------------------------------------------- 1 | var RulesValidator = (function() { 2 | "use strict"; 3 | 4 | // RULES VALIDATOR 5 | var RulesValidator = function(rules) { 6 | if (this instanceof RulesValidator) { 7 | this.rules = rules; 8 | } else { 9 | return new RulesValidator(rules); 10 | } 11 | }; 12 | 13 | RulesValidator.prototype.validate = function(done) { 14 | var self = this; 15 | 16 | var validations = self.rules.map(r => r.validate.bind(r)); 17 | if (done) { 18 | validations = validations.map(v => wrap(v)); 19 | } 20 | 21 | var promise = Promise.all(validations.map(v => v())) 22 | .then(() => { 23 | if (done) return done(null, self.rules); 24 | return Promise.resolve(self.rules); 25 | }) 26 | .catch(e => { 27 | if (done) return done(e); 28 | return Promise.reject(e); 29 | }); 30 | 31 | if (!done) return promise; 32 | 33 | function wrap(fn) { 34 | return function() { 35 | return new Promise((resolve, reject) => { 36 | fn(function(err, result) { 37 | if (err) return reject(err); 38 | resolve(result); 39 | }); 40 | }); 41 | } 42 | } 43 | }; 44 | 45 | return RulesValidator; 46 | 47 | })(); 48 | 49 | 50 | module.exports = RulesValidator; 51 | -------------------------------------------------------------------------------- /src/sampleWithCallbacks.js: -------------------------------------------------------------------------------- 1 | // This sample is meant to illustrate how to create business services, 2 | // custom commands, and rules, and to showcase how they interact with 3 | // each other. In a real world application, you would most likely keep 4 | // each business service, command, and rule in its own file, or at least 5 | // similar actors in the same files. 6 | 7 | "use strict"; 8 | 9 | var peasy = require('./../dist/peasy'); 10 | var Rule = peasy.Rule; 11 | var BusinessService = peasy.BusinessService; 12 | var Command = peasy.Command; 13 | 14 | 15 | // CREATE RULES 16 | // see https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules for more details 17 | 18 | var AgeRule = Rule.extend({ 19 | association: "age", 20 | params: ['birthdate'], 21 | functions: { 22 | _onValidate: function(birthdate, done) { 23 | if (new Date().getFullYear() - birthdate.getFullYear() < 50) { 24 | this._invalidate("You are too young"); 25 | } 26 | var time = Math.floor((Math.random() * 3000) + 1); 27 | setTimeout(() => done(), time); // simulate latency 28 | } 29 | } 30 | }); 31 | 32 | var NameRule = Rule.extend({ 33 | association: "name", 34 | params: ['name'], 35 | functions: { 36 | _onValidate: function(name, done) { 37 | if (name === "Jimi") { 38 | this._invalidate("Name cannot be Jimi"); 39 | } 40 | var time = Math.floor((Math.random() * 3000) + 1); 41 | setTimeout(() => done(), time); // simulate latency 42 | } 43 | } 44 | }); 45 | 46 | var FieldRequiredRule = Rule.extend({ 47 | params: ['field', 'data'], 48 | functions: { 49 | _onValidate: function(field, data, done) { 50 | if (!data[field]) { 51 | this.association = field; 52 | this._invalidate(field + " is required"); 53 | } 54 | var time = Math.floor((Math.random() * 3000) + 1); 55 | setTimeout(() => done(), time); // simulate latency 56 | } 57 | } 58 | }); 59 | 60 | var CustomerAuthorizationRule = Rule.extend({ 61 | params: ['roles'], 62 | functions: { 63 | _onValidate: function(roles, done) { 64 | var validRoles = ['super admin', 'admin']; 65 | if (!roles.some(r => validRoles.indexOf(r) > -1)) { 66 | this._invalidate("You do not have sufficient priviledges to access national security information"); 67 | } 68 | done(); 69 | } 70 | } 71 | }); 72 | 73 | 74 | // CREATE SERVICES, CUSTOM COMMAND, AND WIRE UP VALIDATION AND BUSINESS RULES 75 | // see https://github.com/peasy/peasy-js/wiki/BusinessService and 76 | // https://github.com/peasy/peasy-js/wiki/Command for more details 77 | 78 | // ROLES SERVICE 79 | var RolesService = BusinessService.extend({ 80 | params: ['userId', 'dataProxy'], 81 | functions: { 82 | _getAll: function(context, done) { 83 | this.dataProxy.getById(this.userId, function(err, roles) { 84 | done(null, roles); 85 | }); 86 | } 87 | } 88 | }).service; 89 | 90 | 91 | // CUSTOMER SERVICE 92 | var CustomerService = BusinessService 93 | .extend({ 94 | params: ['dataProxy', 'rolesService'], 95 | functions: { 96 | _getRulesForInsertCommand: getRulesForInsert, 97 | _getAll: function(context, done) { 98 | this.dataProxy.getAll((err, data) => { 99 | data.forEach(customer => { 100 | delete customer.nsd; // remove confidential data 101 | }); 102 | done(err, data); 103 | }); 104 | }, 105 | _getById: function(id, context, done) { 106 | this.dataProxy.getById((err, data) => { 107 | delete data.nsd; // remove confidential data 108 | done(err, data); 109 | }); 110 | } 111 | } 112 | }) 113 | .createCommand({ 114 | name: 'getNationalSecurityCommand', 115 | params: ['id'], 116 | functions: 117 | { 118 | _getRules: function(id, context, done) { 119 | var getRolesForCurrentUserCommand = this.rolesService.getAllCommand(); 120 | getRolesForCurrentUserCommand.execute((err, result) => { 121 | if (!result.success) return done(null, result.errors); 122 | var roles = result.value; 123 | done(err, new CustomerAuthorizationRule(roles)); 124 | }); 125 | }, 126 | _onValidationSuccess: function(id, context, done) { 127 | this.dataProxy.getById(id, (err, data) => { 128 | done(err, { id: data.id, nsd: data.nsd }); 129 | }); 130 | } 131 | } 132 | }) 133 | .service; 134 | 135 | function getRulesForInsert(data, context, done) { 136 | 137 | // see https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules for more details 138 | 139 | var customer = data; 140 | 141 | // these will all execute 142 | // done(null, [ 143 | // new AgeRule(customer.age), 144 | // new NameRule(customer.name), 145 | // new FieldRequiredRule("address", customer) 146 | // ]); 147 | 148 | // chained rules - rules will only execute upon successful validation of predecessor 149 | done(null, new AgeRule(customer.age) 150 | .ifValidThenExecute(() => console.log("Age succeeded")) 151 | .ifInvalidThenExecute(() => console.log("Age failed")) 152 | .ifValidThenValidate(new NameRule(customer.name) 153 | .ifValidThenExecute(() => console.log("Name succeeded")) 154 | .ifInvalidThenExecute(() => console.log("Name failed")) 155 | .ifValidThenValidate(new FieldRequiredRule("address", customer) 156 | .ifValidThenExecute(() => console.log("Address succeeded")) 157 | .ifInvalidThenExecute(() => console.log("Address failed")) 158 | ))); 159 | } 160 | 161 | 162 | // CREATE IN-MEMORY DATA PROXIES (these could be duck typed angular resources, react stores, mongo db implementations, http proxies, etc.) 163 | // See https://github.com/peasy/peasy-js/wiki/Data-Proxy for details 164 | 165 | var customerDataProxy = (function() { 166 | var state = [ 167 | { id: 1, name: "James Hendrix", nsd: "234322345" }, 168 | { id: 2, name: "James Page", nsd: "8492834926" }, 169 | { id: 3, name: "David Gilmour", nsd: "433423422" }, 170 | ]; 171 | 172 | return { 173 | insert: insert, 174 | getAll: getAll, 175 | getById: getById 176 | }; 177 | 178 | function insert(data, done) { 179 | var nextId = state.length + 1; 180 | data.id = nextId; 181 | state.push(Object.assign({}, data)); 182 | done(null, data); 183 | } 184 | 185 | function getAll(done) { 186 | var data = state.map(function(customer) { 187 | return Object.assign({}, customer); 188 | }); 189 | done(null, data); 190 | } 191 | 192 | function getById(id, done) { 193 | var customer = state.filter(function(c) { 194 | return c.id === id; 195 | })[0]; 196 | done(null, Object.assign({}, customer)); 197 | } 198 | 199 | })(); 200 | 201 | var rolesDataProxy = { 202 | getById: function(id, done) { 203 | // add/remove roles to manipulate execution of getNationalSecurityCommand 204 | done(null, ['', 'user']); 205 | } 206 | } 207 | 208 | 209 | // CREATE INSTANCE OF A CUSTOMER SERVICE WITH THE REQUIRED DATA PROXY 210 | var currentUserId = 12345; // this id would likely come from some authentication service 211 | var rolesService = new RolesService(currentUserId, rolesDataProxy); 212 | var customerService = new CustomerService(customerDataProxy, rolesService); 213 | 214 | 215 | // EXECUTE CUSTOM COMMAND 216 | var customerId = 1; 217 | customerService.getNationalSecurityCommand(customerId).execute(function(err, result) { 218 | if (err) console.log("ERROR!", err); 219 | console.log("getNationalSecurityCommand execution complete!", result) 220 | }); 221 | 222 | 223 | // CREATE AN ARRAY OF INSERT COMMANDS 224 | var commands = [ 225 | customerService.insertCommand({name: "Jimi", age: new Date('2/3/1975')}), 226 | customerService.insertCommand({name: "James", age: new Date('2/3/1975'), address: 'aa'}), 227 | customerService.insertCommand({name: "Jimi", age: new Date('2/3/1925'), address: 'aa'}), 228 | customerService.insertCommand({name: "James", age: new Date('2/3/1925')}), 229 | customerService.insertCommand({name: "James", age: new Date('2/3/1925'), address: 'aaa'}) 230 | ]; 231 | 232 | // LOOP THROUGH EACH COMMAND AND EXECUTE IT 233 | commands.forEach(function(command, index) { 234 | command.execute((err, result) => { 235 | console.log('---------------'); 236 | console.log(result); 237 | 238 | if (index === commands.length - 1) { 239 | console.log('---------------'); 240 | customerService.getAllCommand().execute(function(err, result) { 241 | console.log("End Result", result.value); 242 | }); 243 | } 244 | }); 245 | }); 246 | 247 | module.exports = customerService; 248 | -------------------------------------------------------------------------------- /src/sampleWithPromises.js: -------------------------------------------------------------------------------- 1 | // This sample is meant to illustrate how to create business services, 2 | // custom commands, and rules, and to showcase how they interact with 3 | // each other. In a real world application, you would most likely keep 4 | // each business service, command, and rule in its own file, or at least 5 | // similar actors in the same files. 6 | 7 | "use strict"; 8 | 9 | var peasy = require('./../dist/peasy'); 10 | var Rule = peasy.Rule; 11 | var BusinessService = peasy.BusinessService; 12 | var Command = peasy.Command; 13 | 14 | 15 | // CREATE RULES 16 | // see https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules for more details 17 | 18 | var AgeRule = Rule.extend({ 19 | association: "age", 20 | params: ['birthdate'], 21 | functions: { 22 | _onValidate: function(birthdate) { 23 | if (new Date().getFullYear() - birthdate.getFullYear() < 50) { 24 | this._invalidate("You are too young"); 25 | } 26 | return new Promise((resolve, reject) => { 27 | var time = Math.floor((Math.random() * 3000) + 1); 28 | setTimeout(() => resolve(), time); // simulate latency 29 | }); 30 | } 31 | } 32 | }); 33 | 34 | var NameRule = Rule.extend({ 35 | association: "name", 36 | params: ['name'], 37 | functions: { 38 | _onValidate: function(name) { 39 | if (name === "Jimi") { 40 | this._invalidate("Name cannot be Jimi"); 41 | } 42 | return new Promise((resolve, reject) => { 43 | var time = Math.floor((Math.random() * 3000) + 1); 44 | setTimeout(() => resolve(), time); // simulate latency 45 | }); 46 | } 47 | } 48 | }); 49 | 50 | var FieldRequiredRule = Rule.extend({ 51 | params: ['field', 'data'], 52 | functions: { 53 | _onValidate: function(field, data) { 54 | if (!data[field]) { 55 | this.association = field; 56 | this._invalidate(field + " is required"); 57 | } 58 | return new Promise((resolve, reject) => { 59 | var time = Math.floor((Math.random() * 3000) + 1); 60 | setTimeout(() => resolve(), time); // simulate latency 61 | }); 62 | } 63 | } 64 | }); 65 | 66 | var CustomerAuthorizationRule = Rule.extend({ 67 | params: ['roles'], 68 | functions: { 69 | _onValidate: function(roles) { 70 | var validRoles = ['super admin', 'admin']; 71 | if (!roles.some(r => validRoles.indexOf(r) > -1)) { 72 | this._invalidate("You do not have sufficient priviledges to access national security information"); 73 | } 74 | return Promise.resolve(); 75 | } 76 | } 77 | }); 78 | 79 | 80 | // CREATE SERVICES, CUSTOM COMMAND, AND WIRE UP VALIDATION AND BUSINESS RULES 81 | // see https://github.com/peasy/peasy-js/wiki/BusinessService and 82 | // https://github.com/peasy/peasy-js/wiki/Command for more details 83 | 84 | // ROLES SERVICE 85 | var RolesService = BusinessService.extend({ 86 | params: ['userId', 'dataProxy'], 87 | functions: { 88 | _getAll: function(context) { 89 | return this.dataProxy.getById(this.userId); 90 | } 91 | } 92 | }).service; 93 | 94 | 95 | // CUSTOMER SERVICE 96 | var CustomerService = BusinessService 97 | .extend({ 98 | params: ['dataProxy', 'rolesService'], 99 | functions: { 100 | _getRulesForInsertCommand: getRulesForInsert, 101 | _getAll: function(context) { 102 | return this.dataProxy.getAll().then(data => { 103 | data.forEach(customer => { 104 | delete customer.nsd; // remove confidential data 105 | }); 106 | return Promise.resolve(data); 107 | }); 108 | }, 109 | _getById: function(id, context) { 110 | return this.dataProxy.getById(id).then(data => { 111 | delete data.nsd; // remove confidential data 112 | return Promise.resolve(data); 113 | }); 114 | } 115 | } 116 | }) 117 | .createCommand({ 118 | name: 'getNationalSecurityCommand', 119 | params: ['id'], 120 | functions: 121 | { 122 | _onInitialization: function(id, context) { 123 | context.foo = "hello"; 124 | return Promise.resolve(); 125 | }, 126 | _getRules: function(id, context) { 127 | var getRolesForCurrentUserCommand = this.rolesService.getAllCommand(); 128 | return getRolesForCurrentUserCommand.execute().then(result => { 129 | var roles = result.value; 130 | return new CustomerAuthorizationRule(roles); 131 | }); 132 | }, 133 | _onValidationSuccess: function(id, context) { 134 | return this.dataProxy.getById(id); 135 | } 136 | } 137 | }) 138 | .service; 139 | 140 | function getRulesForInsert(data, context) { 141 | 142 | // see https://github.com/peasy/peasy-js/wiki/Business-and-Validation-Rules for more details 143 | 144 | var customer = data; 145 | 146 | // these will all execute 147 | // return Promise.resolve([ 148 | //new AgeRule(customer.age), 149 | //new NameRule(customer.name), 150 | //new FieldRequiredRule("address", customer) 151 | //]); 152 | 153 | // chained rules - rules will only execute upon successful validation of predecessor 154 | return Promise.resolve(new AgeRule(customer.age) 155 | .ifValidThenExecute(() => console.log("Age succeeded")) 156 | .ifInvalidThenExecute(() => console.log("Age failed")) 157 | .ifValidThenValidate(new NameRule(customer.name) 158 | .ifValidThenExecute(() => console.log("Name succeeded")) 159 | .ifInvalidThenExecute(() => console.log("Name failed")) 160 | .ifValidThenValidate(new FieldRequiredRule("address", customer) 161 | .ifValidThenExecute(() => console.log("Address succeeded")) 162 | .ifInvalidThenExecute(() => console.log("Address failed")) 163 | ))); 164 | 165 | } 166 | 167 | 168 | // CREATE IN-MEMORY DATA PROXIES (these could be duck typed angular resources, react stores, mongo db implementations, http proxies, etc.) 169 | // See https://github.com/peasy/peasy-js/wiki/Data-Proxy for details 170 | 171 | var customerDataProxy = (function() { 172 | var state = [ 173 | { id: 1, name: "James Hendrix", nsd: "234322345" }, 174 | { id: 2, name: "James Page", nsd: "8492834926" }, 175 | { id: 3, name: "David Gilmour", nsd: "433423422" }, 176 | ]; 177 | 178 | return { 179 | insert: insert, 180 | getAll: getAll, 181 | getById: getById 182 | }; 183 | 184 | function insert(data) { 185 | var nextId = state.length + 1; 186 | data.id = nextId; 187 | state.push(Object.assign({}, data)); 188 | return Promise.resolve(data); 189 | } 190 | 191 | function getAll() { 192 | var data = state.map(customer => { 193 | return Object.assign({}, customer); 194 | }); 195 | return Promise.resolve(data); 196 | } 197 | 198 | function getById(id) { 199 | var customer = state.filter(c => { 200 | return c.id === id; 201 | })[0]; 202 | return Promise.resolve(Object.assign({}, customer)); 203 | } 204 | 205 | })(); 206 | 207 | var rolesDataProxy = { 208 | getById: function(id) { 209 | // add/remove roles to manipulate execution of getNationalSecurityCommand 210 | return Promise.resolve(['', 'user']); 211 | } 212 | } 213 | 214 | 215 | // CREATE INSTANCE OF A CUSTOMER SERVICE WITH THE REQUIRED DATA PROXY 216 | var currentUserId = 12345; // this id would likely come from some authentication service 217 | var rolesService = new RolesService(currentUserId, rolesDataProxy); 218 | var customerService = new CustomerService(customerDataProxy, rolesService); 219 | 220 | 221 | // EXECUTE CUSTOM COMMAND 222 | var customerId = 1; 223 | customerService.getNationalSecurityCommand(customerId).execute().then(result => { 224 | console.log("getNationalSecurityCommand execution complete!", result) 225 | }); 226 | 227 | // CREATE AN ARRAY OF INSERT COMMANDS 228 | var commands = [ 229 | customerService.insertCommand({name: "Jimi", age: new Date('2/3/1975')}), 230 | customerService.insertCommand({name: "James", age: new Date('2/3/1975'), address: 'aa'}), 231 | customerService.insertCommand({name: "Jimi", age: new Date('2/3/1925'), address: 'aa'}), 232 | customerService.insertCommand({name: "James", age: new Date('2/3/1925')}), 233 | customerService.insertCommand({name: "James", age: new Date('2/3/1925'), address: 'aaa'}) 234 | ]; 235 | 236 | // LOOP THROUGH EACH COMMAND AND EXECUTE IT 237 | Promise.all(commands.map(command => { 238 | return command.execute().then((result) => { 239 | console.log('---------------'); 240 | console.log(result); 241 | }) 242 | })).then(() => { 243 | customerService.getAllCommand().execute().then(result => { 244 | console.log('---------------'); 245 | console.log("End Result", result.value); 246 | }); 247 | }); 248 | 249 | module.exports = customerService; 250 | -------------------------------------------------------------------------------- /src/serviceException.js: -------------------------------------------------------------------------------- 1 | var ServiceException = function(message) { 2 | this.message = message; 3 | this.errors = []; 4 | }; 5 | 6 | ServiceException.prototype = new Error(); 7 | 8 | module.exports = ServiceException; 9 | -------------------------------------------------------------------------------- /src/utility.js: -------------------------------------------------------------------------------- 1 | var Configuration = require('./configuration'); 2 | 3 | function wrap(result) { 4 | if (Configuration.autoPromiseWrap && 5 | (result === undefined || typeof result.then != 'function')) { 6 | return Promise.resolve(result); 7 | } 8 | return result; 9 | } 10 | 11 | var utility = { 12 | 13 | autoWrapInitializationResult: (result) => { 14 | return wrap(result); 15 | }, 16 | 17 | autoWrapRulesResult: (result) => { 18 | if (Configuration.autoPromiseWrap) { 19 | if (Array.isArray(result)) { 20 | result = Promise.resolve(result); 21 | } 22 | if (result === undefined) { 23 | result = Promise.resolve([]); 24 | } 25 | if (typeof result.then != 'function') { 26 | result = Promise.resolve(result); 27 | } 28 | } 29 | return result; 30 | }, 31 | 32 | autoWrapValidationCompleteResult: (result) => { 33 | return wrap(result); 34 | }, 35 | 36 | autoWrapValidationResult: (result) => { 37 | return wrap(result); 38 | }, 39 | 40 | autoWrapRuleValidationCompleteResult: (result) => { 41 | return wrap(result); 42 | }, 43 | 44 | }; 45 | 46 | module.exports = utility; 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./built", 4 | "allowJs": true, 5 | "target": "es5" 6 | }, 7 | "include": [ 8 | "./src/**/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = { 5 | // entry is the "main" source file we want to include/import 6 | entry: "./src/index.js", 7 | // output tells webpack where to put the bundle it creates 8 | output: { 9 | // in the case of a "plain global browser library", this 10 | // will be used as the reference to our module that is 11 | // hung off of the window object. 12 | library: "peasy", 13 | // We want webpack to build a UMD wrapper for our module 14 | libraryTarget: "umd", 15 | // the destination file name 16 | filename: "peasy.js", 17 | globalObject: "this", 18 | path: path.resolve(__dirname, 'dist') // Output directory 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['@babel/preset-env'] 29 | } 30 | } 31 | } 32 | ] 33 | }, 34 | optimization: { 35 | minimize: true, 36 | minimizer: [new TerserPlugin()] 37 | } 38 | }; 39 | --------------------------------------------------------------------------------