Repository navigation
Replies: 10 comments 1 reply
|
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. |
|
The union is the trap. With Since your models are identical (only the provider and 1. Type services against one client, cast once at the composition root (recommended): // services only ever see the main client's types
constructor(private readonly prisma: MainClient) {}
// in your test wiring — the only cast in the codebase
const testDb = new TestClient(/* ... */) as unknown as MainClient
const users = new UserService(testDb)The cast is honest here: the runtime shapes really are interchangeable for identical models, and it stays contained to wiring code while every service keeps full, precise typing. 2. Structural interface via type AppDb = Pick<MainClient, 'user' | 'item' /* the delegates your services use */>This works cleanly when the delegate types are compatible; if the generated delegate types clash on variance, fall back to option 1. 3. Generic services ( What I'd avoid is trying to "fix" the union — it will keep fighting you on every delegate. Option 1 with the cast isolated in the factory that builds your test client is what most dual-client setups end up with. |
|
Can you NOT post AI generated answers? |
|
The error happens because Here are the two best solutions to solve this cleanly without duplicating code: Solution 1: Parameterize the Service with a Generic Type (Fastest & Fully Type-Safe)Instead of a union ( import { PrismaClient as MainClient } from "../prisma/main/generated/client";
export default class UserService<TClient extends { user: any } = MainClient> {
constructor(private prisma: TClient) {}
async create(data: IRegister) {
delete data.password2;
data.email = data.email.toLowerCase();
data.password = hashSync(data.password, 10);
// TypeScript now infers the exact return type based on whether
// this instance was initialized with MainClient or TestClient!
const user = await this.prisma.user.create({ data });
logger.info(logMessages.USER_SERV.create, { userId: user.id });
return mapper.toClass(user);
}
}Now both usages work without any errors or type casting: // In production controller:
import mainClient from "../prisma/main/client";
const service = new UserService(mainClient); // inferred as UserService<MainClient>
// In tests:
import testClient from "../prisma/test/client";
const testService = new UserService(testClient); // inferred as UserService<TestClient>Solution 2: Only Depend on the Delegate (
|
|
DO NOT POST AI GENERATED ANSWERS! |
|
The error happens because Here are the two best solutions to solve this cleanly without duplicating code: Solution 1: Parameterize the Service with a Generic Type (Fastest & Fully Type-Safe)Instead of a union ( import { PrismaClient as MainClient } from "../prisma/main/generated/client";
export default class UserService<TClient extends { user: any } = MainClient> {
constructor(private prisma: TClient) {}
async create(data: IRegister) {
delete data.password2;
data.email = data.email.toLowerCase();
data.password = hashSync(data.password, 10);
// TypeScript now infers the exact return type based on whether
// this instance was initialized with MainClient or TestClient!
const user = await this.prisma.user.create({ data });
logger.info(logMessages.USER_SERV.create, { userId: user.id });
return mapper.toClass(user);
}
}Now both usages work without any errors or type casting: // In production controller:
import mainClient from "../prisma/main/client";
const service = new UserService(mainClient); // inferred as UserService<MainClient>
// In tests:
import testClient from "../prisma/test/client";
const testService = new UserService(testClient); // inferred as UserService<TestClient>Solution 2: Only Depend on the Delegate (
|
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