@vidyano-labs/virtual-service 0.1.1 → 0.3.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 +148 -2
- package/index.d.ts +28 -6
- package/index.js +97 -36
- 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,12 +359,152 @@ 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
|
|
365
398
|
|
|
399
|
+
### Translating Validation Messages
|
|
400
|
+
|
|
401
|
+
Provide custom translations for validation error messages to support multiple languages:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { VirtualService } from "@vidyano-labs/virtual-service";
|
|
405
|
+
|
|
406
|
+
// Define your translations
|
|
407
|
+
const translations: Record<string, string> = {
|
|
408
|
+
"Required": "Dit veld is verplicht",
|
|
409
|
+
"NotEmpty": "Dit veld mag niet leeg zijn",
|
|
410
|
+
"IsEmail": "E-mailformaat is ongeldig",
|
|
411
|
+
"MaxLength": "Maximale lengte is {0} tekens",
|
|
412
|
+
"MinLength": "Minimale lengte is {0} tekens",
|
|
413
|
+
"MaxValue": "Maximale waarde is {0}",
|
|
414
|
+
"MinValue": "Minimale waarde is {0}"
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Create service and register translation function
|
|
418
|
+
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
|
+
|
|
425
|
+
service.registerPersistentObject({
|
|
426
|
+
type: "Person",
|
|
427
|
+
attributes: [
|
|
428
|
+
{
|
|
429
|
+
name: "Email",
|
|
430
|
+
type: "String",
|
|
431
|
+
rules: "Required; MaxLength(100); IsEmail"
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
await service.initialize();
|
|
437
|
+
|
|
438
|
+
const person = await service.getPersistentObject(null, "Person", null, true);
|
|
439
|
+
await person.save();
|
|
440
|
+
|
|
441
|
+
// Error message is now translated
|
|
442
|
+
console.log(person.getAttribute("Email").validationError);
|
|
443
|
+
// "Dit veld is verplicht"
|
|
444
|
+
```
|
|
445
|
+
|
|
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
|
+
**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
|
|
460
|
+
|
|
461
|
+
**Custom rules can also use translation:**
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
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
|
+
|
|
474
|
+
// Custom rule using translation
|
|
475
|
+
service.registerBusinessRule("MinimumAge", (value: any, context: RuleValidationContext, minAge: number) => {
|
|
476
|
+
if (!value)
|
|
477
|
+
return;
|
|
478
|
+
|
|
479
|
+
const age = Number(value);
|
|
480
|
+
if (age < minAge)
|
|
481
|
+
throw new Error(context.translate("MinimumAge", minAge));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Custom rule can still throw direct errors (backward compatible)
|
|
485
|
+
service.registerBusinessRule("IsPhoneNumber", (value: any, _context: RuleValidationContext) => {
|
|
486
|
+
if (!value)
|
|
487
|
+
return;
|
|
488
|
+
|
|
489
|
+
const phoneRegex = /^\+?[\d\s-()]+$/;
|
|
490
|
+
if (!phoneRegex.test(String(value)))
|
|
491
|
+
throw new Error("Invalid phone number format"); // Not translated
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Built-in rule keys:**
|
|
496
|
+
- `Required` - Field is required (no params)
|
|
497
|
+
- `NotEmpty` - Field cannot be empty (no params)
|
|
498
|
+
- `IsEmail` - Invalid email format (no params)
|
|
499
|
+
- `IsUrl` - Invalid URL format (no params)
|
|
500
|
+
- `MaxLength` - Maximum length exceeded (param: max length)
|
|
501
|
+
- `MinLength` - Minimum length not met (param: min length)
|
|
502
|
+
- `MaxValue` - Maximum value exceeded (param: max value)
|
|
503
|
+
- `MinValue` - Minimum value not met (param: min value)
|
|
504
|
+
- `IsBase64` - Invalid base64 string (no params)
|
|
505
|
+
- `IsRegex` - Invalid regex pattern (no params)
|
|
506
|
+
- `IsWord` - Invalid word characters (no params)
|
|
507
|
+
|
|
366
508
|
## Queries
|
|
367
509
|
|
|
368
510
|
### Basic Query Registration
|
|
@@ -1058,6 +1200,7 @@ test("search and sort query results", async () => {
|
|
|
1058
1200
|
| `registerAction(config)` | Register a custom action |
|
|
1059
1201
|
| `registerBusinessRule(name, validator)` | Register a validation rule |
|
|
1060
1202
|
| `registerPersistentObjectActions(type, Class)` | Register lifecycle handlers |
|
|
1203
|
+
| `registerMessageTranslator(translateFn)` | Register message translator for system messages |
|
|
1061
1204
|
| `initialize()` | Finalize registrations |
|
|
1062
1205
|
|
|
1063
1206
|
### VirtualPersistentObjectActions
|
|
@@ -1097,7 +1240,10 @@ import type {
|
|
|
1097
1240
|
ActionHandler,
|
|
1098
1241
|
ActionArgs,
|
|
1099
1242
|
ActionContext,
|
|
1100
|
-
RuleValidatorFn
|
|
1243
|
+
RuleValidatorFn,
|
|
1244
|
+
RuleValidationContext,
|
|
1245
|
+
TranslateFunction,
|
|
1246
|
+
TranslateFunction
|
|
1101
1247
|
} from "@vidyano-labs/virtual-service";
|
|
1102
1248
|
```
|
|
1103
1249
|
|
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
|
*/
|
|
@@ -237,6 +233,17 @@ type ActionContext = {
|
|
|
237
233
|
*/
|
|
238
234
|
setNotification: (message: string, type: Dto.NotificationType, duration?: number) => void;
|
|
239
235
|
};
|
|
236
|
+
/**
|
|
237
|
+
* Translation function for system messages
|
|
238
|
+
* @param key - The message key (e.g., "Required", "MaxLength")
|
|
239
|
+
* @param params - Positional parameters for the message
|
|
240
|
+
* @returns Translated message with parameters interpolated
|
|
241
|
+
*
|
|
242
|
+
* Example:
|
|
243
|
+
* translate("MaxLength", 50) => "Maximum length is 50 characters"
|
|
244
|
+
* translate("MinValue", 18) => "Minimum value is 18"
|
|
245
|
+
*/
|
|
246
|
+
type TranslateFunction = (key: string, ...params: any[]) => string;
|
|
240
247
|
/**
|
|
241
248
|
* Context provided to business rule validators for accessing the persistent object
|
|
242
249
|
*/
|
|
@@ -249,6 +256,10 @@ type RuleValidationContext = {
|
|
|
249
256
|
* The attribute being validated (wrapped with helper methods)
|
|
250
257
|
*/
|
|
251
258
|
attribute: VirtualPersistentObjectAttribute;
|
|
259
|
+
/**
|
|
260
|
+
* Translation function for system messages
|
|
261
|
+
*/
|
|
262
|
+
translate: TranslateFunction;
|
|
252
263
|
};
|
|
253
264
|
/**
|
|
254
265
|
* PersistentObject configuration - converted to PersistentObjectDto
|
|
@@ -464,6 +475,10 @@ declare class VirtualPersistentObjectActions {
|
|
|
464
475
|
declare class VirtualServiceHooks extends ServiceHooks {
|
|
465
476
|
#private;
|
|
466
477
|
constructor();
|
|
478
|
+
/**
|
|
479
|
+
* Sets the translation function for system messages
|
|
480
|
+
*/
|
|
481
|
+
setTranslate(translate: TranslateFunction): void;
|
|
467
482
|
/**
|
|
468
483
|
* Intercepts fetch requests and routes them to virtual handlers
|
|
469
484
|
*/
|
|
@@ -516,7 +531,7 @@ declare class VirtualService extends Service {
|
|
|
516
531
|
#private;
|
|
517
532
|
/**
|
|
518
533
|
* Creates a new VirtualService instance.
|
|
519
|
-
* @param hooks - Optional custom
|
|
534
|
+
* @param hooks - Optional custom hooks instance.
|
|
520
535
|
*/
|
|
521
536
|
constructor(hooks?: VirtualServiceHooks);
|
|
522
537
|
/**
|
|
@@ -566,7 +581,14 @@ declare class VirtualService extends Service {
|
|
|
566
581
|
* @throws Error if called after initialize().
|
|
567
582
|
*/
|
|
568
583
|
registerPersistentObjectActions(type: string, ActionsClass: typeof VirtualPersistentObjectActions): void;
|
|
584
|
+
/**
|
|
585
|
+
* Registers a message translator for translating system messages.
|
|
586
|
+
* Must be called before initialize().
|
|
587
|
+
* @param translate - The translation function.
|
|
588
|
+
* @throws Error if called after initialize().
|
|
589
|
+
*/
|
|
590
|
+
registerMessageTranslator(translate: TranslateFunction): void;
|
|
569
591
|
}
|
|
570
592
|
|
|
571
593
|
export { VirtualPersistentObjectActions, VirtualService, VirtualServiceHooks };
|
|
572
|
-
export type { ActionArgs, ActionConfig, ActionContext, ActionHandler, RuleValidationContext, RuleValidatorFn, VirtualPersistentObject, VirtualPersistentObjectAttribute, VirtualPersistentObjectAttributeConfig, VirtualPersistentObjectConfig, VirtualQueryConfig, VirtualQueryExecuteResult };
|
|
594
|
+
export type { ActionArgs, ActionConfig, ActionContext, ActionHandler, RuleValidationContext, RuleValidatorFn, TranslateFunction, VirtualPersistentObject, VirtualPersistentObjectAttribute, VirtualPersistentObjectAttributeConfig, VirtualPersistentObjectConfig, VirtualQueryConfig, VirtualQueryExecuteResult };
|
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
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
377
|
+
isRequired,
|
|
359
378
|
isReadOnly: config.isReadOnly || false,
|
|
360
379
|
rules: config.rules,
|
|
361
380
|
visibility: config.visibility || "Always",
|
|
@@ -1209,7 +1228,9 @@ class VirtualPersistentObjectActionsRegistry {
|
|
|
1209
1228
|
class BusinessRuleValidator {
|
|
1210
1229
|
#builtInRules = new Map();
|
|
1211
1230
|
#customRules = new Map();
|
|
1231
|
+
#translate;
|
|
1212
1232
|
constructor() {
|
|
1233
|
+
this.#translate = this.#defaultTranslate;
|
|
1213
1234
|
// Register all built-in rules
|
|
1214
1235
|
this.#builtInRules.set("IsBase64", this.#validateIsBase64.bind(this));
|
|
1215
1236
|
this.#builtInRules.set("IsEmail", this.#validateIsEmail.bind(this));
|
|
@@ -1221,7 +1242,33 @@ class BusinessRuleValidator {
|
|
|
1221
1242
|
this.#builtInRules.set("MinLength", this.#validateMinLength.bind(this));
|
|
1222
1243
|
this.#builtInRules.set("MinValue", this.#validateMinValue.bind(this));
|
|
1223
1244
|
this.#builtInRules.set("NotEmpty", this.#validateNotEmpty.bind(this));
|
|
1224
|
-
this.#builtInRules.set("Required", this.#
|
|
1245
|
+
this.#builtInRules.set("Required", this.#validateRequired.bind(this));
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Sets the translation function
|
|
1249
|
+
*/
|
|
1250
|
+
setTranslate(translate) {
|
|
1251
|
+
this.#translate = translate;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Default English translations - used when no translate function provided
|
|
1255
|
+
*/
|
|
1256
|
+
#defaultTranslate(key, ...params) {
|
|
1257
|
+
const defaults = {
|
|
1258
|
+
"Required": () => "This field is required",
|
|
1259
|
+
"NotEmpty": () => "This field cannot be empty",
|
|
1260
|
+
"IsEmail": () => "Email format is invalid",
|
|
1261
|
+
"IsUrl": () => "Value must be a valid URL",
|
|
1262
|
+
"MaxLength": (max) => `Maximum length is ${max} characters`,
|
|
1263
|
+
"MinLength": (min) => `Minimum length is ${min} characters`,
|
|
1264
|
+
"MaxValue": (max) => `Maximum value is ${max}`,
|
|
1265
|
+
"MinValue": (min) => `Minimum value is ${min}`,
|
|
1266
|
+
"IsBase64": () => "Value must be a valid base64 string",
|
|
1267
|
+
"IsRegex": () => "Value must be a valid regular expression",
|
|
1268
|
+
"IsWord": () => "Value must contain only word characters"
|
|
1269
|
+
};
|
|
1270
|
+
const translator = defaults[key];
|
|
1271
|
+
return translator ? translator(...params) : key;
|
|
1225
1272
|
}
|
|
1226
1273
|
/**
|
|
1227
1274
|
* Register a custom business rule
|
|
@@ -1253,20 +1300,14 @@ class BusinessRuleValidator {
|
|
|
1253
1300
|
// Create validation context
|
|
1254
1301
|
const context = {
|
|
1255
1302
|
persistentObject: wrappedPo,
|
|
1256
|
-
attribute: wrappedAttr
|
|
1303
|
+
attribute: wrappedAttr,
|
|
1304
|
+
translate: this.#translate
|
|
1257
1305
|
};
|
|
1258
1306
|
// Get the converted value (e.g., boolean from "True"/"False", number from string)
|
|
1259
1307
|
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
1308
|
// Parse and validate rules string
|
|
1309
|
+
// Note: isRequired is auto-set from rules for UI purposes only (e.g., showing asterisks)
|
|
1310
|
+
// All validation logic is handled by the rules themselves
|
|
1270
1311
|
if (!attr.rules)
|
|
1271
1312
|
return null;
|
|
1272
1313
|
const rules = this.parseRules(attr.rules);
|
|
@@ -1333,83 +1374,87 @@ class BusinessRuleValidator {
|
|
|
1333
1374
|
return fromServiceValue(attr.value, attr.type);
|
|
1334
1375
|
}
|
|
1335
1376
|
// Built-in validators - throw errors instead of returning strings
|
|
1336
|
-
#validateIsBase64(value,
|
|
1377
|
+
#validateIsBase64(value, _context) {
|
|
1337
1378
|
if (value == null || value === "")
|
|
1338
1379
|
return;
|
|
1339
1380
|
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
1340
1381
|
if (!base64Regex.test(String(value)))
|
|
1341
|
-
throw new Error("
|
|
1382
|
+
throw new Error(this.#translate("IsBase64"));
|
|
1342
1383
|
}
|
|
1343
|
-
#validateIsEmail(value,
|
|
1384
|
+
#validateIsEmail(value, _context) {
|
|
1344
1385
|
if (value == null || value === "")
|
|
1345
1386
|
return;
|
|
1346
1387
|
// Only allow ASCII characters in email addresses
|
|
1347
1388
|
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
1348
1389
|
if (!emailRegex.test(String(value)))
|
|
1349
|
-
throw new Error("
|
|
1390
|
+
throw new Error(this.#translate("IsEmail"));
|
|
1350
1391
|
}
|
|
1351
|
-
#validateIsRegex(value,
|
|
1392
|
+
#validateIsRegex(value, _context) {
|
|
1352
1393
|
if (value == null || value === "")
|
|
1353
1394
|
return;
|
|
1354
1395
|
try {
|
|
1355
1396
|
new RegExp(String(value));
|
|
1356
1397
|
}
|
|
1357
1398
|
catch {
|
|
1358
|
-
throw new Error("
|
|
1399
|
+
throw new Error(this.#translate("IsRegex"));
|
|
1359
1400
|
}
|
|
1360
1401
|
}
|
|
1361
|
-
#validateIsUrl(value,
|
|
1402
|
+
#validateIsUrl(value, _context) {
|
|
1362
1403
|
if (value == null || value === "")
|
|
1363
1404
|
return;
|
|
1364
1405
|
try {
|
|
1365
1406
|
new URL(String(value));
|
|
1366
1407
|
}
|
|
1367
1408
|
catch {
|
|
1368
|
-
throw new Error("
|
|
1409
|
+
throw new Error(this.#translate("IsUrl"));
|
|
1369
1410
|
}
|
|
1370
1411
|
}
|
|
1371
|
-
#validateIsWord(value,
|
|
1412
|
+
#validateIsWord(value, _context) {
|
|
1372
1413
|
if (value == null || value === "")
|
|
1373
1414
|
return;
|
|
1374
1415
|
const wordRegex = /^\w+$/;
|
|
1375
1416
|
if (!wordRegex.test(String(value)))
|
|
1376
|
-
throw new Error("
|
|
1417
|
+
throw new Error(this.#translate("IsWord"));
|
|
1377
1418
|
}
|
|
1378
|
-
#validateMaxLength(value,
|
|
1419
|
+
#validateMaxLength(value, _context, maxLength) {
|
|
1379
1420
|
if (value == null || value === "")
|
|
1380
1421
|
return;
|
|
1381
1422
|
const length = String(value).length;
|
|
1382
1423
|
if (length > maxLength)
|
|
1383
|
-
throw new Error(
|
|
1424
|
+
throw new Error(this.#translate("MaxLength", maxLength));
|
|
1384
1425
|
}
|
|
1385
|
-
#validateMaxValue(value,
|
|
1426
|
+
#validateMaxValue(value, _context, maximum) {
|
|
1386
1427
|
if (value == null || value === "")
|
|
1387
1428
|
return;
|
|
1388
1429
|
const num = Number(value);
|
|
1389
1430
|
if (isNaN(num))
|
|
1390
1431
|
throw new Error("Value must be a number");
|
|
1391
1432
|
if (num > maximum)
|
|
1392
|
-
throw new Error(
|
|
1433
|
+
throw new Error(this.#translate("MaxValue", maximum));
|
|
1393
1434
|
}
|
|
1394
|
-
#validateMinLength(value,
|
|
1435
|
+
#validateMinLength(value, _context, minLength) {
|
|
1395
1436
|
if (value == null || value === "")
|
|
1396
1437
|
return;
|
|
1397
1438
|
const length = String(value).length;
|
|
1398
1439
|
if (length < minLength)
|
|
1399
|
-
throw new Error(
|
|
1440
|
+
throw new Error(this.#translate("MinLength", minLength));
|
|
1400
1441
|
}
|
|
1401
|
-
#validateMinValue(value,
|
|
1442
|
+
#validateMinValue(value, _context, minimum) {
|
|
1402
1443
|
if (value == null || value === "")
|
|
1403
1444
|
return;
|
|
1404
1445
|
const num = Number(value);
|
|
1405
1446
|
if (isNaN(num))
|
|
1406
1447
|
throw new Error("Value must be a number");
|
|
1407
1448
|
if (num < minimum)
|
|
1408
|
-
throw new Error(
|
|
1449
|
+
throw new Error(this.#translate("MinValue", minimum));
|
|
1409
1450
|
}
|
|
1410
|
-
#
|
|
1451
|
+
#validateRequired(value, _context) {
|
|
1452
|
+
if (value == null)
|
|
1453
|
+
throw new Error(this.#translate("Required"));
|
|
1454
|
+
}
|
|
1455
|
+
#validateNotEmpty(value, _context) {
|
|
1411
1456
|
if (value == null || value === "" || (typeof value === "string" && value.trim() === ""))
|
|
1412
|
-
throw new Error("
|
|
1457
|
+
throw new Error(this.#translate("NotEmpty"));
|
|
1413
1458
|
}
|
|
1414
1459
|
}
|
|
1415
1460
|
|
|
@@ -1445,6 +1490,12 @@ class VirtualServiceHooks extends ServiceHooks {
|
|
|
1445
1490
|
this.#actionDefinitions.set("Save", { name: "Save", displayName: "Save", isPinned: false });
|
|
1446
1491
|
this.#actionDefinitions.set("SelectReference", { name: "SelectReference", displayName: "Select", isPinned: false });
|
|
1447
1492
|
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Sets the translation function for system messages
|
|
1495
|
+
*/
|
|
1496
|
+
setTranslate(translate) {
|
|
1497
|
+
this.#validator.setTranslate(translate);
|
|
1498
|
+
}
|
|
1448
1499
|
/**
|
|
1449
1500
|
* Intercepts fetch requests and routes them to virtual handlers
|
|
1450
1501
|
*/
|
|
@@ -1953,7 +2004,7 @@ class VirtualService extends Service {
|
|
|
1953
2004
|
#isInitialized = false;
|
|
1954
2005
|
/**
|
|
1955
2006
|
* Creates a new VirtualService instance.
|
|
1956
|
-
* @param hooks - Optional custom
|
|
2007
|
+
* @param hooks - Optional custom hooks instance.
|
|
1957
2008
|
*/
|
|
1958
2009
|
constructor(hooks) {
|
|
1959
2010
|
super("http://virtual.local", hooks ?? new VirtualServiceHooks(), true);
|
|
@@ -2020,6 +2071,16 @@ class VirtualService extends Service {
|
|
|
2020
2071
|
this.#ensureNotInitialized();
|
|
2021
2072
|
this.virtualHooks.registerPersistentObjectActions(type, ActionsClass);
|
|
2022
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Registers a message translator for translating system messages.
|
|
2076
|
+
* Must be called before initialize().
|
|
2077
|
+
* @param translate - The translation function.
|
|
2078
|
+
* @throws Error if called after initialize().
|
|
2079
|
+
*/
|
|
2080
|
+
registerMessageTranslator(translate) {
|
|
2081
|
+
this.#ensureNotInitialized();
|
|
2082
|
+
this.virtualHooks.setTranslate(translate);
|
|
2083
|
+
}
|
|
2023
2084
|
/**
|
|
2024
2085
|
* Throws an error if the service has already been initialized.
|
|
2025
2086
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vidyano-labs/virtual-service",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": "
|
|
23
|
+
"gitHash": "db4cd7380e2b729aedf28a81ab348823aa338c4b"
|
|
24
24
|
}
|