hono 2.2.4 → 2.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.
@@ -1,26 +1,106 @@
1
- import { JSONPath } from '../../utils/json';
1
+ import { JSONPathCopy } from '../../utils/json';
2
2
  import { rule } from './rule';
3
3
  import { sanitizer } from './sanitizer';
4
+ export class VObjectBase {
5
+ constructor(container, key) {
6
+ this.keys = [];
7
+ this._isOptional = false;
8
+ this.getValidators = () => {
9
+ const validators = [];
10
+ const thisKeys = [];
11
+ Object.assign(thisKeys, this.keys);
12
+ const walk = (container, keys, isOptional) => {
13
+ for (const v of Object.values(container)) {
14
+ if (v instanceof VArray || v instanceof VObject) {
15
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
+ // @ts-ignore
17
+ isOptional || (isOptional = v._isOptional);
18
+ keys.push(...v.keys);
19
+ walk(v.container, keys, isOptional);
20
+ const tmp = [];
21
+ Object.assign(tmp, thisKeys);
22
+ keys = tmp;
23
+ }
24
+ else if (v instanceof VBase) {
25
+ if (isOptional)
26
+ v.isOptional();
27
+ v.baseKeys.push(...keys);
28
+ validators.push(v);
29
+ }
30
+ }
31
+ };
32
+ walk(this.container, this.keys, this._isOptional);
33
+ return validators;
34
+ };
35
+ this.container = container;
36
+ if (this instanceof VArray) {
37
+ this.keys.push(key, '[*]');
38
+ }
39
+ else if (this instanceof VObject) {
40
+ this.keys.push(key);
41
+ }
42
+ }
43
+ isOptional() {
44
+ this._isOptional = true;
45
+ return this;
46
+ }
47
+ }
48
+ export class VObject extends VObjectBase {
49
+ constructor(container, key) {
50
+ super(container, key);
51
+ }
52
+ }
53
+ export class VArray extends VObjectBase {
54
+ constructor(container, key) {
55
+ super(container, key);
56
+ this.type = 'array';
57
+ }
58
+ }
4
59
  export class Validator {
5
60
  constructor() {
61
+ this.isArray = false;
6
62
  this.query = (key) => new VString({ target: 'query', key: key });
7
63
  this.header = (key) => new VString({ target: 'header', key: key });
8
64
  this.body = (key) => new VString({ target: 'body', key: key });
9
- this.json = (key) => new VString({ target: 'json', key: key });
65
+ this.json = (key) => {
66
+ if (this.isArray) {
67
+ return new VStringArray({ target: 'json', key: key });
68
+ }
69
+ else {
70
+ return new VString({ target: 'json', key: key });
71
+ }
72
+ };
73
+ this.array = (path, validator) => {
74
+ this.isArray = true;
75
+ const res = validator(this);
76
+ const arr = new VArray(res, path);
77
+ return arr;
78
+ };
79
+ this.object = (path, validator) => {
80
+ this.isArray = false;
81
+ const res = validator(this);
82
+ const obj = new VObject(res, path);
83
+ return obj;
84
+ };
10
85
  }
11
86
  }
12
87
  export class VBase {
13
88
  constructor(options) {
14
- this.addRule = (rule) => {
15
- this.rules.push(rule);
16
- return this;
17
- };
89
+ this.baseKeys = [];
90
+ this._nested = () => (this.baseKeys.length ? true : false);
18
91
  this.addSanitizer = (sanitizer) => {
19
92
  this.sanitizers.push(sanitizer);
20
93
  return this;
21
94
  };
95
+ this.message = (text) => {
96
+ const len = this.rules.length;
97
+ if (len >= 1) {
98
+ this.rules[len - 1].customMessage = text;
99
+ }
100
+ return this;
101
+ };
22
102
  this.isRequired = () => {
23
- return this.addRule((value) => {
103
+ return this.addRule('isRequired', (value) => {
24
104
  if (value !== undefined && value !== null && value !== '')
25
105
  return true;
26
106
  return false;
@@ -28,34 +108,28 @@ export class VBase {
28
108
  };
29
109
  this.isOptional = () => {
30
110
  this._optional = true;
31
- return this.addRule(() => true);
111
+ return this.addRule('isOptional', () => true);
32
112
  };
33
113
  this.isEqual = (comparison) => {
34
- return this.addRule((value) => {
114
+ return this.addRule('isEqual', (value) => {
35
115
  return value === comparison;
36
116
  });
37
117
  };
38
118
  this.asNumber = () => {
39
- const newVNumber = new VNumber(this);
119
+ const newVNumber = new VNumber({ ...this, type: 'number' });
120
+ if (this.isArray)
121
+ return newVNumber.asArray();
40
122
  return newVNumber;
41
123
  };
42
124
  this.asBoolean = () => {
43
- const newVBoolean = new VBoolean(this);
125
+ const newVBoolean = new VBoolean({ ...this, type: 'boolean' });
126
+ if (this.isArray)
127
+ return newVBoolean.asArray();
44
128
  return newVBoolean;
45
129
  };
46
- this.asObject = () => {
47
- const newVObject = new VObject(this);
48
- return newVObject;
49
- };
50
130
  this.validate = async (req) => {
51
- const result = {
52
- isValid: true,
53
- message: undefined,
54
- target: this.target,
55
- key: this.key,
56
- value: undefined,
57
- };
58
131
  let value = undefined;
132
+ let jsonData = undefined;
59
133
  if (this.target === 'query') {
60
134
  value = req.query(this.key);
61
135
  }
@@ -67,105 +141,178 @@ export class VBase {
67
141
  value = body[this.key];
68
142
  }
69
143
  if (this.target === 'json') {
144
+ if (this._nested()) {
145
+ this.key = `${this.baseKeys.join('.')}.${this.key}`;
146
+ }
147
+ let obj = {};
70
148
  try {
71
- const obj = (await req.json());
72
- value = JSONPath(obj, this.key);
149
+ obj = (await req.json());
73
150
  }
74
151
  catch (e) {
75
152
  throw new Error('Malformed JSON in request body');
76
153
  }
154
+ const dst = {};
155
+ value = JSONPathCopy(obj, dst, this.key);
156
+ if (this._nested())
157
+ jsonData = dst;
77
158
  }
78
- result.value = value;
79
- result.isValid = this.validateValue(value);
80
- if (result.isValid === false) {
81
- if (this._message) {
82
- result.message = this._message;
159
+ const results = [];
160
+ let typeRule = this.rules.shift();
161
+ for (const rule of this.rules) {
162
+ if (rule.type === 'type') {
163
+ typeRule = rule;
83
164
  }
84
- else {
85
- const valToStr = Array.isArray(value)
86
- ? `[${value
87
- .map((val) => val === undefined ? 'undefined' : typeof val === 'string' ? `"${val}"` : val)
88
- .join(', ')}]`
89
- : value;
90
- switch (this.target) {
91
- case 'query':
92
- result.message = `Invalid Value: the query parameter "${this.key}" is invalid - ${valToStr}`;
93
- break;
94
- case 'header':
95
- result.message = `Invalid Value: the request header "${this.key}" is invalid - ${valToStr}`;
96
- break;
97
- case 'body':
98
- result.message = `Invalid Value: the request body "${this.key}" is invalid - ${valToStr}`;
99
- break;
100
- case 'json':
101
- result.message = `Invalid Value: the JSON body "${this.key}" is invalid - ${valToStr}`;
102
- break;
103
- }
165
+ else if (rule.type === 'value') {
166
+ const result = this.validateRule(rule, value);
167
+ result.jsonData || (result.jsonData = jsonData);
168
+ results.push(result);
104
169
  }
105
170
  }
106
- return result;
171
+ if (typeRule) {
172
+ const typeResult = this.validateRule(typeRule, value);
173
+ typeResult.jsonData || (typeResult.jsonData = jsonData);
174
+ results.unshift(typeResult);
175
+ }
176
+ return results;
107
177
  };
108
- this.validateValue = (value) => {
109
- // Check type
178
+ this.validateType = (value) => {
110
179
  if (this.isArray) {
111
180
  if (!Array.isArray(value)) {
112
181
  return false;
113
182
  }
114
183
  for (const val of value) {
115
- if (typeof val !== this.type) {
116
- // Value is of wrong type here
117
- // If not optional, or optional and not undefined, return false
118
- if (!this._optional || typeof val !== 'undefined')
119
- return false;
184
+ if (typeof val === 'undefined' && this._nested()) {
185
+ value.pop();
120
186
  }
121
- }
122
- // Sanitize
123
- for (const sanitizer of this.sanitizers) {
124
- value = value.map((innerVal) => sanitizer(innerVal));
125
- }
126
- for (const rule of this.rules) {
127
187
  for (const val of value) {
128
- if (!rule(val)) {
129
- return false;
188
+ if (typeof val !== this.type) {
189
+ // Value is of wrong type here
190
+ // If it is not optional and not undefined, return false
191
+ if (!this._optional || typeof val !== 'undefined')
192
+ return false;
130
193
  }
131
194
  }
132
195
  }
133
- return true;
134
196
  }
135
197
  else {
136
198
  if (typeof value !== this.type) {
137
- if (this._optional && typeof value === 'undefined') {
199
+ if (this._optional && (typeof value === 'undefined' || Array.isArray(value))) {
138
200
  // Do nothing.
139
- // The value is allowed to be `undefined` if it is `optional`
201
+ // If it is optional it's OK to be `undefined` or Array
140
202
  }
141
203
  else {
142
204
  return false;
143
205
  }
144
206
  }
207
+ }
208
+ return true;
209
+ };
210
+ this.validateValue = (func, value) => {
211
+ if (Array.isArray(value)) {
145
212
  // Sanitize
146
213
  for (const sanitizer of this.sanitizers) {
147
- value = sanitizer(value);
214
+ value = value.map((innerVal) => sanitizer(innerVal));
148
215
  }
149
- for (const rule of this.rules) {
150
- if (!rule(value)) {
216
+ for (const val of value) {
217
+ if (!func(val)) {
151
218
  return false;
152
219
  }
153
220
  }
154
221
  return true;
155
222
  }
223
+ else {
224
+ // Sanitize
225
+ for (const sanitizer of this.sanitizers) {
226
+ value = sanitizer(value);
227
+ }
228
+ if (!func(value)) {
229
+ return false;
230
+ }
231
+ return true;
232
+ }
233
+ };
234
+ this.getMessage = (opts) => {
235
+ let keyText;
236
+ const valueText = Array.isArray(opts.value)
237
+ ? `${opts.value
238
+ .map((val) => val === undefined ? 'undefined' : typeof val === 'string' ? `"${val}"` : val)
239
+ .join(', ')}`
240
+ : opts.value;
241
+ switch (this.target) {
242
+ case 'query':
243
+ keyText = `the query parameter "${this.key}"`;
244
+ break;
245
+ case 'header':
246
+ keyText = `the request header "${this.key}"`;
247
+ break;
248
+ case 'body':
249
+ keyText = `the request body "${this.key}"`;
250
+ break;
251
+ case 'json':
252
+ keyText = `the JSON body "${this.key}"`;
253
+ break;
254
+ }
255
+ return `Invalid Value [${valueText}]: ${keyText} is invalid - ${opts.ruleName}`;
156
256
  };
157
257
  this.target = options.target;
158
258
  this.key = options.key;
159
259
  this.type = options.type || 'string';
160
- this.rules = [];
260
+ this.rules = [
261
+ {
262
+ name: this.getTypeRuleName(),
263
+ type: 'type',
264
+ func: this.validateType,
265
+ },
266
+ ];
161
267
  this.sanitizers = [];
162
- this.isArray = options.isArray || false;
163
268
  this._optional = false;
269
+ this.isArray = options.isArray || false;
270
+ }
271
+ addRule(arg, func) {
272
+ if (typeof arg === 'string' && func) {
273
+ this.rules.push({ name: arg, func, type: 'value' });
274
+ }
275
+ else if (arg instanceof Function) {
276
+ this.rules.push({ name: arg.name, func: arg, type: 'value' });
277
+ }
278
+ return this;
164
279
  }
165
- message(value) {
166
- this._message = value;
280
+ get(value) {
281
+ const len = this.rules.length;
282
+ if (len > 0) {
283
+ this.rules[this.rules.length - 1].customMessage = value;
284
+ }
167
285
  return this;
168
286
  }
287
+ getTypeRuleName() {
288
+ const prefix = 'should be';
289
+ return this.isArray ? `${prefix} "${this.type}[]"` : `${prefix} "${this.type}"`;
290
+ }
291
+ validateRule(rule, value) {
292
+ let isValid = false;
293
+ if (this._nested() && this.target != 'json') {
294
+ isValid = false;
295
+ }
296
+ else if (rule.type === 'value') {
297
+ isValid = this.validateValue(rule.func, value);
298
+ }
299
+ else if (rule.type === 'type') {
300
+ isValid = this.validateType(value);
301
+ }
302
+ const message = isValid
303
+ ? undefined
304
+ : rule.customMessage || this.getMessage({ ruleName: rule.name, value });
305
+ const result = {
306
+ isValid: isValid,
307
+ message: message,
308
+ target: this.target,
309
+ key: this.key,
310
+ value,
311
+ ruleName: rule.name,
312
+ ruleType: rule.type,
313
+ };
314
+ return result;
315
+ }
169
316
  }
170
317
  export class VString extends VBase {
171
318
  constructor(options) {
@@ -174,28 +321,28 @@ export class VString extends VBase {
174
321
  return new VStringArray(this);
175
322
  };
176
323
  this.isEmpty = (options = { ignore_whitespace: false }) => {
177
- return this.addRule((value) => rule.isEmpty(value, options));
324
+ return this.addRule('isEmpty', (value) => rule.isEmpty(value, options));
178
325
  };
179
326
  this.isLength = (options, arg2) => {
180
- return this.addRule((value) => rule.isLength(value, options, arg2));
327
+ return this.addRule('isLength', (value) => rule.isLength(value, options, arg2));
181
328
  };
182
329
  this.isAlpha = () => {
183
- return this.addRule((value) => rule.isAlpha(value));
330
+ return this.addRule('isAlpha', (value) => rule.isAlpha(value));
184
331
  };
185
332
  this.isNumeric = () => {
186
- return this.addRule((value) => rule.isNumeric(value));
333
+ return this.addRule('isNumeric', (value) => rule.isNumeric(value));
187
334
  };
188
335
  this.contains = (elem, options = {
189
336
  ignoreCase: false,
190
337
  minOccurrences: 1,
191
338
  }) => {
192
- return this.addRule((value) => rule.contains(value, elem, options));
339
+ return this.addRule('contains', (value) => rule.contains(value, elem, options));
193
340
  };
194
341
  this.isIn = (options) => {
195
- return this.addRule((value) => rule.isIn(value, options));
342
+ return this.addRule('isIn', (value) => rule.isIn(value, options));
196
343
  };
197
344
  this.match = (regExp) => {
198
- return this.addRule((value) => rule.match(value, regExp));
345
+ return this.addRule('match', (value) => rule.match(value, regExp));
199
346
  };
200
347
  this.trim = () => {
201
348
  return this.addSanitizer((value) => sanitizer.trim(value));
@@ -210,10 +357,10 @@ export class VNumber extends VBase {
210
357
  return new VNumberArray(this);
211
358
  };
212
359
  this.isGte = (min) => {
213
- return this.addRule((value) => rule.isGte(value, min));
360
+ return this.addRule('isGte', (value) => rule.isGte(value, min));
214
361
  };
215
362
  this.isLte = (min) => {
216
- return this.addRule((value) => rule.isLte(value, min));
363
+ return this.addRule('isLte', (value) => rule.isLte(value, min));
217
364
  };
218
365
  this.type = 'number';
219
366
  }
@@ -225,35 +372,32 @@ export class VBoolean extends VBase {
225
372
  return new VBooleanArray(this);
226
373
  };
227
374
  this.isTrue = () => {
228
- return this.addRule((value) => rule.isTrue(value));
375
+ return this.addRule('isTrue', (value) => rule.isTrue(value));
229
376
  };
230
377
  this.isFalse = () => {
231
- return this.addRule((value) => rule.isFalse(value));
378
+ return this.addRule('isFalse', (value) => rule.isFalse(value));
232
379
  };
233
380
  this.type = 'boolean';
234
381
  }
235
382
  }
236
- export class VObject extends VBase {
237
- constructor(options) {
238
- super(options);
239
- this.type = 'object';
240
- }
241
- }
242
383
  export class VNumberArray extends VNumber {
243
384
  constructor(options) {
244
385
  super(options);
245
386
  this.isArray = true;
387
+ this.rules[0].name = this.getTypeRuleName();
246
388
  }
247
389
  }
248
390
  export class VStringArray extends VString {
249
391
  constructor(options) {
250
392
  super(options);
251
393
  this.isArray = true;
394
+ this.rules[0].name = this.getTypeRuleName();
252
395
  }
253
396
  }
254
397
  export class VBooleanArray extends VBoolean {
255
398
  constructor(options) {
256
399
  super(options);
257
400
  this.isArray = true;
401
+ this.rules[0].name = this.getTypeRuleName();
258
402
  }
259
403
  }
@@ -4,4 +4,4 @@ export declare type JSONObject = {
4
4
  [key: string]: JSONPrimitive | JSONArray | JSONObject;
5
5
  };
6
6
  export declare type JSONValue = JSONObject | JSONArray | JSONPrimitive;
7
- export declare const JSONPath: (data: JSONObject, path: string) => JSONValue;
7
+ export declare const JSONPathCopy: (src: JSONObject, dst: JSONObject, path: string) => JSONPrimitive | JSONObject | JSONArray;
@@ -1,27 +1,81 @@
1
- const JSONPathInternal = (data, parts) => {
1
+ const JSONPathCopyInternal = (src, dst, parts, results) => {
2
+ let srcVal = src;
3
+ let dstVal = dst;
2
4
  const length = parts.length;
3
- for (let i = 0; i < length && data !== undefined; i++) {
5
+ for (let i = 0; i < length && srcVal !== undefined && dstVal; i++) {
4
6
  const p = parts[i];
5
- if (p === '') {
6
- continue;
7
+ if (typeof srcVal !== 'object') {
8
+ return srcVal;
7
9
  }
8
- if (typeof data !== 'object' || data === null) {
10
+ if (srcVal === null) {
9
11
  return undefined;
10
12
  }
11
13
  if (p === '*') {
12
14
  const restParts = parts.slice(i + 1);
13
- const values = Object.values(data).map((v) => JSONPathInternal(v, restParts));
14
- return restParts.indexOf('*') === -1 ? values : values.flat();
15
+ const restLength = srcVal.length;
16
+ if (restLength === undefined) {
17
+ parts = Object.keys(srcVal);
18
+ for (const p of parts) {
19
+ const srcVal2 = srcVal;
20
+ const dst2 = {};
21
+ JSONPathCopyInternal(srcVal2, dst2, [p], results);
22
+ dstVal[p] = dst2[p];
23
+ }
24
+ }
25
+ else {
26
+ const res = [];
27
+ for (let i2 = 0; i2 < restLength; i2++) {
28
+ if (typeof srcVal[i2] !== 'object' || srcVal[i2] === undefined) {
29
+ res.push(srcVal[i2]);
30
+ }
31
+ else {
32
+ const srcVal2 = srcVal[i2];
33
+ const dst2 = {};
34
+ const res2 = JSONPathCopyInternal(srcVal2, dst2, restParts, results);
35
+ if (res2 === undefined)
36
+ results.push(undefined);
37
+ dstVal[i2] = dst2;
38
+ }
39
+ }
40
+ if (res.length) {
41
+ Object.assign(dstVal, srcVal);
42
+ results.push(...res);
43
+ }
44
+ }
45
+ return results;
46
+ }
47
+ if (typeof srcVal[p] === 'object') {
48
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
49
+ // @ts-ignore
50
+ dstVal[p] || (dstVal[p] = new srcVal[p].constructor());
51
+ }
52
+ else if (typeof srcVal[p] !== 'undefined') {
53
+ dstVal[p] = srcVal[p];
15
54
  }
16
55
  else {
17
- data = data[p]; // `data` may be an array, but accessing it as an object yields the same result.
56
+ return undefined;
18
57
  }
58
+ srcVal = srcVal[p];
59
+ dstVal = dstVal[p];
60
+ }
61
+ if (typeof srcVal === 'object' && dstVal) {
62
+ Object.assign(dstVal, srcVal);
19
63
  }
20
- return data;
64
+ results.push(srcVal);
65
+ return results;
21
66
  };
22
- export const JSONPath = (data, path) => {
67
+ export const JSONPathCopy = (src, dst, path) => {
68
+ const results = [];
69
+ const parts = path.replace(/\.?\[(.*?)\]/g, '.$1').split(/\./);
23
70
  try {
24
- return JSONPathInternal(data, path.replace(/\[(.*?)\]/g, '.$1').split(/\./));
71
+ JSONPathCopyInternal(src, dst, parts, results);
72
+ if (results.length === 0) {
73
+ return undefined;
74
+ }
75
+ else if (results.length === 1 && !parts.includes('*')) {
76
+ return results[0];
77
+ }
78
+ return results;
25
79
  }
26
80
  catch (e) {
27
81
  return undefined;
@@ -0,0 +1,2 @@
1
+ export declare const isObject: (val: any) => boolean;
2
+ export declare const mergeObjects: (target: any, source: any) => any;
@@ -0,0 +1,35 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ export const isObject = (val) => val && typeof val === 'object' && !Array.isArray(val);
3
+ export const mergeObjects = (target, source) => {
4
+ const merged = Object.assign({}, target);
5
+ if (isObject(target) && isObject(source)) {
6
+ for (const key of Object.keys(source)) {
7
+ if (isObject(source[key])) {
8
+ if (target[key] === undefined)
9
+ Object.assign(merged, { [key]: source[key] });
10
+ else
11
+ merged[key] = mergeObjects(target[key], source[key]);
12
+ }
13
+ else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
14
+ const srcArr = source[key];
15
+ const tgtArr = target[key];
16
+ const outArr = [];
17
+ for (let i = 0; i < srcArr.length; i += 1) {
18
+ // If corresponding index for both arrays is an object, then merge them
19
+ // Otherwise just copy src arr index into out arr index
20
+ if (isObject(srcArr[i]) && isObject(tgtArr[i])) {
21
+ outArr[i] = mergeObjects(tgtArr[i], srcArr[i]);
22
+ }
23
+ else {
24
+ outArr[i] = srcArr[i];
25
+ }
26
+ }
27
+ Object.assign(merged, { [key]: outArr });
28
+ }
29
+ else {
30
+ Object.assign(merged, { [key]: source[key] });
31
+ }
32
+ }
33
+ }
34
+ return merged;
35
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono",
3
- "version": "2.2.4",
3
+ "version": "2.3.0",
4
4
  "description": "Ultrafast web framework for Cloudflare Workers.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",