@qubit-ltd/common-decorator 3.8.11 → 3.9.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.
- package/README.md +656 -11
- package/README.zh_CN.md +455 -7
- package/dist/common-decorator.cjs +10651 -4508
- package/dist/common-decorator.cjs.map +1 -1
- package/dist/common-decorator.min.cjs +1 -1
- package/dist/common-decorator.min.cjs.map +1 -1
- package/dist/common-decorator.min.mjs +1 -1
- package/dist/common-decorator.min.mjs.map +1 -1
- package/dist/common-decorator.mjs +10643 -4500
- package/dist/common-decorator.mjs.map +1 -1
- package/doc/api/DefaultAssignmentOptions.html +2 -2
- package/doc/api/DefaultOptions.html +2 -2
- package/doc/api/DefaultToJsonOptions.html +2 -2
- package/doc/api/Enum.html +2 -2
- package/doc/api/Model.html +2 -2
- package/doc/api/Page.html +2 -2
- package/doc/api/global.html +703 -938
- package/doc/api/index.html +630 -13
- package/doc/common-decorator.min.visualization.html +1 -1
- package/doc/common-decorator.visualization.html +1 -1
- package/package.json +31 -26
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# js-common-decorator
|
|
2
2
|
|
|
3
3
|
[](https://npmjs.com/package/@qubit-ltd/common-decorator)
|
|
4
4
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
@@ -6,10 +6,35 @@
|
|
|
6
6
|
[](https://dl.circleci.com/status-badge/redirect/gh/Haixing-Hu/js-common-decorator/tree/master)
|
|
7
7
|
[](https://coveralls.io/github/Haixing-Hu/js-common-decorator?branch=master)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
supports the most recent (
|
|
12
|
-
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
[@qubit-ltd/common-decorator] is a JavaScript library of common decorators that provides powerful tools to enhance your domain classes. The library supports the most recent (as of November 2023) [stage 3 proposal of JavaScript decorators].
|
|
12
|
+
|
|
13
|
+
With this library, you can easily add common methods to your domain classes, implement Java-like enum functionality, add validation and normalization capabilities, and much more - all using the modern decorator syntax.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Modern Decorator Support**: Compatible with the latest Stage 3 proposal for JavaScript decorators
|
|
18
|
+
- **Model Enhancement**: `@Model` decorator adds common methods to domain model classes
|
|
19
|
+
- **Enum Implementation**: `@Enum` decorator provides Java-like enumeration capabilities
|
|
20
|
+
- **Validation Support**: `@Validatable` decorator enables field validation
|
|
21
|
+
- **Normalization Support**: `@Normalizable` decorator enables field normalization
|
|
22
|
+
- **Type Safety**: `@Type` and `@ElementType` decorators for type checking
|
|
23
|
+
- **Serialization Utilities**: Built-in JSON serialization/deserialization support
|
|
24
|
+
- **High Test Coverage**: Comprehensive test suite ensuring reliability of all features
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Using npm
|
|
30
|
+
npm install @qubit-ltd/common-decorator
|
|
31
|
+
|
|
32
|
+
# Using yarn
|
|
33
|
+
yarn add @qubit-ltd/common-decorator
|
|
34
|
+
|
|
35
|
+
# Using pnpm
|
|
36
|
+
pnpm add @qubit-ltd/common-decorator
|
|
37
|
+
```
|
|
13
38
|
|
|
14
39
|
## <span id="content">Table of Contents</span>
|
|
15
40
|
|
|
@@ -54,6 +79,7 @@ supports the most recent (currently May 2023)
|
|
|
54
79
|
- [Configuration](#configuration)
|
|
55
80
|
- [Bundling with webpack](#webpack)
|
|
56
81
|
- [Bundling with vite](#vite)
|
|
82
|
+
- [Recent Updates](#recent-updates)
|
|
57
83
|
- [Contributing](#contributing)
|
|
58
84
|
- [License](#license)
|
|
59
85
|
|
|
@@ -1004,14 +1030,14 @@ expect(opt3.convertNaming).toBe(false);
|
|
|
1004
1030
|
|
|
1005
1031
|
## <span id="configuration">Configuration</span>
|
|
1006
1032
|
|
|
1007
|
-
This library uses the most recent (currently
|
|
1033
|
+
This library uses the most recent (currently November 2023)
|
|
1008
1034
|
[stage 3 proposal of JavaScript decorators]. Therefore, you must configure
|
|
1009
1035
|
[Babel] with [@babel/plugin-transform-class-properties] and the
|
|
1010
1036
|
[@babel/plugin-proposal-decorators] plugins.
|
|
1011
1037
|
|
|
1012
|
-
**NOTE:** To support the [stage 3 proposal of JavaScript decorator metadata]
|
|
1013
|
-
the version of the [Babel] plugin [@babel/plugin-proposal-decorators]
|
|
1014
|
-
at least `7.
|
|
1038
|
+
**NOTE:** To support the [stage 3 proposal of JavaScript decorator metadata]
|
|
1039
|
+
at November 2023, the version of the [Babel] plugin [@babel/plugin-proposal-decorators]
|
|
1040
|
+
must be at least `7.24.0`.
|
|
1015
1041
|
|
|
1016
1042
|
### <span id="webpack">Bundling with [webpack]</span>
|
|
1017
1043
|
|
|
@@ -1031,7 +1057,7 @@ at least `7.23.0`.
|
|
|
1031
1057
|
],
|
|
1032
1058
|
"plugins": [
|
|
1033
1059
|
"@babel/plugin-transform-runtime",
|
|
1034
|
-
["@babel/plugin-proposal-decorators", { "version": "2023-
|
|
1060
|
+
["@babel/plugin-proposal-decorators", { "version": "2023-11" }],
|
|
1035
1061
|
"@babel/plugin-transform-class-properties"
|
|
1036
1062
|
]
|
|
1037
1063
|
}
|
|
@@ -1055,7 +1081,7 @@ at least `7.23.0`.
|
|
|
1055
1081
|
],
|
|
1056
1082
|
"plugins": [
|
|
1057
1083
|
"@babel/plugin-transform-runtime",
|
|
1058
|
-
["@babel/plugin-proposal-decorators", { "version": "2023-
|
|
1084
|
+
["@babel/plugin-proposal-decorators", { "version": "2023-11" }],
|
|
1059
1085
|
"@babel/plugin-transform-class-properties"
|
|
1060
1086
|
]
|
|
1061
1087
|
}
|
|
@@ -1132,6 +1158,20 @@ at least `7.23.0`.
|
|
|
1132
1158
|
});
|
|
1133
1159
|
```
|
|
1134
1160
|
|
|
1161
|
+
## <span id="recent-updates">Recent Updates</span>
|
|
1162
|
+
|
|
1163
|
+
### Test Coverage Enhancements (December 2023)
|
|
1164
|
+
|
|
1165
|
+
- **Enum Clone Hook Test**: Added comprehensive tests to verify the behavior of enum clone hooks, ensuring that enumerator objects are properly handled during cloning operations.
|
|
1166
|
+
- **Field Validation Coverage**: Enhanced test coverage for field validation, specifically targeting edge cases to ensure robust validation in all scenarios.
|
|
1167
|
+
- **Model Implementation Tests**: Added additional tests for various model implementation functions to achieve higher code coverage.
|
|
1168
|
+
|
|
1169
|
+
The latest updates focus on improving test coverage and reliability, with particular attention to:
|
|
1170
|
+
|
|
1171
|
+
- Ensuring enumerator objects maintain their singleton pattern during cloning
|
|
1172
|
+
- Validating empty fields with proper error handling
|
|
1173
|
+
- Verifying that model implementation functions work correctly in all edge cases
|
|
1174
|
+
|
|
1135
1175
|
## <span id="contributing">Contributing</span>
|
|
1136
1176
|
|
|
1137
1177
|
If you find any issues or have suggestions for improvements, please feel free
|
|
@@ -1154,3 +1194,608 @@ See the [LICENSE](LICENSE) file for more details.
|
|
|
1154
1194
|
[vite-plugin-vue]: https://www.npmjs.com/package/@vitejs/plugin-vue
|
|
1155
1195
|
[vite-plugin-babel]: https://www.npmjs.com/package/vite-plugin-babel
|
|
1156
1196
|
[our version of vite-plugin-babel]: https://npmjs.com/package/@qubit-ltd/vite-plugin-babel
|
|
1197
|
+
|
|
1198
|
+
## Front-end and Back-end Data Exchange Example
|
|
1199
|
+
|
|
1200
|
+
In real-world applications, data exchange between front-end and back-end is a common scenario. The following example demonstrates how to use this library to process data from RESTful APIs and send it back to the server.
|
|
1201
|
+
|
|
1202
|
+
### Complete Example
|
|
1203
|
+
|
|
1204
|
+
Suppose we have an e-commerce application that needs to handle order data. The back-end API returns data in snake_case naming convention (e.g., `order_id`), while the front-end code uses camelCase (e.g., `orderId`). Additionally, order IDs use Java's Long type (which may exceed JavaScript's safe integer range).
|
|
1205
|
+
|
|
1206
|
+
First, let's define our domain models:
|
|
1207
|
+
|
|
1208
|
+
```javascript
|
|
1209
|
+
import { Model, Type, ElementType, Normalizable, Validatable, NonEmpty } from '@qubit-ltd/common-decorator';
|
|
1210
|
+
|
|
1211
|
+
// Configure default options for naming style conversion
|
|
1212
|
+
DefaultOptions.set('assign', {
|
|
1213
|
+
normalize: true,
|
|
1214
|
+
convertNaming: true,
|
|
1215
|
+
sourceNamingStyle: 'LOWER_UNDERSCORE', // JSON data format from back-end
|
|
1216
|
+
targetNamingStyle: 'LOWER_CAMEL' // Format used in front-end
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
DefaultOptions.set('toJSON', {
|
|
1220
|
+
normalize: true,
|
|
1221
|
+
removeEmptyFields: true, // Automatically remove empty fields
|
|
1222
|
+
convertNaming: true,
|
|
1223
|
+
sourceNamingStyle: 'LOWER_CAMEL', // Format used in front-end
|
|
1224
|
+
targetNamingStyle: 'LOWER_UNDERSCORE' // Format to send to back-end
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// Define the OrderItem model
|
|
1228
|
+
@Model
|
|
1229
|
+
class OrderItem {
|
|
1230
|
+
constructor() {
|
|
1231
|
+
this.id = null; // Java Long type, automatically converted to BigInt
|
|
1232
|
+
this.productId = null; // Front-end uses camelCase, corresponding to product_id in back-end
|
|
1233
|
+
this.productName = '';
|
|
1234
|
+
this.quantity = 0;
|
|
1235
|
+
this.unitPrice = 0;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
@Normalizable
|
|
1239
|
+
@Validatable
|
|
1240
|
+
@Type(String)
|
|
1241
|
+
get productName() {
|
|
1242
|
+
return this._productName;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
set productName(value) {
|
|
1246
|
+
this._productName = value;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
@Normalizable
|
|
1250
|
+
@Validatable
|
|
1251
|
+
@Type(Number)
|
|
1252
|
+
get quantity() {
|
|
1253
|
+
return this._quantity;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
set quantity(value) {
|
|
1257
|
+
this._quantity = value;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
@Normalizable
|
|
1261
|
+
@Validatable
|
|
1262
|
+
@Type(Number)
|
|
1263
|
+
get unitPrice() {
|
|
1264
|
+
return this._unitPrice;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
set unitPrice(value) {
|
|
1268
|
+
this._unitPrice = value;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Calculate total price
|
|
1272
|
+
getTotalPrice() {
|
|
1273
|
+
return this.quantity * this.unitPrice;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Define the Order model
|
|
1278
|
+
@Model
|
|
1279
|
+
class Order {
|
|
1280
|
+
constructor() {
|
|
1281
|
+
this.id = null; // Java Long type, automatically converted to BigInt
|
|
1282
|
+
this.orderNumber = ''; // Front-end uses camelCase, corresponding to order_number in back-end
|
|
1283
|
+
this.customerId = null;
|
|
1284
|
+
this.customerName = '';
|
|
1285
|
+
this.orderDate = null;
|
|
1286
|
+
this.orderItems = []; // Array of order items
|
|
1287
|
+
this.totalAmount = 0;
|
|
1288
|
+
this.status = '';
|
|
1289
|
+
this.note = ''; // Optional field, empty values will be removed when sent to back-end
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
@Normalizable
|
|
1293
|
+
@Validatable
|
|
1294
|
+
@NonEmpty
|
|
1295
|
+
@Type(String)
|
|
1296
|
+
get orderNumber() {
|
|
1297
|
+
return this._orderNumber;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
set orderNumber(value) {
|
|
1301
|
+
this._orderNumber = value;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
@Normalizable
|
|
1305
|
+
@Validatable
|
|
1306
|
+
@Type(String)
|
|
1307
|
+
get customerName() {
|
|
1308
|
+
return this._customerName;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
set customerName(value) {
|
|
1312
|
+
this._customerName = value;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
@Normalizable
|
|
1316
|
+
@Validatable
|
|
1317
|
+
@Type(Date)
|
|
1318
|
+
get orderDate() {
|
|
1319
|
+
return this._orderDate;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
set orderDate(value) {
|
|
1323
|
+
this._orderDate = value;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
@Normalizable
|
|
1327
|
+
@Validatable
|
|
1328
|
+
@ElementType(OrderItem)
|
|
1329
|
+
get orderItems() {
|
|
1330
|
+
return this._orderItems;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
set orderItems(value) {
|
|
1334
|
+
this._orderItems = value;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Example usage - Fetching data from back-end
|
|
1339
|
+
async function fetchOrder(orderId) {
|
|
1340
|
+
try {
|
|
1341
|
+
// Assume this is the response from a back-end API
|
|
1342
|
+
const response = await fetch(`/api/orders/${orderId}`);
|
|
1343
|
+
const data = await response.json();
|
|
1344
|
+
|
|
1345
|
+
// Sample data (in snake_case naming style)
|
|
1346
|
+
// {
|
|
1347
|
+
// "id": "9223372036854775807", // Note: Max value of Java Long, exceeds JS Number safe range
|
|
1348
|
+
// "order_number": "ORD-2023-001",
|
|
1349
|
+
// "customer_id": "5678",
|
|
1350
|
+
// "customer_name": "John Doe",
|
|
1351
|
+
// "order_date": "2023-08-15T14:30:00.000Z",
|
|
1352
|
+
// "order_items": [
|
|
1353
|
+
// {
|
|
1354
|
+
// "id": "8345678912345678901", // Another large integer
|
|
1355
|
+
// "product_id": "101",
|
|
1356
|
+
// "product_name": "Laptop",
|
|
1357
|
+
// "quantity": 1,
|
|
1358
|
+
// "unit_price": 999.99
|
|
1359
|
+
// },
|
|
1360
|
+
// {
|
|
1361
|
+
// "id": "8345678912345678902",
|
|
1362
|
+
// "product_id": "202",
|
|
1363
|
+
// "product_name": "Wireless Mouse",
|
|
1364
|
+
// "quantity": 2,
|
|
1365
|
+
// "unit_price": 29.99
|
|
1366
|
+
// }
|
|
1367
|
+
// ],
|
|
1368
|
+
// "total_amount": 1059.97,
|
|
1369
|
+
// "status": "PENDING",
|
|
1370
|
+
// "note": null
|
|
1371
|
+
// }
|
|
1372
|
+
|
|
1373
|
+
// Create domain object using Order.create(), automatically handling naming style conversion and large integers
|
|
1374
|
+
const order = Order.create(data);
|
|
1375
|
+
|
|
1376
|
+
console.log(order.id); // Output: 9223372036854775807n (BigInt type)
|
|
1377
|
+
console.log(order.orderNumber); // Output: "ORD-2023-001" (converted to camelCase)
|
|
1378
|
+
console.log(order.orderItems[0].productName); // Output: "Laptop"
|
|
1379
|
+
|
|
1380
|
+
// Validation and normalization
|
|
1381
|
+
order.normalize();
|
|
1382
|
+
const validationResult = order.validate();
|
|
1383
|
+
if (!validationResult.valid) {
|
|
1384
|
+
console.error('Order data validation failed:', validationResult.message);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return order;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
console.error('Failed to fetch order:', error);
|
|
1390
|
+
throw error;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Sending data back to the back-end
|
|
1395
|
+
async function updateOrder(order) {
|
|
1396
|
+
try {
|
|
1397
|
+
// Use toJSON to convert domain object to plain JavaScript object
|
|
1398
|
+
// Automatically handles: 1. Naming style conversion 2. Empty field removal 3. BigInt conversion
|
|
1399
|
+
const orderData = order.toJSON();
|
|
1400
|
+
|
|
1401
|
+
// Example of converted data format:
|
|
1402
|
+
// {
|
|
1403
|
+
// "id": "9223372036854775807", // BigInt converted to string, without 'n' suffix
|
|
1404
|
+
// "order_number": "ORD-2023-001",
|
|
1405
|
+
// "customer_id": "5678",
|
|
1406
|
+
// "customer_name": "John Doe",
|
|
1407
|
+
// "order_date": "2023-08-15T14:30:00.000Z",
|
|
1408
|
+
// "order_items": [
|
|
1409
|
+
// {
|
|
1410
|
+
// "id": "8345678912345678901",
|
|
1411
|
+
// "product_id": "101",
|
|
1412
|
+
// "product_name": "Laptop",
|
|
1413
|
+
// "quantity": 1,
|
|
1414
|
+
// "unit_price": 999.99
|
|
1415
|
+
// },
|
|
1416
|
+
// // ...other order items
|
|
1417
|
+
// ],
|
|
1418
|
+
// "total_amount": 1059.97,
|
|
1419
|
+
// "status": "PENDING"
|
|
1420
|
+
// // Note: 'note' field was null and has been automatically removed
|
|
1421
|
+
// }
|
|
1422
|
+
|
|
1423
|
+
const response = await fetch(`/api/orders/${order.id}`, {
|
|
1424
|
+
method: 'PUT',
|
|
1425
|
+
headers: {
|
|
1426
|
+
'Content-Type': 'application/json'
|
|
1427
|
+
},
|
|
1428
|
+
body: JSON.stringify(orderData) // No additional processing needed, already in the right format
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
if (!response.ok) {
|
|
1432
|
+
throw new Error(`Failed to update order: ${response.status}`);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return await response.json();
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
console.error('Failed to update order:', error);
|
|
1438
|
+
throw error;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Example usage
|
|
1443
|
+
async function example() {
|
|
1444
|
+
// Fetch order data and modify it
|
|
1445
|
+
const order = await fetchOrder('123');
|
|
1446
|
+
order.status = 'COMPLETED';
|
|
1447
|
+
order.note = ''; // Empty string, will be removed when sent to back-end
|
|
1448
|
+
|
|
1449
|
+
// Add a new item to the order
|
|
1450
|
+
const newItem = new OrderItem();
|
|
1451
|
+
newItem.productId = '303';
|
|
1452
|
+
newItem.productName = 'Headphones';
|
|
1453
|
+
newItem.quantity = 1;
|
|
1454
|
+
newItem.unitPrice = 79.99;
|
|
1455
|
+
order.orderItems.push(newItem);
|
|
1456
|
+
|
|
1457
|
+
// Recalculate total amount
|
|
1458
|
+
order.totalAmount = order.orderItems.reduce(
|
|
1459
|
+
(sum, item) => sum + item.getTotalPrice(), 0
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
// Send the updated order back to the server
|
|
1463
|
+
await updateOrder(order);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
### Key Points
|
|
1469
|
+
|
|
1470
|
+
1. **Automatic Naming Style Conversion**:
|
|
1471
|
+
- Data from the back-end uses snake_case format (e.g., `order_number`)
|
|
1472
|
+
- Front-end domain objects use camelCase format (e.g., `orderNumber`)
|
|
1473
|
+
- Through `DefaultOptions.set('assign', {...})` configuration, `Model.create()` and `model.assign()` automatically convert naming styles
|
|
1474
|
+
- Through `DefaultOptions.set('toJSON', {...})` configuration, `model.toJSON()` automatically converts camelCase back to snake_case
|
|
1475
|
+
|
|
1476
|
+
2. **Large Integer Handling**:
|
|
1477
|
+
- Back-end uses Java Long type IDs (e.g., `9223372036854775807`) that exceed JavaScript's safe integer range
|
|
1478
|
+
- `Model.create()` and `model.assign()` automatically convert these large integers to JavaScript's BigInt type
|
|
1479
|
+
- `model.toJSON()` automatically converts BigInt to the correct JSON format (without the 'n' suffix)
|
|
1480
|
+
|
|
1481
|
+
3. **Empty Field Handling**:
|
|
1482
|
+
- With `removeEmptyFields: true` configuration, `model.toJSON()` automatically removes null, undefined, and empty string properties
|
|
1483
|
+
- In the example, the 'note' field with null or empty string value won't be sent to the back-end
|
|
1484
|
+
|
|
1485
|
+
4. **Type Conversion and Validation**:
|
|
1486
|
+
- Use `@Type` and `@ElementType` decorators to ensure type safety
|
|
1487
|
+
- Use `model.normalize()` for data normalization
|
|
1488
|
+
- Use `model.validate()` to validate data integrity
|
|
1489
|
+
|
|
1490
|
+
This complete example demonstrates how to use the features of this library to easily handle various challenges in front-end and back-end data exchange in real-world applications.
|
|
1491
|
+
|
|
1492
|
+
## Advanced Usage
|
|
1493
|
+
|
|
1494
|
+
### Combining Multiple Decorators
|
|
1495
|
+
|
|
1496
|
+
You can combine multiple decorators to add rich functionality to your classes:
|
|
1497
|
+
|
|
1498
|
+
```javascript
|
|
1499
|
+
import {
|
|
1500
|
+
Model,
|
|
1501
|
+
Type,
|
|
1502
|
+
ElementType,
|
|
1503
|
+
Normalizable,
|
|
1504
|
+
Validatable,
|
|
1505
|
+
NonEmpty
|
|
1506
|
+
} from '@qubit-ltd/common-decorator';
|
|
1507
|
+
|
|
1508
|
+
@Model
|
|
1509
|
+
class Product {
|
|
1510
|
+
constructor() {
|
|
1511
|
+
this.id = null;
|
|
1512
|
+
this.name = '';
|
|
1513
|
+
this.price = 0;
|
|
1514
|
+
this.tags = [];
|
|
1515
|
+
this.createdAt = null;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
@NonEmpty
|
|
1519
|
+
@Validatable
|
|
1520
|
+
@Normalizable
|
|
1521
|
+
@Type(String)
|
|
1522
|
+
get name() {
|
|
1523
|
+
return this._name;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
set name(value) {
|
|
1527
|
+
this._name = value;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
@Validatable
|
|
1531
|
+
@Normalizable
|
|
1532
|
+
@Type(Number)
|
|
1533
|
+
get price() {
|
|
1534
|
+
return this._price;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
set price(value) {
|
|
1538
|
+
this._price = value;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
@Normalizable
|
|
1542
|
+
@ElementType(String)
|
|
1543
|
+
get tags() {
|
|
1544
|
+
return this._tags;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
set tags(value) {
|
|
1548
|
+
this._tags = value;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Usage
|
|
1553
|
+
const product = new Product();
|
|
1554
|
+
product.assign({
|
|
1555
|
+
name: ' Product Name ',
|
|
1556
|
+
price: '99.99',
|
|
1557
|
+
tags: ['tag1', 2, 'tag3']
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// After normalization, product.name will be trimmed,
|
|
1561
|
+
// product.price will be a Number,
|
|
1562
|
+
// and all elements in product.tags will be strings
|
|
1563
|
+
product.normalize();
|
|
1564
|
+
|
|
1565
|
+
console.log(product.validate()); // Checks if name is not empty and all types match
|
|
1566
|
+
```
|
|
1567
|
+
|
|
1568
|
+
### Custom Validation and Normalization
|
|
1569
|
+
|
|
1570
|
+
You can implement custom validation and normalization logic:
|
|
1571
|
+
|
|
1572
|
+
```javascript
|
|
1573
|
+
import { Model, Normalizable, Validatable } from '@qubit-ltd/common-decorator';
|
|
1574
|
+
|
|
1575
|
+
@Model
|
|
1576
|
+
class EmailSubscription {
|
|
1577
|
+
constructor() {
|
|
1578
|
+
this.email = '';
|
|
1579
|
+
this.subscribed = false;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
@Normalizable((value) => {
|
|
1583
|
+
// Custom normalizer that converts email to lowercase and trims whitespace
|
|
1584
|
+
return typeof value === 'string' ? value.toLowerCase().trim() : value;
|
|
1585
|
+
})
|
|
1586
|
+
@Validatable((value) => {
|
|
1587
|
+
// Custom validator that checks if email is valid
|
|
1588
|
+
if (typeof value !== 'string' || !value.includes('@')) {
|
|
1589
|
+
return {
|
|
1590
|
+
valid: false,
|
|
1591
|
+
message: 'Invalid email address',
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
return { valid: true };
|
|
1595
|
+
})
|
|
1596
|
+
get email() {
|
|
1597
|
+
return this._email;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
set email(value) {
|
|
1601
|
+
this._email = value;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
### Working with DefaultOptions
|
|
1607
|
+
|
|
1608
|
+
You can configure default options for various operations:
|
|
1609
|
+
|
|
1610
|
+
```javascript
|
|
1611
|
+
import { DefaultOptions, Model } from '@qubit-ltd/common-decorator';
|
|
1612
|
+
|
|
1613
|
+
// Configure default options for JSON serialization
|
|
1614
|
+
DefaultOptions.set('toJSON', {
|
|
1615
|
+
normalize: true,
|
|
1616
|
+
removeEmptyFields: true,
|
|
1617
|
+
convertNaming: true,
|
|
1618
|
+
sourceNamingStyle: 'LOWER_CAMEL',
|
|
1619
|
+
targetNamingStyle: 'LOWER_UNDERSCORE'
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
@Model
|
|
1623
|
+
class Order {
|
|
1624
|
+
constructor() {
|
|
1625
|
+
this.orderId = null;
|
|
1626
|
+
this.customerName = '';
|
|
1627
|
+
this.orderItems = [];
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const order = new Order();
|
|
1632
|
+
order.assign({
|
|
1633
|
+
orderId: '12345',
|
|
1634
|
+
customerName: 'John Doe',
|
|
1635
|
+
orderItems: [
|
|
1636
|
+
{ itemId: 1, name: 'Product 1', quantity: 2 }
|
|
1637
|
+
]
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// Will use the configured default options for serialization
|
|
1641
|
+
const json = order.toJsonString();
|
|
1642
|
+
console.log(json);
|
|
1643
|
+
// Output will use lower_underscore naming:
|
|
1644
|
+
// {"order_id":"12345","customer_name":"John Doe","order_items":[{"item_id":1,"name":"Product 1","quantity":2}]}
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
## Integration Examples
|
|
1648
|
+
|
|
1649
|
+
### Using with Vue.js
|
|
1650
|
+
|
|
1651
|
+
```javascript
|
|
1652
|
+
import { Model, Normalizable, Validatable } from '@qubit-ltd/common-decorator';
|
|
1653
|
+
import { defineComponent, ref } from 'vue';
|
|
1654
|
+
|
|
1655
|
+
@Model
|
|
1656
|
+
class UserProfile {
|
|
1657
|
+
constructor() {
|
|
1658
|
+
this.username = '';
|
|
1659
|
+
this.email = '';
|
|
1660
|
+
this.bio = '';
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
@Normalizable
|
|
1664
|
+
@Validatable
|
|
1665
|
+
get username() {
|
|
1666
|
+
return this._username;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
set username(value) {
|
|
1670
|
+
this._username = value;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Other getters and setters...
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
export default defineComponent({
|
|
1677
|
+
setup() {
|
|
1678
|
+
const profile = ref(new UserProfile());
|
|
1679
|
+
|
|
1680
|
+
const updateProfile = (formData) => {
|
|
1681
|
+
profile.value.assign(formData);
|
|
1682
|
+
profile.value.normalize();
|
|
1683
|
+
const validation = profile.value.validate();
|
|
1684
|
+
|
|
1685
|
+
if (validation.valid) {
|
|
1686
|
+
// Save profile
|
|
1687
|
+
} else {
|
|
1688
|
+
// Handle validation errors
|
|
1689
|
+
console.error(validation.message);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
return {
|
|
1694
|
+
profile,
|
|
1695
|
+
updateProfile
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1701
|
+
### Using with Express.js
|
|
1702
|
+
|
|
1703
|
+
```javascript
|
|
1704
|
+
import express from 'express';
|
|
1705
|
+
import { Model, Normalizable, Validatable, NonEmpty } from '@qubit-ltd/common-decorator';
|
|
1706
|
+
|
|
1707
|
+
const app = express();
|
|
1708
|
+
app.use(express.json());
|
|
1709
|
+
|
|
1710
|
+
@Model
|
|
1711
|
+
class NewUserRequest {
|
|
1712
|
+
constructor() {
|
|
1713
|
+
this.username = '';
|
|
1714
|
+
this.email = '';
|
|
1715
|
+
this.password = '';
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
@NonEmpty
|
|
1719
|
+
@Normalizable
|
|
1720
|
+
@Validatable
|
|
1721
|
+
get username() {
|
|
1722
|
+
return this._username;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
set username(value) {
|
|
1726
|
+
this._username = value;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Other getters and setters...
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
app.post('/api/users', (req, res) => {
|
|
1733
|
+
try {
|
|
1734
|
+
const userRequest = NewUserRequest.create(req.body);
|
|
1735
|
+
userRequest.normalize();
|
|
1736
|
+
const validation = userRequest.validate();
|
|
1737
|
+
|
|
1738
|
+
if (!validation.valid) {
|
|
1739
|
+
return res.status(400).json({ error: validation.message });
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Create user in database
|
|
1743
|
+
return res.status(201).json({ message: 'User created successfully' });
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
return res.status(500).json({ error: error.message });
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
app.listen(3000, () => {
|
|
1750
|
+
console.log('Server is running on port 3000');
|
|
1751
|
+
});
|
|
1752
|
+
```
|
|
1753
|
+
|
|
1754
|
+
## Compatibility and Requirements
|
|
1755
|
+
|
|
1756
|
+
- **Node.js**: 14.x or higher
|
|
1757
|
+
- **ECMAScript**: ES2022 or higher
|
|
1758
|
+
- **Decorator Support**: Requires babel configuration for Stage 3 decorators
|
|
1759
|
+
- **Browser Support**: Modern browsers with ES6+ support
|
|
1760
|
+
|
|
1761
|
+
## Best Practices
|
|
1762
|
+
|
|
1763
|
+
### Project Structure
|
|
1764
|
+
|
|
1765
|
+
When using this library, we recommend organizing your domain models in a structured way:
|
|
1766
|
+
|
|
1767
|
+
```
|
|
1768
|
+
src/
|
|
1769
|
+
├── models/
|
|
1770
|
+
│ ├── base/
|
|
1771
|
+
│ │ └── BaseModel.js
|
|
1772
|
+
│ ├── User.js
|
|
1773
|
+
│ ├── Product.js
|
|
1774
|
+
│ └── Order.js
|
|
1775
|
+
├── enums/
|
|
1776
|
+
│ ├── Status.js
|
|
1777
|
+
│ └── Role.js
|
|
1778
|
+
└── app.js
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
### Performance Considerations
|
|
1782
|
+
|
|
1783
|
+
- Use `normalize()` only when necessary, not on every operation
|
|
1784
|
+
- Consider caching validation results for frequently accessed objects
|
|
1785
|
+
- For large collections, use `createArray()` method instead of mapping each item
|
|
1786
|
+
|
|
1787
|
+
### Type Safety
|
|
1788
|
+
|
|
1789
|
+
While JavaScript is dynamically typed, this library provides several ways to ensure type safety:
|
|
1790
|
+
|
|
1791
|
+
1. Use the `@Type` decorator to enforce type checking for properties
|
|
1792
|
+
2. Use the `@ElementType` decorator for arrays
|
|
1793
|
+
3. Enable normalization to automatically convert values to the correct type
|
|
1794
|
+
|
|
1795
|
+
### Memory Management
|
|
1796
|
+
|
|
1797
|
+
When working with large object graphs:
|
|
1798
|
+
|
|
1799
|
+
1. Avoid deep cloning unnecessarily
|
|
1800
|
+
2. Use the `removeEmptyFields` option in `toJSON` for large objects
|
|
1801
|
+
3. Be cautious with circular references when serializing objects
|