Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 61 additions & 53 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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,
})
})
})
71 changes: 57 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { shiftParameters } from "./shiftParameters";

type ParamKey = `p_${number}`;
type ParamText = `$${ParamKey}`;
export type ParamKey = `p_${number}`;

type Input = {
strings: string[];
Expand All @@ -13,7 +12,6 @@ export type Output = {
parameters: Record<ParamKey, any>;
i: number;
};
export type ParameterEntries = [ParamKey, any][];

function isOutput(input: unknown): input is Output {
if (typeof input === "undefined") return false;
Expand Down Expand Up @@ -48,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;
Expand All @@ -64,31 +84,54 @@ function parseTemplate({
return {
input: { strings, expressions },
output: { text, parameters, i: i - 1 },
options,
};
}

let nextText = (text += headString);
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
: options.convertValues
? convertValue(headParam)
: outputify(headParam);

const { text, parameters, i: paramI } = shiftParameters(toCombine, nextI);
nextText += text;
nextParams = { ...nextParams, ...parameters };
nextI = paramI;
}
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<T>(value: T): Output {
return {
i: 0,
text: "$p_0",
parameters: { p_0: value },
};
}

// Experimental
function convertValue<T>(value: T): Output {
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() },
};
}
3 changes: 2 additions & 1 deletion src/shiftParameters.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down