From 7fdbc1404134160dc38d9c15d279cd71eb1c7e0f Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:08:05 +0100 Subject: [PATCH 01/12] Enable Object Mapping for Parameters --- packages/core/src/driver.ts | 4 +- packages/core/src/graph-types.ts | 1 + packages/core/src/internal/query-executor.ts | 5 +- packages/core/src/internal/util.ts | 9 ++-- packages/core/src/mapping.highlevel.ts | 39 +++++++++++++++- packages/core/src/mapping.rulesfactories.ts | 25 +++++++++- packages/core/src/session.ts | 8 +++- packages/core/src/temporal-types.ts | 46 +++++++++++++++++++ packages/core/src/transaction-managed.ts | 7 +-- packages/core/src/transaction.ts | 6 ++- packages/core/src/types.ts | 3 +- packages/neo4j-driver-deno/lib/core/driver.ts | 4 +- .../neo4j-driver-deno/lib/core/graph-types.ts | 1 + .../lib/core/internal/query-executor.ts | 5 +- .../lib/core/internal/util.ts | 11 +++-- .../lib/core/mapping.highlevel.ts | 40 +++++++++++++++- .../lib/core/mapping.rulesfactories.ts | 25 +++++++++- .../neo4j-driver-deno/lib/core/session.ts | 8 +++- .../lib/core/temporal-types.ts | 46 +++++++++++++++++++ .../lib/core/transaction-managed.ts | 7 +-- .../neo4j-driver-deno/lib/core/transaction.ts | 6 ++- packages/neo4j-driver-deno/lib/core/types.ts | 3 +- .../test/record-object-mapping.test.js | 32 +++++++++++++ 23 files changed, 306 insertions(+), 35 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 261d6860c..0126c8fda 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -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 @@ -368,6 +369,7 @@ class QueryConfig { transactionConfig?: TransactionConfig auth?: AuthToken signal?: AbortSignal + parameterRules?: Rules /** * @constructor @@ -630,7 +632,7 @@ class Driver { transactionConfig: config.transactionConfig, auth: config.auth, signal: config.signal - }, query, parameters) + }, query, parameters, config.parameterRules) } /** diff --git a/packages/core/src/graph-types.ts b/packages/core/src/graph-types.ts index 9b0aa16b6..e5924f9d4 100644 --- a/packages/core/src/graph-types.ts +++ b/packages/core/src/graph-types.ts @@ -18,6 +18,7 @@ import Integer from './integer' import { stringify } from './json' import { Rules, GenericConstructor, as } from './mapping.highlevel' +export const StandardDateClass = Date type StandardDate = Date /** * @typedef {number | Integer | bigint} NumberOrInteger diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts index 53074ef3e..7cd2327a9 100644 --- a/packages/core/src/internal/query-executor.ts +++ b/packages/core/src/internal/query-executor.ts @@ -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 @@ -42,7 +43,7 @@ export default class QueryExecutor { } - public async execute(config: ExecutionConfig, query: Query, parameters?: any): Promise { + public async execute(config: ExecutionConfig, query: Query, parameters?: any, parameterRules?: Rules): Promise { const session = this._createSession({ database: config.database, bookmarkManager: config.bookmarkManager, @@ -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 { diff --git a/packages/core/src/internal/util.ts b/packages/core/src/internal/util.ts index 4151fd17d..3e5de94b2 100644 --- a/packages/core/src/internal/util.ts +++ b/packages/core/src/internal/util.ts @@ -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, validateAndCleanParams } from '../mapping.highlevel' const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF' @@ -62,17 +63,17 @@ 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 }, parameters?: any, - opt?: { skipAsserts: boolean } + opt?: { skipAsserts?: boolean, parameterRules?: Rules } ): { validatedQuery: string params: any } { let validatedQuery: string = '' let params = parameters ?? {} + let parameterRules = opt?.parameterRules const skipAsserts: boolean = opt?.skipAsserts ?? false - if (typeof query === 'string') { validatedQuery = query } else if (query instanceof String) { @@ -80,9 +81,11 @@ function validateQueryAndParameters ( } else if (typeof query === 'object' && query.text != null) { validatedQuery = query.text params = query.parameters ?? {} + parameterRules = query.parameterRules } if (!skipAsserts) { + params = validateAndCleanParams(params, parameterRules) assertCypherQuery(validatedQuery) assertQueryParameters(params) } diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index f08c85457..34870ac92 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -29,6 +29,7 @@ export interface Rule { optional?: boolean from?: string convert?: (recordValue: any, field: string) => any + convertToParam?: (objectValue: any) => any validate?: (recordValue: any, field: string) => void } @@ -36,7 +37,7 @@ export type Rules = Record export let rulesRegistry: Record = {} -let nameMapping: (name: string) => string = (name) => name +export let nameMapping: (name: string) => string = (name) => name function register (constructor: GenericConstructor, rules: Rules): void { rulesRegistry[constructor.name] = rules @@ -179,6 +180,42 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } + +export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { + const cleanedParams: Record = {} + // @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]?.convertToParam !== undefined) { + param = parameterRules[key].convertToParam(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 (constructorOrRules: Rules | GenericConstructor, rules: Rules | undefined): Rules | undefined { const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules if (rulesDefined != null) { diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index 84591a8d2..d1403c132 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -18,10 +18,10 @@ */ import { Rule, valueAs } from './mapping.highlevel' -import { StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types' +import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } 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 Vector from './vector' +import Vector, { vector } from './vector' /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. @@ -250,6 +250,7 @@ export const rule = Object.freeze({ } }, convert: (value: Duration) => rule?.stringify === true ? value.toString() : value, + convertToParam: rule?.stringify === true ? (str: string) => Duration.fromString(str) : undefined, ...rule } }, @@ -268,6 +269,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalTime) => rule?.stringify === true ? value.toString() : value, + convertToParam: rule?.stringify === true ? (str: string) => LocalTime.fromString(str) : undefined, ...rule } }, @@ -286,6 +288,7 @@ export const rule = Object.freeze({ } }, convert: (value: Time) => rule?.stringify === true ? value.toString() : value, + convertToParam: rule?.stringify === true ? (str: string) => Time.fromString(str) : undefined, ...rule } }, @@ -304,6 +307,7 @@ export const rule = Object.freeze({ } }, convert: (value: Date) => convertStdDate(value, rule), + convertToParam: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new StandardDateClass(str)) : undefined, ...rule } }, @@ -315,6 +319,13 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asLocalDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { + let convertToParam + if (rule?.stringify === true) { + convertToParam = (str: string) => LocalDateTime.fromStandardDate(new StandardDateClass(str)) + } + if (rule?.toStandardDate === true) { + convertToParam = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) + } return { validate: (value: any, field: string) => { if (!isLocalDateTime(value)) { @@ -322,6 +333,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalDateTime) => convertStdDate(value, rule), + convertToParam, ...rule } }, @@ -333,6 +345,13 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { + let convertToParam + if (rule?.stringify === true) { + convertToParam = (str: string) => DateTime.fromStandardDate(new StandardDateClass(str)) + } + if (rule?.toStandardDate === true) { + convertToParam = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) + } return { validate: (value: any, field: string) => { if (!isDateTime(value)) { @@ -340,6 +359,7 @@ export const rule = Object.freeze({ } }, convert: (value: DateTime) => convertStdDate(value, rule), + convertToParam, ...rule } }, @@ -386,6 +406,7 @@ export const rule = Object.freeze({ } return value }, + convertToParam: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray) : undefined, ...rule } } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index df3e6dc1b..cb6922535 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -38,6 +38,7 @@ import { RecordShape } from './record' import NotificationFilter from './notification-filter' import { Logger } from './internal/logger' import { cacheKey } from './internal/auth-util' +import { Rules } from './mapping.highlevel' type ConnectionConsumer = (connection: Connection) => Promise | T type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T @@ -45,6 +46,7 @@ type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T interface TransactionConfig { timeout?: NumberOrInteger metadata?: object + parameterRules?: Rules } /** @@ -186,11 +188,13 @@ class Session { run ( query: Query, parameters?: any, - transactionConfig?: TransactionConfig + transactionConfig?: TransactionConfig, + parameterRules?: Rules ): Result { const { validatedQuery, params } = validateQueryAndParameters( query, - parameters + parameters, + { parameterRules } ) const autoCommitTxConfig = (transactionConfig != null) ? new TxConfig(transactionConfig, this._log) diff --git a/packages/core/src/temporal-types.ts b/packages/core/src/temporal-types.ts index e2ae73362..9c12c7a73 100644 --- a/packages/core/src/temporal-types.ts +++ b/packages/core/src/temporal-types.ts @@ -94,6 +94,20 @@ export class Duration { Object.freeze(this) } + static fromString (str: string): Duration { + const matches = String(str).match(/P(?:([-?.,\d]+)Y)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)W)?(?:([-?.,\d]+)D)?T(?:([-?.,\d]+)H)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)S)?/) + if (matches !== null) { + const dur = new Duration( + ~~parseInt(matches[1]) * 12 + ~~parseInt(matches[2]), + ~~parseInt(matches[3]) * 7 + ~~parseInt(matches[4]), + ~~parseInt(matches[5]) * 3600 + ~~parseInt(matches[6]) * 60 + ~~parseInt(matches[7]), + Math.round((parseFloat(matches[7]) - parseInt(matches[7])) * 10 ** 9) + ) + return dur + } + throw newError('Duration could not be parsed from string') + } + /** * @ignore */ @@ -203,6 +217,21 @@ export class LocalTime { this.nanosecond ) } + + static fromString (str: string): LocalTime { + console.log(str) + const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) + console.log(values) + if (values !== null) { + return new LocalTime( + parseInt(values[0]), + parseInt(values[1]), + parseInt(values[2]), + Math.round(parseFloat('0.' + values[3]) * 10 ** 9) + ) + } + throw newError('LocalTime could not be parsed from string') + } } Object.defineProperty( @@ -312,6 +341,23 @@ export class Time { ) + util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds) ) } + + static fromString (str: string): Time { + const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)(Z|\+|-)?(\d*)/) + if (values !== null) { + if (values[4] === 'Z') { + return new Time(parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseInt(values[3]) * 10 ** 9, 0) + } + return new Time( + parseInt(values[0]), + parseInt(values[1]), + parseInt(values[2]), + Math.round(parseFloat('0.' + values[3]) * 10 ** 9), + (values[4] === '+' ? 1 : -1) * parseInt(values[5]) + ) + } + throw newError('Time could not be parsed from string') + } } Object.defineProperty( diff --git a/packages/core/src/transaction-managed.ts b/packages/core/src/transaction-managed.ts index 25cb0b04b..c25fbdba7 100644 --- a/packages/core/src/transaction-managed.ts +++ b/packages/core/src/transaction-managed.ts @@ -19,8 +19,9 @@ import Result from './result' import Transaction from './transaction' import { Query } from './types' import { RecordShape } from './record' +import { Rules } from './mapping.highlevel' -type Run = (query: Query, parameters?: any) => Result +type Run = (query: Query, parameters?: any, parameterRules?: Rules) => Result /** * Represents a transaction that is managed by the transaction executor. @@ -59,8 +60,8 @@ class ManagedTransaction { * @param {Object} parameters - Map with parameters to use in query * @return {Result} New Result */ - run (query: Query, parameters?: any): Result { - return this._run(query, parameters) + run (query: Query, parameters?: any, parameterRules?: Rules): Result { + return this._run(query, parameters, parameterRules) } } diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index a1fbc447c..9cd96b55b 100644 --- a/packages/core/src/transaction.ts +++ b/packages/core/src/transaction.ts @@ -38,6 +38,7 @@ import { Query } from './types' import { RecordShape } from './record' import NotificationFilter from './notification-filter' import { TelemetryApis, TELEMETRY_APIS } from './internal/constants' +import { Rules } from './mapping.highlevel' type NonAutoCommitTelemetryApis = Exclude type NonAutoCommitApiTelemetryConfig = ApiTelemetryConfig @@ -196,10 +197,11 @@ class Transaction { * @param {Object} parameters - Map with parameters to use in query * @return {Result} New Result */ - run (query: Query, parameters?: any): Result { + run (query: Query, parameters?: any, parameterRules?: Rules): Result { const { validatedQuery, params } = validateQueryAndParameters( query, - parameters + parameters, + { parameterRules } ) const result = this._state.run(validatedQuery, params, { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 644353b43..46bd3135a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -16,12 +16,13 @@ */ import ClientCertificate, { ClientCertificateProvider } from './client-certificate' +import { Rules } from './mapping.highlevel' import NotificationFilter from './notification-filter' /** * @private */ -export type Query = string | String | { text: string, parameters?: any } +export type Query = string | String | { text: string, parameters?: any, parameterRules?: Rules } export type EncryptionLevel = 'ENCRYPTION_ON' | 'ENCRYPTION_OFF' diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index b4db89fb7..693ca62c7 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -49,6 +49,7 @@ import NotificationFilter from './notification-filter.ts' import HomeDatabaseCache from './internal/homedb-cache.ts' import { cacheKey } from './internal/auth-util.ts' import { ProtocolVersion } from './protocol-version.ts' +import { Rules } from './mapping.highlevel.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -368,6 +369,7 @@ class QueryConfig { transactionConfig?: TransactionConfig auth?: AuthToken signal?: AbortSignal + parameterRules?: Rules /** * @constructor @@ -630,7 +632,7 @@ class Driver { transactionConfig: config.transactionConfig, auth: config.auth, signal: config.signal - }, query, parameters) + }, query, parameters, config.parameterRules) } /** diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts index 29cfb1d36..fa400bbed 100644 --- a/packages/neo4j-driver-deno/lib/core/graph-types.ts +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -18,6 +18,7 @@ import Integer from './integer.ts' import { stringify } from './json.ts' import { Rules, GenericConstructor, as } from './mapping.highlevel.ts' +export const StandardDateClass = Date type StandardDate = Date /** * @typedef {number | Integer | bigint} NumberOrInteger diff --git a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts index d26dc9333..efbf0362f 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -21,6 +21,7 @@ import Result from '../result.ts' import ManagedTransaction from '../transaction-managed.ts' import { AuthToken, Query } from '../types.ts' import { TELEMETRY_APIS } from './constants.ts' +import { Rules } from '../mapping.highlevel.ts' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string, auth?: AuthToken }) => Session @@ -42,7 +43,7 @@ export default class QueryExecutor { } - public async execute(config: ExecutionConfig, query: Query, parameters?: any): Promise { + public async execute(config: ExecutionConfig, query: Query, parameters?: any, parameterRules?: Rules): Promise { const session = this._createSession({ database: config.database, bookmarkManager: config.bookmarkManager, @@ -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 { diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts index be860bf08..c3c80b22d 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/util.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -19,6 +19,7 @@ import Integer, { isInt, int } from '../integer.ts' import { NumberOrInteger } from '../graph-types.ts' import { EncryptionLevel } from '../types.ts' import { stringify } from '../json.ts' +import { Rules, validateAndCleanParams } from '../mapping.highlevel.ts' const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF' @@ -62,17 +63,17 @@ 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}, parameters?: any, - opt?: { skipAsserts: boolean } + opt?: { skipAsserts?: boolean, parameterRules?: Rules } ): { validatedQuery: string params: any } { let validatedQuery: string = '' let params = parameters ?? {} + let parameterRules = opt?.parameterRules const skipAsserts: boolean = opt?.skipAsserts ?? false - if (typeof query === 'string') { validatedQuery = query } else if (query instanceof String) { @@ -80,9 +81,11 @@ function validateQueryAndParameters ( } else if (typeof query === 'object' && query.text != null) { validatedQuery = query.text params = query.parameters ?? {} + parameterRules = query.parameterRules } - + if (!skipAsserts) { + params = validateAndCleanParams(params, parameterRules) assertCypherQuery(validatedQuery) assertQueryParameters(params) } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 520dc9505..5f6e509e6 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -29,6 +29,7 @@ export interface Rule { optional?: boolean from?: string convert?: (recordValue: any, field: string) => any + convertToParam?: (objectValue: any) => any validate?: (recordValue: any, field: string) => void } @@ -36,7 +37,7 @@ export type Rules = Record export let rulesRegistry: Record = {} -let nameMapping: (name: string) => string = (name) => name +export let nameMapping: (name: string) => string = (name) => name function register (constructor: GenericConstructor, rules: Rules): void { rulesRegistry[constructor.name] = rules @@ -179,6 +180,43 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } + +export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { + let cleanedParams: Record = {} + // @ts-ignore + 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]?.convertToParam !== undefined) { + param = parameterRules[key].convertToParam(params[key]) + } + if(param === undefined) { + throw newError("Parameter object did not include required parameter.") + } + if(parameterRules[key].validate) { + parameterRules[key].validate(param, key) + // @ts-ignore + if(parameterRules[key].apply !== undefined) { + for(const entryKey in param){ + // @ts-ignore + parameterRules[key].apply.validate(param[entryKey], entryKey) + } + } + } + const mappedKey = parameterRules[key].from ?? nameMapping(key) + + cleanedParams[mappedKey] = param + } + } + return cleanedParams + } + else { + return params + } +} + function getRules (constructorOrRules: Rules | GenericConstructor, rules: Rules | undefined): Rules | undefined { const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules if (rulesDefined != null) { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 4b2208951..43f1fea26 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -18,10 +18,10 @@ */ import { Rule, valueAs } from './mapping.highlevel.ts' -import { StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types.ts' +import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types.ts' import { isPoint } from './spatial-types.ts' import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from './temporal-types.ts' -import Vector from './vector.ts' +import Vector, { vector } from './vector.ts' /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. @@ -250,6 +250,7 @@ export const rule = Object.freeze({ } }, convert: (value: Duration) => rule?.stringify === true ? value.toString() : value, + convertToParam: rule?.stringify === true ? (str: string) => Duration.fromString(str): undefined, ...rule } }, @@ -268,6 +269,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalTime) => rule?.stringify === true ? value.toString() : value, + convertToParam: rule?.stringify === true ? (str: string) => LocalTime.fromString(str): undefined, ...rule } }, @@ -286,6 +288,7 @@ export const rule = Object.freeze({ } }, convert: (value: Time) => rule?.stringify === true ? value.toString() : value, + convertToParam: rule?.stringify === true ? (str: string) => Time.fromString(str): undefined, ...rule } }, @@ -304,6 +307,7 @@ export const rule = Object.freeze({ } }, convert: (value: Date) => convertStdDate(value, rule), + convertToParam: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new StandardDateClass(str)): undefined, ...rule } }, @@ -315,6 +319,13 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asLocalDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { + let convertToParam = undefined + if(rule?.stringify === true) { + convertToParam = (str: string) => LocalDateTime.fromStandardDate(new StandardDateClass(str)) + } + if(rule?.toStandardDate === true) { + convertToParam = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) + } return { validate: (value: any, field: string) => { if (!isLocalDateTime(value)) { @@ -322,6 +333,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalDateTime) => convertStdDate(value, rule), + convertToParam: convertToParam, ...rule } }, @@ -333,6 +345,13 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { + let convertToParam = undefined + if(rule?.stringify === true) { + convertToParam = (str: string) => DateTime.fromStandardDate(new StandardDateClass(str)) + } + if(rule?.toStandardDate === true) { + convertToParam = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) + } return { validate: (value: any, field: string) => { if (!isDateTime(value)) { @@ -340,6 +359,7 @@ export const rule = Object.freeze({ } }, convert: (value: DateTime) => convertStdDate(value, rule), + convertToParam: convertToParam, ...rule } }, @@ -386,6 +406,7 @@ export const rule = Object.freeze({ } return value }, + convertToParam: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray): undefined, ...rule } } diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index 1af4f8dea..022111d08 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -38,6 +38,7 @@ import { RecordShape } from './record.ts' import NotificationFilter from './notification-filter.ts' import { Logger } from './internal/logger.ts' import { cacheKey } from './internal/auth-util.ts' +import { Rules } from './mapping.highlevel.ts' type ConnectionConsumer = (connection: Connection) => Promise | T type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T @@ -45,6 +46,7 @@ type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T interface TransactionConfig { timeout?: NumberOrInteger metadata?: object + parameterRules?: Rules } /** @@ -186,11 +188,13 @@ class Session { run ( query: Query, parameters?: any, - transactionConfig?: TransactionConfig + transactionConfig?: TransactionConfig, + parameterRules?: Rules ): Result { const { validatedQuery, params } = validateQueryAndParameters( query, - parameters + parameters, + {parameterRules: parameterRules} ) const autoCommitTxConfig = (transactionConfig != null) ? new TxConfig(transactionConfig, this._log) diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts index 93b289767..6b7a574a7 100644 --- a/packages/neo4j-driver-deno/lib/core/temporal-types.ts +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -94,6 +94,20 @@ export class Duration { Object.freeze(this) } + static fromString(str: string): Duration { + const matches = String(str).match(/P(?:([-?.,\d]+)Y)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)W)?(?:([-?.,\d]+)D)?T(?:([-?.,\d]+)H)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)S)?/) + if(matches !== null) { + const dur = new Duration( + ~~parseInt(matches[1]) * 12 + ~~parseInt(matches[2]), + ~~parseInt(matches[3]) * 7 + ~~parseInt(matches[4]), + ~~parseInt(matches[5]) * 3600 + ~~parseInt(matches[6]) * 60 + ~~parseInt(matches[7]), + Math.round((parseFloat(matches[7]) - parseInt(matches[7])) * 10**9) + ) + return dur + } + throw newError("Duration could not be parsed from string") + } + /** * @ignore */ @@ -203,6 +217,21 @@ export class LocalTime { this.nanosecond ) } + + static fromString(str: string): LocalTime { + console.log(str) + const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) + console.log(values) + if(values !== null) { + return new LocalTime( + parseInt(values[0]), + parseInt(values[1]), + parseInt(values[2]), + Math.round(parseFloat("0." + values[3]) * 10**9) + ) + } + throw newError("LocalTime could not be parsed from string") + } } Object.defineProperty( @@ -312,6 +341,23 @@ export class Time { ) + util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds) ) } + + static fromString(str: string): Time { + const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)(Z|\+|-)?(\d*)/) + if(values !== null) { + if(values[4] === "Z") { + return new Time(parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseInt(values[3]) * 10**9, 0) + } + return new Time( + parseInt(values[0]), + parseInt(values[1]), + parseInt(values[2]), + Math.round(parseFloat("0." + values[3]) * 10**9), + (values[4] === "+" ? 1 : -1) * parseInt(values[5]) + ) + } + throw newError("Time could not be parsed from string") + } } Object.defineProperty( diff --git a/packages/neo4j-driver-deno/lib/core/transaction-managed.ts b/packages/neo4j-driver-deno/lib/core/transaction-managed.ts index dbce04ad4..ab9e7a546 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction-managed.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction-managed.ts @@ -19,8 +19,9 @@ import Result from './result.ts' import Transaction from './transaction.ts' import { Query } from './types.ts' import { RecordShape } from './record.ts' +import { Rules } from './mapping.highlevel.ts' -type Run = (query: Query, parameters?: any) => Result +type Run = (query: Query, parameters?: any, parameterRules?: Rules) => Result /** * Represents a transaction that is managed by the transaction executor. @@ -59,8 +60,8 @@ class ManagedTransaction { * @param {Object} parameters - Map with parameters to use in query * @return {Result} New Result */ - run (query: Query, parameters?: any): Result { - return this._run(query, parameters) + run (query: Query, parameters?: any, parameterRules?: Rules): Result { + return this._run(query, parameters, parameterRules) } } diff --git a/packages/neo4j-driver-deno/lib/core/transaction.ts b/packages/neo4j-driver-deno/lib/core/transaction.ts index 39f426b7d..bde8ea988 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction.ts @@ -38,6 +38,7 @@ import { Query } from './types.ts' import { RecordShape } from './record.ts' import NotificationFilter from './notification-filter.ts' import { TelemetryApis, TELEMETRY_APIS } from './internal/constants.ts' +import { Rules } from './mapping.highlevel.ts' type NonAutoCommitTelemetryApis = Exclude type NonAutoCommitApiTelemetryConfig = ApiTelemetryConfig @@ -196,10 +197,11 @@ class Transaction { * @param {Object} parameters - Map with parameters to use in query * @return {Result} New Result */ - run (query: Query, parameters?: any): Result { + run (query: Query, parameters?: any, parameterRules?: Rules): Result { const { validatedQuery, params } = validateQueryAndParameters( query, - parameters + parameters, + {parameterRules: parameterRules} ) const result = this._state.run(validatedQuery, params, { diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index 7cde8ad6f..87dd0f68c 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -16,12 +16,13 @@ */ import ClientCertificate, { ClientCertificateProvider } from './client-certificate.ts' +import { Rules } from './mapping.highlevel.ts' import NotificationFilter from './notification-filter.ts' /** * @private */ -export type Query = string | String | { text: string, parameters?: any } +export type Query = string | String | { text: string, parameters?: any, parameterRules?: Rules } export type EncryptionLevel = 'ENCRYPTION_ON' | 'ENCRYPTION_OFF' diff --git a/packages/neo4j-driver/test/record-object-mapping.test.js b/packages/neo4j-driver/test/record-object-mapping.test.js index f91fe8195..cb16a7987 100644 --- a/packages/neo4j-driver/test/record-object-mapping.test.js +++ b/packages/neo4j-driver/test/record-object-mapping.test.js @@ -306,4 +306,36 @@ describe('#integration record object mapping', () => { session.close() }) + + it('map input', async () => { + const session = driverGlobal.session() + + const rules = { + number: neo4j.rule.asNumber(), + string: neo4j.rule.asString(), + bigint: neo4j.rule.asBigInt(), + date: neo4j.rule.asDate(), + dateTime: neo4j.rule.asDateTime(), + duration: neo4j.rule.asDuration(), + time: neo4j.rule.asTime({ from: 'heyaaaa' }), + list: neo4j.rule.asList({ apply: neo4j.rule.asString() }) + } + + const obj = { + string: 'hi', + number: 1, + bigint: BigInt(1), + date: new neo4j.Date(1, 1, 1), + dateTime: new neo4j.DateTime(1, 1, 1, 1, 1, 1, 1, 1), + duration: new neo4j.Duration(1, 1, 1, 1), + time: new neo4j.Time(1, 1, 1, 1, 1), + list: ['hi'] + } + + neo4j.RecordObjectMapping.translateIdentifiers(neo4j.RecordObjectMapping.getCaseTranslator('snake_case', 'camelCase')) + + const res = await session.run('MERGE (n {string: $string, number: $number, bigint: $bigint, date: $date, date_time: $date_time, duration: $duration, time: $heyaaaa, list: $list}) RETURN n', obj, {}, rules) + + expect(res.records[0].get('n').properties.date_time).toEqual(new neo4j.DateTime(1, 1, 1, 1, 1, 1, 1, 1)) + }) }) From 8974b0e690e8d2322047f8809073f5db0890f9e5 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:11:43 +0100 Subject: [PATCH 02/12] deno sync --- .../lib/core/internal/util.ts | 4 +-- .../lib/core/mapping.highlevel.ts | 27 ++++++++--------- .../lib/core/mapping.rulesfactories.ts | 26 ++++++++-------- .../neo4j-driver-deno/lib/core/session.ts | 2 +- .../lib/core/temporal-types.ts | 30 +++++++++---------- .../neo4j-driver-deno/lib/core/transaction.ts | 2 +- 6 files changed, 45 insertions(+), 46 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts index c3c80b22d..860a3a82d 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/util.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -63,7 +63,7 @@ function isObject (obj: any): boolean { * @throws TypeError when either given query or parameters are invalid. */ function validateQueryAndParameters ( - query: string | String | { text: string, parameters?: any, parameterRules?: Rules}, + query: string | String | { text: string, parameters?: any, parameterRules?: Rules }, parameters?: any, opt?: { skipAsserts?: boolean, parameterRules?: Rules } ): { @@ -83,7 +83,7 @@ function validateQueryAndParameters ( params = query.parameters ?? {} parameterRules = query.parameterRules } - + if (!skipAsserts) { params = validateAndCleanParams(params, parameterRules) assertCypherQuery(validatedQuery) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 5f6e509e6..10d9939eb 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -182,37 +182,36 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { } export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { - let cleanedParams: Record = {} - // @ts-ignore + const cleanedParams: Record = {} + // @ts-expect-error const parameterRules = getRules(params.constructor, suppliedRules) if (parameterRules !== undefined) { - for(const key in parameterRules) { - if(!(parameterRules?.[key]?.optional === true)) { + for (const key in parameterRules) { + if (!(parameterRules?.[key]?.optional === true)) { let param = params[key] if (parameterRules[key]?.convertToParam !== undefined) { param = parameterRules[key].convertToParam(params[key]) } - if(param === undefined) { - throw newError("Parameter object did not include required parameter.") + if (param === undefined) { + throw newError('Parameter object did not include required parameter.') } - if(parameterRules[key].validate) { + if (parameterRules[key].validate != null) { parameterRules[key].validate(param, key) - // @ts-ignore - if(parameterRules[key].apply !== undefined) { - for(const entryKey in param){ - // @ts-ignore + // @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 { + } else { return params } } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 43f1fea26..7e4751adf 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -250,7 +250,7 @@ export const rule = Object.freeze({ } }, convert: (value: Duration) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => Duration.fromString(str): undefined, + convertToParam: rule?.stringify === true ? (str: string) => Duration.fromString(str) : undefined, ...rule } }, @@ -269,7 +269,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalTime) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => LocalTime.fromString(str): undefined, + convertToParam: rule?.stringify === true ? (str: string) => LocalTime.fromString(str) : undefined, ...rule } }, @@ -288,7 +288,7 @@ export const rule = Object.freeze({ } }, convert: (value: Time) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => Time.fromString(str): undefined, + convertToParam: rule?.stringify === true ? (str: string) => Time.fromString(str) : undefined, ...rule } }, @@ -307,7 +307,7 @@ export const rule = Object.freeze({ } }, convert: (value: Date) => convertStdDate(value, rule), - convertToParam: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new StandardDateClass(str)): undefined, + convertToParam: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new StandardDateClass(str)) : undefined, ...rule } }, @@ -319,11 +319,11 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asLocalDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { - let convertToParam = undefined - if(rule?.stringify === true) { + let convertToParam + if (rule?.stringify === true) { convertToParam = (str: string) => LocalDateTime.fromStandardDate(new StandardDateClass(str)) } - if(rule?.toStandardDate === true) { + if (rule?.toStandardDate === true) { convertToParam = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) } return { @@ -333,7 +333,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalDateTime) => convertStdDate(value, rule), - convertToParam: convertToParam, + convertToParam, ...rule } }, @@ -345,11 +345,11 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { - let convertToParam = undefined - if(rule?.stringify === true) { + let convertToParam + if (rule?.stringify === true) { convertToParam = (str: string) => DateTime.fromStandardDate(new StandardDateClass(str)) } - if(rule?.toStandardDate === true) { + if (rule?.toStandardDate === true) { convertToParam = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) } return { @@ -359,7 +359,7 @@ export const rule = Object.freeze({ } }, convert: (value: DateTime) => convertStdDate(value, rule), - convertToParam: convertToParam, + convertToParam, ...rule } }, @@ -406,7 +406,7 @@ export const rule = Object.freeze({ } return value }, - convertToParam: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray): undefined, + convertToParam: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray) : undefined, ...rule } } diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index 022111d08..3e84366ca 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -194,7 +194,7 @@ class Session { const { validatedQuery, params } = validateQueryAndParameters( query, parameters, - {parameterRules: parameterRules} + { parameterRules } ) const autoCommitTxConfig = (transactionConfig != null) ? new TxConfig(transactionConfig, this._log) diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts index 6b7a574a7..12fab7b94 100644 --- a/packages/neo4j-driver-deno/lib/core/temporal-types.ts +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -94,18 +94,18 @@ export class Duration { Object.freeze(this) } - static fromString(str: string): Duration { + static fromString (str: string): Duration { const matches = String(str).match(/P(?:([-?.,\d]+)Y)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)W)?(?:([-?.,\d]+)D)?T(?:([-?.,\d]+)H)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)S)?/) - if(matches !== null) { + if (matches !== null) { const dur = new Duration( ~~parseInt(matches[1]) * 12 + ~~parseInt(matches[2]), ~~parseInt(matches[3]) * 7 + ~~parseInt(matches[4]), ~~parseInt(matches[5]) * 3600 + ~~parseInt(matches[6]) * 60 + ~~parseInt(matches[7]), - Math.round((parseFloat(matches[7]) - parseInt(matches[7])) * 10**9) + Math.round((parseFloat(matches[7]) - parseInt(matches[7])) * 10 ** 9) ) return dur } - throw newError("Duration could not be parsed from string") + throw newError('Duration could not be parsed from string') } /** @@ -218,19 +218,19 @@ export class LocalTime { ) } - static fromString(str: string): LocalTime { + static fromString (str: string): LocalTime { console.log(str) const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) console.log(values) - if(values !== null) { + if (values !== null) { return new LocalTime( parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), - Math.round(parseFloat("0." + values[3]) * 10**9) + Math.round(parseFloat('0.' + values[3]) * 10 ** 9) ) } - throw newError("LocalTime could not be parsed from string") + throw newError('LocalTime could not be parsed from string') } } @@ -342,21 +342,21 @@ export class Time { ) } - static fromString(str: string): Time { + static fromString (str: string): Time { const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)(Z|\+|-)?(\d*)/) - if(values !== null) { - if(values[4] === "Z") { - return new Time(parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseInt(values[3]) * 10**9, 0) + if (values !== null) { + if (values[4] === 'Z') { + return new Time(parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseInt(values[3]) * 10 ** 9, 0) } return new Time( parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), - Math.round(parseFloat("0." + values[3]) * 10**9), - (values[4] === "+" ? 1 : -1) * parseInt(values[5]) + Math.round(parseFloat('0.' + values[3]) * 10 ** 9), + (values[4] === '+' ? 1 : -1) * parseInt(values[5]) ) } - throw newError("Time could not be parsed from string") + throw newError('Time could not be parsed from string') } } diff --git a/packages/neo4j-driver-deno/lib/core/transaction.ts b/packages/neo4j-driver-deno/lib/core/transaction.ts index bde8ea988..002458b2c 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction.ts @@ -201,7 +201,7 @@ class Transaction { const { validatedQuery, params } = validateQueryAndParameters( query, parameters, - {parameterRules: parameterRules} + { parameterRules } ) const result = this._state.run(validatedQuery, params, { From 8bed08522415a5a2de94433c6aa13965b6d36030 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:40:46 +0100 Subject: [PATCH 03/12] fix tests --- packages/core/test/driver.test.ts | 6 +++--- packages/core/test/internal/query-executor.test.ts | 10 +++++----- packages/core/test/session.test.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 53177c99e..14c150d5e 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -405,7 +405,7 @@ describe('Driver', () => { routing: routing.WRITE, database: undefined, impersonatedUser: undefined - }, query, params) + }, query, params, undefined) }) it('should be able to destruct the result in records, keys and summary', async () => { @@ -431,7 +431,7 @@ describe('Driver', () => { routing: routing.WRITE, database: undefined, impersonatedUser: undefined - }, query, params) + }, query, params, undefined) }) it('should be able get type-safe Records', async () => { @@ -505,7 +505,7 @@ describe('Driver', () => { await driver?.executeQuery(query, params, config) - expect(spiedExecute).toBeCalledWith(buildExpectedConfig(), query, params) + expect(spiedExecute).toBeCalledWith(buildExpectedConfig(), query, params, undefined) }) it('should handle correct type mapping for a custom result transformer', async () => { diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index 7b582f015..04b0c5134 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { bookmarkManager, newError, Result, Session, TransactionConfig } from '../../src' +import { bookmarkManager, newError, Result, Rules, Session, TransactionConfig } from '../../src' import QueryExecutor from '../../src/internal/query-executor' import ManagedTransaction from '../../src/transaction-managed' import ResultStreamObserverMock from '../utils/result-stream-observer.mock' @@ -150,7 +150,7 @@ describe('QueryExecutor', () => { await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) expect(spyOnRun).toHaveBeenCalledTimes(1) - expect(spyOnRun).toHaveBeenCalledWith('query', { a: 'b' }) + expect(spyOnRun).toHaveBeenCalledWith('query', { a: 'b' }, undefined) }) it('should return the transformed result', async () => { @@ -353,7 +353,7 @@ describe('QueryExecutor', () => { await queryExecutor.execute(baseConfig, 'query', { a: 'b' }) expect(spyOnRun).toHaveBeenCalledTimes(1) - expect(spyOnRun).toHaveBeenCalledWith('query', { a: 'b' }) + expect(spyOnRun).toHaveBeenCalledWith('query', { a: 'b' }, undefined) }) it('should return the transformed result', async () => { @@ -522,7 +522,7 @@ describe('QueryExecutor', () => { function createManagedTransaction (): { managedTransaction: ManagedTransaction - spyOnRun: jest.SpyInstance + spyOnRun: jest.SpyInstance resultObservers: ResultStreamObserverMock[] results: Result[] } { @@ -531,7 +531,7 @@ describe('QueryExecutor', () => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const managedTransaction = { - run: (query: string, parameters?: any): Result => { + run: (query: string, parameters?: any, parameterRules?: Rules | undefined): Result => { const resultObserver = new ResultStreamObserverMock() resultObservers.push(resultObserver) const result = new Result( diff --git a/packages/core/test/session.test.ts b/packages/core/test/session.test.ts index 3824dbfe3..0a07e7620 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -665,7 +665,7 @@ describe('session', () => { }) expect(status.functionCalled).toEqual(true) - expect(run).toHaveBeenCalledWith(query, params) + expect(run).toHaveBeenCalledWith(query, params, undefined) }) it('should round up sub milliseconds transaction timeouts', async () => { From 596cb31f8f2033a69c2314d9f712c8307dfa1eac Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:57:18 +0100 Subject: [PATCH 04/12] Update bolt-v3.test.js --- packages/neo4j-driver/test/bolt-v3.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/neo4j-driver/test/bolt-v3.test.js b/packages/neo4j-driver/test/bolt-v3.test.js index 5d3ce1d6d..7eeb34f81 100644 --- a/packages/neo4j-driver/test/bolt-v3.test.js +++ b/packages/neo4j-driver/test/bolt-v3.test.js @@ -209,8 +209,7 @@ describe('#integration Bolt V3 API', () => { try { await tx.run( 'MATCH (n:Node) SET n.prop = $newValue', - { newValue: 2 }, - { timeout: 1 } + { newValue: 2 } ) } catch (e) { // ClientError on 4.1 and later From cdfcbf38d4adf0a5f436bbaadf158be14b579fe6 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:35:01 +0100 Subject: [PATCH 05/12] support converting lists of params and add asInteger --- packages/core/src/mapping.highlevel.ts | 7 +++ packages/core/src/mapping.rulesfactories.ts | 50 ++++++++++++++++++- packages/core/test/mapping.highlevel.test.ts | 41 ++++++++++++++- .../lib/core/mapping.highlevel.ts | 7 +++ .../lib/core/mapping.rulesfactories.ts | 50 ++++++++++++++++++- 5 files changed, 152 insertions(+), 3 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 34870ac92..b0e99be77 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -181,6 +181,13 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } +export function valueAsParam (value: unknown, rule?: Rule): unknown { + if (rule?.optional === true && value == null) { + return value + } + return ((rule?.convertToParam) != null) ? rule.convertToParam(value) : value +} + export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { const cleanedParams: Record = {} // @ts-expect-error diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index d1403c132..66b023465 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -17,11 +17,13 @@ * limitations under the License. */ -import { Rule, valueAs } from './mapping.highlevel' +import { Rule, valueAs, valueAsParam } from './mapping.highlevel' import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } 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 Vector, { vector } from './vector' +import { newError } from './error' +import Integer, { int, isInt } from './integer' /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. @@ -30,6 +32,8 @@ import Vector, { vector } from './vector' * * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. * + * @property {function(rule: ?Rule & { asNumber?: boolean, asBigInt?: boolean })} AsInteger Create a {@link Rule} that validates the value is an 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}. @@ -140,6 +144,44 @@ export const rule = Object.freeze({ ...rule } }, + /** + * Create a {@link Rule} that validates the value is an {@link Integer}. + * + * @experimental Part of the Record Object Mapping preview feature + * @param {Rule & { asNumber?: boolean, asBigInt?: boolean }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asInteger (rule?: Rule & { asNumber?: boolean, asBigInt?: boolean }): Rule { + if (rule?.asNumber === true && rule.asBigInt === true) { + throw newError('Cannot set both asNumber and asBigInt in a asInteger rule') + } + return { + validate: (value: any, field: string) => { + if (!isInt(value)) { + throw new TypeError(`${field} should be an Integer but received ${typeof value}`) + } + }, + convert: (value: Integer) => { + if (rule?.asNumber === true) { + return value.low + } + if (rule?.asBigInt === true) { + return value.toBigInt() + } + return value + }, + convertToParam: (objectValue: any) => { + if (rule?.asNumber === true) { + return int(objectValue) + } + if (rule?.asBigInt === true) { + return int(objectValue) + } + return objectValue + }, + ...rule + } + }, /** * Create a {@link Rule} that validates the value is a {@link Node}. * @@ -383,6 +425,12 @@ export const rule = Object.freeze({ } return list }, + convertToParam: (list: any[]) => { + if (rule?.apply != null) { + return list.map((value) => valueAsParam(value, rule.apply)) + } + return list + }, ...rule } }, diff --git a/packages/core/test/mapping.highlevel.test.ts b/packages/core/test/mapping.highlevel.test.ts index 32e613609..aec6564d7 100644 --- a/packages/core/test/mapping.highlevel.test.ts +++ b/packages/core/test/mapping.highlevel.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Date, DateTime, Duration, RecordObjectMapping, Node, Relationship, Rules, rule, Time, Vector } from '../src' +import { Date, DateTime, Duration, RecordObjectMapping, Node, Relationship, Rules, rule, Time, Vector, int } from '../src' import { as } from '../src/mapping.highlevel' describe('#unit Record Object Mapping', () => { @@ -72,6 +72,45 @@ describe('#unit Record Object Mapping', () => { // @ts-expect-error expect(() => as(gettable, personRules)).toThrow('Object#name should be a number but received string') }) + + it.each([ + ['Number', rule.asNumber(), 1, 1], + ['String', rule.asString(), 'hi', 'hi'], + ['BigInt', rule.asBigInt(), BigInt(1), BigInt(1)], + ['Integer Converted to BigInt', rule.asInteger({ asBigInt: true }), BigInt(1), int(1)], + ['Integer Converted to Number', rule.asInteger({ asNumber: true }), 1, int(1)], + ['Date', rule.asDate(), new Date(1, 1, 1), new Date(1, 1, 1)], + ['DateTime', rule.asDateTime(), new DateTime(1, 1, 1, 1, 1, 1, 1, 1), new DateTime(1, 1, 1, 1, 1, 1, 1, 1)], + ['Duration', rule.asDuration(), new Duration(1, 1, 1, 1), new Duration(1, 1, 1, 1)], + ['Time', rule.asTime(), new Time(1, 1, 1, 1, 1), new Time(1, 1, 1, 1, 1)], + ['Simple List', rule.asList({ apply: rule.asString() }), ['hello'], ['hello']], + [ + 'Complex List', + rule.asList({ apply: rule.asVector({ asTypedList: true }) }), + [Float32Array.from([0.1, 0.2]), Float32Array.from([0.3, 0.4]), Float32Array.from([0.5, 0.6])], + [new Vector(Float32Array.from([0.1, 0.2])), new Vector(Float32Array.from([0.3, 0.4])), new Vector(Float32Array.from([0.5, 0.6]))] + ], + [ + 'Vector', + rule.asVector(), + new Vector(Int32Array.from([0, 1, 2])), + new Vector(Int32Array.from([0, 1, 2])) + ], + [ + 'Converted Vector', + rule.asVector({ asTypedList: true, from: 'vec' }), + Float32Array.from([0.1, 0.2]), + new Vector(Float32Array.from([0.1, 0.2])) + ] + ])('should be able to map %s as property', (_, rule, param, expected) => { + if (rule.convertToParam != null) { + param = rule.convertToParam(param) + } + // @ts-expect-error + rule.validate(param) + expect(param).toEqual(expected) + }) + it('should be able to read all property types', () => { class Person { name diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 10d9939eb..55a24f58e 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -181,6 +181,13 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } +export function valueAsParam (value: unknown, rule?: Rule): unknown { + if (rule?.optional === true && value == null) { + return value + } + return ((rule?.convertToParam) != null) ? rule.convertToParam(value) : value +} + export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { const cleanedParams: Record = {} // @ts-expect-error diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 7e4751adf..33b0d5e36 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -17,11 +17,13 @@ * limitations under the License. */ -import { Rule, valueAs } from './mapping.highlevel.ts' +import { Rule, valueAs, valueAsParam } from './mapping.highlevel.ts' import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types.ts' import { isPoint } from './spatial-types.ts' import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from './temporal-types.ts' import Vector, { vector } from './vector.ts' +import { newError } from './error.ts' +import Integer, { int, isInt } from './integer.ts' /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. @@ -30,6 +32,8 @@ import Vector, { vector } from './vector.ts' * * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. * + * @property {function(rule: ?Rule & { asNumber?: boolean, asBigInt?: boolean })} AsInteger Create a {@link Rule} that validates the value is an 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}. @@ -140,6 +144,44 @@ export const rule = Object.freeze({ ...rule } }, + /** + * Create a {@link Rule} that validates the value is an {@link Integer}. + * + * @experimental Part of the Record Object Mapping preview feature + * @param {Rule & { asNumber?: boolean, asBigInt?: boolean }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asInteger (rule?: Rule & { asNumber?: boolean, asBigInt?: boolean }): Rule { + if(rule?.asNumber === true && rule.asBigInt === true) { + throw newError("Cannot set both asNumber and asBigInt in a asInteger rule") + } + return { + validate: (value: any, field: string) => { + if (isInt(value) !== true) { + throw new TypeError(`${field} should be an Integer but received ${typeof value}`) + } + }, + convert: (value: Integer) => { + if (rule?.asNumber === true) { + return value.low + } + if (rule?.asBigInt === true) { + return value.toBigInt() + } + return value + }, + convertToParam: (objectValue: any) => { + if(rule?.asNumber === true) { + return int(objectValue) + } + if(rule?.asBigInt === true) { + return int(objectValue) + } + return objectValue + }, + ...rule + } + }, /** * Create a {@link Rule} that validates the value is a {@link Node}. * @@ -383,6 +425,12 @@ export const rule = Object.freeze({ } return list }, + convertToParam: (list: any[]) => { + if (rule?.apply != null) { + return list.map((value) => valueAsParam(value, rule.apply)) + } + return list + }, ...rule } }, From 70bbadbaac5a14913dbca19b998b78f4a4cd0275 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:35:53 +0100 Subject: [PATCH 06/12] deno sync --- .../lib/core/mapping.rulesfactories.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 33b0d5e36..5867615b7 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -152,12 +152,12 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asInteger (rule?: Rule & { asNumber?: boolean, asBigInt?: boolean }): Rule { - if(rule?.asNumber === true && rule.asBigInt === true) { - throw newError("Cannot set both asNumber and asBigInt in a asInteger rule") + if (rule?.asNumber === true && rule.asBigInt === true) { + throw newError('Cannot set both asNumber and asBigInt in a asInteger rule') } return { validate: (value: any, field: string) => { - if (isInt(value) !== true) { + if (!isInt(value)) { throw new TypeError(`${field} should be an Integer but received ${typeof value}`) } }, @@ -171,10 +171,10 @@ export const rule = Object.freeze({ return value }, convertToParam: (objectValue: any) => { - if(rule?.asNumber === true) { + if (rule?.asNumber === true) { return int(objectValue) } - if(rule?.asBigInt === true) { + if (rule?.asBigInt === true) { return int(objectValue) } return objectValue From 68f84e0029df544ee2624b95acda6ebc0ea93184 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:28:03 +0100 Subject: [PATCH 07/12] remove as integer, makes no sense with asNumber and asBigInt --- packages/core/src/mapping.rulesfactories.ts | 40 ------------------- .../lib/core/mapping.rulesfactories.ts | 40 ------------------- 2 files changed, 80 deletions(-) diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index 66b023465..a23f42824 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -22,8 +22,6 @@ import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnbo import { isPoint } from './spatial-types' import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from './temporal-types' import Vector, { vector } from './vector' -import { newError } from './error' -import Integer, { int, isInt } from './integer' /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. @@ -144,44 +142,6 @@ export const rule = Object.freeze({ ...rule } }, - /** - * Create a {@link Rule} that validates the value is an {@link Integer}. - * - * @experimental Part of the Record Object Mapping preview feature - * @param {Rule & { asNumber?: boolean, asBigInt?: boolean }} rule Configurations for the rule - * @returns {Rule} A new rule for the value - */ - asInteger (rule?: Rule & { asNumber?: boolean, asBigInt?: boolean }): Rule { - if (rule?.asNumber === true && rule.asBigInt === true) { - throw newError('Cannot set both asNumber and asBigInt in a asInteger rule') - } - return { - validate: (value: any, field: string) => { - if (!isInt(value)) { - throw new TypeError(`${field} should be an Integer but received ${typeof value}`) - } - }, - convert: (value: Integer) => { - if (rule?.asNumber === true) { - return value.low - } - if (rule?.asBigInt === true) { - return value.toBigInt() - } - return value - }, - convertToParam: (objectValue: any) => { - if (rule?.asNumber === true) { - return int(objectValue) - } - if (rule?.asBigInt === true) { - return int(objectValue) - } - return objectValue - }, - ...rule - } - }, /** * Create a {@link Rule} that validates the value is a {@link Node}. * diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 5867615b7..220084330 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -22,8 +22,6 @@ import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnbo import { isPoint } from './spatial-types.ts' import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from './temporal-types.ts' import Vector, { vector } from './vector.ts' -import { newError } from './error.ts' -import Integer, { int, isInt } from './integer.ts' /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. @@ -144,44 +142,6 @@ export const rule = Object.freeze({ ...rule } }, - /** - * Create a {@link Rule} that validates the value is an {@link Integer}. - * - * @experimental Part of the Record Object Mapping preview feature - * @param {Rule & { asNumber?: boolean, asBigInt?: boolean }} rule Configurations for the rule - * @returns {Rule} A new rule for the value - */ - asInteger (rule?: Rule & { asNumber?: boolean, asBigInt?: boolean }): Rule { - if (rule?.asNumber === true && rule.asBigInt === true) { - throw newError('Cannot set both asNumber and asBigInt in a asInteger rule') - } - return { - validate: (value: any, field: string) => { - if (!isInt(value)) { - throw new TypeError(`${field} should be an Integer but received ${typeof value}`) - } - }, - convert: (value: Integer) => { - if (rule?.asNumber === true) { - return value.low - } - if (rule?.asBigInt === true) { - return value.toBigInt() - } - return value - }, - convertToParam: (objectValue: any) => { - if (rule?.asNumber === true) { - return int(objectValue) - } - if (rule?.asBigInt === true) { - return int(objectValue) - } - return objectValue - }, - ...rule - } - }, /** * Create a {@link Rule} that validates the value is a {@link Node}. * From 6466d4b37ab7390c2c12d8a6ee947c23927017f3 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:43:48 +0100 Subject: [PATCH 08/12] drop lingering bits of asInteger --- packages/core/src/mapping.rulesfactories.ts | 2 -- packages/core/test/mapping.highlevel.test.ts | 4 +--- packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index a23f42824..853fc3453 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -30,8 +30,6 @@ import Vector, { vector } from './vector' * * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. * - * @property {function(rule: ?Rule & { asNumber?: boolean, asBigInt?: boolean })} AsInteger Create a {@link Rule} that validates the value is an 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}. diff --git a/packages/core/test/mapping.highlevel.test.ts b/packages/core/test/mapping.highlevel.test.ts index aec6564d7..2a3f0cd00 100644 --- a/packages/core/test/mapping.highlevel.test.ts +++ b/packages/core/test/mapping.highlevel.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Date, DateTime, Duration, RecordObjectMapping, Node, Relationship, Rules, rule, Time, Vector, int } from '../src' +import { Date, DateTime, Duration, RecordObjectMapping, Node, Relationship, Rules, rule, Time, Vector } from '../src' import { as } from '../src/mapping.highlevel' describe('#unit Record Object Mapping', () => { @@ -77,8 +77,6 @@ describe('#unit Record Object Mapping', () => { ['Number', rule.asNumber(), 1, 1], ['String', rule.asString(), 'hi', 'hi'], ['BigInt', rule.asBigInt(), BigInt(1), BigInt(1)], - ['Integer Converted to BigInt', rule.asInteger({ asBigInt: true }), BigInt(1), int(1)], - ['Integer Converted to Number', rule.asInteger({ asNumber: true }), 1, int(1)], ['Date', rule.asDate(), new Date(1, 1, 1), new Date(1, 1, 1)], ['DateTime', rule.asDateTime(), new DateTime(1, 1, 1, 1, 1, 1, 1, 1), new DateTime(1, 1, 1, 1, 1, 1, 1, 1)], ['Duration', rule.asDuration(), new Duration(1, 1, 1, 1), new Duration(1, 1, 1, 1)], diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 220084330..31cfe0fb7 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -30,8 +30,6 @@ import Vector, { vector } from './vector.ts' * * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. * - * @property {function(rule: ?Rule & { asNumber?: boolean, asBigInt?: boolean })} AsInteger Create a {@link Rule} that validates the value is an 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}. From 232b11ad61082043ef8df465c1d7f7e5f46b6f56 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:49:26 +0100 Subject: [PATCH 09/12] fix temporal bugs and some renaming --- packages/core/src/graph-types.ts | 2 +- packages/core/src/internal/util.ts | 4 +- packages/core/src/mapping.highlevel.ts | 12 ++-- packages/core/src/mapping.rulesfactories.ts | 34 +++++----- packages/core/src/temporal-types.ts | 65 ++++++++++++++++--- packages/core/test/mapping.highlevel.test.ts | 38 +---------- .../core/test/mapping.rulesfactories.test.ts | 64 ++++++++++++++++++ .../neo4j-driver-deno/lib/core/graph-types.ts | 2 +- .../lib/core/internal/util.ts | 4 +- .../lib/core/mapping.highlevel.ts | 12 ++-- .../lib/core/mapping.rulesfactories.ts | 34 +++++----- .../lib/core/temporal-types.ts | 65 ++++++++++++++++--- 12 files changed, 231 insertions(+), 105 deletions(-) create mode 100644 packages/core/test/mapping.rulesfactories.test.ts diff --git a/packages/core/src/graph-types.ts b/packages/core/src/graph-types.ts index e5924f9d4..16d2c91c4 100644 --- a/packages/core/src/graph-types.ts +++ b/packages/core/src/graph-types.ts @@ -18,7 +18,7 @@ import Integer from './integer' import { stringify } from './json' import { Rules, GenericConstructor, as } from './mapping.highlevel' -export const StandardDateClass = Date +export const JSDate = Date type StandardDate = Date /** * @typedef {number | Integer | bigint} NumberOrInteger diff --git a/packages/core/src/internal/util.ts b/packages/core/src/internal/util.ts index 3e5de94b2..28c2500ec 100644 --- a/packages/core/src/internal/util.ts +++ b/packages/core/src/internal/util.ts @@ -19,7 +19,7 @@ import Integer, { isInt, int } from '../integer' import { NumberOrInteger } from '../graph-types' import { EncryptionLevel } from '../types' import { stringify } from '../json' -import { Rules, validateAndCleanParams } from '../mapping.highlevel' +import { Rules, validateAndcleanParameters } from '../mapping.highlevel' const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF' @@ -85,7 +85,7 @@ function validateQueryAndParameters ( } if (!skipAsserts) { - params = validateAndCleanParams(params, parameterRules) + params = validateAndcleanParameters(params, parameterRules) assertCypherQuery(validatedQuery) assertQueryParameters(params) } diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index b0e99be77..633e40059 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -29,7 +29,7 @@ export interface Rule { optional?: boolean from?: string convert?: (recordValue: any, field: string) => any - convertToParam?: (objectValue: any) => any + parameterConversion?: (objectValue: any) => any validate?: (recordValue: any, field: string) => void } @@ -181,14 +181,14 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } -export function valueAsParam (value: unknown, rule?: Rule): unknown { +export function optionalParameterConversion (value: unknown, rule?: Rule): unknown { if (rule?.optional === true && value == null) { return value } - return ((rule?.convertToParam) != null) ? rule.convertToParam(value) : value + return ((rule?.parameterConversion) != null) ? rule.parameterConversion(value) : value } -export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { +export function validateAndcleanParameters (params: Record, suppliedRules?: Rules): Record { const cleanedParams: Record = {} // @ts-expect-error const parameterRules = getRules(params.constructor, suppliedRules) @@ -196,8 +196,8 @@ export function validateAndCleanParams (params: Record, suppliedRul for (const key in parameterRules) { if (!(parameterRules?.[key]?.optional === true)) { let param = params[key] - if (parameterRules[key]?.convertToParam !== undefined) { - param = parameterRules[key].convertToParam(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.') diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index 853fc3453..850cda761 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Rule, valueAs, valueAsParam } from './mapping.highlevel' -import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types' +import { Rule, valueAs, optionalParameterConversion } from './mapping.highlevel' +import { JSDate, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } 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 Vector, { vector } from './vector' @@ -250,7 +250,7 @@ export const rule = Object.freeze({ } }, convert: (value: Duration) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => Duration.fromString(str) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => Duration.fromString(str) : undefined, ...rule } }, @@ -269,7 +269,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalTime) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => LocalTime.fromString(str) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => LocalTime.fromString(str) : undefined, ...rule } }, @@ -288,7 +288,7 @@ export const rule = Object.freeze({ } }, convert: (value: Time) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => Time.fromString(str) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => Time.fromString(str) : undefined, ...rule } }, @@ -307,7 +307,7 @@ export const rule = Object.freeze({ } }, convert: (value: Date) => convertStdDate(value, rule), - convertToParam: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new StandardDateClass(str)) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new JSDate(str)) : undefined, ...rule } }, @@ -319,12 +319,12 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asLocalDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { - let convertToParam + let parameterConversion if (rule?.stringify === true) { - convertToParam = (str: string) => LocalDateTime.fromStandardDate(new StandardDateClass(str)) + parameterConversion = (str: string) => LocalDateTime.fromString(str) } if (rule?.toStandardDate === true) { - convertToParam = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) + parameterConversion = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) } return { validate: (value: any, field: string) => { @@ -333,7 +333,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalDateTime) => convertStdDate(value, rule), - convertToParam, + parameterConversion, ...rule } }, @@ -345,12 +345,12 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { - let convertToParam + let parameterConversion if (rule?.stringify === true) { - convertToParam = (str: string) => DateTime.fromStandardDate(new StandardDateClass(str)) + parameterConversion = (str: string) => DateTime.fromString(str) } if (rule?.toStandardDate === true) { - convertToParam = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) + parameterConversion = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) } return { validate: (value: any, field: string) => { @@ -359,7 +359,7 @@ export const rule = Object.freeze({ } }, convert: (value: DateTime) => convertStdDate(value, rule), - convertToParam, + parameterConversion, ...rule } }, @@ -383,9 +383,9 @@ export const rule = Object.freeze({ } return list }, - convertToParam: (list: any[]) => { + parameterConversion: (list: any[]) => { if (rule?.apply != null) { - return list.map((value) => valueAsParam(value, rule.apply)) + return list.map((value) => optionalParameterConversion(value, rule.apply)) } return list }, @@ -412,7 +412,7 @@ export const rule = Object.freeze({ } return value }, - convertToParam: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray) : undefined, + parameterConversion: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray) : undefined, ...rule } } diff --git a/packages/core/src/temporal-types.ts b/packages/core/src/temporal-types.ts index 9c12c7a73..137105838 100644 --- a/packages/core/src/temporal-types.ts +++ b/packages/core/src/temporal-types.ts @@ -219,9 +219,7 @@ export class LocalTime { } static fromString (str: string): LocalTime { - console.log(str) const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) - console.log(values) if (values !== null) { return new LocalTime( parseInt(values[0]), @@ -343,17 +341,23 @@ export class Time { } static fromString (str: string): Time { - const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)(Z|\+|-)?(\d*)/) + const values = String(str).match(/(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { - if (values[4] === 'Z') { - return new Time(parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseInt(values[3]) * 10 ** 9, 0) + if (values[5] === 'Z') { + return new Time( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + values[4] !== undefined ? Math.round(parseFloat('0.' + values[4]) * 10 ** 9) : 0, + 0 + ) } return new Time( - parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), - Math.round(parseFloat('0.' + values[3]) * 10 ** 9), - (values[4] === '+' ? 1 : -1) * parseInt(values[5]) + parseInt(values[3]), + values[4] !== undefined ? Math.round(parseFloat('0.' + values[4]) * 10 ** 9) : 0, + (values[5] === '+' ? 1 : -1) * (parseInt(values[6]) * 3600 + parseInt(values[7]) * 60 + parseInt(values[8])) ) } throw newError('Time could not be parsed from string') @@ -619,6 +623,22 @@ export class LocalDateTime { this.nanosecond ) } + + static fromString (str: string): LocalDateTime { + const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) + if (values !== null) { + return new LocalDateTime( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + parseInt(values[4]), + parseInt(values[5]), + parseInt(values[6]), + Math.round(parseFloat('0' + values[7]) * 10 ** 9) + ) + } + throw newError('Time could not be parsed from string') + } } Object.defineProperty( @@ -795,6 +815,35 @@ export class DateTime { return localDateTimeStr + timeOffset + timeZoneStr } + static fromString (str: string): DateTime { + const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) + if (values !== null) { + if (values[8] === 'Z') { + return new DateTime( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + parseInt(values[4]), + parseInt(values[5]), + parseInt(values[6]), + Math.round(parseFloat('0' + values[7]) * 10 ** 9), + 0 + ) + } + return new DateTime( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + parseInt(values[4]), + parseInt(values[5]), + parseInt(values[6]), + Math.round(parseFloat('0.' + values[7]) * 10 ** 9), + (values[8] === '+' ? 1 : -1) * (parseInt(values[9]) * 3600 + parseInt(values[10]) * 60 + parseInt(values[11])) + ) + } + throw newError('Time could not be parsed from string') + } + /** * @private * @returns {number} diff --git a/packages/core/test/mapping.highlevel.test.ts b/packages/core/test/mapping.highlevel.test.ts index 2a3f0cd00..b44dde5ac 100644 --- a/packages/core/test/mapping.highlevel.test.ts +++ b/packages/core/test/mapping.highlevel.test.ts @@ -18,7 +18,7 @@ import { Date, DateTime, Duration, RecordObjectMapping, Node, Relationship, Rules, rule, Time, Vector } from '../src' import { as } from '../src/mapping.highlevel' -describe('#unit Record Object Mapping', () => { +describe('Record Object Mapping', () => { describe('as', () => { it('should use rules set with register', () => { class Person { @@ -73,42 +73,6 @@ describe('#unit Record Object Mapping', () => { expect(() => as(gettable, personRules)).toThrow('Object#name should be a number but received string') }) - it.each([ - ['Number', rule.asNumber(), 1, 1], - ['String', rule.asString(), 'hi', 'hi'], - ['BigInt', rule.asBigInt(), BigInt(1), BigInt(1)], - ['Date', rule.asDate(), new Date(1, 1, 1), new Date(1, 1, 1)], - ['DateTime', rule.asDateTime(), new DateTime(1, 1, 1, 1, 1, 1, 1, 1), new DateTime(1, 1, 1, 1, 1, 1, 1, 1)], - ['Duration', rule.asDuration(), new Duration(1, 1, 1, 1), new Duration(1, 1, 1, 1)], - ['Time', rule.asTime(), new Time(1, 1, 1, 1, 1), new Time(1, 1, 1, 1, 1)], - ['Simple List', rule.asList({ apply: rule.asString() }), ['hello'], ['hello']], - [ - 'Complex List', - rule.asList({ apply: rule.asVector({ asTypedList: true }) }), - [Float32Array.from([0.1, 0.2]), Float32Array.from([0.3, 0.4]), Float32Array.from([0.5, 0.6])], - [new Vector(Float32Array.from([0.1, 0.2])), new Vector(Float32Array.from([0.3, 0.4])), new Vector(Float32Array.from([0.5, 0.6]))] - ], - [ - 'Vector', - rule.asVector(), - new Vector(Int32Array.from([0, 1, 2])), - new Vector(Int32Array.from([0, 1, 2])) - ], - [ - 'Converted Vector', - rule.asVector({ asTypedList: true, from: 'vec' }), - Float32Array.from([0.1, 0.2]), - new Vector(Float32Array.from([0.1, 0.2])) - ] - ])('should be able to map %s as property', (_, rule, param, expected) => { - if (rule.convertToParam != null) { - param = rule.convertToParam(param) - } - // @ts-expect-error - rule.validate(param) - expect(param).toEqual(expected) - }) - it('should be able to read all property types', () => { class Person { name diff --git a/packages/core/test/mapping.rulesfactories.test.ts b/packages/core/test/mapping.rulesfactories.test.ts new file mode 100644 index 000000000..5f00c2002 --- /dev/null +++ b/packages/core/test/mapping.rulesfactories.test.ts @@ -0,0 +1,64 @@ +import { Date, DateTime, Duration, Time, Vector } from '../src' +import { rule } from '../src/mapping.rulesfactories' + +describe('Rulesfactories', () => { + it.each([ + ['Number', rule.asNumber(), 1, 1], + ['String', rule.asString(), 'hi', 'hi'], + ['BigInt', rule.asBigInt(), BigInt(1), BigInt(1)], + ['Date', rule.asDate(), new Date(1, 1, 1), new Date(1, 1, 1)], + ['DateTime', rule.asDateTime(), new DateTime(1, 1, 1, 1, 1, 1, 1, 1), new DateTime(1, 1, 1, 1, 1, 1, 1, 1)], + ['Duration', rule.asDuration(), new Duration(1, 1, 1, 1), new Duration(1, 1, 1, 1)], + ['Time', rule.asTime(), new Time(1, 1, 1, 1, 1), new Time(1, 1, 1, 1, 1)], + ['Simple List', rule.asList({ apply: rule.asString() }), ['hello'], ['hello']], + [ + 'Complex List', + rule.asList({ apply: rule.asVector({ asTypedList: true }) }), + [Float32Array.from([0.1, 0.2]), Float32Array.from([0.3, 0.4]), Float32Array.from([0.5, 0.6])], + [new Vector(Float32Array.from([0.1, 0.2])), new Vector(Float32Array.from([0.3, 0.4])), new Vector(Float32Array.from([0.5, 0.6]))] + ], + [ + 'Vector', + rule.asVector(), + new Vector(Int32Array.from([0, 1, 2])), + new Vector(Int32Array.from([0, 1, 2])) + ], + [ + 'Converted Vector', + rule.asVector({ asTypedList: true, from: 'vec' }), + Float32Array.from([0.1, 0.2]), + new Vector(Float32Array.from([0.1, 0.2])) + ] + ])('should be able to map %s as property', (_, rule, param, expected) => { + if (rule.parameterConversion != null) { + param = rule.parameterConversion(param) + } + // @ts-expect-error + rule.validate(param) + expect(param).toEqual(expected) + }) + + it.each([ + ['Date', rule.asDate({ stringify: true }), '2024-01-12'], + ['DateTime', rule.asDateTime({ stringify: true }), '2024-01-01T01:01:01-10:23:03'], + ['Duration', rule.asDuration({ stringify: true }), 'PT1H'], + ['Time', rule.asTime({ stringify: true }), '10:10:10Z'], + ['Simple List', rule.asList({ apply: rule.asString() }), ['hello']], + [ + 'Complex List', + rule.asList({ apply: rule.asVector({ asTypedList: true }) }), + [Float32Array.from([0.1, 0.2]), Float32Array.from([0.3, 0.4]), Float32Array.from([0.5, 0.6])] + ], + [ + 'Converted Vector', + rule.asVector({ asTypedList: true, from: 'vec' }), + Float32Array.from([0.1, 0.2]) + ] + ])('mapping %s as property and back should be lossless', (_, rule, param) => { + if (rule.parameterConversion != null && rule.convert != null) { + expect(rule.convert(rule.parameterConversion(param), 'test convertion')).toEqual(param) + } else { + throw new Error('rule lacks parameterConversion and/or convert') + } + }) +}) diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts index fa400bbed..0c3ccd7cf 100644 --- a/packages/neo4j-driver-deno/lib/core/graph-types.ts +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -18,7 +18,7 @@ import Integer from './integer.ts' import { stringify } from './json.ts' import { Rules, GenericConstructor, as } from './mapping.highlevel.ts' -export const StandardDateClass = Date +export const JSDate = Date type StandardDate = Date /** * @typedef {number | Integer | bigint} NumberOrInteger diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts index 860a3a82d..4dc6b3049 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/util.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -19,7 +19,7 @@ import Integer, { isInt, int } from '../integer.ts' import { NumberOrInteger } from '../graph-types.ts' import { EncryptionLevel } from '../types.ts' import { stringify } from '../json.ts' -import { Rules, validateAndCleanParams } from '../mapping.highlevel.ts' +import { Rules, validateAndcleanParameters } from '../mapping.highlevel.ts' const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF' @@ -85,7 +85,7 @@ function validateQueryAndParameters ( } if (!skipAsserts) { - params = validateAndCleanParams(params, parameterRules) + params = validateAndcleanParameters(params, parameterRules) assertCypherQuery(validatedQuery) assertQueryParameters(params) } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 55a24f58e..f0e2fcc13 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -29,7 +29,7 @@ export interface Rule { optional?: boolean from?: string convert?: (recordValue: any, field: string) => any - convertToParam?: (objectValue: any) => any + parameterConversion?: (objectValue: any) => any validate?: (recordValue: any, field: string) => void } @@ -181,14 +181,14 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } -export function valueAsParam (value: unknown, rule?: Rule): unknown { +export function optionalParameterConversion (value: unknown, rule?: Rule): unknown { if (rule?.optional === true && value == null) { return value } - return ((rule?.convertToParam) != null) ? rule.convertToParam(value) : value + return ((rule?.parameterConversion) != null) ? rule.parameterConversion(value) : value } -export function validateAndCleanParams (params: Record, suppliedRules?: Rules): Record { +export function validateAndcleanParameters (params: Record, suppliedRules?: Rules): Record { const cleanedParams: Record = {} // @ts-expect-error const parameterRules = getRules(params.constructor, suppliedRules) @@ -196,8 +196,8 @@ export function validateAndCleanParams (params: Record, suppliedRul for (const key in parameterRules) { if (!(parameterRules?.[key]?.optional === true)) { let param = params[key] - if (parameterRules[key]?.convertToParam !== undefined) { - param = parameterRules[key].convertToParam(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.') diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 31cfe0fb7..6933e0b0d 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Rule, valueAs, valueAsParam } from './mapping.highlevel.ts' -import { StandardDateClass, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types.ts' +import { Rule, valueAs, optionalParameterConversion } from './mapping.highlevel.ts' +import { JSDate, StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types.ts' import { isPoint } from './spatial-types.ts' import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from './temporal-types.ts' import Vector, { vector } from './vector.ts' @@ -250,7 +250,7 @@ export const rule = Object.freeze({ } }, convert: (value: Duration) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => Duration.fromString(str) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => Duration.fromString(str) : undefined, ...rule } }, @@ -269,7 +269,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalTime) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => LocalTime.fromString(str) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => LocalTime.fromString(str) : undefined, ...rule } }, @@ -288,7 +288,7 @@ export const rule = Object.freeze({ } }, convert: (value: Time) => rule?.stringify === true ? value.toString() : value, - convertToParam: rule?.stringify === true ? (str: string) => Time.fromString(str) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => Time.fromString(str) : undefined, ...rule } }, @@ -307,7 +307,7 @@ export const rule = Object.freeze({ } }, convert: (value: Date) => convertStdDate(value, rule), - convertToParam: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new StandardDateClass(str)) : undefined, + parameterConversion: rule?.stringify === true ? (str: string) => Date.fromStandardDateLocal(new JSDate(str)) : undefined, ...rule } }, @@ -319,12 +319,12 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asLocalDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { - let convertToParam + let parameterConversion if (rule?.stringify === true) { - convertToParam = (str: string) => LocalDateTime.fromStandardDate(new StandardDateClass(str)) + parameterConversion = (str: string) => LocalDateTime.fromString(str) } if (rule?.toStandardDate === true) { - convertToParam = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) + parameterConversion = (standardDate: StandardDate) => LocalDateTime.fromStandardDate(standardDate) } return { validate: (value: any, field: string) => { @@ -333,7 +333,7 @@ export const rule = Object.freeze({ } }, convert: (value: LocalDateTime) => convertStdDate(value, rule), - convertToParam, + parameterConversion, ...rule } }, @@ -345,12 +345,12 @@ export const rule = Object.freeze({ * @returns {Rule} A new rule for the value */ asDateTime (rule?: Rule & { stringify?: boolean, toStandardDate?: boolean }): Rule { - let convertToParam + let parameterConversion if (rule?.stringify === true) { - convertToParam = (str: string) => DateTime.fromStandardDate(new StandardDateClass(str)) + parameterConversion = (str: string) => DateTime.fromString(str) } if (rule?.toStandardDate === true) { - convertToParam = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) + parameterConversion = (standardDate: StandardDate) => DateTime.fromStandardDate(standardDate) } return { validate: (value: any, field: string) => { @@ -359,7 +359,7 @@ export const rule = Object.freeze({ } }, convert: (value: DateTime) => convertStdDate(value, rule), - convertToParam, + parameterConversion, ...rule } }, @@ -383,9 +383,9 @@ export const rule = Object.freeze({ } return list }, - convertToParam: (list: any[]) => { + parameterConversion: (list: any[]) => { if (rule?.apply != null) { - return list.map((value) => valueAsParam(value, rule.apply)) + return list.map((value) => optionalParameterConversion(value, rule.apply)) } return list }, @@ -412,7 +412,7 @@ export const rule = Object.freeze({ } return value }, - convertToParam: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray) : undefined, + parameterConversion: rule?.asTypedList === true ? (typedArray: Int16Array | Int32Array | BigInt64Array | Float32Array | Float64Array) => vector(typedArray) : undefined, ...rule } } diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts index 12fab7b94..e9a395faf 100644 --- a/packages/neo4j-driver-deno/lib/core/temporal-types.ts +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -219,9 +219,7 @@ export class LocalTime { } static fromString (str: string): LocalTime { - console.log(str) const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) - console.log(values) if (values !== null) { return new LocalTime( parseInt(values[0]), @@ -343,17 +341,23 @@ export class Time { } static fromString (str: string): Time { - const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)(Z|\+|-)?(\d*)/) + const values = String(str).match(/(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { - if (values[4] === 'Z') { - return new Time(parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseInt(values[3]) * 10 ** 9, 0) + if (values[5] === 'Z') { + return new Time( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + values[4] !== undefined ? Math.round(parseFloat('0.' + values[4]) * 10 ** 9) : 0, + 0 + ) } return new Time( - parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), - Math.round(parseFloat('0.' + values[3]) * 10 ** 9), - (values[4] === '+' ? 1 : -1) * parseInt(values[5]) + parseInt(values[3]), + values[4] !== undefined ? Math.round(parseFloat('0.' + values[4]) * 10 ** 9) : 0, + (values[5] === '+' ? 1 : -1) * ( parseInt(values[6]) * 3600 + parseInt(values[7]) * 60 + parseInt(values[8])) ) } throw newError('Time could not be parsed from string') @@ -619,6 +623,22 @@ export class LocalDateTime { this.nanosecond ) } + + static fromString (str: string): LocalDateTime { + const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) + if (values !== null) { + return new LocalDateTime( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + parseInt(values[4]), + parseInt(values[5]), + parseInt(values[6]), + Math.round(parseFloat('0' + values[7]) * 10 ** 9) + ) + } + throw newError('Time could not be parsed from string') + } } Object.defineProperty( @@ -795,6 +815,35 @@ export class DateTime { return localDateTimeStr + timeOffset + timeZoneStr } + static fromString (str: string): DateTime { + const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) + if (values !== null) { + if (values[8] === 'Z') { + return new DateTime( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + parseInt(values[4]), + parseInt(values[5]), + parseInt(values[6]), + Math.round(parseFloat('0' + values[7]) * 10 ** 9), + 0 + ) + } + return new DateTime( + parseInt(values[1]), + parseInt(values[2]), + parseInt(values[3]), + parseInt(values[4]), + parseInt(values[5]), + parseInt(values[6]), + Math.round(parseFloat('0.' + values[7]) * 10 ** 9), + (values[8] === '+' ? 1 : -1) * ( parseInt(values[9]) * 3600 + parseInt(values[10]) * 60 + parseInt(values[11])) + ) + } + throw newError('Time could not be parsed from string') + } + /** * @private * @returns {number} From 86254dcd08dd24b313ffe244f40dccf94bd10a2f Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:06:05 +0100 Subject: [PATCH 10/12] deno sync --- packages/neo4j-driver-deno/lib/core/temporal-types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts index e9a395faf..217d00e63 100644 --- a/packages/neo4j-driver-deno/lib/core/temporal-types.ts +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -357,7 +357,7 @@ export class Time { parseInt(values[2]), parseInt(values[3]), values[4] !== undefined ? Math.round(parseFloat('0.' + values[4]) * 10 ** 9) : 0, - (values[5] === '+' ? 1 : -1) * ( parseInt(values[6]) * 3600 + parseInt(values[7]) * 60 + parseInt(values[8])) + (values[5] === '+' ? 1 : -1) * (parseInt(values[6]) * 3600 + parseInt(values[7]) * 60 + parseInt(values[8])) ) } throw newError('Time could not be parsed from string') @@ -825,7 +825,7 @@ export class DateTime { parseInt(values[3]), parseInt(values[4]), parseInt(values[5]), - parseInt(values[6]), + parseInt(values[6]), Math.round(parseFloat('0' + values[7]) * 10 ** 9), 0 ) @@ -838,7 +838,7 @@ export class DateTime { parseInt(values[5]), parseInt(values[6]), Math.round(parseFloat('0.' + values[7]) * 10 ** 9), - (values[8] === '+' ? 1 : -1) * ( parseInt(values[9]) * 3600 + parseInt(values[10]) * 60 + parseInt(values[11])) + (values[8] === '+' ? 1 : -1) * (parseInt(values[9]) * 3600 + parseInt(values[10]) * 60 + parseInt(values[11])) ) } throw newError('Time could not be parsed from string') From 66ead652ce7fd9fa92dfd57d862d851bd2bb3954 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:21:40 +0100 Subject: [PATCH 11/12] document temporal types string parsing --- packages/core/src/temporal-types.ts | 30 +++++++++++++++++++ .../lib/core/temporal-types.ts | 30 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/core/src/temporal-types.ts b/packages/core/src/temporal-types.ts index 137105838..0a06f57b1 100644 --- a/packages/core/src/temporal-types.ts +++ b/packages/core/src/temporal-types.ts @@ -94,6 +94,12 @@ export class Duration { Object.freeze(this) } + /** + * Creates a {@link Duration} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {Duration} + */ static fromString (str: string): Duration { const matches = String(str).match(/P(?:([-?.,\d]+)Y)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)W)?(?:([-?.,\d]+)D)?T(?:([-?.,\d]+)H)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)S)?/) if (matches !== null) { @@ -218,6 +224,12 @@ export class LocalTime { ) } + /** + * Creates a {@link LocalTime} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {LocalTime} + */ static fromString (str: string): LocalTime { const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) if (values !== null) { @@ -340,6 +352,12 @@ export class Time { ) } + /** + * Creates a {@link Time} from an ISO 8601 string. + * + * @param {string} str The string to convert + * @returns {Time} + */ static fromString (str: string): Time { const values = String(str).match(/(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { @@ -624,6 +642,12 @@ export class LocalDateTime { ) } + /** + * Creates a {@link LocalDateTime} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {LocalDateTime} + */ static fromString (str: string): LocalDateTime { const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { @@ -815,6 +839,12 @@ export class DateTime { return localDateTimeStr + timeOffset + timeZoneStr } + /** + * Creates a {@link DateTime} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {DateTime} + */ static fromString (str: string): DateTime { const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts index 217d00e63..86cf5059a 100644 --- a/packages/neo4j-driver-deno/lib/core/temporal-types.ts +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -94,6 +94,12 @@ export class Duration { Object.freeze(this) } + /** + * Creates a {@link Duration} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {Duration} + */ static fromString (str: string): Duration { const matches = String(str).match(/P(?:([-?.,\d]+)Y)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)W)?(?:([-?.,\d]+)D)?T(?:([-?.,\d]+)H)?(?:([-?.,\d]+)M)?(?:([-?.,\d]+)S)?/) if (matches !== null) { @@ -218,6 +224,12 @@ export class LocalTime { ) } + /** + * Creates a {@link LocalTime} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {LocalTime} + */ static fromString (str: string): LocalTime { const values = String(str).match(/(\d+):(\d+):(\d+).(\d+)/) if (values !== null) { @@ -340,6 +352,12 @@ export class Time { ) } + /** + * Creates a {@link Time} from an ISO 8601 string. + * + * @param {string} str The string to convert + * @returns {Time} + */ static fromString (str: string): Time { const values = String(str).match(/(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { @@ -624,6 +642,12 @@ export class LocalDateTime { ) } + /** + * Creates a {@link LocalDateTime} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {LocalDateTime} + */ static fromString (str: string): LocalDateTime { const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { @@ -815,6 +839,12 @@ export class DateTime { return localDateTimeStr + timeOffset + timeZoneStr } + /** + * Creates a {@link DateTime} from an ISO 8601 string + * + * @param {string} str The string to convert + * @returns {DateTime} + */ static fromString (str: string): DateTime { const values = String(str).match(/(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|\+|-)?(\d*):?(\d*):?(\d*)/) if (values !== null) { From a1078003b1d883236b588846568f650d9dcaf231 Mon Sep 17 00:00:00 2001 From: ci <61233757+MaxAake@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:22:22 +0100 Subject: [PATCH 12/12] deno sync --- packages/neo4j-driver-deno/lib/core/temporal-types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts index 86cf5059a..abf9d1bd9 100644 --- a/packages/neo4j-driver-deno/lib/core/temporal-types.ts +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -96,7 +96,7 @@ export class Duration { /** * Creates a {@link Duration} from an ISO 8601 string - * + * * @param {string} str The string to convert * @returns {Duration} */ @@ -226,7 +226,7 @@ export class LocalTime { /** * Creates a {@link LocalTime} from an ISO 8601 string - * + * * @param {string} str The string to convert * @returns {LocalTime} */ @@ -354,7 +354,7 @@ export class Time { /** * Creates a {@link Time} from an ISO 8601 string. - * + * * @param {string} str The string to convert * @returns {Time} */ @@ -644,7 +644,7 @@ export class LocalDateTime { /** * Creates a {@link LocalDateTime} from an ISO 8601 string - * + * * @param {string} str The string to convert * @returns {LocalDateTime} */ @@ -841,7 +841,7 @@ export class DateTime { /** * Creates a {@link DateTime} from an ISO 8601 string - * + * * @param {string} str The string to convert * @returns {DateTime} */