From cf82c4a00244d23d52ead4cec0d2a59e7f4421ad Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Sat, 17 Jun 2023 20:56:02 +0100 Subject: [PATCH 1/4] Move types around to aid reading --- src/index.ts | 3 +-- src/shiftParameters.ts | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3ee5e5b..b7d3386 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { shiftParameters } from "./shiftParameters"; -type ParamKey = `p_${number}`; +export type ParamKey = `p_${number}`; type ParamText = `$${ParamKey}`; type Input = { @@ -13,7 +13,6 @@ export type Output = { parameters: Record; i: number; }; -export type ParameterEntries = [ParamKey, any][]; function isOutput(input: unknown): input is Output { if (typeof input === "undefined") return false; diff --git a/src/shiftParameters.ts b/src/shiftParameters.ts index 865a6f5..e41937e 100644 --- a/src/shiftParameters.ts +++ b/src/shiftParameters.ts @@ -1,5 +1,6 @@ -import { Output, ParameterEntries } from "./index"; +import { Output, ParamKey } from "./index"; +type ParameterEntries = [ParamKey, any][]; export function shiftParameters(output: Output, shift: number): Output { const newText = output.text.replace( From 11b5f30b17e50657fa725ed48c79430ab89136b1 Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Sat, 17 Jun 2023 21:07:53 +0100 Subject: [PATCH 2/4] Reduce control flow by making all input look like an Output --- src/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index b7d3386..db7ef0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,17 +70,17 @@ function parseTemplate({ let nextParams = { ...parameters }; let nextI = Number(i) + 1; if (!isUndefinedOrNull(headParam)) { - if (isOutput(headParam)) { - const { text, parameters, i: paramI } = shiftParameters(headParam, nextI); - nextText += text; - nextParams = { ...nextParams, ...parameters }; - nextI = paramI; - } else { - const paramText: ParamText = `$p_${nextI}`; - const paramKey: ParamKey = `p_${nextI}`; - nextText += paramText; - nextParams = { ...nextParams, [paramKey]: headParam }; - } + const toCombine: Output = isOutput(headParam) + ? headParam + : { + i: 0, + text: "$p_0", + parameters: { p_0: headParam }, + }; + const { text, parameters, i: paramI } = shiftParameters(toCombine, nextI); + nextText += text; + nextParams = { ...nextParams, ...parameters }; + nextI = paramI; } return parseTemplate({ input: { strings: tailStrings, expressions: tailParams }, From c86cb6efcc65878a2cd2f24bcf5d3f16dc447134 Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Sat, 17 Jun 2023 21:32:13 +0100 Subject: [PATCH 3/4] Implement first conversion of types to native neo4j counterparts --- src/index.test.ts | 114 +++++++++++++++++++++++++--------------------- src/index.ts | 58 ++++++++++++++++++++--- 2 files changed, 113 insertions(+), 59 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index e1a9a1d..7471593 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,65 +1,73 @@ -import cypher from "./index"; +import cypher, { magiccypher } from "./index"; -describe("cypher", () => { - const template = cypher`a ${1} b ${"str"} c ${[1, 2]} d ${{ - foo: "bar", - }} e ${false} f ${6.66} g`; +const template = cypher`a ${1} b ${"str"} c ${[1, 2]} d ${{ + foo: "bar", +}} e ${false} f ${6.66} g`; - it("should provide the query with the replaced parameters", () => { - expect(template).toEqual( - expect.objectContaining({ - text: "a $p_0 b $p_1 c $p_2 d $p_3 e $p_4 f $p_5 g", - }) - ); - }); +it("should provide the query with the replaced parameters", () => { + expect(template).toEqual( + expect.objectContaining({ + text: "a $p_0 b $p_1 c $p_2 d $p_3 e $p_4 f $p_5 g", + }) + ); +}); - it("should provide parameters to match the query", () => { - expect(template).toEqual( - expect.objectContaining({ - parameters: { - p_0: 1, - p_1: "str", - p_2: [1, 2], - p_3: { foo: "bar" }, - p_4: false, - p_5: 6.66, - }, - }) - ); - }); +it("should provide parameters to match the query", () => { + expect(template).toEqual( + expect.objectContaining({ + parameters: { + p_0: 1, + p_1: "str", + p_2: [1, 2], + p_3: { foo: "bar" }, + p_4: false, + p_5: 6.66, + }, + }) + ); +}); - it("should return empty params when no expressions", () => { - expect(cypher`no params`).toEqual({ - text: "no params", - parameters: {}, - i: -1, - }); +it("should return empty params when no expressions", () => { + expect(cypher`no params`).toEqual({ + text: "no params", + parameters: {}, + i: -1, }); +}); - it("should handle nested cypher templates", () => { - expect( - cypher`a = ${1}, ${cypher`then ${2} ${cypher`and finally ${3}`} with sibling ${4}`}` - ).toEqual({ - text: "a = $p_0, then $p_1 and finally $p_2 with sibling $p_3", - parameters: { p_0: 1, p_1: 2, p_2: 3, p_3: 4 }, - i: 3, - }); +it("should handle nested cypher templates", () => { + expect( + cypher`a = ${1}, ${cypher`then ${2} ${cypher`and finally ${3}`} with sibling ${4}`}` + ).toEqual({ + text: "a = $p_0, then $p_1 and finally $p_2 with sibling $p_3", + parameters: { p_0: 1, p_1: 2, p_2: 3, p_3: 4 }, + i: 3, }); +}); - it("should not include undefined or null expressions", () => { - expect(cypher`test ${undefined}${null}`).toEqual({ - text: "test ", - parameters: {}, - i: 1, - }); +it("should not include undefined or null expressions", () => { + expect(cypher`test ${undefined}${null}`).toEqual({ + text: "test ", + parameters: {}, + i: 1, }); +}); - it("should maintain ordering across multiple contexts", () => { - const otherGenerator = (b: number, c: number) => cypher`b = ${b}, c = ${c}`; - expect(cypher`a = ${1}, ${otherGenerator(2, 3)}, d = ${4}`).toEqual({ - text: "a = $p_0, b = $p_1, c = $p_2, d = $p_3", - parameters: { p_0: 1, p_1: 2, p_2: 3, p_3: 4 }, - i: 3, - }); +it("should maintain ordering across multiple contexts", () => { + const otherGenerator = (b: number, c: number) => cypher`b = ${b}, c = ${c}`; + expect(cypher`a = ${1}, ${otherGenerator(2, 3)}, d = ${4}`).toEqual({ + text: "a = $p_0, b = $p_1, c = $p_2, d = $p_3", + parameters: { p_0: 1, p_1: 2, p_2: 3, p_3: 4 }, + i: 3, }); }); + +describe(magiccypher.name, () => { + it("should convert dates to native datetime objects", () => { + expect(magiccypher`${new Date("2023-01-01")}`).toEqual({ + text: "datetime($p_0)", + parameters: { p_0: "2023-01-01T00:00:00.000Z" }, + i: 0, + }) + }) +}) \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index db7ef0d..5d2a684 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import { shiftParameters } from "./shiftParameters"; export type ParamKey = `p_${number}`; -type ParamText = `$${ParamKey}`; type Input = { strings: string[]; @@ -47,14 +46,36 @@ export function cypher( } export default cypher; +export const magiccypher: typeof cypher = (strings, ...expressions) => { + const { output } = parseTemplate({ + input: { + strings: [...strings], + expressions, + }, + output: { + text: "", + parameters: {}, + i: -1, + }, + options: { convertValues: true }, + }); + + return output; +} + +type ParserOptions = { + convertValues: boolean; +}; type ParserState = { input: Input; output: Output; + options?: ParserOptions; }; function parseTemplate({ input: { strings, expressions }, output: { text, parameters, i }, + options = { convertValues: false }, }: ParserState): ParserState { const [headString, ...tailStrings] = strings; const [headParam, ...tailParams] = expressions; @@ -63,6 +84,7 @@ function parseTemplate({ return { input: { strings, expressions }, output: { text, parameters, i: i - 1 }, + options, }; } @@ -72,11 +94,10 @@ function parseTemplate({ if (!isUndefinedOrNull(headParam)) { const toCombine: Output = isOutput(headParam) ? headParam - : { - i: 0, - text: "$p_0", - parameters: { p_0: headParam }, - }; + : options.convertValues + ? convertValue(headParam) + : outputify(headParam); + const { text, parameters, i: paramI } = shiftParameters(toCombine, nextI); nextText += text; nextParams = { ...nextParams, ...parameters }; @@ -85,9 +106,34 @@ function parseTemplate({ return parseTemplate({ input: { strings: tailStrings, expressions: tailParams }, output: { text: nextText, parameters: nextParams, i: nextI }, + options, }); } function isUndefinedOrNull(x: any): x is undefined | null { return typeof x === "undefined" || x === null; } + +function outputify(value: T): Output { + return { + i: 0, + text: "$p_0", + parameters: { p_0: value }, + }; +} + +// Experimental +let conversionOn = true; +function convertValue(value: T): Output { + if (!conversionOn) return outputify(value); + if (value instanceof Date) return convertDatetime(value); + return outputify(value); +} + +function convertDatetime(value: Date): Output { + return { + i: 0, + text: "datetime($p_0)", + parameters: { p_0: value.toISOString() }, + }; +} From aa5f6d824dd733cbb3a23a32d2ef15cb07dffac8 Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Sat, 17 Jun 2023 21:35:00 +0100 Subject: [PATCH 4/4] Remove unused flag --- src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5d2a684..04b40a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -123,9 +123,7 @@ function outputify(value: T): Output { } // Experimental -let conversionOn = true; function convertValue(value: T): Output { - if (!conversionOn) return outputify(value); if (value instanceof Date) return convertDatetime(value); return outputify(value); }