From 5c45b0c5a08c5077fff2f4e6238d0ff4363e4ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 8 Nov 2023 12:17:41 +0100 Subject: [PATCH 01/32] Introduce Result, Record and Graph types mappping --- packages/core/src/graph-types.ts | 28 +++ packages/core/src/index.ts | 12 +- packages/core/src/mapping.highlevel.ts | 72 ++++++ packages/core/src/mapping.rulesfactories.ts | 214 +++++++++++++++++ packages/core/src/record.ts | 8 + packages/core/src/result-transformers.ts | 7 + packages/core/src/result.ts | 12 +- .../neo4j-driver-deno/lib/core/graph-types.ts | 29 +++ packages/neo4j-driver-deno/lib/core/index.ts | 12 +- .../lib/core/mapping.highlevel.ts | 76 ++++++ .../lib/core/mapping.rulesfactories.ts | 217 ++++++++++++++++++ packages/neo4j-driver-deno/lib/core/record.ts | 8 + .../lib/core/result-transformers.ts | 7 + packages/neo4j-driver-deno/lib/core/result.ts | 12 +- packages/neo4j-driver-deno/lib/mod.ts | 15 +- packages/neo4j-driver-lite/src/index.ts | 15 +- 16 files changed, 726 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/mapping.highlevel.ts create mode 100644 packages/core/src/mapping.rulesfactories.ts create mode 100644 packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts create mode 100644 packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts diff --git a/packages/core/src/graph-types.ts b/packages/core/src/graph-types.ts index 538d546ba..b7bd1069f 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' type StandardDate = Date /** @@ -84,6 +85,15 @@ class Node identity.toString()) } + as (rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as({ + get: (key) => this.properties[key] + }, constructorOrRules, rules) + } + /** * @ignore */ @@ -201,6 +211,15 @@ class Relationship end.toString()) } + as (rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as({ + get: (key) => this.properties[key] + }, constructorOrRules, rules) + } + /** * @ignore */ @@ -322,6 +341,15 @@ class UnboundRelationship(rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as({ + get: (key) => this.properties[key] + }, constructorOrRules, rules) + } + /** * @ignore */ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 82f0c76cd..5200fb846 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -94,6 +94,8 @@ import * as types from './types' import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' import * as internal from './internal' // todo: removed afterwards +import { Rule, Rules } from './mapping.highlevel' +import { RulesFactories } from './mapping.rulesfactories' /** * Object containing string constants representing predefined {@link Neo4jError} codes. @@ -171,7 +173,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export { @@ -240,7 +243,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export type { @@ -265,7 +269,9 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + Rule, + Rules } export default forExport diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts new file mode 100644 index 000000000..31dcd0bd7 --- /dev/null +++ b/packages/core/src/mapping.highlevel.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type GenericConstructor = new (...args: any[]) => T + +export interface Rule { + optional?: boolean + from?: string + convert?: (recordValue: any, field: string) => any + validate?: (recordValue: any, field: string) => void +} + +export type Rules = Record + +interface Gettable { get: (key: string) => V } + +export function as (gettable: Gettable, constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object + const theRules = typeof constructorOrRules === 'object' ? constructorOrRules : rules + const vistedKeys: string[] = [] + + const obj = new GenericConstructor() + + for (const [key, rule] of Object.entries(theRules ?? {})) { + vistedKeys.push(key) + _apply(gettable, obj, key, rule) + } + + for (const key of Object.getOwnPropertyNames(obj)) { + if (!vistedKeys.includes(key)) { + _apply(gettable, obj, key, theRules?.[key]) + } + } + + return obj as unknown as T +} + +function _apply (gettable: Gettable, obj: T, key: string, rule?: Rule): void { + const value = gettable.get(rule?.from ?? key) + const field = `${obj.constructor.name}#${key}` + const processedValue = valueAs(value, field, rule) + + // @ts-expect-error + obj[key] = processedValue ?? obj[key] +} + +export function valueAs (value: unknown, field: string, rule?: Rule): unknown { + if (rule?.optional === true && value == null) { + return value + } + + if (typeof rule?.validate === 'function') { + rule.validate(value, field) + } + + return ((rule?.convert) != null) ? rule.convert(value, field) : value +} diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts new file mode 100644 index 000000000..629431668 --- /dev/null +++ b/packages/core/src/mapping.rulesfactories.ts @@ -0,0 +1,214 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Rule, valueAs } from './mapping.highlevel' + +import { 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' + +export const RulesFactories = Object.freeze({ + asString (rule?: Rule): Rule { + return { + validate: (value, field) => { + if (typeof value !== 'string') { + throw new TypeError(`${field} should be a string but received ${typeof value}`) + } + }, + ...rule + } + }, + asNumber (rule?: Rule & { acceptBigInt?: boolean }) { + return { + validate: (value: any, field: string) => { + if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { + throw new TypeError(`${field} should be a number but received ${typeof value}`) + } + }, + convert: (value: number | bigint) => { + if (typeof value === 'bigint') { + return Number(value) + } + return value + }, + ...rule + } + }, + asBigInt (rule?: Rule & { acceptNumber?: boolean }) { + return { + validate: (value: any, field: string) => { + if (typeof value !== 'bigint' && (rule?.acceptNumber !== true || typeof value !== 'number')) { + throw new TypeError(`${field} should be a bigint but received ${typeof value}`) + } + }, + convert: (value: number | bigint) => { + if (typeof value === 'number') { + return BigInt(value) + } + return value + }, + ...rule + } + }, + asNode (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isNode(value)) { + throw new TypeError(`${field} should be a Node but received ${typeof value}`) + } + }, + ...rule + } + }, + asRelationship (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isRelationship(value)) { + throw new TypeError(`${field} should be a Relationship but received ${typeof value}`) + } + }, + ...rule + } + }, + asUnboundRelationship (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isUnboundRelationship(value)) { + throw new TypeError(`${field} should be a UnboundRelationship but received ${typeof value}`) + } + }, + ...rule + } + }, + asPath (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isPath(value)) { + throw new TypeError(`${field} should be a Path but received ${typeof value}`) + } + }, + ...rule + } + }, + asPoint (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isPoint(value)) { + throw new TypeError(`${field} should be a Point but received ${typeof value}`) + } + }, + ...rule + } + }, + asDuration (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDuration(value)) { + throw new TypeError(`${field} should be a Duration but received ${typeof value}`) + } + }, + convert: (value: Duration) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asLocalTime (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isLocalTime(value)) { + throw new TypeError(`${field} should be a LocalTime but received ${typeof value}`) + } + }, + convert: (value: LocalTime) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asTime (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isTime(value)) { + throw new TypeError(`${field} should be a Time but received ${typeof value}`) + } + }, + convert: (value: Time) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDate(value)) { + throw new TypeError(`${field} should be a Date but received ${typeof value}`) + } + }, + convert: (value: Date) => convertStdDate(value, rule), + ...rule + } + }, + asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isLocalDateTime(value)) { + throw new TypeError(`${field} should be a LocalDateTime but received ${typeof value}`) + } + }, + convert: (value: LocalDateTime) => convertStdDate(value, rule), + ...rule + } + }, + asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDateTime(value)) { + throw new TypeError(`${field} should be a DateTime but received ${typeof value}`) + } + }, + convert: (value: DateTime) => convertStdDate(value, rule), + ...rule + } + }, + asList (rule?: Rule & { apply?: Rule }) { + return { + validate: (value: any, field: string) => { + if (!Array.isArray(value)) { + throw new TypeError(`${field} should be a string but received ${typeof value}`) + } + }, + convert: (list: any[], field: string) => { + if (rule?.apply != null) { + return list.map((value, index) => valueAs(value, `${field}[${index}]`, rule.apply)) + } + return list + }, + ...rule + } + } +}) + +interface ConvertableToStdDateOrStr { toStandardDate: () => StandardDate, toString: () => string } + +function convertStdDate (value: V, rule?: { toString?: boolean, toStandardDate?: boolean }): string | V | StandardDate { + if (rule != null) { + if (rule.toString === true) { + return value.toString() + } else if (rule.toStandardDate === true) { + return value.toStandardDate() + } + } + return value +} diff --git a/packages/core/src/record.ts b/packages/core/src/record.ts index cac99dd01..059122116 100644 --- a/packages/core/src/record.ts +++ b/packages/core/src/record.ts @@ -18,6 +18,7 @@ */ import { newError } from './error' +import { Rules, GenericConstructor, as } from './mapping.highlevel' type RecordShape = { [K in Key]: Value @@ -134,6 +135,13 @@ class Record< return resultArray } + as (rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as(this, constructorOrRules, rules) + } + /** * Iterate over results. Each iteration will yield an array * of exactly two items - the key, and the value (in order). diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 8dbc4e9e1..4794f085b 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -22,6 +22,7 @@ import Result from './result' import EagerResult from './result-eager' import ResultSummary from './result-summary' import { newError } from './error' +import { GenericConstructor, Rules } from './mapping.highlevel' async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result @@ -164,6 +165,12 @@ class ResultTransformers { }) } } + + hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { + return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules) + } } /** diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index 938f04ff1..c25d6f20d 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -25,6 +25,7 @@ import { Query, PeekableAsyncIterator } from './types' import { observer, util, connectionHolder } from './internal' import { newError, PROTOCOL_ERROR } from './error' import { NumberOrInteger } from './graph-types' +import { GenericConstructor, Rules } from './mapping.highlevel' const { EMPTY_CONNECTION_HOLDER } = connectionHolder @@ -151,6 +152,13 @@ class Result implements Promise(rules: Rules): Promise<{ records: T[], summary: ResultSummary }> + as (genericConstructor: GenericConstructor, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> { + // @ts-expect-error + return this._getOrCreatePromise(r => r.as(constructorOrRules, rules)) + } + /** * Returns a promise for the field keys. * @@ -212,13 +220,13 @@ class Result implements Promise> { + private _getOrCreatePromise (mapper: (r: Record) => O = r => r as unknown as R): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: Array> = [] const observer = { onNext: (record: Record) => { - records.push(record) + records.push(mapper(record) as unknown as Record) }, onCompleted: (summary: ResultSummary) => { resolve({ records, summary }) diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts index 735f0105d..62d5c5656 100644 --- a/packages/neo4j-driver-deno/lib/core/graph-types.ts +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -18,6 +18,8 @@ */ import Integer from './integer.ts' import { stringify } from './json.ts' +import { Rules, GenericConstructor, as } from './mapping.highlevel.ts' + type StandardDate = Date /** @@ -84,6 +86,15 @@ class Node identity.toString()) } + as (rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as({ + get: (key) => this.properties[key] + }, constructorOrRules, rules) + } + /** * @ignore */ @@ -201,6 +212,15 @@ class Relationship end.toString()) } + as (rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as({ + get: (key) => this.properties[key] + }, constructorOrRules, rules) + } + /** * @ignore */ @@ -322,6 +342,15 @@ class UnboundRelationship(rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as({ + get: (key) => this.properties[key] + }, constructorOrRules, rules) + } + /** * @ignore */ diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 0242df6c3..a0c85b0c4 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -94,6 +94,8 @@ import * as types from './types.ts' import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import * as internal from './internal/index.ts' +import { Rule, Rules } from './mapping.highlevel.ts' +import { RulesFactories } from './mapping.rulesfactories.ts' /** * Object containing string constants representing predefined {@link Neo4jError} codes. @@ -171,7 +173,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export { @@ -240,7 +243,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export type { @@ -265,7 +269,9 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + Rule, + Rules } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts new file mode 100644 index 000000000..35831c256 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type GenericConstructor = new (...args: any[]) => T + +export interface Rule { + optional?: boolean, + from?: string, + convert?: (recordValue: any, field: string) => any + validate?: (recordValue: any, field: string) => void +} + +export type Rules = Record + +type Gettable = { get(key: string): V } + + +export function as (gettable: Gettable , constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object + const theRules = typeof constructorOrRules === 'object' ? constructorOrRules : rules + const vistedKeys: string[] = [] + + const obj = new GenericConstructor + + for (const [key, rule] of Object.entries(theRules ?? {})) { + vistedKeys.push(key) + _apply(gettable, obj, key, rule) + } + + for (const key of Object.getOwnPropertyNames(obj)) { + if (!vistedKeys.includes(key)) { + _apply(gettable, obj, key, theRules?.[key]) + } + } + + return obj as unknown as T +} + + +function _apply(gettable: Gettable, obj: T, key: string, rule?: Rule): void { + const value = gettable.get(rule?.from ?? key) + const field = `${obj.constructor.name}#${key}` + const processedValue = valueAs(value, field, rule) + + // @ts-ignore + obj[key] = processedValue ?? obj[key] +} + +export function valueAs (value: unknown, field: string, rule?: Rule): unknown { + if (rule?.optional === true && value == null) { + return value + } + + if (typeof rule?.validate === 'function') { + rule.validate(value, field) + } + + return rule?.convert ? rule.convert(value, field) : value +} + + diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts new file mode 100644 index 000000000..23648ab40 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -0,0 +1,217 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Rule, valueAs } from './mapping.highlevel.ts' + + +import { 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' + + +export const RulesFactories = Object.freeze({ + asString (rule?: Rule): Rule { + return { + validate: (value, field) => { + if (typeof value !== 'string') { + throw new TypeError(`${field} should be a string but received ${typeof value}`) + } + }, + ...rule + } + }, + asNumber (rule?: Rule & { acceptBigInt?: boolean }) { + return { + validate: (value: any, field: string) => { + if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { + throw new TypeError(`${field} should be a number but received ${typeof value}`) + } + }, + convert: (value: number | bigint) => { + if (typeof value === 'bigint') { + return Number(value) + } + return value + }, + ...rule + } + }, + asBigInt (rule?: Rule & { acceptNumber?: boolean }) { + return { + validate: (value: any, field: string) => { + if (typeof value !== 'bigint' && (rule?.acceptNumber !== true || typeof value !== 'number')) { + throw new TypeError(`${field} should be a bigint but received ${typeof value}`) + } + }, + convert: (value: number | bigint) => { + if (typeof value === 'number') { + return BigInt(value) + } + return value + }, + ...rule + } + }, + asNode (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isNode(value)) { + throw new TypeError(`${field} should be a Node but received ${typeof value}`) + } + }, + ...rule + } + }, + asRelationship (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isRelationship(value)) { + throw new TypeError(`${field} should be a Relationship but received ${typeof value}`) + } + }, + ...rule + } + }, + asUnboundRelationship (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isUnboundRelationship(value)) { + throw new TypeError(`${field} should be a UnboundRelationship but received ${typeof value}`) + } + }, + ...rule + } + }, + asPath (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isPath(value)) { + throw new TypeError(`${field} should be a Path but received ${typeof value}`) + } + }, + ...rule + } + }, + asPoint (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isPoint(value)) { + throw new TypeError(`${field} should be a Point but received ${typeof value}`) + } + }, + ...rule + } + }, + asDuration (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDuration(value)) { + throw new TypeError(`${field} should be a Duration but received ${typeof value}`) + } + }, + convert: (value: Duration) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asLocalTime (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isLocalTime(value)) { + throw new TypeError(`${field} should be a LocalTime but received ${typeof value}`) + } + }, + convert: (value: LocalTime) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asTime (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isTime(value)) { + throw new TypeError(`${field} should be a Time but received ${typeof value}`) + } + }, + convert: (value: Time) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDate(value)) { + throw new TypeError(`${field} should be a Date but received ${typeof value}`) + } + }, + convert: (value: Date) => convertStdDate(value, rule), + ...rule + } + }, + asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isLocalDateTime(value)) { + throw new TypeError(`${field} should be a LocalDateTime but received ${typeof value}`) + } + }, + convert: (value: LocalDateTime) => convertStdDate(value, rule), + ...rule + } + }, + asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDateTime(value)) { + throw new TypeError(`${field} should be a DateTime but received ${typeof value}`) + } + }, + convert: (value: DateTime) => convertStdDate(value, rule), + ...rule + } + }, + asList (rule?: Rule & { apply?: Rule }) { + return { + validate: (value: any, field: string) => { + if (!Array.isArray(value)) { + throw new TypeError(`${field} should be a string but received ${typeof value}`) + } + }, + convert: (list: any[], field: string) => { + if (rule?.apply != null) { + return list.map((value, index) => valueAs(value, `${field}[${index}]`, rule.apply)) + } + return list + }, + ...rule + } + + } +}) + +type ConvertableToStdDateOrStr = { toStandardDate: () => StandardDate, toString: () => string } + +function convertStdDate(value: V, rule?: { toString?: boolean, toStandardDate?: boolean }):string | V | StandardDate { + if (rule != null) { + if (rule.toString === true) { + return value.toString() + } else if (rule.toStandardDate === true) { + return value.toStandardDate() + } + } + return value +} diff --git a/packages/neo4j-driver-deno/lib/core/record.ts b/packages/neo4j-driver-deno/lib/core/record.ts index 71ba02742..0dcae2b62 100644 --- a/packages/neo4j-driver-deno/lib/core/record.ts +++ b/packages/neo4j-driver-deno/lib/core/record.ts @@ -18,6 +18,7 @@ */ import { newError } from './error.ts' +import { Rules, GenericConstructor, as } from './mapping.highlevel.ts' type RecordShape = { [K in Key]: Value @@ -134,6 +135,13 @@ class Record< return resultArray } + as (rules: Rules): T + as (genericConstructor: GenericConstructor): T + as (genericConstructor: GenericConstructor, rules?: Rules): T + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + return as(this, constructorOrRules, rules) + } + /** * Iterate over results. Each iteration will yield an array * of exactly two items - the key, and the value (in order). diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 0ac8d94f6..5ff951f7d 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -22,6 +22,7 @@ import Result from './result.ts' import EagerResult from './result-eager.ts' import ResultSummary from './result-summary.ts' import { newError } from './error.ts' +import { GenericConstructor, Rules } from './mapping.highlevel.ts' async function createEagerResultFromResult (result: Result): Promise> { const { summary, records } = await result @@ -164,6 +165,12 @@ class ResultTransformers { }) } } + + hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { + return result => result.as(constructorOrRules as unknown as GenericConstructor, rules) + } } /** diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 43ba35dfe..b56635353 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -25,6 +25,7 @@ import { Query, PeekableAsyncIterator } from './types.ts' import { observer, util, connectionHolder } from './internal/index.ts' import { newError, PROTOCOL_ERROR } from './error.ts' import { NumberOrInteger } from './graph-types.ts' +import { GenericConstructor, Rules } from './mapping.highlevel.ts' const { EMPTY_CONNECTION_HOLDER } = connectionHolder @@ -151,6 +152,13 @@ class Result implements Promise(rules: Rules): Promise<{ records: T[], summary: ResultSummary }> + as (genericConstructor: GenericConstructor, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> { + // @ts-expect-error + return this._getOrCreatePromise(r => r.as(constructorOrRules, rules)) + } + /** * Returns a promise for the field keys. * @@ -212,13 +220,13 @@ class Result implements Promise> { + private _getOrCreatePromise (mapper: (r: Record) => O = r => r as unknown as R ): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: Array> = [] const observer = { onNext: (record: Record) => { - records.push(record) + records.push(mapper(record) as unknown as Record) }, onCompleted: (summary: ResultSummary) => { resolve({ records, summary }) diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index d32a84300..346b34fe6 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -99,7 +99,10 @@ import { Transaction, TransactionPromise, types as coreTypes, - UnboundRelationship + UnboundRelationship, + Rule, + Rules, + RulesFactories } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -426,7 +429,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export { @@ -493,7 +497,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export type { QueryResult, @@ -518,6 +523,8 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + Rule, + Rules } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 994491996..fc01bcd44 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -99,7 +99,10 @@ import { Transaction, TransactionPromise, types as coreTypes, - UnboundRelationship + UnboundRelationship, + Rule, + Rules, + RulesFactories } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -425,7 +428,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export { @@ -492,7 +496,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + RulesFactories } export type { QueryResult, @@ -517,6 +522,8 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + Rule, + Rules } export default forExport From 0a93a376e83ef99972d3c29bf09bccc7c875af3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 8 Nov 2023 16:38:22 +0100 Subject: [PATCH 02/32] Add global registry --- packages/core/src/index.ts | 8 +- packages/core/src/mapping.highlevel.ts | 20 +- .../neo4j-driver-deno/lib/core/graph-types.ts | 7 +- packages/neo4j-driver-deno/lib/core/index.ts | 8 +- .../lib/core/mapping.highlevel.ts | 85 ++-- .../lib/core/mapping.rulesfactories.ts | 369 +++++++++--------- packages/neo4j-driver-deno/lib/core/record.ts | 2 +- .../lib/core/result-transformers.ts | 2 +- packages/neo4j-driver-deno/lib/core/result.ts | 2 +- packages/neo4j-driver-deno/lib/mod.ts | 9 +- packages/neo4j-driver-lite/src/index.ts | 9 +- packages/testkit-backend/package.json | 3 +- 12 files changed, 282 insertions(+), 242 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5200fb846..2a5c1f670 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -94,7 +94,7 @@ import * as types from './types' import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' import * as internal from './internal' // todo: removed afterwards -import { Rule, Rules } from './mapping.highlevel' +import { Rule, Rules, mapping } from './mapping.highlevel' import { RulesFactories } from './mapping.rulesfactories' /** @@ -174,7 +174,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export { @@ -244,7 +245,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export type { diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 31dcd0bd7..82de717ab 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -27,11 +27,21 @@ export interface Rule { export type Rules = Record +const rulesRegistry: Record = {} + +export function register (constructor: GenericConstructor, rules: Rules): void { + rulesRegistry[constructor.toString()] = rules +} + +export const mapping = { + register +} + interface Gettable { get: (key: string) => V } export function as (gettable: Gettable, constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object - const theRules = typeof constructorOrRules === 'object' ? constructorOrRules : rules + const theRules = getRules(constructorOrRules, rules) const vistedKeys: string[] = [] const obj = new GenericConstructor() @@ -70,3 +80,11 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } +function getRules (constructorOrRules: Rules | GenericConstructor, rules: Rules | undefined): Rules | undefined { + const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules + if (rulesDefined != null) { + return rulesDefined + } + + return typeof constructorOrRules !== 'object' ? rulesRegistry[constructorOrRules.toString()] : undefined +} diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts index 62d5c5656..75707ab1e 100644 --- a/packages/neo4j-driver-deno/lib/core/graph-types.ts +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -20,7 +20,6 @@ import Integer from './integer.ts' import { stringify } from './json.ts' import { Rules, GenericConstructor, as } from './mapping.highlevel.ts' - type StandardDate = Date /** * @typedef {number | Integer | bigint} NumberOrInteger @@ -92,7 +91,7 @@ class Node(constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { return as({ get: (key) => this.properties[key] - }, constructorOrRules, rules) + }, constructorOrRules, rules) } /** @@ -218,7 +217,7 @@ class Relationship(constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { return as({ get: (key) => this.properties[key] - }, constructorOrRules, rules) + }, constructorOrRules, rules) } /** @@ -348,7 +347,7 @@ class UnboundRelationship(constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { return as({ get: (key) => this.properties[key] - }, constructorOrRules, rules) + }, constructorOrRules, rules) } /** diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index a0c85b0c4..9feb8a5e2 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -94,7 +94,7 @@ import * as types from './types.ts' import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import * as internal from './internal/index.ts' -import { Rule, Rules } from './mapping.highlevel.ts' +import { Rule, Rules, mapping } from './mapping.highlevel.ts' import { RulesFactories } from './mapping.rulesfactories.ts' /** @@ -174,7 +174,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export { @@ -244,7 +245,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export type { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 35831c256..609e446c5 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -19,58 +19,73 @@ export type GenericConstructor = new (...args: any[]) => T export interface Rule { - optional?: boolean, - from?: string, - convert?: (recordValue: any, field: string) => any - validate?: (recordValue: any, field: string) => void + optional?: boolean + from?: string + convert?: (recordValue: any, field: string) => any + validate?: (recordValue: any, field: string) => void } export type Rules = Record -type Gettable = { get(key: string): V } +const rulesRegistry: Record = {} +export function register (constructor: GenericConstructor, rules: Rules): void { + rulesRegistry[constructor.toString()] = rules +} + +export const mapping = { + register +} -export function as (gettable: Gettable , constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { - const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object - const theRules = typeof constructorOrRules === 'object' ? constructorOrRules : rules - const vistedKeys: string[] = [] +interface Gettable { get: (key: string) => V } - const obj = new GenericConstructor +export function as (gettable: Gettable, constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { + const GenericConstructor = typeof constructorOrRules === 'function' ? constructorOrRules : Object + const theRules = getRules(constructorOrRules, rules) + const vistedKeys: string[] = [] - for (const [key, rule] of Object.entries(theRules ?? {})) { - vistedKeys.push(key) - _apply(gettable, obj, key, rule) - } + const obj = new GenericConstructor() - for (const key of Object.getOwnPropertyNames(obj)) { - if (!vistedKeys.includes(key)) { - _apply(gettable, obj, key, theRules?.[key]) - } + for (const [key, rule] of Object.entries(theRules ?? {})) { + vistedKeys.push(key) + _apply(gettable, obj, key, rule) + } + + for (const key of Object.getOwnPropertyNames(obj)) { + if (!vistedKeys.includes(key)) { + _apply(gettable, obj, key, theRules?.[key]) } - - return obj as unknown as T -} + } + return obj as unknown as T +} -function _apply(gettable: Gettable, obj: T, key: string, rule?: Rule): void { - const value = gettable.get(rule?.from ?? key) - const field = `${obj.constructor.name}#${key}` - const processedValue = valueAs(value, field, rule) +function _apply (gettable: Gettable, obj: T, key: string, rule?: Rule): void { + const value = gettable.get(rule?.from ?? key) + const field = `${obj.constructor.name}#${key}` + const processedValue = valueAs(value, field, rule) - // @ts-ignore - obj[key] = processedValue ?? obj[key] + // @ts-expect-error + obj[key] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { - if (rule?.optional === true && value == null) { - return value + if (rule?.optional === true && value == null) { + return value + } + + if (typeof rule?.validate === 'function') { + rule.validate(value, field) + } + + return ((rule?.convert) != null) ? rule.convert(value, field) : value +} +function getRules(constructorOrRules: Rules | GenericConstructor, rules: Rules | undefined): Rules | undefined { + const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules + if (rulesDefined != null) { + return rulesDefined } - if (typeof rule?.validate === 'function') { - rule.validate(value, field) - } - - return rule?.convert ? rule.convert(value, field) : value + return typeof constructorOrRules !== 'object' ? rulesRegistry[constructorOrRules.toString()] : undefined } - diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 23648ab40..50298d5ff 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -19,199 +19,196 @@ import { Rule, valueAs } from './mapping.highlevel.ts' - -import { StandardDate, isNode, isPath, isRelationship, isUnboundRelationship } from './graph-types.ts' +import { 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' - export const RulesFactories = Object.freeze({ - asString (rule?: Rule): Rule { - return { - validate: (value, field) => { - if (typeof value !== 'string') { - throw new TypeError(`${field} should be a string but received ${typeof value}`) - } - }, - ...rule - } - }, - asNumber (rule?: Rule & { acceptBigInt?: boolean }) { - return { - validate: (value: any, field: string) => { - if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { - throw new TypeError(`${field} should be a number but received ${typeof value}`) - } - }, - convert: (value: number | bigint) => { - if (typeof value === 'bigint') { - return Number(value) - } - return value - }, - ...rule - } - }, - asBigInt (rule?: Rule & { acceptNumber?: boolean }) { - return { - validate: (value: any, field: string) => { - if (typeof value !== 'bigint' && (rule?.acceptNumber !== true || typeof value !== 'number')) { - throw new TypeError(`${field} should be a bigint but received ${typeof value}`) - } - }, - convert: (value: number | bigint) => { - if (typeof value === 'number') { - return BigInt(value) - } - return value - }, - ...rule - } - }, - asNode (rule?: Rule) { - return { - validate: (value: any, field: string) => { - if (!isNode(value)) { - throw new TypeError(`${field} should be a Node but received ${typeof value}`) - } - }, - ...rule - } - }, - asRelationship (rule?: Rule) { - return { - validate: (value: any, field: string) => { - if (!isRelationship(value)) { - throw new TypeError(`${field} should be a Relationship but received ${typeof value}`) - } - }, - ...rule - } - }, - asUnboundRelationship (rule?: Rule) { - return { - validate: (value: any, field: string) => { - if (!isUnboundRelationship(value)) { - throw new TypeError(`${field} should be a UnboundRelationship but received ${typeof value}`) - } - }, - ...rule - } - }, - asPath (rule?: Rule) { - return { - validate: (value: any, field: string) => { - if (!isPath(value)) { - throw new TypeError(`${field} should be a Path but received ${typeof value}`) - } - }, - ...rule - } - }, - asPoint (rule?: Rule) { - return { - validate: (value: any, field: string) => { - if (!isPoint(value)) { - throw new TypeError(`${field} should be a Point but received ${typeof value}`) - } - }, - ...rule - } - }, - asDuration (rule?: Rule & { toString?: boolean }) { - return { - validate: (value: any, field: string) => { - if (!isDuration(value)) { - throw new TypeError(`${field} should be a Duration but received ${typeof value}`) - } - }, - convert: (value: Duration) => rule?.toString === true ? value.toString() : value, - ...rule - } - }, - asLocalTime (rule?: Rule & { toString?: boolean }) { - return { - validate: (value: any, field: string) => { - if (!isLocalTime(value)) { - throw new TypeError(`${field} should be a LocalTime but received ${typeof value}`) - } - }, - convert: (value: LocalTime) => rule?.toString === true ? value.toString() : value, - ...rule - } - }, - asTime (rule?: Rule & { toString?: boolean }) { - return { - validate: (value: any, field: string) => { - if (!isTime(value)) { - throw new TypeError(`${field} should be a Time but received ${typeof value}`) - } - }, - convert: (value: Time) => rule?.toString === true ? value.toString() : value, - ...rule - } - }, - asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { - return { - validate: (value: any, field: string) => { - if (!isDate(value)) { - throw new TypeError(`${field} should be a Date but received ${typeof value}`) - } - }, - convert: (value: Date) => convertStdDate(value, rule), - ...rule - } - }, - asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { - return { - validate: (value: any, field: string) => { - if (!isLocalDateTime(value)) { - throw new TypeError(`${field} should be a LocalDateTime but received ${typeof value}`) - } - }, - convert: (value: LocalDateTime) => convertStdDate(value, rule), - ...rule - } - }, - asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { - return { - validate: (value: any, field: string) => { - if (!isDateTime(value)) { - throw new TypeError(`${field} should be a DateTime but received ${typeof value}`) - } - }, - convert: (value: DateTime) => convertStdDate(value, rule), - ...rule - } - }, - asList (rule?: Rule & { apply?: Rule }) { - return { - validate: (value: any, field: string) => { - if (!Array.isArray(value)) { - throw new TypeError(`${field} should be a string but received ${typeof value}`) - } - }, - convert: (list: any[], field: string) => { - if (rule?.apply != null) { - return list.map((value, index) => valueAs(value, `${field}[${index}]`, rule.apply)) - } - return list - }, - ...rule - } - + asString (rule?: Rule): Rule { + return { + validate: (value, field) => { + if (typeof value !== 'string') { + throw new TypeError(`${field} should be a string but received ${typeof value}`) + } + }, + ...rule + } + }, + asNumber (rule?: Rule & { acceptBigInt?: boolean }) { + return { + validate: (value: any, field: string) => { + if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { + throw new TypeError(`${field} should be a number but received ${typeof value}`) + } + }, + convert: (value: number | bigint) => { + if (typeof value === 'bigint') { + return Number(value) + } + return value + }, + ...rule + } + }, + asBigInt (rule?: Rule & { acceptNumber?: boolean }) { + return { + validate: (value: any, field: string) => { + if (typeof value !== 'bigint' && (rule?.acceptNumber !== true || typeof value !== 'number')) { + throw new TypeError(`${field} should be a bigint but received ${typeof value}`) + } + }, + convert: (value: number | bigint) => { + if (typeof value === 'number') { + return BigInt(value) + } + return value + }, + ...rule + } + }, + asNode (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isNode(value)) { + throw new TypeError(`${field} should be a Node but received ${typeof value}`) + } + }, + ...rule + } + }, + asRelationship (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isRelationship(value)) { + throw new TypeError(`${field} should be a Relationship but received ${typeof value}`) + } + }, + ...rule + } + }, + asUnboundRelationship (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isUnboundRelationship(value)) { + throw new TypeError(`${field} should be a UnboundRelationship but received ${typeof value}`) + } + }, + ...rule + } + }, + asPath (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isPath(value)) { + throw new TypeError(`${field} should be a Path but received ${typeof value}`) + } + }, + ...rule } + }, + asPoint (rule?: Rule) { + return { + validate: (value: any, field: string) => { + if (!isPoint(value)) { + throw new TypeError(`${field} should be a Point but received ${typeof value}`) + } + }, + ...rule + } + }, + asDuration (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDuration(value)) { + throw new TypeError(`${field} should be a Duration but received ${typeof value}`) + } + }, + convert: (value: Duration) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asLocalTime (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isLocalTime(value)) { + throw new TypeError(`${field} should be a LocalTime but received ${typeof value}`) + } + }, + convert: (value: LocalTime) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asTime (rule?: Rule & { toString?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isTime(value)) { + throw new TypeError(`${field} should be a Time but received ${typeof value}`) + } + }, + convert: (value: Time) => rule?.toString === true ? value.toString() : value, + ...rule + } + }, + asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDate(value)) { + throw new TypeError(`${field} should be a Date but received ${typeof value}`) + } + }, + convert: (value: Date) => convertStdDate(value, rule), + ...rule + } + }, + asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isLocalDateTime(value)) { + throw new TypeError(`${field} should be a LocalDateTime but received ${typeof value}`) + } + }, + convert: (value: LocalDateTime) => convertStdDate(value, rule), + ...rule + } + }, + asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + return { + validate: (value: any, field: string) => { + if (!isDateTime(value)) { + throw new TypeError(`${field} should be a DateTime but received ${typeof value}`) + } + }, + convert: (value: DateTime) => convertStdDate(value, rule), + ...rule + } + }, + asList (rule?: Rule & { apply?: Rule }) { + return { + validate: (value: any, field: string) => { + if (!Array.isArray(value)) { + throw new TypeError(`${field} should be a string but received ${typeof value}`) + } + }, + convert: (list: any[], field: string) => { + if (rule?.apply != null) { + return list.map((value, index) => valueAs(value, `${field}[${index}]`, rule.apply)) + } + return list + }, + ...rule + } + } }) -type ConvertableToStdDateOrStr = { toStandardDate: () => StandardDate, toString: () => string } +interface ConvertableToStdDateOrStr { toStandardDate: () => StandardDate, toString: () => string } -function convertStdDate(value: V, rule?: { toString?: boolean, toStandardDate?: boolean }):string | V | StandardDate { - if (rule != null) { - if (rule.toString === true) { - return value.toString() - } else if (rule.toStandardDate === true) { - return value.toStandardDate() - } +function convertStdDate (value: V, rule?: { toString?: boolean, toStandardDate?: boolean }): string | V | StandardDate { + if (rule != null) { + if (rule.toString === true) { + return value.toString() + } else if (rule.toStandardDate === true) { + return value.toStandardDate() } - return value + } + return value } diff --git a/packages/neo4j-driver-deno/lib/core/record.ts b/packages/neo4j-driver-deno/lib/core/record.ts index 0dcae2b62..69da97bd8 100644 --- a/packages/neo4j-driver-deno/lib/core/record.ts +++ b/packages/neo4j-driver-deno/lib/core/record.ts @@ -139,7 +139,7 @@ class Record< as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { - return as(this, constructorOrRules, rules) + return as(this, constructorOrRules, rules) } /** diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 5ff951f7d..386828098 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -169,7 +169,7 @@ class ResultTransformers { hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { - return result => result.as(constructorOrRules as unknown as GenericConstructor, rules) + return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules) } } diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index b56635353..3c5ab0f98 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -220,7 +220,7 @@ class Result implements Promise (mapper: (r: Record) => O = r => r as unknown as R ): Promise> { + private _getOrCreatePromise (mapper: (r: Record) => O = r => r as unknown as R): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: Array> = [] diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 346b34fe6..78af028fd 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -102,7 +102,8 @@ import { UnboundRelationship, Rule, Rules, - RulesFactories + RulesFactories, + mapping } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -430,7 +431,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export { @@ -498,7 +500,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export type { QueryResult, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index fc01bcd44..0999da693 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -102,7 +102,8 @@ import { UnboundRelationship, Rule, Rules, - RulesFactories + RulesFactories, + mapping } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -429,7 +430,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export { @@ -497,7 +499,8 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - RulesFactories + RulesFactories, + mapping } export type { QueryResult, diff --git a/packages/testkit-backend/package.json b/packages/testkit-backend/package.json index 45ae6e92b..e01cccb1e 100644 --- a/packages/testkit-backend/package.json +++ b/packages/testkit-backend/package.json @@ -15,7 +15,8 @@ "start::deno": "deno run --allow-read --allow-write --allow-net --allow-env --allow-sys --allow-run deno/index.ts", "clean": "rm -fr node_modules public/index.js", "prepare": "npm run build", - "node": "node" + "node": "node", + "deno": "deno run --allow-read --allow-write --allow-net --allow-env --allow-sys --allow-run" }, "repository": { "type": "git", From 918989f26b45594da2001428a48873c686fa8b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 8 Nov 2023 16:38:48 +0100 Subject: [PATCH 03/32] sync deno --- .../lib/core/mapping.highlevel.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 609e446c5..82de717ab 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -30,11 +30,11 @@ export type Rules = Record const rulesRegistry: Record = {} export function register (constructor: GenericConstructor, rules: Rules): void { - rulesRegistry[constructor.toString()] = rules + rulesRegistry[constructor.toString()] = rules } export const mapping = { - register + register } interface Gettable { get: (key: string) => V } @@ -80,12 +80,11 @@ export function valueAs (value: unknown, field: string, rule?: Rule): unknown { return ((rule?.convert) != null) ? rule.convert(value, field) : value } -function getRules(constructorOrRules: Rules | GenericConstructor, rules: Rules | undefined): Rules | undefined { - const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules - if (rulesDefined != null) { - return rulesDefined - } - - return typeof constructorOrRules !== 'object' ? rulesRegistry[constructorOrRules.toString()] : undefined -} +function getRules (constructorOrRules: Rules | GenericConstructor, rules: Rules | undefined): Rules | undefined { + const rulesDefined = typeof constructorOrRules === 'object' ? constructorOrRules : rules + if (rulesDefined != null) { + return rulesDefined + } + return typeof constructorOrRules !== 'object' ? rulesRegistry[constructorOrRules.toString()] : undefined +} From 4e8ac68bd1842d73dfb99fc776c783f73dff47ef Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 31 Jan 2025 13:10:35 +0100 Subject: [PATCH 04/32] conflict resolution fix --- packages/core/src/result-transformers.ts | 6 ------ packages/neo4j-driver-deno/lib/core/result-transformers.ts | 6 ------ 2 files changed, 12 deletions(-) diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index fe84a1199..cde7515b1 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -24,12 +24,6 @@ import { GenericConstructor, Rules } from './mapping.highlevel' import { NumberOrInteger } from './graph-types' import Integer from './integer' -async function createEagerResultFromResult (result: Result): Promise> { - const { summary, records } = await result - const keys = await result.keys() - return new EagerResult(keys, records, summary) -} - type ResultTransformer = (result: Result) => Promise /** * Protocol for transforming {@link Result}. diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 98d633c14..c2ae86a13 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -24,12 +24,6 @@ import { GenericConstructor, Rules } from './mapping.highlevel.ts' import { NumberOrInteger } from './graph-types.ts' import Integer from './integer.ts' -async function createEagerResultFromResult (result: Result): Promise> { - const { summary, records } = await result - const keys = await result.keys() - return new EagerResult(keys, records, summary) -} - type ResultTransformer = (result: Result) => Promise /** * Protocol for transforming {@link Result}. From d71a19c52e24b51d198c3e4cd6d1334675e14f73 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:14:23 +0100 Subject: [PATCH 05/32] Documentation additions, import fixes and result.as fix --- packages/core/src/graph-types.ts | 21 +++ packages/core/src/mapping.highlevel.ts | 24 +++ packages/core/src/mapping.rulesfactories.ts | 163 ++++++++++++++++-- packages/core/src/result-transformers.ts | 36 +++- packages/core/src/result.ts | 53 +++++- .../neo4j-driver-deno/lib/core/graph-types.ts | 21 +++ .../lib/core/mapping.highlevel.ts | 25 +++ .../lib/core/mapping.rulesfactories.ts | 163 ++++++++++++++++-- .../lib/core/result-transformers.ts | 36 +++- packages/neo4j-driver-deno/lib/core/result.ts | 53 +++++- packages/neo4j-driver/src/index.js | 16 +- 11 files changed, 559 insertions(+), 52 deletions(-) diff --git a/packages/core/src/graph-types.ts b/packages/core/src/graph-types.ts index fbac2993d..1001d0486 100644 --- a/packages/core/src/graph-types.ts +++ b/packages/core/src/graph-types.ts @@ -83,6 +83,13 @@ class Node identity.toString()) } + /** + * Hydrates an object of a given type with the properties of the node + * + * @param {GenericConstructor | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration + * @param {Rules} [rules] {@link Rules} for the hydration + * @returns {T} + */ as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T @@ -209,6 +216,13 @@ class Relationship end.toString()) } + /** + * Hydrates an object of a given type with the properties of the relationship + * + * @param {GenericConstructor | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration + * @param {Rules} [rules] {@link Rules} for the hydration + * @returns {T} + */ as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T @@ -339,6 +353,13 @@ class UnboundRelationship | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration + * @param {Rules} [rules] {@link Rules} for the hydration + * @returns {T} + */ as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 82de717ab..9f751685a 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -16,6 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * constructor function of any class + */ export type GenericConstructor = new (...args: any[]) => T export interface Rule { @@ -29,6 +33,26 @@ export type Rules = Record const rulesRegistry: Record = {} +/** + * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. + * + * @example + * // The following code: + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * }) + * + * can instead be written: + * neo4j.mapping.register(Person, personClassRules) + * + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * }) + * + * + * @param {GenericConstructor} constructor The constructor function of the class to set rules for + * @param {Rules} rules The rules to set for the provided class + */ export function register (constructor: GenericConstructor, rules: Rules): void { rulesRegistry[constructor.toString()] = rules } diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index 629431668..f63ff44a8 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -18,12 +18,46 @@ */ import { Rule, valueAs } from './mapping.highlevel' - import { 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' +/** + * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. + * + * @property {function(rule: ?Rule & { acceptBigInt?: boolean })} asNumber Create a {@link Rule} that validates the value is a Number. + * + * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. + * + * @property {function(rule: ?Rule)} asNode Create a {@link Rule} that validates the value is a {@link Node}. + * + * @property {function(rule: ?Rule)} asRelationship Create a {@link Rule} that validates the value is a {@link Relationship}. + * + * @property {function(rule: ?Rule)} asPath Create a {@link Rule} that validates the value is a {@link Path}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asDuration Create a {@link Rule} that validates the value is a {@link Duration}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asLocalTime Create a {@link Rule} that validates the value is a {@link LocalTime}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asLocalDateTime Create a {@link Rule} that validates the value is a {@link LocalDateTime}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asTime Create a {@link Rule} that validates the value is a {@link Time}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asDateTime Create a {@link Rule} that validates the value is a {@link DateTime}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asDate Create a {@link Rule} that validates the value is a {@link Date}. + * + * @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}. + * + * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. + */ export const RulesFactories = Object.freeze({ + /** + * Create a {@link Rule} that validates the value is a String. + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ asString (rule?: Rule): Rule { return { validate: (value, field) => { @@ -34,9 +68,18 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asNumber (rule?: Rule & { acceptBigInt?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Number}. + * + * @param {Rule & { acceptBigInt?: boolean }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asNumber (rule?: Rule & { acceptBigInt?: boolean }): Rule { return { validate: (value: any, field: string) => { + if (typeof value === 'object' && value.low !== undefined && value.high !== undefined && Object.keys(value).length === 2) { + throw new TypeError('Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt to true in driver config object') + } if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { throw new TypeError(`${field} should be a number but received ${typeof value}`) } @@ -50,7 +93,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asBigInt (rule?: Rule & { acceptNumber?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link BigInt}. + * + * @param {Rule & { acceptNumber?: boolean }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asBigInt (rule?: Rule & { acceptNumber?: boolean }): Rule { return { validate: (value: any, field: string) => { if (typeof value !== 'bigint' && (rule?.acceptNumber !== true || typeof value !== 'number')) { @@ -66,7 +115,23 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asNode (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Node}. + * + * @example + * const actingJobsRules: Rules = { + * // Converts the person node to a Person object in accordance with provided rules + * person: neo4j.RulesFactories.asNode({ + * convert: (node: Node) => node.as(Person, personRules) + * }), + * // Returns the movie node as a Node + * movie: neo4j.RulesFactories.asNode({}), + * } + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asNode (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isNode(value)) { @@ -76,7 +141,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asRelationship (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Relationship}. + * + * @param {Rule} rule Configurations for the rule. + * @returns {Rule} A new rule for the value + */ + asRelationship (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isRelationship(value)) { @@ -86,7 +157,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asUnboundRelationship (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is an {@link UnboundRelationship} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asUnboundRelationship (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isUnboundRelationship(value)) { @@ -96,7 +173,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asPath (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Path} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asPath (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isPath(value)) { @@ -106,7 +189,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asPoint (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Point} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asPoint (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isPoint(value)) { @@ -116,7 +205,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asDuration (rule?: Rule & { toString?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Duration} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asDuration (rule?: Rule & { toString?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isDuration(value)) { @@ -127,7 +222,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asLocalTime (rule?: Rule & { toString?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link LocalTime} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asLocalTime (rule?: Rule & { toString?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isLocalTime(value)) { @@ -138,7 +239,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asTime (rule?: Rule & { toString?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Time} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asTime (rule?: Rule & { toString?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isTime(value)) { @@ -149,7 +256,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Date} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isDate(value)) { @@ -160,7 +273,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link LocalDateTime} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isLocalDateTime(value)) { @@ -171,7 +290,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link DateTime} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isDateTime(value)) { @@ -182,11 +307,17 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asList (rule?: Rule & { apply?: Rule }) { + /** + * Create a {@link Rule} that validates the value is a List. Optionally taking a rule for hydrating the contained values. + * + * @param {Rule & { apply?: Rule }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asList (rule?: Rule & { apply?: Rule }): Rule { return { validate: (value: any, field: string) => { if (!Array.isArray(value)) { - throw new TypeError(`${field} should be a string but received ${typeof value}`) + throw new TypeError(`${field} should be a list but received ${typeof value}`) } }, convert: (list: any[], field: string) => { diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index cde7515b1..1c7cdde35 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -270,8 +270,42 @@ class ResultTransformers { hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + /** + * Creates a {@link ResultTransformer} which maps the result to a hydrated object + * + * @example + * + * class Person { + * constructor (name) { + * this.name = name + * } + * + * const personRules: Rules = { + * name: neo4j.RulesFactories.asString() + * } + * + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * }) + * + * // Alternatively, the rules can be registered in the mapping registry. + * // This registry exists in global memory and will persist even between driver instances. + * + * neo4j.mapping.register(Person, PersonRules) + * + * // after registering the rule the transformer will follow them when mapping to the provided type + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * }) + * + * // A hydratedResultTransformer can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation + * + * @returns {ResultTransformer>} The result transformer + * @see {@link Driver#executeQuery} + * @experimental This is a preview feature + */ hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { - return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules) + return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules).then() } } diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index bbab73d67..dcba71ee5 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -61,6 +61,8 @@ interface QueryResult { summary: ResultSummary } +export interface MappedResult extends Result> {} + /** * Interface to observe updates on the Result which is being produced. * @@ -122,6 +124,7 @@ class Result implements Promise implements Promise(rules: Rules): Promise<{ records: T[], summary: ResultSummary }> - as (genericConstructor: GenericConstructor, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> - as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> { + as (rules: Rules): MappedResult + as (genericConstructor: GenericConstructor, rules?: Rules): MappedResult + /** + * Maps the records of this result to a provided type or according to provided Rules. + * + * NOTE: This modifies the Result object itself, and can not be run on a Result that is already being consumed. + * + * @example + * class Person { + * constructor ( + * public readonly name: string, + * public readonly born?: number + * ) {} + * } + * + * const personRules: Rules = { + * name: RulesFactories.asString(), + * born: RulesFactories.asNumber({ acceptBigInt: true, optional: true }) + * } + * + * await session.executeRead(async (tx: Transaction) => { + * let txres = tx.run(`MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + * WHERE id(p) <> id(c) + * RETURN p.name as name, p.born as born`).as(personRules) + * + * @param {GenericConstructor | Rules} constructorOrRules + * @param {Rules} rules + * @returns {MappedResult} + */ + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): MappedResult { + if (this._p != null) { + throw newError('Cannot call .as() on a Result that is being consumed') + } // @ts-expect-error - return this._getOrCreatePromise(r => r.as(constructorOrRules, rules)) + this._mapper = r => r.as(constructorOrRules, rules) + // @ts-expect-error + return this } /** @@ -223,13 +258,13 @@ class Result implements Promise (mapper: (r: Record) => O = r => r as unknown as R): Promise> { + private _getOrCreatePromise (): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: Array> = [] const observer = { onNext: (record: Record) => { - records.push(mapper(record) as unknown as Record) + records.push(this._mapper(record) as unknown as Record) }, onCompleted: (summary: ResultSummary) => { resolve({ records, summary }) @@ -580,7 +615,11 @@ class Result implements Promise { - observer._push({ done: false, value: record }) + if (this._mapper != null) { + observer._push({ done: false, value: this._mapper(record) }) + } else { + observer._push({ done: false, value: record }) + } }, onCompleted: (summary: ResultSummary) => { observer._push({ done: true, value: summary }) diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts index 994397495..035828573 100644 --- a/packages/neo4j-driver-deno/lib/core/graph-types.ts +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -83,6 +83,13 @@ class Node identity.toString()) } + /** + * Hydrates an object of a given type with the properties of the node + * + * @param {GenericConstructor | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration + * @param {Rules} [rules] {@link Rules} for the hydration + * @returns {T} + */ as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T @@ -209,6 +216,13 @@ class Relationship end.toString()) } + /** + * Hydrates an object of a given type with the properties of the relationship + * + * @param {GenericConstructor | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration + * @param {Rules} [rules] {@link Rules} for the hydration + * @returns {T} + */ as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T @@ -339,6 +353,13 @@ class UnboundRelationship | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration + * @param {Rules} [rules] {@link Rules} for the hydration + * @returns {T} + */ as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 82de717ab..e8d16dc69 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -16,6 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * constructor function of any class + */ export type GenericConstructor = new (...args: any[]) => T export interface Rule { @@ -29,6 +33,27 @@ export type Rules = Record const rulesRegistry: Record = {} + +/** + * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. + * + * @example + * // The following code: + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * }) + * + * can instead be written: + * neo4j.mapping.register(Person, personClassRules) + * + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * }) + * + * + * @param {GenericConstructor} constructor The constructor function of the class to set rules for + * @param {Rules} rules The rules to set for the provided class + */ export function register (constructor: GenericConstructor, rules: Rules): void { rulesRegistry[constructor.toString()] = rules } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index 50298d5ff..e2c638a13 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -18,12 +18,46 @@ */ import { Rule, valueAs } from './mapping.highlevel.ts' - import { 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' +/** + * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. + * + * @property {function(rule: ?Rule & { acceptBigInt?: boolean })} asNumber Create a {@link Rule} that validates the value is a Number. + * + * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. + * + * @property {function(rule: ?Rule)} asNode Create a {@link Rule} that validates the value is a {@link Node}. + * + * @property {function(rule: ?Rule)} asRelationship Create a {@link Rule} that validates the value is a {@link Relationship}. + * + * @property {function(rule: ?Rule)} asPath Create a {@link Rule} that validates the value is a {@link Path}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asDuration Create a {@link Rule} that validates the value is a {@link Duration}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asLocalTime Create a {@link Rule} that validates the value is a {@link LocalTime}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asLocalDateTime Create a {@link Rule} that validates the value is a {@link LocalDateTime}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asTime Create a {@link Rule} that validates the value is a {@link Time}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asDateTime Create a {@link Rule} that validates the value is a {@link DateTime}. + * + * @property {function(rule: ?Rule & { toString?: boolean })} asDate Create a {@link Rule} that validates the value is a {@link Date}. + * + * @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}. + * + * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. + */ export const RulesFactories = Object.freeze({ + /** + * Create a {@link Rule} that validates the value is a String. + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ asString (rule?: Rule): Rule { return { validate: (value, field) => { @@ -34,9 +68,18 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asNumber (rule?: Rule & { acceptBigInt?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Number}. + * + * @param {Rule & { acceptBigInt?: boolean }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asNumber (rule?: Rule & { acceptBigInt?: boolean }): Rule { return { validate: (value: any, field: string) => { + if (typeof value === 'object' && value.low !== undefined && value.high !== undefined && Object.keys(value).length === 2) { + throw new TypeError(`Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt to true in driver config object`) + } if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { throw new TypeError(`${field} should be a number but received ${typeof value}`) } @@ -50,7 +93,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asBigInt (rule?: Rule & { acceptNumber?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link BigInt}. + * + * @param {Rule & { acceptNumber?: boolean }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asBigInt (rule?: Rule & { acceptNumber?: boolean }): Rule { return { validate: (value: any, field: string) => { if (typeof value !== 'bigint' && (rule?.acceptNumber !== true || typeof value !== 'number')) { @@ -66,7 +115,23 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asNode (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Node}. + * + * @example + * const actingJobsRules: Rules = { + * // Converts the person node to a Person object in accordance with provided rules + * person: neo4j.RulesFactories.asNode({ + * convert: (node: Node) => node.as(Person, personRules) + * }), + * // Returns the movie node as a Node + * movie: neo4j.RulesFactories.asNode({}), + * } + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asNode (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isNode(value)) { @@ -76,7 +141,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asRelationship (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Relationship}. + * + * @param {Rule} rule Configurations for the rule. + * @returns {Rule} A new rule for the value + */ + asRelationship (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isRelationship(value)) { @@ -86,7 +157,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asUnboundRelationship (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is an {@link UnboundRelationship} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asUnboundRelationship (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isUnboundRelationship(value)) { @@ -96,7 +173,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asPath (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Path} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asPath (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isPath(value)) { @@ -106,7 +189,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asPoint (rule?: Rule) { + /** + * Create a {@link Rule} that validates the value is a {@link Point} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asPoint (rule?: Rule): Rule { return { validate: (value: any, field: string) => { if (!isPoint(value)) { @@ -116,7 +205,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asDuration (rule?: Rule & { toString?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Duration} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asDuration (rule?: Rule & { toString?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isDuration(value)) { @@ -127,7 +222,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asLocalTime (rule?: Rule & { toString?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link LocalTime} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asLocalTime (rule?: Rule & { toString?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isLocalTime(value)) { @@ -138,7 +239,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asTime (rule?: Rule & { toString?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Time} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asTime (rule?: Rule & { toString?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isTime(value)) { @@ -149,7 +256,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link Date} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asDate (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isDate(value)) { @@ -160,7 +273,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link LocalDateTime} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asLocalDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isLocalDateTime(value)) { @@ -171,7 +290,13 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }) { + /** + * Create a {@link Rule} that validates the value is a {@link DateTime} + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asDateTime (rule?: Rule & { toString?: boolean, toStandardDate?: boolean }): Rule { return { validate: (value: any, field: string) => { if (!isDateTime(value)) { @@ -182,11 +307,17 @@ export const RulesFactories = Object.freeze({ ...rule } }, - asList (rule?: Rule & { apply?: Rule }) { + /** + * Create a {@link Rule} that validates the value is a List. Optionally taking a rule for hydrating the contained values. + * + * @param {Rule & { apply?: Rule }} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asList (rule?: Rule & { apply?: Rule }): Rule { return { validate: (value: any, field: string) => { if (!Array.isArray(value)) { - throw new TypeError(`${field} should be a string but received ${typeof value}`) + throw new TypeError(`${field} should be a list but received ${typeof value}`) } }, convert: (list: any[], field: string) => { diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index c2ae86a13..5421b870e 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -270,8 +270,42 @@ class ResultTransformers { hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + /** + * Creates a {@link ResultTransformer} which maps the result to a hydrated object + * + * @example + * + * class Person { + * constructor (name) { + * this.name = name + * } + * + * const personRules: Rules = { + * name: neo4j.RulesFactories.asString() + * } + * + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * }) + * + * // Alternatively, the rules can be registered in the mapping registry. + * // This registry exists in global memory and will persist even between driver instances. + * + * neo4j.mapping.register(Person, PersonRules) + * + * // after registering the rule the transformer will follow them when mapping to the provided type + * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { + * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * }) + * + * // A hydratedResultTransformer can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation + * + * @returns {ResultTransformer>} The result transformer + * @see {@link Driver#executeQuery} + * @experimental This is a preview feature + */ hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { - return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules) + return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules).then() } } diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 8eda67f50..6c2241ae9 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -61,6 +61,8 @@ interface QueryResult { summary: ResultSummary } +export interface MappedResult extends Result> {} + /** * Interface to observe updates on the Result which is being produced. * @@ -122,6 +124,7 @@ class Result implements Promise implements Promise(rules: Rules): Promise<{ records: T[], summary: ResultSummary }> - as (genericConstructor: GenericConstructor, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> - as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): Promise<{ records: T[], summary: ResultSummary }> { + as (rules: Rules): MappedResult + as (genericConstructor: GenericConstructor, rules?: Rules): MappedResult + /** + * Maps the records of this result to a provided type or according to provided Rules. + * + * NOTE: This modifies the Result object itself, and can not be run on a Result that is already being consumed. + * + * @example + * class Person { + * constructor ( + * public readonly name: string, + * public readonly born?: number + * ) {} + * } + * + * const personRules: Rules = { + * name: RulesFactories.asString(), + * born: RulesFactories.asNumber({ acceptBigInt: true, optional: true }) + * } + * + * await session.executeRead(async (tx: Transaction) => { + * let txres = tx.run(`MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + * WHERE id(p) <> id(c) + * RETURN p.name as name, p.born as born`).as(personRules) + * + * @param {GenericConstructor | Rules} constructorOrRules + * @param {Rules} rules + * @returns {MappedResult} + */ + as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): MappedResult { + if(this._p != null) { + throw newError("Cannot call .as() on a Result that is being consumed") + } + // @ts-expect-error + this._mapper = (r => r.as(constructorOrRules, rules)) // @ts-expect-error - return this._getOrCreatePromise(r => r.as(constructorOrRules, rules)) + return this } /** @@ -223,13 +258,13 @@ class Result implements Promise (mapper: (r: Record) => O = r => r as unknown as R): Promise> { + private _getOrCreatePromise (): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: Array> = [] const observer = { onNext: (record: Record) => { - records.push(mapper(record) as unknown as Record) + records.push(this._mapper(record) as unknown as Record) }, onCompleted: (summary: ResultSummary) => { resolve({ records, summary }) @@ -580,7 +615,11 @@ class Result implements Promise { - observer._push({ done: false, value: record }) + if (this._mapper != null) { + observer._push({ done: false, value: this._mapper(record) }) + } else { + observer._push({ done: false, value: record }) + } }, onCompleted: (summary: ResultSummary) => { observer._push({ done: true, value: summary }) diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 911ad9fcd..660ecf58b 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -78,7 +78,10 @@ import { notificationFilterMinimumSeverityLevel, staticAuthTokenManager, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Rules, + RulesFactories, + mapping } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -282,7 +285,8 @@ const types = { LocalDateTime, LocalTime, Time, - Integer + Integer, + Rules } /** @@ -402,7 +406,9 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + RulesFactories, + mapping } export { @@ -474,6 +480,8 @@ export { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + RulesFactories, + mapping } export default forExport From cf7521da468a5acb177501c07a2fb500d290d248 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:15:03 +0100 Subject: [PATCH 06/32] deno sync --- .../neo4j-driver-deno/lib/core/graph-types.ts | 6 +- .../lib/core/mapping.highlevel.ts | 11 ++-- .../lib/core/mapping.rulesfactories.ts | 60 +++++++++---------- .../lib/core/result-transformers.ts | 18 +++--- packages/neo4j-driver-deno/lib/core/result.ts | 24 ++++---- 5 files changed, 59 insertions(+), 60 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts index 035828573..15d88d3ef 100644 --- a/packages/neo4j-driver-deno/lib/core/graph-types.ts +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -85,7 +85,7 @@ class Node | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration * @param {Rules} [rules] {@link Rules} for the hydration * @returns {T} @@ -218,7 +218,7 @@ class Relationship | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration * @param {Rules} [rules] {@link Rules} for the hydration * @returns {T} @@ -355,7 +355,7 @@ class UnboundRelationship | Rules} constructorOrRules Contructor for the desired type or {@link Rules} for the hydration * @param {Rules} [rules] {@link Rules} for the hydration * @returns {T} diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index e8d16dc69..9f751685a 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -33,24 +33,23 @@ export type Rules = Record const rulesRegistry: Record = {} - /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. - * + * * @example * // The following code: * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) * }) - * + * * can instead be written: * neo4j.mapping.register(Person, personClassRules) - * + * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) * }) - * - * + * + * * @param {GenericConstructor} constructor The constructor function of the class to set rules for * @param {Rules} rules The rules to set for the provided class */ diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index e2c638a13..b13437ae8 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -24,37 +24,37 @@ import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDat /** * @property {function(rule: ?Rule)} asString Create a {@link Rule} that validates the value is a String. - * + * * @property {function(rule: ?Rule & { acceptBigInt?: boolean })} asNumber Create a {@link Rule} that validates the value is a Number. - * + * * @property {function(rule: ?Rule & { acceptNumber?: boolean })} AsBigInt Create a {@link Rule} that validates the value is a BigInt. - * + * * @property {function(rule: ?Rule)} asNode Create a {@link Rule} that validates the value is a {@link Node}. - * + * * @property {function(rule: ?Rule)} asRelationship Create a {@link Rule} that validates the value is a {@link Relationship}. - * + * * @property {function(rule: ?Rule)} asPath Create a {@link Rule} that validates the value is a {@link Path}. - * + * * @property {function(rule: ?Rule & { toString?: boolean })} asDuration Create a {@link Rule} that validates the value is a {@link Duration}. - * + * * @property {function(rule: ?Rule & { toString?: boolean })} asLocalTime Create a {@link Rule} that validates the value is a {@link LocalTime}. - * + * * @property {function(rule: ?Rule & { toString?: boolean })} asLocalDateTime Create a {@link Rule} that validates the value is a {@link LocalDateTime}. - * + * * @property {function(rule: ?Rule & { toString?: boolean })} asTime Create a {@link Rule} that validates the value is a {@link Time}. - * + * * @property {function(rule: ?Rule & { toString?: boolean })} asDateTime Create a {@link Rule} that validates the value is a {@link DateTime}. - * + * * @property {function(rule: ?Rule & { toString?: boolean })} asDate Create a {@link Rule} that validates the value is a {@link Date}. - * + * * @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}. - * + * * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. */ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a String. - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -70,7 +70,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Number}. - * + * * @param {Rule & { acceptBigInt?: boolean }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -78,7 +78,7 @@ export const RulesFactories = Object.freeze({ return { validate: (value: any, field: string) => { if (typeof value === 'object' && value.low !== undefined && value.high !== undefined && Object.keys(value).length === 2) { - throw new TypeError(`Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt to true in driver config object`) + throw new TypeError('Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt to true in driver config object') } if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { throw new TypeError(`${field} should be a number but received ${typeof value}`) @@ -95,7 +95,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link BigInt}. - * + * * @param {Rule & { acceptNumber?: boolean }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -117,7 +117,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Node}. - * + * * @example * const actingJobsRules: Rules = { * // Converts the person node to a Person object in accordance with provided rules @@ -127,7 +127,7 @@ export const RulesFactories = Object.freeze({ * // Returns the movie node as a Node * movie: neo4j.RulesFactories.asNode({}), * } - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -143,7 +143,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Relationship}. - * + * * @param {Rule} rule Configurations for the rule. * @returns {Rule} A new rule for the value */ @@ -159,7 +159,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is an {@link UnboundRelationship} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -175,7 +175,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Path} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -191,7 +191,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Point} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -207,7 +207,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Duration} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -224,7 +224,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link LocalTime} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -241,7 +241,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Time} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -258,7 +258,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link Date} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -275,7 +275,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link LocalDateTime} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -292,7 +292,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a {@link DateTime} - * + * * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -309,7 +309,7 @@ export const RulesFactories = Object.freeze({ }, /** * Create a {@link Rule} that validates the value is a List. Optionally taking a rule for hydrating the contained values. - * + * * @param {Rule & { apply?: Rule }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index 5421b870e..dc1ff9b66 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -272,32 +272,32 @@ class ResultTransformers { hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> /** * Creates a {@link ResultTransformer} which maps the result to a hydrated object - * + * * @example - * + * * class Person { * constructor (name) { * this.name = name * } - * + * * const personRules: Rules = { * name: neo4j.RulesFactories.asString() * } - * + * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) * }) - * - * // Alternatively, the rules can be registered in the mapping registry. + * + * // Alternatively, the rules can be registered in the mapping registry. * // This registry exists in global memory and will persist even between driver instances. - * + * * neo4j.mapping.register(Person, PersonRules) - * + * * // after registering the rule the transformer will follow them when mapping to the provided type * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) * }) - * + * * // A hydratedResultTransformer can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation * * @returns {ResultTransformer>} The result transformer diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 6c2241ae9..bc3300048 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -158,9 +158,9 @@ class Result implements Promise(genericConstructor: GenericConstructor, rules?: Rules): MappedResult /** * Maps the records of this result to a provided type or according to provided Rules. - * - * NOTE: This modifies the Result object itself, and can not be run on a Result that is already being consumed. - * + * + * NOTE: This modifies the Result object itself, and can not be run on a Result that is already being consumed. + * * @example * class Person { * constructor ( @@ -173,22 +173,22 @@ class Result implements Promise { - * let txres = tx.run(`MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) - * WHERE id(p) <> id(c) + * let txres = tx.run(`MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + * WHERE id(p) <> id(c) * RETURN p.name as name, p.born as born`).as(personRules) - * - * @param {GenericConstructor | Rules} constructorOrRules - * @param {Rules} rules + * + * @param {GenericConstructor | Rules} constructorOrRules + * @param {Rules} rules * @returns {MappedResult} */ as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): MappedResult { - if(this._p != null) { - throw newError("Cannot call .as() on a Result that is being consumed") + if (this._p != null) { + throw newError('Cannot call .as() on a Result that is being consumed') } // @ts-expect-error - this._mapper = (r => r.as(constructorOrRules, rules)) + this._mapper = r => r.as(constructorOrRules, rules) // @ts-expect-error return this } From 009dd4240dca78d5edf9100fa8202333f2639322 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:11:54 +0100 Subject: [PATCH 07/32] mapper undef check --- packages/core/src/result.ts | 6 +++++- packages/neo4j-driver-deno/lib/core/result.ts | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index dcba71ee5..5ea110266 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -264,7 +264,11 @@ class Result implements Promise> = [] const observer = { onNext: (record: Record) => { - records.push(this._mapper(record) as unknown as Record) + if (this._mapper != null) { + records.push(this._mapper(record) as unknown as Record) + } else { + records.push(record as unknown as Record) + } }, onCompleted: (summary: ResultSummary) => { resolve({ records, summary }) diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index bc3300048..9595ddc4e 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -264,7 +264,12 @@ class Result implements Promise> = [] const observer = { onNext: (record: Record) => { - records.push(this._mapper(record) as unknown as Record) + if(this._mapper != null) { + records.push(this._mapper(record) as unknown as Record) + } + else { + records.push(record as unknown as Record) + } }, onCompleted: (summary: ResultSummary) => { resolve({ records, summary }) From 777c3921a3fdbd4c9823ca07daf8fb49eacccc6e Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:12:20 +0100 Subject: [PATCH 08/32] deno sync --- packages/neo4j-driver-deno/lib/core/result.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 9595ddc4e..dfc5ba4db 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -264,10 +264,9 @@ class Result implements Promise> = [] const observer = { onNext: (record: Record) => { - if(this._mapper != null) { + if (this._mapper != null) { records.push(this._mapper(record) as unknown as Record) - } - else { + } else { records.push(record as unknown as Record) } }, From 561456d0708fd2740bc1b33be451b213686633d1 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:39:00 +0100 Subject: [PATCH 09/32] initialize mapper and add namemapping --- packages/core/src/mapping.highlevel.ts | 38 +++++++++++++++--- packages/core/src/result.ts | 3 +- .../lib/core/mapping.highlevel.ts | 40 ++++++++++++++++--- packages/neo4j-driver-deno/lib/core/result.ts | 3 +- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 9f751685a..170b5ec30 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -31,8 +31,18 @@ export interface Rule { export type Rules = Record +interface nameMapper { + from: (name: string) => string + to: (name: string) => string +} + const rulesRegistry: Record = {} +let nameMapping: nameMapper = { + from: (name) => name, + to: (name) => name +} + /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * @@ -57,8 +67,25 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } +export function setNameMapping (newMapping: nameMapper): void { + nameMapping = newMapping +} + +export const nameMappers = { + pascalToCamel: { + from: (name: String) => name.charAt(0).toLowerCase() + name.slice(1), + to: (name: String) => name.charAt(0).toUpperCase() + name.slice(1) + }, + camelToPascal: { + from: (name: String) => name.charAt(0).toUpperCase() + name.slice(1), + to: (name: String) => name.charAt(0).toLowerCase() + name.slice(1) + } +} + export const mapping = { - register + register, + setNameMapping, + nameMappers } interface Gettable { get: (key: string) => V } @@ -72,12 +99,13 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - _apply(gettable, obj, key, rule) + _apply(gettable, obj, nameMapping.to(key), rule) } for (const key of Object.getOwnPropertyNames(obj)) { - if (!vistedKeys.includes(key)) { - _apply(gettable, obj, key, theRules?.[key]) + const mappedkey = nameMapping.from(key) + if (!vistedKeys.includes(mappedkey)) { + _apply(gettable, obj, key, theRules?.[mappedkey]) } } @@ -90,7 +118,7 @@ function _apply (gettable: Gettable, obj: T, key: string, rule?: R const processedValue = valueAs(value, field, rule) // @ts-expect-error - obj[key] = processedValue ?? obj[key] + obj[nameMapping.from(key)] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index 5ea110266..ffe3d286a 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -124,7 +124,7 @@ class Result implements Promise implements Promise +type nameMapper = { + from: (name: string) => string + to: (name: string) => string +} + const rulesRegistry: Record = {} + +let nameMapping: nameMapper = { + from: (name) => name, + to: (name) => name +} + + /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * @@ -57,8 +69,25 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } +export function setNameMapping(newMapping: nameMapper): void { + nameMapping = newMapping +} + +export const nameMappers = { + pascalToCamel: { + from: (name:String) => name.charAt(0).toLowerCase() + name.slice(1), + to: (name:String) => name.charAt(0).toUpperCase() + name.slice(1) + }, + camelToPascal: { + from: (name:String) => name.charAt(0).toUpperCase() + name.slice(1), + to: (name:String) => name.charAt(0).toLowerCase() + name.slice(1) + }, +} + export const mapping = { - register + register, + setNameMapping, + nameMappers } interface Gettable { get: (key: string) => V } @@ -72,12 +101,13 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - _apply(gettable, obj, key, rule) + _apply(gettable, obj, nameMapping.to(key), rule) } for (const key of Object.getOwnPropertyNames(obj)) { - if (!vistedKeys.includes(key)) { - _apply(gettable, obj, key, theRules?.[key]) + const mappedkey = nameMapping.from(key) + if (!vistedKeys.includes(mappedkey)) { + _apply(gettable, obj, key, theRules?.[mappedkey]) } } @@ -90,7 +120,7 @@ function _apply (gettable: Gettable, obj: T, key: string, rule?: R const processedValue = valueAs(value, field, rule) // @ts-expect-error - obj[key] = processedValue ?? obj[key] + obj[nameMapping.from(key)] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index dfc5ba4db..204d93d6d 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -124,7 +124,7 @@ class Result implements Promise implements Promise Date: Tue, 11 Mar 2025 08:39:44 +0100 Subject: [PATCH 10/32] deny sync --- .../lib/core/mapping.highlevel.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 6c55ddafe..170b5ec30 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -31,20 +31,18 @@ export interface Rule { export type Rules = Record -type nameMapper = { +interface nameMapper { from: (name: string) => string to: (name: string) => string } const rulesRegistry: Record = {} - -let nameMapping: nameMapper = { +let nameMapping: nameMapper = { from: (name) => name, to: (name) => name } - /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * @@ -69,19 +67,19 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } -export function setNameMapping(newMapping: nameMapper): void { +export function setNameMapping (newMapping: nameMapper): void { nameMapping = newMapping } export const nameMappers = { pascalToCamel: { - from: (name:String) => name.charAt(0).toLowerCase() + name.slice(1), - to: (name:String) => name.charAt(0).toUpperCase() + name.slice(1) + from: (name: String) => name.charAt(0).toLowerCase() + name.slice(1), + to: (name: String) => name.charAt(0).toUpperCase() + name.slice(1) }, camelToPascal: { - from: (name:String) => name.charAt(0).toUpperCase() + name.slice(1), - to: (name:String) => name.charAt(0).toLowerCase() + name.slice(1) - }, + from: (name: String) => name.charAt(0).toUpperCase() + name.slice(1), + to: (name: String) => name.charAt(0).toLowerCase() + name.slice(1) + } } export const mapping = { From 7ddb87ea0b920d9270154bd8e96e362537af7c83 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:42:17 +0100 Subject: [PATCH 11/32] improved name mappers --- packages/core/src/mapping.highlevel.ts | 44 +++++++++---------- packages/core/src/mapping.nameconventions.ts | 43 ++++++++++++++++++ .../lib/core/mapping.highlevel.ts | 43 +++++++++--------- .../lib/core/mapping.nameconventions.ts | 43 ++++++++++++++++++ 4 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 packages/core/src/mapping.nameconventions.ts create mode 100644 packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 170b5ec30..12725a881 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -17,6 +17,8 @@ * limitations under the License. */ +import { NameConvention, nameConventions } from './mapping.nameconventions' + /** * constructor function of any class */ @@ -32,15 +34,15 @@ export interface Rule { export type Rules = Record interface nameMapper { - from: (name: string) => string - to: (name: string) => string + from: NameConvention | undefined + to: NameConvention | undefined } const rulesRegistry: Record = {} -let nameMapping: nameMapper = { - from: (name) => name, - to: (name) => name +const nameMapping: nameMapper = { + from: undefined, + to: undefined } /** @@ -67,25 +69,19 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } -export function setNameMapping (newMapping: nameMapper): void { - nameMapping = newMapping +export function setDatabaseNameMapping (newMapping: NameConvention): void { + nameMapping.from = newMapping } -export const nameMappers = { - pascalToCamel: { - from: (name: String) => name.charAt(0).toLowerCase() + name.slice(1), - to: (name: String) => name.charAt(0).toUpperCase() + name.slice(1) - }, - camelToPascal: { - from: (name: String) => name.charAt(0).toUpperCase() + name.slice(1), - to: (name: String) => name.charAt(0).toLowerCase() + name.slice(1) - } +export function setCodeNameMapping (newMapping: NameConvention): void { + nameMapping.to = newMapping } export const mapping = { register, - setNameMapping, - nameMappers + setDatabaseNameMapping, + setCodeNameMapping, + nameConventions } interface Gettable { get: (key: string) => V } @@ -99,11 +95,15 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - _apply(gettable, obj, nameMapping.to(key), rule) + if (nameMapping.from !== undefined && nameMapping.to !== undefined) { + _apply(gettable, obj, nameMapping.from.encode(nameMapping.to.tokenize(key)), rule) + } else { + _apply(gettable, obj, key, rule) + } } for (const key of Object.getOwnPropertyNames(obj)) { - const mappedkey = nameMapping.from(key) + const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key if (!vistedKeys.includes(mappedkey)) { _apply(gettable, obj, key, theRules?.[mappedkey]) } @@ -116,9 +116,9 @@ function _apply (gettable: Gettable, obj: T, key: string, rule?: R const value = gettable.get(rule?.from ?? key) const field = `${obj.constructor.name}#${key}` const processedValue = valueAs(value, field, rule) - + const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key // @ts-expect-error - obj[nameMapping.from(key)] = processedValue ?? obj[key] + obj[mappedkey] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { diff --git a/packages/core/src/mapping.nameconventions.ts b/packages/core/src/mapping.nameconventions.ts new file mode 100644 index 000000000..73c01aad8 --- /dev/null +++ b/packages/core/src/mapping.nameconventions.ts @@ -0,0 +1,43 @@ +export interface NameConvention { + tokenize: (name: string) => string[] + encode: (tokens: string[]) => string +} + +export const nameConventions = { + snake_case: { + tokenize: (name: string) => name.split('_'), + encode: (tokens: string[]) => tokens.join('_') + }, + 'kebab-case': { + tokenize: (name: string) => name.split('-'), + encode: (tokens: string[]) => tokens.join('-') + }, + PascalCase: { + tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), + encode: (tokens: string[]) => { + let name: string = '' + for (let token of tokens) { + token = token.charAt(0).toUpperCase() + token.slice(1) + name += token + } + return name + } + }, + camelCase: { + tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), + encode: (tokens: string[]) => { + let name: string = '' + for (let [i, token] of tokens.entries()) { + if (i !== 0) { + token = token.charAt(0).toUpperCase() + token.slice(1) + } + name += token + } + return name + } + }, + SNAKE_CAPS: { + tokenize: (name: string) => name.split('_').map((token) => token.toLowerCase()), + encode: (tokens: string[]) => tokens.join('_').toUpperCase() + } +} diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 170b5ec30..fef101ab0 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -17,6 +17,8 @@ * limitations under the License. */ +import { NameConvention, nameConventions } from './mapping.nameconventions.ts' + /** * constructor function of any class */ @@ -32,15 +34,15 @@ export interface Rule { export type Rules = Record interface nameMapper { - from: (name: string) => string - to: (name: string) => string + from: NameConvention | undefined + to: NameConvention | undefined } const rulesRegistry: Record = {} let nameMapping: nameMapper = { - from: (name) => name, - to: (name) => name + from: undefined, + to: undefined } /** @@ -67,25 +69,19 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } -export function setNameMapping (newMapping: nameMapper): void { - nameMapping = newMapping +export function setDatabaseNameMapping (newMapping: NameConvention): void { + nameMapping.from = newMapping } -export const nameMappers = { - pascalToCamel: { - from: (name: String) => name.charAt(0).toLowerCase() + name.slice(1), - to: (name: String) => name.charAt(0).toUpperCase() + name.slice(1) - }, - camelToPascal: { - from: (name: String) => name.charAt(0).toUpperCase() + name.slice(1), - to: (name: String) => name.charAt(0).toLowerCase() + name.slice(1) - } +export function setCodeNameMapping (newMapping: NameConvention): void { + nameMapping.to = newMapping } export const mapping = { register, - setNameMapping, - nameMappers + setDatabaseNameMapping, + setCodeNameMapping, + nameConventions, } interface Gettable { get: (key: string) => V } @@ -99,11 +95,16 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - _apply(gettable, obj, nameMapping.to(key), rule) + if(nameMapping.from !== undefined && nameMapping.to !== undefined) { + _apply(gettable, obj, nameMapping.from.encode(nameMapping.to.tokenize(key)), rule) + } + else { + _apply(gettable, obj, key, rule) + } } for (const key of Object.getOwnPropertyNames(obj)) { - const mappedkey = nameMapping.from(key) + const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key if (!vistedKeys.includes(mappedkey)) { _apply(gettable, obj, key, theRules?.[mappedkey]) } @@ -116,9 +117,9 @@ function _apply (gettable: Gettable, obj: T, key: string, rule?: R const value = gettable.get(rule?.from ?? key) const field = `${obj.constructor.name}#${key}` const processedValue = valueAs(value, field, rule) - + const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key // @ts-expect-error - obj[nameMapping.from(key)] = processedValue ?? obj[key] + obj[mappedkey] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts new file mode 100644 index 000000000..26c54bd31 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts @@ -0,0 +1,43 @@ +export type NameConvention = { + tokenize: (name: string) => Array + encode: (tokens: Array) => string +} + +export const nameConventions = { + "snake_case": { + tokenize: (name: string) => name.split("_"), + encode: (tokens: Array) => tokens.join("_") + }, + "kebab-case": { + tokenize: (name: string) => name.split("-"), + encode: (tokens: Array) => tokens.join("-") + }, + "PascalCase": { + tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), + encode: (tokens: Array) => { + let name:string = "" + for (var token of tokens) { + token = token.charAt(0).toUpperCase() + token.slice(1) + name += token + } + return name + } + }, + "camelCase": { + tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), + encode: (tokens: Array) => { + let name:string = "" + for (var [i, token] of tokens.entries()) { + if( i !== 0) { + token = token.charAt(0).toUpperCase() + token.slice(1) + } + name += token + } + return name + } + }, + "SNAKE_CAPS": { + tokenize: (name: string) => name.split("_").map((token) => token.toLowerCase()), + encode: (tokens: Array) => tokens.join("_").toUpperCase() + } +} From 65d2c56870db85c7591b05b855a7b5ae3ba251f2 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:42:58 +0100 Subject: [PATCH 12/32] deno sync --- .../lib/core/mapping.highlevel.ts | 9 +-- .../lib/core/mapping.nameconventions.ts | 74 +++++++++---------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index fef101ab0..bd5d6a460 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -40,7 +40,7 @@ interface nameMapper { const rulesRegistry: Record = {} -let nameMapping: nameMapper = { +const nameMapping: nameMapper = { from: undefined, to: undefined } @@ -81,7 +81,7 @@ export const mapping = { register, setDatabaseNameMapping, setCodeNameMapping, - nameConventions, + nameConventions } interface Gettable { get: (key: string) => V } @@ -95,10 +95,9 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - if(nameMapping.from !== undefined && nameMapping.to !== undefined) { + if (nameMapping.from !== undefined && nameMapping.to !== undefined) { _apply(gettable, obj, nameMapping.from.encode(nameMapping.to.tokenize(key)), rule) - } - else { + } else { _apply(gettable, obj, key, rule) } } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts index 26c54bd31..73c01aad8 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts @@ -1,43 +1,43 @@ -export type NameConvention = { - tokenize: (name: string) => Array - encode: (tokens: Array) => string +export interface NameConvention { + tokenize: (name: string) => string[] + encode: (tokens: string[]) => string } export const nameConventions = { - "snake_case": { - tokenize: (name: string) => name.split("_"), - encode: (tokens: Array) => tokens.join("_") - }, - "kebab-case": { - tokenize: (name: string) => name.split("-"), - encode: (tokens: Array) => tokens.join("-") - }, - "PascalCase": { - tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), - encode: (tokens: Array) => { - let name:string = "" - for (var token of tokens) { - token = token.charAt(0).toUpperCase() + token.slice(1) - name += token - } - return name - } - }, - "camelCase": { - tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), - encode: (tokens: Array) => { - let name:string = "" - for (var [i, token] of tokens.entries()) { - if( i !== 0) { - token = token.charAt(0).toUpperCase() + token.slice(1) - } - name += token - } - return name + snake_case: { + tokenize: (name: string) => name.split('_'), + encode: (tokens: string[]) => tokens.join('_') + }, + 'kebab-case': { + tokenize: (name: string) => name.split('-'), + encode: (tokens: string[]) => tokens.join('-') + }, + PascalCase: { + tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), + encode: (tokens: string[]) => { + let name: string = '' + for (let token of tokens) { + token = token.charAt(0).toUpperCase() + token.slice(1) + name += token + } + return name + } + }, + camelCase: { + tokenize: (name: string) => name.split(/(?=[A-Z])/).map((token) => token.toLowerCase()), + encode: (tokens: string[]) => { + let name: string = '' + for (let [i, token] of tokens.entries()) { + if (i !== 0) { + token = token.charAt(0).toUpperCase() + token.slice(1) } - }, - "SNAKE_CAPS": { - tokenize: (name: string) => name.split("_").map((token) => token.toLowerCase()), - encode: (tokens: Array) => tokens.join("_").toUpperCase() + name += token + } + return name } + }, + SNAKE_CAPS: { + tokenize: (name: string) => name.split('_').map((token) => token.toLowerCase()), + encode: (tokens: string[]) => tokens.join('_').toUpperCase() + } } From f2c8d422ea50baac3f9322b16e49db14d0d2081a Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:39:49 +0100 Subject: [PATCH 13/32] simplify name translations --- packages/core/src/mapping.highlevel.ts | 43 +++++++------------ .../lib/core/mapping.highlevel.ts | 43 +++++++------------ 2 files changed, 30 insertions(+), 56 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 12725a881..add80d887 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { NameConvention, nameConventions } from './mapping.nameconventions' +import { nameConventions } from './mapping.nameconventions' /** * constructor function of any class @@ -33,17 +33,9 @@ export interface Rule { export type Rules = Record -interface nameMapper { - from: NameConvention | undefined - to: NameConvention | undefined -} - const rulesRegistry: Record = {} -const nameMapping: nameMapper = { - from: undefined, - to: undefined -} +let nameMapping: (name: string) => string = (name) => name /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. @@ -69,19 +61,19 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } -export function setDatabaseNameMapping (newMapping: NameConvention): void { - nameMapping.from = newMapping +export function translatePropertyNames (translationFunction: (name: string) => string): void { + nameMapping = translationFunction } -export function setCodeNameMapping (newMapping: NameConvention): void { - nameMapping.to = newMapping +export function defaultNameTranslation (from: string, to: string): ((name: string) => string) { + // @ts-expect-error + return (name: string) => nameConventions[from].encode(nameConventions[to].tokenize(name)) } export const mapping = { register, - setDatabaseNameMapping, - setCodeNameMapping, - nameConventions + translatePropertyNames, + defaultNameTranslation } interface Gettable { get: (key: string) => V } @@ -95,17 +87,12 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - if (nameMapping.from !== undefined && nameMapping.to !== undefined) { - _apply(gettable, obj, nameMapping.from.encode(nameMapping.to.tokenize(key)), rule) - } else { - _apply(gettable, obj, key, rule) - } + _apply(gettable, obj, key, rule) } for (const key of Object.getOwnPropertyNames(obj)) { - const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key - if (!vistedKeys.includes(mappedkey)) { - _apply(gettable, obj, key, theRules?.[mappedkey]) + if (!vistedKeys.includes(key)) { + _apply(gettable, obj, key, theRules?.[key]) } } @@ -113,12 +100,12 @@ export function as (gettable: Gettable, constructorOrRul } function _apply (gettable: Gettable, obj: T, key: string, rule?: Rule): void { - const value = gettable.get(rule?.from ?? key) + const mappedkey = nameMapping(key) + const value = gettable.get(rule?.from ?? mappedkey) const field = `${obj.constructor.name}#${key}` const processedValue = valueAs(value, field, rule) - const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key // @ts-expect-error - obj[mappedkey] = processedValue ?? obj[key] + obj[key] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index bd5d6a460..05a26498d 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { NameConvention, nameConventions } from './mapping.nameconventions.ts' +import { nameConventions } from './mapping.nameconventions.ts' /** * constructor function of any class @@ -33,17 +33,9 @@ export interface Rule { export type Rules = Record -interface nameMapper { - from: NameConvention | undefined - to: NameConvention | undefined -} - const rulesRegistry: Record = {} -const nameMapping: nameMapper = { - from: undefined, - to: undefined -} +let nameMapping: (name:string) => string = (name) => name /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. @@ -69,19 +61,19 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } -export function setDatabaseNameMapping (newMapping: NameConvention): void { - nameMapping.from = newMapping +export function translatePropertyNames(translationFunction: (name: string) => string): void { + nameMapping = translationFunction } -export function setCodeNameMapping (newMapping: NameConvention): void { - nameMapping.to = newMapping +export function defaultNameTranslation(from: string, to: string): ((name: string) => string) { + // @ts-expect-error + return (name:string) => nameConventions[from].encode(nameConventions[to].tokenize(name)) } export const mapping = { register, - setDatabaseNameMapping, - setCodeNameMapping, - nameConventions + translatePropertyNames, + defaultNameTranslation } interface Gettable { get: (key: string) => V } @@ -95,17 +87,12 @@ export function as (gettable: Gettable, constructorOrRul for (const [key, rule] of Object.entries(theRules ?? {})) { vistedKeys.push(key) - if (nameMapping.from !== undefined && nameMapping.to !== undefined) { - _apply(gettable, obj, nameMapping.from.encode(nameMapping.to.tokenize(key)), rule) - } else { - _apply(gettable, obj, key, rule) - } + _apply(gettable, obj, key, rule) } for (const key of Object.getOwnPropertyNames(obj)) { - const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key - if (!vistedKeys.includes(mappedkey)) { - _apply(gettable, obj, key, theRules?.[mappedkey]) + if (!vistedKeys.includes(key)) { + _apply(gettable, obj, key, theRules?.[key]) } } @@ -113,12 +100,12 @@ export function as (gettable: Gettable, constructorOrRul } function _apply (gettable: Gettable, obj: T, key: string, rule?: Rule): void { - const value = gettable.get(rule?.from ?? key) + const mappedkey = nameMapping(key) + const value = gettable.get(rule?.from ?? mappedkey) const field = `${obj.constructor.name}#${key}` const processedValue = valueAs(value, field, rule) - const mappedkey = (nameMapping.from !== undefined && nameMapping.to !== undefined) ? nameMapping.to.encode(nameMapping.from.tokenize(key)) : key // @ts-expect-error - obj[mappedkey] = processedValue ?? obj[key] + obj[key] = processedValue ?? obj[key] } export function valueAs (value: unknown, field: string, rule?: Rule): unknown { From 3ce02e60cf4afd80f3ec1e7559b52ad1d9b5a507 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:40:28 +0100 Subject: [PATCH 14/32] deno sync --- packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 05a26498d..66dbec2fb 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -35,7 +35,7 @@ export type Rules = Record const rulesRegistry: Record = {} -let nameMapping: (name:string) => string = (name) => name +let nameMapping: (name: string) => string = (name) => name /** * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. @@ -61,13 +61,13 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } -export function translatePropertyNames(translationFunction: (name: string) => string): void { +export function translatePropertyNames (translationFunction: (name: string) => string): void { nameMapping = translationFunction } -export function defaultNameTranslation(from: string, to: string): ((name: string) => string) { +export function defaultNameTranslation (from: string, to: string): ((name: string) => string) { // @ts-expect-error - return (name:string) => nameConventions[from].encode(nameConventions[to].tokenize(name)) + return (name: string) => nameConventions[from].encode(nameConventions[to].tokenize(name)) } export const mapping = { From 281c5cdf80ed22ac886d3c38362f6377411134a8 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:56:55 +0100 Subject: [PATCH 15/32] name mapping documentation --- packages/core/src/mapping.highlevel.ts | 63 ++++++++++++++++++- .../lib/core/mapping.highlevel.ts | 63 ++++++++++++++++++- 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index add80d887..6b23503e7 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { newError } from './error' import { nameConventions } from './mapping.nameconventions' /** @@ -33,7 +34,7 @@ export interface Rule { export type Rules = Record -const rulesRegistry: Record = {} +let rulesRegistry: Record = {} let nameMapping: (name: string) => string = (name) => name @@ -61,17 +62,73 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } +/** + * Clears all registered type mappings from the mapping registry. + */ +export function clearMappingRegistry (): void { + rulesRegistry = {} +} + +/** + * Sets a default name translation from record keys to object properties. + * If providing a function, provide a function that maps FROM your object properties names TO record key names. + * + * The function defaultNameTranslation can be used to provide a prewritten translation function between some common naming conventions. + * + * @example + * //if the keys on records from the database are in ALLCAPS + * mapping.translatePropertyNames((name) => name.toUpperCase()) + * + * //if you utilize PacalCase in the database and camelCase in JavaScript code. + * mapping.translatePropertyNames(mapping.defaultNameTranslation("PascalCase", "camelCase")) + * + * //if a type has one odd mapping you can override the translation with the rule + * const personRules = { + * firstName: neo4j.RulesFactories.asString(), + * bornAt: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true }) + * weird_name-property: neo4j.RulesFactories.asString({from: 'homeTown'}) + * } + * //These rules can then be used by providing them to a hydratedResultsMapper + * record.as(personRules) + * //or by registering them to the mapping registry + * mapping.register(Person, personRules) + * + * @param {function} translationFunction A function translating the names of your JS object property names to record key names + */ export function translatePropertyNames (translationFunction: (name: string) => string): void { nameMapping = translationFunction } -export function defaultNameTranslation (from: string, to: string): ((name: string) => string) { +/** + * Creates a translation frunction from record key names to object property names, for use with the {@link translatePropertyNames} function + * + * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SNAKE_CAPS" + * + * @param {string} databaseConvention The naming convention in use in database result Records + * @param {string} codeConvention The naming convention in use in JavaScript object properties + * @returns {function} translation function + */ +export function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { + const keys = Object.keys(nameConventions) + if (!(databaseConvention in keys)) { + throw newError( + `Naming convention ${databaseConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function` + ) + } + if (!(codeConvention in keys)) { + throw newError( + `Naming convention ${codeConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function` + ) + } // @ts-expect-error - return (name: string) => nameConventions[from].encode(nameConventions[to].tokenize(name)) + return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) } export const mapping = { register, + clearMappingRegistry, translatePropertyNames, defaultNameTranslation } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 66dbec2fb..159b038bf 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { newError } from './error.ts' import { nameConventions } from './mapping.nameconventions.ts' /** @@ -33,7 +34,7 @@ export interface Rule { export type Rules = Record -const rulesRegistry: Record = {} +let rulesRegistry: Record = {} let nameMapping: (name: string) => string = (name) => name @@ -61,17 +62,73 @@ export function register (constructor: GenericConstructo rulesRegistry[constructor.toString()] = rules } +/** + * Clears all registered type mappings from the mapping registry. + */ +export function clearMappingRegistry (): void { + rulesRegistry = {} +} + +/** + * Sets a default name translation from record keys to object properties. + * If providing a function, provide a function that maps FROM your object properties names TO record key names. + * + * The function defaultNameTranslation can be used to provide a prewritten translation function between some common naming conventions. + * + * @example + * //if the keys on records from the database are in ALLCAPS + * mapping.translatePropertyNames((name) => name.toUpperCase()) + * + * //if you utilize PacalCase in the database and camelCase in JavaScript code. + * mapping.translatePropertyNames(mapping.defaultNameTranslation("PascalCase", "camelCase")) + * + * //if a type has one odd mapping you can override the translation with the rule + * const personRules = { + * firstName: neo4j.RulesFactories.asString(), + * bornAt: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true }) + * weird_name-property: neo4j.RulesFactories.asString({from: 'homeTown'}) + * } + * //These rules can then be used by providing them to a hydratedResultsMapper + * record.as(personRules) + * //or by registering them to the mapping registry + * mapping.register(Person, personRules) + * + * @param {function} translationFunction A function translating the names of your JS object property names to record key names + */ export function translatePropertyNames (translationFunction: (name: string) => string): void { nameMapping = translationFunction } -export function defaultNameTranslation (from: string, to: string): ((name: string) => string) { +/** + * Creates a translation frunction from record key names to object property names, for use with the {@link translatePropertyNames} function + * + * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SNAKE_CAPS" + * + * @param {string} databaseConvention The naming convention in use in database result Records + * @param {string} codeConvention The naming convention in use in JavaScript object properties + * @returns {function} translation function + */ +export function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { + const keys = Object.keys(nameConventions) + if (!(databaseConvention in keys)) { + throw newError( + `Naming convention ${databaseConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function` + ) + } + if (!(codeConvention in keys)) { + throw newError( + `Naming convention ${codeConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function` + ) + } // @ts-expect-error - return (name: string) => nameConventions[from].encode(nameConventions[to].tokenize(name)) + return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) } export const mapping = { register, + clearMappingRegistry, translatePropertyNames, defaultNameTranslation } From 3116c635bfbd35784ee99c434dadd3fe0708868b Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:57:21 +0100 Subject: [PATCH 16/32] deno sync --- .../lib/core/mapping.highlevel.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 159b038bf..2961db9d7 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -72,16 +72,16 @@ export function clearMappingRegistry (): void { /** * Sets a default name translation from record keys to object properties. * If providing a function, provide a function that maps FROM your object properties names TO record key names. - * + * * The function defaultNameTranslation can be used to provide a prewritten translation function between some common naming conventions. - * + * * @example * //if the keys on records from the database are in ALLCAPS * mapping.translatePropertyNames((name) => name.toUpperCase()) - * + * * //if you utilize PacalCase in the database and camelCase in JavaScript code. - * mapping.translatePropertyNames(mapping.defaultNameTranslation("PascalCase", "camelCase")) - * + * mapping.translatePropertyNames(mapping.defaultNameTranslation("PascalCase", "camelCase")) + * * //if a type has one odd mapping you can override the translation with the rule * const personRules = { * firstName: neo4j.RulesFactories.asString(), @@ -92,7 +92,7 @@ export function clearMappingRegistry (): void { * record.as(personRules) * //or by registering them to the mapping registry * mapping.register(Person, personRules) - * + * * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ export function translatePropertyNames (translationFunction: (name: string) => string): void { @@ -101,9 +101,9 @@ export function translatePropertyNames (translationFunction: (name: string) => s /** * Creates a translation frunction from record key names to object property names, for use with the {@link translatePropertyNames} function - * + * * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SNAKE_CAPS" - * + * * @param {string} databaseConvention The naming convention in use in database result Records * @param {string} codeConvention The naming convention in use in JavaScript object properties * @returns {function} translation function From 11a74c4b337dc98ae58e3220c370442813aa24e7 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:00:56 +0100 Subject: [PATCH 17/32] example test and fix to convention namecheck --- packages/core/src/mapping.highlevel.ts | 8 +- .../lib/core/mapping.highlevel.ts | 8 +- packages/neo4j-driver/test/examples.test.js | 120 ++++++++++++++++++ 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 6b23503e7..28092d85b 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -110,16 +110,16 @@ export function translatePropertyNames (translationFunction: (name: string) => s */ export function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { const keys = Object.keys(nameConventions) - if (!(databaseConvention in keys)) { + if (!keys.includes(databaseConvention)) { throw newError( `Naming convention ${databaseConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function` + please provide a recognized name convention or manually provide a translation function.` ) } - if (!(codeConvention in keys)) { + if (!keys.includes(codeConvention)) { throw newError( `Naming convention ${codeConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function` + please provide a recognized name convention or manually provide a translation function.` ) } // @ts-expect-error diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 2961db9d7..8f6a7163e 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -110,16 +110,16 @@ export function translatePropertyNames (translationFunction: (name: string) => s */ export function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { const keys = Object.keys(nameConventions) - if (!(databaseConvention in keys)) { + if (!keys.includes(databaseConvention)) { throw newError( `Naming convention ${databaseConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function` + please provide a recognized name convention or manually provide a translation function.` ) } - if (!(codeConvention in keys)) { + if (!keys.includes(codeConvention)) { throw newError( `Naming convention ${codeConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function` + please provide a recognized name convention or manually provide a translation function.` ) } // @ts-expect-error diff --git a/packages/neo4j-driver/test/examples.test.js b/packages/neo4j-driver/test/examples.test.js index d282e54ca..47c013dd8 100644 --- a/packages/neo4j-driver/test/examples.test.js +++ b/packages/neo4j-driver/test/examples.test.js @@ -1564,6 +1564,126 @@ describe('#integration examples', () => { }) }) + describe('Record Object Mapping', () => { + it('Record mapping', async () => { + const driver = driverGlobal + + await driver.executeQuery( + `MERGE (p1:Person {name: $name1, born: $born1}) + MERGE (p2:Person {name: $name2, born: $born2}) + MERGE (m:Movie {title: $title, tagline: $tagline}) + MERGE (p1)-[:ACTED_IN {characterName: $char1}]->(m) + MERGE (p2)-[:ACTED_IN {characterName: $char2}]->(m) + `, { + name1: 'Max', + born1: 2024, + name2: 'TBD', + born2: 2030, + title: 'Neo4j JavaScript Driver', + tagline: 'The best driver for the best database!', + char1: 'current dev', + char2: 'next dev' + }) + + class ActingJobs { + constructor ( + Person, + Movie, + Costars + ) { + this.Person = Person + this.Movie = Movie + this.Costars = Costars + } + } + + class Movie { + constructor ( + Title, + Released, + Tagline + ) { + this.Title = Title + this.Released = Released + this.Tagline = Tagline + } + } + + class Person { + constructor ( + Name, + Born + ) { + this.Name = Name + this.Born = Born + } + } + + class Role { + constructor ( + Name + ) { + this.Name = Name + } + } + + const personRules = { + Name: neo4j.RulesFactories.asString(), + Born: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true }) + } + + const movieRules = { + Title: neo4j.RulesFactories.asString(), + Released: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true, from: 'release' }), + Tagline: neo4j.RulesFactories.asString({ optional: true }) + } + + const roleRules = { + Name: neo4j.RulesFactories.asString({ from: 'characterName' }) + } + + const actingJobsRules = { + Person: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Person, personRules) + }), + Role: neo4j.RulesFactories.asRelationship({ + convert: (rel) => rel.as(Role, roleRules) + }), + Movie: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Movie, movieRules) + }), + Costars: neo4j.RulesFactories.asList({ + apply: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Person, personRules) + }) + }) + } + + neo4j.mapping.translatePropertyNames(neo4j.mapping.defaultNameTranslation('camelCase', 'PascalCase')) + neo4j.mapping.register(Person, personRules) + neo4j.mapping.register(Movie, movieRules) + neo4j.mapping.register(ActingJobs, actingJobsRules) + neo4j.mapping.register(Role, roleRules) + const session = driver.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + `MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + WHERE id(p) <> id(c) AND p.name = "Max" + RETURN p AS person, r as role, m AS movie, COLLECT(c) AS costars` + ) + return txres.as(ActingJobs) + }) + + expect(res.records[0].Person.Born).toBe(2024) + expect(res.records[0].Role.Name).toBe('current dev') + expect(res.records[0].Costars[0].Name).toBe('TBD') + + neo4j.mapping.clearMappingRegistry() + session.close() + }) + }) + it('should control flow by resume and pause the stream', async () => { const driver = driverGlobal const callCostlyApi = async () => {} From 0a83829ff79d544045e89efb55026535aad3901d Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:35:45 +0100 Subject: [PATCH 18/32] license header --- packages/core/src/mapping.nameconventions.ts | 19 +++++++++++++++++++ .../lib/core/mapping.nameconventions.ts | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/packages/core/src/mapping.nameconventions.ts b/packages/core/src/mapping.nameconventions.ts index 73c01aad8..d65ceb0fa 100644 --- a/packages/core/src/mapping.nameconventions.ts +++ b/packages/core/src/mapping.nameconventions.ts @@ -1,3 +1,22 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export interface NameConvention { tokenize: (name: string) => string[] encode: (tokens: string[]) => string diff --git a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts index 73c01aad8..d65ceb0fa 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts @@ -1,3 +1,22 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export interface NameConvention { tokenize: (name: string) => string[] encode: (tokens: string[]) => string From 6af3a9036babd7a9886c8f2f628886f47c3220f3 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 18 Mar 2025 10:05:30 +0100 Subject: [PATCH 19/32] unit tests for mapping --- packages/core/test/mapping.highlevel.test.ts | 163 ++++++++++++++++++ .../core/test/mapping.nameconventions.test.ts | 37 ++++ 2 files changed, 200 insertions(+) create mode 100644 packages/core/test/mapping.highlevel.test.ts create mode 100644 packages/core/test/mapping.nameconventions.test.ts diff --git a/packages/core/test/mapping.highlevel.test.ts b/packages/core/test/mapping.highlevel.test.ts new file mode 100644 index 000000000..f2a1722c7 --- /dev/null +++ b/packages/core/test/mapping.highlevel.test.ts @@ -0,0 +1,163 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Date, DateTime, Duration, mapping, Node, Relationship, Rules, RulesFactories, Time } from '../src' +import { as } from '../src/mapping.highlevel' + +describe('#unit Record Object Mapping', () => { + describe('as', () => { + it('should use rules set with register', () => { + class Person { + name + constructor ( + name: String + ) { + this.name = name + } + } + + const personRules: Rules = { + name: RulesFactories.asString({ from: 'firstname' }) + } + + const gettable = { + get: (index: string) => { + if (index === 'firstname') { + return 'hi' + } + if (index === 'name') { + return 'hello' + } + return undefined + } + } + + mapping.register(Person, personRules) + // @ts-expect-error + expect(as(gettable, Person).name).toBe('hi') + mapping.clearMappingRegistry() + // @ts-expect-error + expect(as(gettable, Person).name).toBe('hello') + }) + it('should perform typechecks according to rules', () => { + const personRules: Rules = { + name: RulesFactories.asNumber({ from: 'firstname' }) + } + + const gettable = { + get: (index: string) => { + if (index === 'firstname') { + return 'hi' + } + if (index === 'name') { + return 'hello' + } + return undefined + } + } + // @ts-expect-error + expect(() => as(gettable, personRules)).toThrow('Object#name should be a number but received string') + }) + it('should be able to read all property types', () => { + class Person { + name + constructor ( + name: String + ) { + this.name = name + } + } + + const personRules: Rules = { + name: RulesFactories.asString({ from: 'firstname' }) + } + const rules: Rules = { + number: RulesFactories.asNumber(), + string: RulesFactories.asString(), + bigint: RulesFactories.asBigInt(), + date: RulesFactories.asDate(), + dateTime: RulesFactories.asDateTime(), + duration: RulesFactories.asDuration(), + time: RulesFactories.asTime(), + list: RulesFactories.asList({ apply: RulesFactories.asString() }), + node: RulesFactories.asNode({ convert: (node) => node.as(Person, personRules) }), + rel: RulesFactories.asRelationship({ convert: (rel) => rel.as(Person, personRules) }) + } + + const gettable = { + get: (index: string) => { + switch (index) { + case 'string': + return 'hi' + case 'number': + return 1 + case 'bigint': + return BigInt(1) + case 'date': + return new Date(1, 1, 1) + case 'dateTime': + return new DateTime(1, 1, 1, 1, 1, 1, 1, 1) + case 'duration': + return new Duration(1, 1, 1, 1) + case 'time': + return new Time(1, 1, 1, 1, 1) + case 'list': + return ['hello'] + case 'node': + return new Node(1, [], { firstname: 'hi' }) + case 'rel': + return new Relationship(2, 1, 1, 'test', { firstname: 'bye' }) + default: + return undefined + } + } + } + // @ts-expect-error + const result = as(gettable, rules) + + // @ts-expect-error + expect(result.string).toBe('hi') + + // @ts-expect-error + expect(result.number).toBe(1) + + // @ts-expect-error + expect(result.bigint).toBe(BigInt(1)) + + // @ts-expect-error + expect(result.list[0]).toBe('hello') + + // @ts-expect-error + expect(result.date.toString()).toBe('0001-01-01') + + // @ts-expect-error + expect(result.dateTime.toString()).toBe('0001-01-01T01:01:01.000000001+00:00:01') + + // @ts-expect-error + expect(result.duration.toString()).toBe('P1M1DT1.000000001S') + + // @ts-expect-error + expect(result.time.toString()).toBe('01:01:01.000000001+00:00:01') + + // @ts-expect-error + expect(result.node.name).toBe('hi') + + // @ts-expect-error + expect(result.rel.name).toBe('bye') + }) + }) +}) diff --git a/packages/core/test/mapping.nameconventions.test.ts b/packages/core/test/mapping.nameconventions.test.ts new file mode 100644 index 000000000..9cf9b11e3 --- /dev/null +++ b/packages/core/test/mapping.nameconventions.test.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mapping } from '../src' + +describe('#unit defaultNameTranslations', () => { + // Each convention has "tokenize" and "encode" functions, so testing each twice is sufficient. + it('camelCase to SNAKE_CAPS', () => { + expect(mapping.defaultNameTranslation('camelCase', 'SNAKE_CAPS')('I_AM_COOL')).toBe('iAmCool') + }) + it('SNAKE_CAPS to PascalCase', () => { + expect(mapping.defaultNameTranslation('SNAKE_CAPS', 'PascalCase')('IAmCool')).toBe('I_AM_COOL') + }) + it('PascalCase to snake_case', () => { + expect(mapping.defaultNameTranslation('PascalCase', 'snake_case')('i_am_cool')).toBe('IAmCool') + }) + it('snake_case to kebab-case', () => { + expect(mapping.defaultNameTranslation('snake_case', 'kebab-case')('i-am-cool')).toBe('i_am_cool') + }) + it('kebab-case to camelCase', () => { + expect(mapping.defaultNameTranslation('kebab-case', 'camelCase')('iAmCool')).toBe('i-am-cool') + }) +}) From b8b037ee7271c6aa9523518790cae60415f6c1d6 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:37:54 +0100 Subject: [PATCH 20/32] integration test expansion --- packages/neo4j-driver/test/examples.test.js | 46 ++- .../test/record-object-mapping.test.js | 303 ++++++++++++++++++ 2 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 packages/neo4j-driver/test/record-object-mapping.test.js diff --git a/packages/neo4j-driver/test/examples.test.js b/packages/neo4j-driver/test/examples.test.js index 47c013dd8..39a8aa96a 100644 --- a/packages/neo4j-driver/test/examples.test.js +++ b/packages/neo4j-driver/test/examples.test.js @@ -1566,12 +1566,13 @@ describe('#integration examples', () => { describe('Record Object Mapping', () => { it('Record mapping', async () => { - const driver = driverGlobal + const driver = neo4j.driver(uri, sharedNeo4j.authToken, { disableLosslessIntegers: true }) + // Setting up the contents of the database for the test. await driver.executeQuery( `MERGE (p1:Person {name: $name1, born: $born1}) MERGE (p2:Person {name: $name2, born: $born2}) - MERGE (m:Movie {title: $title, tagline: $tagline}) + MERGE (m:Movie {title: $title, release: $release, tagline: $tagline}) MERGE (p1)-[:ACTED_IN {characterName: $char1}]->(m) MERGE (p2)-[:ACTED_IN {characterName: $char2}]->(m) `, { @@ -1579,12 +1580,14 @@ describe('#integration examples', () => { born1: 2024, name2: 'TBD', born2: 2030, + release: 2015, title: 'Neo4j JavaScript Driver', tagline: 'The best driver for the best database!', char1: 'current dev', char2: 'next dev' }) + // A few dummy classes for the test. class ActingJobs { constructor ( Person, @@ -1627,6 +1630,7 @@ describe('#integration examples', () => { } } + // Create rules for the hydration of the created types const personRules = { Name: neo4j.RulesFactories.asString(), Born: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true }) @@ -1643,27 +1647,34 @@ describe('#integration examples', () => { } const actingJobsRules = { + // The following rule unpacks the person node from the result into a Person object. + // The rules for the types don't need to be provided as we will be registering the rules for Person, Role and Movie in the mapping registry Person: neo4j.RulesFactories.asNode({ - convert: (node) => node.as(Person, personRules) + convert: (node) => node.as(Person) }), Role: neo4j.RulesFactories.asRelationship({ - convert: (rel) => rel.as(Role, roleRules) + convert: (rel) => rel.as(Role) }), Movie: neo4j.RulesFactories.asNode({ - convert: (node) => node.as(Movie, movieRules) + convert: (node) => node.as(Movie) }), Costars: neo4j.RulesFactories.asList({ apply: neo4j.RulesFactories.asNode({ - convert: (node) => node.as(Person, personRules) + convert: (node) => node.as(Person) }) }) } - neo4j.mapping.translatePropertyNames(neo4j.mapping.defaultNameTranslation('camelCase', 'PascalCase')) + // Register the rules for the custom types in the mapping registry. + // This is optional, but not doing it means that rules must be provided for every conversion. + neo4j.mapping.register(Role, roleRules) neo4j.mapping.register(Person, personRules) neo4j.mapping.register(Movie, movieRules) neo4j.mapping.register(ActingJobs, actingJobsRules) - neo4j.mapping.register(Role, roleRules) + + // The code uses PascalCase for property names, while the cypher has camelCase. This issue can be solved with the following line. + neo4j.mapping.translatePropertyNames(neo4j.mapping.defaultNameTranslation('camelCase', 'PascalCase')) + const session = driver.session() const res = await session.executeRead(async (tx) => { @@ -1679,8 +1690,25 @@ describe('#integration examples', () => { expect(res.records[0].Role.Name).toBe('current dev') expect(res.records[0].Costars[0].Name).toBe('TBD') - neo4j.mapping.clearMappingRegistry() session.close() + + // alternatively, conversions can be performed with hydratedResultTransformers + const executeQueryRes = await driver.executeQuery( + `MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + WHERE id(p) <> id(c) AND p.name = "Max" + RETURN p AS person, r as role, m AS movie, COLLECT(c) AS costars`, + {}, + { resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(ActingJobs) } + ) + + expect(executeQueryRes.records[0].Person.Born).toBe(2024) + expect(executeQueryRes.records[0].Role.Name).toBe('current dev') + expect(executeQueryRes.records[0].Costars[0].Name).toBe('TBD') + + // The following line removes all rules from the mapping registry, this is run here just to not interfere with other tests. + neo4j.mapping.clearMappingRegistry() + + driver.close() }) }) diff --git a/packages/neo4j-driver/test/record-object-mapping.test.js b/packages/neo4j-driver/test/record-object-mapping.test.js new file mode 100644 index 000000000..dfa45e0a5 --- /dev/null +++ b/packages/neo4j-driver/test/record-object-mapping.test.js @@ -0,0 +1,303 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import neo4j, { Date, Duration, Time, Point, DateTime, LocalDateTime, LocalTime } from '../src' +import sharedNeo4j from './internal/shared-neo4j' + +describe('#integration record object mapping', () => { + let driverGlobal + + class ActingJobs { + person + movie + } + class Movie { + title + released + tagline + } + + class Person { + name + born + } + + class Role { + name + } + + // Create rules for the hydration of the created types + const personRules = { + name: neo4j.RulesFactories.asString(), + born: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true }) + } + + const movieRules = { + title: neo4j.RulesFactories.asString(), + released: neo4j.RulesFactories.asNumber({ acceptBigInt: true, optional: true, from: 'release' }), + tagline: neo4j.RulesFactories.asString({ optional: true }) + } + + const roleRules = { + name: neo4j.RulesFactories.asString({ from: 'characterName' }) + } + + const actingJobsRules = { + person: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Person) + }), + role: neo4j.RulesFactories.asRelationship({ + convert: (rel) => rel.as(Role) + }), + movie: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Movie) + }), + costars: neo4j.RulesFactories.asList({ + apply: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Person) + }) + }) + } + + const actingJobsNestedRules = { + person: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Person, personRules) + }), + role: neo4j.RulesFactories.asRelationship({ + convert: (rel) => rel.as(Role, roleRules) + }), + movie: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Movie, movieRules) + }), + costars: neo4j.RulesFactories.asList({ + apply: neo4j.RulesFactories.asNode({ + convert: (node) => node.as(Person, personRules) + }) + }) + } + const uri = `bolt://${sharedNeo4j.hostnameWithBoltPort}` + + beforeAll(() => { + driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken, { disableLosslessIntegers: true }) + }) + + afterAll(async () => { + await driverGlobal.close() + }) + + it('map transaction result with registered mappings', async () => { + await driverGlobal.executeQuery( + `MERGE (p1:Person {name: $name1, born: $born1}) + MERGE (p2:Person {name: $name2, born: $born2}) + MERGE (m:Movie {title: $title, release: 2015, tagline: $tagline}) + MERGE (p1)-[:ACTED_IN {characterName: $char1}]->(m) + MERGE (p2)-[:ACTED_IN {characterName: $char2}]->(m) + `, { + name1: 'Max', + born1: 2024, + name2: 'TBD', + born2: 2030, + title: 'Neo4j JavaScript Driver', + tagline: 'The best driver for the best database!', + char1: 'current dev', + char2: 'next dev' + }) + + neo4j.mapping.register(Role, roleRules) + neo4j.mapping.register(Person, personRules) + neo4j.mapping.register(Movie, movieRules) + neo4j.mapping.register(ActingJobs, actingJobsRules) + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + `MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + WHERE id(p) <> id(c) AND p.name = "Max" + RETURN p AS person, r as role, m AS movie, COLLECT(c) AS costars` + ) + return txres.as(ActingJobs) + }) + + expect(res.records[0].person.born).toBe(2024) + expect(res.records[0].role.name).toBe('current dev') + expect(res.records[0].costars[0].name).toBe('TBD') + + session.close() + + neo4j.mapping.clearMappingRegistry() + }) + + it('map transaction result with registered mappings', async () => { + await driverGlobal.executeQuery( + `MERGE (p1:Person {name: $name1, born: $born1}) + MERGE (p2:Person {name: $name2, born: $born2}) + MERGE (m:Movie {title: $title, release: 2015, tagline: $tagline}) + MERGE (p1)-[:ACTED_IN {characterName: $char1}]->(m) + MERGE (p2)-[:ACTED_IN {characterName: $char2}]->(m) + `, { + name1: 'Max', + born1: 2024, + name2: 'TBD', + born2: 2030, + title: 'Neo4j JavaScript Driver', + tagline: 'The best driver for the best database!', + char1: 'current dev', + char2: 'next dev' + }) + + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + `MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) + WHERE id(p) <> id(c) AND p.name = "Max" + RETURN p AS person, r as role, m AS movie, COLLECT(c) AS costars` + ) + return txres.as(ActingJobs, actingJobsNestedRules) + }) + + expect(res.records[0].person.born).toBe(2024) + expect(res.records[0].role.name).toBe('current dev') + expect(res.records[0].costars[0].name).toBe('TBD') + + session.close() + }) + + it('map duration', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new Duration(1, 1, 1, 1) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asDuration() }) + }) + expect(res.records[0].obj.months).toBe(1) + + session.close() + }) + + it('map local time', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new LocalTime(1, 1, 1, 1) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asLocalTime() }) + }) + expect(res.records[0].obj.hour).toBe(1) + + session.close() + }) + + it('map time', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new Time(1, 1, 1, 1, 42) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asTime() }) + }) + expect(res.records[0].obj.hour).toBe(1) + expect(res.records[0].obj.timeZoneOffsetSeconds).toBe(42) + + session.close() + }) + + it('map date', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new Date(1, 1, 1, 1) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asDate() }) + }) + expect(res.records[0].obj.month).toBe(1) + + session.close() + }) + + it('map datetime', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new DateTime(1, 1, 1, 1, 1, 1, 1, 42) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asDateTime() }) + }) + expect(res.records[0].obj.month).toBe(1) + expect(res.records[0].obj.hour).toBe(1) + expect(res.records[0].obj.timeZoneOffsetSeconds).toBe(42) + + session.close() + }) + + it('map local datetime', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new LocalDateTime(1, 1, 1, 1, 1, 1, 1) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asLocalDateTime() }) + }) + expect(res.records[0].obj.month).toBe(1) + expect(res.records[0].obj.hour).toBe(1) + + session.close() + }) + + it('map point', async () => { + const session = driverGlobal.session() + + const res = await session.executeRead(async (tx) => { + const txres = tx.run( + 'RETURN $obj as obj', + { + obj: new Point(4326, 32.812493, 42.983216) + } + ) + return txres.as({ obj: neo4j.RulesFactories.asPoint() }) + }) + expect(res.records[0].obj.x).toBe(32.812493) + expect(res.records[0].obj.srid).toBe(4326) + + session.close() + }) +}) From 28df1896fa5954053b982a58be2851f1faa2489a Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:09:14 +0100 Subject: [PATCH 21/32] small doc changes and renaming hydrated --- packages/core/src/mapping.highlevel.ts | 6 +++--- packages/core/src/record.ts | 7 +++++++ packages/core/src/result-transformers.ts | 14 +++++++------- packages/core/src/result.ts | 2 +- .../lib/core/mapping.highlevel.ts | 6 +++--- packages/neo4j-driver-deno/lib/core/record.ts | 7 +++++++ .../lib/core/result-transformers.ts | 14 +++++++------- packages/neo4j-driver-deno/lib/core/result.ts | 2 +- 8 files changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 28092d85b..7acd447e3 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -39,19 +39,19 @@ let rulesRegistry: Record = {} let nameMapping: (name: string) => string = (name) => name /** - * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. + * Registers a set of {@link Rules} to be used by {@link hydrated} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * * @example * // The following code: * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * resultTransformer: neo4j.resultTransformers.hydrated(Person, personClassRules) * }) * * can instead be written: * neo4j.mapping.register(Person, personClassRules) * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * resultTransformer: neo4j.resultTransformers.hydrated(Person) * }) * * diff --git a/packages/core/src/record.ts b/packages/core/src/record.ts index 9231fbf45..d8052189c 100644 --- a/packages/core/src/record.ts +++ b/packages/core/src/record.ts @@ -136,6 +136,13 @@ class Record< as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T + /** + * Maps the record to a provided type and/or according to provided Rules. + * + * @param {GenericConstructor | Rules} constructorOrRules + * @param {Rules} rules + * @returns {T} + */ as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { return as(this, constructorOrRules, rules) } diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 1c7cdde35..154bcdd2b 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -268,10 +268,10 @@ class ResultTransformers { return summary } - hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> - hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydrated (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydrated (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> /** - * Creates a {@link ResultTransformer} which maps the result to a hydrated object + * Creates a {@link ResultTransformer} which maps each record of the result to a hydrated object of a provided type and/or according to provided rules. * * @example * @@ -285,7 +285,7 @@ class ResultTransformers { * } * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * resultTransformer: neo4j.resultTransformers.hydrated(Person, personClassRules) * }) * * // Alternatively, the rules can be registered in the mapping registry. @@ -295,16 +295,16 @@ class ResultTransformers { * * // after registering the rule the transformer will follow them when mapping to the provided type * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * resultTransformer: neo4j.resultTransformers.hydrated(Person) * }) * - * // A hydratedResultTransformer can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation + * // A hydrated can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation * * @returns {ResultTransformer>} The result transformer * @see {@link Driver#executeQuery} * @experimental This is a preview feature */ - hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { + hydrated (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules).then() } } diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index ffe3d286a..ec1cdd45e 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -158,7 +158,7 @@ class Result implements Promise(rules: Rules): MappedResult as (genericConstructor: GenericConstructor, rules?: Rules): MappedResult /** - * Maps the records of this result to a provided type or according to provided Rules. + * Maps the records of this result to a provided type and/or according to provided Rules. * * NOTE: This modifies the Result object itself, and can not be run on a Result that is already being consumed. * diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 8f6a7163e..43fb0577a 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -39,19 +39,19 @@ let rulesRegistry: Record = {} let nameMapping: (name: string) => string = (name) => name /** - * Registers a set of {@link Rules} to be used by {@link hydratedResultTransformer} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. + * Registers a set of {@link Rules} to be used by {@link hydrated} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * * @example * // The following code: * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * resultTransformer: neo4j.resultTransformers.hydrated(Person, personClassRules) * }) * * can instead be written: * neo4j.mapping.register(Person, personClassRules) * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * resultTransformer: neo4j.resultTransformers.hydrated(Person) * }) * * diff --git a/packages/neo4j-driver-deno/lib/core/record.ts b/packages/neo4j-driver-deno/lib/core/record.ts index 40fa621a8..d52cac551 100644 --- a/packages/neo4j-driver-deno/lib/core/record.ts +++ b/packages/neo4j-driver-deno/lib/core/record.ts @@ -136,6 +136,13 @@ class Record< as (rules: Rules): T as (genericConstructor: GenericConstructor): T as (genericConstructor: GenericConstructor, rules?: Rules): T + /** + * Maps the record to a provided type and/or according to provided Rules. + * + * @param {GenericConstructor | Rules} constructorOrRules + * @param {Rules} rules + * @returns {T} + */ as (constructorOrRules: GenericConstructor | Rules, rules?: Rules): T { return as(this, constructorOrRules, rules) } diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index dc1ff9b66..f57eefa45 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -268,10 +268,10 @@ class ResultTransformers { return summary } - hydratedResultTransformer (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> - hydratedResultTransformer (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydrated (rules: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> + hydrated (genericConstructor: GenericConstructor, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> /** - * Creates a {@link ResultTransformer} which maps the result to a hydrated object + * Creates a {@link ResultTransformer} which maps each record of the result to a hydrated object of a provided type and/or according to provided rules. * * @example * @@ -285,7 +285,7 @@ class ResultTransformers { * } * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person, personClassRules) + * resultTransformer: neo4j.resultTransformers.hydrated(Person, personClassRules) * }) * * // Alternatively, the rules can be registered in the mapping registry. @@ -295,16 +295,16 @@ class ResultTransformers { * * // after registering the rule the transformer will follow them when mapping to the provided type * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { - * resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(Person) + * resultTransformer: neo4j.resultTransformers.hydrated(Person) * }) * - * // A hydratedResultTransformer can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation + * // A hydrated can be used without providing or registering Rules beforehand, but in such case the mapping will be done without any type validation * * @returns {ResultTransformer>} The result transformer * @see {@link Driver#executeQuery} * @experimental This is a preview feature */ - hydratedResultTransformer (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { + hydrated (constructorOrRules: GenericConstructor | Rules, rules?: Rules): ResultTransformer<{ records: T[], summary: ResultSummary }> { return async result => await result.as(constructorOrRules as unknown as GenericConstructor, rules).then() } } diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 204d93d6d..3783128b7 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -158,7 +158,7 @@ class Result implements Promise(rules: Rules): MappedResult as (genericConstructor: GenericConstructor, rules?: Rules): MappedResult /** - * Maps the records of this result to a provided type or according to provided Rules. + * Maps the records of this result to a provided type and/or according to provided Rules. * * NOTE: This modifies the Result object itself, and can not be run on a Result that is already being consumed. * From eafc6412ea6413a22b701f33cc16ae99a7a5dc96 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:00:29 +0100 Subject: [PATCH 22/32] remove global exports of mapping functions --- packages/core/src/mapping.highlevel.ts | 12 ++++++------ .../neo4j-driver-deno/lib/core/mapping.highlevel.ts | 6 +++--- packages/neo4j-driver-deno/lib/core/record.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 7acd447e3..3b96d29b4 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -65,7 +65,7 @@ export function register (constructor: GenericConstructo /** * Clears all registered type mappings from the mapping registry. */ -export function clearMappingRegistry (): void { +function clearMappingRegistry (): void { rulesRegistry = {} } @@ -95,7 +95,7 @@ export function clearMappingRegistry (): void { * * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ -export function translatePropertyNames (translationFunction: (name: string) => string): void { +function translatePropertyNames (translationFunction: (name: string) => string): void { nameMapping = translationFunction } @@ -108,7 +108,7 @@ export function translatePropertyNames (translationFunction: (name: string) => s * @param {string} codeConvention The naming convention in use in JavaScript object properties * @returns {function} translation function */ -export function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { +function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { const keys = Object.keys(nameConventions) if (!keys.includes(databaseConvention)) { throw newError( @@ -127,10 +127,10 @@ export function defaultNameTranslation (databaseConvention: string, codeConventi } export const mapping = { - register, clearMappingRegistry, - translatePropertyNames, - defaultNameTranslation + defaultNameTranslation, + register, + translatePropertyNames } interface Gettable { get: (key: string) => V } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 43fb0577a..2eaf1465e 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -65,7 +65,7 @@ export function register (constructor: GenericConstructo /** * Clears all registered type mappings from the mapping registry. */ -export function clearMappingRegistry (): void { +function clearMappingRegistry (): void { rulesRegistry = {} } @@ -95,7 +95,7 @@ export function clearMappingRegistry (): void { * * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ -export function translatePropertyNames (translationFunction: (name: string) => string): void { +function translatePropertyNames (translationFunction: (name: string) => string): void { nameMapping = translationFunction } @@ -108,7 +108,7 @@ export function translatePropertyNames (translationFunction: (name: string) => s * @param {string} codeConvention The naming convention in use in JavaScript object properties * @returns {function} translation function */ -export function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { +function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { const keys = Object.keys(nameConventions) if (!keys.includes(databaseConvention)) { throw newError( diff --git a/packages/neo4j-driver-deno/lib/core/record.ts b/packages/neo4j-driver-deno/lib/core/record.ts index d52cac551..9b669c042 100644 --- a/packages/neo4j-driver-deno/lib/core/record.ts +++ b/packages/neo4j-driver-deno/lib/core/record.ts @@ -138,7 +138,7 @@ class Record< as (genericConstructor: GenericConstructor, rules?: Rules): T /** * Maps the record to a provided type and/or according to provided Rules. - * + * * @param {GenericConstructor | Rules} constructorOrRules * @param {Rules} rules * @returns {T} From b407c47462ec7022bce4c3c68c2ab54b193c63de Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:01:11 +0100 Subject: [PATCH 23/32] hydrated renamed in examples testing --- packages/neo4j-driver/test/examples.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/neo4j-driver/test/examples.test.js b/packages/neo4j-driver/test/examples.test.js index 39a8aa96a..a0005645a 100644 --- a/packages/neo4j-driver/test/examples.test.js +++ b/packages/neo4j-driver/test/examples.test.js @@ -1692,13 +1692,13 @@ describe('#integration examples', () => { session.close() - // alternatively, conversions can be performed with hydratedResultTransformers + // alternatively, conversions can be performed with hydrated const executeQueryRes = await driver.executeQuery( `MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(c:Person) WHERE id(p) <> id(c) AND p.name = "Max" RETURN p AS person, r as role, m AS movie, COLLECT(c) AS costars`, {}, - { resultTransformer: neo4j.resultTransformers.hydratedResultTransformer(ActingJobs) } + { resultTransformer: neo4j.resultTransformers.hydrated(ActingJobs) } ) expect(executeQueryRes.records[0].Person.Born).toBe(2024) From 5ec4d1f55568c42fde4f7c25d5363dff6e39b2bc Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:27:53 +0200 Subject: [PATCH 24/32] Refactored Results --- packages/core/src/internal/observers.ts | 3 +- packages/core/src/mapping.rulesfactories.ts | 18 ++- packages/core/src/result.ts | 120 +++++++++-------- .../lib/core/internal/observers.ts | 3 +- .../lib/core/mapping.highlevel.ts | 6 +- .../lib/core/mapping.rulesfactories.ts | 18 ++- packages/neo4j-driver-deno/lib/core/result.ts | 124 ++++++++++-------- packages/neo4j-driver/src/index.js | 4 + 8 files changed, 184 insertions(+), 112 deletions(-) diff --git a/packages/core/src/internal/observers.ts b/packages/core/src/internal/observers.ts index 07e5b919b..88712691a 100644 --- a/packages/core/src/internal/observers.ts +++ b/packages/core/src/internal/observers.ts @@ -16,6 +16,7 @@ */ import Record from '../record' +import { AbstractResultObserver } from '../result' import ResultSummary from '../result-summary' interface StreamObserver { @@ -115,7 +116,7 @@ export interface ResultStreamObserver extends StreamObserver { * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the summary. * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. */ - subscribe: (observer: ResultObserver) => void + subscribe: (observer: AbstractResultObserver) => void } export class CompletedObserver implements ResultStreamObserver { diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index f63ff44a8..d495a4702 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -52,6 +52,22 @@ import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDat * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. */ export const RulesFactories = Object.freeze({ + /** + * Create a {@link Rule} that validates the value is a Boolean. + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asBoolean (rule?: Rule): Rule { + return { + validate: (value, field) => { + if (typeof value !== 'boolean') { + throw new TypeError(`${field} should be a boolean but received ${typeof value}`) + } + }, + ...rule + } + }, /** * Create a {@link Rule} that validates the value is a String. * @@ -78,7 +94,7 @@ export const RulesFactories = Object.freeze({ return { validate: (value: any, field: string) => { if (typeof value === 'object' && value.low !== undefined && value.high !== undefined && Object.keys(value).length === 2) { - throw new TypeError('Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt to true in driver config object') + throw new TypeError('Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt in driver config object') } if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { throw new TypeError(`${field} should be a number but received ${typeof value}`) diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index ec1cdd45e..dd6767155 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -52,22 +52,22 @@ const DEFAULT_ON_COMPLETED = (summary: ResultSummary): void => {} */ const DEFAULT_ON_KEYS = (keys: string[]): void => {} +interface AbstractQueryResult { + records: R[] + summary: ResultSummary +} + /** * The query result is the combination of the {@link ResultSummary} and * the array {@link Record[]} produced by the query */ -interface QueryResult { - records: Array> - summary: ResultSummary -} - -export interface MappedResult extends Result> {} +interface QueryResult extends AbstractQueryResult> {} /** * Interface to observe updates on the Result which is being produced. * */ -interface ResultObserver { +interface AbstractResultObserver { /** * Receive the keys present on the record whenever this information is available * @@ -79,7 +79,7 @@ interface ResultObserver { * Receive the each record present on the {@link @Result} * @param {Record} record The {@link Record} produced */ - onNext?: (record: Record) => void + onNext?: (record: R) => void /** * Called when the result is fully received @@ -94,29 +94,47 @@ interface ResultObserver { onError?: (error: Error) => void } +interface ResultObserver extends AbstractResultObserver> {} + /** * Defines a ResultObserver interface which can be used to enqueue records and dequeue * them until the result is fully received. * @access private */ -interface QueuedResultObserver extends ResultObserver { - dequeue: () => Promise> - dequeueUntilDone: () => Promise> - head: () => Promise> +interface QueuedResultObserver extends AbstractResultObserver { + dequeue: () => Promise> + dequeueUntilDone: () => Promise> + head: () => Promise> size: number } +function captureStacktrace (): string | null { + const error = new Error('') + if (error.stack != null) { + return error.stack.replace(/^Error(\n\r)*/, '') // we don't need the 'Error\n' part, if only it exists + } + return null +} + /** - * A stream of {@link Record} representing the result of a query. - * Can be consumed eagerly as {@link Promise} resolved with array of records and {@link ResultSummary} - * summary, or rejected with error that contains {@link string} code and {@link string} message. - * Alternatively can be consumed lazily using {@link Result#subscribe} function. - * @access public + * @private + * @param {Error} error The error + * @param {string| null} newStack The newStack + * @returns {void} */ -class Result implements Promise> { +function replaceStacktrace (error: Error, newStack?: string | null): void { + if (newStack != null) { + // Error.prototype.toString() concatenates error.name and error.message nicely + // then we add the rest of the stack trace + // eslint-disable-next-line @typescript-eslint/no-base-to-string + error.stack = error.toString() + '\n' + newStack + } +} + +class AbstractResult implements Promise> { private readonly _stack: string | null private readonly _streamObserverPromise: Promise - private _p: Promise | null + private _p: Promise> | null private readonly _query: Query private readonly _parameters: any private readonly _connectionHolder: connectionHolder.ConnectionHolder @@ -259,16 +277,16 @@ class Result implements Promise> { + private _getOrCreatePromise (): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { - const records: Array> = [] + const records: R[] = [] const observer = { - onNext: (record: Record) => { + onNext: (record: R) => { if (this._mapper != null) { - records.push(this._mapper(record) as unknown as Record) + records.push(this._mapper(record) as unknown as R) } else { - records.push(record as unknown as Record) + records.push(record as unknown as R) } }, onCompleted: (summary: ResultSummary) => { @@ -291,9 +309,9 @@ class Result implements Promise, ResultSummary>} The async iterator for the Results + * @returns {PeekableAsyncIterator} The async iterator for the Results */ - [Symbol.asyncIterator] (): PeekableAsyncIterator, ResultSummary> { + [Symbol.asyncIterator] (): PeekableAsyncIterator { if (!this.isOpen()) { const error = newError('Result is already consumed') return { @@ -396,9 +414,9 @@ class Result implements Promise, TResult2 = never>( + then, TResult2 = never>( onFulfilled?: - | ((value: QueryResult) => TResult1 | PromiseLike) + | ((value: AbstractQueryResult) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { @@ -415,7 +433,7 @@ class Result implements Promise( onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise | TResult> { + ): Promise | TResult> { return this._getOrCreatePromise().catch(onRejected) } @@ -427,7 +445,7 @@ class Result implements Promise void) | null): Promise> { + finally (onfinally?: (() => void) | null): Promise> { return this._getOrCreatePromise().finally(onfinally) } @@ -442,7 +460,7 @@ class Result implements Promise): void { + subscribe (observer: AbstractResultObserver): void { this._subscribe(observer) .catch(() => {}) } @@ -460,11 +478,11 @@ class Result implements Promise} The result stream observer. */ - _subscribe (observer: ResultObserver, paused: boolean = false): Promise { + _subscribe (observer: AbstractResultObserver, paused: boolean = false): Promise { const _observer = this._decorateObserver(observer) return this._streamObserverPromise @@ -490,7 +508,7 @@ class Result implements Promise): AbstractResultObserver { const onCompletedOriginal = observer.onCompleted ?? DEFAULT_ON_COMPLETED const onErrorOriginal = observer.onError ?? DEFAULT_ON_ERROR const onKeysOriginal = observer.onKeys ?? DEFAULT_ON_KEYS @@ -683,29 +701,27 @@ class Result implements Promise extends AbstractResult> { -function captureStacktrace (): string | null { - const error = new Error('') - if (error.stack != null) { - return error.stack.replace(/^Error(\n\r)*/, '') // we don't need the 'Error\n' part, if only it exists - } - return null } /** - * @private - * @param {Error} error The error - * @param {string| null} newStack The newStack - * @returns {void} + * A stream of mapped Objects representing the result of a query as mapped with a Record Object Mapping function. + * Can be consumed eagerly as {@link Promise} resolved with array of records and {@link ResultSummary} + * summary, or rejected with error that contains {@link string} code and {@link string} message. + * Alternatively can be consumed lazily using {@link MappedResult#subscribe} function. + * @access public */ -function replaceStacktrace (error: Error, newStack?: string | null): void { - if (newStack != null) { - // Error.prototype.toString() concatenates error.name and error.message nicely - // then we add the rest of the stack trace - // eslint-disable-next-line @typescript-eslint/no-base-to-string - error.stack = error.toString() + '\n' + newStack - } +class MappedResult extends AbstractResult { + } export default Result -export type { QueryResult, ResultObserver } +export type { QueryResult, ResultObserver, AbstractResultObserver } diff --git a/packages/neo4j-driver-deno/lib/core/internal/observers.ts b/packages/neo4j-driver-deno/lib/core/internal/observers.ts index cc3223f0a..97043f96f 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/observers.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/observers.ts @@ -16,6 +16,7 @@ */ import Record from '../record.ts' +import { AbstractResultObserver } from '../result.ts' import ResultSummary from '../result-summary.ts' interface StreamObserver { @@ -115,7 +116,7 @@ export interface ResultStreamObserver extends StreamObserver { * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the summary. * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. */ - subscribe: (observer: ResultObserver) => void + subscribe: (observer: AbstractResultObserver) => void } export class CompletedObserver implements ResultStreamObserver { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 2eaf1465e..57804a153 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -127,10 +127,10 @@ function defaultNameTranslation (databaseConvention: string, codeConvention: str } export const mapping = { - register, clearMappingRegistry, - translatePropertyNames, - defaultNameTranslation + defaultNameTranslation, + register, + translatePropertyNames } interface Gettable { get: (key: string) => V } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index b13437ae8..eee905471 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -52,6 +52,22 @@ import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDat * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. */ export const RulesFactories = Object.freeze({ + /** + * Create a {@link Rule} that validates the value is a Boolean. + * + * @param {Rule} rule Configurations for the rule + * @returns {Rule} A new rule for the value + */ + asBoolean (rule?: Rule): Rule { + return { + validate: (value, field) => { + if (typeof value !== 'boolean') { + throw new TypeError(`${field} should be a boolean but received ${typeof value}`) + } + }, + ...rule + } + }, /** * Create a {@link Rule} that validates the value is a String. * @@ -78,7 +94,7 @@ export const RulesFactories = Object.freeze({ return { validate: (value: any, field: string) => { if (typeof value === 'object' && value.low !== undefined && value.high !== undefined && Object.keys(value).length === 2) { - throw new TypeError('Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt to true in driver config object') + throw new TypeError('Number returned as Object. To use asNumber mapping, set disableLosslessIntegers or useBigInt in driver config object') } if (typeof value !== 'number' && (rule?.acceptBigInt !== true || typeof value !== 'bigint')) { throw new TypeError(`${field} should be a number but received ${typeof value}`) diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 3783128b7..4d41348bf 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -52,22 +52,22 @@ const DEFAULT_ON_COMPLETED = (summary: ResultSummary): void => {} */ const DEFAULT_ON_KEYS = (keys: string[]): void => {} +interface AbstractQueryResult { + records: Array + summary: ResultSummary +} + /** * The query result is the combination of the {@link ResultSummary} and * the array {@link Record[]} produced by the query */ -interface QueryResult { - records: Array> - summary: ResultSummary -} - -export interface MappedResult extends Result> {} +interface QueryResult extends AbstractQueryResult>{} /** * Interface to observe updates on the Result which is being produced. * */ -interface ResultObserver { +interface AbstractResultObserver { /** * Receive the keys present on the record whenever this information is available * @@ -79,7 +79,7 @@ interface ResultObserver { * Receive the each record present on the {@link @Result} * @param {Record} record The {@link Record} produced */ - onNext?: (record: Record) => void + onNext?: (record: R) => void /** * Called when the result is fully received @@ -94,29 +94,49 @@ interface ResultObserver { onError?: (error: Error) => void } +interface ResultObserver extends AbstractResultObserver> {} + /** * Defines a ResultObserver interface which can be used to enqueue records and dequeue * them until the result is fully received. * @access private */ -interface QueuedResultObserver extends ResultObserver { - dequeue: () => Promise> - dequeueUntilDone: () => Promise> - head: () => Promise> +interface QueuedResultObserver extends AbstractResultObserver { + dequeue: () => Promise> + dequeueUntilDone: () => Promise> + head: () => Promise> size: number } + + +function captureStacktrace (): string | null { + const error = new Error('') + if (error.stack != null) { + return error.stack.replace(/^Error(\n\r)*/, '') // we don't need the 'Error\n' part, if only it exists + } + return null +} + /** - * A stream of {@link Record} representing the result of a query. - * Can be consumed eagerly as {@link Promise} resolved with array of records and {@link ResultSummary} - * summary, or rejected with error that contains {@link string} code and {@link string} message. - * Alternatively can be consumed lazily using {@link Result#subscribe} function. - * @access public + * @private + * @param {Error} error The error + * @param {string| null} newStack The newStack + * @returns {void} */ -class Result implements Promise> { +function replaceStacktrace (error: Error, newStack?: string | null): void { + if (newStack != null) { + // Error.prototype.toString() concatenates error.name and error.message nicely + // then we add the rest of the stack trace + // eslint-disable-next-line @typescript-eslint/no-base-to-string + error.stack = error.toString() + '\n' + newStack + } +} + +class AbstractResult implements Promise>{ private readonly _stack: string | null private readonly _streamObserverPromise: Promise - private _p: Promise | null + private _p: Promise> | null private readonly _query: Query private readonly _parameters: any private readonly _connectionHolder: connectionHolder.ConnectionHolder @@ -259,16 +279,16 @@ class Result implements Promise> { + private _getOrCreatePromise (): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { - const records: Array> = [] + const records: Array = [] const observer = { - onNext: (record: Record) => { + onNext: (record: R) => { if (this._mapper != null) { - records.push(this._mapper(record) as unknown as Record) + records.push(this._mapper(record) as unknown as R) } else { - records.push(record as unknown as Record) + records.push(record as unknown as R) } }, onCompleted: (summary: ResultSummary) => { @@ -291,9 +311,9 @@ class Result implements Promise, ResultSummary>} The async iterator for the Results + * @returns {PeekableAsyncIterator} The async iterator for the Results */ - [Symbol.asyncIterator] (): PeekableAsyncIterator, ResultSummary> { + [Symbol.asyncIterator] (): PeekableAsyncIterator { if (!this.isOpen()) { const error = newError('Result is already consumed') return { @@ -396,9 +416,9 @@ class Result implements Promise, TResult2 = never>( + then, TResult2 = never>( onFulfilled?: - | ((value: QueryResult) => TResult1 | PromiseLike) + | ((value: AbstractQueryResult) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { @@ -415,7 +435,7 @@ class Result implements Promise( onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise | TResult> { + ): Promise | TResult> { return this._getOrCreatePromise().catch(onRejected) } @@ -427,7 +447,7 @@ class Result implements Promise void) | null): Promise> { + finally (onfinally?: (() => void) | null): Promise> { return this._getOrCreatePromise().finally(onfinally) } @@ -442,7 +462,7 @@ class Result implements Promise): void { + subscribe (observer: AbstractResultObserver): void { this._subscribe(observer) .catch(() => {}) } @@ -460,11 +480,11 @@ class Result implements Promise} The result stream observer. */ - _subscribe (observer: ResultObserver, paused: boolean = false): Promise { + _subscribe (observer: AbstractResultObserver, paused: boolean = false): Promise { const _observer = this._decorateObserver(observer) return this._streamObserverPromise @@ -490,7 +510,7 @@ class Result implements Promise): AbstractResultObserver { const onCompletedOriginal = observer.onCompleted ?? DEFAULT_ON_COMPLETED const onErrorOriginal = observer.onError ?? DEFAULT_ON_ERROR const onKeysOriginal = observer.onKeys ?? DEFAULT_ON_KEYS @@ -683,29 +703,27 @@ class Result implements Promise extends AbstractResult> { + } /** - * @private - * @param {Error} error The error - * @param {string| null} newStack The newStack - * @returns {void} + * A stream of mapped Objects representing the result of a query as mapped with a Record Object Mapping function. + * Can be consumed eagerly as {@link Promise} resolved with array of records and {@link ResultSummary} + * summary, or rejected with error that contains {@link string} code and {@link string} message. + * Alternatively can be consumed lazily using {@link MappedResult#subscribe} function. + * @access public */ -function replaceStacktrace (error: Error, newStack?: string | null): void { - if (newStack != null) { - // Error.prototype.toString() concatenates error.name and error.message nicely - // then we add the rest of the stack trace - // eslint-disable-next-line @typescript-eslint/no-base-to-string - error.stack = error.toString() + '\n' + newStack - } +class MappedResult extends AbstractResult { + } export default Result -export type { QueryResult, ResultObserver } +export type { QueryResult, ResultObserver, AbstractResultObserver } diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 660ecf58b..edfd0b3da 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -79,6 +79,7 @@ import { staticAuthTokenManager, clientCertificateProviders, resolveCertificateProvider, + Rule, Rules, RulesFactories, mapping @@ -286,6 +287,7 @@ const types = { LocalTime, Time, Integer, + Rule, Rules } @@ -481,6 +483,8 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, + Rule, + Rules, RulesFactories, mapping } From fba3a09fe43d5ea5721647e591c509a8cbd038da Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:28:20 +0200 Subject: [PATCH 25/32] deno sync --- packages/neo4j-driver-deno/lib/core/result.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 4d41348bf..8084efd1c 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -53,7 +53,7 @@ const DEFAULT_ON_COMPLETED = (summary: ResultSummary): void => {} const DEFAULT_ON_KEYS = (keys: string[]): void => {} interface AbstractQueryResult { - records: Array + records: R[] summary: ResultSummary } @@ -61,7 +61,7 @@ interface AbstractQueryResult { * The query result is the combination of the {@link ResultSummary} and * the array {@link Record[]} produced by the query */ -interface QueryResult extends AbstractQueryResult>{} +interface QueryResult extends AbstractQueryResult> {} /** * Interface to observe updates on the Result which is being produced. @@ -108,8 +108,6 @@ interface QueuedResultObserver extends AbstractResultObserver { size: number } - - function captureStacktrace (): string | null { const error = new Error('') if (error.stack != null) { @@ -133,7 +131,7 @@ function replaceStacktrace (error: Error, newStack?: string | null): void { } } -class AbstractResult implements Promise>{ +class AbstractResult implements Promise> { private readonly _stack: string | null private readonly _streamObserverPromise: Promise private _p: Promise> | null @@ -282,7 +280,7 @@ class AbstractResult implements Promise>{ private _getOrCreatePromise (): Promise> { if (this._p == null) { this._p = new Promise((resolve, reject) => { - const records: Array = [] + const records: R[] = [] const observer = { onNext: (record: R) => { if (this._mapper != null) { @@ -418,7 +416,7 @@ class AbstractResult implements Promise>{ */ then, TResult2 = never>( onFulfilled?: - | ((value: AbstractQueryResult) => TResult1 | PromiseLike) + | ((value: AbstractQueryResult) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { @@ -711,7 +709,7 @@ class AbstractResult implements Promise>{ * @access public */ class Result extends AbstractResult> { - + } /** @@ -722,7 +720,7 @@ class Result extends AbstractResult extends AbstractResult { - + } export default Result From d97edde2483a98e0ef2d57ac490a8cfc4bfe880a Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:49:10 +0200 Subject: [PATCH 26/32] renaming and exporting fix --- packages/core/src/index.ts | 3 +- packages/core/src/internal/observers.ts | 4 +- packages/core/src/result.ts | 56 ++++++++++--------- packages/neo4j-driver-deno/lib/core/index.ts | 3 +- .../lib/core/internal/observers.ts | 4 +- packages/neo4j-driver-deno/lib/core/result.ts | 56 ++++++++++--------- packages/neo4j-driver-deno/lib/mod.ts | 12 ++-- packages/neo4j-driver-lite/src/index.ts | 10 ++-- packages/neo4j-driver/types/index.d.ts | 12 +++- 9 files changed, 94 insertions(+), 66 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 83e5d52e2..9e3bec4a2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,7 +82,7 @@ import NotificationFilter, { notificationFilterMinimumSeverityLevel, NotificationFilterMinimumSeverityLevel } from './notification-filter' -import Result, { QueryResult, ResultObserver } from './result' +import Result, { MappedQueryResult, QueryResult, ResultObserver } from './result' import EagerResult from './result-eager' import ConnectionProvider, { Releasable } from './connection-provider' import Connection from './connection' @@ -277,6 +277,7 @@ export type { NumberOrInteger, NotificationPosition, QueryResult, + MappedQueryResult, ResultObserver, TransactionConfig, BookmarkManager, diff --git a/packages/core/src/internal/observers.ts b/packages/core/src/internal/observers.ts index 88712691a..e20a6185b 100644 --- a/packages/core/src/internal/observers.ts +++ b/packages/core/src/internal/observers.ts @@ -16,7 +16,7 @@ */ import Record from '../record' -import { AbstractResultObserver } from '../result' +import { GenericResultObserver } from '../result' import ResultSummary from '../result-summary' interface StreamObserver { @@ -116,7 +116,7 @@ export interface ResultStreamObserver extends StreamObserver { * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the summary. * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. */ - subscribe: (observer: AbstractResultObserver) => void + subscribe: (observer: GenericResultObserver) => void } export class CompletedObserver implements ResultStreamObserver { diff --git a/packages/core/src/result.ts b/packages/core/src/result.ts index dd6767155..42d1a1427 100644 --- a/packages/core/src/result.ts +++ b/packages/core/src/result.ts @@ -52,7 +52,7 @@ const DEFAULT_ON_COMPLETED = (summary: ResultSummary): void => {} */ const DEFAULT_ON_KEYS = (keys: string[]): void => {} -interface AbstractQueryResult { +interface GenericQueryResult { records: R[] summary: ResultSummary } @@ -61,13 +61,19 @@ interface AbstractQueryResult { * The query result is the combination of the {@link ResultSummary} and * the array {@link Record[]} produced by the query */ -interface QueryResult extends AbstractQueryResult> {} +interface QueryResult extends GenericQueryResult> {} + +/** + * The query result is the combination of the {@link ResultSummary} and + * an array of mapped objects produced by the query. + */ +interface MappedQueryResult extends GenericQueryResult {} /** * Interface to observe updates on the Result which is being produced. * */ -interface AbstractResultObserver { +interface GenericResultObserver { /** * Receive the keys present on the record whenever this information is available * @@ -94,14 +100,14 @@ interface AbstractResultObserver { onError?: (error: Error) => void } -interface ResultObserver extends AbstractResultObserver> {} +interface ResultObserver extends GenericResultObserver> {} /** * Defines a ResultObserver interface which can be used to enqueue records and dequeue * them until the result is fully received. * @access private */ -interface QueuedResultObserver extends AbstractResultObserver { +interface QueuedResultObserver extends GenericResultObserver { dequeue: () => Promise> dequeueUntilDone: () => Promise> head: () => Promise> @@ -131,10 +137,10 @@ function replaceStacktrace (error: Error, newStack?: string | null): void { } } -class AbstractResult implements Promise> { +class GenericResult> implements Promise { private readonly _stack: string | null private readonly _streamObserverPromise: Promise - private _p: Promise> | null + private _p: Promise | null private readonly _query: Query private readonly _parameters: any private readonly _connectionHolder: connectionHolder.ConnectionHolder @@ -277,7 +283,7 @@ class AbstractResult implements Promise> { * @private * @return {Promise} new Promise. */ - private _getOrCreatePromise (): Promise> { + private _getOrCreatePromise (): Promise { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: R[] = [] @@ -290,7 +296,7 @@ class AbstractResult implements Promise> { } }, onCompleted: (summary: ResultSummary) => { - resolve({ records, summary }) + resolve({ records, summary } as unknown as T) }, onError: (error: Error) => { reject(error) @@ -414,9 +420,9 @@ class AbstractResult implements Promise> { * @param {function(error: {message:string, code:string})} onRejected - function to be called upon errors. * @return {Promise} promise. */ - then, TResult2 = never>( + then( onFulfilled?: - | ((value: AbstractQueryResult) => TResult1 | PromiseLike) + | ((value: T) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { @@ -433,7 +439,7 @@ class AbstractResult implements Promise> { */ catch ( onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise | TResult> { + ): Promise { return this._getOrCreatePromise().catch(onRejected) } @@ -445,7 +451,7 @@ class AbstractResult implements Promise> { * @return {Promise} promise. */ [Symbol.toStringTag]: string = 'Result' - finally (onfinally?: (() => void) | null): Promise> { + finally (onfinally?: (() => void) | null): Promise { return this._getOrCreatePromise().finally(onfinally) } @@ -460,7 +466,7 @@ class AbstractResult implements Promise> { * @param {function(error: {message:string, code:string})} observer.onError - handle errors. * @return {void} */ - subscribe (observer: AbstractResultObserver): void { + subscribe (observer: GenericResultObserver): void { this._subscribe(observer) .catch(() => {}) } @@ -478,11 +484,11 @@ class AbstractResult implements Promise> { * of handling the results, and allows you to handle arbitrarily large results. * * @access private - * @param {AbstractResultObserver} observer The observer to send records to. + * @param {GenericResultObserver} observer The observer to send records to. * @param {boolean} paused The flag to indicate if the stream should be started paused * @returns {Promise} The result stream observer. */ - _subscribe (observer: AbstractResultObserver, paused: boolean = false): Promise { + _subscribe (observer: GenericResultObserver, paused: boolean = false): Promise { const _observer = this._decorateObserver(observer) return this._streamObserverPromise @@ -508,7 +514,7 @@ class AbstractResult implements Promise> { * @param {ResultObserver} observer The ResultObserver to decorate. * @returns The decorated result observer */ - _decorateObserver (observer: AbstractResultObserver): AbstractResultObserver { + _decorateObserver (observer: GenericResultObserver): GenericResultObserver { const onCompletedOriginal = observer.onCompleted ?? DEFAULT_ON_COMPLETED const onErrorOriginal = observer.onError ?? DEFAULT_ON_ERROR const onKeysOriginal = observer.onKeys ?? DEFAULT_ON_KEYS @@ -603,7 +609,7 @@ class AbstractResult implements Promise> { reject: (arg: Error) => any | undefined } - function createResolvablePromise (): ResolvablePromise> { + function createResolvablePromise (): ResolvablePromise> { const resolvablePromise: any = {} resolvablePromise.promise = new Promise((resolve, reject) => { resolvablePromise.resolve = resolve @@ -612,13 +618,13 @@ class AbstractResult implements Promise> { return resolvablePromise } - type QueuedResultElementOrError = IteratorResult | Error + type QueuedResultElementOrError = IteratorResult | Error function isError (elementOrError: QueuedResultElementOrError): elementOrError is Error { return elementOrError instanceof Error } - async function dequeue (): Promise> { + async function dequeue (): Promise> { if (buffer.length > 0) { const element = buffer.shift() ?? newError('Unexpected empty buffer', PROTOCOL_ERROR) onQueueSizeChanged() @@ -633,11 +639,11 @@ class AbstractResult implements Promise> { const buffer: QueuedResultElementOrError[] = [] const promiseHolder: { - resolvable: ResolvablePromise> | null + resolvable: ResolvablePromise> | null } = { resolvable: null } const observer = { - onNext: (record: Record) => { + onNext: (record: any) => { if (this._mapper != null) { observer._push({ done: false, value: this._mapper(record) }) } else { @@ -708,7 +714,7 @@ class AbstractResult implements Promise> { * Alternatively can be consumed lazily using {@link Result#subscribe} function. * @access public */ -class Result extends AbstractResult> { +class Result extends GenericResult, QueryResult> { } @@ -719,9 +725,9 @@ class Result extends AbstractResult extends AbstractResult { +class MappedResult extends GenericResult> { } export default Result -export type { QueryResult, ResultObserver, AbstractResultObserver } +export type { MappedQueryResult, QueryResult, ResultObserver, GenericResultObserver } diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 32b46c514..b05e1b7f6 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -82,7 +82,7 @@ import NotificationFilter, { notificationFilterMinimumSeverityLevel, NotificationFilterMinimumSeverityLevel } from './notification-filter.ts' -import Result, { QueryResult, ResultObserver } from './result.ts' +import Result, { MappedQueryResult, QueryResult, ResultObserver } from './result.ts' import EagerResult from './result-eager.ts' import ConnectionProvider, { Releasable } from './connection-provider.ts' import Connection from './connection.ts' @@ -277,6 +277,7 @@ export type { NumberOrInteger, NotificationPosition, QueryResult, + MappedQueryResult, ResultObserver, TransactionConfig, BookmarkManager, diff --git a/packages/neo4j-driver-deno/lib/core/internal/observers.ts b/packages/neo4j-driver-deno/lib/core/internal/observers.ts index 97043f96f..91a53e206 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/observers.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/observers.ts @@ -16,7 +16,7 @@ */ import Record from '../record.ts' -import { AbstractResultObserver } from '../result.ts' +import { GenericResultObserver } from '../result.ts' import ResultSummary from '../result-summary.ts' interface StreamObserver { @@ -116,7 +116,7 @@ export interface ResultStreamObserver extends StreamObserver { * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the summary. * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. */ - subscribe: (observer: AbstractResultObserver) => void + subscribe: (observer: GenericResultObserver) => void } export class CompletedObserver implements ResultStreamObserver { diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 8084efd1c..7607ff1e4 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -52,7 +52,7 @@ const DEFAULT_ON_COMPLETED = (summary: ResultSummary): void => {} */ const DEFAULT_ON_KEYS = (keys: string[]): void => {} -interface AbstractQueryResult { +interface GenericQueryResult { records: R[] summary: ResultSummary } @@ -61,13 +61,19 @@ interface AbstractQueryResult { * The query result is the combination of the {@link ResultSummary} and * the array {@link Record[]} produced by the query */ -interface QueryResult extends AbstractQueryResult> {} +interface QueryResult extends GenericQueryResult> {} + +/** + * The query result is the combination of the {@link ResultSummary} and + * an array of mapped objects produced by the query. + */ +interface MappedQueryResult extends GenericQueryResult {} /** * Interface to observe updates on the Result which is being produced. * */ -interface AbstractResultObserver { +interface GenericResultObserver { /** * Receive the keys present on the record whenever this information is available * @@ -94,14 +100,14 @@ interface AbstractResultObserver { onError?: (error: Error) => void } -interface ResultObserver extends AbstractResultObserver> {} +interface ResultObserver extends GenericResultObserver> {} /** * Defines a ResultObserver interface which can be used to enqueue records and dequeue * them until the result is fully received. * @access private */ -interface QueuedResultObserver extends AbstractResultObserver { +interface QueuedResultObserver extends GenericResultObserver { dequeue: () => Promise> dequeueUntilDone: () => Promise> head: () => Promise> @@ -131,10 +137,10 @@ function replaceStacktrace (error: Error, newStack?: string | null): void { } } -class AbstractResult implements Promise> { +class GenericResult> implements Promise { private readonly _stack: string | null private readonly _streamObserverPromise: Promise - private _p: Promise> | null + private _p: Promise | null private readonly _query: Query private readonly _parameters: any private readonly _connectionHolder: connectionHolder.ConnectionHolder @@ -277,7 +283,7 @@ class AbstractResult implements Promise> { * @private * @return {Promise} new Promise. */ - private _getOrCreatePromise (): Promise> { + private _getOrCreatePromise (): Promise { if (this._p == null) { this._p = new Promise((resolve, reject) => { const records: R[] = [] @@ -290,7 +296,7 @@ class AbstractResult implements Promise> { } }, onCompleted: (summary: ResultSummary) => { - resolve({ records, summary }) + resolve({ records, summary } as T) }, onError: (error: Error) => { reject(error) @@ -414,9 +420,9 @@ class AbstractResult implements Promise> { * @param {function(error: {message:string, code:string})} onRejected - function to be called upon errors. * @return {Promise} promise. */ - then, TResult2 = never>( + then( onFulfilled?: - | ((value: AbstractQueryResult) => TResult1 | PromiseLike) + | ((value:T) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { @@ -433,7 +439,7 @@ class AbstractResult implements Promise> { */ catch ( onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise | TResult> { + ): Promise { return this._getOrCreatePromise().catch(onRejected) } @@ -445,7 +451,7 @@ class AbstractResult implements Promise> { * @return {Promise} promise. */ [Symbol.toStringTag]: string = 'Result' - finally (onfinally?: (() => void) | null): Promise> { + finally (onfinally?: (() => void) | null): Promise { return this._getOrCreatePromise().finally(onfinally) } @@ -460,7 +466,7 @@ class AbstractResult implements Promise> { * @param {function(error: {message:string, code:string})} observer.onError - handle errors. * @return {void} */ - subscribe (observer: AbstractResultObserver): void { + subscribe (observer: GenericResultObserver): void { this._subscribe(observer) .catch(() => {}) } @@ -478,11 +484,11 @@ class AbstractResult implements Promise> { * of handling the results, and allows you to handle arbitrarily large results. * * @access private - * @param {AbstractResultObserver} observer The observer to send records to. + * @param {GenericResultObserver} observer The observer to send records to. * @param {boolean} paused The flag to indicate if the stream should be started paused * @returns {Promise} The result stream observer. */ - _subscribe (observer: AbstractResultObserver, paused: boolean = false): Promise { + _subscribe (observer: GenericResultObserver, paused: boolean = false): Promise { const _observer = this._decorateObserver(observer) return this._streamObserverPromise @@ -508,7 +514,7 @@ class AbstractResult implements Promise> { * @param {ResultObserver} observer The ResultObserver to decorate. * @returns The decorated result observer */ - _decorateObserver (observer: AbstractResultObserver): AbstractResultObserver { + _decorateObserver (observer: GenericResultObserver): GenericResultObserver { const onCompletedOriginal = observer.onCompleted ?? DEFAULT_ON_COMPLETED const onErrorOriginal = observer.onError ?? DEFAULT_ON_ERROR const onKeysOriginal = observer.onKeys ?? DEFAULT_ON_KEYS @@ -603,7 +609,7 @@ class AbstractResult implements Promise> { reject: (arg: Error) => any | undefined } - function createResolvablePromise (): ResolvablePromise> { + function createResolvablePromise (): ResolvablePromise> { const resolvablePromise: any = {} resolvablePromise.promise = new Promise((resolve, reject) => { resolvablePromise.resolve = resolve @@ -612,13 +618,13 @@ class AbstractResult implements Promise> { return resolvablePromise } - type QueuedResultElementOrError = IteratorResult | Error + type QueuedResultElementOrError = IteratorResult | Error function isError (elementOrError: QueuedResultElementOrError): elementOrError is Error { return elementOrError instanceof Error } - async function dequeue (): Promise> { + async function dequeue (): Promise> { if (buffer.length > 0) { const element = buffer.shift() ?? newError('Unexpected empty buffer', PROTOCOL_ERROR) onQueueSizeChanged() @@ -633,11 +639,11 @@ class AbstractResult implements Promise> { const buffer: QueuedResultElementOrError[] = [] const promiseHolder: { - resolvable: ResolvablePromise> | null + resolvable: ResolvablePromise> | null } = { resolvable: null } const observer = { - onNext: (record: Record) => { + onNext: (record: any) => { if (this._mapper != null) { observer._push({ done: false, value: this._mapper(record) }) } else { @@ -708,7 +714,7 @@ class AbstractResult implements Promise> { * Alternatively can be consumed lazily using {@link Result#subscribe} function. * @access public */ -class Result extends AbstractResult> { +class Result extends GenericResult, QueryResult> { } @@ -719,9 +725,9 @@ class Result extends AbstractResult extends AbstractResult { +class MappedResult extends GenericResult> { } export default Result -export type { QueryResult, ResultObserver, AbstractResultObserver } +export type { MappedQueryResult, QueryResult, ResultObserver, GenericResultObserver } diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index bdc601b23..51243acb4 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -112,7 +112,8 @@ import { Rule, Rules, RulesFactories, - mapping + mapping, + MappedQueryResult, } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -129,7 +130,7 @@ type ConfiguredCustomResolver = internal.resolver.ConfiguredCustomResolver const { READ, WRITE } = coreDriver const { - util: { ENCRYPTION_ON, assertString, isEmptyObjectOrNull }, + util: { ENCRYPTION_ON, assertString, isEmptyObjectOrNull}, serverAddress: { ServerAddress }, urlUtil } = internal @@ -211,7 +212,7 @@ function driver ( routing = true break default: - throw new Error(`Unknown scheme: ${parsedUrl.scheme ?? 'null'}`) + throw new Error(`Unknown scheme: ${(parsedUrl.scheme as string) ?? 'null'}`) } // Encryption enabled on URL, propagate trust to the config. @@ -263,7 +264,7 @@ function driver ( routingContext: parsedUrl.query }) } else { - if (!isEmptyObjectOrNull(parsedUrl.query)) { + if (!(isEmptyObjectOrNull(parsedUrl.query) === true)) { throw new Error( `Parameters are not supported with none routed scheme. Given URL: '${url}'` ) @@ -552,6 +553,7 @@ export type { ClientCertificateProviders, RotatingClientCertificateProvider, Rule, - Rules + Rules, + MappedQueryResult } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index c37e8f5fc..c2b5a994d 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -112,7 +112,8 @@ import { Rule, Rules, RulesFactories, - mapping + mapping, + MappedQueryResult } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -210,7 +211,7 @@ function driver ( routing = true break default: - throw new Error(`Unknown scheme: ${parsedUrl.scheme ?? 'null'}`) + throw new Error(`Unknown scheme: ${(parsedUrl.scheme as string) ?? 'null'}`) } // Encryption enabled on URL, propagate trust to the config. @@ -262,7 +263,7 @@ function driver ( routingContext: parsedUrl.query }) } else { - if (!isEmptyObjectOrNull(parsedUrl.query)) { + if (!(isEmptyObjectOrNull(parsedUrl.query) === true)) { throw new Error( `Parameters are not supported with none routed scheme. Given URL: '${url}'` ) @@ -551,6 +552,7 @@ export type { ClientCertificateProviders, RotatingClientCertificateProvider, Rule, - Rules + Rules, + MappedQueryResult } export default forExport diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 659ea07bf..52774403b 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -97,6 +97,11 @@ import { ClientCertificateProviders, RotatingClientCertificateProvider, clientCertificateProviders, + Rule, + Rules, + RulesFactories, + mapping, + MappedQueryResult, types as coreTypes } from 'neo4j-driver-core' import { @@ -400,7 +405,12 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + Rule, + Rules, + RulesFactories, + mapping, + MappedQueryResult } export default forExport From 2d1c1622be1a29c431db49f5aed3c03c8e5b3370 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:49:30 +0200 Subject: [PATCH 27/32] deno --- packages/neo4j-driver-deno/lib/core/result.ts | 4 ++-- packages/neo4j-driver-deno/lib/mod.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts index 7607ff1e4..635b6df2c 100644 --- a/packages/neo4j-driver-deno/lib/core/result.ts +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -296,7 +296,7 @@ class GenericResult> implements Promise { } }, onCompleted: (summary: ResultSummary) => { - resolve({ records, summary } as T) + resolve({ records, summary } as unknown as T) }, onError: (error: Error) => { reject(error) @@ -422,7 +422,7 @@ class GenericResult> implements Promise { */ then( onFulfilled?: - | ((value:T) => TResult1 | PromiseLike) + | ((value: T) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 51243acb4..959ebd30a 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -113,7 +113,7 @@ import { Rules, RulesFactories, mapping, - MappedQueryResult, + MappedQueryResult } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -130,7 +130,7 @@ type ConfiguredCustomResolver = internal.resolver.ConfiguredCustomResolver const { READ, WRITE } = coreDriver const { - util: { ENCRYPTION_ON, assertString, isEmptyObjectOrNull}, + util: { ENCRYPTION_ON, assertString, isEmptyObjectOrNull }, serverAddress: { ServerAddress }, urlUtil } = internal From 0c37551b42283908acd4264fa98011e7adf2997e Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:19:17 +0200 Subject: [PATCH 28/32] small naming conventions refactor --- packages/core/src/index.ts | 7 +++-- packages/core/src/mapping.highlevel.ts | 26 +++++++++++-------- packages/core/src/mapping.nameconventions.ts | 10 ++++++- packages/core/src/mapping.rulesfactories.ts | 17 ++++++++++++ .../core/test/mapping.nameconventions.test.ts | 16 ++++++------ packages/neo4j-driver-deno/lib/core/index.ts | 7 +++-- .../lib/core/mapping.highlevel.ts | 26 +++++++++++-------- .../lib/core/mapping.nameconventions.ts | 10 ++++++- .../lib/core/mapping.rulesfactories.ts | 17 ++++++++++++ packages/neo4j-driver-deno/lib/mod.ts | 7 +++-- packages/neo4j-driver-lite/src/index.ts | 7 +++-- packages/neo4j-driver/src/index.js | 9 ++++--- packages/neo4j-driver/test/examples.test.js | 2 +- packages/neo4j-driver/types/index.d.ts | 2 ++ 14 files changed, 119 insertions(+), 44 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9e3bec4a2..5d18600ac 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -101,6 +101,7 @@ import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate' import * as internal from './internal' // todo: removed afterwards +import { StandardCase } from './mapping.nameconventions' import { Rule, Rules, mapping } from './mapping.highlevel' import { RulesFactories } from './mapping.rulesfactories' @@ -190,7 +191,8 @@ const forExport = { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping + mapping, + StandardCase } export { @@ -269,7 +271,8 @@ export { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping + mapping, + StandardCase } export type { diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 3b96d29b4..495bdd720 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -18,7 +18,7 @@ */ import { newError } from './error' -import { nameConventions } from './mapping.nameconventions' +import { nameConventions, StandardCase } from './mapping.nameconventions' /** * constructor function of any class @@ -54,7 +54,7 @@ let nameMapping: (name: string) => string = (name) => name * resultTransformer: neo4j.resultTransformers.hydrated(Person) * }) * - * + * @experimental * @param {GenericConstructor} constructor The constructor function of the class to set rules for * @param {Rules} rules The rules to set for the provided class */ @@ -64,6 +64,7 @@ export function register (constructor: GenericConstructo /** * Clears all registered type mappings from the mapping registry. + * @experimental */ function clearMappingRegistry (): void { rulesRegistry = {} @@ -73,14 +74,14 @@ function clearMappingRegistry (): void { * Sets a default name translation from record keys to object properties. * If providing a function, provide a function that maps FROM your object properties names TO record key names. * - * The function defaultNameTranslation can be used to provide a prewritten translation function between some common naming conventions. + * The function getCaseTranslator can be used to provide a prewritten translation function between some common naming conventions. * * @example * //if the keys on records from the database are in ALLCAPS - * mapping.translatePropertyNames((name) => name.toUpperCase()) + * mapping.translateIdentifiers((name) => name.toUpperCase()) * * //if you utilize PacalCase in the database and camelCase in JavaScript code. - * mapping.translatePropertyNames(mapping.defaultNameTranslation("PascalCase", "camelCase")) + * mapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase")) * * //if a type has one odd mapping you can override the translation with the rule * const personRules = { @@ -93,22 +94,24 @@ function clearMappingRegistry (): void { * //or by registering them to the mapping registry * mapping.register(Person, personRules) * + * @experimental * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ -function translatePropertyNames (translationFunction: (name: string) => string): void { +function translateIdentifiers (translationFunction: (name: string) => string): void { nameMapping = translationFunction } /** - * Creates a translation frunction from record key names to object property names, for use with the {@link translatePropertyNames} function + * Creates a translation frunction from record key names to object property names, for use with the {@link translateIdentifiers} function * - * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SNAKE_CAPS" + * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE" * + * @experimental * @param {string} databaseConvention The naming convention in use in database result Records * @param {string} codeConvention The naming convention in use in JavaScript object properties * @returns {function} translation function */ -function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { +function getCaseTranslator (databaseConvention: string | StandardCase, codeConvention: string | StandardCase): ((name: string) => string) { const keys = Object.keys(nameConventions) if (!keys.includes(databaseConvention)) { throw newError( @@ -128,9 +131,10 @@ function defaultNameTranslation (databaseConvention: string, codeConvention: str export const mapping = { clearMappingRegistry, - defaultNameTranslation, + getCaseTranslator, register, - translatePropertyNames + StandardCase, + translateIdentifiers } interface Gettable { get: (key: string) => V } diff --git a/packages/core/src/mapping.nameconventions.ts b/packages/core/src/mapping.nameconventions.ts index d65ceb0fa..75b2b00cd 100644 --- a/packages/core/src/mapping.nameconventions.ts +++ b/packages/core/src/mapping.nameconventions.ts @@ -22,6 +22,14 @@ export interface NameConvention { encode: (tokens: string[]) => string } +export enum StandardCase { + SnakeCase = 'snake_case', + KebabCase = 'kebab-case', + ScreamingSnakeCase = 'SCREAMING_SNAKE_CASE', + PascalCase = 'PascalCase', + CamelCase = 'camelCase' +} + export const nameConventions = { snake_case: { tokenize: (name: string) => name.split('_'), @@ -55,7 +63,7 @@ export const nameConventions = { return name } }, - SNAKE_CAPS: { + SCREAMING_SNAKE_CASE: { tokenize: (name: string) => name.split('_').map((token) => token.toLowerCase()), encode: (tokens: string[]) => tokens.join('_').toUpperCase() } diff --git a/packages/core/src/mapping.rulesfactories.ts b/packages/core/src/mapping.rulesfactories.ts index d495a4702..f0f0b6f9f 100644 --- a/packages/core/src/mapping.rulesfactories.ts +++ b/packages/core/src/mapping.rulesfactories.ts @@ -50,11 +50,14 @@ import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDat * @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}. * * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. + * + * @experimental */ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a Boolean. * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -71,6 +74,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a String. * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -87,6 +91,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Number}. * + * @experimental * @param {Rule & { acceptBigInt?: boolean }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -112,6 +117,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link BigInt}. * + * @experimental * @param {Rule & { acceptNumber?: boolean }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -144,6 +150,7 @@ export const RulesFactories = Object.freeze({ * movie: neo4j.RulesFactories.asNode({}), * } * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -176,6 +183,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is an {@link UnboundRelationship} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -192,6 +200,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Path} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -208,6 +217,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Point} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -224,6 +234,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Duration} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -241,6 +252,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link LocalTime} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -258,6 +270,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Time} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -275,6 +288,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Date} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -292,6 +306,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link LocalDateTime} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -309,6 +324,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link DateTime} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -326,6 +342,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a List. Optionally taking a rule for hydrating the contained values. * + * @experimental * @param {Rule & { apply?: Rule }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ diff --git a/packages/core/test/mapping.nameconventions.test.ts b/packages/core/test/mapping.nameconventions.test.ts index 9cf9b11e3..8ab8bac79 100644 --- a/packages/core/test/mapping.nameconventions.test.ts +++ b/packages/core/test/mapping.nameconventions.test.ts @@ -17,21 +17,21 @@ import { mapping } from '../src' -describe('#unit defaultNameTranslations', () => { +describe('#unit getCaseTranslator', () => { // Each convention has "tokenize" and "encode" functions, so testing each twice is sufficient. - it('camelCase to SNAKE_CAPS', () => { - expect(mapping.defaultNameTranslation('camelCase', 'SNAKE_CAPS')('I_AM_COOL')).toBe('iAmCool') + it('camelCase to SCREAMING_SNAKE_CASE', () => { + expect(mapping.getCaseTranslator(mapping.StandardCase.CamelCase, 'SCREAMING_SNAKE_CASE')('I_AM_COOL')).toBe('iAmCool') }) - it('SNAKE_CAPS to PascalCase', () => { - expect(mapping.defaultNameTranslation('SNAKE_CAPS', 'PascalCase')('IAmCool')).toBe('I_AM_COOL') + it('SCREAMING_SNAKE_CASE to PascalCase', () => { + expect(mapping.getCaseTranslator(mapping.StandardCase.ScreamingSnakeCase, 'PascalCase')('IAmCool')).toBe('I_AM_COOL') }) it('PascalCase to snake_case', () => { - expect(mapping.defaultNameTranslation('PascalCase', 'snake_case')('i_am_cool')).toBe('IAmCool') + expect(mapping.getCaseTranslator(mapping.StandardCase.PascalCase, 'snake_case')('i_am_cool')).toBe('IAmCool') }) it('snake_case to kebab-case', () => { - expect(mapping.defaultNameTranslation('snake_case', 'kebab-case')('i-am-cool')).toBe('i_am_cool') + expect(mapping.getCaseTranslator(mapping.StandardCase.SnakeCase, 'kebab-case')('i-am-cool')).toBe('i_am_cool') }) it('kebab-case to camelCase', () => { - expect(mapping.defaultNameTranslation('kebab-case', 'camelCase')('iAmCool')).toBe('i-am-cool') + expect(mapping.getCaseTranslator(mapping.StandardCase.KebabCase, 'camelCase')('iAmCool')).toBe('i-am-cool') }) }) diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index b05e1b7f6..e7e5b0c31 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -101,6 +101,7 @@ import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' +import { StandardCase } from './mapping.nameconventions.ts' import { Rule, Rules, mapping } from './mapping.highlevel.ts' import { RulesFactories } from './mapping.rulesfactories.ts' @@ -190,7 +191,8 @@ const forExport = { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping + mapping, + StandardCase } export { @@ -269,7 +271,8 @@ export { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping + mapping, + StandardCase } export type { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index 57804a153..df75fa587 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -18,7 +18,7 @@ */ import { newError } from './error.ts' -import { nameConventions } from './mapping.nameconventions.ts' +import { nameConventions, StandardCase } from './mapping.nameconventions.ts' /** * constructor function of any class @@ -54,7 +54,7 @@ let nameMapping: (name: string) => string = (name) => name * resultTransformer: neo4j.resultTransformers.hydrated(Person) * }) * - * + * @experimental * @param {GenericConstructor} constructor The constructor function of the class to set rules for * @param {Rules} rules The rules to set for the provided class */ @@ -64,6 +64,7 @@ export function register (constructor: GenericConstructo /** * Clears all registered type mappings from the mapping registry. + * @experimental */ function clearMappingRegistry (): void { rulesRegistry = {} @@ -73,14 +74,14 @@ function clearMappingRegistry (): void { * Sets a default name translation from record keys to object properties. * If providing a function, provide a function that maps FROM your object properties names TO record key names. * - * The function defaultNameTranslation can be used to provide a prewritten translation function between some common naming conventions. + * The function getCaseTranslator can be used to provide a prewritten translation function between some common naming conventions. * * @example * //if the keys on records from the database are in ALLCAPS - * mapping.translatePropertyNames((name) => name.toUpperCase()) + * mapping.translateIdentifiers((name) => name.toUpperCase()) * * //if you utilize PacalCase in the database and camelCase in JavaScript code. - * mapping.translatePropertyNames(mapping.defaultNameTranslation("PascalCase", "camelCase")) + * mapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase")) * * //if a type has one odd mapping you can override the translation with the rule * const personRules = { @@ -93,22 +94,24 @@ function clearMappingRegistry (): void { * //or by registering them to the mapping registry * mapping.register(Person, personRules) * + * @experimental * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ -function translatePropertyNames (translationFunction: (name: string) => string): void { +function translateIdentifiers (translationFunction: (name: string) => string): void { nameMapping = translationFunction } /** - * Creates a translation frunction from record key names to object property names, for use with the {@link translatePropertyNames} function + * Creates a translation frunction from record key names to object property names, for use with the {@link translateIdentifiers} function * - * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SNAKE_CAPS" + * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE" * + * @experimental * @param {string} databaseConvention The naming convention in use in database result Records * @param {string} codeConvention The naming convention in use in JavaScript object properties * @returns {function} translation function */ -function defaultNameTranslation (databaseConvention: string, codeConvention: string): ((name: string) => string) { +function getCaseTranslator (databaseConvention: string | StandardCase, codeConvention: string | StandardCase): ((name: string) => string) { const keys = Object.keys(nameConventions) if (!keys.includes(databaseConvention)) { throw newError( @@ -128,9 +131,10 @@ function defaultNameTranslation (databaseConvention: string, codeConvention: str export const mapping = { clearMappingRegistry, - defaultNameTranslation, + getCaseTranslator, register, - translatePropertyNames + StandardCase, + translateIdentifiers, } interface Gettable { get: (key: string) => V } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts index d65ceb0fa..b0a6f4906 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts @@ -22,6 +22,14 @@ export interface NameConvention { encode: (tokens: string[]) => string } +export enum StandardCase { + SnakeCase = "snake_case", + KebabCase = "kebab-case", + ScreamingSnakeCase = "SCREAMING_SNAKE_CASE", + PascalCase = "PascalCase", + CamelCase = "camelCase" +} + export const nameConventions = { snake_case: { tokenize: (name: string) => name.split('_'), @@ -55,7 +63,7 @@ export const nameConventions = { return name } }, - SNAKE_CAPS: { + SCREAMING_SNAKE_CASE: { tokenize: (name: string) => name.split('_').map((token) => token.toLowerCase()), encode: (tokens: string[]) => tokens.join('_').toUpperCase() } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index eee905471..d6972ac4f 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -50,11 +50,14 @@ import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDat * @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}. * * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. + * + * @experimental */ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a Boolean. * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -71,6 +74,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a String. * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -87,6 +91,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Number}. * + * @experimental * @param {Rule & { acceptBigInt?: boolean }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -112,6 +117,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link BigInt}. * + * @experimental * @param {Rule & { acceptNumber?: boolean }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -144,6 +150,7 @@ export const RulesFactories = Object.freeze({ * movie: neo4j.RulesFactories.asNode({}), * } * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -176,6 +183,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is an {@link UnboundRelationship} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -192,6 +200,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Path} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -208,6 +217,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Point} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -224,6 +234,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Duration} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -241,6 +252,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link LocalTime} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -258,6 +270,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Time} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -275,6 +288,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link Date} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -292,6 +306,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link LocalDateTime} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -309,6 +324,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a {@link DateTime} * + * @experimental * @param {Rule} rule Configurations for the rule * @returns {Rule} A new rule for the value */ @@ -326,6 +342,7 @@ export const RulesFactories = Object.freeze({ /** * Create a {@link Rule} that validates the value is a List. Optionally taking a rule for hydrating the contained values. * + * @experimental * @param {Rule & { apply?: Rule }} rule Configurations for the rule * @returns {Rule} A new rule for the value */ diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 959ebd30a..b5748eec4 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -113,6 +113,7 @@ import { Rules, RulesFactories, mapping, + StandardCase, MappedQueryResult } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts @@ -447,7 +448,8 @@ const forExport = { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping + mapping, + StandardCase } export { @@ -520,7 +522,8 @@ export { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping + mapping, + StandardCase } export type { QueryResult, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index c2b5a994d..12722162f 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -113,6 +113,7 @@ import { Rules, RulesFactories, mapping, + StandardCase, MappedQueryResult } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -446,7 +447,8 @@ const forExport = { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping + mapping, + StandardCase } export { @@ -519,7 +521,8 @@ export { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping + mapping, + StandardCase } export type { QueryResult, diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index edfd0b3da..15a8c82d8 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -82,7 +82,8 @@ import { Rule, Rules, RulesFactories, - mapping + mapping, + StandardCase } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -410,7 +411,8 @@ const forExport = { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping + mapping, + StandardCase } export { @@ -486,6 +488,7 @@ export { Rule, Rules, RulesFactories, - mapping + mapping, + StandardCase } export default forExport diff --git a/packages/neo4j-driver/test/examples.test.js b/packages/neo4j-driver/test/examples.test.js index a0005645a..0f0e06b15 100644 --- a/packages/neo4j-driver/test/examples.test.js +++ b/packages/neo4j-driver/test/examples.test.js @@ -1673,7 +1673,7 @@ describe('#integration examples', () => { neo4j.mapping.register(ActingJobs, actingJobsRules) // The code uses PascalCase for property names, while the cypher has camelCase. This issue can be solved with the following line. - neo4j.mapping.translatePropertyNames(neo4j.mapping.defaultNameTranslation('camelCase', 'PascalCase')) + neo4j.mapping.translateIdentifiers(neo4j.mapping.getCaseTranslator('camelCase', 'PascalCase')) const session = driver.session() diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 52774403b..8624c1e1b 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -101,6 +101,7 @@ import { Rules, RulesFactories, mapping, + StandardCase, MappedQueryResult, types as coreTypes } from 'neo4j-driver-core' @@ -410,6 +411,7 @@ export type { Rules, RulesFactories, mapping, + StandardCase, MappedQueryResult } From 022b03ac406503120059133dff763006a7b15a44 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:19:54 +0200 Subject: [PATCH 29/32] deno sync --- .../neo4j-driver-deno/lib/core/mapping.highlevel.ts | 2 +- .../lib/core/mapping.nameconventions.ts | 10 +++++----- .../lib/core/mapping.rulesfactories.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index df75fa587..cd451341c 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -134,7 +134,7 @@ export const mapping = { getCaseTranslator, register, StandardCase, - translateIdentifiers, + translateIdentifiers } interface Gettable { get: (key: string) => V } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts index b0a6f4906..75b2b00cd 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.nameconventions.ts @@ -23,11 +23,11 @@ export interface NameConvention { } export enum StandardCase { - SnakeCase = "snake_case", - KebabCase = "kebab-case", - ScreamingSnakeCase = "SCREAMING_SNAKE_CASE", - PascalCase = "PascalCase", - CamelCase = "camelCase" + SnakeCase = 'snake_case', + KebabCase = 'kebab-case', + ScreamingSnakeCase = 'SCREAMING_SNAKE_CASE', + PascalCase = 'PascalCase', + CamelCase = 'camelCase' } export const nameConventions = { diff --git a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts index d6972ac4f..feafc087d 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.rulesfactories.ts @@ -50,7 +50,7 @@ import { Date, DateTime, Duration, LocalDateTime, LocalTime, Time, isDate, isDat * @property {function(rule: ?Rule)} asPoint Create a {@link Rule} that validates the value is a {@link Point}. * * @property {function(rule: ?Rule & { apply?: Rule })} asList Create a {@link Rule} that validates the value is a List. - * + * * @experimental */ export const RulesFactories = Object.freeze({ From 9cdcc627cf2b92653850131478488c2d106d7ab0 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 16 Apr 2025 10:25:56 +0200 Subject: [PATCH 30/32] rename the mapping object for clarity --- packages/core/src/index.ts | 6 +++--- packages/core/src/mapping.highlevel.ts | 12 ++++++------ packages/core/src/result-transformers.ts | 2 +- packages/core/test/mapping.highlevel.test.ts | 6 +++--- packages/core/test/mapping.nameconventions.test.ts | 12 ++++++------ packages/neo4j-driver-deno/lib/core/index.ts | 6 +++--- .../neo4j-driver-deno/lib/core/mapping.highlevel.ts | 12 ++++++------ .../lib/core/result-transformers.ts | 2 +- packages/neo4j-driver-deno/lib/mod.ts | 6 +++--- packages/neo4j-driver-lite/src/index.ts | 6 +++--- packages/neo4j-driver/src/index.js | 6 +++--- packages/neo4j-driver/test/examples.test.js | 12 ++++++------ .../neo4j-driver/test/record-object-mapping.test.js | 10 +++++----- packages/neo4j-driver/types/index.d.ts | 4 ++-- 14 files changed, 51 insertions(+), 51 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5d18600ac..98e3838d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -102,7 +102,7 @@ import resultTransformers, { ResultTransformer } from './result-transformers' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate' import * as internal from './internal' // todo: removed afterwards import { StandardCase } from './mapping.nameconventions' -import { Rule, Rules, mapping } from './mapping.highlevel' +import { Rule, Rules, RecordObjectMapping } from './mapping.highlevel' import { RulesFactories } from './mapping.rulesfactories' /** @@ -191,7 +191,7 @@ const forExport = { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } @@ -271,7 +271,7 @@ export { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index 495bdd720..de0c85bdc 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -48,7 +48,7 @@ let nameMapping: (name: string) => string = (name) => name * }) * * can instead be written: - * neo4j.mapping.register(Person, personClassRules) + * neo4j.RecordObjectMapping.register(Person, personClassRules) * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.hydrated(Person) @@ -63,7 +63,7 @@ export function register (constructor: GenericConstructo } /** - * Clears all registered type mappings from the mapping registry. + * Clears all registered type mappings from the record object mapping registry. * @experimental */ function clearMappingRegistry (): void { @@ -78,10 +78,10 @@ function clearMappingRegistry (): void { * * @example * //if the keys on records from the database are in ALLCAPS - * mapping.translateIdentifiers((name) => name.toUpperCase()) + * RecordObjectMapping.translateIdentifiers((name) => name.toUpperCase()) * * //if you utilize PacalCase in the database and camelCase in JavaScript code. - * mapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase")) + * RecordObjectMapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase")) * * //if a type has one odd mapping you can override the translation with the rule * const personRules = { @@ -92,7 +92,7 @@ function clearMappingRegistry (): void { * //These rules can then be used by providing them to a hydratedResultsMapper * record.as(personRules) * //or by registering them to the mapping registry - * mapping.register(Person, personRules) + * RecordObjectMapping.register(Person, personRules) * * @experimental * @param {function} translationFunction A function translating the names of your JS object property names to record key names @@ -129,7 +129,7 @@ function getCaseTranslator (databaseConvention: string | StandardCase, codeConve return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) } -export const mapping = { +export const RecordObjectMapping = { clearMappingRegistry, getCaseTranslator, register, diff --git a/packages/core/src/result-transformers.ts b/packages/core/src/result-transformers.ts index 154bcdd2b..df9a5c8ca 100644 --- a/packages/core/src/result-transformers.ts +++ b/packages/core/src/result-transformers.ts @@ -291,7 +291,7 @@ class ResultTransformers { * // Alternatively, the rules can be registered in the mapping registry. * // This registry exists in global memory and will persist even between driver instances. * - * neo4j.mapping.register(Person, PersonRules) + * neo4j.RecordObjectMapping.register(Person, PersonRules) * * // after registering the rule the transformer will follow them when mapping to the provided type * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { diff --git a/packages/core/test/mapping.highlevel.test.ts b/packages/core/test/mapping.highlevel.test.ts index f2a1722c7..ca761e47e 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, mapping, Node, Relationship, Rules, RulesFactories, Time } from '../src' +import { Date, DateTime, Duration, RecordObjectMapping, Node, Relationship, Rules, RulesFactories, Time } from '../src' import { as } from '../src/mapping.highlevel' describe('#unit Record Object Mapping', () => { @@ -46,10 +46,10 @@ describe('#unit Record Object Mapping', () => { } } - mapping.register(Person, personRules) + RecordObjectMapping.register(Person, personRules) // @ts-expect-error expect(as(gettable, Person).name).toBe('hi') - mapping.clearMappingRegistry() + RecordObjectMapping.clearMappingRegistry() // @ts-expect-error expect(as(gettable, Person).name).toBe('hello') }) diff --git a/packages/core/test/mapping.nameconventions.test.ts b/packages/core/test/mapping.nameconventions.test.ts index 8ab8bac79..1dda301ec 100644 --- a/packages/core/test/mapping.nameconventions.test.ts +++ b/packages/core/test/mapping.nameconventions.test.ts @@ -15,23 +15,23 @@ * limitations under the License. */ -import { mapping } from '../src' +import { RecordObjectMapping } from '../src' describe('#unit getCaseTranslator', () => { // Each convention has "tokenize" and "encode" functions, so testing each twice is sufficient. it('camelCase to SCREAMING_SNAKE_CASE', () => { - expect(mapping.getCaseTranslator(mapping.StandardCase.CamelCase, 'SCREAMING_SNAKE_CASE')('I_AM_COOL')).toBe('iAmCool') + expect(RecordObjectMapping.getCaseTranslator(RecordObjectMapping.StandardCase.CamelCase, 'SCREAMING_SNAKE_CASE')('I_AM_COOL')).toBe('iAmCool') }) it('SCREAMING_SNAKE_CASE to PascalCase', () => { - expect(mapping.getCaseTranslator(mapping.StandardCase.ScreamingSnakeCase, 'PascalCase')('IAmCool')).toBe('I_AM_COOL') + expect(RecordObjectMapping.getCaseTranslator(RecordObjectMapping.StandardCase.ScreamingSnakeCase, 'PascalCase')('IAmCool')).toBe('I_AM_COOL') }) it('PascalCase to snake_case', () => { - expect(mapping.getCaseTranslator(mapping.StandardCase.PascalCase, 'snake_case')('i_am_cool')).toBe('IAmCool') + expect(RecordObjectMapping.getCaseTranslator(RecordObjectMapping.StandardCase.PascalCase, 'snake_case')('i_am_cool')).toBe('IAmCool') }) it('snake_case to kebab-case', () => { - expect(mapping.getCaseTranslator(mapping.StandardCase.SnakeCase, 'kebab-case')('i-am-cool')).toBe('i_am_cool') + expect(RecordObjectMapping.getCaseTranslator(RecordObjectMapping.StandardCase.SnakeCase, 'kebab-case')('i-am-cool')).toBe('i_am_cool') }) it('kebab-case to camelCase', () => { - expect(mapping.getCaseTranslator(mapping.StandardCase.KebabCase, 'camelCase')('iAmCool')).toBe('i-am-cool') + expect(RecordObjectMapping.getCaseTranslator(RecordObjectMapping.StandardCase.KebabCase, 'camelCase')('iAmCool')).toBe('i-am-cool') }) }) diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index e7e5b0c31..c758bd33e 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -102,7 +102,7 @@ import resultTransformers, { ResultTransformer } from './result-transformers.ts' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' import { StandardCase } from './mapping.nameconventions.ts' -import { Rule, Rules, mapping } from './mapping.highlevel.ts' +import { Rule, Rules, RecordObjectMapping } from './mapping.highlevel.ts' import { RulesFactories } from './mapping.rulesfactories.ts' /** @@ -191,7 +191,7 @@ const forExport = { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } @@ -271,7 +271,7 @@ export { clientCertificateProviders, resolveCertificateProvider, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index cd451341c..ab71b1953 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -48,7 +48,7 @@ let nameMapping: (name: string) => string = (name) => name * }) * * can instead be written: - * neo4j.mapping.register(Person, personClassRules) + * neo4j.RecordObjectMapping.register(Person, personClassRules) * * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { * resultTransformer: neo4j.resultTransformers.hydrated(Person) @@ -63,7 +63,7 @@ export function register (constructor: GenericConstructo } /** - * Clears all registered type mappings from the mapping registry. + * Clears all registered type mappings from the record object mapping registry. * @experimental */ function clearMappingRegistry (): void { @@ -78,10 +78,10 @@ function clearMappingRegistry (): void { * * @example * //if the keys on records from the database are in ALLCAPS - * mapping.translateIdentifiers((name) => name.toUpperCase()) + * RecordObjectMapping.translateIdentifiers((name) => name.toUpperCase()) * * //if you utilize PacalCase in the database and camelCase in JavaScript code. - * mapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase")) + * RecordObjectMapping.translateIdentifiers(mapping.getCaseTranslator("PascalCase", "camelCase")) * * //if a type has one odd mapping you can override the translation with the rule * const personRules = { @@ -92,7 +92,7 @@ function clearMappingRegistry (): void { * //These rules can then be used by providing them to a hydratedResultsMapper * record.as(personRules) * //or by registering them to the mapping registry - * mapping.register(Person, personRules) + * RecordObjectMapping.register(Person, personRules) * * @experimental * @param {function} translationFunction A function translating the names of your JS object property names to record key names @@ -129,7 +129,7 @@ function getCaseTranslator (databaseConvention: string | StandardCase, codeConve return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) } -export const mapping = { +export const RecordObjectMapping = { clearMappingRegistry, getCaseTranslator, register, diff --git a/packages/neo4j-driver-deno/lib/core/result-transformers.ts b/packages/neo4j-driver-deno/lib/core/result-transformers.ts index f57eefa45..32dc6d1c2 100644 --- a/packages/neo4j-driver-deno/lib/core/result-transformers.ts +++ b/packages/neo4j-driver-deno/lib/core/result-transformers.ts @@ -291,7 +291,7 @@ class ResultTransformers { * // Alternatively, the rules can be registered in the mapping registry. * // This registry exists in global memory and will persist even between driver instances. * - * neo4j.mapping.register(Person, PersonRules) + * neo4j.RecordObjectMapping.register(Person, PersonRules) * * // after registering the rule the transformer will follow them when mapping to the provided type * const summary = await driver.executeQuery('CREATE (p:Person{ name: $name }) RETURN p', { name: 'Person1'}, { diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index b5748eec4..2a3727fc0 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -112,7 +112,7 @@ import { Rule, Rules, RulesFactories, - mapping, + RecordObjectMapping, StandardCase, MappedQueryResult } from './core/index.ts' @@ -448,7 +448,7 @@ const forExport = { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } @@ -522,7 +522,7 @@ export { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } export type { diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 12722162f..2bb01a76d 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -112,7 +112,7 @@ import { Rule, Rules, RulesFactories, - mapping, + RecordObjectMapping, StandardCase, MappedQueryResult } from 'neo4j-driver-core' @@ -447,7 +447,7 @@ const forExport = { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } @@ -521,7 +521,7 @@ export { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } export type { diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 15a8c82d8..2a0f1045f 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -82,7 +82,7 @@ import { Rule, Rules, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } from 'neo4j-driver-core' import { @@ -411,7 +411,7 @@ const forExport = { notificationFilterMinimumSeverityLevel, clientCertificateProviders, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } @@ -488,7 +488,7 @@ export { Rule, Rules, RulesFactories, - mapping, + RecordObjectMapping, StandardCase } export default forExport diff --git a/packages/neo4j-driver/test/examples.test.js b/packages/neo4j-driver/test/examples.test.js index 0f0e06b15..6ef9e4d54 100644 --- a/packages/neo4j-driver/test/examples.test.js +++ b/packages/neo4j-driver/test/examples.test.js @@ -1667,13 +1667,13 @@ describe('#integration examples', () => { // Register the rules for the custom types in the mapping registry. // This is optional, but not doing it means that rules must be provided for every conversion. - neo4j.mapping.register(Role, roleRules) - neo4j.mapping.register(Person, personRules) - neo4j.mapping.register(Movie, movieRules) - neo4j.mapping.register(ActingJobs, actingJobsRules) + neo4j.RecordObjectMapping.register(Role, roleRules) + neo4j.RecordObjectMapping.register(Person, personRules) + neo4j.RecordObjectMapping.register(Movie, movieRules) + neo4j.RecordObjectMapping.register(ActingJobs, actingJobsRules) // The code uses PascalCase for property names, while the cypher has camelCase. This issue can be solved with the following line. - neo4j.mapping.translateIdentifiers(neo4j.mapping.getCaseTranslator('camelCase', 'PascalCase')) + neo4j.RecordObjectMapping.translateIdentifiers(neo4j.RecordObjectMapping.getCaseTranslator('camelCase', 'PascalCase')) const session = driver.session() @@ -1706,7 +1706,7 @@ describe('#integration examples', () => { expect(executeQueryRes.records[0].Costars[0].Name).toBe('TBD') // The following line removes all rules from the mapping registry, this is run here just to not interfere with other tests. - neo4j.mapping.clearMappingRegistry() + neo4j.RecordObjectMapping.clearMappingRegistry() driver.close() }) diff --git a/packages/neo4j-driver/test/record-object-mapping.test.js b/packages/neo4j-driver/test/record-object-mapping.test.js index dfa45e0a5..ac12e22b5 100644 --- a/packages/neo4j-driver/test/record-object-mapping.test.js +++ b/packages/neo4j-driver/test/record-object-mapping.test.js @@ -117,10 +117,10 @@ describe('#integration record object mapping', () => { char2: 'next dev' }) - neo4j.mapping.register(Role, roleRules) - neo4j.mapping.register(Person, personRules) - neo4j.mapping.register(Movie, movieRules) - neo4j.mapping.register(ActingJobs, actingJobsRules) + neo4j.RecordObjectMapping.register(Role, roleRules) + neo4j.RecordObjectMapping.register(Person, personRules) + neo4j.RecordObjectMapping.register(Movie, movieRules) + neo4j.RecordObjectMapping.register(ActingJobs, actingJobsRules) const session = driverGlobal.session() const res = await session.executeRead(async (tx) => { @@ -138,7 +138,7 @@ describe('#integration record object mapping', () => { session.close() - neo4j.mapping.clearMappingRegistry() + neo4j.RecordObjectMapping.clearMappingRegistry() }) it('map transaction result with registered mappings', async () => { diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 8624c1e1b..246b135ff 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -100,7 +100,7 @@ import { Rule, Rules, RulesFactories, - mapping, + RecordObjectMapping, StandardCase, MappedQueryResult, types as coreTypes @@ -410,7 +410,7 @@ export type { Rule, Rules, RulesFactories, - mapping, + RecordObjectMapping, StandardCase, MappedQueryResult } From 509d02057ab598d5bc577aa6c25862113ab2e23f Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:45:57 +0200 Subject: [PATCH 31/32] improved ROM object documentation --- packages/core/src/mapping.highlevel.ts | 103 +++++++++-------- .../lib/core/mapping.highlevel.ts | 104 +++++++++--------- 2 files changed, 103 insertions(+), 104 deletions(-) diff --git a/packages/core/src/mapping.highlevel.ts b/packages/core/src/mapping.highlevel.ts index de0c85bdc..7dced3475 100644 --- a/packages/core/src/mapping.highlevel.ts +++ b/packages/core/src/mapping.highlevel.ts @@ -38,7 +38,54 @@ let rulesRegistry: Record = {} let nameMapping: (name: string) => string = (name) => name -/** +function register (constructor: GenericConstructor, rules: Rules): void { + rulesRegistry[constructor.toString()] = rules +} + +function clearMappingRegistry (): void { + rulesRegistry = {} +} + +function translateIdentifiers (translationFunction: (name: string) => string): void { + nameMapping = translationFunction +} + +function getCaseTranslator (databaseConvention: string | StandardCase, codeConvention: string | StandardCase): ((name: string) => string) { + const keys = Object.keys(nameConventions) + if (!keys.includes(databaseConvention)) { + throw newError( + `Naming convention ${databaseConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function.` + ) + } + if (!keys.includes(codeConvention)) { + throw newError( + `Naming convention ${codeConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function.` + ) + } + // @ts-expect-error + return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) +} + +export const RecordObjectMapping = Object.freeze({ + /** + * Clears all registered type mappings from the record object mapping registry. + * @experimental + */ + clearMappingRegistry, + /** + * Creates a translation frunction from record key names to object property names, for use with the {@link translateIdentifiers} function + * + * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE" + * + * @experimental + * @param {string} databaseConvention The naming convention in use in database result Records + * @param {string} codeConvention The naming convention in use in JavaScript object properties + * @returns {function} translation function + */ + getCaseTranslator, + /** * Registers a set of {@link Rules} to be used by {@link hydrated} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * * @example @@ -58,19 +105,8 @@ let nameMapping: (name: string) => string = (name) => name * @param {GenericConstructor} constructor The constructor function of the class to set rules for * @param {Rules} rules The rules to set for the provided class */ -export function register (constructor: GenericConstructor, rules: Rules): void { - rulesRegistry[constructor.toString()] = rules -} - -/** - * Clears all registered type mappings from the record object mapping registry. - * @experimental - */ -function clearMappingRegistry (): void { - rulesRegistry = {} -} - -/** + register, + /** * Sets a default name translation from record keys to object properties. * If providing a function, provide a function that maps FROM your object properties names TO record key names. * @@ -97,45 +133,8 @@ function clearMappingRegistry (): void { * @experimental * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ -function translateIdentifiers (translationFunction: (name: string) => string): void { - nameMapping = translationFunction -} - -/** - * Creates a translation frunction from record key names to object property names, for use with the {@link translateIdentifiers} function - * - * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE" - * - * @experimental - * @param {string} databaseConvention The naming convention in use in database result Records - * @param {string} codeConvention The naming convention in use in JavaScript object properties - * @returns {function} translation function - */ -function getCaseTranslator (databaseConvention: string | StandardCase, codeConvention: string | StandardCase): ((name: string) => string) { - const keys = Object.keys(nameConventions) - if (!keys.includes(databaseConvention)) { - throw newError( - `Naming convention ${databaseConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function.` - ) - } - if (!keys.includes(codeConvention)) { - throw newError( - `Naming convention ${codeConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function.` - ) - } - // @ts-expect-error - return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) -} - -export const RecordObjectMapping = { - clearMappingRegistry, - getCaseTranslator, - register, - StandardCase, translateIdentifiers -} +}) interface Gettable { get: (key: string) => V } diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index ab71b1953..a5f3ab2f6 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -38,7 +38,55 @@ let rulesRegistry: Record = {} let nameMapping: (name: string) => string = (name) => name -/** +function register (constructor: GenericConstructor, rules: Rules): void { + rulesRegistry[constructor.toString()] = rules +} + +function clearMappingRegistry (): void { + rulesRegistry = {} +} + +function translateIdentifiers (translationFunction: (name: string) => string): void { + nameMapping = translationFunction +} + +function getCaseTranslator (databaseConvention: string | StandardCase, codeConvention: string | StandardCase): ((name: string) => string) { + const keys = Object.keys(nameConventions) + if (!keys.includes(databaseConvention)) { + throw newError( + `Naming convention ${databaseConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function.` + ) + } + if (!keys.includes(codeConvention)) { + throw newError( + `Naming convention ${codeConvention} is not recognized, + please provide a recognized name convention or manually provide a translation function.` + ) + } + // @ts-expect-error + return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) +} + + +export const RecordObjectMapping = Object.freeze({ + /** + * Clears all registered type mappings from the record object mapping registry. + * @experimental + */ + clearMappingRegistry, + /** + * Creates a translation frunction from record key names to object property names, for use with the {@link translateIdentifiers} function + * + * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE" + * + * @experimental + * @param {string} databaseConvention The naming convention in use in database result Records + * @param {string} codeConvention The naming convention in use in JavaScript object properties + * @returns {function} translation function + */ + getCaseTranslator, + /** * Registers a set of {@link Rules} to be used by {@link hydrated} for the provided class when no other rules are specified. This registry exists in global memory, not the driver instance. * * @example @@ -58,19 +106,8 @@ let nameMapping: (name: string) => string = (name) => name * @param {GenericConstructor} constructor The constructor function of the class to set rules for * @param {Rules} rules The rules to set for the provided class */ -export function register (constructor: GenericConstructor, rules: Rules): void { - rulesRegistry[constructor.toString()] = rules -} - -/** - * Clears all registered type mappings from the record object mapping registry. - * @experimental - */ -function clearMappingRegistry (): void { - rulesRegistry = {} -} - -/** + register, + /** * Sets a default name translation from record keys to object properties. * If providing a function, provide a function that maps FROM your object properties names TO record key names. * @@ -97,45 +134,8 @@ function clearMappingRegistry (): void { * @experimental * @param {function} translationFunction A function translating the names of your JS object property names to record key names */ -function translateIdentifiers (translationFunction: (name: string) => string): void { - nameMapping = translationFunction -} - -/** - * Creates a translation frunction from record key names to object property names, for use with the {@link translateIdentifiers} function - * - * Recognized naming conventions are "camelCase", "PascalCase", "snake_case", "kebab-case", "SCREAMING_SNAKE_CASE" - * - * @experimental - * @param {string} databaseConvention The naming convention in use in database result Records - * @param {string} codeConvention The naming convention in use in JavaScript object properties - * @returns {function} translation function - */ -function getCaseTranslator (databaseConvention: string | StandardCase, codeConvention: string | StandardCase): ((name: string) => string) { - const keys = Object.keys(nameConventions) - if (!keys.includes(databaseConvention)) { - throw newError( - `Naming convention ${databaseConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function.` - ) - } - if (!keys.includes(codeConvention)) { - throw newError( - `Naming convention ${codeConvention} is not recognized, - please provide a recognized name convention or manually provide a translation function.` - ) - } - // @ts-expect-error - return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) -} - -export const RecordObjectMapping = { - clearMappingRegistry, - getCaseTranslator, - register, - StandardCase, translateIdentifiers -} +}) interface Gettable { get: (key: string) => V } From 00a68ea168ccdaa61a8dc6c9ca8bbb1e15af0ca2 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:46:27 +0200 Subject: [PATCH 32/32] deno sync --- packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts index a5f3ab2f6..a020f3025 100644 --- a/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts +++ b/packages/neo4j-driver-deno/lib/core/mapping.highlevel.ts @@ -68,7 +68,6 @@ function getCaseTranslator (databaseConvention: string | StandardCase, codeConve return (name: string) => nameConventions[databaseConvention].encode(nameConventions[codeConvention].tokenize(name)) } - export const RecordObjectMapping = Object.freeze({ /** * Clears all registered type mappings from the record object mapping registry.