@hestia-earth/schema-validation 37.1.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 ADDED
@@ -0,0 +1,2 @@
1
+ export * from './validate';
2
+ export * from './validate-jsonld';
@@ -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
+ });
@@ -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.1.0",
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
  }