@onlineapps/service-wrapper 3.0.6 → 3.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { ValidationError } = require('./ErrorMapper');
4
+
3
5
  /**
4
6
  * OperationContext — immutable per-invocation context passed to every
5
7
  * business handler by ServiceWrapper.invokeOperation().
@@ -29,12 +31,19 @@
29
31
  * abortSignal — AbortSignal
30
32
  * config — read-only service config snapshot
31
33
  *
34
+ * Prototype-level helpers (not fields, not frozen on the instance):
35
+ * - normalizeDateRange(dateFrom, dateTo) → { from: Date, to: Date }
36
+ * Promoted from property/v3 statements handler (W2-R-D3) so every biz
37
+ * service can normalise `{dateFrom, dateTo}` query parameters the same
38
+ * way. Fail-fast: throws ValidationError on null/undefined, non-string,
39
+ * unparseable, or inverted range.
40
+ *
32
41
  * Design notes:
33
- * - No getter helpers, no `.scoped()`, no methods. The handler destructures
34
- * what it needs: `const { tenant_id, db, logger } = ctx;`.
35
42
  * - `Object.freeze` prevents reassignment of top-level fields
36
43
  * (`ctx.tenant_id = 99` throws in strict mode) but is shallow by design —
37
44
  * `ctx.db.query(...)` and `ctx.logger.info(...)` must remain callable.
45
+ * Prototype methods like `normalizeDateRange` live on the prototype and
46
+ * are therefore unaffected by the instance freeze.
38
47
  * - Lifecycle (release of db client, logger flush) is owned by ContextBuilder;
39
48
  * see its `build()` return contract.
40
49
  */
@@ -54,6 +63,103 @@ class OperationContext {
54
63
  Object.assign(this, fields);
55
64
  Object.freeze(this);
56
65
  }
66
+
67
+ /**
68
+ * Normalise a `{dateFrom, dateTo}` pair into two JavaScript `Date` objects
69
+ * suitable for SQL range predicates (e.g. `BETWEEN :from AND :to`).
70
+ *
71
+ * Input rules:
72
+ * - Bare ISO date `YYYY-MM-DD`:
73
+ * from → 00:00:00.000 UTC of that day
74
+ * to → 23:59:59.999 UTC of that day
75
+ * - ISO datetime (`YYYY-MM-DDTHH:MM[:SS[.sss]][Z|±HH:MM]`):
76
+ * time is preserved as given (UTC-anchored by JS `Date` semantics).
77
+ *
78
+ * Fail-fast (throws {@link ValidationError}) if either argument is:
79
+ * - null / undefined
80
+ * - not a string
81
+ * - unparseable as ISO date or datetime
82
+ * and if the resulting `to` is earlier than `from`.
83
+ *
84
+ * @param {string} dateFrom - ISO date or datetime string.
85
+ * @param {string} dateTo - ISO date or datetime string.
86
+ * @returns {{ from: Date, to: Date }}
87
+ * @throws {ValidationError}
88
+ */
89
+ normalizeDateRange(dateFrom, dateTo) {
90
+ const from = OperationContext._parseBoundary(dateFrom, 'dateFrom', 'start');
91
+ const to = OperationContext._parseBoundary(dateTo, 'dateTo', 'end');
92
+
93
+ if (to.getTime() < from.getTime()) {
94
+ throw new ValidationError({
95
+ message:
96
+ '[OperationContext.normalizeDateRange] dateTo is earlier than dateFrom - ' +
97
+ 'Expected ISO date string (YYYY-MM-DD) or ISO datetime'
98
+ });
99
+ }
100
+
101
+ return { from, to };
102
+ }
103
+
104
+ /**
105
+ * @private
106
+ * @param {*} value - candidate boundary
107
+ * @param {string} field - 'dateFrom' | 'dateTo' (used in error messages)
108
+ * @param {'start'|'end'} mode - defaults applied to bare-date inputs
109
+ * @returns {Date}
110
+ */
111
+ static _parseBoundary(value, field, mode) {
112
+ if (value === null || value === undefined) {
113
+ throw new ValidationError({
114
+ message:
115
+ `[OperationContext.normalizeDateRange] ${field} is null or undefined - ` +
116
+ 'Expected ISO date string (YYYY-MM-DD) or ISO datetime'
117
+ });
118
+ }
119
+ if (typeof value !== 'string') {
120
+ throw new ValidationError({
121
+ message:
122
+ `[OperationContext.normalizeDateRange] ${field} is not a string - ` +
123
+ 'Expected ISO date string (YYYY-MM-DD) or ISO datetime'
124
+ });
125
+ }
126
+
127
+ const trimmed = value.trim();
128
+ if (trimmed.length === 0) {
129
+ throw new ValidationError({
130
+ message:
131
+ `[OperationContext.normalizeDateRange] ${field} is empty - ` +
132
+ 'Expected ISO date string (YYYY-MM-DD) or ISO datetime'
133
+ });
134
+ }
135
+
136
+ const isoDate = /^\d{4}-\d{2}-\d{2}$/;
137
+ const isoDateTime = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
138
+
139
+ let d;
140
+ if (isoDate.test(trimmed)) {
141
+ const suffix = mode === 'end' ? 'T23:59:59.999Z' : 'T00:00:00.000Z';
142
+ d = new Date(`${trimmed}${suffix}`);
143
+ } else if (isoDateTime.test(trimmed)) {
144
+ d = new Date(trimmed);
145
+ } else {
146
+ throw new ValidationError({
147
+ message:
148
+ `[OperationContext.normalizeDateRange] ${field} is unparseable - ` +
149
+ 'Expected ISO date string (YYYY-MM-DD) or ISO datetime'
150
+ });
151
+ }
152
+
153
+ if (isNaN(d.getTime())) {
154
+ throw new ValidationError({
155
+ message:
156
+ `[OperationContext.normalizeDateRange] ${field} is unparseable - ` +
157
+ 'Expected ISO date string (YYYY-MM-DD) or ISO datetime'
158
+ });
159
+ }
160
+
161
+ return d;
162
+ }
57
163
  }
58
164
 
59
165
  module.exports = { OperationContext };