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 | 
2 |
3 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------