lib6/mapping.rulesfactories.js
/**
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { valueAs, optionalParameterConversion, defaultNameMapping, getRules } from './mapping.highlevel';
import { isNode, isPath, isRelationship } from './graph-types';
import { isPoint } from './spatial-types';
import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from './temporal-types';
import { isVector, vector } from './vector';
import { newError } from './error';
import Integer, { isInt } from './integer';
/**
* @property {function(rule: ?Rule)} asBoolean Create a {@link Rule} that validates the value is a Boolean.
*
* @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String.
*
* @property {function(rule: ?Rule & { isInteger?: boolean })} asNumber Create a {@link Rule} that validates the value is a {@link Number}.
*
* @property {function(rule: ?Rule & { acceptNumber?: boolean })} asBigInt Create a {@link Rule} that validates the value is a {@link BigInt}.
*
* @property {function(rule: ?Rule & { acceptNumber?: boolean })} asInteger Create a {@link Rule} that validates the value is an {@link Integer}.
*
* @property {function(rule: ?Rule)} asNode Create a {@link Rule} that validates the value is a {@link Node}.
*
* @property {function(rule: ?Rule)} asRelationship Create a {@link Rule} that validates the value is a {@link Relationship}.
*
* @property {function(rule: ?Rule)} asPath Create a {@link Rule} that validates the value is a {@link Path}.
*
* @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}.
*
* @property {function(rule: ?Rule & { stringify?: boolean })} asDuration Create a {@link Rule} that validates the value is a {@link Duration}.
*
* @property {function(rule: ?Rule & { stringify?: boolean })} asLocalTime Create a {@link Rule} that validates the value is a {@link LocalTime}.
*
* @property {function(rule: ?Rule & { stringify?: boolean })} asTime Create a {@link Rule} that validates the value is a {@link Time}.
*
* @property {function(rule: ?Rule & { stringify?: boolean, jsNativeDate?: boolean })} asDate Create a {@link Rule} that validates the value is a {@link Date}.
*
* @property {function(rule: ?Rule & { stringify?: boolean, jsNativeDate?: boolean })} asLocalDateTime Create a {@link Rule} that validates the value is a {@link LocalDateTime}.
*
* @property {function(rule: ?Rule & { stringify?: boolean, jsNativeDate?: boolean })} asDateTime Create a {@link Rule} that validates the value is a {@link DateTime}.
*
* @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List.
*
* @property {function(rule: ?Rule & { asTypedList?: boolean, dimension?: number, type?: VectorType })} asVector Create a {@link Rule} that validates the value is a Vector.
*
* @property {function(rules: Rules)} asObject Create a {@link Rule} for an object, allowing complex mapping of even nested results.
*/
export const rule = Object.freeze({
/**
* Create a {@link Rule} that validates the value is a Boolean.
*
* @param {Rule | undefined} rule Configurations for the rule
* @returns {Rule} A new rule for the value
*/
asBoolean(rule) {
return Object.assign({ validate: (value, field) => {
if (typeof value !== 'boolean') {
throw new TypeError(`${field} should be a boolean but received ${typeof value}`);
}
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a String.
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @param {Rule | undefined} rule Configurations for the rule
* @returns {Rule} A new rule for the value
*/
asString(rule) {
return Object.assign({ validate: (value, field) => {
if (typeof value !== 'string') {
throw new TypeError(`${field} should be a string but received ${typeof value}`);
}
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Number}.
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @param {Rule & { isInteger?: boolean } | undefined} rule Configurations for the rule.
* If `isInteger` is set to true, the created validate function will allow Integer values through, and the conversion functions will ensure results are return as numbers while parameters are transmitted as integers.
* @returns {Rule} A new rule for the value
*/
asNumber(rule) {
return Object.assign({ validate: (value, field) => {
if (isInt(value) && (rule === null || rule === void 0 ? void 0 : rule.isInteger) !== true) {
throw new TypeError('Number returned as Integer Object. To use asNumber mapping with Integers, set "isInteger" in rule configuration.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.isInteger) !== true && typeof value === 'bigint') {
throw new TypeError('Number returned as BigInt. To use asNumber mapping with integer values, set "isInteger" in rule configuration.');
}
if (typeof value !== 'number' && !(isInt(value) && (rule === null || rule === void 0 ? void 0 : rule.isInteger) === true)) {
throw new TypeError(`${field} should be a number but received ${typeof value}`);
}
if ((rule === null || rule === void 0 ? void 0 : rule.isInteger) === true && typeof value === 'number' && !Number.isInteger(value)) {
throw new TypeError(`${field} should be an integer value but received decimal number.`);
}
}, convert: (value) => {
if (typeof value === 'bigint') {
return Number(value);
}
if (isInt(value)) {
return value.toNumber();
}
return value;
}, parameterConversion: (value) => {
if ((rule === null || rule === void 0 ? void 0 : rule.isInteger) === true) {
return Integer.fromValue(value);
}
return value;
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link BigInt}.
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @returns {Rule} A new rule for the value
*/
asBigInt(rule) {
return Object.assign({ validate: (value, field) => {
if (typeof value !== 'bigint' && ((rule === null || rule === void 0 ? void 0 : rule.acceptNumber) !== true || typeof value !== 'number') && !isInt(value)) {
throw new TypeError(`${field} should be a bigint but received ${typeof value}`);
}
}, convert: (value) => {
if (typeof value === 'number') {
return BigInt(value);
}
if (isInt(value)) {
return value.toBigInt();
}
return value;
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is an {@link Integer}.
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @param {Rule & { acceptNumber?: boolean } | undefined} rule Configurations for the rule, if `acceptNumber` is set to true, the created validate function will allow Numbers through and the conversion functions will turn Numbers into Integers.
* @returns {Rule} A new rule for the value
*/
asInteger(rule) {
return Object.assign({ validate: (value, field) => {
if (typeof value !== 'bigint' && !isInt(value) && !(typeof value === 'number' && (rule === null || rule === void 0 ? void 0 : rule.acceptNumber) === true)) {
throw new TypeError(`${field} should be an Integer but received ${typeof value}`);
}
}, convert: (value) => {
if (typeof value === 'bigint') {
return Integer.fromValue(value);
}
return value;
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Node}.
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @example
* const actingJobsRules: Rules = {
* // Converts the person node to a Person object in accordance with provided rules
* person: neo4j.rule.asNode({
* convert: (node: Node) => node.as(Person, personRules)
* }),
* // Returns the movie node as a Node
* movie: neo4j.rule.asNode({}),
* }
*
* @param {Rule | undefined} rule Configurations for the rule
* @returns {Rule} A new rule for the value
*/
asNode(rule) {
return Object.assign({ validate: (value, field) => {
if (!isNode(value)) {
throw new TypeError(`${field} should be a Node but received ${typeof value}`);
}
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Relationship}.
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @example
* const actingJobsRules: Rules = {
* // Converts the role relationship to a Role object in accordance with provided rules
* role: neo4j.rule.asRelationship({
* convert: (rel: Relationship) => rel.as(Role, roleRules)
* }),
* // Returns the employment relationship as a Relationship
* employment: neo4j.rule.asRelationship({}),
* }
*
* @param {Rule | undefined} rule Configurations for the rule.
* @returns {Rule} A new rule for the value
*/
asRelationship(rule) {
return Object.assign({ validate: (value, field) => {
if (!isRelationship(value)) {
throw new TypeError(`${field} should be a Relationship but received ${typeof value}`);
}
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Path}
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @param {Rule | undefined} rule Configurations for the rule
* @returns {Rule} A new rule for the value
*/
asPath(rule) {
return Object.assign({ validate: (value, field) => {
if (!isPath(value)) {
throw new TypeError(`${field} should be a Path but received ${typeof value}`);
}
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Point}
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @param {Rule | undefined} rule Configurations for the rule
* @returns {Rule} A new rule for the value
*/
asPoint(rule) {
return Object.assign({ validate: (value, field) => {
if (!isPoint(value)) {
throw new TypeError(`${field} should be a Point but received ${typeof value}`);
}
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Duration}
*
* Optionally takes a {@link Rule}, in which case the returned rule will keep all fields of the one provided.
*
* @param {Rule & { stringify?: boolean } | undefined} rule Configurations for the rule. If `stringify` is set, the returned rule will have `convert` and `parameterConversion` functions which automatically convert between strings in user code and {@link Duration}s in the database.
* @returns {Rule} A new rule for the value
*/
asDuration(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, stringify can not be used in combination with custom conversion functions.');
}
return Object.assign({ validate: (value, field) => {
if (!isDuration(value)) {
throw new TypeError(`${field} should be a Duration but received ${typeof value}`);
}
}, convert: (value) => (rule === null || rule === void 0 ? void 0 : rule.stringify) === true ? value.toString() : value, parameterConversion: (rule === null || rule === void 0 ? void 0 : rule.stringify) === true ? (str) => Duration.fromString(str) : undefined }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link LocalTime}
*
* @param {Rule & { stringify?: boolean } | undefined} rule Configurations for the rule. If `stringify` is set, the returned rule will have `convert` and `parameterConversion` functions which automatically convert between strings in user code and {@link LocalTime}s in the database.
* @returns {Rule} A new rule for the value
*/
asLocalTime(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, stringify can not be used in combination with custom conversion functions.');
}
return Object.assign({ validate: (value, field) => {
if (!isLocalTime(value)) {
throw new TypeError(`${field} should be a LocalTime but received ${typeof value}`);
}
}, convert: (value) => (rule === null || rule === void 0 ? void 0 : rule.stringify) === true ? value.toString() : value, parameterConversion: (rule === null || rule === void 0 ? void 0 : rule.stringify) === true ? (str) => LocalTime.fromString(str) : undefined }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Time}
*
* @param {Rule & { stringify?: boolean } | undefined} rule Configurations for the rule. If `stringify` is set, the returned rule will have `convert` and `parameterConversion` functions which automatically convert between strings in user code and {@link Time}s in the database.
* @returns {Rule} A new rule for the value
*/
asTime(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, stringify can not be used in combination with custom conversion functions.');
}
return Object.assign({ validate: (value, field) => {
if (!isTime(value)) {
throw new TypeError(`${field} should be a Time but received ${typeof value}`);
}
}, convert: (value) => (rule === null || rule === void 0 ? void 0 : rule.stringify) === true ? value.toString() : value, parameterConversion: (rule === null || rule === void 0 ? void 0 : rule.stringify) === true ? (str) => Time.fromString(str) : undefined }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link Date}
*
* @param {Rule & { stringify?: boolean, jsNativeDate?: boolean } | undefined} rule Configurations for the rule. If `stringify`/`jsNativeDate` is set, the returned rule will have `convert` and `parameterConversion` functions which automatically convert between strings/JavaScript Dates in user code and {@link Date}s in the database.
* @returns {Rule} A new rule for the value
*/
asDate(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, stringify can not be used in combination with custom conversion functions.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, jsNativeDate can not be used in combination with custom conversion functions.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) === true && (rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) === true) {
throw newError('both stringify and jsNativeDate cannot be set; use one or neither');
}
let parameterConversion;
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) === true) {
parameterConversion = (str) => Date.fromString(str);
}
if ((rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) === true) {
parameterConversion = (standardDate) => Date.fromStandardDateLocal(standardDate);
}
return Object.assign({ validate: (value, field) => {
if (!isDate(value)) {
throw new TypeError(`${field} should be a Date but received ${typeof value}`);
}
}, convert: (value) => convertStdDate(value, rule), parameterConversion }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link LocalDateTime}
*
* @param {Rule & { stringify?: boolean, jsNativeDate?: boolean } | undefined} rule Configurations for the rule. If `stringify`/`jsNativeDate` is set, the returned rule will have `convert` and `parameterConversion` functions which automatically convert between strings/JavaScript Dates in user code and {@link LocalDateTime}s in the database.
* @returns {Rule} A new rule for the value
*/
asLocalDateTime(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, stringify can not be used in combination with custom conversion functions.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, jsNativeDate can not be used in combination with custom conversion functions.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) === true && (rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) === true) {
throw newError('both stringify and jsNativeDate cannot be set; use one or neither');
}
let parameterConversion;
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) === true) {
parameterConversion = (str) => LocalDateTime.fromString(str);
}
if ((rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) === true) {
parameterConversion = (standardDate) => LocalDateTime.fromStandardDate(standardDate);
}
return Object.assign({ validate: (value, field) => {
if (!isLocalDateTime(value)) {
throw new TypeError(`${field} should be a LocalDateTime but received ${typeof value}`);
}
}, convert: (value) => convertStdDate(value, rule), parameterConversion }, rule);
},
/**
* Create a {@link Rule} that validates the value is a {@link DateTime}
*
* @param {Rule & { stringify?: boolean, jsNativeDate?: boolean } | undefined} rule Configurations for the rule. If `stringify`/`jsNativeDate` is set, the returned rule will have `convert` and `parameterConversion` functions which automatically convert between strings/JavaScript Dates in user code and {@link DateTime}s in the database.
* @returns {Rule} A new rule for the value
*/
asDateTime(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, stringify can not be used in combination with custom conversion functions.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, jsNativeDate can not be used in combination with custom conversion functions.');
}
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) === true && (rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) === true) {
throw newError('both stringify and jsNativeDate cannot be set; use one or neither');
}
let parameterConversion;
if ((rule === null || rule === void 0 ? void 0 : rule.stringify) === true) {
parameterConversion = (str) => DateTime.fromString(str);
}
if ((rule === null || rule === void 0 ? void 0 : rule.jsNativeDate) === true) {
parameterConversion = (standardDate) => DateTime.fromStandardDate(standardDate);
}
return Object.assign({ validate: (value, field) => {
if (!isDateTime(value)) {
throw new TypeError(`${field} should be a DateTime but received ${typeof value}`);
}
}, convert: (value) => convertStdDate(value, rule), parameterConversion }, rule);
},
/**
* Create a {@link Rule} that validates the value is a List.
*
* Optionally taking a rule for hydrating the contained values.
*
* @param {Rule & { apply?: Rule } | undefined} rule Configurations for the rule. Setting apply to a rule will apply that rule to all elements of the list.
* @returns {Rule} A new rule for the value
*/
asList(rule) {
return Object.assign({ validate: (list, field) => {
var _a;
if (!Array.isArray(list)) {
throw new TypeError(`${field} should be a list but received ${typeof list}`);
}
const validate = (_a = rule === null || rule === void 0 ? void 0 : rule.apply) === null || _a === void 0 ? void 0 : _a.validate;
if (validate != null) {
list.forEach((value, index) => validate(value, `${field}[${index}]`));
}
}, convert: (list, field) => {
if ((rule === null || rule === void 0 ? void 0 : rule.apply) != null) {
return list.map((value, index) => valueAs(value, `${field}[${index}]`, rule.apply));
}
return list;
}, parameterConversion: (list) => {
const apply = rule === null || rule === void 0 ? void 0 : rule.apply;
if (apply != null) {
return list.map((value) => optionalParameterConversion(value, apply));
}
return list;
} }, rule);
},
/**
* Create a {@link Rule} that validates the value is a Vector.
*
* @param {Rule & { asTypedList?: boolean, dimension?: number, type?: VectorType } | undefined} rule Configurations for the rule. Setting asTypedList will automatically convert between TypedList in user code and Vectors in the database.
* @returns {Rule} A new rule for the value
*/
asVector(rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.asTypedList) != null && ((rule === null || rule === void 0 ? void 0 : rule.convert) != null || rule.parameterConversion != null)) {
throw newError('Provided rule already has convert and/or parameterConversion function, asTypedList can not be used in combination with custom conversion functions.');
}
return Object.assign({ validate: (value, field) => {
if (!isVector(value)) {
throw new TypeError(`${field} should be a vector but received ${typeof value}`);
}
if ((rule === null || rule === void 0 ? void 0 : rule.dimension) != null && value.asTypedArray().length !== rule.dimension) {
throw new TypeError(`${field} should be a vector of length ${rule.dimension} but received length ${value.asTypedArray().length}`);
}
if ((rule === null || rule === void 0 ? void 0 : rule.type) != null && value.getType() !== rule.type) {
throw new TypeError(`${field} should be a vector of type ${rule.type} but received type ${value.getType()}`);
}
}, convert: (value) => {
if ((rule === null || rule === void 0 ? void 0 : rule.asTypedList) === true) {
return value._typedArray;
}
return value;
}, parameterConversion: (rule === null || rule === void 0 ? void 0 : rule.asTypedList) === true ? (typedArray) => vector(typedArray) : undefined }, rule);
},
/**
* Create a {@link Rule} for an object, allowing complex mapping of even nested results
*
* NOTE: When using this rule, object identifiers will be mapped according to any name mapping set with neo4j.RecordObjectMapping.translateIdentifiers.
*
* @param {Rules} rules rules for the fields of the object.
* @param {GenericConstructor} constructor The constructor function of the class to map to. The constructor must be callable with all arguments undefined.
* @returns {Rule} A new rule for the value
*/
asObject(constructorOrRules, rules) {
const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object;
const theRules = getRules(constructorOrRules, rules);
return Object.assign({ validate: (value, field) => {
for (const key in theRules) {
const mappedKey = theRules[key].from != null ? theRules[key].from : defaultNameMapping(key);
if (value[mappedKey] == null) {
if (theRules[key].optional === true) {
continue;
}
else {
throw newError(`Mapped object did not include required field ${field} with key ${mappedKey}.`);
}
}
if (theRules[key].validate != null) {
theRules[key].validate(value[mappedKey], `${field}[${key}]`);
}
}
}, convert: (value, field) => {
const convertedValue = new GenericConstructor();
for (const key in theRules) {
const mappedKey = theRules[key].from != null ? theRules[key].from : defaultNameMapping(key);
if (value[mappedKey] != null && theRules[key].convert != null) {
// @ts-expect-error
convertedValue[key] = theRules[key].convert(value[mappedKey], `${field}[${mappedKey}]`);
}
else {
// @ts-expect-error
convertedValue[key] = value[mappedKey];
}
}
return convertedValue;
}, parameterConversion: (value) => {
const convertedValue = {};
for (const key in theRules) {
const mappedKey = theRules[key].from != null ? theRules[key].from : defaultNameMapping(key);
if (value[key] != null && theRules[key].parameterConversion != null) {
convertedValue[mappedKey] = theRules[key].parameterConversion(value[key]);
}
else {
convertedValue[mappedKey] = value[key];
}
}
return convertedValue;
} }, rule);
}
});
function convertStdDate(value, rule) {
if (rule != null) {
if (rule.stringify === true) {
return value.toString();
}
else if (rule.jsNativeDate === true) {
return value.toStandardDate();
}
}
return value;
}