Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 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
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
4 changes: 3 additions & 1 deletion 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 @@ -630,7 +632,7 @@ class Driver {
transactionConfig: config.transactionConfig,
auth: config.auth,
signal: config.signal
}, query, parameters)
}, query, parameters, config.parameterRules)
}

/**
Expand Down
7 changes: 1 addition & 6 deletions packages/core/src/graph-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Integer from './integer'
import { stringify } from './json'
import { Rules, GenericConstructor, as } from './mapping.highlevel'

export const JSDate = Date
type StandardDate = Date
/**
* @typedef {number | Integer | bigint} NumberOrInteger
Expand Down Expand Up @@ -89,8 +90,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
*/
as <T extends {} = Object>(rules: Rules): T
as <T extends {} = Object>(genericConstructor: GenericConstructor<T>): T
Expand Down Expand Up @@ -224,8 +223,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 +360,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 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameterRules missing in doc comment

parameters?: any,
opt?: { skipAsserts: boolean }
opt?: { skipAsserts?: boolean, parameterRules?: Rules }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opt missing in doc comment

): {
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
}

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 }) {
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 }) {
return (_: any, context: any) => {
Expand All @@ -162,7 +150,6 @@ function vectorProperty (config?: Rule & { asTypedList?: boolean }) {
*
* @param {Rule} config
* @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
* @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
* @returns {Function} Property Decorator
* @experimental Part of the Record Object Mapping preview feature
*/
function convertPropertyToType (type: any) {
return (_: any, context: any) => {
Expand Down
50 changes: 45 additions & 5 deletions packages/core/src/mapping.highlevel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ export interface Rule {
optional?: boolean
from?: string
convert?: (recordValue: any, field: string) => any
parameterConversion?: (objectValue: any) => any
validate?: (recordValue: any, field: string) => void
Comment on lines 29 to 33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I've missed it, but I couldn't see a place where these options are properly documented. Specifically, the optional configuration behaves differently, comparing input and output mapping. Further, not all of these names are self-explanatory in my mind.

}

export type Rules = Record<string, Rule>

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

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

function register <T extends {} = Object> (constructor: GenericConstructor<T>, rules: Rules): void {
rulesRegistry[constructor.name] = rules
Expand Down Expand Up @@ -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 Down Expand Up @@ -130,7 +128,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 @@ -179,6 +176,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ((rule?.parameterConversion) != null) ? rule.parameterConversion(value) : value
return (rule.parameterConversion != null) ? rule.parameterConversion(value) : value

}

export function validateAndcleanParameters (params: Record<string, any>, suppliedRules?: Rules): Record<string, any> {
const cleanedParams: Record<string, any> = {}
// @ts-expect-error
const parameterRules = getRules(params.constructor, suppliedRules)
if (parameterRules !== undefined) {
for (const key in parameterRules) {
if (!(parameterRules?.[key]?.optional === true)) {
let param = params[key]
if (parameterRules[key]?.parameterConversion !== undefined) {
param = parameterRules[key].parameterConversion(params[key])
}
if (param === undefined) {
throw newError('Parameter object did not include required parameter.')
}
if (parameterRules[key].validate != null) {
parameterRules[key].validate(param, key)
// @ts-expect-error
if (parameterRules[key].apply !== undefined) {
for (const entryKey in param) {
// @ts-expect-error
parameterRules[key].apply.validate(param[entryKey], entryKey)
}
}
}
const mappedKey = parameterRules[key].from ?? nameMapping(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