diff --git a/.changeset/hip-files-repair.md b/.changeset/hip-files-repair.md new file mode 100644 index 00000000..9839e320 --- /dev/null +++ b/.changeset/hip-files-repair.md @@ -0,0 +1,27 @@ +--- +"@styra/ucast-prisma": patch +--- + +support translating table and column names via extra options + +An extra options object can be passed to `ucastToPrisma` to translate table and column names. +This is useful when the Prisma schema uses different names than the OPA policy used to generate +the conditions. + +```typescript +const p = ucastToPrisma( + { or: [{ "tickets.resolved": false }, { "users.name": "ceasar" }] }, + "tickets0", + { + translations: { + tickets: { $self: "tickets0", resolved: "resolved0" }, + users: { $self: "users0", name: "name0" }, + }, + } +); +``` + +In this example, the conditions `{ or: [{ "tickets.resolved": false }, { "users.name": "ceasar" }] }` +will be rewritten to `{ OR: [{ tickets0: { resolved0: false } }, { users0: { name0: "ceasar" } }] }`, +assuming that the Prisma schema uses `tickets0` and `users0` as table names and `resolved0` and `name0` +as column names respectively. diff --git a/packages/ucast-prisma/src/adapter.ts b/packages/ucast-prisma/src/adapter.ts index f913c601..55872f27 100644 --- a/packages/ucast-prisma/src/adapter.ts +++ b/packages/ucast-prisma/src/adapter.ts @@ -3,10 +3,22 @@ import { createPrismaInterpreter } from "./interpreter.js"; import * as instructions from "./instructions.js"; import * as interpreters from "./interpreters.js"; +export type Options = { + translations?: Record>; +}; + export function ucastToPrisma( ucast: Record, - primary: string + primary: string, + { translations }: Options = {} ): Record { const parsed = new ObjectQueryParser(instructions).parse(ucast); - return createPrismaInterpreter(primary, interpreters)(parsed); + return createPrismaInterpreter(primary, { + interpreters, + translate: (tbl: string, col: string): [string, string] => { + const tbl0 = translations?.[tbl]?.$self || tbl; + const col0 = translations?.[tbl]?.[col] || col; + return [tbl0, col0]; + }, + })(parsed); } diff --git a/packages/ucast-prisma/src/interpreter.ts b/packages/ucast-prisma/src/interpreter.ts index 09349583..e44b43ec 100644 --- a/packages/ucast-prisma/src/interpreter.ts +++ b/packages/ucast-prisma/src/interpreter.ts @@ -61,11 +61,24 @@ export type PrismaOperator = ( context: InterpretationContext> ) => Query; +export type interpreterOpts = { + interpreters: Record>; // TODO(sr): this doesn't feel right. +} & rawOpts; + +type rawOpts = { + translate?: (tbl: string, col: string) => [string, string]; +}; + export function createPrismaInterpreter( primary: string, - operators: Record> // TODO(sr): this doesn't feel right. + { interpreters, translate }: interpreterOpts ) { - const interpret = createInterpreter>(operators); + const interpret = createInterpreter, rawOpts>( + interpreters, + { + translate, + } + ); return (condition: Condition) => interpret(condition, new Query(primary)).toJSON(); } diff --git a/packages/ucast-prisma/src/interpreters.ts b/packages/ucast-prisma/src/interpreters.ts index 808bbe88..7069c7ba 100644 --- a/packages/ucast-prisma/src/interpreters.ts +++ b/packages/ucast-prisma/src/interpreters.ts @@ -3,6 +3,7 @@ import { FieldCondition, Condition, Comparable, + InterpretationContext, } from "@ucast/core"; import { PrismaOperator } from "./interpreter.js"; @@ -53,8 +54,10 @@ export const or: PrismaOperator = ( }; function op(name: string): PrismaOperator> { - return (condition, query) => { - const [tbl, field] = condition.field.split("."); + return (condition, query, options) => { + const translate = + (options as any)?.translate || ((...x: string[]) => [x[0], x[1]]); + const [tbl, field] = translate(...condition.field.split(".")); return query.addCondition(tbl, { [field]: { [name]: condition.value } }); }; } diff --git a/packages/ucast-prisma/tests/adapter.test.ts b/packages/ucast-prisma/tests/adapter.test.ts index 7f90b22b..bb7cf55f 100644 --- a/packages/ucast-prisma/tests/adapter.test.ts +++ b/packages/ucast-prisma/tests/adapter.test.ts @@ -61,4 +61,59 @@ describe("ucastToPrisma", () => { }); }); }); + describe("translations", () => { + describe("field operators", () => { + it("converts column names", () => { + const p = ucastToPrisma({ "table.name": "test" }, "table", { + translations: { table: { name: "name_col" } }, + }); + expect(p).toStrictEqual({ name_col: { equals: "test" } }); + }); + + it("converts table names", () => { + const p = ucastToPrisma({ "table.name": "test" }, "tbl", { + translations: { table: { $self: "tbl" } }, + }); + expect(p).toStrictEqual({ name: { equals: "test" } }); + }); + + it("converts multiple table+col names", () => { + const p = ucastToPrisma( + { "table.name": "test", "user.name": "alice" }, + "tbl", + { + translations: { + table: { $self: "tbl", name: "name_col" }, + user: { $self: "usr", name: "name_col_0" }, + }, + } + ); + expect(p).toStrictEqual({ + name_col: { equals: "test" }, + usr: { name_col_0: { equals: "alice" } }, + }); + }); + }); + + describe("compound operators", () => { + it("supports translations for 'or'", () => { + const p = ucastToPrisma( + { or: [{ "tickets.resolved": false }, { "users.name": "ceasar" }] }, + "tickets0", + { + translations: { + tickets: { $self: "tickets0", resolved: "resolved0" }, + users: { $self: "users0", name: "name0" }, + }, + } + ); + expect(p).toStrictEqual({ + OR: [ + { resolved0: { equals: false } }, + { users0: { name0: { equals: "ceasar" } } }, + ], + }); + }); + }); + }); }); diff --git a/packages/ucast-prisma/tests/interpreters.test.ts b/packages/ucast-prisma/tests/interpreters.test.ts index 23b22620..c004159d 100644 --- a/packages/ucast-prisma/tests/interpreters.test.ts +++ b/packages/ucast-prisma/tests/interpreters.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect } from "vitest"; describe("Condition interpreter", () => { describe("field operators", () => { - const interpret = createPrismaInterpreter("table", interpreters); + const interpret = createPrismaInterpreter("table", { interpreters }); it('generates query with `equals operator for "eq"', () => { const condition = new FieldCondition("eq", "table.name", "test"); @@ -39,7 +39,7 @@ describe("Condition interpreter", () => { }); describe("compound operators", () => { - const interpret = createPrismaInterpreter("user", interpreters); + const interpret = createPrismaInterpreter("user", { interpreters }); it('generates query without extra fluff for "AND"', () => { const condition = new CompoundCondition("and", [