schema-shield 0.0.2

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.
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "../index";
2
+ export declare const ArrayKeywords: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=array-keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"array-keywords.d.ts","sourceRoot":"","sources":["../../lib/keywords/array-keywords.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAwM3D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "../index";
2
+ export declare const NumberKeywords: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=number-keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-keywords.d.ts","sourceRoot":"","sources":["../../lib/keywords/number-keywords.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CA2E5D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "../index";
2
+ export declare const ObjectKeywords: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=object-keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-keywords.d.ts","sourceRoot":"","sources":["../../lib/keywords/object-keywords.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7D,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAkO5D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "../index";
2
+ export declare const OtherKeywords: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=other-keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"other-keywords.d.ts","sourceRoot":"","sources":["../../lib/keywords/other-keywords.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAoP3D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "../index";
2
+ export declare const StringKeywords: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=string-keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-keywords.d.ts","sourceRoot":"","sources":["../../lib/keywords/string-keywords.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAgK5D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "./index";
2
+ export declare const keywords: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=keywords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keywords.d.ts","sourceRoot":"","sources":["../lib/keywords.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAMtD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { ValidatorFunction } from "./index";
2
+ export declare const Types: Record<string, ValidatorFunction>;
3
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5C,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CA4KnD,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { ValidationErrorProps } from "./index";
2
+ export declare class ValidationError extends Error {
3
+ name: string;
4
+ pointer: string;
5
+ message: string;
6
+ value: any;
7
+ code: string;
8
+ constructor(message: string, options?: ValidationErrorProps);
9
+ }
10
+ export declare const defaultValidator: (schema: any, data: any, pointer: any) => ValidationError[];
11
+ export declare function deepEqual(obj: Array<any> | Record<string, any>, other: Array<any> | Record<string, any>): boolean;
12
+ export declare function isObject(data: any): boolean;
13
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE7D,qBAAa,eAAgB,SAAQ,KAAK;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,GAAG,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;gBAGX,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAIR;CASJ;AAED,eAAO,MAAM,gBAAgB,6DAQ5B,CAAC;AAEF,wBAAgB,SAAS,CACvB,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,WAoCxC;AAED,wBAAgB,QAAQ,CAAC,IAAI,KAAA,WAE5B"}
package/lib/formats.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { FormatFunction, ValidationError, ValidatorFunction } from "./utils";
2
+
3
+ const RegExps = {
4
+ "date-time":
5
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/,
6
+ uri: /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/,
7
+ email:
8
+ /^[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])?)*$/,
9
+ ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
10
+ ipv6: /^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?=(?:[0-9a-fA-F]{0,4}:){0,7}[0-9a-fA-F]{0,4}(?![:.\w]))(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:))$/,
11
+ hostname: /^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*$/
12
+ };
13
+
14
+ function notImplementedFormat(data: any) {
15
+ throw new ValidationError(
16
+ `Format "${data}" is not implemented yet. Please open an issue on GitHub.`
17
+ );
18
+
19
+ return false;
20
+ }
21
+
22
+ export const Formats: Record<string, FormatFunction> = {
23
+ ["date-time"](data) {
24
+ return RegExps["date-time"].test(data);
25
+ },
26
+ uri(data) {
27
+ return RegExps.uri.test(data);
28
+ },
29
+ email(data) {
30
+ return RegExps.email.test(data);
31
+ },
32
+ ipv4(data) {
33
+ return RegExps.ipv4.test(data);
34
+ },
35
+ ipv6(data) {
36
+ return RegExps.ipv6.test(data);
37
+ },
38
+ hostname(data) {
39
+ return RegExps.hostname.test(data);
40
+ },
41
+
42
+ // Not supported yet
43
+ time: notImplementedFormat,
44
+ date: notImplementedFormat,
45
+ duration: notImplementedFormat,
46
+ "idn-email": notImplementedFormat,
47
+ "idn-hostname": notImplementedFormat,
48
+ uuid: notImplementedFormat,
49
+ "uri-reference": notImplementedFormat,
50
+ iri: notImplementedFormat,
51
+ "iri-reference": notImplementedFormat,
52
+ "uri-template": notImplementedFormat,
53
+ "json-pointer": notImplementedFormat,
54
+ "relative-json-pointer": notImplementedFormat,
55
+ regex: notImplementedFormat
56
+ };
package/lib/index.ts ADDED
@@ -0,0 +1,308 @@
1
+ import { ValidationError, isObject } from "./utils";
2
+
3
+ import { Formats } from "./formats";
4
+ import { Types } from "./types";
5
+ import { keywords } from "./keywords";
6
+
7
+ export interface ValidationErrorProps {
8
+ pointer: string;
9
+ value: any;
10
+ code: string;
11
+ }
12
+
13
+ export interface Result {
14
+ valid: boolean;
15
+ errors: ValidationError[];
16
+ data: any;
17
+ }
18
+
19
+ export interface ValidatorFunction {
20
+ (
21
+ schema: CompiledSchema,
22
+ data: any,
23
+ pointer: string,
24
+ schemaShieldInstance: SchemaShield
25
+ ): Result;
26
+ }
27
+
28
+ export interface FormatFunction {
29
+ (data: any): boolean;
30
+ }
31
+
32
+ export interface CompiledSchema {
33
+ pointer: string;
34
+ validator?: ValidatorFunction;
35
+ type?: string;
36
+ validators?: ValidatorFunction[];
37
+ keywords?: Record<string, ValidatorFunction>;
38
+ [key: string]: any;
39
+ }
40
+
41
+ export interface Validator {
42
+ (data: any): Result;
43
+ compiledSchema: CompiledSchema;
44
+ }
45
+
46
+ export class SchemaShield {
47
+ types = new Map<string, ValidatorFunction>();
48
+ formats = new Map<string, FormatFunction>();
49
+ keywords = new Map<string, ValidatorFunction>();
50
+
51
+ constructor() {
52
+ for (const type in Types) {
53
+ this.addType(type, Types[type]);
54
+ }
55
+
56
+ for (const keyword in keywords) {
57
+ this.addKeyword(keyword, keywords[keyword]);
58
+ }
59
+
60
+ for (const format in Formats) {
61
+ this.addFormat(format, Formats[format]);
62
+ }
63
+ }
64
+
65
+ addType(name: string, validator: ValidatorFunction) {
66
+ this.types.set(name, validator);
67
+ }
68
+
69
+ addFormat(name: string, validator: FormatFunction) {
70
+ this.formats.set(name, validator);
71
+ }
72
+
73
+ addKeyword(name: string, validator: ValidatorFunction) {
74
+ this.keywords.set(name, validator);
75
+ }
76
+
77
+ compile(schema: any): Validator {
78
+ const compiledSchema = this.compileSchema(schema, "#");
79
+ const schemaShield = this;
80
+
81
+ function validate(data: any) {
82
+ return compiledSchema.validator(compiledSchema, data, "#", schemaShield);
83
+ }
84
+
85
+ validate.compiledSchema = compiledSchema;
86
+
87
+ return validate;
88
+ }
89
+
90
+ private compileSchema(
91
+ schema: Partial<CompiledSchema>,
92
+ pointer
93
+ ): CompiledSchema {
94
+ if (typeof schema !== "object" || schema === null) {
95
+ throw new ValidationError("Schema is not an object", {
96
+ pointer,
97
+ value: schema,
98
+ code: "SCHEMA_NOT_OBJECT"
99
+ });
100
+ }
101
+
102
+ const compiledSchema = {
103
+ ...schema,
104
+ pointer
105
+ };
106
+
107
+ if ("type" in compiledSchema) {
108
+ const types = Array.isArray(compiledSchema.type)
109
+ ? compiledSchema.type
110
+ : compiledSchema.type.split(",").map((t) => t.trim());
111
+
112
+ compiledSchema.validators = types
113
+ .filter((type) => this.types.has(type))
114
+ .map((type) => this.types.get(type));
115
+ }
116
+
117
+ // Compile schema type
118
+ const validator: ValidatorFunction = (
119
+ schema: any,
120
+ data: any,
121
+ pointer: string
122
+ ) => {
123
+ if (typeof data === "undefined") {
124
+ if (pointer === "#") {
125
+ return {
126
+ valid: false,
127
+ errors: [
128
+ new ValidationError("Data is undefined", {
129
+ pointer,
130
+ value: data,
131
+ code: "DATA_UNDEFINED"
132
+ })
133
+ ],
134
+ data
135
+ };
136
+ }
137
+ }
138
+
139
+ let finalData = data;
140
+ const typeErrorsResult = this.validateTypes(schema, finalData, pointer);
141
+ if (typeErrorsResult.valid === false) {
142
+ return typeErrorsResult;
143
+ }
144
+ finalData = typeErrorsResult.data;
145
+
146
+ return this.validateKeywords(schema, finalData, pointer);
147
+ };
148
+
149
+ compiledSchema.validator = validator;
150
+
151
+ // Recursively compile sub schemas
152
+ for (let key in schema) {
153
+ // Skip type as it is already compiled
154
+ if (key === "type") {
155
+ continue;
156
+ }
157
+
158
+ if (this.keywords.has(key)) {
159
+ const validator = this.keywords.get(key);
160
+ compiledSchema.keywords = compiledSchema.keywords || {};
161
+ compiledSchema.keywords[key] = validator;
162
+ }
163
+
164
+ if (Array.isArray(schema[key])) {
165
+ this.handleArraySchema(key, schema, pointer, compiledSchema);
166
+ continue;
167
+ }
168
+
169
+ if (isObject(schema[key])) {
170
+ this.handleObjectSchema(key, schema, pointer, compiledSchema);
171
+ continue;
172
+ }
173
+ }
174
+
175
+ return compiledSchema;
176
+ }
177
+
178
+ private handleArraySchema(
179
+ key: string,
180
+ schema: any,
181
+ pointer: string,
182
+ compiledSchema: any
183
+ ) {
184
+ compiledSchema[key] = schema[key].map((subSchema, index) => {
185
+ if (typeof subSchema === "object" && subSchema !== null) {
186
+ if ("type" in subSchema) {
187
+ return this.compileSchema(subSchema, `${pointer}/${key}/${index}`);
188
+ }
189
+
190
+ for (let subKey in subSchema) {
191
+ if (this.keywords.has(subKey)) {
192
+ return this.compileSchema(subSchema, `${pointer}/${key}/${index}`);
193
+ }
194
+ }
195
+ }
196
+ return subSchema;
197
+ });
198
+ }
199
+
200
+ private handleObjectSchema(
201
+ key: string,
202
+ schema: any,
203
+ pointer: string,
204
+ compiledSchema: any
205
+ ) {
206
+ if ("type" in schema[key]) {
207
+ compiledSchema[key] = this.compileSchema(
208
+ schema[key],
209
+ `${pointer}/${key}`
210
+ );
211
+ return;
212
+ }
213
+
214
+ for (let subKey in schema[key]) {
215
+ compiledSchema[key] = compiledSchema[key] || {};
216
+
217
+ if (this.keywords.has(subKey)) {
218
+ compiledSchema[key][subKey] = this.compileSchema(
219
+ schema[key][subKey],
220
+ `${pointer}/${subKey}`
221
+ );
222
+ continue;
223
+ }
224
+
225
+ if (typeof schema[key][subKey] === "object") {
226
+ if ("type" in schema[key][subKey]) {
227
+ compiledSchema[key][subKey] = this.compileSchema(
228
+ schema[key][subKey],
229
+ `${pointer}/${key}/${subKey}`
230
+ );
231
+ continue;
232
+ }
233
+
234
+ for (let subSubKey in schema[key][subKey]) {
235
+ if (this.keywords.has(subSubKey)) {
236
+ compiledSchema[key][subKey] = this.compileSchema(
237
+ schema[key][subKey],
238
+ `${pointer}/${key}/${subKey}`
239
+ );
240
+ continue;
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ private validateTypes(schema: CompiledSchema, data, pointer): Result {
248
+ if (
249
+ typeof data === "undefined" ||
250
+ !Array.isArray(schema.validators) ||
251
+ schema.validators.length === 0
252
+ ) {
253
+ return {
254
+ valid: true,
255
+ errors: [],
256
+ data
257
+ };
258
+ }
259
+
260
+ let errors = [];
261
+ let finalData = data;
262
+
263
+ for (let schemaValidator of schema.validators) {
264
+ const schemaResult = schemaValidator(schema, data, pointer, this);
265
+
266
+ finalData = schemaResult.data;
267
+
268
+ if (schemaResult.valid) {
269
+ return schemaResult;
270
+ }
271
+
272
+ errors = schemaResult.errors;
273
+ }
274
+
275
+ return {
276
+ valid: errors.length === 0,
277
+ errors,
278
+ data: finalData
279
+ };
280
+ }
281
+
282
+ private validateKeywords(schema: CompiledSchema, data, pointer): Result {
283
+ const errors = [];
284
+ let finalData = data;
285
+
286
+ if ("keywords" in schema) {
287
+ for (let keyword in schema.keywords) {
288
+ const keywordValidator: ValidatorFunction = schema.keywords[keyword];
289
+ const keywordResult = keywordValidator(
290
+ schema,
291
+ finalData,
292
+ pointer,
293
+ this
294
+ );
295
+ finalData = keywordResult.data;
296
+ if (!keywordResult.valid) {
297
+ errors.push(...keywordResult.errors);
298
+ }
299
+ }
300
+ }
301
+
302
+ return {
303
+ valid: errors.length === 0,
304
+ errors,
305
+ data: finalData
306
+ };
307
+ }
308
+ }
@@ -0,0 +1,204 @@
1
+ import { CompiledSchema, ValidatorFunction } from "../index";
2
+ import { ValidationError, isObject } from "../utils";
3
+
4
+ export const ArrayKeywords: Record<string, ValidatorFunction> = {
5
+ items(schema, data, pointer, schemaShieldInstance) {
6
+ if (!Array.isArray(data)) {
7
+ return { valid: true, errors: [], data };
8
+ }
9
+
10
+ const errors = [];
11
+ let finalData = [...data];
12
+ if (Array.isArray(schema.items)) {
13
+ for (let i = 0; i < schema.items.length; i++) {
14
+ if (typeof schema.items[i] === "boolean") {
15
+ if (schema.items[i] === false && typeof data[i] !== "undefined") {
16
+ errors.push(
17
+ new ValidationError("Array item is not allowed", {
18
+ pointer: `${pointer}/${i}`,
19
+ value: data[i],
20
+ code: "ARRAY_ITEM_NOT_ALLOWED"
21
+ })
22
+ );
23
+ }
24
+ continue;
25
+ }
26
+
27
+ const { validator } = schema.items[i] as CompiledSchema;
28
+ if (!validator) {
29
+ continue;
30
+ }
31
+ const validatorResult = validator(
32
+ schema.items[i],
33
+ finalData[i],
34
+ `${pointer}/${i}`,
35
+ schemaShieldInstance
36
+ );
37
+
38
+ finalData[i] = validatorResult.data;
39
+
40
+ if (!validatorResult.valid) {
41
+ errors.push(...validatorResult.errors);
42
+ }
43
+ }
44
+ } else if (typeof schema.items === "boolean") {
45
+ if (schema.items === false && data.length > 0) {
46
+ errors.push(
47
+ new ValidationError("Array is not allowed", {
48
+ pointer,
49
+ value: data,
50
+ code: "ARRAY_NOT_ALLOWED"
51
+ })
52
+ );
53
+ }
54
+ } else {
55
+ const { validator } = schema.items as CompiledSchema;
56
+ if (!validator) {
57
+ return { valid: true, errors: [], data };
58
+ }
59
+
60
+ for (let i = 0; i < finalData.length; i++) {
61
+ const validatorErrors = validator(
62
+ schema.items,
63
+ finalData[i],
64
+ `${pointer}/${i}`,
65
+ schemaShieldInstance
66
+ );
67
+
68
+ finalData[i] = validatorErrors.data;
69
+
70
+ if (!validatorErrors.valid) {
71
+ errors.push(...validatorErrors.errors);
72
+ }
73
+ }
74
+ }
75
+
76
+ return { valid: errors.length === 0, errors, data: finalData };
77
+ },
78
+
79
+ minItems(schema, data, pointer) {
80
+ if (!Array.isArray(data) || data.length >= schema.minItems) {
81
+ return { valid: true, errors: [], data };
82
+ }
83
+
84
+ return {
85
+ valid: false,
86
+ errors: [
87
+ new ValidationError("Array is too short", {
88
+ pointer,
89
+ value: data,
90
+ code: "ARRAY_TOO_SHORT"
91
+ })
92
+ ],
93
+ data
94
+ };
95
+ },
96
+
97
+ maxItems(schema, data, pointer) {
98
+ if (!Array.isArray(data) || data.length <= schema.maxItems) {
99
+ return { valid: true, errors: [], data };
100
+ }
101
+
102
+ return {
103
+ valid: false,
104
+ errors: [
105
+ new ValidationError("Array is too long", {
106
+ pointer,
107
+ value: data,
108
+ code: "ARRAY_TOO_LONG"
109
+ })
110
+ ],
111
+ data
112
+ };
113
+ },
114
+
115
+ additionalItems(schema, data, pointer, schemaShieldInstance) {
116
+ if (!Array.isArray(data) || !schema.items || !Array.isArray(schema.items)) {
117
+ return { valid: true, errors: [], data };
118
+ }
119
+
120
+ if (schema.additionalItems === false) {
121
+ if (data.length > schema.items.length) {
122
+ return {
123
+ valid: false,
124
+ errors: [
125
+ new ValidationError("Array has too many items", {
126
+ pointer,
127
+ value: data,
128
+ code: "ARRAY_TOO_MANY_ITEMS"
129
+ })
130
+ ],
131
+ data
132
+ };
133
+ }
134
+
135
+ return { valid: true, errors: [], data };
136
+ }
137
+
138
+ const errors = [];
139
+ let finalData = [...data];
140
+ if (typeof schema.additionalItems === "object") {
141
+ for (let i = schema.items.length; i < finalData.length; i++) {
142
+ const { validator } = schema.additionalItems as CompiledSchema;
143
+ const validatorResult = validator(
144
+ schema.additionalItems,
145
+ finalData[i],
146
+ `${pointer}/${i}`,
147
+ schemaShieldInstance
148
+ );
149
+ if (!validatorResult.valid) {
150
+ errors.push(...validatorResult.errors);
151
+ }
152
+ finalData[i] = validatorResult.data;
153
+ }
154
+ }
155
+
156
+ return { valid: errors.length === 0, errors, data: finalData };
157
+ },
158
+
159
+ uniqueItems(schema, data, pointer) {
160
+ if (!Array.isArray(data) || !schema.uniqueItems) {
161
+ return { valid: true, errors: [], data };
162
+ }
163
+
164
+ const unique = new Set();
165
+
166
+ for (const item of data) {
167
+ let itemStr = item;
168
+
169
+ // Change string to "string" to avoid false positives
170
+ if (typeof item === "string") {
171
+ itemStr = `"${item}"`;
172
+
173
+ // Sort object keys to avoid false positives
174
+ } else if (isObject(item)) {
175
+ const keys = Object.keys(item).sort();
176
+ const sorted = {};
177
+ for (let i = 0; i < keys.length; i++) {
178
+ sorted[keys[i]] = item[keys[i]];
179
+ }
180
+ itemStr = JSON.stringify(sorted);
181
+ } else if (Array.isArray(item)) {
182
+ itemStr = JSON.stringify(item);
183
+ }
184
+
185
+ if (unique.has(itemStr)) {
186
+ return {
187
+ valid: false,
188
+ errors: [
189
+ new ValidationError("Array items are not unique", {
190
+ pointer,
191
+ value: data,
192
+ code: "ARRAY_ITEMS_NOT_UNIQUE"
193
+ })
194
+ ],
195
+ data
196
+ };
197
+ } else {
198
+ unique.add(itemStr);
199
+ }
200
+ }
201
+
202
+ return { valid: true, errors: [], data };
203
+ }
204
+ };