@vidyano-labs/virtual-service 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -210
- package/index.d.ts +376 -150
- package/index.js +1391 -1597
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,11 +56,10 @@ const service = new VirtualService();
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**Key methods:**
|
|
59
|
-
- `registerPersistentObject(config)` - Register a mock persistent object type
|
|
59
|
+
- `registerPersistentObject(config, actionsClass?)` - Register a mock persistent object type with optional lifecycle class
|
|
60
60
|
- `registerQuery(config)` - Register a mock query
|
|
61
|
-
- `
|
|
61
|
+
- `registerCustomAction(name, handler)` or `registerCustomAction(config, handler)` - Register a custom action
|
|
62
62
|
- `registerBusinessRule(name, validator)` - Add custom validation rules
|
|
63
|
-
- `registerPersistentObjectActions(type, ActionsClass)` - Register lifecycle handlers
|
|
64
63
|
- `initialize()` - Finalize registrations (must call before using service)
|
|
65
64
|
|
|
66
65
|
> **Important:** All registrations must happen BEFORE calling `initialize()`. Attempting to register after initialization throws an error.
|
|
@@ -70,9 +69,8 @@ const service = new VirtualService();
|
|
|
70
69
|
Dependencies must be registered before the things that reference them:
|
|
71
70
|
|
|
72
71
|
1. **Actions** first - Custom actions must be registered before PersistentObjects/Queries that reference them
|
|
73
|
-
2. **PersistentObjects** - Define the data schema
|
|
72
|
+
2. **PersistentObjects** - Define the data schema (with optional lifecycle class)
|
|
74
73
|
3. **Queries** - Must reference an already-registered PersistentObject type
|
|
75
|
-
4. **PersistentObjectActions** - Lifecycle handlers (can be registered anytime before initialize)
|
|
76
74
|
|
|
77
75
|
```typescript
|
|
78
76
|
// Correct order
|
|
@@ -308,7 +306,7 @@ service.registerPersistentObject({
|
|
|
308
306
|
|
|
309
307
|
### Validation Flow
|
|
310
308
|
|
|
311
|
-
Validation runs automatically when
|
|
309
|
+
Validation runs automatically when saving - `onSave` calls `checkRules()` before delegating to `saveNew`/`saveExisting`:
|
|
312
310
|
|
|
313
311
|
```typescript
|
|
314
312
|
const contact = await service.getPersistentObject(null, "Contact", null, true);
|
|
@@ -322,24 +320,77 @@ await contact.save();
|
|
|
322
320
|
// Check validation error
|
|
323
321
|
console.log(contact.getAttribute("Email").validationError);
|
|
324
322
|
// "Email format is invalid"
|
|
323
|
+
|
|
324
|
+
// Notification is also set on the object
|
|
325
|
+
console.log(contact.notification);
|
|
326
|
+
// "Some required information is missing or incorrect."
|
|
325
327
|
```
|
|
326
328
|
|
|
327
329
|
**Validation behavior:**
|
|
328
|
-
- Runs
|
|
330
|
+
- Runs inside `onSave` via the `checkRules()` method (before `saveNew`/`saveExisting`)
|
|
329
331
|
- First failing rule stops validation for that attribute
|
|
330
332
|
- Validation errors set on the attribute's `validationError` property
|
|
331
|
-
- If any attribute fails,
|
|
333
|
+
- If any attribute fails, a notification is set and save handlers are not called
|
|
332
334
|
- Null and undefined values skip validation (unless using `NotEmpty`/`Required`)
|
|
333
335
|
|
|
336
|
+
### Overriding Validation
|
|
337
|
+
|
|
338
|
+
Override `checkRules()` in your `VirtualPersistentObjectActions` class to customize validation:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
class PersonActions extends VirtualPersistentObjectActions {
|
|
342
|
+
// Custom validation - completely replace default behavior
|
|
343
|
+
checkRules(obj: VirtualPersistentObject): boolean {
|
|
344
|
+
const name = obj.getAttributeValue("Name");
|
|
345
|
+
if (name === "reserved") {
|
|
346
|
+
obj.setValidationError("Name", "This name is reserved");
|
|
347
|
+
obj.setNotification("Validation failed", "Error");
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return true; // Skip default validation
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Or combine custom + default validation
|
|
355
|
+
class OrderActions extends VirtualPersistentObjectActions {
|
|
356
|
+
checkRules(obj: VirtualPersistentObject): boolean {
|
|
357
|
+
// Custom business logic first
|
|
358
|
+
const total = obj.getAttributeValue("Total");
|
|
359
|
+
const discount = obj.getAttributeValue("Discount");
|
|
360
|
+
if (discount > total) {
|
|
361
|
+
obj.setValidationError("Discount", "Discount cannot exceed total");
|
|
362
|
+
obj.setNotification("Validation failed", "Error");
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Then run default rule-based validation
|
|
367
|
+
return super.checkRules(obj);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Skip validation entirely
|
|
372
|
+
class DraftActions extends VirtualPersistentObjectActions {
|
|
373
|
+
checkRules(_obj: VirtualPersistentObject): boolean {
|
|
374
|
+
return true; // Always valid - skip all validation
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**checkRules behavior:**
|
|
380
|
+
- Returns `true` if all validations pass, `false` if any fail
|
|
381
|
+
- When returning `false`, `saveNew`/`saveExisting` are not called
|
|
382
|
+
- The object passed to `checkRules` is a `VirtualPersistentObject`
|
|
383
|
+
- Call `super.checkRules(obj)` to include default rule-based validation
|
|
384
|
+
|
|
334
385
|
### Custom Business Rules
|
|
335
386
|
|
|
336
387
|
Register your own validation rules for domain-specific requirements:
|
|
337
388
|
|
|
338
389
|
```typescript
|
|
339
|
-
import type {
|
|
390
|
+
import type { VirtualPersistentObjectAttribute } from "@vidyano-labs/virtual-service";
|
|
340
391
|
|
|
341
392
|
// Register a custom rule (before registerPersistentObject)
|
|
342
|
-
service.registerBusinessRule("IsPhoneNumber", (value: any,
|
|
393
|
+
service.registerBusinessRule("IsPhoneNumber", (value: any, attr: VirtualPersistentObjectAttribute) => {
|
|
343
394
|
if (value == null || value === "") return;
|
|
344
395
|
const phoneRegex = /^\+?[\d\s-()]+$/;
|
|
345
396
|
if (!phoneRegex.test(String(value)))
|
|
@@ -359,21 +410,23 @@ service.registerPersistentObject({
|
|
|
359
410
|
});
|
|
360
411
|
```
|
|
361
412
|
|
|
362
|
-
**
|
|
363
|
-
The `
|
|
364
|
-
- `
|
|
365
|
-
- `
|
|
413
|
+
**Attribute parameter:**
|
|
414
|
+
The `attr` parameter is the `VirtualPersistentObjectAttribute` being validated, which provides access to:
|
|
415
|
+
- `attr.service` - The VirtualService instance (use for `getMessage()` translations)
|
|
416
|
+
- `attr.persistentObject` - The persistent object being validated
|
|
417
|
+
- `attr.getValue()` / `attr.setValue()` - Get/set the attribute value
|
|
418
|
+
- All DTO properties (`name`, `type`, `rules`, etc.)
|
|
366
419
|
|
|
367
420
|
This allows cross-field validation:
|
|
368
421
|
|
|
369
422
|
```typescript
|
|
370
423
|
// Validate password confirmation matches password
|
|
371
|
-
service.registerBusinessRule("MatchesPassword", (value: any,
|
|
424
|
+
service.registerBusinessRule("MatchesPassword", (value: any, attr: VirtualPersistentObjectAttribute) => {
|
|
372
425
|
if (!value) return;
|
|
373
426
|
|
|
374
|
-
const passwordValue =
|
|
427
|
+
const passwordValue = attr.persistentObject.getAttributeValue("Password");
|
|
375
428
|
if (value !== passwordValue)
|
|
376
|
-
throw new Error("
|
|
429
|
+
throw new Error(attr.service.getMessage("MatchesPassword"));
|
|
377
430
|
});
|
|
378
431
|
|
|
379
432
|
service.registerPersistentObject({
|
|
@@ -391,36 +444,35 @@ service.registerPersistentObject({
|
|
|
391
444
|
|
|
392
445
|
**Custom rule requirements:**
|
|
393
446
|
- Must be registered before `registerPersistentObject()`
|
|
394
|
-
- Receives two parameters: `value` (the attribute value) and `
|
|
447
|
+
- Receives two parameters: `value` (the converted attribute value) and `attr` (the attribute being validated)
|
|
395
448
|
- Throw an `Error` with a message if validation fails
|
|
396
449
|
- Return nothing (or undefined) if validation passes
|
|
397
450
|
- Cannot override built-in rules
|
|
398
451
|
|
|
399
452
|
### Translating Validation Messages
|
|
400
453
|
|
|
401
|
-
|
|
454
|
+
Customize validation error messages by setting the static `VirtualService.messages` property:
|
|
402
455
|
|
|
403
456
|
```typescript
|
|
404
457
|
import { VirtualService } from "@vidyano-labs/virtual-service";
|
|
405
458
|
|
|
406
|
-
//
|
|
407
|
-
|
|
459
|
+
// Set custom messages (e.g., Dutch translations)
|
|
460
|
+
VirtualService.messages = {
|
|
408
461
|
"Required": "Dit veld is verplicht",
|
|
409
462
|
"NotEmpty": "Dit veld mag niet leeg zijn",
|
|
410
463
|
"IsEmail": "E-mailformaat is ongeldig",
|
|
411
464
|
"MaxLength": "Maximale lengte is {0} tekens",
|
|
412
465
|
"MinLength": "Minimale lengte is {0} tekens",
|
|
413
466
|
"MaxValue": "Maximale waarde is {0}",
|
|
414
|
-
"MinValue": "Minimale waarde is {0}"
|
|
467
|
+
"MinValue": "Minimale waarde is {0}",
|
|
468
|
+
"IsBase64": "Waarde moet een geldige base64 string zijn",
|
|
469
|
+
"IsRegex": "Waarde moet een geldige reguliere expressie zijn",
|
|
470
|
+
"IsWord": "Waarde mag alleen woordtekens bevatten",
|
|
471
|
+
"IsUrl": "Waarde moet een geldige URL zijn",
|
|
472
|
+
"ValidationRulesFailed": "Sommige vereiste informatie ontbreekt of is onjuist."
|
|
415
473
|
};
|
|
416
474
|
|
|
417
|
-
// Create service and register translation function
|
|
418
475
|
const service = new VirtualService();
|
|
419
|
-
service.registerMessageTranslator((key: string, ...params: any[]) => {
|
|
420
|
-
const template = translations[key] || key;
|
|
421
|
-
// Use String.format from @vidyano/core for {0}, {1} placeholders
|
|
422
|
-
return String.format(template, ...params);
|
|
423
|
-
});
|
|
424
476
|
|
|
425
477
|
service.registerPersistentObject({
|
|
426
478
|
type: "Person",
|
|
@@ -438,61 +490,53 @@ await service.initialize();
|
|
|
438
490
|
const person = await service.getPersistentObject(null, "Person", null, true);
|
|
439
491
|
await person.save();
|
|
440
492
|
|
|
441
|
-
// Error message
|
|
493
|
+
// Error message uses custom translation
|
|
442
494
|
console.log(person.getAttribute("Email").validationError);
|
|
443
495
|
// "Dit veld is verplicht"
|
|
444
496
|
```
|
|
445
497
|
|
|
446
|
-
**Translation function signature:**
|
|
447
|
-
```typescript
|
|
448
|
-
type TranslateFunction = (key: string, ...params: any[]) => string;
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
**Parameters:**
|
|
452
|
-
- `key` - The validation rule name (e.g., "Required", "MaxLength")
|
|
453
|
-
- `params` - Positional parameters for the message (e.g., max length value)
|
|
454
|
-
|
|
455
498
|
**How it works:**
|
|
456
|
-
1.
|
|
457
|
-
2.
|
|
458
|
-
3.
|
|
459
|
-
4. If
|
|
499
|
+
1. Set `VirtualService.messages` with your custom messages before creating services
|
|
500
|
+
2. Messages use `{0}`, `{1}` placeholders for positional parameters
|
|
501
|
+
3. The `getMessage(key, ...params)` method formats messages with the provided parameters
|
|
502
|
+
4. If a key is not found in the messages dictionary, the key itself is returned
|
|
460
503
|
|
|
461
|
-
**Custom rules can also use
|
|
504
|
+
**Custom rules can also use getMessage:**
|
|
462
505
|
|
|
463
506
|
```typescript
|
|
507
|
+
import type { VirtualPersistentObjectAttribute } from "@vidyano-labs/virtual-service";
|
|
508
|
+
|
|
509
|
+
// Set custom messages including your custom rule keys
|
|
510
|
+
VirtualService.messages = {
|
|
511
|
+
...VirtualService.messages, // Keep default messages
|
|
512
|
+
"MinimumAge": "L'âge minimum est {0}",
|
|
513
|
+
"MatchesPassword": "Les mots de passe ne correspondent pas"
|
|
514
|
+
};
|
|
515
|
+
|
|
464
516
|
const service = new VirtualService();
|
|
465
|
-
service.registerMessageTranslator((key: string, ...params: any[]) => {
|
|
466
|
-
const translations: Record<string, string> = {
|
|
467
|
-
"MinimumAge": "L'âge minimum est {0}",
|
|
468
|
-
"MatchesPassword": "Les mots de passe ne correspondent pas"
|
|
469
|
-
};
|
|
470
|
-
const template = translations[key] || key;
|
|
471
|
-
return String.format(template, ...params);
|
|
472
|
-
});
|
|
473
517
|
|
|
474
|
-
// Custom rule using
|
|
475
|
-
service.registerBusinessRule("MinimumAge", (value: any,
|
|
518
|
+
// Custom rule using getMessage via attr.service
|
|
519
|
+
service.registerBusinessRule("MinimumAge", (value: any, attr: VirtualPersistentObjectAttribute, minAge: number) => {
|
|
476
520
|
if (!value)
|
|
477
521
|
return;
|
|
478
522
|
|
|
479
523
|
const age = Number(value);
|
|
480
524
|
if (age < minAge)
|
|
481
|
-
throw new Error(
|
|
525
|
+
throw new Error(attr.service.getMessage("MinimumAge", minAge));
|
|
482
526
|
});
|
|
483
527
|
|
|
484
|
-
// Custom rule can still throw direct
|
|
485
|
-
service.registerBusinessRule("IsPhoneNumber", (value: any,
|
|
528
|
+
// Custom rule can still throw direct error strings
|
|
529
|
+
service.registerBusinessRule("IsPhoneNumber", (value: any, _attr: VirtualPersistentObjectAttribute) => {
|
|
486
530
|
if (!value)
|
|
487
531
|
return;
|
|
488
532
|
|
|
489
533
|
const phoneRegex = /^\+?[\d\s-()]+$/;
|
|
490
534
|
if (!phoneRegex.test(String(value)))
|
|
491
|
-
throw new Error("Invalid phone number format"); //
|
|
535
|
+
throw new Error("Invalid phone number format"); // Direct string, not translated
|
|
492
536
|
});
|
|
493
537
|
```
|
|
494
538
|
|
|
495
|
-
**Built-in
|
|
539
|
+
**Built-in message keys:**
|
|
496
540
|
- `Required` - Field is required (no params)
|
|
497
541
|
- `NotEmpty` - Field cannot be empty (no params)
|
|
498
542
|
- `IsEmail` - Invalid email format (no params)
|
|
@@ -504,6 +548,7 @@ service.registerBusinessRule("IsPhoneNumber", (value: any, _context: RuleValidat
|
|
|
504
548
|
- `IsBase64` - Invalid base64 string (no params)
|
|
505
549
|
- `IsRegex` - Invalid regex pattern (no params)
|
|
506
550
|
- `IsWord` - Invalid word characters (no params)
|
|
551
|
+
- `ValidationRulesFailed` - Notification message when validation fails (no params)
|
|
507
552
|
|
|
508
553
|
## Queries
|
|
509
554
|
|
|
@@ -647,109 +692,108 @@ const page2 = await query.items.sliceAsync(10, 20);
|
|
|
647
692
|
|
|
648
693
|
### Custom Actions
|
|
649
694
|
|
|
650
|
-
Register actions with custom handlers:
|
|
695
|
+
Register actions with custom handlers. You can use either a simple string name or a full config object:
|
|
651
696
|
|
|
652
697
|
```typescript
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
698
|
+
// Simple: just the action name
|
|
699
|
+
service.registerCustomAction("Approve", async (args) => {
|
|
700
|
+
args.parent.setAttributeValue("Status", "Approved");
|
|
701
|
+
return args.parent;
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Full config: with displayName, isPinned, etc.
|
|
705
|
+
service.registerCustomAction(
|
|
706
|
+
{
|
|
707
|
+
name: "Approve",
|
|
708
|
+
displayName: "Approve Order",
|
|
709
|
+
isPinned: true
|
|
710
|
+
},
|
|
711
|
+
async (args) => {
|
|
712
|
+
args.parent.setAttributeValue("Status", "Approved");
|
|
713
|
+
args.parent.setNotification("Order approved!", "OK", 3000);
|
|
663
714
|
return args.parent;
|
|
664
715
|
}
|
|
665
|
-
|
|
716
|
+
);
|
|
666
717
|
```
|
|
667
718
|
|
|
668
719
|
### ActionArgs
|
|
669
720
|
|
|
670
|
-
Action handlers receive `
|
|
721
|
+
Action handlers receive an `args` object with execution context:
|
|
671
722
|
|
|
672
723
|
```typescript
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
selectedItems?: QueryResultItemDto[]; // Selected items in query
|
|
679
|
-
parameters?: Record<string, any>; // Action parameters
|
|
680
|
-
context: ActionContext; // Helper methods
|
|
681
|
-
}
|
|
724
|
+
// args parameter contains:
|
|
725
|
+
// - parent: VirtualPersistentObject | null // The PO being acted on
|
|
726
|
+
// - query?: VirtualQuery // The query (for query actions)
|
|
727
|
+
// - selectedItems?: VirtualQueryResultItem[] // Selected items in query
|
|
728
|
+
// - parameters?: Record<string, any> // Action parameters
|
|
682
729
|
```
|
|
683
730
|
|
|
684
|
-
### ActionContext
|
|
685
731
|
|
|
686
|
-
|
|
732
|
+
### Working with Parent in Action Handlers
|
|
733
|
+
|
|
734
|
+
Since `args.parent` is a `VirtualPersistentObject`, you can use its methods directly:
|
|
687
735
|
|
|
688
736
|
```typescript
|
|
689
|
-
handler: async (args
|
|
737
|
+
handler: async (args) => {
|
|
690
738
|
// Get an attribute
|
|
691
|
-
const emailAttr = args.
|
|
739
|
+
const emailAttr = args.parent.getAttribute("Email");
|
|
692
740
|
|
|
693
|
-
// Read attribute values
|
|
694
|
-
const email = args.
|
|
741
|
+
// Read attribute values (type-converted)
|
|
742
|
+
const email = args.parent.getAttributeValue("Email");
|
|
695
743
|
|
|
696
|
-
//
|
|
697
|
-
const age =
|
|
744
|
+
// Read from the attribute directly
|
|
745
|
+
const age = emailAttr?.getValue();
|
|
698
746
|
|
|
699
747
|
// Modify attribute values
|
|
700
|
-
args.
|
|
748
|
+
args.parent.setAttributeValue("Status", "Active");
|
|
701
749
|
|
|
702
|
-
// Set
|
|
703
|
-
|
|
704
|
-
args.context.setConvertedValue(countAttr, 42);
|
|
750
|
+
// Set via attribute directly
|
|
751
|
+
emailAttr?.setValue("new@example.com");
|
|
705
752
|
|
|
706
753
|
// Set validation errors
|
|
707
754
|
if (!email?.includes("@"))
|
|
708
|
-
args.
|
|
755
|
+
args.parent.setValidationError("Email", "Invalid email format");
|
|
709
756
|
|
|
710
|
-
// Clear validation errors
|
|
711
|
-
args.
|
|
757
|
+
// Clear validation errors (pass null or empty string)
|
|
758
|
+
args.parent.setValidationError("Email", null);
|
|
712
759
|
|
|
713
760
|
// Show notifications
|
|
714
|
-
args.
|
|
715
|
-
args.
|
|
716
|
-
args.
|
|
761
|
+
args.parent.setNotification("Saved successfully", "OK", 3000);
|
|
762
|
+
args.parent.setNotification("Warning!", "Warning", 5000);
|
|
763
|
+
args.parent.setNotification("Error occurred", "Error");
|
|
717
764
|
|
|
718
765
|
return args.parent;
|
|
719
766
|
}
|
|
720
767
|
```
|
|
721
768
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
| Method | Description |
|
|
725
|
-
|--------|-------------|
|
|
726
|
-
| `getAttribute(name)` | Get the full attribute DTO |
|
|
727
|
-
| `getAttributeValue(name)` | Get the raw attribute value |
|
|
728
|
-
| `getConvertedValue(attr)` | Get type-converted value from attribute DTO |
|
|
729
|
-
| `setAttributeValue(name, value)` | Update raw attribute value |
|
|
730
|
-
| `setConvertedValue(attr, value)` | Update attribute DTO with type conversion |
|
|
731
|
-
| `setValidationError(name, error)` | Set a validation error message |
|
|
732
|
-
| `clearValidationError(name)` | Clear validation error |
|
|
733
|
-
| `setNotification(msg, type, duration?)` | Show notification to user |
|
|
769
|
+
See [VirtualPersistentObject Methods](#virtualpersistentobject-methods) for the full list.
|
|
734
770
|
|
|
735
771
|
### Query Actions
|
|
736
772
|
|
|
737
773
|
Actions can operate on query results:
|
|
738
774
|
|
|
739
775
|
```typescript
|
|
740
|
-
service.
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
return null; // Silent completion
|
|
776
|
+
service.registerCustomAction("BulkDelete", async (args) => {
|
|
777
|
+
// Access selected items
|
|
778
|
+
for (const item of args.selectedItems || []) {
|
|
779
|
+
console.log(`Deleting item: ${item.id}`);
|
|
780
|
+
// Use getValue to read column values
|
|
781
|
+
const name = item.getValue("Name");
|
|
782
|
+
console.log(` Name: ${name}`);
|
|
749
783
|
}
|
|
784
|
+
|
|
785
|
+
return null; // Silent completion
|
|
750
786
|
});
|
|
751
787
|
```
|
|
752
788
|
|
|
789
|
+
**VirtualQueryResultItem methods:**
|
|
790
|
+
|
|
791
|
+
| Method | Description |
|
|
792
|
+
|--------|-------------|
|
|
793
|
+
| `getValue(columnName)` | Get a value from the item by column name |
|
|
794
|
+
| `query` | Reference to the parent VirtualQuery |
|
|
795
|
+
| `service` | Reference to the VirtualService instance |
|
|
796
|
+
|
|
753
797
|
## Lifecycle Hooks
|
|
754
798
|
|
|
755
799
|
### VirtualPersistentObjectActions
|
|
@@ -757,43 +801,45 @@ service.registerAction({
|
|
|
757
801
|
For complex scenarios, create a class extending `VirtualPersistentObjectActions`:
|
|
758
802
|
|
|
759
803
|
```typescript
|
|
760
|
-
import { VirtualPersistentObjectActions } from "@vidyano-labs/virtual-service";
|
|
804
|
+
import { VirtualPersistentObjectActions, VirtualQuery } from "@vidyano-labs/virtual-service";
|
|
761
805
|
import type { VirtualPersistentObject, VirtualPersistentObjectAttribute } from "@vidyano-labs/virtual-service";
|
|
762
|
-
import { Dto } from "@vidyano/core";
|
|
763
806
|
|
|
764
807
|
class PersonActions extends VirtualPersistentObjectActions {
|
|
765
|
-
// Called
|
|
808
|
+
// Called after the object is built
|
|
766
809
|
onConstruct(obj: VirtualPersistentObject): void {
|
|
767
|
-
// Set defaults - note: this is synchronous
|
|
768
810
|
obj.setAttributeValue("CreatedDate", new Date().toISOString());
|
|
769
811
|
}
|
|
770
812
|
|
|
771
813
|
// Called when loading an existing Person
|
|
772
814
|
async onLoad(
|
|
773
|
-
|
|
815
|
+
objectId: string,
|
|
774
816
|
parent: VirtualPersistentObject | null
|
|
775
817
|
): Promise<VirtualPersistentObject> {
|
|
818
|
+
const obj = await super.onLoad(objectId, parent);
|
|
819
|
+
|
|
776
820
|
// Load additional data based on ID
|
|
777
|
-
|
|
778
|
-
|
|
821
|
+
console.log(`Loading person: ${objectId}`);
|
|
822
|
+
|
|
779
823
|
return obj;
|
|
780
824
|
}
|
|
781
825
|
|
|
782
826
|
// Called when creating a new Person via "New" action
|
|
783
827
|
async onNew(
|
|
784
|
-
obj: VirtualPersistentObject,
|
|
785
828
|
parent: VirtualPersistentObject | null,
|
|
786
|
-
query:
|
|
829
|
+
query: VirtualQuery | null,
|
|
787
830
|
parameters: Record<string, string> | null
|
|
788
831
|
): Promise<VirtualPersistentObject> {
|
|
832
|
+
const obj = await super.onNew(parent, query, parameters);
|
|
833
|
+
|
|
789
834
|
// Initialize new object
|
|
790
835
|
obj.setAttributeValue("Status", "Draft");
|
|
836
|
+
|
|
791
837
|
return obj;
|
|
792
838
|
}
|
|
793
839
|
|
|
794
840
|
// Called when saving
|
|
795
841
|
async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
796
|
-
// Calls saveNew or saveExisting based on obj.isNew
|
|
842
|
+
// Calls checkRules, then saveNew or saveExisting based on obj.isNew
|
|
797
843
|
return await super.onSave(obj);
|
|
798
844
|
}
|
|
799
845
|
|
|
@@ -810,18 +856,26 @@ class PersonActions extends VirtualPersistentObjectActions {
|
|
|
810
856
|
}
|
|
811
857
|
}
|
|
812
858
|
|
|
813
|
-
// Register the actions class
|
|
814
|
-
service.
|
|
859
|
+
// Register with the actions class
|
|
860
|
+
service.registerPersistentObject({
|
|
861
|
+
type: "Person",
|
|
862
|
+
attributes: [
|
|
863
|
+
{ name: "FirstName", type: "String" },
|
|
864
|
+
{ name: "CreatedDate", type: "DateTime" },
|
|
865
|
+
{ name: "ModifiedDate", type: "DateTime" },
|
|
866
|
+
{ name: "Status", type: "String" }
|
|
867
|
+
]
|
|
868
|
+
}, PersonActions);
|
|
815
869
|
```
|
|
816
870
|
|
|
817
871
|
### Lifecycle Flow
|
|
818
872
|
|
|
819
873
|
```
|
|
820
|
-
PersistentObject Load: onConstruct
|
|
821
|
-
PersistentObject New: onConstruct
|
|
822
|
-
PersistentObject Save:
|
|
823
|
-
Query
|
|
824
|
-
Query Execution: onExecuteQuery →
|
|
874
|
+
PersistentObject Load: onLoad (calls onConstruct internally)
|
|
875
|
+
PersistentObject New: onNew (calls onConstruct internally)
|
|
876
|
+
PersistentObject Save: onSave → checkRules → (saveNew | saveExisting)
|
|
877
|
+
Query Get: onGetQuery (calls onConstructQuery internally)
|
|
878
|
+
Query Execution: onExecuteQuery → getEntities
|
|
825
879
|
Attribute Refresh: onRefresh
|
|
826
880
|
Reference Selection: onSelectReference
|
|
827
881
|
Deletion: onDelete
|
|
@@ -860,6 +914,27 @@ service.registerPersistentObject({
|
|
|
860
914
|
});
|
|
861
915
|
```
|
|
862
916
|
|
|
917
|
+
### Query Retrieval
|
|
918
|
+
|
|
919
|
+
Customize how queries are retrieved:
|
|
920
|
+
|
|
921
|
+
```typescript
|
|
922
|
+
class PersonActions extends VirtualPersistentObjectActions {
|
|
923
|
+
// Called when a query is requested
|
|
924
|
+
async onGetQuery(
|
|
925
|
+
queryName: string,
|
|
926
|
+
parent: VirtualPersistentObject | null
|
|
927
|
+
): Promise<VirtualQuery> {
|
|
928
|
+
const query = await super.onGetQuery(queryName, parent);
|
|
929
|
+
|
|
930
|
+
// Custom post-processing
|
|
931
|
+
console.log(`Query ${queryName} loaded with ${query.totalItems} items`);
|
|
932
|
+
|
|
933
|
+
return query;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
863
938
|
### Query Execution
|
|
864
939
|
|
|
865
940
|
Provide dynamic query data:
|
|
@@ -871,7 +946,7 @@ class PersonActions extends VirtualPersistentObjectActions {
|
|
|
871
946
|
// Provide data for query execution
|
|
872
947
|
// Framework handles text search, sort, and pagination automatically
|
|
873
948
|
async getEntities(
|
|
874
|
-
query:
|
|
949
|
+
query: VirtualQuery,
|
|
875
950
|
parent: VirtualPersistentObject | null,
|
|
876
951
|
data: Record<string, any>[]
|
|
877
952
|
): Promise<Record<string, any>[]> {
|
|
@@ -881,7 +956,7 @@ class PersonActions extends VirtualPersistentObjectActions {
|
|
|
881
956
|
|
|
882
957
|
// Or fully control query execution
|
|
883
958
|
async onExecuteQuery(
|
|
884
|
-
query:
|
|
959
|
+
query: VirtualQuery,
|
|
885
960
|
parent: VirtualPersistentObject | null,
|
|
886
961
|
data: Record<string, any>[]
|
|
887
962
|
): Promise<VirtualQueryExecuteResult> {
|
|
@@ -900,16 +975,16 @@ Handle reference attribute selection:
|
|
|
900
975
|
class OrderActions extends VirtualPersistentObjectActions {
|
|
901
976
|
async onSelectReference(
|
|
902
977
|
parent: VirtualPersistentObject,
|
|
903
|
-
referenceAttribute:
|
|
904
|
-
query:
|
|
905
|
-
selectedItem:
|
|
978
|
+
referenceAttribute: VirtualPersistentObjectAttribute,
|
|
979
|
+
query: VirtualQuery,
|
|
980
|
+
selectedItem: VirtualQueryResultItem | null
|
|
906
981
|
): Promise<void> {
|
|
907
982
|
// Default: sets objectId and value from displayAttribute
|
|
908
983
|
await super.onSelectReference(parent, referenceAttribute, query, selectedItem);
|
|
909
984
|
|
|
910
985
|
// Custom: also copy related fields
|
|
911
986
|
if (selectedItem) {
|
|
912
|
-
const customerName = selectedItem.
|
|
987
|
+
const customerName = selectedItem.getValue("Name");
|
|
913
988
|
parent.setAttributeValue("CustomerName", customerName);
|
|
914
989
|
}
|
|
915
990
|
}
|
|
@@ -1013,23 +1088,47 @@ const linesQuery = order.queries.find(q => q.name === "OrderLines");
|
|
|
1013
1088
|
await linesQuery.search();
|
|
1014
1089
|
```
|
|
1015
1090
|
|
|
1016
|
-
|
|
1091
|
+
### Detail Query Pre-Execution
|
|
1092
|
+
|
|
1093
|
+
By default, detail queries are pre-executed when the parent PersistentObject is loaded. This means the client immediately has access to the query results without needing to call `search()`.
|
|
1094
|
+
|
|
1095
|
+
You can control this behavior in `onConstruct` by setting `isIncludedInParentObject`:
|
|
1096
|
+
|
|
1097
|
+
```typescript
|
|
1098
|
+
class OrderActions extends VirtualPersistentObjectActions {
|
|
1099
|
+
onConstruct(obj: VirtualPersistentObject): void {
|
|
1100
|
+
// Exclude OrderLines from being pre-executed (lazy load instead)
|
|
1101
|
+
const orderLinesQuery = obj.queries!.find(q => q.name === "OrderLines")!;
|
|
1102
|
+
orderLinesQuery.isIncludedInParentObject = false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Behavior:**
|
|
1108
|
+
- `isIncludedInParentObject = true` (default) - Query is executed before returning the PersistentObject; results are immediately available
|
|
1109
|
+
- `isIncludedInParentObject = false` - Query is not pre-executed; client must call `search()` to load results
|
|
1017
1110
|
|
|
1018
|
-
|
|
1111
|
+
This is useful for optimizing performance when detail queries contain large datasets that aren't always needed immediately.
|
|
1112
|
+
|
|
1113
|
+
## VirtualPersistentObject Methods
|
|
1114
|
+
|
|
1115
|
+
The `VirtualPersistentObject` type provides these methods:
|
|
1019
1116
|
|
|
1020
1117
|
```typescript
|
|
1021
|
-
// In lifecycle hooks, objects are wrapped with helpers
|
|
1022
1118
|
async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
1023
|
-
// Get attribute by name
|
|
1119
|
+
// Get attribute by name
|
|
1024
1120
|
const attr = obj.getAttribute("Email");
|
|
1025
1121
|
|
|
1026
1122
|
// Get/set values
|
|
1027
1123
|
const email = obj.getAttributeValue("Email");
|
|
1028
1124
|
obj.setAttributeValue("Email", "new@example.com");
|
|
1029
1125
|
|
|
1030
|
-
// Validation errors
|
|
1126
|
+
// Validation errors (pass null/empty to clear)
|
|
1031
1127
|
obj.setValidationError("Email", "Invalid format");
|
|
1032
|
-
obj.
|
|
1128
|
+
obj.setValidationError("Email", null); // Clear error
|
|
1129
|
+
|
|
1130
|
+
// Access the service
|
|
1131
|
+
const message = obj.service.getMessage("CustomKey");
|
|
1033
1132
|
|
|
1034
1133
|
// Notifications
|
|
1035
1134
|
obj.setNotification("Saved!", "OK", 3000);
|
|
@@ -1042,12 +1141,12 @@ async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
|
1042
1141
|
|
|
1043
1142
|
| Method | Description |
|
|
1044
1143
|
|--------|-------------|
|
|
1045
|
-
| `getAttribute(name)` | Get attribute
|
|
1144
|
+
| `getAttribute(name)` | Get attribute by name |
|
|
1046
1145
|
| `getAttributeValue(name)` | Get converted attribute value |
|
|
1047
1146
|
| `setAttributeValue(name, value)` | Set attribute value with conversion |
|
|
1048
|
-
| `setValidationError(name, error)` | Set validation error |
|
|
1049
|
-
| `clearValidationError(name)` | Clear validation error |
|
|
1147
|
+
| `setValidationError(name, error)` | Set validation error (pass `null`/empty to clear) |
|
|
1050
1148
|
| `setNotification(msg, type, duration?)` | Set notification |
|
|
1149
|
+
| `service` | Reference to the VirtualService instance |
|
|
1051
1150
|
|
|
1052
1151
|
**VirtualPersistentObjectAttribute methods:**
|
|
1053
1152
|
|
|
@@ -1055,8 +1154,10 @@ async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
|
1055
1154
|
|--------|-------------|
|
|
1056
1155
|
| `getValue()` | Get converted value |
|
|
1057
1156
|
| `setValue(value)` | Set value with conversion |
|
|
1058
|
-
| `setValidationError(error)` | Set validation error |
|
|
1059
|
-
| `
|
|
1157
|
+
| `setValidationError(error)` | Set validation error (pass `null`/empty to clear) |
|
|
1158
|
+
| `persistentObject` | Reference to the parent VirtualPersistentObject |
|
|
1159
|
+
| `service` | Reference to the VirtualService instance |
|
|
1160
|
+
|
|
1060
1161
|
|
|
1061
1162
|
## Testing Examples
|
|
1062
1163
|
|
|
@@ -1092,30 +1193,23 @@ test("validates email format", async () => {
|
|
|
1092
1193
|
```typescript
|
|
1093
1194
|
import { test, expect } from "@playwright/test";
|
|
1094
1195
|
import { VirtualService } from "@vidyano-labs/virtual-service";
|
|
1095
|
-
import type { ActionArgs } from "@vidyano-labs/virtual-service";
|
|
1096
1196
|
|
|
1097
1197
|
test("complete order workflow", async () => {
|
|
1098
1198
|
const service = new VirtualService();
|
|
1099
1199
|
|
|
1100
|
-
service.
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
args.context.setAttributeValue("Status", "Submitted");
|
|
1104
|
-
return args.parent;
|
|
1105
|
-
}
|
|
1200
|
+
service.registerCustomAction("Submit", async (args) => {
|
|
1201
|
+
args.parent.setAttributeValue("Status", "Submitted");
|
|
1202
|
+
return args.parent;
|
|
1106
1203
|
});
|
|
1107
1204
|
|
|
1108
|
-
service.
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if (status !== "Submitted") {
|
|
1113
|
-
args.context.setNotification("Order must be submitted first", "Error");
|
|
1114
|
-
return args.parent;
|
|
1115
|
-
}
|
|
1116
|
-
args.context.setAttributeValue("Status", "Approved");
|
|
1205
|
+
service.registerCustomAction("Approve", async (args) => {
|
|
1206
|
+
const status = args.parent.getAttributeValue("Status");
|
|
1207
|
+
if (status !== "Submitted") {
|
|
1208
|
+
args.parent.setNotification("Order must be submitted first", "Error");
|
|
1117
1209
|
return args.parent;
|
|
1118
1210
|
}
|
|
1211
|
+
args.parent.setAttributeValue("Status", "Approved");
|
|
1212
|
+
return args.parent;
|
|
1119
1213
|
});
|
|
1120
1214
|
|
|
1121
1215
|
service.registerPersistentObject({
|
|
@@ -1195,57 +1289,36 @@ test("search and sort query results", async () => {
|
|
|
1195
1289
|
| Method | Description |
|
|
1196
1290
|
|--------|-------------|
|
|
1197
1291
|
| `constructor(hooks?)` | Create service with optional custom hooks |
|
|
1198
|
-
| `registerPersistentObject(config)` | Register a PersistentObject type |
|
|
1292
|
+
| `registerPersistentObject(config, actionsClass?)` | Register a PersistentObject type with optional lifecycle class |
|
|
1199
1293
|
| `registerQuery(config)` | Register a Query |
|
|
1200
|
-
| `
|
|
1294
|
+
| `registerCustomAction(name, handler)` | Register a custom action (simple) |
|
|
1295
|
+
| `registerCustomAction(config, handler)` | Register a custom action (with config) |
|
|
1201
1296
|
| `registerBusinessRule(name, validator)` | Register a validation rule |
|
|
1202
|
-
| `
|
|
1203
|
-
| `registerMessageTranslator(translateFn)` | Register message translator for system messages |
|
|
1297
|
+
| `getMessage(key, ...params)` | Get a formatted message by key |
|
|
1204
1298
|
| `initialize()` | Finalize registrations |
|
|
1205
1299
|
|
|
1300
|
+
| Static Property | Description |
|
|
1301
|
+
|-----------------|-------------|
|
|
1302
|
+
| `VirtualService.messages` | Get/set the global messages dictionary for translations |
|
|
1303
|
+
|
|
1206
1304
|
### VirtualPersistentObjectActions
|
|
1207
1305
|
|
|
1208
1306
|
| Method | Description |
|
|
1209
1307
|
|--------|-------------|
|
|
1210
|
-
| `onConstruct(obj)` | Called
|
|
1211
|
-
| `onLoad(
|
|
1212
|
-
| `onNew(
|
|
1213
|
-
| `
|
|
1308
|
+
| `onConstruct(obj: VirtualPersistentObject)` | Called after the object is built (synchronous) |
|
|
1309
|
+
| `onLoad(objectId: string, parent)` | Called when loading an existing object - call `super.onLoad()` to build |
|
|
1310
|
+
| `onNew(parent, query: VirtualQuery, params)` | Called when creating a new object - call `super.onNew()` to build |
|
|
1311
|
+
| `onGetQuery(queryName: string, parent)` | Called when retrieving a query - call `super.onGetQuery()` to build |
|
|
1312
|
+
| `onSave(obj)` | Called when saving (calls checkRules, then saveNew/saveExisting) |
|
|
1313
|
+
| `checkRules(obj)` | Validates attributes against rules (overridable) |
|
|
1214
1314
|
| `saveNew(obj)` | Called for new objects (protected) |
|
|
1215
1315
|
| `saveExisting(obj)` | Called for existing objects (protected) |
|
|
1216
|
-
| `onRefresh(obj, attribute)` | Called when refreshing |
|
|
1217
|
-
| `onDelete(parent, query, items)` | Called when deleting items |
|
|
1218
|
-
| `onConstructQuery(query, parent)` | Called
|
|
1219
|
-
| `onExecuteQuery(query, parent, data)` | Called when executing a query |
|
|
1220
|
-
| `getEntities(query, parent, data)` | Provide query data |
|
|
1221
|
-
| `onSelectReference(parent, attr, query, item)` | Called when selecting a reference |
|
|
1222
|
-
|
|
1223
|
-
### Type Exports
|
|
1224
|
-
|
|
1225
|
-
```typescript
|
|
1226
|
-
import {
|
|
1227
|
-
VirtualService,
|
|
1228
|
-
VirtualServiceHooks,
|
|
1229
|
-
VirtualPersistentObjectActions
|
|
1230
|
-
} from "@vidyano-labs/virtual-service";
|
|
1231
|
-
|
|
1232
|
-
import type {
|
|
1233
|
-
VirtualPersistentObject,
|
|
1234
|
-
VirtualPersistentObjectAttribute,
|
|
1235
|
-
VirtualPersistentObjectConfig,
|
|
1236
|
-
VirtualPersistentObjectAttributeConfig,
|
|
1237
|
-
VirtualQueryConfig,
|
|
1238
|
-
VirtualQueryExecuteResult,
|
|
1239
|
-
ActionConfig,
|
|
1240
|
-
ActionHandler,
|
|
1241
|
-
ActionArgs,
|
|
1242
|
-
ActionContext,
|
|
1243
|
-
RuleValidatorFn,
|
|
1244
|
-
RuleValidationContext,
|
|
1245
|
-
TranslateFunction,
|
|
1246
|
-
TranslateFunction
|
|
1247
|
-
} from "@vidyano-labs/virtual-service";
|
|
1248
|
-
```
|
|
1316
|
+
| `onRefresh(obj, attribute: VirtualPersistentObjectAttribute)` | Called when refreshing |
|
|
1317
|
+
| `onDelete(parent, query: VirtualQuery, items: VirtualQueryResultItem[])` | Called when deleting items |
|
|
1318
|
+
| `onConstructQuery(query: VirtualQuery, parent)` | Called after query is built (synchronous) |
|
|
1319
|
+
| `onExecuteQuery(query: VirtualQuery, parent, data)` | Called when executing a query |
|
|
1320
|
+
| `getEntities(query: VirtualQuery, parent, data)` | Provide query data |
|
|
1321
|
+
| `onSelectReference(parent, attr: VirtualPersistentObjectAttribute, query: VirtualQuery, item: VirtualQueryResultItem)` | Called when selecting a reference |
|
|
1249
1322
|
|
|
1250
1323
|
## Best Practices
|
|
1251
1324
|
|