@vidyano-labs/virtual-service 0.2.0 → 0.4.0
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 +352 -167
- package/index.d.ts +379 -127
- package/index.js +1427 -1611
- 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,11 +444,112 @@ 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
|
|
|
452
|
+
### Translating Validation Messages
|
|
453
|
+
|
|
454
|
+
Customize validation error messages by setting the static `VirtualService.messages` property:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { VirtualService } from "@vidyano-labs/virtual-service";
|
|
458
|
+
|
|
459
|
+
// Set custom messages (e.g., Dutch translations)
|
|
460
|
+
VirtualService.messages = {
|
|
461
|
+
"Required": "Dit veld is verplicht",
|
|
462
|
+
"NotEmpty": "Dit veld mag niet leeg zijn",
|
|
463
|
+
"IsEmail": "E-mailformaat is ongeldig",
|
|
464
|
+
"MaxLength": "Maximale lengte is {0} tekens",
|
|
465
|
+
"MinLength": "Minimale lengte is {0} tekens",
|
|
466
|
+
"MaxValue": "Maximale 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."
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const service = new VirtualService();
|
|
476
|
+
|
|
477
|
+
service.registerPersistentObject({
|
|
478
|
+
type: "Person",
|
|
479
|
+
attributes: [
|
|
480
|
+
{
|
|
481
|
+
name: "Email",
|
|
482
|
+
type: "String",
|
|
483
|
+
rules: "Required; MaxLength(100); IsEmail"
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
await service.initialize();
|
|
489
|
+
|
|
490
|
+
const person = await service.getPersistentObject(null, "Person", null, true);
|
|
491
|
+
await person.save();
|
|
492
|
+
|
|
493
|
+
// Error message uses custom translation
|
|
494
|
+
console.log(person.getAttribute("Email").validationError);
|
|
495
|
+
// "Dit veld is verplicht"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**How it works:**
|
|
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
|
|
503
|
+
|
|
504
|
+
**Custom rules can also use getMessage:**
|
|
505
|
+
|
|
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
|
+
|
|
516
|
+
const service = new VirtualService();
|
|
517
|
+
|
|
518
|
+
// Custom rule using getMessage via attr.service
|
|
519
|
+
service.registerBusinessRule("MinimumAge", (value: any, attr: VirtualPersistentObjectAttribute, minAge: number) => {
|
|
520
|
+
if (!value)
|
|
521
|
+
return;
|
|
522
|
+
|
|
523
|
+
const age = Number(value);
|
|
524
|
+
if (age < minAge)
|
|
525
|
+
throw new Error(attr.service.getMessage("MinimumAge", minAge));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Custom rule can still throw direct error strings
|
|
529
|
+
service.registerBusinessRule("IsPhoneNumber", (value: any, _attr: VirtualPersistentObjectAttribute) => {
|
|
530
|
+
if (!value)
|
|
531
|
+
return;
|
|
532
|
+
|
|
533
|
+
const phoneRegex = /^\+?[\d\s-()]+$/;
|
|
534
|
+
if (!phoneRegex.test(String(value)))
|
|
535
|
+
throw new Error("Invalid phone number format"); // Direct string, not translated
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Built-in message keys:**
|
|
540
|
+
- `Required` - Field is required (no params)
|
|
541
|
+
- `NotEmpty` - Field cannot be empty (no params)
|
|
542
|
+
- `IsEmail` - Invalid email format (no params)
|
|
543
|
+
- `IsUrl` - Invalid URL format (no params)
|
|
544
|
+
- `MaxLength` - Maximum length exceeded (param: max length)
|
|
545
|
+
- `MinLength` - Minimum length not met (param: min length)
|
|
546
|
+
- `MaxValue` - Maximum value exceeded (param: max value)
|
|
547
|
+
- `MinValue` - Minimum value not met (param: min value)
|
|
548
|
+
- `IsBase64` - Invalid base64 string (no params)
|
|
549
|
+
- `IsRegex` - Invalid regex pattern (no params)
|
|
550
|
+
- `IsWord` - Invalid word characters (no params)
|
|
551
|
+
- `ValidationRulesFailed` - Notification message when validation fails (no params)
|
|
552
|
+
|
|
399
553
|
## Queries
|
|
400
554
|
|
|
401
555
|
### Basic Query Registration
|
|
@@ -538,109 +692,108 @@ const page2 = await query.items.sliceAsync(10, 20);
|
|
|
538
692
|
|
|
539
693
|
### Custom Actions
|
|
540
694
|
|
|
541
|
-
Register actions with custom handlers:
|
|
695
|
+
Register actions with custom handlers. You can use either a simple string name or a full config object:
|
|
542
696
|
|
|
543
697
|
```typescript
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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);
|
|
554
714
|
return args.parent;
|
|
555
715
|
}
|
|
556
|
-
|
|
716
|
+
);
|
|
557
717
|
```
|
|
558
718
|
|
|
559
719
|
### ActionArgs
|
|
560
720
|
|
|
561
|
-
Action handlers receive `
|
|
721
|
+
Action handlers receive an `args` object with execution context:
|
|
562
722
|
|
|
563
723
|
```typescript
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
selectedItems?: QueryResultItemDto[]; // Selected items in query
|
|
570
|
-
parameters?: Record<string, any>; // Action parameters
|
|
571
|
-
context: ActionContext; // Helper methods
|
|
572
|
-
}
|
|
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
|
|
573
729
|
```
|
|
574
730
|
|
|
575
|
-
### ActionContext
|
|
576
731
|
|
|
577
|
-
|
|
732
|
+
### Working with Parent in Action Handlers
|
|
733
|
+
|
|
734
|
+
Since `args.parent` is a `VirtualPersistentObject`, you can use its methods directly:
|
|
578
735
|
|
|
579
736
|
```typescript
|
|
580
|
-
handler: async (args
|
|
737
|
+
handler: async (args) => {
|
|
581
738
|
// Get an attribute
|
|
582
|
-
const emailAttr = args.
|
|
739
|
+
const emailAttr = args.parent.getAttribute("Email");
|
|
583
740
|
|
|
584
|
-
// Read attribute values
|
|
585
|
-
const email = args.
|
|
741
|
+
// Read attribute values (type-converted)
|
|
742
|
+
const email = args.parent.getAttributeValue("Email");
|
|
586
743
|
|
|
587
|
-
//
|
|
588
|
-
const age =
|
|
744
|
+
// Read from the attribute directly
|
|
745
|
+
const age = emailAttr?.getValue();
|
|
589
746
|
|
|
590
747
|
// Modify attribute values
|
|
591
|
-
args.
|
|
748
|
+
args.parent.setAttributeValue("Status", "Active");
|
|
592
749
|
|
|
593
|
-
// Set
|
|
594
|
-
|
|
595
|
-
args.context.setConvertedValue(countAttr, 42);
|
|
750
|
+
// Set via attribute directly
|
|
751
|
+
emailAttr?.setValue("new@example.com");
|
|
596
752
|
|
|
597
753
|
// Set validation errors
|
|
598
754
|
if (!email?.includes("@"))
|
|
599
|
-
args.
|
|
755
|
+
args.parent.setValidationError("Email", "Invalid email format");
|
|
600
756
|
|
|
601
|
-
// Clear validation errors
|
|
602
|
-
args.
|
|
757
|
+
// Clear validation errors (pass null or empty string)
|
|
758
|
+
args.parent.setValidationError("Email", null);
|
|
603
759
|
|
|
604
760
|
// Show notifications
|
|
605
|
-
args.
|
|
606
|
-
args.
|
|
607
|
-
args.
|
|
761
|
+
args.parent.setNotification("Saved successfully", "OK", 3000);
|
|
762
|
+
args.parent.setNotification("Warning!", "Warning", 5000);
|
|
763
|
+
args.parent.setNotification("Error occurred", "Error");
|
|
608
764
|
|
|
609
765
|
return args.parent;
|
|
610
766
|
}
|
|
611
767
|
```
|
|
612
768
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
| Method | Description |
|
|
616
|
-
|--------|-------------|
|
|
617
|
-
| `getAttribute(name)` | Get the full attribute DTO |
|
|
618
|
-
| `getAttributeValue(name)` | Get the raw attribute value |
|
|
619
|
-
| `getConvertedValue(attr)` | Get type-converted value from attribute DTO |
|
|
620
|
-
| `setAttributeValue(name, value)` | Update raw attribute value |
|
|
621
|
-
| `setConvertedValue(attr, value)` | Update attribute DTO with type conversion |
|
|
622
|
-
| `setValidationError(name, error)` | Set a validation error message |
|
|
623
|
-
| `clearValidationError(name)` | Clear validation error |
|
|
624
|
-
| `setNotification(msg, type, duration?)` | Show notification to user |
|
|
769
|
+
See [VirtualPersistentObject Methods](#virtualpersistentobject-methods) for the full list.
|
|
625
770
|
|
|
626
771
|
### Query Actions
|
|
627
772
|
|
|
628
773
|
Actions can operate on query results:
|
|
629
774
|
|
|
630
775
|
```typescript
|
|
631
|
-
service.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
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}`);
|
|
640
783
|
}
|
|
784
|
+
|
|
785
|
+
return null; // Silent completion
|
|
641
786
|
});
|
|
642
787
|
```
|
|
643
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
|
+
|
|
644
797
|
## Lifecycle Hooks
|
|
645
798
|
|
|
646
799
|
### VirtualPersistentObjectActions
|
|
@@ -648,43 +801,45 @@ service.registerAction({
|
|
|
648
801
|
For complex scenarios, create a class extending `VirtualPersistentObjectActions`:
|
|
649
802
|
|
|
650
803
|
```typescript
|
|
651
|
-
import { VirtualPersistentObjectActions } from "@vidyano-labs/virtual-service";
|
|
804
|
+
import { VirtualPersistentObjectActions, VirtualQuery } from "@vidyano-labs/virtual-service";
|
|
652
805
|
import type { VirtualPersistentObject, VirtualPersistentObjectAttribute } from "@vidyano-labs/virtual-service";
|
|
653
|
-
import { Dto } from "@vidyano/core";
|
|
654
806
|
|
|
655
807
|
class PersonActions extends VirtualPersistentObjectActions {
|
|
656
|
-
// Called
|
|
808
|
+
// Called after the object is built
|
|
657
809
|
onConstruct(obj: VirtualPersistentObject): void {
|
|
658
|
-
// Set defaults - note: this is synchronous
|
|
659
810
|
obj.setAttributeValue("CreatedDate", new Date().toISOString());
|
|
660
811
|
}
|
|
661
812
|
|
|
662
813
|
// Called when loading an existing Person
|
|
663
814
|
async onLoad(
|
|
664
|
-
|
|
815
|
+
objectId: string,
|
|
665
816
|
parent: VirtualPersistentObject | null
|
|
666
817
|
): Promise<VirtualPersistentObject> {
|
|
818
|
+
const obj = await super.onLoad(objectId, parent);
|
|
819
|
+
|
|
667
820
|
// Load additional data based on ID
|
|
668
|
-
|
|
669
|
-
|
|
821
|
+
console.log(`Loading person: ${objectId}`);
|
|
822
|
+
|
|
670
823
|
return obj;
|
|
671
824
|
}
|
|
672
825
|
|
|
673
826
|
// Called when creating a new Person via "New" action
|
|
674
827
|
async onNew(
|
|
675
|
-
obj: VirtualPersistentObject,
|
|
676
828
|
parent: VirtualPersistentObject | null,
|
|
677
|
-
query:
|
|
829
|
+
query: VirtualQuery | null,
|
|
678
830
|
parameters: Record<string, string> | null
|
|
679
831
|
): Promise<VirtualPersistentObject> {
|
|
832
|
+
const obj = await super.onNew(parent, query, parameters);
|
|
833
|
+
|
|
680
834
|
// Initialize new object
|
|
681
835
|
obj.setAttributeValue("Status", "Draft");
|
|
836
|
+
|
|
682
837
|
return obj;
|
|
683
838
|
}
|
|
684
839
|
|
|
685
840
|
// Called when saving
|
|
686
841
|
async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
687
|
-
// Calls saveNew or saveExisting based on obj.isNew
|
|
842
|
+
// Calls checkRules, then saveNew or saveExisting based on obj.isNew
|
|
688
843
|
return await super.onSave(obj);
|
|
689
844
|
}
|
|
690
845
|
|
|
@@ -701,18 +856,26 @@ class PersonActions extends VirtualPersistentObjectActions {
|
|
|
701
856
|
}
|
|
702
857
|
}
|
|
703
858
|
|
|
704
|
-
// Register the actions class
|
|
705
|
-
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);
|
|
706
869
|
```
|
|
707
870
|
|
|
708
871
|
### Lifecycle Flow
|
|
709
872
|
|
|
710
873
|
```
|
|
711
|
-
PersistentObject Load: onConstruct
|
|
712
|
-
PersistentObject New: onConstruct
|
|
713
|
-
PersistentObject Save:
|
|
714
|
-
Query
|
|
715
|
-
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
|
|
716
879
|
Attribute Refresh: onRefresh
|
|
717
880
|
Reference Selection: onSelectReference
|
|
718
881
|
Deletion: onDelete
|
|
@@ -751,6 +914,27 @@ service.registerPersistentObject({
|
|
|
751
914
|
});
|
|
752
915
|
```
|
|
753
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
|
+
|
|
754
938
|
### Query Execution
|
|
755
939
|
|
|
756
940
|
Provide dynamic query data:
|
|
@@ -762,7 +946,7 @@ class PersonActions extends VirtualPersistentObjectActions {
|
|
|
762
946
|
// Provide data for query execution
|
|
763
947
|
// Framework handles text search, sort, and pagination automatically
|
|
764
948
|
async getEntities(
|
|
765
|
-
query:
|
|
949
|
+
query: VirtualQuery,
|
|
766
950
|
parent: VirtualPersistentObject | null,
|
|
767
951
|
data: Record<string, any>[]
|
|
768
952
|
): Promise<Record<string, any>[]> {
|
|
@@ -772,7 +956,7 @@ class PersonActions extends VirtualPersistentObjectActions {
|
|
|
772
956
|
|
|
773
957
|
// Or fully control query execution
|
|
774
958
|
async onExecuteQuery(
|
|
775
|
-
query:
|
|
959
|
+
query: VirtualQuery,
|
|
776
960
|
parent: VirtualPersistentObject | null,
|
|
777
961
|
data: Record<string, any>[]
|
|
778
962
|
): Promise<VirtualQueryExecuteResult> {
|
|
@@ -791,16 +975,16 @@ Handle reference attribute selection:
|
|
|
791
975
|
class OrderActions extends VirtualPersistentObjectActions {
|
|
792
976
|
async onSelectReference(
|
|
793
977
|
parent: VirtualPersistentObject,
|
|
794
|
-
referenceAttribute:
|
|
795
|
-
query:
|
|
796
|
-
selectedItem:
|
|
978
|
+
referenceAttribute: VirtualPersistentObjectAttribute,
|
|
979
|
+
query: VirtualQuery,
|
|
980
|
+
selectedItem: VirtualQueryResultItem | null
|
|
797
981
|
): Promise<void> {
|
|
798
982
|
// Default: sets objectId and value from displayAttribute
|
|
799
983
|
await super.onSelectReference(parent, referenceAttribute, query, selectedItem);
|
|
800
984
|
|
|
801
985
|
// Custom: also copy related fields
|
|
802
986
|
if (selectedItem) {
|
|
803
|
-
const customerName = selectedItem.
|
|
987
|
+
const customerName = selectedItem.getValue("Name");
|
|
804
988
|
parent.setAttributeValue("CustomerName", customerName);
|
|
805
989
|
}
|
|
806
990
|
}
|
|
@@ -904,23 +1088,47 @@ const linesQuery = order.queries.find(q => q.name === "OrderLines");
|
|
|
904
1088
|
await linesQuery.search();
|
|
905
1089
|
```
|
|
906
1090
|
|
|
907
|
-
|
|
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
|
|
1110
|
+
|
|
1111
|
+
This is useful for optimizing performance when detail queries contain large datasets that aren't always needed immediately.
|
|
908
1112
|
|
|
909
|
-
|
|
1113
|
+
## VirtualPersistentObject Methods
|
|
1114
|
+
|
|
1115
|
+
The `VirtualPersistentObject` type provides these methods:
|
|
910
1116
|
|
|
911
1117
|
```typescript
|
|
912
|
-
// In lifecycle hooks, objects are wrapped with helpers
|
|
913
1118
|
async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
914
|
-
// Get attribute by name
|
|
1119
|
+
// Get attribute by name
|
|
915
1120
|
const attr = obj.getAttribute("Email");
|
|
916
1121
|
|
|
917
1122
|
// Get/set values
|
|
918
1123
|
const email = obj.getAttributeValue("Email");
|
|
919
1124
|
obj.setAttributeValue("Email", "new@example.com");
|
|
920
1125
|
|
|
921
|
-
// Validation errors
|
|
1126
|
+
// Validation errors (pass null/empty to clear)
|
|
922
1127
|
obj.setValidationError("Email", "Invalid format");
|
|
923
|
-
obj.
|
|
1128
|
+
obj.setValidationError("Email", null); // Clear error
|
|
1129
|
+
|
|
1130
|
+
// Access the service
|
|
1131
|
+
const message = obj.service.getMessage("CustomKey");
|
|
924
1132
|
|
|
925
1133
|
// Notifications
|
|
926
1134
|
obj.setNotification("Saved!", "OK", 3000);
|
|
@@ -933,12 +1141,12 @@ async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
|
933
1141
|
|
|
934
1142
|
| Method | Description |
|
|
935
1143
|
|--------|-------------|
|
|
936
|
-
| `getAttribute(name)` | Get attribute
|
|
1144
|
+
| `getAttribute(name)` | Get attribute by name |
|
|
937
1145
|
| `getAttributeValue(name)` | Get converted attribute value |
|
|
938
1146
|
| `setAttributeValue(name, value)` | Set attribute value with conversion |
|
|
939
|
-
| `setValidationError(name, error)` | Set validation error |
|
|
940
|
-
| `clearValidationError(name)` | Clear validation error |
|
|
1147
|
+
| `setValidationError(name, error)` | Set validation error (pass `null`/empty to clear) |
|
|
941
1148
|
| `setNotification(msg, type, duration?)` | Set notification |
|
|
1149
|
+
| `service` | Reference to the VirtualService instance |
|
|
942
1150
|
|
|
943
1151
|
**VirtualPersistentObjectAttribute methods:**
|
|
944
1152
|
|
|
@@ -946,8 +1154,10 @@ async onSave(obj: VirtualPersistentObject): Promise<VirtualPersistentObject> {
|
|
|
946
1154
|
|--------|-------------|
|
|
947
1155
|
| `getValue()` | Get converted value |
|
|
948
1156
|
| `setValue(value)` | Set value with conversion |
|
|
949
|
-
| `setValidationError(error)` | Set validation error |
|
|
950
|
-
| `
|
|
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
|
+
|
|
951
1161
|
|
|
952
1162
|
## Testing Examples
|
|
953
1163
|
|
|
@@ -983,30 +1193,23 @@ test("validates email format", async () => {
|
|
|
983
1193
|
```typescript
|
|
984
1194
|
import { test, expect } from "@playwright/test";
|
|
985
1195
|
import { VirtualService } from "@vidyano-labs/virtual-service";
|
|
986
|
-
import type { ActionArgs } from "@vidyano-labs/virtual-service";
|
|
987
1196
|
|
|
988
1197
|
test("complete order workflow", async () => {
|
|
989
1198
|
const service = new VirtualService();
|
|
990
1199
|
|
|
991
|
-
service.
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
args.context.setAttributeValue("Status", "Submitted");
|
|
995
|
-
return args.parent;
|
|
996
|
-
}
|
|
1200
|
+
service.registerCustomAction("Submit", async (args) => {
|
|
1201
|
+
args.parent.setAttributeValue("Status", "Submitted");
|
|
1202
|
+
return args.parent;
|
|
997
1203
|
});
|
|
998
1204
|
|
|
999
|
-
service.
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (status !== "Submitted") {
|
|
1004
|
-
args.context.setNotification("Order must be submitted first", "Error");
|
|
1005
|
-
return args.parent;
|
|
1006
|
-
}
|
|
1007
|
-
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");
|
|
1008
1209
|
return args.parent;
|
|
1009
1210
|
}
|
|
1211
|
+
args.parent.setAttributeValue("Status", "Approved");
|
|
1212
|
+
return args.parent;
|
|
1010
1213
|
});
|
|
1011
1214
|
|
|
1012
1215
|
service.registerPersistentObject({
|
|
@@ -1086,54 +1289,36 @@ test("search and sort query results", async () => {
|
|
|
1086
1289
|
| Method | Description |
|
|
1087
1290
|
|--------|-------------|
|
|
1088
1291
|
| `constructor(hooks?)` | Create service with optional custom hooks |
|
|
1089
|
-
| `registerPersistentObject(config)` | Register a PersistentObject type |
|
|
1292
|
+
| `registerPersistentObject(config, actionsClass?)` | Register a PersistentObject type with optional lifecycle class |
|
|
1090
1293
|
| `registerQuery(config)` | Register a Query |
|
|
1091
|
-
| `
|
|
1294
|
+
| `registerCustomAction(name, handler)` | Register a custom action (simple) |
|
|
1295
|
+
| `registerCustomAction(config, handler)` | Register a custom action (with config) |
|
|
1092
1296
|
| `registerBusinessRule(name, validator)` | Register a validation rule |
|
|
1093
|
-
| `
|
|
1297
|
+
| `getMessage(key, ...params)` | Get a formatted message by key |
|
|
1094
1298
|
| `initialize()` | Finalize registrations |
|
|
1095
1299
|
|
|
1300
|
+
| Static Property | Description |
|
|
1301
|
+
|-----------------|-------------|
|
|
1302
|
+
| `VirtualService.messages` | Get/set the global messages dictionary for translations |
|
|
1303
|
+
|
|
1096
1304
|
### VirtualPersistentObjectActions
|
|
1097
1305
|
|
|
1098
1306
|
| Method | Description |
|
|
1099
1307
|
|--------|-------------|
|
|
1100
|
-
| `onConstruct(obj)` | Called
|
|
1101
|
-
| `onLoad(
|
|
1102
|
-
| `onNew(
|
|
1103
|
-
| `
|
|
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) |
|
|
1104
1314
|
| `saveNew(obj)` | Called for new objects (protected) |
|
|
1105
1315
|
| `saveExisting(obj)` | Called for existing objects (protected) |
|
|
1106
|
-
| `onRefresh(obj, attribute)` | Called when refreshing |
|
|
1107
|
-
| `onDelete(parent, query, items)` | Called when deleting items |
|
|
1108
|
-
| `onConstructQuery(query, parent)` | Called
|
|
1109
|
-
| `onExecuteQuery(query, parent, data)` | Called when executing a query |
|
|
1110
|
-
| `getEntities(query, parent, data)` | Provide query data |
|
|
1111
|
-
| `onSelectReference(parent, attr, query, item)` | Called when selecting a reference |
|
|
1112
|
-
|
|
1113
|
-
### Type Exports
|
|
1114
|
-
|
|
1115
|
-
```typescript
|
|
1116
|
-
import {
|
|
1117
|
-
VirtualService,
|
|
1118
|
-
VirtualServiceHooks,
|
|
1119
|
-
VirtualPersistentObjectActions
|
|
1120
|
-
} from "@vidyano-labs/virtual-service";
|
|
1121
|
-
|
|
1122
|
-
import type {
|
|
1123
|
-
VirtualPersistentObject,
|
|
1124
|
-
VirtualPersistentObjectAttribute,
|
|
1125
|
-
VirtualPersistentObjectConfig,
|
|
1126
|
-
VirtualPersistentObjectAttributeConfig,
|
|
1127
|
-
VirtualQueryConfig,
|
|
1128
|
-
VirtualQueryExecuteResult,
|
|
1129
|
-
ActionConfig,
|
|
1130
|
-
ActionHandler,
|
|
1131
|
-
ActionArgs,
|
|
1132
|
-
ActionContext,
|
|
1133
|
-
RuleValidatorFn,
|
|
1134
|
-
RuleValidationContext
|
|
1135
|
-
} from "@vidyano-labs/virtual-service";
|
|
1136
|
-
```
|
|
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 |
|
|
1137
1322
|
|
|
1138
1323
|
## Best Practices
|
|
1139
1324
|
|