@dwtechs/antity 0.18.2 → 0.19.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
@@ -2,7 +2,7 @@
2
2
  [![License: MIT](https://img.shields.io/npm/l/@dwtechs/antity.svg?color=brightgreen)](https://opensource.org/licenses/MIT)
3
3
  [![npm version](https://badge.fury.io/js/%40dwtechs%2Fantity.svg)](https://www.npmjs.com/package/@dwtechs/antity)
4
4
  [![last version release date](https://img.shields.io/github/release-date/DWTechs/Antity.js)](https://www.npmjs.com/package/@dwtechs/antity)
5
- ![Jest:coverage](https://img.shields.io/badge/Jest:coverage-75%25-brightgreen.svg)
5
+ ![Jest:coverage](https://img.shields.io/badge/Jest:coverage-79%25-brightgreen.svg)
6
6
 
7
7
  - [Synopsis](#synopsis)
8
8
  - [Support](#support)
@@ -45,7 +45,7 @@ $ npm i @dwtechs/antity
45
45
  import { Entity } from "@dwtechs/antity";
46
46
  import { normalizeName, normalizeNickname } from "@dwtechs/checkard";
47
47
 
48
- const entity = new Entity("consumers", [
48
+ const entity = new Entity("users", [
49
49
  {
50
50
  key: "id",
51
51
  type: "integer",
@@ -89,7 +89,7 @@ const entity = new Entity("consumers", [
89
89
  max: 255,
90
90
  isTypeChecked: true,
91
91
  requiredFor: ["PUT"],
92
- isPrivate: true,
92
+ isPrivate: false,
93
93
  sanitizer: null,
94
94
  normalizer: normalizeNickname,
95
95
  validator: null,
@@ -146,14 +146,15 @@ type Method = "PATCH" | "PUT" | "POST";
146
146
  class Property {
147
147
  key: string;
148
148
  type: Type;
149
- min: number | Date | null;
150
- max: number | Date | null;
149
+ min: number | Date;
150
+ max: number | Date;
151
151
  requiredFor: Method[];
152
152
  isPrivate: boolean;
153
153
  isTypeChecked: boolean;
154
154
  sanitizer: Function | null;
155
155
  normalizer: Function | null;
156
156
  validator: Function | null;
157
+ readOnly: boolean;
157
158
  };
158
159
 
159
160
  class Entity {
@@ -208,6 +209,28 @@ class Entity {
208
209
  * ```
209
210
  */
210
211
  getPropsByMethod(method: Method): Property[];
212
+
213
+ /**
214
+ * Builds a single Property instance from a plain field-definition object.
215
+ * Called once per entry of the `properties` array passed to the constructor.
216
+ *
217
+ * - Not meant to be called directly; override it in a subclass to build
218
+ * your own `Property` subclass (e.g. a library adding its own fields)
219
+ * while still reusing `Entity`'s constructor bookkeeping.
220
+ *
221
+ * @param {Record<string, unknown>} p - Plain field-definition object
222
+ * @returns {Property} The constructed Property instance
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * class MyEntity extends Entity {
227
+ * protected createProperty(p: Record<string, unknown>): Property {
228
+ * return new MyProperty(p.key, p.type, ..., p.myCustomField);
229
+ * }
230
+ * }
231
+ * ```
232
+ */
233
+ protected createProperty(p: Record<string, unknown>): Property;
211
234
 
212
235
  /**
213
236
  * Normalizes an array of records by applying sanitization and normalization
@@ -219,7 +242,7 @@ class Entity {
219
242
  * - Calls next(error) on failure, next() on success
220
243
  *
221
244
  * @param {Request} req - Express request object containing rows
222
- * @param {Response} _res - Express response object (not used)
245
+ * @param {Response} res - Express response object. Set `res.locals.allowReadOnly = true` beforehand to allow writing `readOnly` properties through this call.
223
246
  * @param {NextFunction} next - Express next function
224
247
  *
225
248
  * @returns {void}
@@ -240,7 +263,7 @@ class Entity {
240
263
  * });
241
264
  * ```
242
265
  */
243
- normalizeArray: (req: Request, _res: Response, next: NextFunction) => void;
266
+ normalizeArray: (req: Request, res: Response, next: NextFunction) => void;
244
267
 
245
268
  /**
246
269
  * Normalizes a single record by applying sanitization and normalization
@@ -252,7 +275,7 @@ class Entity {
252
275
  * - Calls next(error) on failure, next() on success
253
276
  *
254
277
  * @param {Request} req - Express request object containing a single record
255
- * @param {Response} _res - Express response object (not used)
278
+ * @param {Response} res - Express response object. Set `res.locals.allowReadOnly = true` beforehand to allow writing `readOnly` properties through this call.
256
279
  * @param {NextFunction} next - Express next function
257
280
  *
258
281
  * @returns {void}
@@ -273,7 +296,7 @@ class Entity {
273
296
  * });
274
297
  * ```
275
298
  */
276
- normalizeOne: (req: Request, _res: Response, next: NextFunction) => void;
299
+ normalizeOne: (req: Request, res: Response, next: NextFunction) => void;
277
300
 
278
301
  /**
279
302
  * Validates an array of rows according to property config and HTTP method.
@@ -282,7 +305,7 @@ class Entity {
282
305
  * - Calls next(error) on failure, next() on success
283
306
  *
284
307
  * @param {Request} req - Express request object containing rows
285
- * @param {Response} _res - Express response object (not used)
308
+ * @param {Response} res - Express response object. Set `res.locals.allowReadOnly = true` beforehand to allow writing `readOnly` properties through this call.
286
309
  * @param {NextFunction} next - Express next function
287
310
  *
288
311
  * @returns {void}
@@ -302,7 +325,7 @@ class Entity {
302
325
  * });
303
326
  * ```
304
327
  */
305
- validateArray: (req: Request, _res: Response, next: NextFunction) => void;
328
+ validateArray: (req: Request, res: Response, next: NextFunction) => void;
306
329
 
307
330
  /**
308
331
  * Validates a single record according to property config and HTTP method.
@@ -311,7 +334,7 @@ class Entity {
311
334
  * - Calls next(error) on failure, next() on success
312
335
  *
313
336
  * @param {Request} req - Express request object containing a single record
314
- * @param {Response} _res - Express response object (not used)
337
+ * @param {Response} res - Express response object. Set `res.locals.allowReadOnly = true` beforehand to allow writing `readOnly` properties through this call.
315
338
  * @param {NextFunction} next - Express next function
316
339
  *
317
340
  * @returns {void}
@@ -331,7 +354,7 @@ class Entity {
331
354
  * });
332
355
  * ```
333
356
  */
334
- validateOne: (req: Request, _res: Response, next: NextFunction) => void;
357
+ validateOne: (req: Request, res: Response, next: NextFunction) => void;
335
358
 
336
359
  ```
337
360
  **normalizeArray()**, **normalizeOne()**, **validateArray()**, and **validateOne()** methods are made to be used as Express.js middlewares.
@@ -371,23 +394,21 @@ Properties **min** and **max** of the password properties will override default
371
394
 
372
395
  ### Available options for a property
373
396
 
374
- Any of these can be passed into the options object for each function.
375
-
376
- | Name | Type | Description | Default value |
377
- | :-------------- | :----------------------- | :----------------------------------------------- | :-------------- |
378
- | key | string | Name of the property |
379
- | type | Type | Type of the property |
380
- | min | number \| Date | Minimum value if applicable | 0 \| 1900-01-01
381
- | max | number \| Date | Maximum value if applicable | 999999999 \| 2200-12-31
382
- | requiredFor | Methods[] | Property is required for the listed methods only | [ "POST", "PUT", "PATCH" ]
383
- | isPrivate | boolean | Property should not be sent in the response | false
384
- | isTypeChecked | boolean | Strict type check at validation | false
385
- | sanitizer | ((v:any) => any) \| null | Custom sanitizer function | null
386
- | normalizer | ((v:any) => any) \| null | Custom Normalizer function | null
387
- | validator | ((v:any, min:number, max:number, typeCheck:boolean) => any) \| null | Custom validator | null
388
-
389
- * *Min and max parameters are not used for boolean type*
390
- * *TypeCheck Parameter is not used for boolean, string and array types*
397
+ Any of these can be passed into the options object for each function. **Behavior** describes exactly what `normalizeArray`/`normalizeOne`/`validateArray`/`validateOne` do with the property at runtime, depending on its value.
398
+
399
+ | Name | Type | Default value | Behavior |
400
+ | :-------------- | :----------------------- | :-------------- | :------- |
401
+ | key | string | | Read/write key on each record. No behavior of its own. |
402
+ | type | Type | | When a value is present, selects the built-in type validator run during `validate()` — skipped entirely if `validator` is set. |
403
+ | min | number \| Date | 0 \| 1900-01-01 | Passed to the `type` validator as a lower bound during `validate()`. Not used for `boolean`. |
404
+ | max | number \| Date | 999999999 \| 2200-12-31 | Passed to the `type` validator as an upper bound during `validate()`. Not used for `boolean`. |
405
+ | requiredFor | Methods[] | [ ] | If the current HTTP method is in this list, `validate()` rejects the record with a 400 when the value is `null`/`undefined`. **Ignored when `readOnly` is `true`** (see below) — a `readOnly` field is never required. |
406
+ | isPrivate | boolean | false | Not enforced by `normalize()`/`validate()`. When `true`, the key is added to `entity.privateProps` — your own response code is responsible for stripping it from output; antity.js never removes it itself. |
407
+ | isTypeChecked | boolean | false | Passed to the `type` validator to toggle strict vs. lenient checking; the exact effect depends on `type` (e.g. a stricter locale/timezone allow-list). Not used for `boolean`, `string` or `array` types. |
408
+ | readOnly | boolean | false | If `true`: `normalize()` **deletes** the key from the record before sanitizing/normalizing, and `validate()` **skips** it entirely (never required, never checked) — a client can never set it or trigger validation on it, whatever `requiredFor` says. Bypassed for one request by setting `res.locals.allowReadOnly = true` beforehand (trusted server-side writes only). If `false`, treated like any other property. |
409
+ | sanitizer | ((v:any) => any) \| null | null | If set, `normalize()` calls it instead of the default sanitizer for any present (truthy) value. If `null`, the default trims strings — recursively for a plain object's string properties, per-element for an array. |
410
+ | normalizer | ((v:any) => any) \| null | null | If set, `normalize()` calls it right after sanitizing, for any present (truthy) value. If `null`, no normalization step runs. |
411
+ | validator | ((v:any) => boolean) \| null | null | If set, `validate()` calls it instead of the built-in `type` validator for any present value: return `false` or throw to fail (a thrown error's message is included in the 400 response). If `null`, the built-in `type`/`min`/`max`/`isTypeChecked` validator runs. |
391
412
 
392
413
 
393
414
  ## Contributors
package/dist/antity.d.ts CHANGED
@@ -24,88 +24,49 @@ SOFTWARE.
24
24
  https://github.com/DWTechs/Antity.js
25
25
  */
26
26
 
27
- import type { Request, Response, NextFunction } from 'express';
28
- declare const Methods: readonly ["PATCH", "PUT", "POST"];
29
- type Method = typeof Methods[number];
30
- type Type =
31
- "boolean" |
32
- "string" |
33
- "number" |
34
- "integer" |
35
- "float" |
36
- "even" |
37
- "odd" |
38
- "positive" |
39
- "negative" |
40
- "powerOfTwo" |
41
- "ascii" |
42
- "array" |
43
- "jwt" |
44
- "symbol" |
45
- "password" |
46
- "email" |
47
- "regex" |
48
- "json" |
49
- "ipAddress" |
50
- "slug" |
51
- "hexadecimal" |
52
- "date" |
53
- "timestamp" |
54
- "function" |
55
- "htmlElement" |
56
- "htmlEventAttribute" |
57
- "node" |
58
- "object" |
59
- "ansiEscapeCode" |
60
- "locale" |
61
- "timeZone";
27
+ import { Request, Response, NextFunction } from 'express';
62
28
 
63
- declare class Entity {
64
- private _name;
65
- private _privateProps;
66
- private _properties;
67
- constructor(name: string, properties: Property[]);
68
- get name(): string;
69
- get privateProps(): string[];
70
- get properties(): Property[];
71
- set name(name: string);
72
- getProp(key: string): Property | undefined;
73
- getPropsByMethod(method: Method): Property[];
74
- normalizeArray: (req: Request, _res: Response, next: NextFunction) => void;
75
- normalizeOne: (req: Request, _res: Response, next: NextFunction) => void;
76
- validateArray: (req: Request, _res: Response, next: NextFunction) => void;
77
- validateOne: (req: Request, _res: Response, next: NextFunction) => void;
78
- }
29
+ declare const METHODS: string[];
30
+
31
+ type Type = "boolean" | "string" | "number" | "integer" | "float" | "even" | "odd" | "positive" | "negative" | "powerOfTwo" | "ascii" | "array" | "jwt" | "symbol" | "password" | "email" | "regex" | "json" | "ipAddress" | "slug" | "hexadecimal" | "date" | "timestamp" | "function" | "htmlElement" | "htmlEventAttribute" | "node" | "object" | "ansiEscapeCode" | "locale" | "timeZone";
32
+ type Method = typeof METHODS[number];
79
33
 
80
34
  declare class Property {
81
- [key: string]: unknown;
82
- key: string;
83
- type: Type;
84
- min: number | Date | null;
85
- max: number | Date | null;
86
- isPrivate: boolean;
87
- requiredFor: Method[];
88
- isTypeChecked: boolean;
89
- sanitizer: ((v: any) => any) | null;
90
- normalizer: ((v: any) => any) | null;
91
- validator: ((v: any) => any) | null;
92
- constructor(
93
- key: string,
94
- type: Type,
95
- min: number | Date | null,
96
- max: number | Date | null,
97
- isPrivate: boolean,
98
- requiredFor: Method[],
99
- isTypeChecked: boolean,
100
- sanitizer: ((v: any) => any) | null,
101
- normalizer: ((v: any) => any) | null,
102
- validator: ((v: any) => any) | null
103
- );
35
+ [key: string]: unknown;
36
+ key: string;
37
+ type: Type;
38
+ min: number | Date;
39
+ max: number | Date;
40
+ isPrivate: boolean;
41
+ requiredFor: Method[];
42
+ isTypeChecked: boolean;
43
+ readOnly: boolean;
44
+ sanitizer: ((v: any) => any) | null;
45
+ normalizer: ((v: any) => any) | null;
46
+ validator: ((v: any) => boolean) | null;
47
+ constructor(key: string, type: Type, min: number | Date | null, max: number | Date | null, isPrivate: boolean, requiredFor: Method[], isTypeChecked: boolean, readOnly: boolean, sanitizer: ((v: any) => any) | null, normalizer: ((v: any) => any) | null, validator: ((v: any) => boolean) | null);
48
+ private interval;
104
49
  }
105
50
 
106
- export type { Type, Method };
107
- export {
108
- Entity,
109
- Property,
110
- };
51
+ declare const STANDARD_PROP_KEYS: ReadonlySet<string>;
52
+ declare class Entity {
53
+ private _name;
54
+ private _privateProps;
55
+ private _properties;
56
+ private _propsByMethod;
57
+ constructor(name: string, properties: Property[]);
58
+ protected createProperty(p: Record<string, unknown>): Property;
59
+ get name(): string;
60
+ get privateProps(): string[];
61
+ get properties(): Property[];
62
+ set name(name: string);
63
+ getProp(key: string): Property | undefined;
64
+ getPropsByMethod(method: Method): Property[];
65
+ normalizeArray: (req: Request, res: Response, next: NextFunction) => void;
66
+ normalizeOne: (req: Request, res: Response, next: NextFunction) => void;
67
+ validateArray: (req: Request, res: Response, next: NextFunction) => void;
68
+ validateOne: (req: Request, res: Response, next: NextFunction) => void;
69
+ }
111
70
 
71
+ export { Entity, Property, STANDARD_PROP_KEYS };
72
+ export type { Method, Type };
package/dist/antity.js CHANGED
@@ -24,7 +24,7 @@ SOFTWARE.
24
24
  https://github.com/DWTechs/Antity.js
25
25
  */
26
26
 
27
- import { isBoolean, isStringOfLength, isValidNumber, isValidInteger, isValidFloat, isEven, isOdd, isPositive, isNegative, isPowerOfTwo, isAscii, isArrayOfLength, isEmail, isRegex, isJson, isJWT, isSymbol, isIpAddress, isSlug, isHexadecimal, isValidDate, isValidTimestamp, isFunction, isHtmlElement, isHtmlEventAttribute, isNode, isObject, isAnsiEscapeCode, isLocale, isTimeZone, isString, isProperty, isArray, isIn, isDate, isNumber, isInteger, isNil } from '@dwtechs/checkard';
27
+ import { isBoolean, isStringOfLength, isValidNumber, isValidInteger, isValidFloat, isEven, isOdd, isPositive, isNegative, isPowerOfTwo, isAscii, isArrayOfLength, isEmail, isRegex, isJson, isJWT, isSymbol, isIpAddress, isSlug, isHexadecimal, isValidDate, isValidTimestamp, isFunction, isHtmlElement, isHtmlEventAttribute, isNode, isObject, isAnsiEscapeCode, isLocale, isTimeZone, isString, isProperty, isArray, isIn, isDate, isNumber, isNil } from '@dwtechs/checkard';
28
28
  import { log } from '@dwtechs/winstan';
29
29
  import { isValidPassword } from '@dwtechs/passken';
30
30
 
@@ -46,34 +46,34 @@ const Types = {
46
46
  validate: (v, min, max, _typeCheck) => isStringOfLength(v, min, max, true)
47
47
  },
48
48
  number: {
49
- validate: (v, min, max, typeCheck) => isValidNumber(v, min || undefined, max || undefined, typeCheck || undefined, true)
49
+ validate: (v, min, max, typeCheck) => isValidNumber(v, min, max, typeCheck, true)
50
50
  },
51
51
  integer: {
52
- validate: (v, min, max, typeCheck) => isValidInteger(v, min ?? undefined, max ?? undefined, typeCheck || undefined, true)
52
+ validate: (v, min, max, typeCheck) => isValidInteger(v, min, max, typeCheck, true)
53
53
  },
54
54
  float: {
55
- validate: (v, min, max, typeCheck) => isValidFloat(v, min || undefined, max || undefined, typeCheck || undefined, true)
55
+ validate: (v, min, max, typeCheck) => isValidFloat(v, min, max, typeCheck, true)
56
56
  },
57
57
  even: {
58
- validate: (v, _min, _max, typeCheck) => isEven(v, typeCheck || undefined, true)
58
+ validate: (v, _min, _max, typeCheck) => isEven(v, typeCheck, true)
59
59
  },
60
60
  odd: {
61
- validate: (v, _min, _max, typeCheck) => isOdd(v, typeCheck || undefined, true)
61
+ validate: (v, _min, _max, typeCheck) => isOdd(v, typeCheck, true)
62
62
  },
63
63
  positive: {
64
- validate: (v, _min, _max, typeCheck) => isPositive(v, typeCheck || undefined, true)
64
+ validate: (v, _min, _max, typeCheck) => isPositive(v, typeCheck, true)
65
65
  },
66
66
  negative: {
67
- validate: (v, _min, _max, typeCheck) => isNegative(v, typeCheck || undefined, true)
67
+ validate: (v, _min, _max, typeCheck) => isNegative(v, typeCheck, true)
68
68
  },
69
69
  powerOfTwo: {
70
- validate: (v, _min, _max, typeCheck) => isPowerOfTwo(v, typeCheck || undefined, true)
70
+ validate: (v, _min, _max, typeCheck) => isPowerOfTwo(v, typeCheck, true)
71
71
  },
72
72
  ascii: {
73
- validate: (v, _min, _max, typeCheck) => isAscii(v, typeCheck || undefined, true)
73
+ validate: (v, _min, _max, typeCheck) => isAscii(v, typeCheck, true)
74
74
  },
75
75
  array: {
76
- validate: (v, min, max, _typeCheck) => isArrayOfLength(v, min || undefined, max || undefined, true)
76
+ validate: (v, min, max, _typeCheck) => isArrayOfLength(v, min, max, true)
77
77
  },
78
78
  password: {
79
79
  validate: (v, min, max, _typeCheck) => {
@@ -92,7 +92,7 @@ const Types = {
92
92
  validate: (v, _min, _max, _typeCheck) => isEmail(v, true)
93
93
  },
94
94
  regex: {
95
- validate: (v, _min, _max, typeCheck) => isRegex(v, typeCheck || undefined, true)
95
+ validate: (v, _min, _max, typeCheck) => isRegex(v, typeCheck, true)
96
96
  },
97
97
  json: {
98
98
  validate: (v, _min, _max, _typeCheck) => isJson(v, true)
@@ -113,10 +113,10 @@ const Types = {
113
113
  validate: (v, _min, _max, _typeCheck) => isHexadecimal(v, true)
114
114
  },
115
115
  date: {
116
- validate: (v, min, max, _typeCheck) => isValidDate(v, min || undefined, max || undefined, true)
116
+ validate: (v, min, max, _typeCheck) => isValidDate(v, min, max, true)
117
117
  },
118
118
  timestamp: {
119
- validate: (v, min, max, typeCheck) => isValidTimestamp(v, min || undefined, max || undefined, typeCheck || undefined, true)
119
+ validate: (v, min, max, typeCheck) => isValidTimestamp(v, min, max, typeCheck, true)
120
120
  },
121
121
  function: {
122
122
  validate: (v, _min, _max, _typeCheck) => isFunction(v, true)
@@ -131,13 +131,13 @@ const Types = {
131
131
  validate: (v, _min, _max, _typeCheck) => isNode(v, true)
132
132
  },
133
133
  object: {
134
- validate: (v, _min, _max, _typeCheck) => isObject(v, true)
134
+ validate: (v, _min, _max, _typeCheck) => isObject(v, false, true)
135
135
  },
136
136
  ansiEscapeCode: {
137
137
  validate: (v, _min, _max, _typeCheck) => isAnsiEscapeCode(v, true)
138
138
  },
139
139
  locale: {
140
- validate: (v, _min, _max, typeCheck) => isLocale(v, typeCheck || undefined, true)
140
+ validate: (v, _min, _max, typeCheck) => isLocale(v, typeCheck, true)
141
141
  },
142
142
  timeZone: {
143
143
  validate: (v, _min, _max, _typeCheck) => isTimeZone(v, true)
@@ -152,10 +152,11 @@ class Property {
152
152
  isPrivate;
153
153
  requiredFor;
154
154
  isTypeChecked;
155
+ readOnly;
155
156
  sanitizer;
156
157
  normalizer;
157
158
  validator;
158
- constructor(key, type, min, max, isPrivate, requiredFor, isTypeChecked, sanitizer, normalizer, validator) {
159
+ constructor(key, type, min, max, isPrivate, requiredFor, isTypeChecked, readOnly, sanitizer, normalizer, validator) {
159
160
  try {
160
161
  isString(key, "!0", null, true);
161
162
  }
@@ -168,16 +169,14 @@ class Property {
168
169
  catch (err) {
169
170
  throw new Error(`${LOGS_PREFIX}Property "type" must be a valid type - caused by: ${err.message}`);
170
171
  }
171
- if (isArray(requiredFor)) {
172
- for (const m of requiredFor) {
172
+ if (isArray(requiredFor))
173
+ for (const m of requiredFor)
173
174
  try {
174
175
  isIn(METHODS, m, 0, true);
175
176
  }
176
177
  catch (err) {
177
178
  throw new Error(`${LOGS_PREFIX}Property "requiredFor" must be an array of REST methods - caused by: ${err.message}`);
178
179
  }
179
- }
180
- }
181
180
  this.key = key;
182
181
  this.type = type;
183
182
  this.min = this.interval(min, type, 0, "1900-01-01T00:00:00Z");
@@ -185,6 +184,7 @@ class Property {
185
184
  this.requiredFor = isArray(requiredFor) ? requiredFor : [];
186
185
  this.isPrivate = isBoolean(isPrivate) ? isPrivate : false;
187
186
  this.isTypeChecked = isBoolean(isTypeChecked) ? isTypeChecked : false;
187
+ this.readOnly = isBoolean(readOnly) ? readOnly : false;
188
188
  this.sanitizer = isFunction(sanitizer) ? sanitizer : null;
189
189
  this.normalizer = isFunction(normalizer) ? normalizer : null;
190
190
  this.validator = isFunction(validator) ? validator : null;
@@ -192,7 +192,7 @@ class Property {
192
192
  interval(val, type, integerDefault, dateDefault) {
193
193
  if (type === "date")
194
194
  return isDate(val) ? val : new Date(dateDefault);
195
- return (isNumber(val, true) && isInteger(val, true)) ? val : integerDefault;
195
+ return isNumber(val, true) ? val : integerDefault;
196
196
  }
197
197
  }
198
198
 
@@ -221,14 +221,22 @@ function trim(v) {
221
221
  return v;
222
222
  }
223
223
 
224
- function normalize(record, properties) {
225
- for (const { key, type, sanitizer, normalizer, } of properties) {
224
+ function logSafe(v) {
225
+ return typeof v === 'string' ? v.replace(/[\r\n\t]/g, '') : v;
226
+ }
227
+
228
+ function normalize(record, properties, allowReadOnly = false) {
229
+ for (const { key, type, sanitizer, normalizer, readOnly, } of properties) {
230
+ if (readOnly && !allowReadOnly) {
231
+ delete record[key];
232
+ continue;
233
+ }
226
234
  let v = record[key];
227
- if (v) {
228
- log.debug(`sanitize ${key}: ${type} = ${v}`);
235
+ if (!isNil(v)) {
236
+ log.debug(`sanitize ${key}: ${type} = ${logSafe(v)}`);
229
237
  v = sanitize(v, sanitizer);
230
238
  if (normalizer) {
231
- log.debug(`normalize ${key}: ${type} = ${v}`);
239
+ log.debug(`normalize ${key}: ${type} = ${logSafe(v)}`);
232
240
  v = normalizer(v);
233
241
  }
234
242
  record[key] = v;
@@ -237,11 +245,12 @@ function normalize(record, properties) {
237
245
  }
238
246
 
239
247
  function control(v, key, type, min, max, typeCheck, cb) {
240
- log.debug(`control ${key}: ${type} = ${v}`);
248
+ log.debug(`control ${key}: ${type} = ${logSafe(v)}`);
241
249
  let errorMessage = "";
242
250
  if (cb)
243
251
  try {
244
- cb(v);
252
+ if (cb(v) === false)
253
+ errorMessage = `Custom validator callback failed for "${key}"`;
245
254
  }
246
255
  catch (err) {
247
256
  errorMessage = `Custom validator callback failed for "${key}" - caused by: ${err.message}`;
@@ -259,19 +268,21 @@ function control(v, key, type, min, max, typeCheck, cb) {
259
268
  }
260
269
 
261
270
  function require(v, key, type) {
262
- log.debug(`require ${key}: ${type} = ${v}`);
271
+ log.debug(`require ${key}: ${type} = ${logSafe(v)}`);
263
272
  return isNil(v) ? { statusCode: 400, message: `${LOGS_PREFIX}Missing ${key} of type ${type}` } : null;
264
273
  }
265
274
 
266
- function validate(record, properties, method) {
267
- for (const { key, type, min, max, requiredFor, isTypeChecked, validator } of properties) {
275
+ function validate(record, properties, method, allowReadOnly = false) {
276
+ for (const { key, type, min, max, requiredFor, isTypeChecked, readOnly, validator, } of properties) {
277
+ if (readOnly && !allowReadOnly)
278
+ continue;
268
279
  const v = record[key];
269
280
  if (requiredFor.includes(method)) {
270
281
  const rq = require(v, key, type);
271
282
  if (rq)
272
283
  return rq;
273
284
  }
274
- if (v) {
285
+ if (!isNil(v)) {
275
286
  const ct = control(v, key, type, min, max, isTypeChecked, validator);
276
287
  if (ct)
277
288
  return ct;
@@ -280,7 +291,10 @@ function validate(record, properties, method) {
280
291
  return null;
281
292
  }
282
293
 
283
- const STANDARD_PROP_KEYS = new Set(['key', 'type', 'min', 'max', 'isPrivate', 'requiredFor', 'isTypeChecked', 'sanitizer', 'normalizer', 'validator']);
294
+ const STANDARD_PROP_KEYS = new Set([
295
+ 'key', 'type', 'min', 'max', 'isPrivate', 'requiredFor',
296
+ 'isTypeChecked', 'readOnly', 'sanitizer', 'normalizer', 'validator',
297
+ ]);
284
298
  class Entity {
285
299
  _name;
286
300
  _privateProps;
@@ -292,10 +306,7 @@ class Entity {
292
306
  this._privateProps = [];
293
307
  this._propsByMethod = new Map(METHODS.map(m => [m, []]));
294
308
  for (const p of properties) {
295
- const prop = new Property(p.key, p.type, p.min, p.max, p.isPrivate, p.requiredFor, p.isTypeChecked, p.sanitizer, p.normalizer, p.validator);
296
- for (const k of Object.keys(p))
297
- if (!STANDARD_PROP_KEYS.has(k))
298
- prop[k] = p[k];
309
+ const prop = this.createProperty(p);
299
310
  this._properties.push(prop);
300
311
  if (prop.isPrivate)
301
312
  this._privateProps.push(prop.key);
@@ -303,6 +314,13 @@ class Entity {
303
314
  this._propsByMethod.get(m)?.push(prop);
304
315
  }
305
316
  }
317
+ createProperty(p) {
318
+ const prop = new Property(p.key, p.type, p.min, p.max, p.isPrivate, p.requiredFor, p.isTypeChecked, p.readOnly, p.sanitizer, p.normalizer, p.validator);
319
+ for (const k of Object.keys(p))
320
+ if (!STANDARD_PROP_KEYS.has(k))
321
+ prop[k] = p[k];
322
+ return prop;
323
+ }
306
324
  get name() {
307
325
  return this._name;
308
326
  }
@@ -323,29 +341,29 @@ class Entity {
323
341
  getPropsByMethod(method) {
324
342
  return this._propsByMethod.get(method) ?? [];
325
343
  }
326
- normalizeArray = (req, _res, next) => {
344
+ normalizeArray = (req, res, next) => {
327
345
  log.debug(`normalizeArray ${this.name}`);
328
346
  const rows = req.body?.rows;
329
347
  if (!isArray(rows, ">", 0)) {
330
348
  next({ statusCode: 400, message: `${LOGS_PREFIX}Normalize: no rows found in request body` });
331
349
  return;
332
350
  }
333
- for (const r of rows) {
334
- normalize(r, this._properties);
335
- }
351
+ const allowReadOnly = res?.locals?.allowReadOnly === true;
352
+ for (const r of rows)
353
+ normalize(r, this._properties, allowReadOnly);
336
354
  next();
337
355
  };
338
- normalizeOne = (req, _res, next) => {
356
+ normalizeOne = (req, res, next) => {
339
357
  log.debug(`normalizeOne ${this.name}`);
340
358
  const r = req.body;
341
359
  if (!isObject(r, true)) {
342
360
  next({ statusCode: 400, message: `${LOGS_PREFIX}Normalize: no data found in request body` });
343
361
  return;
344
362
  }
345
- normalize(r, this._properties);
363
+ normalize(r, this._properties, res?.locals?.allowReadOnly === true);
346
364
  next();
347
365
  };
348
- validateArray = (req, _res, next) => {
366
+ validateArray = (req, res, next) => {
349
367
  log.debug(`validateArray ${this.name}`);
350
368
  const rows = req.body?.rows;
351
369
  const method = req.method;
@@ -357,8 +375,9 @@ class Entity {
357
375
  next({ statusCode: 400, message: `${LOGS_PREFIX}Invalid REST method. Received: ${method}. Must be one of: ${METHODS.toString()}` });
358
376
  return;
359
377
  }
378
+ const allowReadOnly = res?.locals?.allowReadOnly === true;
360
379
  for (const r of rows) {
361
- const error = validate(r, this._properties, method);
380
+ const error = validate(r, this._properties, method, allowReadOnly);
362
381
  if (error) {
363
382
  next(error);
364
383
  return;
@@ -366,7 +385,7 @@ class Entity {
366
385
  }
367
386
  next();
368
387
  };
369
- validateOne = (req, _res, next) => {
388
+ validateOne = (req, res, next) => {
370
389
  log.debug(`validateOne ${this.name}`);
371
390
  const record = req.body;
372
391
  const method = req.method;
@@ -378,7 +397,7 @@ class Entity {
378
397
  next({ statusCode: 400, message: `${LOGS_PREFIX}Invalid REST method. Received: ${method}. Must be one of: ${METHODS.toString()}` });
379
398
  return;
380
399
  }
381
- const error = validate(record, this._properties, method);
400
+ const error = validate(record, this._properties, method, res?.locals?.allowReadOnly === true);
382
401
  if (error) {
383
402
  next(error);
384
403
  return;
@@ -387,4 +406,4 @@ class Entity {
387
406
  };
388
407
  }
389
408
 
390
- export { Entity };
409
+ export { Entity, Property, STANDARD_PROP_KEYS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dwtechs/antity",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Open source library for easy entity management",
5
5
  "keywords": [
6
6
  "entities"
@@ -9,12 +9,6 @@
9
9
  "type": "module",
10
10
  "main": "dist/antity.js",
11
11
  "types": "dist/antity.d.ts",
12
- "exports": {
13
- ".": {
14
- "types": "./dist/antity.d.ts",
15
- "import": "./dist/antity.js"
16
- }
17
- },
18
12
  "repository": {
19
13
  "type": "git",
20
14
  "url": "https://github.com/DWTechs/Antity.js"
@@ -37,11 +31,11 @@
37
31
  "files": [
38
32
  "dist/"
39
33
  ],
40
- "engines": {
34
+ "engines": {
41
35
  "node": ">= 22"
42
36
  },
43
37
  "dependencies": {
44
- "@dwtechs/checkard": "3.6.1",
38
+ "@dwtechs/checkard": "3.7.0",
45
39
  "@dwtechs/passken": "0.6.1",
46
40
  "@dwtechs/sparray": "0.2.1",
47
41
  "@dwtechs/winstan": "0.7.1"
@@ -56,6 +50,7 @@
56
50
  "core-js": "3.33.0",
57
51
  "jest": "29.7.0",
58
52
  "rollup": "4.24.0",
53
+ "rollup-plugin-dts": "6.5.1",
59
54
  "typescript": "6.0.3"
60
55
  }
61
56
  }