@vidyano-labs/virtual-service 0.1.1 → 0.2.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 CHANGED
@@ -336,8 +336,10 @@ console.log(contact.getAttribute("Email").validationError);
336
336
  Register your own validation rules for domain-specific requirements:
337
337
 
338
338
  ```typescript
339
+ import type { RuleValidationContext } from "@vidyano-labs/virtual-service";
340
+
339
341
  // Register a custom rule (before registerPersistentObject)
340
- service.registerBusinessRule("IsPhoneNumber", (value: any) => {
342
+ service.registerBusinessRule("IsPhoneNumber", (value: any, context: RuleValidationContext) => {
341
343
  if (value == null || value === "") return;
342
344
  const phoneRegex = /^\+?[\d\s-()]+$/;
343
345
  if (!phoneRegex.test(String(value)))
@@ -357,8 +359,39 @@ service.registerPersistentObject({
357
359
  });
358
360
  ```
359
361
 
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)
366
+
367
+ This allows cross-field validation:
368
+
369
+ ```typescript
370
+ // Validate password confirmation matches password
371
+ service.registerBusinessRule("MatchesPassword", (value: any, context: RuleValidationContext) => {
372
+ if (!value) return;
373
+
374
+ const passwordValue = context.persistentObject.getAttributeValue("Password");
375
+ if (value !== passwordValue)
376
+ throw new Error("Passwords do not match");
377
+ });
378
+
379
+ service.registerPersistentObject({
380
+ type: "User",
381
+ attributes: [
382
+ { name: "Password", type: "String" },
383
+ {
384
+ name: "ConfirmPassword",
385
+ type: "String",
386
+ rules: "MatchesPassword"
387
+ }
388
+ ]
389
+ });
390
+ ```
391
+
360
392
  **Custom rule requirements:**
361
393
  - Must be registered before `registerPersistentObject()`
394
+ - Receives two parameters: `value` (the attribute value) and `context` (validation context)
362
395
  - Throw an `Error` with a message if validation fails
363
396
  - Return nothing (or undefined) if validation passes
364
397
  - Cannot override built-in rules
@@ -1097,7 +1130,8 @@ import type {
1097
1130
  ActionHandler,
1098
1131
  ActionArgs,
1099
1132
  ActionContext,
1100
- RuleValidatorFn
1133
+ RuleValidatorFn,
1134
+ RuleValidationContext
1101
1135
  } from "@vidyano-labs/virtual-service";
1102
1136
  ```
1103
1137
 
package/index.d.ts CHANGED
@@ -77,10 +77,6 @@ type VirtualPersistentObjectAttributeConfig = {
77
77
  * Initial value for the attribute.
78
78
  */
79
79
  value?: any;
80
- /**
81
- * Indicates whether this attribute is required. Defaults to false.
82
- */
83
- isRequired?: boolean;
84
80
  /**
85
81
  * Indicates whether the value of this attribute can be changed. Defaults to false.
86
82
  */
package/index.js CHANGED
@@ -215,15 +215,15 @@ class VirtualPersistentObjectRegistry {
215
215
  for (const attr of po.attributes) {
216
216
  // Clear previous validation errors
217
217
  attr.validationError = undefined;
218
- // Find the attribute config to get rules and isRequired
218
+ // Find the attribute config to get rules
219
219
  const attrConfig = config.attributes.find(a => a.name === attr.name);
220
220
  if (!attrConfig)
221
221
  continue;
222
- // Create a DTO with the rules and isRequired from config
222
+ // Always use server config for rules and isRequired (never trust client values)
223
223
  const attrWithRules = {
224
224
  ...attr,
225
225
  rules: attrConfig.rules,
226
- isRequired: attrConfig.isRequired || false
226
+ isRequired: hasRequiredRule(attrConfig.rules)
227
227
  };
228
228
  // Validate the attribute
229
229
  const error = this.#validator.validateAttribute(attrWithRules, po);
@@ -345,17 +345,36 @@ async function buildPersistentObjectDto(config, queryRegistry, objectId, isNew)
345
345
  queries
346
346
  };
347
347
  }
348
+ /**
349
+ * Checks if rules string contains NotEmpty or Required
350
+ */
351
+ function hasRequiredRule(rules) {
352
+ if (!rules)
353
+ return false;
354
+ // Parse the rules string (semicolon-separated)
355
+ const ruleNames = rules
356
+ .split(";")
357
+ .map(rule => rule.trim())
358
+ .map(rule => {
359
+ // Extract just the rule name (before any parentheses)
360
+ const match = rule.match(/^(\w+)/);
361
+ return match ? match[1] : "";
362
+ });
363
+ return ruleNames.includes("NotEmpty") || ruleNames.includes("Required");
364
+ }
348
365
  /**
349
366
  * Builds a PersistentObjectAttributeDto from configuration
350
367
  */
351
368
  async function buildAttributeDto(config, index, queryRegistry) {
369
+ // Automatically set isRequired if rules contain NotEmpty or Required
370
+ const isRequired = hasRequiredRule(config.rules);
352
371
  const baseDto = {
353
372
  id: config.id || crypto.randomUUID(),
354
373
  name: config.name,
355
374
  type: config.type || "String",
356
375
  label: config.label || config.name,
357
376
  value: config.value,
358
- isRequired: config.isRequired || false,
377
+ isRequired,
359
378
  isReadOnly: config.isReadOnly || false,
360
379
  rules: config.rules,
361
380
  visibility: config.visibility || "Always",
@@ -1221,7 +1240,7 @@ class BusinessRuleValidator {
1221
1240
  this.#builtInRules.set("MinLength", this.#validateMinLength.bind(this));
1222
1241
  this.#builtInRules.set("MinValue", this.#validateMinValue.bind(this));
1223
1242
  this.#builtInRules.set("NotEmpty", this.#validateNotEmpty.bind(this));
1224
- this.#builtInRules.set("Required", this.#validateNotEmpty.bind(this)); // Required is same as NotEmpty
1243
+ this.#builtInRules.set("Required", this.#validateRequired.bind(this));
1225
1244
  }
1226
1245
  /**
1227
1246
  * Register a custom business rule
@@ -1257,16 +1276,9 @@ class BusinessRuleValidator {
1257
1276
  };
1258
1277
  // Get the converted value (e.g., boolean from "True"/"False", number from string)
1259
1278
  const convertedValue = this.#getConvertedValue(attr);
1260
- // Check isRequired first
1261
- if (attr.isRequired) {
1262
- try {
1263
- this.#validateNotEmpty(convertedValue, context);
1264
- }
1265
- catch (error) {
1266
- return error instanceof Error ? error.message : String(error);
1267
- }
1268
- }
1269
1279
  // Parse and validate rules string
1280
+ // Note: isRequired is auto-set from rules for UI purposes only (e.g., showing asterisks)
1281
+ // All validation logic is handled by the rules themselves
1270
1282
  if (!attr.rules)
1271
1283
  return null;
1272
1284
  const rules = this.parseRules(attr.rules);
@@ -1407,9 +1419,13 @@ class BusinessRuleValidator {
1407
1419
  if (num < minimum)
1408
1420
  throw new Error(`Minimum value is ${minimum}`);
1409
1421
  }
1422
+ #validateRequired(value, context) {
1423
+ if (value == null)
1424
+ throw new Error("This field is required");
1425
+ }
1410
1426
  #validateNotEmpty(value, context) {
1411
1427
  if (value == null || value === "" || (typeof value === "string" && value.trim() === ""))
1412
- throw new Error("This field is required");
1428
+ throw new Error("This field cannot be empty");
1413
1429
  }
1414
1430
  }
1415
1431
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidyano-labs/virtual-service",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Virtual service implementation for testing Vidyano applications",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -20,5 +20,5 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
- "gitHash": "c69d76d2dd9e0289838ca50f1cc89a81fa9a89c3"
23
+ "gitHash": "a8a427169445d844bc42c226c93da215441e6242"
24
24
  }