Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7fdbc14
Enable Object Mapping for Parameters
MaxAake Nov 19, 2025
8974b0e
deno sync
MaxAake Nov 19, 2025
8bed085
fix tests
MaxAake Nov 20, 2025
596cb31
Update bolt-v3.test.js
MaxAake Nov 20, 2025
cdfcbf3
support converting lists of params and add asInteger
MaxAake Dec 8, 2025
70bbadb
deno sync
MaxAake Dec 8, 2025
68f84e0
remove as integer, makes no sense with asNumber and asBigInt
MaxAake Dec 9, 2025
6466d4b
drop lingering bits of asInteger
MaxAake Dec 9, 2025
232b11a
fix temporal bugs and some renaming
MaxAake Dec 12, 2025
86254dc
deno sync
MaxAake Dec 12, 2025
66ead65
document temporal types string parsing
MaxAake Dec 12, 2025
a107800
deno sync
MaxAake Dec 12, 2025
2e53950
add tests for string parsing temporal types
MaxAake Jan 7, 2026
de76027
deno sync
MaxAake Jan 7, 2026
1040d63
improve duration parsing
MaxAake Jan 7, 2026
c3e03f4
deno sync
MaxAake Jan 7, 2026
0127f10
error message testing
MaxAake Jan 8, 2026
6cf1e45
improve test and take record object mapping out of preview
MaxAake Jan 20, 2026
224031e
Update record-object-mapping.test.js
MaxAake Jan 21, 2026
9c410e9
test nested maps and dont ignore parameter mapping for optionals
MaxAake Jan 26, 2026
1f34571
Update record-object-mapping.test.js
MaxAake Jan 29, 2026
d102b06
deno sync
MaxAake Jan 29, 2026
0587594
fix typo, allow parsing zoned datetimes
MaxAake Jan 29, 2026
d760fcd
Fixing most review comments
MaxAake Feb 26, 2026
eae1683
deno sync
MaxAake Feb 26, 2026
f59c279
query parameterRules should only override default rules if provided
MaxAake Feb 26, 2026
17b760e
Update deno.lock
MaxAake Feb 26, 2026
e26d70b
allow optional fields to be missing
MaxAake Mar 3, 2026
8c52440
deno sync
MaxAake Mar 3, 2026
9601d8e
add string parser for Date to avoid needing to export JSDate
MaxAake Mar 3, 2026
0bc3c1b
deno sync
MaxAake Mar 3, 2026
2f3ac83
Update temporal-types.test.ts
MaxAake Mar 4, 2026
d5e9243
addressing PR review comments
MaxAake Mar 20, 2026
75bd611
deno sync
MaxAake Mar 20, 2026
9497926
asInteger ruleFactory
MaxAake Mar 24, 2026
e18586c
deno sync
MaxAake Mar 24, 2026
af87f6e
fix vector validation for browser
MaxAake Mar 24, 2026
0fab267
add tests for asInteger and asNumber isInterger setting
MaxAake Mar 25, 2026
6f22238
test integer mapping with own driver
MaxAake Mar 25, 2026
5bb2ac5
addressing review comments
MaxAake Apr 10, 2026
5af0636
deno sync
MaxAake Apr 10, 2026
691f441
clarify logic in fromString functions
MaxAake Apr 16, 2026
c9114cd
deno sync
MaxAake Apr 16, 2026
e39f8a8
addressing comments
MaxAake Apr 22, 2026
224f352
deno sync
MaxAake Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/core/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import NotificationFilter from './notification-filter'
import HomeDatabaseCache from './internal/homedb-cache'
import { cacheKey } from './internal/auth-util'
import { ProtocolVersion } from './protocol-version'
import { Rules } from './mapping.highlevel'

const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour

Expand Down Expand Up @@ -368,6 +369,7 @@ class QueryConfig<T = EagerResult> {
transactionConfig?: TransactionConfig
auth?: AuthToken
signal?: AbortSignal
parameterRules?: Rules

/**
* @constructor
Expand Down Expand Up @@ -605,7 +607,7 @@ class Driver {
* }
*
* @public
* @param {string | {text: string, parameters?: object}} query - Cypher query to execute
* @param {string | {text: string, parameters?: object, parameterRules?: Rules}} query - Cypher query to execute
* @param {Object} parameters - Map with parameters to use in the query
* @param {QueryConfig<T>} config - The query configuration
Comment thread
MaxAake marked this conversation as resolved.
* @returns {Promise<T>}
Expand All @@ -630,7 +632,7 @@ class Driver {
transactionConfig: config.transactionConfig,
auth: config.auth,
signal: config.signal
}, query, parameters)
}, query, parameters, config.parameterRules)
}

/**
Expand Down
6 changes: 0 additions & 6 deletions packages/core/src/graph-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ class Node<T extends NumberOrInteger = Integer, P extends Properties = Propertie
* @param {GenericConstructor<T> | Rules} constructorOrRules Constructor for the desired type or {@link Rules} for the hydration
* @param {Rules} [rules] {@link Rules} for the hydration
* @returns {T}
*
* @experimental Part of the Record Object Mapping preview feature
Comment thread
MaxAake marked this conversation as resolved.
*/
as <T extends {} = Object>(rules: Rules): T
as <T extends {} = Object>(genericConstructor: GenericConstructor<T>): T
Expand Down Expand Up @@ -224,8 +222,6 @@ class Relationship<T extends NumberOrInteger = Integer, P extends Properties = P
* @param {GenericConstructor<T> | Rules} constructorOrRules Constructor for the desired type or {@link Rules} for the hydration
* @param {Rules} [rules] {@link Rules} for the hydration
* @returns {T}
*
* @experimental Part of the Record Object Mapping preview feature
*/
as <T extends {} = Object>(rules: Rules): T
as <T extends {} = Object>(genericConstructor: GenericConstructor<T>): T
Expand Down Expand Up @@ -363,8 +359,6 @@ class UnboundRelationship<T extends NumberOrInteger = Integer, P extends Propert
* @param {GenericConstructor<T> | Rules} constructorOrRules Constructor for the desired type or {@link Rules} for the hydration
* @param {Rules} [rules] {@link Rules} for the hydration
* @returns {T}
*
* @experimental Part of the Record Object Mapping preview feature
*/
as <T extends {} = Object>(rules: Rules): T
as <T extends {} = Object>(genericConstructor: GenericConstructor<T>): T
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/internal/query-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Result from '../result'
import ManagedTransaction from '../transaction-managed'
import { AuthToken, Query } from '../types'
import { TELEMETRY_APIS } from './constants'
import { Rules } from '../mapping.highlevel'

type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string, auth?: AuthToken }) => Session

Expand All @@ -42,7 +43,7 @@ export default class QueryExecutor {

}

public async execute<T>(config: ExecutionConfig<T>, query: Query, parameters?: any): Promise<T> {
public async execute<T>(config: ExecutionConfig<T>, query: Query, parameters?: any, parameterRules?: Rules): Promise<T> {
const session = this._createSession({
database: config.database,
bookmarkManager: config.bookmarkManager,
Expand All @@ -65,7 +66,7 @@ export default class QueryExecutor {
: session.executeWrite.bind(session)

return await executeInTransaction(async (tx: ManagedTransaction) => {
const result = tx.run(query, parameters)
const result = tx.run(query, parameters, parameterRules)
return await config.resultTransformer(result)
}, config.transactionConfig)
} finally {
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/internal/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Integer, { isInt, int } from '../integer'
import { NumberOrInteger } from '../graph-types'
import { EncryptionLevel } from '../types'
import { stringify } from '../json'
import { Rules, validateAndCleanParameters } from '../mapping.highlevel'

const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON'
const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF'
Expand Down Expand Up @@ -62,15 +63,16 @@ function isObject (obj: any): boolean {
* @throws TypeError when either given query or parameters are invalid.
*/
function validateQueryAndParameters (
query: string | String | { text: string, parameters?: any },
query: string | String | { text: string, parameters?: any, parameterRules?: Rules },
Comment thread
MaxAake marked this conversation as resolved.
parameters?: any,
opt?: { skipAsserts: boolean }
opt?: { skipAsserts?: boolean, parameterRules?: Rules }
Comment thread
MaxAake marked this conversation as resolved.
): {
validatedQuery: string
params: any
} {
let validatedQuery: string = ''
let params = parameters ?? {}
let parameterRules = opt?.parameterRules
const skipAsserts: boolean = opt?.skipAsserts ?? false

if (typeof query === 'string') {
Expand All @@ -80,9 +82,11 @@ function validateQueryAndParameters (
} else if (typeof query === 'object' && query.text != null) {
validatedQuery = query.text
params = query.parameters ?? {}
parameterRules = query.parameterRules ?? parameterRules
}

if (!skipAsserts) {
params = validateAndCleanParameters(params, parameterRules)
assertCypherQuery(validatedQuery)
assertQueryParameters(params)
}
Expand Down
15 changes: 0 additions & 15 deletions packages/core/src/mapping.decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { rule } from './mapping.rulesfactories'
* Class Decorator Factory that enables the Neo4j Driver to map result records to this class
*
* @returns {Function} Class Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function mappedClass () {
return (_: any, context: any) => {
Expand All @@ -18,7 +17,6 @@ function mappedClass () {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function booleanProperty (config?: Rule) {
return (_: any, context: any) => {
Expand All @@ -31,7 +29,6 @@ function booleanProperty (config?: Rule) {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function stringProperty (config?: Rule) {
return (_: any, context: any) => {
Expand All @@ -44,7 +41,6 @@ function stringProperty (config?: Rule) {
*
* @param {Rule & { acceptBigInt?: boolean }} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function numberProperty (config?: Rule & { acceptBigInt?: boolean }) {
return (_: any, context: any) => {
Expand All @@ -57,7 +53,6 @@ function numberProperty (config?: Rule & { acceptBigInt?: boolean }) {
*
* @param {Rule & { acceptNumber?: boolean }} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function bigIntProperty (config?: Rule & { acceptNumber?: boolean }) {
return (_: any, context: any) => {
Expand All @@ -70,7 +65,6 @@ function bigIntProperty (config?: Rule & { acceptNumber?: boolean }) {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function nodeProperty (config?: Rule) {
return (_: any, context: any) => {
Expand All @@ -83,7 +77,6 @@ function nodeProperty (config?: Rule) {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function relationshipProperty (config?: Rule) {
return (_: any, context: any) => {
Expand All @@ -96,7 +89,6 @@ function relationshipProperty (config?: Rule) {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function pathProperty (config?: Rule) {
return (_: any, context: any) => {
Expand All @@ -109,7 +101,6 @@ function pathProperty (config?: Rule) {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function pointProperty (config?: Rule) {
return (_: any, context: any) => {
Expand All @@ -122,7 +113,6 @@ function pointProperty (config?: Rule) {
*
* @param {Rule} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function durationProperty (config?: Rule & { stringify?: boolean }) {
Comment thread
MaxAake marked this conversation as resolved.
return (_: any, context: any) => {
Expand All @@ -135,7 +125,6 @@ function durationProperty (config?: Rule & { stringify?: boolean }) {
*
* @param {Rule & { apply?: Rule }} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function listProperty (config?: Rule & { apply?: Rule }) {
return (_: any, context: any) => {
Expand All @@ -148,7 +137,6 @@ function listProperty (config?: Rule & { apply?: Rule }) {
*
* @param {Rule & { asTypedList?: boolean }} config
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function vectorProperty (config?: Rule & { asTypedList?: boolean }) {
Comment thread
MaxAake marked this conversation as resolved.
Outdated
return (_: any, context: any) => {
Expand All @@ -162,7 +150,6 @@ function vectorProperty (config?: Rule & { asTypedList?: boolean }) {
*
* @param {Rule} config
Comment thread
MaxAake marked this conversation as resolved.
Outdated
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function optionalProperty () {
return (_: any, context: any) => {
Expand All @@ -176,7 +163,6 @@ function optionalProperty () {
*
* @param {Rule} config
Comment thread
MaxAake marked this conversation as resolved.
Outdated
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function mapPropertyFromName (name: string) {
return (_: any, context: any) => {
Expand All @@ -190,7 +176,6 @@ function mapPropertyFromName (name: string) {
*
* @param {Rule} config
Comment thread
MaxAake marked this conversation as resolved.
Outdated
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function convertPropertyToType (type: any) {
return (_: any, context: any) => {
Expand Down
68 changes: 59 additions & 9 deletions packages/core/src/mapping.highlevel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*/

import { newError } from './error'
import { Neo4jError, newError } from './error'
import { nameConventions } from './mapping.nameconventions'

/**
Expand All @@ -29,14 +29,15 @@ export interface Rule {
optional?: boolean
from?: string
convert?: (recordValue: any, field: string) => any
parameterConversion?: (objectValue: any) => any
Comment thread
MaxAake marked this conversation as resolved.
validate?: (recordValue: any, field: string) => void
Comment thread
robsdedude marked this conversation as resolved.
Outdated
}

export type Rules = Record<string, Rule>

export let rulesRegistry: Record<string, Rules> = {}

let nameMapping: (name: string) => string = (name) => name
export let defaultNameMapping: (name: string) => string = (name) => name

function register <T extends {} = Object> (constructor: GenericConstructor<T>, rules: Rules): void {
rulesRegistry[constructor.name] = rules
Expand All @@ -47,7 +48,7 @@ function clearMappingRegistry (): void {
}

function translateIdentifiers (translationFunction: (name: string) => string): void {
nameMapping = translationFunction
defaultNameMapping = translationFunction
}

function getCaseTranslator (databaseConvention: string, codeConvention: string): ((name: string) => string) {
Expand All @@ -71,15 +72,13 @@ function getCaseTranslator (databaseConvention: string, codeConvention: string):
export const RecordObjectMapping = Object.freeze({
/**
* Clears all registered type mappings from the record object mapping registry.
* @experimental Part of the Record Object Mapping preview feature
*/
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"
*
* @experimental Part of the Record Object Mapping preview feature
* @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
Expand All @@ -101,7 +100,6 @@ export const RecordObjectMapping = Object.freeze({
* resultTransformer: neo4j.resultTransformers.hydrated(Person)
* })
*
* @experimental Part of the Record Object Mapping preview feature
* @param {GenericConstructor} constructor The constructor function of the class to set rules for
* @param {Rules} rules The rules to set for the provided class
*/
Expand All @@ -110,6 +108,8 @@ export const RecordObjectMapping = Object.freeze({
* 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
Expand All @@ -130,7 +130,6 @@ export const RecordObjectMapping = Object.freeze({
* //or by registering them to the mapping registry
* RecordObjectMapping.register(Person, personRules)
*
* @experimental Part of the Record Object Mapping preview feature
* @param {function} translationFunction A function translating the names of your JS object property names to record key names
*/
translateIdentifiers
Expand Down Expand Up @@ -160,8 +159,16 @@ export function as <T extends {} = Object> (gettable: Gettable, constructorOrRul
}

function _apply<T extends {}> (gettable: Gettable, obj: T, key: string, rule?: Rule): void {
const mappedkey = nameMapping(key)
const value = gettable.get(rule?.from ?? mappedkey)
const mappedkey = defaultNameMapping(key)
Comment thread
MaxAake marked this conversation as resolved.
Outdated
let value
try {
value = gettable.get(rule?.from ?? mappedkey)
} catch (e) {
if (rule?.optional === true && e instanceof Neo4jError && e.message.includes('This record has no field with key')) {
Comment thread
robsdedude marked this conversation as resolved.
Outdated
return
}
throw e
}
const field = `${obj.constructor.name}#${key}`
const processedValue = valueAs(value, field, rule)
// @ts-expect-error
Expand All @@ -179,6 +186,49 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown {

return ((rule?.convert) != null) ? rule.convert(value, field) : value
}

export function optionalParameterConversion (value: unknown, rule: Rule): unknown {
if (rule.optional === true && value == null) {
return value
}
return ((rule?.parameterConversion) != null) ? rule.parameterConversion(value) : value
Comment thread
MaxAake marked this conversation as resolved.
Outdated
}

export function validateAndCleanParameters (params: Record<string, any>, suppliedRules?: Rules): Record<string, any> {
const cleanedParams: Record<string, any> = {}
const parameterRules = getRules(Object.getPrototypeOf(params).constructor, suppliedRules)
if (parameterRules != null) {
for (const key in parameterRules) {
let param = params[key]
if (param == null && parameterRules[key].optional === true) {
continue
}
if (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 {
continue
}
}
}
Comment thread
MaxAake marked this conversation as resolved.
Outdated
if (parameterRules[key].validate != null) {
parameterRules[key].validate(param, key)
}
const mappedKey = parameterRules[key].from ?? defaultNameMapping(key)

cleanedParams[mappedKey] = param
}
return cleanedParams
} else {
return params
}
}

function getRules<T extends {} = Object> (constructorOrRules: Rules | GenericConstructor<T>, rules: Rules | undefined): Rules | undefined {
const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules
if (rulesDefined != null) {
Expand Down
Loading