@hestia-earth/schema-validation 37.0.0 → 37.2.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/esm/index.js +2 -0
- package/esm/validate-jsonld.js +56 -0
- package/esm/validate.js +82 -0
- package/esm/validators/arraySameSize.js +27 -0
- package/esm/validators/datePattern.js +57 -0
- package/esm/validators/dateTimePattern.js +32 -0
- package/esm/validators/geojson.js +28 -0
- package/esm/validators/index.js +21 -0
- package/esm/validators/matrixSameSize.js +23 -0
- package/esm/validators/sumIs100.js +25 -0
- package/esm/validators/sumMax100.js +25 -0
- package/esm/validators/uniqueArrayItem.js +43 -0
- package/esm/validators/uniquePrimaryItem.js +24 -0
- package/package.json +17 -1
package/esm/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { readdirSync, readFileSync, lstatSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { validator } from './validate';
|
|
13
|
+
const [folder, strictMode] = process.argv.slice(2);
|
|
14
|
+
const domain = 'https://www.hestia.earth';
|
|
15
|
+
const loadFile = (filename) => JSON.parse(readFileSync(filename, 'utf8'));
|
|
16
|
+
const extensions = ['jsonld', 'json', 'hestia'];
|
|
17
|
+
const findFiles = (directory) => readdirSync(directory)
|
|
18
|
+
.map(path => {
|
|
19
|
+
const isDirectory = lstatSync(join(directory, path)).isDirectory();
|
|
20
|
+
return isDirectory
|
|
21
|
+
? findFiles(join(directory, path))
|
|
22
|
+
: extensions.includes(path.split('.')[1])
|
|
23
|
+
? join(directory, path)
|
|
24
|
+
: null;
|
|
25
|
+
})
|
|
26
|
+
.flat()
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
export const run = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
const contentValidator = validator(domain, strictMode === 'true');
|
|
30
|
+
const jsonldFiles = findFiles(folder);
|
|
31
|
+
const results = [];
|
|
32
|
+
for (const filepath of jsonldFiles) {
|
|
33
|
+
console.log('Validating', filepath);
|
|
34
|
+
const content = loadFile(filepath);
|
|
35
|
+
const nodes = (Array.isArray(content) ? content : 'nodes' in content ? content.nodes : [content])
|
|
36
|
+
.map(node => (node['@type'] || node.type ? node : null))
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
const allErrors = [];
|
|
39
|
+
for (const node of nodes) {
|
|
40
|
+
const { errors } = yield contentValidator(node);
|
|
41
|
+
allErrors.push(errors);
|
|
42
|
+
}
|
|
43
|
+
const success = !allErrors.some(v => v.length);
|
|
44
|
+
if (!success) {
|
|
45
|
+
results.push({
|
|
46
|
+
filepath,
|
|
47
|
+
success,
|
|
48
|
+
errors: allErrors
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (results.length) {
|
|
53
|
+
results.map(value => console.error(JSON.stringify(value, null, 2)));
|
|
54
|
+
throw new Error('Validation errors: see above for more details');
|
|
55
|
+
}
|
|
56
|
+
});
|
package/esm/validate.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
21
|
+
import * as Ajv from 'ajv';
|
|
22
|
+
import { isTypeNode } from '@hestia-earth/schema';
|
|
23
|
+
import { loadSchemas } from '@hestia-earth/json-schema';
|
|
24
|
+
import { init as initValidators } from './validators';
|
|
25
|
+
const draft4 = require('ajv/lib/refs/json-schema-draft-04.json');
|
|
26
|
+
const existingNodeRequired = Object.freeze({ required: ['@id', '@type'] });
|
|
27
|
+
const validateSchemaType = (schemas, content) => {
|
|
28
|
+
const type = content['@type'] || content.type;
|
|
29
|
+
if (!(type in schemas)) {
|
|
30
|
+
throw new Error(`Unknown or invalid type "${type}"`);
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
const schemaData = ({ required, properties }) => (Object.assign(Object.assign({}, ((required === null || required === void 0 ? void 0 : required.length) ? { required } : {})), (properties ? { properties } : {})));
|
|
35
|
+
// using `verbose: true` includes data which we don't need
|
|
36
|
+
const cleanErrors = (errors) => errors.map((_a) => {
|
|
37
|
+
var { data, schema, parentSchema } = _a, error = __rest(_a, ["data", "schema", "parentSchema"]);
|
|
38
|
+
return (Object.assign(Object.assign(Object.assign({}, error), (schema ? { schema: schemaData(schema) } : {})), (error.message === 'should NOT be valid' ? { schema } : {})));
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Validate a single node. Function is asynchronous to return a list of `success` and a list of `errors`.
|
|
42
|
+
*
|
|
43
|
+
* @param ajv The AJV object. Use `initAjv()` or pass your own.
|
|
44
|
+
* @param schemas The schema definitions. Use `loadSchemas` from `@hestia-earth/json-schema` or pass your own.
|
|
45
|
+
* @returns
|
|
46
|
+
*/
|
|
47
|
+
export const validateContent = (ajv, schemas) => (content) => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
return validateSchemaType(schemas, content) && {
|
|
49
|
+
success: yield ajv.validate(schemas[content['@type'] || content.type], content),
|
|
50
|
+
errors: cleanErrors(ajv.errors || [])
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
export const initAjv = () => {
|
|
54
|
+
const ajv = new Ajv({
|
|
55
|
+
schemaId: 'auto',
|
|
56
|
+
allErrors: true,
|
|
57
|
+
verbose: true
|
|
58
|
+
});
|
|
59
|
+
ajv.addMetaSchema(draft4);
|
|
60
|
+
initValidators(ajv);
|
|
61
|
+
return ajv;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Create a validator instance. Call with a Node to validate it.
|
|
65
|
+
*
|
|
66
|
+
* @param domain The domain of the validator (defaults to Hestia's website)
|
|
67
|
+
* @param strictMode Allow validating non-existing nodes, i.e. without unique `@id`.
|
|
68
|
+
* @returns Function to validate a Node. Use `await validator()(node)`
|
|
69
|
+
*/
|
|
70
|
+
export const validator = (domain = 'https://www.hestia.earth', strictMode = true) => {
|
|
71
|
+
const ajv = initAjv();
|
|
72
|
+
const schemas = loadSchemas();
|
|
73
|
+
Object.keys(schemas).map(schemaName => {
|
|
74
|
+
const schema = schemas[schemaName];
|
|
75
|
+
schema.properties = Object.assign({ '@context': {
|
|
76
|
+
type: 'string'
|
|
77
|
+
} }, schema.properties);
|
|
78
|
+
schema.oneOf = strictMode && isTypeNode(schemaName) ? [existingNodeRequired] : schema.oneOf;
|
|
79
|
+
ajv.addSchema(schema, `${domain}/schema/json-schema/${schemaName}#`);
|
|
80
|
+
});
|
|
81
|
+
return validateContent(ajv, schemas);
|
|
82
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const keyword = 'arraySameSize';
|
|
2
|
+
export const validate = (schema, data, _parentSchema, dataPath, parentData) => {
|
|
3
|
+
const current = data.length;
|
|
4
|
+
const errors = schema
|
|
5
|
+
.filter(key => key in parentData)
|
|
6
|
+
.map(key => {
|
|
7
|
+
const expected = parentData[key].length;
|
|
8
|
+
return current !== expected
|
|
9
|
+
? {
|
|
10
|
+
dataPath,
|
|
11
|
+
keyword,
|
|
12
|
+
schemaPath: '#/invalid',
|
|
13
|
+
message: `must contain as many items as ${key}s`,
|
|
14
|
+
params: { keyword, current, expected }
|
|
15
|
+
}
|
|
16
|
+
: null;
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
// only return the first 100 errors to limit the memory footprint
|
|
20
|
+
validate.errors = errors.slice(0, 100);
|
|
21
|
+
return errors.length === 0;
|
|
22
|
+
};
|
|
23
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
24
|
+
type: 'array',
|
|
25
|
+
errors: 'full',
|
|
26
|
+
validate
|
|
27
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const formatDate = (date) => new Date(`${new Date(date).toJSON().substring(0, 10)}T12:00:00.000Z`);
|
|
2
|
+
const validLength = (value, length) => !value || value.length === length;
|
|
3
|
+
/**
|
|
4
|
+
* JavaScript does a lenient parsing of dates, so even `2000-30-30` will return a date.
|
|
5
|
+
* This makes sure the parse date is the date as the given date.
|
|
6
|
+
*
|
|
7
|
+
* @param value
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
const isValidDateRange = (value) => {
|
|
11
|
+
const values = value.substring(0, 10).split('-');
|
|
12
|
+
const year = Number(values[0]) || 1970;
|
|
13
|
+
const month = Number(values[1]) || 1;
|
|
14
|
+
const day = Number(values[2]) || 1;
|
|
15
|
+
const date = new Date(year, month - 1, day, 12); // Months are 0-indexed
|
|
16
|
+
return [
|
|
17
|
+
validLength(values[0], 4),
|
|
18
|
+
validLength(values[1], 2),
|
|
19
|
+
validLength(values[2], 2),
|
|
20
|
+
date.getFullYear() === year,
|
|
21
|
+
date.getMonth() === month - 1,
|
|
22
|
+
date.getDate() === day
|
|
23
|
+
].every(Boolean);
|
|
24
|
+
};
|
|
25
|
+
export const isFutureDate = (date) => formatDate(date) > formatDate(new Date());
|
|
26
|
+
export const isValidDate = (date, withTime = true) => (date.length <= 10 || withTime) && isFinite(new Date(date).getTime()) && isValidDateRange(date);
|
|
27
|
+
export const keyword = 'datePattern';
|
|
28
|
+
export const validate = (_schema, value, _parentSchema, dataPath) => {
|
|
29
|
+
const values = Array.isArray(value) ? value : [value];
|
|
30
|
+
const errors = values
|
|
31
|
+
.map((v, index) => isValidDate(v, false)
|
|
32
|
+
? isFutureDate(v)
|
|
33
|
+
? {
|
|
34
|
+
dataPath: `${dataPath}${Array.isArray(value) ? `[${index}]` : ''}`,
|
|
35
|
+
keyword,
|
|
36
|
+
schemaPath: '#/invalid',
|
|
37
|
+
message: 'date cannot be in the future',
|
|
38
|
+
params: {}
|
|
39
|
+
}
|
|
40
|
+
: null
|
|
41
|
+
: {
|
|
42
|
+
dataPath: `${dataPath}${Array.isArray(value) ? `[${index}]` : ''}`,
|
|
43
|
+
keyword,
|
|
44
|
+
schemaPath: '#/invalid',
|
|
45
|
+
message: 'not a valid date',
|
|
46
|
+
params: {}
|
|
47
|
+
})
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
// only return the first 10 errors to limit the memory footprint
|
|
50
|
+
validate.errors = errors.slice(0, 10);
|
|
51
|
+
return errors.length === 0;
|
|
52
|
+
};
|
|
53
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
54
|
+
type: ['string', 'array'],
|
|
55
|
+
errors: 'full',
|
|
56
|
+
validate
|
|
57
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isValidDate, isFutureDate } from './datePattern';
|
|
2
|
+
export const keyword = 'dateTimePattern';
|
|
3
|
+
export const validate = (_schema, value, _parentSchema, dataPath) => {
|
|
4
|
+
const values = Array.isArray(value) ? value : [value];
|
|
5
|
+
const errors = values
|
|
6
|
+
.map((v, index) => isValidDate(v)
|
|
7
|
+
? isFutureDate(v)
|
|
8
|
+
? {
|
|
9
|
+
dataPath: `${dataPath}${Array.isArray(value) ? `[${index}]` : ''}`,
|
|
10
|
+
keyword,
|
|
11
|
+
schemaPath: '#/invalid',
|
|
12
|
+
message: 'date cannot be in the future',
|
|
13
|
+
params: {}
|
|
14
|
+
}
|
|
15
|
+
: null
|
|
16
|
+
: {
|
|
17
|
+
dataPath: `${dataPath}${Array.isArray(value) ? `[${index}]` : ''}`,
|
|
18
|
+
keyword,
|
|
19
|
+
schemaPath: '#/invalid',
|
|
20
|
+
message: 'not a valid date',
|
|
21
|
+
params: {}
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
// only return the first 10 errors to limit the memory footprint
|
|
25
|
+
validate.errors = errors.slice(0, 10);
|
|
26
|
+
return errors.length === 0;
|
|
27
|
+
};
|
|
28
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
29
|
+
type: ['string', 'array'],
|
|
30
|
+
errors: 'full',
|
|
31
|
+
validate
|
|
32
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as gjv from 'geojson-validation';
|
|
2
|
+
export const keyword = 'geojson';
|
|
3
|
+
const invalidCoordinatesError = 'invalid coordinates';
|
|
4
|
+
const maxLongitude = 180;
|
|
5
|
+
const maxLatitude = 90;
|
|
6
|
+
gjv.define('Position', ([longitude, latitude]) => [longitude < maxLongitude, longitude > -maxLongitude, latitude < maxLatitude, latitude > -maxLatitude].every(Boolean) || [invalidCoordinatesError]);
|
|
7
|
+
export const validate = (_schema, value, _parentSchema, dataPath) => {
|
|
8
|
+
const validationErrors = gjv.valid(value, true);
|
|
9
|
+
const isInvalidCoordinates = validationErrors.every(error => error.includes(invalidCoordinatesError));
|
|
10
|
+
const errors = [
|
|
11
|
+
validationErrors.length === 0
|
|
12
|
+
? null
|
|
13
|
+
: {
|
|
14
|
+
dataPath,
|
|
15
|
+
keyword,
|
|
16
|
+
schemaPath: '#/invalid',
|
|
17
|
+
message: 'not a valid GeoJSON',
|
|
18
|
+
params: Object.assign({}, (isInvalidCoordinates ? { invalidCoordinates: true } : {}))
|
|
19
|
+
}
|
|
20
|
+
].filter(Boolean);
|
|
21
|
+
validate.errors = errors;
|
|
22
|
+
return validationErrors.length === 0;
|
|
23
|
+
};
|
|
24
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
25
|
+
type: 'object',
|
|
26
|
+
errors: 'full',
|
|
27
|
+
validate
|
|
28
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { keyword as arraySameSize, init as arraySameSizeInit } from './arraySameSize';
|
|
2
|
+
import { keyword as datePattern, init as datePatternInit } from './datePattern';
|
|
3
|
+
import { keyword as dateTimePattern, init as dateTimePatternInit } from './dateTimePattern';
|
|
4
|
+
import { keyword as geojson, init as geojsonInit } from './geojson';
|
|
5
|
+
import { keyword as matrixSameSize, init as matrixSameSizeInit } from './matrixSameSize';
|
|
6
|
+
import { keyword as sumIs100, init as sumIs100Init } from './sumIs100';
|
|
7
|
+
import { keyword as sumMax100, init as sumMax100Init } from './sumMax100';
|
|
8
|
+
import { keyword as uniqueArrayItem, init as uniqueArrayItemInit } from './uniqueArrayItem';
|
|
9
|
+
import { keyword as uniquePrimaryItem, init as uniquePrimaryItemInit } from './uniquePrimaryItem';
|
|
10
|
+
export { arraySameSize, datePattern, dateTimePattern, geojson, matrixSameSize, sumIs100, sumMax100, uniqueArrayItem, uniquePrimaryItem };
|
|
11
|
+
export const init = (ajv) => {
|
|
12
|
+
arraySameSizeInit(ajv);
|
|
13
|
+
datePatternInit(ajv);
|
|
14
|
+
dateTimePatternInit(ajv);
|
|
15
|
+
geojsonInit(ajv);
|
|
16
|
+
matrixSameSizeInit(ajv);
|
|
17
|
+
sumIs100Init(ajv);
|
|
18
|
+
sumMax100Init(ajv);
|
|
19
|
+
uniqueArrayItemInit(ajv);
|
|
20
|
+
uniquePrimaryItemInit(ajv);
|
|
21
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const keyword = 'matrixSameSize';
|
|
2
|
+
export const validate = (schema, data, _parentSchema, dataPath, parentData) => {
|
|
3
|
+
const matrixFields = parentData[schema];
|
|
4
|
+
const matrixLength = matrixFields.length;
|
|
5
|
+
const isLengthCorrect = data.length === matrixLength && data.every(value => value.length === matrixLength);
|
|
6
|
+
validate.errors = [
|
|
7
|
+
isLengthCorrect
|
|
8
|
+
? null
|
|
9
|
+
: {
|
|
10
|
+
dataPath,
|
|
11
|
+
keyword,
|
|
12
|
+
schemaPath: '#/invalid',
|
|
13
|
+
message: `must contain as many items as ${schema}`,
|
|
14
|
+
params: { keyword, expected: matrixLength }
|
|
15
|
+
}
|
|
16
|
+
].filter(Boolean);
|
|
17
|
+
return validate.errors.length === 0;
|
|
18
|
+
};
|
|
19
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
20
|
+
type: 'array',
|
|
21
|
+
errors: 'full',
|
|
22
|
+
validate
|
|
23
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const keyword = 'sumIs100';
|
|
2
|
+
const parseValue = (value) => (typeof value === 'string' || typeof value == 'number' ? +value : 0);
|
|
3
|
+
export const validate = (schema, data, _parentSchema, dataPath = '') => {
|
|
4
|
+
const totals = Object.fromEntries(schema.map(key => [schema, data.reduce((prev, item) => prev + parseValue(item[key]), -1)]));
|
|
5
|
+
validate.errors = Object.entries(totals)
|
|
6
|
+
.map(([key, total]) => total === -1 || (total >= 98 && total <= 100)
|
|
7
|
+
? null
|
|
8
|
+
: {
|
|
9
|
+
dataPath,
|
|
10
|
+
keyword,
|
|
11
|
+
schemaPath: '#/invalid',
|
|
12
|
+
message: `sum of ${key} must be 100`,
|
|
13
|
+
params: {
|
|
14
|
+
current: total + 1,
|
|
15
|
+
expected: 100
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
return validate.errors.length === 0;
|
|
20
|
+
};
|
|
21
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
22
|
+
type: 'array',
|
|
23
|
+
errors: 'full',
|
|
24
|
+
validate
|
|
25
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const keyword = 'sumMax100';
|
|
2
|
+
const parseValue = (value) => (typeof value === 'string' || typeof value == 'number' ? +value : 0);
|
|
3
|
+
export const validate = (schema, data, _parentSchema, dataPath = '') => {
|
|
4
|
+
const totals = Object.fromEntries(schema.map(key => [schema, data.reduce((prev, item) => prev + parseValue(item[key]), 0)]));
|
|
5
|
+
validate.errors = Object.entries(totals)
|
|
6
|
+
.map(([key, total]) => total <= 100
|
|
7
|
+
? null
|
|
8
|
+
: {
|
|
9
|
+
dataPath,
|
|
10
|
+
keyword,
|
|
11
|
+
schemaPath: '#/invalid',
|
|
12
|
+
message: `sum of ${key} must be 100`,
|
|
13
|
+
params: {
|
|
14
|
+
current: total,
|
|
15
|
+
expected: 100
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
return validate.errors.length === 0;
|
|
20
|
+
};
|
|
21
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
22
|
+
type: 'array',
|
|
23
|
+
errors: 'full',
|
|
24
|
+
validate
|
|
25
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const getByKey = (prev, curr) => !curr || !prev
|
|
2
|
+
? prev
|
|
3
|
+
: Array.isArray(prev)
|
|
4
|
+
? prev.map(item => getItemValue(item, curr))
|
|
5
|
+
: prev
|
|
6
|
+
? prev[curr]
|
|
7
|
+
: undefined;
|
|
8
|
+
const getItemValue = (item, key) => key.split('.').reduce(getByKey, item);
|
|
9
|
+
export const keyword = 'uniqueArrayItem';
|
|
10
|
+
const findIdenticalIndexes = (values) => (value, index) => ({
|
|
11
|
+
index,
|
|
12
|
+
duplicatedIndexes: values
|
|
13
|
+
.map((otherValue, otherIndex) => [otherValue, otherIndex])
|
|
14
|
+
.filter(([otherValue, otherIndex]) => otherIndex !== index && otherValue === value)
|
|
15
|
+
.map(([_otherValue, otherIndex]) => otherIndex)
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Make sure empty JSON are not set as `'{}'`.
|
|
19
|
+
*
|
|
20
|
+
* @param value
|
|
21
|
+
* @returns
|
|
22
|
+
*/
|
|
23
|
+
const toString = (value) => Object.values(value).filter(Boolean).length ? JSON.stringify(value) : null;
|
|
24
|
+
export const validate = (schema, data, _parentSchema, dataPath = '') => {
|
|
25
|
+
const values = data.map(item => toString(schema.reduce((prev, key) => (Object.assign(Object.assign({}, prev), { [key]: getItemValue(item, key) })), {}))).filter(Boolean);
|
|
26
|
+
const indexes = values
|
|
27
|
+
.map(findIdenticalIndexes(values))
|
|
28
|
+
.filter(({ duplicatedIndexes }) => duplicatedIndexes.length > 0)
|
|
29
|
+
.sort();
|
|
30
|
+
validate.errors = indexes.map(({ index, duplicatedIndexes }) => ({
|
|
31
|
+
dataPath: `${dataPath}[${index}]`,
|
|
32
|
+
keyword,
|
|
33
|
+
schemaPath: '#/invalid',
|
|
34
|
+
message: 'every item in the list should be unique',
|
|
35
|
+
params: { keyword, keys: schema, duplicatedIndexes }
|
|
36
|
+
}));
|
|
37
|
+
return indexes.length === 0;
|
|
38
|
+
};
|
|
39
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
40
|
+
type: 'array',
|
|
41
|
+
errors: 'full',
|
|
42
|
+
validate
|
|
43
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const keyword = 'uniquePrimaryItem';
|
|
2
|
+
export const validate = (schema, data, _parentSchema, dataPath = '') => {
|
|
3
|
+
const primaryCount = data.filter(item => item.primary === true).length;
|
|
4
|
+
const errors = primaryCount <= 1
|
|
5
|
+
? []
|
|
6
|
+
: data
|
|
7
|
+
.map((v, index) => v.primary !== true
|
|
8
|
+
? null
|
|
9
|
+
: {
|
|
10
|
+
dataPath: `${dataPath}[${index}]`,
|
|
11
|
+
keyword,
|
|
12
|
+
schemaPath: '#/invalid',
|
|
13
|
+
message: 'can only contain one primary item',
|
|
14
|
+
params: {}
|
|
15
|
+
})
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
validate.errors = errors;
|
|
18
|
+
return errors.length === 0;
|
|
19
|
+
};
|
|
20
|
+
export const init = (ajv) => ajv.addKeyword(keyword, {
|
|
21
|
+
type: 'array',
|
|
22
|
+
errors: 'full',
|
|
23
|
+
validate
|
|
24
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hestia-earth/schema-validation",
|
|
3
|
-
"version": "37.
|
|
3
|
+
"version": "37.2.0",
|
|
4
4
|
"description": "HESTIA Schema Validation",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -28,5 +28,21 @@
|
|
|
28
28
|
"@hestia-earth/json-schema": "*",
|
|
29
29
|
"ajv": "^6.12.6",
|
|
30
30
|
"geojson-validation": "^1.0.2"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"module": "esm/index.js",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": "./esm/index.js",
|
|
37
|
+
"require": "./index.js"
|
|
38
|
+
},
|
|
39
|
+
"./validate-jsonld": {
|
|
40
|
+
"import": "./esm/validate-jsonld.js",
|
|
41
|
+
"require": "./validate-jsonld.js"
|
|
42
|
+
},
|
|
43
|
+
"./validate": {
|
|
44
|
+
"import": "./esm/validate.js",
|
|
45
|
+
"require": "./validate.js"
|
|
46
|
+
}
|
|
31
47
|
}
|
|
32
48
|
}
|