Repository navigation
Replies: 5 comments 1 reply
|
The error occurs because TypeScript treats methods on union types (\MainClient | TestClient) strictly: when two different generated client types have generic or slightly different function signatures on .create(), TypeScript cannot find a common call signature and throws: Here are the two best solutions to solve this cleanly without duplicating service logic: Solution 1: Use a Single Generated Client with Swappable Driver Adapters (Recommended)In your schemas, the models (\User, \Item, \Listing) are identical-only the \datasource\ provider (PostgreSQL vs. SQLite) differs. With Prisma Driver Adapters (@prisma/adapter-pg\ and @prisma/adapter-better-sqlite3), you do not need two separate generated client directories. You can generate a single client and instantiate it with whichever driver adapter you need at runtime: \\ ypescript // Production / Dev client (PostgreSQL) // Test client (SQLite) Because both \mainClient\ and \ estClient\ are instances of the exact same \PrismaClient\ class, they share identical types: \\ ypescript export default class UserService { async create(data: IRegister) { Solution 2: Generic Class with Model Delegate TypingIf you must keep two distinct generated client paths, inject the client as a generic parameter constrained by the delegate methods your service requires: \\ ypescript
async create(data: IRegister) { } Alternatively, inject only the model delegate instead of the entire client: \\ ypescript
async create(data: IRegister) { // Usage: Solution 1 is the most idiomatic Prisma architecture since modern driver adapters were specifically designed to allow swapping database engines without duplicating client definitions. |
|
Type the service against only the delegate methods it uses, taken from the SQLite client. Both generated clients are assignable to that, so you keep autocomplete without import type { PrismaClient as TestClient } from "../prisma/test/generated/client";
type Methods = "create" | "findUnique" | "findFirst" | "findMany"
| "update" | "upsert" | "delete" | "deleteMany" | "count";
export type Client = {
user: Pick<TestClient["user"], Methods>;
item: Pick<TestClient["item"], Methods>;
// listing, terms: same pattern
};
export default class UserService {
constructor(private prisma: Client) {}
}Use the SQLite types because the Postgres client accepts extra options, such as One thing that doesn't work: using a single client generated for Tested on Prisma 7.10.0 with both clients generated. |
|
Hey! Just a quick warning about the other proposed solution: You cannot use a single Prisma Client with both PostgreSQL and SQLite driver adapters. Prisma generates database-specific SQL inside the query engine. A client generated with provider = "postgresql" will execute Postgres-specific SQL (like RETURNING) which will immediately crash when passed to the @prisma/adapter-better-sqlite3 adapter. Because Prisma 7 still requires distinct generated clients for different providers, you have the right approach by generating two separate clients. The TypeScript union error This expression is not callable... happens because TypeScript strictly views MainClient and TestClient as two completely distinct types, even though their .user.create() signatures look identical to us. The cleanest fix: Type Assertion (Duck Typing) Since both generated clients have the exact same runtime methods and schema structure, the easiest and cleanest way to fix this without duplicating your service logic is to just cast the injected client to your MainClient type inside the service. Change your user.service.ts to this: typescript } By casting prisma inside the constructor, TypeScript stops trying to reconcile the union types. Your IDE autocomplete will work perfectly using the MainClient types, but at runtime, if you pass in testClient, the exact same methods will execute seamlessly against SQLite. (If this solves your TS errors and prevents the adapter crash, please click Mark as answer so others trying to run dual-databases can find this solution!) |
|
Hi @IArnaut2, The reason you are encountering the error:
is a TypeScript limitation with unions of generic functions. Even though Here is how to solve this cleanly: Solution: Make the Service Class Generic (Recommended)Instead of typing the property as a union ( // services/user.service.ts
import { MainClient, TestClient } from "../prisma";
import { compareSync, hashSync } from "bcrypt";
import logger from "../global/logger";
import logMessages from "../global/logMessages";
import { userMapper as mapper } from "../mappers";
import { IRegister } from "../interfaces";
type SupportedClient = MainClient | TestClient;
export default class UserService<T extends SupportedClient = MainClient> {
constructor(private prisma: T) {}
async create(data: IRegister) {
delete data.password2;
data.email = data.email.toLowerCase();
data.password = hashSync(data.password, 10);
// ✅ Works! Full autocomplete and type-safety restored
const user = await this.prisma.user.create({ data });
logger.info(logMessages.USER_SERV.create, { userId: user.id });
return mapper.toClass(user);
}
async findById(id: bigint) {
return this.prisma.user.findUnique({ where: { id } });
}
}How it works:When you instantiate the service, TypeScript automatically infers the exact client type for that instance: // controllers/auth.controller.ts
import { mainClient } from "../prisma";
import UserService from "../services/user.service";
// Inferred as UserService<MainClient>
const service = new UserService(mainClient);// tests/userService.test.ts
import { testClient } from "../prisma";
import UserService from "../services/user.service";
// Inferred as UserService<TestClient>
const testService = new UserService(testClient);Alternative: Pass Only the Model DelegateIf your service only needs export default class UserService<T extends MainClient["user"] | TestClient["user"]> {
constructor(private userRepo: T) {}
async create(data: IRegister) {
return this.userRepo.create({ data });
}
}
// Usage:
const service = new UserService(mainClient.user);
const testService = new UserService(testClient.user);💡 Architectural Tip: Testcontainers instead of Dual SchemasMaintaining two separate Rather than maintaining dual schemas, the industry standard for testing Prisma apps is spinning up a temporary PostgreSQL instance using Testcontainers: npm install -D @testcontainers/postgresqlimport { PostgreSqlContainer } from "@testcontainers/postgresql";
const container = await new PostgreSqlContainer().start();
const testDatabaseUrl = container.getConnectionUri();
// Run migrations and run tests against real PostgreSQLThis allows you to keep a single |
|
I tested this with Prisma 7.10.0, TypeScript 5 and your setup (two schemas, Why the union and generic versions fail
What works: pick the methods you need from the SQLite clientThe SQLite client's query arguments are a subset of the Postgres client's. Postgres has extras like // db.ts
import type { PrismaClient as MainClient } from "./generated/main/client";
import type { PrismaClient as TestClient } from "./generated/test/client";
type Crud =
| "findUnique" | "findUniqueOrThrow" | "findFirst" | "findFirstOrThrow" | "findMany"
| "create" | "createMany" | "update" | "updateMany" | "upsert"
| "delete" | "deleteMany" | "count";
export type Db = {
[M in "user" | "post"]: Pick<TestClient[M], Crud>;
};// user.service.ts
import type { Db } from "./db";
export class UserService {
constructor(private prisma: Db) {}
create(email: string, name?: string) {
return this.prisma.user.create({ data: { email, name } });
}
listWithPosts() {
return this.prisma.user.findMany({ include: { posts: true } });
}
}new UserService(mainPrisma); // ✅ PrismaClient with PrismaPg
new UserService(testPrisma); // ✅ PrismaClient with PrismaBetterSqlite3Return types are inferred, so Limits I hitI checked each method one at a time against both clients:
So picking the whole Worth consideringThe two-provider setup exists only for tests. Running tests against Postgres too, through Testcontainers or a throwaway Postgres database, removes the problem entirely: you have one schema and one client, and your tests use the same SQL dialect as production. SQLite tests can also pass on queries that would behave differently on Postgres, for example case sensitivity or JSON filters. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I wanted to have two databases, a main database (PostgreSQL) and test database (sqlite). Because Prisma 7 does not support multiple datasource providers in a single schema, I had to make another schema for the test database.
prisma/main/schema.prismaandprisma/test/schema.prisma:I made a configuration file for each schema.
prisma/main/prisma,config.tsandprisma/test/prisma.config.ts:I generated client classes and initialized Prisma clients with respective adapters.
prisma/main/generated/client.tsandprisma/test/generated/client.ts:prisma/main/client.tsandprisma/test/client.ts:I have services
ItemService,ListingService,TermsServiceandUserService.UserServiceuses the main Prisma client. To use the service for the other client, I'd have to duplicate the entire service file just for that client, which would be a huge code smell and violate DRY principle.I turned the service into a class whose constructor has
prisma: anyas an argument......so that I could instantiate the service with respective clients.
controllers/auth.controller.ts:tests/userService.test.ts:Because
prismawasanytype, its properties and functions wouldn't be auto-completed. So I tried to joinMainClientandTestClientclasses into one type just calledClient. And changed the type ofprismaintoClient.prisma/index.ts:services/user.service.ts:But then the functions of
user, such ascreate,findUniqueetc, became uncallable.Error:
How do I use service for many Prisma clients simultaneously without duplicating code?
All reactions