Skip to content

Expand Record Object Mapping to allow Parameter Mapping#1362

Open
MaxAake wants to merge 50 commits into6.xfrom
parameter-mapping
Open

Expand Record Object Mapping to allow Parameter Mapping#1362
MaxAake wants to merge 50 commits into6.xfrom
parameter-mapping

Conversation

@MaxAake
Copy link
Copy Markdown
Contributor

@MaxAake MaxAake commented Nov 19, 2025

closes DRIVERS-107

This PR provides support for typechecking and automatic conversion of parameters, as well as using the object mapping registry to register mapping strategies for classes. This lets users directly pass objects even with problematic properties that can not be sent over bolt (like functions), by automatically converting them or by omitting them from the mapping strategy.

This is the 2nd half of the Record Object Mapping feature, which is also marked stabilized and taken out of preview in this PR.

Examples

class Obj {
    constructor (obj) {
        this.string = obj?.string ?? 'hi'
        this.number = obj?.number ?? 1
        this.bigint = obj?.bigint ?? BigInt(1)
        this.date = obj?.date ?? "2024-01-01"
        this.localDate = obj?.localDate ?? "2024-01-01"
        this.dateTime = obj?.dateTime ?? new neo4j.DateTime(1, 1, 1, 1, 1, 1, 1, 0)._toUTC()
        this.localDateTime = obj?.localDateTime ?? new neo4j.LocalDateTime(1, 1, 1, 1, 1, 1, 1).toString()
        this.duration = obj?.duration ?? "P1DT5.00007S"
        this.time = obj?.time ?? "10:11:12.13Z"
        this.localTime = obj?.localTime ?? "10:11:12.0001"
        this.list = obj?.list ?? ["hi"]
        this.function = () => "function string" // bolt cannot send functions
        this.node = new neo4j.Node("123", [], {}) // nor can it send nodes
    }
}

const session = driver.session()
const rules = {
    number: neo4j.rule.asNumber(),
    string: neo4j.rule.asString(),
    bigint: neo4j.rule.asBigInt({acceptNumber: true}),
    date: neo4j.rule.asDate({stringify: true}), // this will ensure date is stored as a Date in the DB and retrieved as a string 
    localDate: neo4j.rule.asDate({stringify: true}),
    dateTime: neo4j.rule.asDateTime({stringify: true}),
    localDateTime: neo4j.rule.asLocalDateTime({stringify: true}),
    duration: neo4j.rule.asDuration({stringify: true}),
    time: neo4j.rule.asTime({from: "dob", stringify: true}),
    localTime: neo4j.rule.asLocalTime({stringify: true}),
    list: neo4j.rule.asList({ apply: neo4j.rule.asString() }),
} // not including function and node here will make the driver skip sending them. They could alternatively be converted to a bolt-compatible type.

neo4j.RecordObjectMapping.register(Obj, rules)

// This allows us to use camelCase for our object properties and snake_case for the properties on our node in the DB
neo4j.RecordObjectMapping.translateIdentifiers(neo4j.RecordObjectMapping.getCaseTranslator("snake_case", "camelCase"))

session.run(
  'MERGE (n {string: $string, number: $number, bigint: $bigint, date: $date, local_date: $local_date, date_time: $date_time, local_date_time: $local_date_time, duration: $duration, dob: $dob, local_time: $local_time, list: $list}) RETURN n',
  new Obj(),
  {}
).as({n: {convert: (n) => new Obj(n.as(rules))}})
.then((res) => {
    console.log(res.records[0])
    session.close()
    driver.close()
})

@MaxAake MaxAake marked this pull request as ready for review January 29, 2026 14:33
@robsdedude robsdedude self-requested a review January 30, 2026 10:03
Copy link
Copy Markdown
Member

@robsdedude robsdedude left a comment

Choose a reason for hiding this comment

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

Review part 1. I'll continue next week with the review. I just want to submit my comments to make sure they don't get lost.

Comment thread packages/core/src/internal/util.ts Outdated
Comment thread packages/core/src/graph-types.ts Outdated
Comment thread packages/core/src/graph-types.ts
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/session.ts
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/test/temporal-types.test.ts Outdated
Comment thread packages/core/test/temporal-types.test.ts Outdated
Comment thread packages/core/test/temporal-types.test.ts
@MaxAake
Copy link
Copy Markdown
Contributor Author

MaxAake commented Feb 13, 2026

Consider making the optional parameter on the rules also allow for the record to totally lack that key, rather than only allow it to be undefined.

@MaxAake MaxAake requested a review from robsdedude March 4, 2026 08:23
Copy link
Copy Markdown
Member

@robsdedude robsdedude left a comment

Choose a reason for hiding this comment

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

Here goes a first round of comments. Submitting them now so they don't get lost until tomorrow when I shall continue.

Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/graph-types.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/internal/util.ts
Comment thread packages/core/src/internal/util.ts
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
@MaxAake MaxAake requested a review from robsdedude March 25, 2026 13:20
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts
Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/mapping.rulesfactories.ts
Comment thread packages/core/src/internal/util.ts
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/test/mapping.highlevel.test.ts
Comment thread packages/core/test/mapping.rulesfactories.test.ts
Comment thread packages/neo4j-driver/test/bolt-v3.test.js
Comment thread packages/neo4j-driver/test/record-object-mapping.test.js Outdated
@robsdedude robsdedude self-requested a review April 15, 2026 15:20
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/temporal-types.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
Comment thread packages/core/src/mapping.rulesfactories.ts
Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/session.ts Outdated
Comment on lines +182 to +185
* @param {mixed} query - Cypher query to execute
* @param {Object} parameters - Map with parameters to use in query
* @param {TransactionConfig} [transactionConfig] - Configuration for the new auto-commit transaction.
* @param {Rules} parameterRules - Rules to typecheck and/or map the parameter object .
* @param {Rules} parameterRules - Rules to typecheck and/or map the parameter object. Must not be provided as a separate argument if an Object is passed as first argument
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

More of the same question as above. To | undefined or not to | undefined as JShakespeare wrote.

Transaction and managed transaction are other places like this that were touched by this PR. As written about, this is likely a separate piece of work. But at least the newly added parameters should probably

  • follow which ever way is right if the whole function is newly added
  • or follow whatever other parameters are doing if the function existed or other functions with optional parameters exist in the near vicinity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

indeed. Aligning this with the transactionConfig parameter.

Comment thread packages/core/src/mapping.rulesfactories.ts
* @param {string} str The string to convert
* @returns {DateTime}
*/
static fromString (str: string): DateTime {
Copy link
Copy Markdown
Member

@robsdedude robsdedude Apr 23, 2026

Choose a reason for hiding this comment

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

Here are 2 tests for invalid input which don't get rejected

['2026-01-05T15:36:42+[America/Anchorage]', 'DateTime could not be parsed from string'],
['2026-01-05T15:36:42-[America/Anchorage]', 'DateTime could not be parsed from string'],

Here are tests for valid input which doesn't pass:

['2026-01-05T15:36:42+0001[America/Anchorage]', new DateTime(int(2026), int(1), int(5), int(15), int(36), int(42), int(0), int(60), 'America/Anchorage')],
// I *think* these should work according to RFC 9557
['2026-01-05T15:36:42Z[America/Anchorage]', new DateTime(int(2026), int(1), int(5), int(15), int(36), int(42), int(0), int(0), 'America/Anchorage')],
['2026-01-05T15:36:42-00:00[America/Anchorage]', new DateTime(int(2026), int(1), int(5), int(15), int(36), int(42), int(0), int(0), 'America/Anchorage')],

These issues exist before, but I didn't see them, sorry. For me it's always like that with temporal types: the longer I look, the more broken things become (including my mind).

Comment thread packages/core/src/mapping.rulesfactories.ts Outdated
Comment thread packages/core/src/mapping.decorators.ts Outdated
Comment thread packages/core/src/mapping.decorators.ts Outdated
Comment thread packages/core/src/mapping.highlevel.ts Outdated
if (processedValue != null || rule?.optional === true) {
// @ts-expect-error
obj[key] = processedValue
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a small corner case left where input and output are asymmetric:

  • if rule.convert returns null/undefined it depends on rule.optional whether that value is assigned or not.
  • if rule.parameterConversion returns null/undefined a null check happens.

If I interpret things correctly, if a user wants to roll a custom Rule that for example maps a custom JS type to NULL | INTEGER in the DBMS they have to set optional. Fair enough, I'd say. But if they don't what currently happens to a value mapped to NULL is:

  • DBMS -> app: if value is NULL in DBMS, validate and convert are correctly called, but the app object's property will not be set (undefined)
  • app -> DBMS: if parameterConversion called and returns null, validateAndCleanParameters rejects that.

I think the 2nd is fine as is. The former, I think is very surprising behavior. I'm not sure what the right behavior is. Maybe the driver should also throw if rule.convert returns a null-ish value but rule.optional === false. Or the driver should just always assign whatever rule.convert returns... Looking at asList, probably this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on changing to always assign what rule.convert returns, it's both simpler and better. Also aligns with the pattern of App <> Convert <> Validate <> DB

Comment thread packages/core/src/temporal-types.ts Outdated
new RegExp(
/^([+|-]\d{5,}|\d{4})-(\d{2})-(\d{2})[T|t](\d{2})(?::?(\d{2}))?(?::?(\d{2}))?(\.\d+)?/.source + // DateTime
/([Z|z]$|\+|-)?(?:(\d{2})?(?::?(\d{2}))?(?::?(\d{2}))?$)?(?:\[([^\]]*)\])?$/.source // Timezone
/([Z|z]|\+|-)?(?:(\d{2})?(?::?(\d{2}))?(?::?(\d{2}))?)?(?:\[([^\]]*)\])?$/.source // Timezone
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👀

['2026-01-05T15:36:42Z00:00[America/Anchorage]', 'DateTime could not be parsed from string']

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

2026-01-05T15:36:42Z:01[America/Anchorage]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants