lib6/mapping.highlevel.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 { Neo4jError, newError } from './error';
import { nameConventions } from './mapping.nameconventions';
export let rulesRegistry = {};
export let defaultNameMapping = (name) => name;
function register(constructor, rules) {
rulesRegistry[constructor.name] = rules;
}
function clearMappingRegistry() {
rulesRegistry = {};
}
function translateIdentifiers(translationFunction) {
defaultNameMapping = translationFunction;
}
function getCaseTranslator(databaseConvention, codeConvention) {
const keys = Object.keys(nameConventions);
if (!keys.includes(databaseConvention)) {
throw newError(`Naming convention ${databaseConvention} is not recognized,
please provide a recognized name convention or manually provide a translation function.`);
}
if (!keys.includes(codeConvention)) {
throw newError(`Naming convention ${codeConvention} is not recognized,
please provide a recognized name convention or manually provide a translation function.`);
}
// @ts-expect-error
return (name) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name));
}
export const RecordObjectMapping = Object.freeze({
/**
* Clears all registered type mappings from the record object mapping registry.
*/
clearMappingRegistry,
/**
* Creates a translation function from record key names to object property names, for use with the {@link translateIdentifiers} function
*
* Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE"
*
* @param {string} databaseConvention The naming convention in use in database result Records
* @param {string} codeConvention The naming convention in use in JavaScript object properties
* @returns {function} translation function
*/
getCaseTranslator,
/**
* Registers a set of {@link Rules} to be used by {@link hydrated} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance.
*
* @example
* // The following code:
* const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, {
* resultTransformer: neo4j.resultTransformers.hydrated(Person, personClassRules)
* })
*
* can instead be written:
* neo4j.RecordObjectMapping.register(Person, personClassRules)
*
* const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, {
* resultTransformer: neo4j.resultTransformers.hydrated(Person)
* })
*
* @param {GenericConstructor} constructor The constructor function of the class to set rules for. The constructor must be callable with all arguments undefined.
* @param {Rules} rules The rules to set for the provided class
*/
register,
/**
* Sets a default name translation from record keys to object properties.
* If providing a function, provide a function that maps FROM your object properties names TO record key names.
*
* NOTE: The keys of objects inside a record will only be translated if using the asObject rule with it, not by default.
*
* The function getCaseTranslator can be used to provide a prewritten translation function between some common naming conventions.
*
* @example
* //if the keys on records from the database are in ALLCAPS
* RecordObjectMapping.translateIdentifiers((name) => name.toUpperCase())
*
* //if you utilize PacalCase in the database and camelCase in JavaScript code.
* RecordObjectMapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase"))
*
* //if a type has one odd mapping you can override the translation with the rule
* const personRules = {
* firstName: neo4j.rule.asString(),
* bornAt: neo4j.rule.asNumber({ optional: true })
* weird_name-property: neo4j.rule.asString({from: 'homeTown'})
* }
* //These rules can then be used by providing them to a hydratedResultsMapper
* record.as<Person>(personRules)
* //or by registering them to the mapping registry
* RecordObjectMapping.register(Person, personRules)
*
* @param {function} translationFunction A function translating the names of your JS object property names to record key names
*/
translateIdentifiers
});
export function as(gettable, constructorOrRules, rules) {
const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object;
const theRules = getRules(constructorOrRules, rules);
const visitedKeys = [];
const obj = new GenericConstructor();
for (const [key, rule] of Object.entries(theRules !== null && theRules !== void 0 ? theRules : {})) {
visitedKeys.push(key);
_apply(gettable, obj, key, rule);
}
for (const key of Object.getOwnPropertyNames(obj)) {
if (!visitedKeys.includes(key)) {
_apply(gettable, obj, key, theRules === null || theRules === void 0 ? void 0 : theRules[key]);
}
}
return obj;
}
function _apply(gettable, obj, key, rule) {
var _a;
const mappedKey = defaultNameMapping(key);
let value;
try {
value = gettable.get((_a = rule === null || rule === void 0 ? void 0 : rule.from) !== null && _a !== void 0 ? _a : mappedKey);
}
catch (e) {
if ((rule === null || rule === void 0 ? void 0 : rule.optional) === true && e instanceof Neo4jError) {
return;
}
throw e;
}
const field = `${obj.constructor.name}#${key}`;
// @ts-expect-error
obj[key] = valueAs(value, field, rule);
}
export function valueAs(value, field, rule) {
if ((rule === null || rule === void 0 ? void 0 : rule.optional) === true && value == null) {
return value;
}
if (typeof (rule === null || rule === void 0 ? void 0 : rule.validate) === 'function') {
rule.validate(value, field);
}
return ((rule === null || rule === void 0 ? void 0 : rule.convert) != null) ? rule.convert(value, field) : value;
}
export function optionalParameterConversion(value, rule) {
if (rule.optional === true && value == null) {
return value;
}
return (value != null && rule.parameterConversion != null) ? rule.parameterConversion(value) : value;
}
export function validateAndCleanParameters(params, suppliedRules) {
var _a;
const cleanedParams = {};
const parameterRules = getRules(Object.getPrototypeOf(params).constructor, suppliedRules);
if (parameterRules != null) {
for (const key in parameterRules) {
let param = params[key];
const mappedKey = (_a = parameterRules[key].from) !== null && _a !== void 0 ? _a : defaultNameMapping(key);
if (param != null && parameterRules[key].parameterConversion != null) {
param = parameterRules[key].parameterConversion(param);
}
if (param == null) {
if (parameterRules[key].optional !== true) {
throw newError(`Mapped Parameter object did not include required parameter with key ${key},
check provided parameters and parameter rules.`);
}
}
else if (parameterRules[key].validate != null) {
parameterRules[key].validate(param, key);
}
cleanedParams[mappedKey] = param;
}
return cleanedParams;
}
else {
return params;
}
}
export function getRules(constructorOrRules, rules) {
const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules;
if (rulesDefined != null) {
return rulesDefined;
}
return typeof constructorOrRules !== 'object' ? rulesRegistry[constructorOrRules.name] : undefined;
}