@vidyano-labs/virtual-service 0.1.0 → 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.
Files changed (4) hide show
  1. package/README.md +36 -2
  2. package/index.d.ts +68 -59
  3. package/index.js +67 -27
  4. package/package.json +2 -2
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
@@ -1,5 +1,58 @@
1
1
  import { Dto, ServiceHooks, Service, Application } from '@vidyano/core';
2
2
 
3
+ /**
4
+ * VirtualPersistentObjectAttribute combines a PersistentObjectAttributeDto with helper methods
5
+ * This allows clean syntax like attr.getValue() and attr.setValue() while keeping the underlying DTO unchanged
6
+ */
7
+ type VirtualPersistentObjectAttribute = Dto.PersistentObjectAttributeDto & {
8
+ /**
9
+ * Gets the converted value of this attribute (e.g., Boolean as boolean, Int32 as number)
10
+ */
11
+ getValue(): any;
12
+ /**
13
+ * Sets the value of this attribute with automatic type conversion
14
+ */
15
+ setValue(value: any): void;
16
+ /**
17
+ * Sets a validation error on this attribute
18
+ */
19
+ setValidationError(error: string): void;
20
+ /**
21
+ * Clears the validation error on this attribute
22
+ */
23
+ clearValidationError(): void;
24
+ };
25
+ /**
26
+ * VirtualPersistentObject combines a PersistentObjectDto with helper methods
27
+ * This allows clean syntax like obj.setAttributeValue() while keeping the underlying DTO unchanged
28
+ */
29
+ type VirtualPersistentObject = Dto.PersistentObjectDto & {
30
+ /**
31
+ * Gets an attribute by name, wrapped with getValue/setValue methods
32
+ */
33
+ getAttribute(name: string): VirtualPersistentObjectAttribute | undefined;
34
+ /**
35
+ * Gets the value of an attribute by name
36
+ */
37
+ getAttributeValue(name: string): any;
38
+ /**
39
+ * Sets the value of an attribute by name
40
+ */
41
+ setAttributeValue(name: string, value: any): void;
42
+ /**
43
+ * Sets a validation error for an attribute
44
+ */
45
+ setValidationError(name: string, error: string): void;
46
+ /**
47
+ * Clears a validation error for an attribute
48
+ */
49
+ clearValidationError(name: string): void;
50
+ /**
51
+ * Sets a notification message on the persistent object
52
+ */
53
+ setNotification(message: string, type: Dto.NotificationType, duration?: number): void;
54
+ };
55
+
3
56
  /**
4
57
  * Simplified attribute configuration - converted to PersistentObjectAttributeDto
5
58
  */
@@ -24,10 +77,6 @@ type VirtualPersistentObjectAttributeConfig = {
24
77
  * Initial value for the attribute.
25
78
  */
26
79
  value?: any;
27
- /**
28
- * Indicates whether this attribute is required. Defaults to false.
29
- */
30
- isRequired?: boolean;
31
80
  /**
32
81
  * Indicates whether the value of this attribute can be changed. Defaults to false.
33
82
  */
@@ -184,6 +233,19 @@ type ActionContext = {
184
233
  */
185
234
  setNotification: (message: string, type: Dto.NotificationType, duration?: number) => void;
186
235
  };
236
+ /**
237
+ * Context provided to business rule validators for accessing the persistent object
238
+ */
239
+ type RuleValidationContext = {
240
+ /**
241
+ * The persistent object being validated (wrapped with helper methods)
242
+ */
243
+ persistentObject: VirtualPersistentObject;
244
+ /**
245
+ * The attribute being validated (wrapped with helper methods)
246
+ */
247
+ attribute: VirtualPersistentObjectAttribute;
248
+ };
187
249
  /**
188
250
  * PersistentObject configuration - converted to PersistentObjectDto
189
251
  */
@@ -286,60 +348,7 @@ type VirtualQueryConfig = {
286
348
  /**
287
349
  * Rule validator function that throws an error if invalid, or returns nothing if valid
288
350
  */
289
- type RuleValidatorFn = (value: any, ...params: any[]) => void;
290
-
291
- /**
292
- * VirtualPersistentObjectAttribute combines a PersistentObjectAttributeDto with helper methods
293
- * This allows clean syntax like attr.getValue() and attr.setValue() while keeping the underlying DTO unchanged
294
- */
295
- type VirtualPersistentObjectAttribute = Dto.PersistentObjectAttributeDto & {
296
- /**
297
- * Gets the converted value of this attribute (e.g., Boolean as boolean, Int32 as number)
298
- */
299
- getValue(): any;
300
- /**
301
- * Sets the value of this attribute with automatic type conversion
302
- */
303
- setValue(value: any): void;
304
- /**
305
- * Sets a validation error on this attribute
306
- */
307
- setValidationError(error: string): void;
308
- /**
309
- * Clears the validation error on this attribute
310
- */
311
- clearValidationError(): void;
312
- };
313
- /**
314
- * VirtualPersistentObject combines a PersistentObjectDto with helper methods
315
- * This allows clean syntax like obj.setAttributeValue() while keeping the underlying DTO unchanged
316
- */
317
- type VirtualPersistentObject = Dto.PersistentObjectDto & {
318
- /**
319
- * Gets an attribute by name, wrapped with getValue/setValue methods
320
- */
321
- getAttribute(name: string): VirtualPersistentObjectAttribute | undefined;
322
- /**
323
- * Gets the value of an attribute by name
324
- */
325
- getAttributeValue(name: string): any;
326
- /**
327
- * Sets the value of an attribute by name
328
- */
329
- setAttributeValue(name: string, value: any): void;
330
- /**
331
- * Sets a validation error for an attribute
332
- */
333
- setValidationError(name: string, error: string): void;
334
- /**
335
- * Clears a validation error for an attribute
336
- */
337
- clearValidationError(name: string): void;
338
- /**
339
- * Sets a notification message on the persistent object
340
- */
341
- setNotification(message: string, type: Dto.NotificationType, duration?: number): void;
342
- };
351
+ type RuleValidatorFn = (value: any, context: RuleValidationContext, ...params: any[]) => void;
343
352
 
344
353
  /**
345
354
  * Base class for PersistentObject lifecycle methods
@@ -556,4 +565,4 @@ declare class VirtualService extends Service {
556
565
  }
557
566
 
558
567
  export { VirtualPersistentObjectActions, VirtualService, VirtualServiceHooks };
559
- export type { ActionArgs, ActionConfig, ActionContext, ActionHandler, RuleValidatorFn, VirtualPersistentObject, VirtualPersistentObjectAttribute, VirtualPersistentObjectAttributeConfig, VirtualPersistentObjectConfig, VirtualQueryConfig, VirtualQueryExecuteResult };
568
+ export type { ActionArgs, ActionConfig, ActionContext, ActionHandler, RuleValidationContext, RuleValidatorFn, VirtualPersistentObject, VirtualPersistentObjectAttribute, VirtualPersistentObjectAttributeConfig, VirtualPersistentObjectConfig, VirtualQueryConfig, VirtualQueryExecuteResult };
package/index.js CHANGED
@@ -215,18 +215,18 @@ 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
- const error = this.#validator.validateAttribute(attrWithRules);
229
+ const error = this.#validator.validateAttribute(attrWithRules, po);
230
230
  if (error) {
231
231
  attr.validationError = error;
232
232
  hasErrors = true;
@@ -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
@@ -1234,19 +1253,32 @@ class BusinessRuleValidator {
1234
1253
  }
1235
1254
  /**
1236
1255
  * Validate an attribute against its rules
1256
+ * @param attr - The attribute to validate
1257
+ * @param po - The persistent object containing the attribute
1237
1258
  * @returns Error message if validation fails, null if valid
1238
1259
  */
1239
- validateAttribute(attr) {
1240
- // Check isRequired first
1241
- if (attr.isRequired) {
1242
- try {
1243
- this.#validateNotEmpty(attr.value);
1244
- }
1245
- catch (error) {
1246
- return error instanceof Error ? error.message : String(error);
1260
+ validateAttribute(attr, po) {
1261
+ // Create conversion context for wrappers
1262
+ const conversionContext = {
1263
+ getConvertedValue: (attribute) => this.#getConvertedValue(attribute),
1264
+ setConvertedValue: (attribute, value) => {
1265
+ attribute.value = toServiceValue(value, attribute.type);
1266
+ attribute.isValueChanged = true;
1247
1267
  }
1248
- }
1268
+ };
1269
+ // Create wrapped objects for the validation context
1270
+ const wrappedPo = createVirtualPersistentObject(po, conversionContext);
1271
+ const wrappedAttr = createVirtualPersistentObjectAttribute(attr, conversionContext);
1272
+ // Create validation context
1273
+ const context = {
1274
+ persistentObject: wrappedPo,
1275
+ attribute: wrappedAttr
1276
+ };
1277
+ // Get the converted value (e.g., boolean from "True"/"False", number from string)
1278
+ const convertedValue = this.#getConvertedValue(attr);
1249
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
1250
1282
  if (!attr.rules)
1251
1283
  return null;
1252
1284
  const rules = this.parseRules(attr.rules);
@@ -1255,7 +1287,7 @@ class BusinessRuleValidator {
1255
1287
  if (!validator)
1256
1288
  throw new Error(`Unknown business rule: ${rule.name}`);
1257
1289
  try {
1258
- validator(attr.value, ...rule.params);
1290
+ validator(convertedValue, context, ...rule.params);
1259
1291
  }
1260
1292
  catch (error) {
1261
1293
  return error instanceof Error ? error.message : String(error);
@@ -1308,15 +1340,19 @@ class BusinessRuleValidator {
1308
1340
  });
1309
1341
  return { name, params };
1310
1342
  }
1343
+ // Helper to convert attribute values based on type
1344
+ #getConvertedValue(attr) {
1345
+ return fromServiceValue(attr.value, attr.type);
1346
+ }
1311
1347
  // Built-in validators - throw errors instead of returning strings
1312
- #validateIsBase64(value) {
1348
+ #validateIsBase64(value, context) {
1313
1349
  if (value == null || value === "")
1314
1350
  return;
1315
1351
  const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
1316
1352
  if (!base64Regex.test(String(value)))
1317
1353
  throw new Error("Value must be a valid base64 string");
1318
1354
  }
1319
- #validateIsEmail(value) {
1355
+ #validateIsEmail(value, context) {
1320
1356
  if (value == null || value === "")
1321
1357
  return;
1322
1358
  // Only allow ASCII characters in email addresses
@@ -1324,7 +1360,7 @@ class BusinessRuleValidator {
1324
1360
  if (!emailRegex.test(String(value)))
1325
1361
  throw new Error("Email format is invalid");
1326
1362
  }
1327
- #validateIsRegex(value) {
1363
+ #validateIsRegex(value, context) {
1328
1364
  if (value == null || value === "")
1329
1365
  return;
1330
1366
  try {
@@ -1334,7 +1370,7 @@ class BusinessRuleValidator {
1334
1370
  throw new Error("Value must be a valid regular expression");
1335
1371
  }
1336
1372
  }
1337
- #validateIsUrl(value) {
1373
+ #validateIsUrl(value, context) {
1338
1374
  if (value == null || value === "")
1339
1375
  return;
1340
1376
  try {
@@ -1344,21 +1380,21 @@ class BusinessRuleValidator {
1344
1380
  throw new Error("Value must be a valid URL");
1345
1381
  }
1346
1382
  }
1347
- #validateIsWord(value) {
1383
+ #validateIsWord(value, context) {
1348
1384
  if (value == null || value === "")
1349
1385
  return;
1350
1386
  const wordRegex = /^\w+$/;
1351
1387
  if (!wordRegex.test(String(value)))
1352
1388
  throw new Error("Value must contain only word characters");
1353
1389
  }
1354
- #validateMaxLength(value, maxLength) {
1390
+ #validateMaxLength(value, context, maxLength) {
1355
1391
  if (value == null || value === "")
1356
1392
  return;
1357
1393
  const length = String(value).length;
1358
1394
  if (length > maxLength)
1359
1395
  throw new Error(`Maximum length is ${maxLength} characters`);
1360
1396
  }
1361
- #validateMaxValue(value, maximum) {
1397
+ #validateMaxValue(value, context, maximum) {
1362
1398
  if (value == null || value === "")
1363
1399
  return;
1364
1400
  const num = Number(value);
@@ -1367,14 +1403,14 @@ class BusinessRuleValidator {
1367
1403
  if (num > maximum)
1368
1404
  throw new Error(`Maximum value is ${maximum}`);
1369
1405
  }
1370
- #validateMinLength(value, minLength) {
1406
+ #validateMinLength(value, context, minLength) {
1371
1407
  if (value == null || value === "")
1372
1408
  return;
1373
1409
  const length = String(value).length;
1374
1410
  if (length < minLength)
1375
1411
  throw new Error(`Minimum length is ${minLength} characters`);
1376
1412
  }
1377
- #validateMinValue(value, minimum) {
1413
+ #validateMinValue(value, context, minimum) {
1378
1414
  if (value == null || value === "")
1379
1415
  return;
1380
1416
  const num = Number(value);
@@ -1383,10 +1419,14 @@ class BusinessRuleValidator {
1383
1419
  if (num < minimum)
1384
1420
  throw new Error(`Minimum value is ${minimum}`);
1385
1421
  }
1386
- #validateNotEmpty(value) {
1387
- if (value == null || value === "" || (typeof value === "string" && value.trim() === ""))
1422
+ #validateRequired(value, context) {
1423
+ if (value == null)
1388
1424
  throw new Error("This field is required");
1389
1425
  }
1426
+ #validateNotEmpty(value, context) {
1427
+ if (value == null || value === "" || (typeof value === "string" && value.trim() === ""))
1428
+ throw new Error("This field cannot be empty");
1429
+ }
1390
1430
  }
1391
1431
 
1392
1432
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vidyano-labs/virtual-service",
3
- "version": "0.1.0",
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": "d6564e9eb23cc8e31609a91945e4ae852ad0b2d9"
23
+ "gitHash": "a8a427169445d844bc42c226c93da215441e6242"
24
24
  }