@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.
Files changed (4) hide show
  1. package/README.md +283 -210
  2. package/index.d.ts +376 -150
  3. package/index.js +1391 -1597
  4. 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
- - `registerAction(config)` - Register a custom action handler
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 the Save action executes:
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 before save handlers execute
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, the save operation is aborted
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 { RuleValidationContext } from "@vidyano-labs/virtual-service";
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, context: RuleValidationContext) => {
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
- **Validation context:**
363
- The `RuleValidationContext` parameter provides access to:
364
- - `context.persistentObject` - The persistent object being validated (wrapped with helper methods)
365
- - `context.attribute` - The attribute being validated (wrapped with helper methods)
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, context: RuleValidationContext) => {
424
+ service.registerBusinessRule("MatchesPassword", (value: any, attr: VirtualPersistentObjectAttribute) => {
372
425
  if (!value) return;
373
426
 
374
- const passwordValue = context.persistentObject.getAttributeValue("Password");
427
+ const passwordValue = attr.persistentObject.getAttributeValue("Password");
375
428
  if (value !== passwordValue)
376
- throw new Error("Passwords do not match");
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 `context` (validation context)
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
- Provide custom translations for validation error messages to support multiple languages:
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
- // Define your translations
407
- const translations: Record<string, string> = {
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 is now translated
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. Built-in validators call `translate(key, ...params)` instead of using hardcoded messages
457
- 2. Your translate function receives the rule name and any parameters
458
- 3. Return the translated message with parameters interpolated
459
- 4. If no translate function is provided, default English messages are used
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 translation:**
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 translation
475
- service.registerBusinessRule("MinimumAge", (value: any, context: RuleValidationContext, minAge: number) => {
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(context.translate("MinimumAge", minAge));
525
+ throw new Error(attr.service.getMessage("MinimumAge", minAge));
482
526
  });
483
527
 
484
- // Custom rule can still throw direct errors (backward compatible)
485
- service.registerBusinessRule("IsPhoneNumber", (value: any, _context: RuleValidationContext) => {
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"); // Not translated
535
+ throw new Error("Invalid phone number format"); // Direct string, not translated
492
536
  });
493
537
  ```
494
538
 
495
- **Built-in rule keys:**
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
- service.registerAction({
654
- name: "Approve",
655
- displayName: "Approve Order",
656
- isPinned: true,
657
- handler: async (args: ActionArgs) => {
658
- // Access context for reading/modifying the object
659
- args.context.setAttributeValue("Status", "Approved");
660
- args.context.setNotification("Order approved!", "OK", 3000);
661
-
662
- // Return the updated object (or null for silent completion)
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 `ActionArgs` with execution context:
721
+ Action handlers receive an `args` object with execution context:
671
722
 
672
723
  ```typescript
673
- import type { ActionArgs } from "@vidyano-labs/virtual-service";
674
-
675
- interface ActionArgs {
676
- parent: PersistentObjectDto | null; // The PO being acted on
677
- query?: QueryDto; // The query (for query actions)
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
- The context provides helper methods for modifying the persistent object:
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: ActionArgs) => {
737
+ handler: async (args) => {
690
738
  // Get an attribute
691
- const emailAttr = args.context.getAttribute("Email");
739
+ const emailAttr = args.parent.getAttribute("Email");
692
740
 
693
- // Read attribute values
694
- const email = args.context.getAttributeValue("Email");
741
+ // Read attribute values (type-converted)
742
+ const email = args.parent.getAttributeValue("Email");
695
743
 
696
- // Type-safe value access (pass the attribute DTO)
697
- const age = args.context.getConvertedValue(args.context.getAttribute("Age")!);
744
+ // Read from the attribute directly
745
+ const age = emailAttr?.getValue();
698
746
 
699
747
  // Modify attribute values
700
- args.context.setAttributeValue("Status", "Active");
748
+ args.parent.setAttributeValue("Status", "Active");
701
749
 
702
- // Set with type conversion (pass the attribute DTO)
703
- const countAttr = args.context.getAttribute("Count")!;
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.context.setValidationError("Email", "Invalid email format");
755
+ args.parent.setValidationError("Email", "Invalid email format");
709
756
 
710
- // Clear validation errors
711
- args.context.clearValidationError("Email");
757
+ // Clear validation errors (pass null or empty string)
758
+ args.parent.setValidationError("Email", null);
712
759
 
713
760
  // Show notifications
714
- args.context.setNotification("Saved successfully", "OK", 3000);
715
- args.context.setNotification("Warning!", "Warning", 5000);
716
- args.context.setNotification("Error occurred", "Error");
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
- **Context methods:**
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.registerAction({
741
- name: "BulkDelete",
742
- handler: async (args: ActionArgs) => {
743
- // Access selected items
744
- for (const item of args.selectedItems || []) {
745
- console.log(`Deleting item: ${item.id}`);
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 when any Person DTO is created
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
- obj: VirtualPersistentObject,
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
- const id = obj.objectId;
778
- console.log(`Loading person: ${id}`);
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: Dto.QueryDto | null,
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.registerPersistentObjectActions("Person", PersonActions);
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 → onLoad
821
- PersistentObject New: onConstruct → onNew
822
- PersistentObject Save: (validation)onSave → (saveNew | saveExisting)
823
- Query Construction: onConstructQuery
824
- Query Execution: onExecuteQuery → (text search, sort, paginate)
874
+ PersistentObject Load: onLoad (calls onConstruct internally)
875
+ PersistentObject New: onNew (calls onConstruct internally)
876
+ PersistentObject Save: onSavecheckRules → (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: Dto.QueryDto,
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: Dto.QueryDto,
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: Dto.PersistentObjectAttributeDto,
904
- query: Dto.QueryDto,
905
- selectedItem: Dto.QueryResultItemDto | null
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.values?.find(v => v.key === "Name")?.value;
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
- ## VirtualPersistentObject Helpers
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
- The `VirtualPersistentObject` type provides convenient helper methods:
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 (wrapped with helpers)
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.clearValidationError("Email");
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 wrapped with helpers |
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
- | `clearValidationError()` | Clear validation error |
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.registerAction({
1101
- name: "Submit",
1102
- handler: async (args: ActionArgs) => {
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.registerAction({
1109
- name: "Approve",
1110
- handler: async (args: ActionArgs) => {
1111
- const status = args.context.getAttributeValue("Status");
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
- | `registerAction(config)` | Register a custom action |
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
- | `registerPersistentObjectActions(type, Class)` | Register lifecycle handlers |
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 when constructing the DTO |
1211
- | `onLoad(obj, parent)` | Called when loading an existing object |
1212
- | `onNew(obj, parent, query, params)` | Called when creating a new object |
1213
- | `onSave(obj)` | Called when saving |
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 when constructing a query |
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