schema-shield 0.0.3 → 0.0.4

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/lib/formats.ts CHANGED
@@ -1,62 +1,190 @@
1
- import isMyIpValid from 'is-my-ip-valid';
2
- import { FormatFunction } from './index';
3
- import { ValidationError } from './utils';
4
-
5
- // The datetime 1990-02-31T15:59:60.123-08:00 must be rejected.
6
- const RegExps = {
7
- 'date-time': /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|([+-])(\d{2}):(\d{2}))$/,
8
- time: /^(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|([+-])(\d{2}):(\d{2}))$/,
9
- uri: /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/,
10
- email:
11
- /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
12
- hostname: /^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*[a-zA-Z0-9]$/,
13
- date: /^(\d{4})-(\d{2})-(\d{2})$/,
14
- 'json-pointer': /^\/(?:[^~]|~0|~1)*$/,
15
- 'relative-json-pointer': /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/,
16
- };
1
+ import { FormatFunction } from "./index";
17
2
 
18
3
  export const Formats: Record<string, FormatFunction | false> = {
19
- ['date-time'](data) {
20
- const upperCaseData = data.toUpperCase();
21
- if (!RegExps['date-time'].test(upperCaseData)) {
4
+ ["date-time"](data) {
5
+ const match = data.match(
6
+ /^(\d{4})-(0[0-9]|1[0-2])-(\d{2})T(0[0-9]|1\d|2[0-3]):([0-5]\d):((?:[0-5]\d|60))(?:.\d+)?(?:([+-])(0[0-9]|1\d|2[0-3]):([0-5]\d)|Z)?$/i
7
+ );
8
+
9
+ if (!match) {
10
+ return false;
11
+ }
12
+
13
+ let day = Number(match[3]);
14
+
15
+ if (match[2] === "02" && day > 29) {
16
+ return false;
17
+ }
18
+
19
+ const [
20
+ ,
21
+ yearStr,
22
+ monthStr,
23
+ ,
24
+ hourStr,
25
+ minuteStr,
26
+ secondStr,
27
+ timezoneSign,
28
+ timezoneHourStr,
29
+ timezoneMinuteStr
30
+ ] = match;
31
+
32
+ let year = Number(yearStr);
33
+ let month = Number(monthStr);
34
+ let hour = Number(hourStr);
35
+ let minute = Number(minuteStr);
36
+ let second = Number(secondStr);
37
+
38
+ if (timezoneSign === "-" || timezoneSign === "+") {
39
+ const timezoneHour = Number(timezoneHourStr);
40
+ const timezoneMinute = Number(timezoneMinuteStr);
41
+
42
+ if (timezoneSign === "-") {
43
+ hour += timezoneHour;
44
+ minute += timezoneMinute;
45
+ } else if (timezoneSign === "+") {
46
+ hour -= timezoneHour;
47
+ minute -= timezoneMinute;
48
+ }
49
+
50
+ if (minute > 59) {
51
+ hour += 1;
52
+ minute -= 60;
53
+ } else if (minute < 0) {
54
+ hour -= 1;
55
+ minute += 60;
56
+ }
57
+
58
+ if (hour > 23) {
59
+ day += 1;
60
+ hour -= 24;
61
+ } else if (hour < 0) {
62
+ day -= 1;
63
+ hour += 24;
64
+ }
65
+
66
+ if (day > 31) {
67
+ month += 1;
68
+ day -= 31;
69
+ } else if (day < 1) {
70
+ month -= 1;
71
+ day += 31;
72
+ }
73
+
74
+ if (month > 12) {
75
+ year += 1;
76
+ month -= 12;
77
+ } else if (month < 1) {
78
+ year -= 1;
79
+ month += 12;
80
+ }
81
+
82
+ if (year < 0) {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ const daysInMonth = [31, , 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
88
+ const maxDays =
89
+ month === 2
90
+ ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
91
+ ? 29
92
+ : 28
93
+ : daysInMonth[month - 1];
94
+
95
+ if (day > maxDays) {
96
+ return false;
97
+ }
98
+
99
+ // Leap seconds
100
+ if (second === 60 && (minute !== 59 || hour !== 23)) {
22
101
  return false;
23
102
  }
24
103
 
25
- const date = new Date(upperCaseData);
26
- return !isNaN(date.getTime());
104
+ return true;
27
105
  },
28
106
  uri(data) {
29
- return RegExps.uri.test(data);
107
+ return /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/.test(data);
30
108
  },
31
109
  email(data) {
32
- if (!RegExps.email.test(data)) {
110
+ return /^(?!\.)(?!.*\.$)[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,20}(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]{1,21}){0,2}@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,60}[a-z0-9])?){0,3}$/i.test(
111
+ data
112
+ );
113
+ },
114
+ ipv4(data) {
115
+ // Matches a string formed by 4 numbers between 0 and 255 separated by dots without leading zeros
116
+ // /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$/
117
+ return /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$/.test(
118
+ data
119
+ );
120
+ },
121
+
122
+ // ipv6: isMyIpValid({ version: 6 }),
123
+ ipv6(data) {
124
+ if (data === "::") {
125
+ return true;
126
+ }
127
+
128
+ if (
129
+ data.indexOf(":") === -1 ||
130
+ /(?:\s+|:::+|^\w{5,}|\w{5}$|^:{1}\w|\w:{1}$)/.test(data)
131
+ ) {
33
132
  return false;
34
133
  }
35
134
 
36
- const [local, domain] = data.split('@');
135
+ const hasIpv4 = data.indexOf(".") !== -1;
136
+ let addressParts = data;
37
137
 
38
- if (local.length > 64 || local.indexOf('..') !== -1 || local[0] === '.' || local[local.length - 1] === '.') {
39
- return false;
138
+ if (hasIpv4) {
139
+ addressParts = data.split(":");
140
+ const ipv4Part = addressParts.pop();
141
+ if (
142
+ !/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$/.test(
143
+ ipv4Part
144
+ )
145
+ ) {
146
+ return false;
147
+ }
40
148
  }
41
149
 
42
- if (domain.length > 255 || domain.indexOf('..') !== -1 || domain[0] === '.' || domain[domain.length - 1] === '.') {
43
- return false;
150
+ const isShortened = data.indexOf("::") !== -1;
151
+ const ipv6Part = hasIpv4 ? addressParts.join(":") : data;
152
+
153
+ if (isShortened) {
154
+ if (ipv6Part.split("::").length - 1 > 1) {
155
+ return false;
156
+ }
157
+
158
+ if (!/^[0-9a-fA-F:.]*$/.test(ipv6Part)) {
159
+ return false;
160
+ }
161
+
162
+ return /^(?:(?:(?:[0-9a-fA-F]{1,4}(?::|$)){1,6}))|(?:::(?:[0-9a-fA-F]{1,4})){0,5}$/.test(
163
+ ipv6Part
164
+ );
44
165
  }
45
166
 
46
- return true;
167
+ const isIpv6Valid =
168
+ /^(?:(?:[0-9a-fA-F]{1,4}:){7}(?:[0-9a-fA-F]{1,4}|:))$/.test(ipv6Part);
169
+
170
+ const hasInvalidChar = /(?:[0-9a-fA-F]{5,}|\D[0-9a-fA-F]{3}:)/.test(
171
+ ipv6Part
172
+ );
173
+
174
+ if (hasIpv4) {
175
+ return isIpv6Valid || !hasInvalidChar;
176
+ }
177
+
178
+ return isIpv6Valid && !hasInvalidChar;
47
179
  },
48
- ipv4: isMyIpValid({ version: 4 }),
49
- ipv6: isMyIpValid({ version: 6 }),
50
180
 
51
181
  hostname(data) {
52
- return RegExps.hostname.test(data);
182
+ return /^[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9][a-z0-9-]{0,62})*[a-z0-9]$/i.test(
183
+ data
184
+ );
53
185
  },
54
186
  date(data) {
55
- if (typeof data !== 'string') {
56
- return false;
57
- }
58
-
59
- if (RegExps.date.test(data) === false) {
187
+ if (/^(\d{4})-(\d{2})-(\d{2})$/.test(data) === false) {
60
188
  return false;
61
189
  }
62
190
 
@@ -70,31 +198,45 @@ export const Formats: Record<string, FormatFunction | false> = {
70
198
  return false;
71
199
  }
72
200
  },
73
- 'json-pointer'(data) {
74
- if (data === '') {
201
+ "json-pointer"(data) {
202
+ if (data === "") {
75
203
  return true;
76
204
  }
77
205
 
78
- return RegExps['json-pointer'].test(data);
206
+ return /^\/(?:[^~]|~0|~1)*$/.test(data);
79
207
  },
80
- 'relative-json-pointer'(data) {
81
- if (data === '') {
208
+ "relative-json-pointer"(data) {
209
+ if (data === "") {
82
210
  return true;
83
211
  }
84
212
 
85
- return RegExps['relative-json-pointer'].test(data);
213
+ return /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/.test(data);
86
214
  },
87
215
  time(data) {
88
- return RegExps.time.test(data);
216
+ return /^(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|([+-])(\d{2}):(\d{2}))$/.test(
217
+ data
218
+ );
219
+ },
220
+ "uri-reference"(data) {
221
+ if (/\\/.test(data)) {
222
+ return false;
223
+ }
224
+
225
+ return /^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#((?![^#]*\\)[^#]*))?/i.test(
226
+ data
227
+ );
228
+ },
229
+ "uri-template"(data) {
230
+ return /^(?:(?:https?:\/\/[\w.-]+)?\/?)?[\w- ;,.\/?%&=]*(?:\{[\w-]+(?::\d+)?\}[\w- ;,.\/?%&=]*)*\/?$/.test(
231
+ data
232
+ );
89
233
  },
90
234
 
91
235
  // Not supported yet
92
236
  duration: false,
93
- 'idn-email': false,
94
- 'idn-hostname': false,
95
237
  uuid: false,
96
- 'uri-reference': false,
238
+ "idn-email": false,
239
+ "idn-hostname": false,
97
240
  iri: false,
98
- 'iri-reference': false,
99
- 'uri-template': false,
241
+ "iri-reference": false
100
242
  };
package/lib/index.ts CHANGED
@@ -45,10 +45,10 @@ export interface Validator {
45
45
  }
46
46
 
47
47
  export class SchemaShield {
48
- types = new Map<string, TypeFunction | false>();
49
- formats = new Map<string, FormatFunction | false>();
50
- keywords = new Map<string, KeywordFunction | false>();
51
- immutable = false;
48
+ private types: Record<string, TypeFunction | false> = {};
49
+ private formats: Record<string, FormatFunction | false> = {};
50
+ private keywords: Record<string, KeywordFunction | false> = {};
51
+ private immutable = false;
52
52
 
53
53
  constructor({
54
54
  immutable = false
@@ -74,16 +74,37 @@ export class SchemaShield {
74
74
  }
75
75
  }
76
76
 
77
- addType(name: string, validator: TypeFunction) {
78
- this.types.set(name, validator);
77
+ addType(name: string, validator: TypeFunction, overwrite = false) {
78
+ if (this.types[name] && !overwrite) {
79
+ throw new ValidationError(`Type "${name}" already exists`);
80
+ }
81
+ this.types[name] = validator;
82
+ }
83
+
84
+ getType(type: string): TypeFunction | false {
85
+ return this.types[type];
79
86
  }
80
87
 
81
- addFormat(name: string, validator: FormatFunction) {
82
- this.formats.set(name, validator);
88
+ addFormat(name: string, validator: FormatFunction, overwrite = false) {
89
+ if (this.formats[name] && !overwrite) {
90
+ throw new ValidationError(`Format "${name}" already exists`);
91
+ }
92
+ this.formats[name] = validator;
83
93
  }
84
94
 
85
- addKeyword(name: string, validator: KeywordFunction) {
86
- this.keywords.set(name, validator);
95
+ getFormat(format: string): FormatFunction | false {
96
+ return this.formats[format];
97
+ }
98
+
99
+ addKeyword(name: string, validator: KeywordFunction, overwrite = false) {
100
+ if (this.keywords[name] && !overwrite) {
101
+ throw new ValidationError(`Keyword "${name}" already exists`);
102
+ }
103
+ this.keywords[name] = validator;
104
+ }
105
+
106
+ getKeyword(keyword: string): KeywordFunction | false {
107
+ return this.keywords[keyword];
87
108
  }
88
109
 
89
110
  compile(schema: any): Validator {
@@ -144,7 +165,7 @@ export class SchemaShield {
144
165
  : schema.type.split(",").map((t) => t.trim());
145
166
 
146
167
  for (const type of types) {
147
- const validator = this.types.get(type);
168
+ const validator = this.getType(type);
148
169
  if (validator) {
149
170
  typeValidations.push(validator);
150
171
  methodName += (methodName ? "_OR_" : "") + validator.name;
@@ -162,10 +183,9 @@ export class SchemaShield {
162
183
  compiledSchema.$validate = getNamedFunction<ValidateFunction>(
163
184
  methodName,
164
185
  (data) => {
165
- if (typeValidation(data)) {
166
- return;
186
+ if (!typeValidation(data)) {
187
+ return defineTypeError("Invalid type", { data });
167
188
  }
168
- return defineTypeError("Invalid type", { data });
169
189
  }
170
190
  );
171
191
  } else if (typeValidationsLength > 1) {
@@ -189,17 +209,9 @@ export class SchemaShield {
189
209
  continue;
190
210
  }
191
211
 
192
- const keywordValidator = this.keywords.get(key);
212
+ const keywordValidator = this.getKeyword(key);
193
213
  if (keywordValidator) {
194
214
  const defineError = getDefinedErrorFunctionForKey(key, schema[key]);
195
- const executeKeywordValidator = (data: any) =>
196
- (keywordValidator as KeywordFunction)(
197
- compiledSchema,
198
- data,
199
- defineError,
200
- this
201
- );
202
-
203
215
  if (compiledSchema.$validate) {
204
216
  const prevValidator = compiledSchema.$validate;
205
217
  methodName += `_AND_${keywordValidator.name}`;
@@ -210,17 +222,25 @@ export class SchemaShield {
210
222
  if (error) {
211
223
  return error;
212
224
  }
213
- const keywordError = executeKeywordValidator(data);
214
- if (keywordError) {
215
- return keywordError;
216
- }
225
+ return (keywordValidator as KeywordFunction)(
226
+ compiledSchema,
227
+ data,
228
+ defineError,
229
+ this
230
+ );
217
231
  }
218
232
  );
219
233
  } else {
220
234
  methodName = keywordValidator.name;
221
235
  compiledSchema.$validate = getNamedFunction<ValidateFunction>(
222
236
  methodName,
223
- executeKeywordValidator
237
+ (data) =>
238
+ (keywordValidator as KeywordFunction)(
239
+ compiledSchema,
240
+ data,
241
+ defineError,
242
+ this
243
+ )
224
244
  );
225
245
  }
226
246
  }
@@ -252,7 +272,7 @@ export class SchemaShield {
252
272
  }
253
273
 
254
274
  for (let subKey in subSchema) {
255
- if (this.keywords.has(subKey)) {
275
+ if (subKey in this.keywords) {
256
276
  return true;
257
277
  }
258
278
  }
@@ -101,7 +101,7 @@ export const ArrayKeywords: Record<string, KeywordFunction> = {
101
101
  },
102
102
 
103
103
  additionalItems(schema, data, defineError) {
104
- if (!Array.isArray(data) || !schema.items || !Array.isArray(schema.items)) {
104
+ if (!schema.items || isObject(schema.items)) {
105
105
  return;
106
106
  }
107
107
 
@@ -35,24 +35,18 @@ export const StringKeywords: Record<string, KeywordFunction> = {
35
35
  return defineError("Value does not match the pattern", { data });
36
36
  },
37
37
 
38
- format(schema, data, defineError, formatInstance) {
38
+ // Take into account that if we receive a format that is not defined, we
39
+ // will not throw an error, we just ignore it.
40
+ format(schema, data, defineError, instance) {
39
41
  if (typeof data !== "string") {
40
42
  return;
41
43
  }
42
44
 
43
- const formatValidate = formatInstance.formats.get(schema.format);
44
- if (formatValidate === false) {
45
+ const formatValidate = instance.getFormat(schema.format);
46
+ if (!formatValidate || formatValidate(data)) {
45
47
  return;
46
48
  }
47
49
 
48
- if (typeof formatValidate === "function") {
49
- if (formatValidate(data)) {
50
- return;
51
- }
52
-
53
- return defineError("Value does not match the format", { data });
54
- }
55
-
56
- return defineError("Format is not supported", { data });
50
+ return defineError("Value does not match the format", { data });
57
51
  }
58
52
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-shield",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A fast library that protects your JSON schema from invalid data.",
5
5
  "repository": "git@github.com:Masquerade-Circus/schema-shield.git",
6
6
  "author": "Masquerade <christian@masquerade-circus.net>",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "scripts": {
72
72
  "test": "mocha --bail --recursive --no-timeouts --forbid-only --exit --require ts-node/register --enable-source-maps tests/**/*.test.ts",
73
- "dev:test": "nodemon -e ts,js -w ./tests -w ./lib --exec npm run test",
73
+ "dev:test": "nodemon -e ts,js -w ./tests -w ./lib --exec mocha --bail --recursive --no-timeouts --exit --require ts-node/register --enable-source-maps tests/**/*.test.ts",
74
74
  "dev:source": "NODE_ENV=development nodemon --enable-source-maps -e tsx,ts,json,css -w ./lib -w ./www source.js",
75
75
  "build": "node source.js",
76
76
  "coverage": "nyc report --reporter=lcov",
@@ -79,13 +79,13 @@
79
79
  "release-test": "release-it --dry-run --verbose"
80
80
  },
81
81
  "dependencies": {
82
- "is-my-ip-valid": "^1.0.1",
83
82
  "ts-node": "^10.9.1",
84
83
  "tsc-prog": "^2.2.1",
85
84
  "tslib": "^2.5.0",
86
85
  "typescript": "^5.0.2"
87
86
  },
88
87
  "devDependencies": {
88
+ "@exodus/schemasafe": "^1.0.0",
89
89
  "@release-it/conventional-changelog": "^5.1.1",
90
90
  "@typescript-eslint/eslint-plugin": "^5.56.0",
91
91
  "@typescript-eslint/parser": "^5.56.0",