@dwtechs/antity-pgsql 0.9.1 → 0.11.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
@@ -62,11 +62,11 @@ const entity = new SQLEntity("consumers", [
62
62
  type: "integer",
63
63
  min: 0,
64
64
  max: 120,
65
- typeCheck: true,
66
- filter: true,
67
- need: ["PUT"],
65
+ isTypeChecked: true,
66
+ isFilterable: true,
67
+ requiredFor: ["PUT"],
68
68
  operations: ["SELECT", "UPDATE"],
69
- send: true,
69
+ isPrivate: false,
70
70
  sanitizer: null,
71
71
  normalizer: null,
72
72
  validator: null,
@@ -76,11 +76,11 @@ const entity = new SQLEntity("consumers", [
76
76
  type: "string",
77
77
  min: 0,
78
78
  max: 255,
79
- typeCheck: true,
80
- filter: false,
81
- need: ["POST", "PUT"],
79
+ isTypeChecked: true,
80
+ isFilterable: false,
81
+ requiredFor: ["POST", "PUT"],
82
82
  operations: ["SELECT", "UPDATE"],
83
- send: true,
83
+ isPrivate: false,
84
84
  sanitizer: null,
85
85
  normalizer: normalizeName,
86
86
  validator: null,
@@ -90,11 +90,11 @@ const entity = new SQLEntity("consumers", [
90
90
  type: "string",
91
91
  min: 0,
92
92
  max: 255,
93
- typeCheck: true,
94
- filter: false,
95
- need: ["POST", "PUT"],
93
+ isTypeChecked: true,
94
+ isFilterable: false,
95
+ requiredFor: ["POST", "PUT"],
96
96
  operations: ["SELECT", "UPDATE"],
97
- send: true,
97
+ isPrivate: false,
98
98
  sanitizer: null,
99
99
  normalizer: normalizeName,
100
100
  validator: null,
@@ -104,11 +104,11 @@ const entity = new SQLEntity("consumers", [
104
104
  type: "string",
105
105
  min: 0,
106
106
  max: 255,
107
- typeCheck: true,
108
- filter: true,
109
- need: ["POST", "PUT"],
107
+ isTypeChecked: true,
108
+ isFilterable: true,
109
+ requiredFor: ["POST", "PUT"],
110
110
  operations: ["SELECT", "UPDATE"],
111
- send: true,
111
+ isPrivate: false,
112
112
  sanitizer: null,
113
113
  normalizer: normalizeNickname,
114
114
  validator: null,
@@ -125,10 +125,10 @@ router.put("/", ...entity.updateArraySubstack);
125
125
  router.post("/manual", entity.normalizeArray, entity.validateArray, ..., entity.add);
126
126
  router.put("/manual", entity.normalizeArray, entity.validateArray, ..., entity.update);
127
127
 
128
- router.put("/archive", ..., entity.archive);
128
+ router.patch("/archive", ..., entity.archive);
129
129
  router.delete("/", ..., entity.delete);
130
130
  router.delete("/archived", ..., entity.deleteArchive);
131
- router.get("/history", ..., entity.getHistory);
131
+ router.get("/:id/history", ..., entity.getHistory);
132
132
 
133
133
  ```
134
134
 
@@ -160,10 +160,14 @@ type MatchMode =
160
160
  "st_dwithin";
161
161
 
162
162
 
163
+ type Filters = {
164
+ [key: string]: Filter | Filter[]; // Supports both simple (object) and complex (array) formats
165
+ }
166
+
163
167
  type Filter = {
164
168
  value: string | number | boolean | Date | number[];
165
- subProps?: string[];
166
169
  matchMode?: MatchMode;
170
+ operator?: string; // 'and' | 'or' - Used when multiple filters apply to the same property
167
171
  }
168
172
 
169
173
  type ExpressMiddleware = (req: Request, res: Response, next: NextFunction) => void;
@@ -175,7 +179,7 @@ class SQLEntity {
175
179
  get name(): string;
176
180
  get table(): string;
177
181
  get schema(): string;
178
- get unsafeProps(): string[];
182
+ get privateProps(): string[];
179
183
  get properties(): Property[];
180
184
  set name(name: string);
181
185
  set table(table: string);
@@ -278,6 +282,56 @@ Using substacks simplifies your route definitions and ensures consistent data pr
278
282
  - **deleteArchive()**: Deletes archived rows that were archived before a specific date using a PostgreSQL SECURITY DEFINER function. Expects `req.body.date` to be a Date object.
279
283
  - **getHistory()**: Retrieves modification history for rows from the `log.history` table. Expects `req.body.rows` to be an array of objects with `id` property. Returns all historical records for the specified entity IDs.
280
284
 
285
+ ### Filters
286
+
287
+ Filters support two formats for maximum flexibility:
288
+
289
+ #### Simple Format (Single Filter per Property)
290
+
291
+ Backward-compatible format using a single filter object:
292
+
293
+ ```javascript
294
+ const filters = {
295
+ name: { value: 'John', matchMode: 'contains' },
296
+ age: { value: 30, matchMode: 'equals' },
297
+ archived: { value: false, matchMode: 'equals' }
298
+ };
299
+ ```
300
+
301
+ #### Complex Format (Multiple Filters per Property)
302
+
303
+ Array-based format supporting multiple filters with logical operators:
304
+
305
+ ```javascript
306
+ const filters = {
307
+ // Multiple filters on the same property with OR operator
308
+ name: [
309
+ { value: 'John', matchMode: 'contains', operator: 'or' },
310
+ { value: 'Jane', matchMode: 'contains', operator: 'or' }
311
+ ],
312
+ // Age range with AND operator
313
+ age: [
314
+ { value: 18, matchMode: 'gte', operator: 'and' },
315
+ { value: 65, matchMode: 'lte', operator: 'and' }
316
+ ],
317
+ // Single filter in array format
318
+ archived: [{ value: false, matchMode: 'equals' }]
319
+ };
320
+ ```
321
+
322
+ This generates SQL like:
323
+ ```sql
324
+ WHERE (name LIKE '%John%' OR name LIKE '%Jane%')
325
+ AND (age >= 18 AND age <= 65)
326
+ AND archived = false
327
+ ```
328
+
329
+ **Notes:**
330
+ - Both formats can be mixed in the same filters object
331
+ - When using arrays with a single filter, the operator is optional
332
+ - Default operator is 'AND' if not specified
333
+ - The operator field is case-insensitive
334
+
281
335
 
282
336
  ## Match modes
283
337
 
@@ -360,10 +414,10 @@ Any of these can be passed into the options object for each function.
360
414
  | type | Type | Type of the property |
361
415
  | min | number \| Date | Minimum value | 0 \| 1900-01-01
362
416
  | max | number \| Date | Maximum value | 999999999 \| 2200-12-31
363
- | need | Method[] | property is validated for the listed methods only | ["PATCH", "PUT", "POST"]
364
- | send | boolean | Property is sent in the response | true
365
- | typeCheck | boolean | Type is checked during validation | false
366
- | filter | boolean | property is filterable in a SELECT operation | true
417
+ | requiredFor | Method[] | property is required for the listed methods only | ["PATCH", "PUT", "POST"]
418
+ | isPrivate | boolean | Property is unsafe to send in the response | true
419
+ | isTypeChecked | boolean | Type is checked during validation | false
420
+ | isFilterable | boolean | property is filterable in a SELECT operation | true
367
421
  | operations | Operation[] | Property is used for the DML operations only | ["SELECT", "INSERT", "UPDATE"]
368
422
  | sanitizer | ((v:any) => any) \| null | Custom sanitizer function if sanitize is true | null
369
423
  | normalizer | ((v:any) => any) \| null | Custom Normalizer function if normalize is true | null
@@ -36,23 +36,23 @@ export type Filters = {
36
36
  };
37
37
  export type Filter = {
38
38
  value: string | number | boolean | Date | number[];
39
- subProps?: string[];
40
39
  matchMode?: MatchMode;
40
+ operator?: string;
41
41
  };
42
42
  export type { Type };
43
43
 
44
44
  export declare class Property extends BaseProperty {
45
- filter: boolean;
45
+ isFilterable: boolean;
46
46
  operations: Operation[];
47
47
  constructor(
48
48
  key: string,
49
49
  type: Type,
50
50
  min: number | Date | null,
51
51
  max: number | Date | null,
52
- need: Method[],
53
- send: boolean,
54
- typeCheck: boolean,
55
- filter: boolean,
52
+ requiredFor: Method[],
53
+ isPrivate: boolean,
54
+ isTypeChecked: boolean,
55
+ isFilterable: boolean,
56
56
  operations: Operation[] | undefined,
57
57
  sanitizer: ((v: unknown) => unknown) | null,
58
58
  normalizer: ((v: unknown) => unknown) | null,
@@ -160,20 +160,33 @@ function comparator(matchMode) {
160
160
  }
161
161
 
162
162
  function add(filters) {
163
+ var _a, _b;
163
164
  const conditions = [];
164
165
  const args = [];
165
166
  if (filters) {
166
167
  let i = 1;
167
168
  for (const k in filters) {
168
- const { value, matchMode } = filters[k];
169
- const indexes = isArray(value) ? value.map(() => i++) : [i++];
170
- const cond = addOne(k, indexes, matchMode);
171
- if (cond) {
172
- conditions.push(cond);
173
- if (isArray(value))
174
- args.push(...value);
175
- else
176
- args.push(value);
169
+ const filterValue = filters[k];
170
+ const filterArray = isArray(filterValue) ? filterValue : [filterValue];
171
+ const groupConditions = [];
172
+ for (const filter of filterArray) {
173
+ const { value, matchMode } = filter;
174
+ const indexes = isArray(value) ? value.map(() => i++) : [i++];
175
+ const cond = addOne(k, indexes, matchMode);
176
+ if (cond) {
177
+ groupConditions.push(cond);
178
+ if (isArray(value))
179
+ args.push(...value);
180
+ else
181
+ args.push(value);
182
+ }
183
+ }
184
+ if (groupConditions.length > 0) {
185
+ const operator = ((_b = (_a = filterArray[0]) === null || _a === void 0 ? void 0 : _a.operator) === null || _b === void 0 ? void 0 : _b.toUpperCase()) || 'AND';
186
+ const combined = groupConditions.length > 1
187
+ ? `(${groupConditions.join(` ${operator} `)})`
188
+ : groupConditions[0];
189
+ conditions.push(combined);
177
190
  }
178
191
  }
179
192
  }
@@ -459,18 +472,26 @@ function cleanFilters(filters, properties) {
459
472
  delete filters[k];
460
473
  continue;
461
474
  }
462
- if (!prop.filter) {
475
+ if (!prop.isFilterable) {
463
476
  log.warn(`${LOGS_PREFIX}Filters: skipping unfilterable property: ${k}`);
464
477
  delete filters[k];
465
478
  continue;
466
479
  }
467
480
  const type$1 = type(prop.type);
468
- const { matchMode: matchMode$1 } = filters[k];
469
- if (!matchMode$1 || !matchMode(type$1, matchMode$1)) {
470
- log.warn(`${LOGS_PREFIX}Filters: skipping invalid match mode: "${matchMode$1}" for type: "${type$1}" at property: "${k}"`);
481
+ const filterValue = filters[k];
482
+ const filterArray = isArray(filterValue) ? filterValue : [filterValue];
483
+ const validFilters = filterArray.filter((f) => {
484
+ const { matchMode: matchMode$1 } = f;
485
+ if (!matchMode$1 || !matchMode(type$1, matchMode$1)) {
486
+ log.warn(`${LOGS_PREFIX}Filters: skipping invalid match mode: "${matchMode$1}" for type: "${type$1}" at property: "${k}"`);
487
+ return false;
488
+ }
489
+ return true;
490
+ });
491
+ if (!validFilters.length)
471
492
  delete filters[k];
472
- continue;
473
- }
493
+ else
494
+ filters[k] = validFilters;
474
495
  }
475
496
  }
476
497
  return filters;
@@ -497,10 +518,10 @@ function generateSummary(name, table, properties) {
497
518
  lines.push(`│ │ ├─ Type: ${p.type}`);
498
519
  lines.push(`│ │ ├─ Min: ${p.min}`);
499
520
  lines.push(`│ │ ├─ Max: ${p.max}`);
500
- lines.push(`│ │ ├─ need: ${p.need}`);
501
- lines.push(`│ │ ├─ Safe: ${p.safe}`);
502
- lines.push(`│ │ ├─ TypeCheck: ${p.typeCheck}`);
503
- lines.push(`│ │ ├─ Filter: ${p.filter}`);
521
+ lines.push(`│ │ ├─ RequiredFor: ${p.requiredFor}`);
522
+ lines.push(`│ │ ├─ IsPrivate: ${p.isPrivate}`);
523
+ lines.push(`│ │ ├─ IsTypeChecked: ${p.isTypeChecked}`);
524
+ lines.push(`│ │ ├─ IsFilterable: ${p.isFilterable}`);
504
525
  lines.push(`│ │ ├─ Operations: [${p.operations.join(', ')}]`);
505
526
  lines.push(`│ │ ├─ Sanitize: ${p.sanitize}`);
506
527
  lines.push(`│ │ ├─ Normalize: ${p.normalize}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dwtechs/antity-pgsql",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Open source library to add PostgreSQL support to @dwtechs/Antity entities.",
5
5
  "keywords": [
6
6
  "entities"
@@ -38,7 +38,7 @@
38
38
  "dependencies": {
39
39
  "@dwtechs/checkard": "3.6.0",
40
40
  "@dwtechs/winstan": "0.5.0",
41
- "@dwtechs/antity": "0.15.0",
41
+ "@dwtechs/antity": "0.16.0",
42
42
  "@dwtechs/sparray": "0.2.1",
43
43
  "pg": "8.13.1"
44
44
  },