From a1eb1254e44f4b9f2543312c8b1888f9081eef6c Mon Sep 17 00:00:00 2001 From: Eliya Cohen Date: Mon, 15 Jun 2026 13:04:16 +0300 Subject: [PATCH] feat(plugin-drizzle): add Drizzle plugin Validate raw `sql` queries and the raw-sql fragments embedded in Drizzle queries against the live database, compiling via Drizzle's own `.toSQL()`. Includes docs on extending SafeQL's plugin API to other ORMs. --- .changeset/drizzle-support.md | 10 + docs/.vitepress/config.ts | 1 + docs/plugins/extending-to-other-orms.md | 68 ++++++ package.json | 1 + packages/plugins/drizzle/build.config.ts | 20 ++ packages/plugins/drizzle/package.json | 50 +++++ packages/plugins/drizzle/src/index.ts | 1 + packages/plugins/drizzle/src/plugin.test.ts | 98 ++++++++ packages/plugins/drizzle/src/plugin.ts | 233 ++++++++++++++++++++ packages/plugins/drizzle/tsconfig.json | 6 + packages/plugins/drizzle/vitest.config.ts | 11 + pnpm-lock.yaml | 148 +++++++++++++ 12 files changed, 647 insertions(+) create mode 100644 .changeset/drizzle-support.md create mode 100644 docs/plugins/extending-to-other-orms.md create mode 100644 packages/plugins/drizzle/build.config.ts create mode 100644 packages/plugins/drizzle/package.json create mode 100644 packages/plugins/drizzle/src/index.ts create mode 100644 packages/plugins/drizzle/src/plugin.test.ts create mode 100644 packages/plugins/drizzle/src/plugin.ts create mode 100644 packages/plugins/drizzle/tsconfig.json create mode 100644 packages/plugins/drizzle/vitest.config.ts diff --git a/.changeset/drizzle-support.md b/.changeset/drizzle-support.md new file mode 100644 index 00000000..6bdec922 --- /dev/null +++ b/.changeset/drizzle-support.md @@ -0,0 +1,10 @@ +--- +"@ts-safeql/plugin-drizzle": minor +--- + +Add `@ts-safeql/plugin-drizzle`, validating Drizzle's `sql` template tag +(`import { sql } from "drizzle-orm"`) and its statically resolvable helpers (`sql.raw`, +`sql.identifier`, `sql.placeholder`, nested fragments). Drizzle's fluent query builder and +column-object interpolation are out of scope. This plugin reuses SafeQL's existing +`onTarget` / `onExpression` hooks with no core changes, demonstrating that the plugin API is +library-agnostic. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3dd26be2..5520b7da 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -63,6 +63,7 @@ export default defineConfig({ text: "Advanced", items: [ { text: "Plugin API", link: "/guide/plugins" }, + { text: "Extending to other ORMs", link: "/plugins/extending-to-other-orms" }, { text: "Incremental adoption", link: "/advanced/incremental-adoption" }, ], }, diff --git a/docs/plugins/extending-to-other-orms.md b/docs/plugins/extending-to-other-orms.md new file mode 100644 index 00000000..a000fbbc --- /dev/null +++ b/docs/plugins/extending-to-other-orms.md @@ -0,0 +1,68 @@ +# Extending SafeQL to other query libraries + +SafeQL's analysis is **library-agnostic**. Everything library-specific lives in a +plugin; the SafeQL core only ever consumes a small, stable seam: + +> a plugin turns a node of user code into **`{ kind: "sql"; text }`**, and SafeQL +> validates that SQL against the (shadow) database. + +Nothing about Kysely, Drizzle, Prisma, or TypeORM is baked into the core. Building +Kysely support proved it: **embedded-sql validation** (`resolveQuery`) compiles a builder +chain — through the raw `sql` fragments embedded in it — to SQL **inside the Kysely plugin** +(its own `DummyDriver` sandbox) and hands SafeQL only the SQL text, with no change to the +core query seam. + +This page maps the plugin hooks to each library so you can add a new one. + +## The hooks + +| Hook | Process | Purpose | +| --------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `onTarget` | rule (checker) | Decide whether a `TaggedTemplateExpression` is a query to validate, a fragment to skip, or not ours. | +| `onExpression` | rule (checker) | Translate each `${…}` interpolation inside a matched tag to a SQL fragment (`$N`, an identifier, an inlined literal, …). | +| `resolveQuery` + `queryNodeKinds` | rule (checker) | Validate non-tag nodes (e.g. fluent builder `CallExpression`s). The plugin produces the SQL itself (statically, or by compiling in its own sandbox). | +| `migrate` | worker | Build the shadow database from the project's migrations (TS migrations, a CLI, …) instead of the built-in `.sql` runner. | +| `createConnection` | worker | Provide a custom database connection. | + +Rule-only hooks (`onTarget`, `onExpression`, `resolveQuery`) run in +the ESLint process and may use the TypeScript checker. Worker hooks (`migrate`, +`createConnection`) run in the synckit worker and must never receive checker-bound values. + +## Drizzle (`@ts-safeql/plugin-drizzle`) + +Validates Drizzle's `sql` template tag (`import { sql } from "drizzle-orm"`): + +- `onTarget` — recognises the `drizzle-orm` `sql` symbol and validates a tag when it is a + standalone query (incl. `db.execute(sql\`…\`)`); a tag passed to a fragment method +(`.where`, `.having`, `.orderBy`, …) or used via `.as()` is skipped. +- `onExpression` — `sql.raw(static)` is inlined, `sql.identifier(static)` is quoted, + `sql.placeholder()` and plain **primitive** interpolations become bound params, nested + `sql` fragments recurse. A non-primitive interpolation (a Drizzle column/table object, + whose SQL can't be reconstructed statically) skips the query rather than guessing. + +Drizzle's fluent builder (`db.select().from(…)`) is out of scope; it would follow the +same `resolveQuery` pattern as Kysely's builder support, compiling via Drizzle's own +`.toSQL()` instead of Kysely's `DummyDriver`. + +## Prisma + +Prisma's raw queries are tagged templates from `@prisma/client` +(`prisma.$queryRaw\`…\``, with `Prisma.sql`/`Prisma.raw`/`Prisma.join`helpers) — the +same shape as Drizzle/Kysely`sql`tags. A Prisma plugin implements`onTarget`(recognise the`$queryRaw`/`$executeRaw`tag) and`onExpression`(translate the`Prisma.\*` +helpers), reusing this page's pattern verbatim. + +## TypeORM + +TypeORM already exercises the **generic migration abstraction**: a TypeORM plugin +implements only the `migrate` hook to run TypeORM TS migrations against the shadow +database (exactly as the Kysely plugin runs Kysely migrations). Its `EntityManager.query` +takes a plain SQL **string** rather than a tag, so a TypeORM plugin would claim the `query` +call by declaring a `CallExpression` query selector and validate it with `resolveQuery` +rather than `onTarget`. Routing the call is straightforward; validating a plain-string +argument — type annotations, `$1` parameters, dynamically-assembled text — is a separate +concern left to the plugin. + +## Summary + +Adding a library is "implement the hooks for that library", never "change SafeQL". The +core seam — `{ kind: "sql"; text }` plus DB introspection — is shared by every plugin. diff --git a/package.json b/package.json index 3ecb5dd3..e235cd2a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "devDependencies": { "@changesets/cli": "catalog:", "@eslint/js": "catalog:", + "drizzle-orm": "^0.45.2", "prettier": "catalog:", "turbo": "catalog:", "typescript": "catalog:", diff --git a/packages/plugins/drizzle/build.config.ts b/packages/plugins/drizzle/build.config.ts new file mode 100644 index 00000000..f6a2f190 --- /dev/null +++ b/packages/plugins/drizzle/build.config.ts @@ -0,0 +1,20 @@ +import { defineBuildConfig } from "unbuild"; + +export default defineBuildConfig([ + { + entries: ["src/index"], + declaration: true, + sourcemap: true, + rollup: { + emitCJS: true, + }, + externals: [ + "@ts-safeql/plugin-utils", + "@typescript-eslint/utils", + "drizzle-orm", + "postgres", + "tsx", + "typescript", + ], + }, +]); diff --git a/packages/plugins/drizzle/package.json b/packages/plugins/drizzle/package.json new file mode 100644 index 00000000..c76e6045 --- /dev/null +++ b/packages/plugins/drizzle/package.json @@ -0,0 +1,50 @@ +{ + "name": "@ts-safeql/plugin-drizzle", + "version": "0.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ts-safeql/safeql.git", + "directory": "packages/plugins/drizzle" + }, + "files": [ + "dist", + "package.json" + ], + "type": "module", + "types": "dist/index.d.ts", + "module": "dist/index.mjs", + "main": "dist/index.cjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "unbuild", + "dev": "unbuild --stub", + "typecheck": "tsc -b", + "test": "vitest", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@ts-safeql/eslint-plugin": "workspace:*", + "@ts-safeql/plugin-utils": "workspace:*", + "@ts-safeql/test-utils": "workspace:*", + "@types/node": "catalog:", + "@typescript-eslint/parser": "catalog:", + "@typescript-eslint/rule-tester": "catalog:", + "@typescript-eslint/utils": "catalog:", + "drizzle-orm": "^0.45.2", + "postgres": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:", + "unbuild": "catalog:", + "vitest": "catalog:" + }, + "peerDependencies": { + "drizzle-orm": ">=0.30.0" + } +} diff --git a/packages/plugins/drizzle/src/index.ts b/packages/plugins/drizzle/src/index.ts new file mode 100644 index 00000000..3b878587 --- /dev/null +++ b/packages/plugins/drizzle/src/index.ts @@ -0,0 +1 @@ +export { default } from "./plugin"; diff --git a/packages/plugins/drizzle/src/plugin.test.ts b/packages/plugins/drizzle/src/plugin.test.ts new file mode 100644 index 00000000..815233ef --- /dev/null +++ b/packages/plugins/drizzle/src/plugin.test.ts @@ -0,0 +1,98 @@ +import { afterAll, describe, expect, it } from "vitest"; +import { PluginTestDriver, type ToSQLResult } from "@ts-safeql/plugin-utils/testing"; +import plugin from "./plugin"; + +const driver = new PluginTestDriver({ + plugin: plugin.factory({}), + projectDir: process.cwd(), +}); + +afterAll(() => driver.teardown()); + +type Case = { name: string; source: string; output: ToSQLResult }; + +const imp = (code: string) => + `import { sql } from "drizzle-orm"; declare const db: { execute: (q: unknown) => unknown }; ${code}`; + +const targetCases: Case[] = [ + { + name: "standalone sql tag is validated", + source: imp("sql`select 1`"), + output: { sql: "select 1" }, + }, + { + name: "sql tag inside db.execute is validated", + source: imp("db.execute(sql`select 1`)"), + output: { sql: "select 1" }, + }, + { + name: "sql fragment passed to .where is skipped", + source: imp("declare const qb: { where: (q: unknown) => unknown }; qb.where(sql`id = 1`)"), + output: { skipped: true }, + }, + { + name: ".as() selection fragment is skipped", + source: imp("sql`first_name`.as('n')"), + output: { skipped: true }, + }, + { + name: ".mapWith() selection fragment is skipped", + source: imp("sql`count(*)`.mapWith(Number)"), + output: { skipped: true }, + }, + { + name: "a sql tag NOT imported from drizzle is ignored", + source: "const sql = (s: TemplateStringsArray) => s; sql`select 1`", + output: { skipped: true }, + }, +]; + +const expressionCases: Case[] = [ + { + name: "plain primitive interpolation -> positional param", + source: imp("const id = 123; sql`select * from person where id = ${id}`"), + output: { sql: "select * from person where id = $1" }, + }, + { + name: "sql.raw static -> inlined", + source: imp("sql`select * from person where ${sql.raw('age > 18')}`"), + output: { sql: "select * from person where age > 18" }, + }, + { + name: "sql.raw dynamic -> skip", + source: imp("declare const cond: string; sql`select * from person where ${sql.raw(cond)}`"), + output: { sql: "select * from person where /* skipped */" }, + }, + { + name: "sql.identifier -> quoted identifier", + source: imp("sql`select ${sql.identifier('first_name')} from person`"), + output: { sql: 'select "first_name" from person' }, + }, + { + name: "sql.placeholder -> positional param", + source: imp("sql`select ${sql.placeholder('id')}`"), + output: { sql: "select $N" }, + }, + { + name: "non-primitive (object) interpolation -> skip", + source: imp("sql`select ${{ a: 1 }}`"), + output: { sql: "select /* skipped */" }, + }, + { + name: "nested sql fragment is spliced in", + source: imp("sql`select * from person where ${sql`age > ${18}`}`"), + output: { sql: "select * from person where age > $N" }, + }, +]; + +describe("drizzle plugin — onTarget", () => { + for (const c of targetCases) { + it(c.name, () => expect(driver.toSQL(c.source)).toEqual(c.output)); + } +}); + +describe("drizzle plugin — onExpression", () => { + for (const c of expressionCases) { + it(c.name, () => expect(driver.toSQL(c.source)).toEqual(c.output)); + } +}); diff --git a/packages/plugins/drizzle/src/plugin.ts b/packages/plugins/drizzle/src/plugin.ts new file mode 100644 index 00000000..2f4af7bb --- /dev/null +++ b/packages/plugins/drizzle/src/plugin.ts @@ -0,0 +1,233 @@ +import type { TSESTree } from "@typescript-eslint/utils"; +import { ast, definePlugin, type TargetContext, type TargetMatch } from "@ts-safeql/plugin-utils"; +import ts from "typescript"; + +export default definePlugin({ + name: "drizzle", + package: "@ts-safeql/plugin-drizzle", + setup() { + return { + onTarget, + onExpression, + }; + }, +}); + +// Methods that consume a `sql` tag as a runnable query (validate the tag). +const querySinkMethods = new Set(["execute", "all", "get", "run", "values"]); + +// Methods that consume a `sql` tag as a *fragment* (skip — not a standalone query). +const fragmentMethods = new Set([ + "where", + "having", + "and", + "or", + "not", + "on", + "orderBy", + "groupBy", + "set", + "leftJoin", + "rightJoin", + "innerJoin", + "fullJoin", + "as", + "mapWith", +]); + +function onTarget({ + node, + context, +}: { + node: TSESTree.TaggedTemplateExpression; + context: TargetContext; +}): TargetMatch | false | undefined { + const tsNode = context.parser.esTreeNodeToTSNodeMap.get(node); + + if (!tsNode || !ts.isTaggedTemplateExpression(tsNode)) { + return undefined; + } + + if (!isDrizzleTag(tsNode.tag, context.checker)) { + return undefined; + } + + return isStandaloneQuery(tsNode) ? {} : false; +} + +function onExpression({ + context, +}: { + node: TSESTree.Expression; + context: { checker: ts.TypeChecker; precedingSQL: string; tsNode: ts.Node; tsType: ts.Type }; +}): string | false | undefined { + return buildExpressionSQL(context.tsNode, context.tsType, context.checker); +} + +// A `sql` tag is standalone unless it's a fragment; when unsure, treat as a fragment (skip) to avoid validating a snippet. +function isStandaloneQuery(tsNode: ts.TaggedTemplateExpression): boolean { + if (tsNode.parent && ts.isTemplateSpan(tsNode.parent)) { + return false; + } + + let current: ts.Node = tsNode; + let parent = current.parent; + + while (parent) { + // `.as(...)` / `.mapWith(...)` etc. mark a fragment, never a query. + if (ts.isPropertyAccessExpression(parent) && parent.expression === current) { + if (fragmentMethods.has(parent.name.text)) { + return false; + } + current = parent; + parent = current.parent; + continue; + } + + if ( + (ts.isAwaitExpression(parent) || + ts.isParenthesizedExpression(parent) || + ts.isAsExpression(parent) || + ts.isNonNullExpression(parent)) && + getInnerExpression(parent) === current + ) { + current = parent; + parent = current.parent; + continue; + } + + break; + } + + // As a call argument: a query sink (`db.execute(sql`...`)`) validates; anything else is a fragment. + if ( + parent !== undefined && + ts.isCallExpression(parent) && + parent.arguments.some((arg) => arg === current) + ) { + const method = ts.isPropertyAccessExpression(parent.expression) + ? parent.expression.name.text + : undefined; + + if (method !== undefined && querySinkMethods.has(method)) { + return true; + } + + return false; + } + + return true; +} + +function getInnerExpression(node: ts.Node): ts.Node | undefined { + if ( + ts.isAwaitExpression(node) || + ts.isParenthesizedExpression(node) || + ts.isAsExpression(node) || + ts.isNonNullExpression(node) + ) { + return node.expression; + } + return undefined; +} + +function buildExpressionSQL( + node: ts.Node, + tsType: ts.Type, + checker: ts.TypeChecker, +): string | false | undefined { + const expression = ast.unwrap({ node: node }); + + if (ts.isTaggedTemplateExpression(expression) && isDrizzleTag(expression.tag, checker)) { + return buildTemplateSQL(expression.template, checker); + } + + if (ts.isCallExpression(expression) && isDrizzleHelperCall(expression, checker)) { + return buildHelperSQL(expression, checker); + } + + // Only a primitive value becomes a bound param; a column/table object (`${users.id}`) isn't reconstructible. + if (isPrimitiveValueType(tsType)) { + return undefined; + } + + return false; +} + +function buildHelperSQL(call: ts.CallExpression, checker: ts.TypeChecker): string | false { + const method = ts.isPropertyAccessExpression(call.expression) + ? call.expression.name.text + : undefined; + + switch (method) { + case "raw": { + const value = getStaticString(call.arguments[0], checker); + return value === ast.UNRESOLVED ? false : value; + } + + case "identifier": { + const value = getStaticString(call.arguments[0], checker); + return value === ast.UNRESOLVED ? false : quoteIdentifier(value); + } + + case "placeholder": + return "$N"; + + default: + return false; + } +} + +function buildTemplateSQL( + template: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression, + checker: ts.TypeChecker, +): string | false { + if (ts.isNoSubstitutionTemplateLiteral(template)) { + return template.text; + } + + let sql = template.head.text; + + for (const span of template.templateSpans) { + const tsType = checker.getTypeAtLocation(span.expression); + const resolved = buildExpressionSQL(span.expression, tsType, checker); + if (resolved === false) return false; + sql += typeof resolved === "string" ? resolved : "$N"; + sql += span.literal.text; + } + + return sql; +} + +function isPrimitiveValueType(type: ts.Type): boolean { + const constituents = type.isUnion() ? type.types : [type]; + const valueFlags = + ts.TypeFlags.StringLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.Null | + ts.TypeFlags.Undefined; + + return constituents.every((member) => (member.flags & valueFlags) !== 0); +} + +function isDrizzleTag(node: ts.Node, checker: ts.TypeChecker): boolean { + return ast.isImportedFrom({ node: node, checker: checker, moduleName: "drizzle-orm" }); +} + +function isDrizzleHelperCall(call: ts.CallExpression, checker: ts.TypeChecker): boolean { + return ts.isPropertyAccessExpression(call.expression) && isDrizzleTag(call.expression, checker); +} + +function getStaticString( + node: ts.Node | undefined, + checker: ts.TypeChecker, +): string | typeof ast.UNRESOLVED { + const value = ast.getStaticValue({ node: node, checker: checker }); + return typeof value === "string" ? value : ast.UNRESOLVED; +} + +function quoteIdentifier(value: string): string { + return `"${value.replace(/"/g, '""')}"`; +} diff --git a/packages/plugins/drizzle/tsconfig.json b/packages/plugins/drizzle/tsconfig.json new file mode 100644 index 00000000..5bf50ec7 --- /dev/null +++ b/packages/plugins/drizzle/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.node.json", + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/packages/plugins/drizzle/vitest.config.ts b/packages/plugins/drizzle/vitest.config.ts new file mode 100644 index 00000000..600a12cf --- /dev/null +++ b/packages/plugins/drizzle/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig, mergeConfig } from "vitest/config"; +import shared from "../../../vitest.shared"; + +export default mergeConfig( + shared, + defineConfig({ + test: { + pool: "forks", + }, + }), +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f12d8cf..8c7f4f6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: '@eslint/js': specifier: 'catalog:' version: 10.0.1(eslint@10.0.3(jiti@2.4.2)) + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@opentelemetry/api@1.9.0)(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@types/pg@8.11.11)(@vercel/postgres@0.10.0)(kysely@0.28.17)(pg@8.20.0)(postgres@3.4.7)(prisma@6.5.0(typescript@5.8.2)) prettier: specifier: 'catalog:' version: 3.5.3 @@ -921,6 +924,48 @@ importers: specifier: 'catalog:' version: 4.1.7(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(vite@6.2.3(@types/node@24.12.0)(jiti@2.4.2)(tsx@4.19.3)) + packages/plugins/drizzle: + devDependencies: + '@ts-safeql/eslint-plugin': + specifier: workspace:* + version: link:../../eslint-plugin + '@ts-safeql/plugin-utils': + specifier: workspace:* + version: link:../../plugin-utils + '@ts-safeql/test-utils': + specifier: workspace:* + version: link:../../test-utils + '@types/node': + specifier: 'catalog:' + version: 24.12.0 + '@typescript-eslint/parser': + specifier: 'catalog:' + version: 8.57.1(eslint@10.0.3(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/rule-tester': + specifier: 'catalog:' + version: 8.57.1(eslint@10.0.3(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/utils': + specifier: 'catalog:' + version: 8.57.1(eslint@10.0.3(jiti@2.4.2))(typescript@5.8.2) + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@opentelemetry/api@1.9.0)(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@types/pg@8.11.11)(@vercel/postgres@0.10.0)(kysely@0.28.17)(pg@8.20.0)(postgres@3.4.7)(prisma@6.5.0(typescript@5.8.2)) + postgres: + specifier: 'catalog:' + version: 3.4.7 + tsx: + specifier: 'catalog:' + version: 4.19.3 + typescript: + specifier: 'catalog:' + version: 5.8.2 + unbuild: + specifier: 'catalog:' + version: 3.5.0(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)) + vitest: + specifier: 'catalog:' + version: 4.1.7(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(vite@6.2.3(@types/node@24.12.0)(jiti@2.4.2)(tsx@4.19.3)) + packages/plugins/kysely: devDependencies: '@ts-safeql/eslint-plugin': @@ -3119,6 +3164,98 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6953,6 +7090,17 @@ snapshots: dotenv@16.4.7: {} + drizzle-orm@0.45.2(@opentelemetry/api@1.9.0)(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@types/pg@8.11.11)(@vercel/postgres@0.10.0)(kysely@0.28.17)(pg@8.20.0)(postgres@3.4.7)(prisma@6.5.0(typescript@5.8.2)): + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@prisma/client': 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2) + '@types/pg': 8.11.11 + '@vercel/postgres': 0.10.0 + kysely: 0.28.17 + pg: 8.20.0 + postgres: 3.4.7 + prisma: 6.5.0(typescript@5.8.2) + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.123: {}