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 1/3] 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 2/3] 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 3/3] 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 +}