diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/build-native-prisma-sql.ts b/frontends/nextjs/src/lib/native-prisma-bridge/build-native-prisma-sql.ts new file mode 100644 index 000000000..059a4fe31 --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/build-native-prisma-sql.ts @@ -0,0 +1,8 @@ +import { Prisma } from '@prisma/client' +import { splitSqlTemplate } from './split-sql-template' +import { toTemplateStrings } from './to-template-strings' + +export function buildNativePrismaSql(sql: string, params: unknown[]): Prisma.Sql { + const { strings, values } = splitSqlTemplate(sql, params) + return Prisma.sql(toTemplateStrings(strings), ...values) +} diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/constants/auth-headers.ts b/frontends/nextjs/src/lib/native-prisma-bridge/constants/auth-headers.ts new file mode 100644 index 000000000..2fa99f0d9 --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/constants/auth-headers.ts @@ -0,0 +1,2 @@ +export const AUTH_TOKEN_HEADER = 'x-dbal-native-prisma-token' +export const AUTHORIZATION_HEADER = 'authorization' diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/get-bridge-secret.ts b/frontends/nextjs/src/lib/native-prisma-bridge/get-bridge-secret.ts new file mode 100644 index 000000000..f9765f580 --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/get-bridge-secret.ts @@ -0,0 +1,3 @@ +export function getBridgeSecret(): string { + return process.env.DBAL_NATIVE_PRISMA_TOKEN?.trim() ?? '' +} diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/index.ts b/frontends/nextjs/src/lib/native-prisma-bridge/index.ts new file mode 100644 index 000000000..2a243322b --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/index.ts @@ -0,0 +1,6 @@ +export { AUTH_TOKEN_HEADER, AUTHORIZATION_HEADER } from './constants/auth-headers' +export { getBridgeSecret } from './get-bridge-secret' +export { isAuthorized } from './is-authorized' +export { splitSqlTemplate } from './split-sql-template' +export { buildNativePrismaSql } from './build-native-prisma-sql' +export { toTemplateStrings } from './to-template-strings' diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/is-authorized.ts b/frontends/nextjs/src/lib/native-prisma-bridge/is-authorized.ts new file mode 100644 index 000000000..ff37a8d59 --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/is-authorized.ts @@ -0,0 +1,26 @@ +import { AUTH_TOKEN_HEADER, AUTHORIZATION_HEADER } from './constants/auth-headers' +import { getBridgeSecret } from './get-bridge-secret' + +export function isAuthorized(request: { + headers: { get(name: string): string | null } +}): boolean { + const secret = getBridgeSecret() + if (!secret) { + return true + } + + const token = request.headers.get(AUTH_TOKEN_HEADER) + if (token === secret) { + return true + } + + const authorization = request.headers.get(AUTHORIZATION_HEADER) + if (authorization?.toLowerCase().startsWith('bearer ')) { + const bearer = authorization.slice(7).trim() + if (bearer === secret) { + return true + } + } + + return false +} diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/split-sql-template.ts b/frontends/nextjs/src/lib/native-prisma-bridge/split-sql-template.ts new file mode 100644 index 000000000..cffc83c72 --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/split-sql-template.ts @@ -0,0 +1,38 @@ +export function splitSqlTemplate(sql: string, params: unknown[]) { + const placeholderPattern = /\$(\d+)/g + const segments: string[] = [] + const values: unknown[] = [] + let lastIndex = 0 + let match: RegExpExecArray | null = null + + while ((match = placeholderPattern.exec(sql)) !== null) { + const index = Number(match[1]) + if (!Number.isFinite(index) || index < 1 || index > params.length) { + throw new Error('Native Prisma bridge placeholder out of range') + } + segments.push(sql.slice(lastIndex, match.index)) + values.push(params[index - 1]) + lastIndex = match.index + match[0].length + } + + if (values.length > 0) { + segments.push(sql.slice(lastIndex)) + return { strings: segments, values } + } + + const hasQuestionMarks = sql.includes('?') + + if (params.length === 0 && !hasQuestionMarks) { + return { strings: [sql], values: [] } + } + + if (!hasQuestionMarks) { + throw new Error('Native Prisma bridge parameter mismatch') + } + + const questionSegments = sql.split('?') + if (questionSegments.length !== params.length + 1) { + throw new Error('Native Prisma bridge parameter mismatch') + } + return { strings: questionSegments, values: params } +} diff --git a/frontends/nextjs/src/lib/native-prisma-bridge/to-template-strings.ts b/frontends/nextjs/src/lib/native-prisma-bridge/to-template-strings.ts new file mode 100644 index 000000000..e11c18c54 --- /dev/null +++ b/frontends/nextjs/src/lib/native-prisma-bridge/to-template-strings.ts @@ -0,0 +1,9 @@ +export function toTemplateStrings(parts: string[]): TemplateStringsArray { + const strings = [...parts] + const template = strings as unknown as TemplateStringsArray + Object.defineProperty(template, 'raw', { + value: [...strings], + enumerable: false, + }) + return template +}