prisma-trpc-generator
Version:
Prisma 2+ generator to emit fully implemented tRPC routers
934 lines (931 loc) • 68.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generate = generate;
const internals_1 = require("@prisma/internals");
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const pluralize_1 = __importDefault(require("pluralize"));
const prisma_generator_1 = require("prisma-trpc-shield-generator/lib/prisma-generator");
const prisma_generator_2 = require("prisma-zod-generator/lib/prisma-generator");
const config_1 = require("./config");
const helpers_1 = require("./helpers");
const project_1 = require("./project");
const getRelativePath_1 = __importDefault(require("./utils/getRelativePath"));
const removeDir_1 = __importDefault(require("./utils/removeDir"));
async function generate(options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
const rawOutput = (0, internals_1.parseEnvValue)(options.generator.output);
// Base resolution: like Prisma would (relative to schema file)
let outputDir = path_1.default.isAbsolute(rawOutput)
? rawOutput
: path_1.default.resolve(path_1.default.dirname(options.schemaPath), rawOutput);
// Load config: prefer an external JSON file via `config` (or `configPath`/`configFile`) key in the generator block.
// This allows the Prisma generator block to only specify `output` and `config`.
const rawInline = ((_a = options.generator.config) !== null && _a !== void 0 ? _a : {});
const configLocator = (rawInline['config'] ||
rawInline['configPath'] ||
rawInline['configFile']);
let rawConfigObject = {};
if (configLocator) {
// If user provided additional inline keys besides provider/output/config*, warn and ignore them
const inlineKeys = Object.keys(rawInline).filter((k) => ![
'provider',
'output',
'config',
'configPath',
'configFile',
'previewFeatures',
].includes(k));
if (inlineKeys.length > 0) {
console.warn(`[prisma-trpc-generator] Note: External config file is provided via "${(rawInline['config'] && 'config') ||
(rawInline['configPath'] && 'configPath') ||
'configFile'}". Inline options (${inlineKeys.join(', ')}) will be ignored. Put options in the JSON file instead.`);
}
const resolvedConfigPath = path_1.default.isAbsolute(configLocator)
? configLocator
: path_1.default.resolve(path_1.default.dirname(options.schemaPath), configLocator);
try {
const cfgText = await fs_1.promises.readFile(resolvedConfigPath, 'utf8');
rawConfigObject = JSON.parse(cfgText);
}
catch (err) {
console.error('[prisma-trpc-generator] Failed to read config file at', configLocator, err);
throw new Error(`Unable to load config JSON from ${configLocator}`);
}
}
else {
// Back-compat: accept inline config if no external config path is provided
rawConfigObject = rawInline;
// Deprecation notice for inline config usage
const inlineKeys = Object.keys(rawInline).filter((k) => !['provider', 'output', 'previewFeatures'].includes(k));
if (inlineKeys.length > 0) {
console.warn('[prisma-trpc-generator] Deprecation: Inline generator options are deprecated. Create a JSON config file and point to it via `config = "./trpc.config.json"` in your generator block. Support for inline options will be removed in a future major release.');
}
}
const results = config_1.configSchema.safeParse(rawConfigObject);
if (!results.success)
throw new Error('Invalid options passed');
const config = results.data;
// Backward-compat notice: withShield default changed to false per README.
// If user didn't explicitly set it, warn once to avoid surprises.
const rawConfig = rawConfigObject !== null && rawConfigObject !== void 0 ? rawConfigObject : {};
if (!('withShield' in rawConfig) && config.withShield === false) {
console.warn('[prisma-trpc-generator] Note: withShield now defaults to false. To enable shield generation, set withShield = true or provide a path.');
}
await fs_1.promises.mkdir(outputDir, { recursive: true });
await (0, removeDir_1.default)(outputDir, true);
if (config.withZod) {
// Generate Zod schemas alongside routers under outputDir/schemas
const zodOutput = path_1.default.join(outputDir, 'schemas');
await (0, prisma_generator_2.generate)({
...options,
generator: {
...options.generator,
// Redirect the zod generator's output to our schemas folder
output: { fromEnvVar: null, value: zodOutput },
// Keep the rest of the config the same, but add dateTimeStrategy
config: {
...options.generator.config,
dateTimeStrategy: config.dateTimeStrategy,
},
},
});
// Ensure schemas directory exists and is not empty for tests that assert presence
try {
await fs_1.promises.mkdir(zodOutput, { recursive: true });
const entries = await fs_1.promises.readdir(zodOutput);
if (!entries.length) {
await fs_1.promises.writeFile(path_1.default.join(zodOutput, '.keep'), '// generated');
}
}
catch {
// ignore
}
}
if (config.withShield === true) {
const shieldOutputPath = path_1.default.join(outputDir, './shield');
await (0, prisma_generator_1.generate)({
...options,
generator: {
...options.generator,
output: {
...options.generator.output,
value: shieldOutputPath,
},
config: {
...options.generator.config,
contextPath: config.contextPath,
},
},
});
}
// Prefer the new prisma-client generator when present; fallback to legacy
const prismaClientProvider = (_b = options.otherGenerators.find((it) => (0, internals_1.parseEnvValue)(it.provider) === 'prisma-client')) !== null && _b !== void 0 ? _b : options.otherGenerators.find((it) => (0, internals_1.parseEnvValue)(it.provider) === 'prisma-client-js');
if (!prismaClientProvider) {
throw new Error('Prisma tRPC Generator requires a Prisma Client generator. Please add one of the following to your schema:\n\n' +
'generator client {\n' +
' provider = "prisma-client-js"\n' +
'}\n\n' +
'OR\n\n' +
'generator client {\n' +
' provider = "prisma-client"\n' +
' output = "./generated/client"\n' +
'}');
}
const prismaClientDmmf = await (0, internals_1.getDMMF)({
datamodel: options.datamodel,
previewFeatures: prismaClientProvider.previewFeatures,
});
const modelOperations = prismaClientDmmf.mappings.modelOperations;
const models = prismaClientDmmf.datamodel.models;
const hiddenModels = [];
(0, helpers_1.resolveModelsComments)([...models], hiddenModels);
const modelGenConfig = (0, helpers_1.getModelsGenConfig)(models);
// Resolve Prisma Client import path respecting custom output path if provided
let prismaClientAbsPath = null;
try {
const outputField = prismaClientProvider === null || prismaClientProvider === void 0 ? void 0 : prismaClientProvider.output;
// Only treat as custom if user explicitly set a value in schema
const rawClientOut = outputField && outputField.value ? outputField.value : null;
if (rawClientOut) {
prismaClientAbsPath = path_1.default.isAbsolute(rawClientOut)
? rawClientOut
: path_1.default.resolve(path_1.default.dirname(options.schemaPath), rawClientOut);
}
}
catch {
prismaClientAbsPath = null;
}
// Check if using new prisma-client generator (vs legacy prisma-client-js)
const isNewPrismaClient = prismaClientProvider &&
(0, internals_1.parseEnvValue)(prismaClientProvider.provider) === 'prisma-client';
const resolveClientImportFrom = (fromDirAbs) => {
if (!prismaClientAbsPath)
return '@prisma/client';
const norm = prismaClientAbsPath.split(path_1.default.sep).join(path_1.default.posix.sep);
// If Prisma Client path points to the official package or .prisma cache, prefer the bare package import
const isDefaultPkg = norm.includes('/@prisma/client') || norm.includes('/.prisma/client');
if (isDefaultPkg)
return '@prisma/client';
const rel = path_1.default
.relative(fromDirAbs, prismaClientAbsPath)
.split(path_1.default.sep)
.join(path_1.default.posix.sep);
const basePath = rel.startsWith('.') ? rel : `./${rel}`;
// New prisma-client generator exports from client.ts, not index.ts
return isNewPrismaClient ? `${basePath}/client` : basePath;
};
const createRouter = project_1.project.createSourceFile(path_1.default.resolve(outputDir, 'routers', 'helpers', 'createRouter.ts'), undefined, { overwrite: true });
(0, helpers_1.generatetRPCImport)(createRouter);
if (config.withShield) {
(0, helpers_1.generateShieldImport)(createRouter, options, config.withShield);
}
(0, helpers_1.generateBaseRouter)(createRouter, config, options);
// Auth: emit helpers and export protected/role procedures when enabled
if (config.auth !== false) {
const rolesField = typeof config.auth === 'object'
? ((_c = config.auth.rolesField) !== null && _c !== void 0 ? _c : 'role')
: 'role';
const strategy = typeof config.auth === 'object'
? ((_d = config.auth.strategy) !== null && _d !== void 0 ? _d : 'session')
: 'session';
const strategiesDir = path_1.default.resolve(outputDir, 'routers', 'helpers');
await fs_1.promises.mkdir(strategiesDir, { recursive: true });
const strategyFile = path_1.default.resolve(strategiesDir, 'auth-strategy.ts');
/* eslint-disable no-useless-escape */
const strategyScaffold = `// @generated\nimport crypto from 'crypto';\n\nfunction b64urlToUtf8(b64url: string) {\n const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(b64url.length / 4) * 4, '=');\n return Buffer.from(b64, 'base64').toString('utf8');\n}\n\nfunction signHS256(data: string, secret: string) {\n return crypto.createHmac('sha256', secret).update(data).digest('base64').replace(/\\+/g, '-').replace(/\\\//g, '_').replace(/=+$/g, '');\n}\n\nexport async function getUser(_req: any) { return null; }\n\n// Default HS256 verifier; override via config.auth.jwt.verifyPath for other algs\nexport async function verifyToken(token: string, secret: string): Promise<any> {\n const parts = (token || '').split('.');\n if (parts.length !== 3) throw new Error('INVALID_TOKEN');\n const h = parts[0];\n const p = parts[1];\n const s = parts[2];\n const data = h + '.' + p;\n const sig = signHS256(data, secret);\n if (sig !== s) throw new Error('INVALID_SIGNATURE');\n const payload = JSON.parse(b64urlToUtf8(p));\n const now = Math.floor(Date.now() / 1000);\n if (typeof payload.exp === 'number' && now >= payload.exp) throw new Error('TOKEN_EXPIRED');\n if (typeof payload.nbf === 'number' && now < payload.nbf) throw new Error('TOKEN_NOT_YET_VALID');\n return payload;\n}\n\nexport async function getUserFromPayload(payload: any) {\n // Default: payload contains user fields directly; override if needed\n return payload?.user ?? payload;\n}\n\nexport async function resolveUser(_req: any) { return null; }\n`;
/* eslint-enable no-useless-escape */
try {
await fs_1.promises.access(strategyFile);
}
catch {
await fs_1.promises.writeFile(strategyFile, strategyScaffold, 'utf8');
}
const authObj = typeof config.auth === 'object' && config.auth !== null
? config.auth
: null;
const jwtCfg = (_e = authObj === null || authObj === void 0 ? void 0 : authObj.jwt) !== null && _e !== void 0 ? _e : {};
const sessionCfg = (_f = authObj === null || authObj === void 0 ? void 0 : authObj.session) !== null && _f !== void 0 ? _f : {};
const customCfg = (_g = authObj === null || authObj === void 0 ? void 0 : authObj.custom) !== null && _g !== void 0 ? _g : {};
const rel = (p) => p
? (0, getRelativePath_1.default)((0, internals_1.parseEnvValue)(options.generator.output), p, true, options.schemaPath)
: './auth-strategy';
const imports = [];
const body = [];
if (strategy === 'session') {
imports.push(`import { getUser as _getUser } from '${rel(sessionCfg.getUserPath)}';`);
body.push(`const _resolveUser = async (req: any) => _getUser(req);`);
}
else if (strategy === 'jwt') {
imports.push(`import { verifyToken as _verifyToken, getUserFromPayload as _getUserFromPayload } from '${rel(jwtCfg.verifyPath || jwtCfg.getUserFromPayloadPath)}';`);
body.push(`const _resolveUser = async (req: any) => {`);
body.push(` const header = '${(_h = jwtCfg.header) !== null && _h !== void 0 ? _h : 'authorization'}';`);
body.push(` const scheme = '${(_j = jwtCfg.scheme) !== null && _j !== void 0 ? _j : 'Bearer'}';`);
body.push(` const raw = (req?.headers?.[header] as string | undefined) || '';`);
body.push(` const token = raw?.startsWith(scheme) ? raw.slice(scheme.length).trim() : raw;`);
body.push(` const secret = process.env['${(_k = jwtCfg.secretEnv) !== null && _k !== void 0 ? _k : 'JWT_SECRET'}'] || '';`);
body.push(` if (!token || !secret) return null;`);
body.push(` const payload = await _verifyToken(token, secret);`);
body.push(` return _getUserFromPayload(payload);`);
body.push(`};`);
}
else {
imports.push(`import { resolveUser as _resolve } from '${rel(customCfg.resolverPath)}';`);
body.push(`const _resolveUser = async (req: any) => _resolve(req);`);
}
createRouter.addStatements(`\n${imports.join('\n')}\n`);
createRouter.addStatements(`\n${body.join('\n')}\n`);
createRouter.addStatements(`\nexport const authMiddleware = t.middleware(async ({ ctx, next }) => {\n try {\n const user = await _resolveUser((ctx as any).req);\n return next({ ctx: { ...ctx, user } });\n } catch {\n return next({ ctx: { ...ctx, user: null } });\n }\n});\n`);
createRouter.addStatements(`\nexport const publicProcedure = t.procedure.use(authMiddleware);\n`);
const authHelpersPath = path_1.default.resolve(outputDir, 'routers', 'helpers', 'auth.ts');
await fs_1.promises.mkdir(path_1.default.dirname(authHelpersPath), { recursive: true });
const authSource = `// @generated\nexport function ensureAuth(ctx: any) {\n if (!ctx?.user) {\n const err: any = new Error('UNAUTHORIZED');\n err.code = 'UNAUTHORIZED';\n throw err;\n }\n}\n\nexport function ensureRole(ctx: any, roles: string[]) {\n ensureAuth(ctx);\n const userRole = (ctx.user && (ctx.user['${rolesField}'] as string)) || null;\n if (!userRole || !roles.includes(userRole)) {\n const err: any = new Error('FORBIDDEN');\n err.code = 'FORBIDDEN';\n throw err;\n }\n}\n`;
await fs_1.promises.writeFile(authHelpersPath, authSource, 'utf8');
createRouter.addStatements(`\nimport { ensureAuth, ensureRole } from './auth';\n`);
createRouter.addStatements(`\nexport const protectedProcedure = publicProcedure.use(t.middleware(async ({ ctx, next }) => {\n ensureAuth(ctx);\n return next();\n}));\n`);
createRouter.addStatements(`\nexport function roleProcedure(roles: string[]) {\n return protectedProcedure.use(t.middleware(async ({ ctx, next }) => {\n ensureRole(ctx, roles);\n return next();\n }));\n}\n`);
}
// Preload schema filenames once to avoid repeated fs.stat calls (needed for services and routers)
const schemasDirAbs = path_1.default.resolve(outputDir, 'schemas');
let availableSchemaFiles = null;
if (config.withZod) {
try {
const files = await fs_1.promises.readdir(schemasDirAbs);
availableSchemaFiles = new Set(files);
}
catch {
availableSchemaFiles = new Set();
}
}
// If services are enabled, create typed BaseService and per-model services + registry
if (config.withServices) {
const servicesDirAbs = path_1.default.resolve(outputDir, config.serviceDir);
const baseServiceFile = project_1.project.createSourceFile(path_1.default.resolve(servicesDirAbs, `BaseService.ts`), undefined, { overwrite: true });
const prismaClientImportForServices = resolveClientImportFrom(servicesDirAbs);
baseServiceFile.addStatements(/* ts */ `
// @generated
import type { Prisma, PrismaClient } from '${prismaClientImportForServices}';
export type HasPrisma = { prisma: PrismaClient };
export class BaseService<M extends keyof PrismaClientModels, C extends HasPrisma = HasPrisma> {
protected readonly model: M;
protected readonly ctx: C;
constructor(model: M, ctx: C) {
this.model = model;
this.ctx = ctx;
}
protected get prisma() {
return this.ctx.prisma;
}
}
// Helper mapped type to access client model delegates in a typed way
type PrismaClientModels = {
${models
.filter((m) => !hiddenModels.includes(m.name))
.map((m) => `${m.name}: Prisma.${m.name}Delegate`) // use default internal args
.join(';\n ')}
};
`);
const indexFile = project_1.project.createSourceFile(path_1.default.resolve(servicesDirAbs, `index.ts`), undefined, { overwrite: true });
// Additional user imports if any
for (const imp of config.serviceImports || []) {
if (imp.namespace) {
indexFile.addImportDeclaration({
moduleSpecifier: imp.from,
namespaceImport: imp.namespace,
});
}
else if (imp.default || (imp.names && imp.names.length)) {
indexFile.addImportDeclaration({
moduleSpecifier: imp.from,
defaultImport: imp.default,
namedImports: imp.names || [],
});
}
else {
indexFile.addImportDeclaration({ moduleSpecifier: imp.from });
}
}
// Types from Prisma Client
const prismaClientImportForServicesIndex = prismaClientImportForServices;
indexFile.addStatements(/* ts */ `
import type { Prisma } from '${prismaClientImportForServicesIndex}';
import { BaseService } from './BaseService';
`);
indexFile.addStatements(/* ts */ `
type OpOpts = { bypassTenant?: boolean; withDeleted?: boolean };
type TenantContext = Context & { tenantId?: string | number };
`);
// Import Context directly from configured path
// Calculate relative path from services directory to context file
const servicesDirForContext = path_1.default.resolve(outputDir, config.serviceDir);
const schemaDir = path_1.default.dirname(options.schemaPath);
const contextAbsPath = path_1.default.isAbsolute(config.contextPath)
? config.contextPath
: path_1.default.resolve(schemaDir, config.contextPath);
const contextImportFromServices = path_1.default
.relative(servicesDirForContext, contextAbsPath)
.split(path_1.default.sep)
.join(path_1.default.posix.sep);
const contextImportPath = contextImportFromServices.startsWith('.')
? contextImportFromServices
: `./${contextImportFromServices}`;
indexFile.addStatements(/* ts */ `
import type { Context } from '${contextImportPath}';
`);
// Prepare schemas import base for services if Zod is enabled
let servicesSchemasImportBase = null;
if (config.withZod) {
const schemasBaseFromServices = path_1.default
.relative(path_1.default.resolve(outputDir, config.serviceDir), path_1.default.resolve(outputDir, 'schemas'))
.split(path_1.default.sep)
.join(path_1.default.posix.sep);
servicesSchemasImportBase = (schemasBaseFromServices.startsWith('.')
? `${schemasBaseFromServices}`
: `./${schemasBaseFromServices}`)
.replace(/\/+$/, '') // trim trailing slash
.replace(/^\.$/, './');
// We'll accumulate all unique schema imports across models, then add once
const importLines = new Set();
// Helper to normalize a service method to an op name expected by helper
const toSchemaOp = (method) => {
switch (method) {
case 'create':
return 'createOne';
case 'delete':
return 'deleteOne';
case 'update':
return 'updateOne';
case 'upsert':
return 'upsertOne';
default:
return method; // includes findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, findMany, aggregate, groupBy, count, createMany, deleteMany, updateMany
}
};
for (const model of models) {
if (hiddenModels.includes(model.name))
continue;
const serviceMethods = [
'findUnique',
'findUniqueOrThrow',
'findFirst',
'findFirstOrThrow',
'findMany',
'aggregate',
'groupBy',
'count',
'create',
'createManyAndReturn',
'createMany',
'delete',
'update',
'deleteMany',
'updateManyAndReturn',
'updateMany',
'upsert',
];
for (const m of serviceMethods) {
const op = toSchemaOp(m);
// Check if schema file exists for this operation
if (availableSchemaFiles) {
const fileName = `${op}${model.name}.schema.ts`;
if (!availableSchemaFiles.has(fileName))
continue;
}
const base = servicesSchemasImportBase !== null && servicesSchemasImportBase !== void 0 ? servicesSchemasImportBase : '';
const line = (0, helpers_1.getRouterSchemaImportByOpName)(op, model.name, base);
if (line)
importLines.add(line);
}
}
if (importLines.size) {
indexFile.addStatements(Array.from(importLines).join('\n'));
}
}
// Build per-model service classes with pass-through to prisma by default and zod parsing
const serviceBlocks = [];
for (const model of models) {
if (hiddenModels.includes(model.name))
continue;
const lc = model.name[0].toLowerCase() + model.name.slice(1);
// Construct method bodies with optional Zod parsing
const methodLine = (method, prismaMethod, prismaArgsType) => {
// Normalize to helper's expected op for schema name resolution
const normalizeOp = (m) => {
switch (m) {
case 'findUniqueOrThrow':
return 'findUnique';
case 'findFirstOrThrow':
return 'findFirst';
case 'create':
return 'createOne';
case 'delete':
return 'deleteOne';
case 'update':
return 'updateOne';
case 'upsert':
return 'upsertOne';
default:
return m; // pass through others
}
};
const schemaInputType = config.withZod
? (0, helpers_1.getInputTypeByOpName)(normalizeOp(method), model.name)
: null;
const parsed = config.withZod && schemaInputType
? `${schemaInputType}.parse(args)`
: 'args';
const cfg = modelGenConfig[model.name] || {};
const tenantKey = cfg.tenantKey;
const softKey = cfg.softDeleteKey;
const isRead = [
'findUnique',
'findUniqueOrThrow',
'findFirst',
'findFirstOrThrow',
'findMany',
'aggregate',
'groupBy',
'count',
].includes(method);
const isDelete = method === 'delete';
const isDeleteMany = method === 'deleteMany';
const isCreate = method === 'create' ||
method === 'createMany' ||
method === 'createManyAndReturn';
const isUpdate = method === 'update' ||
method === 'updateMany' ||
method === 'updateManyAndReturn' ||
method === 'upsert';
const header = ` ${method}(args: Prisma.${model.name}${prismaArgsType}, opts?: OpOpts) {`;
const baseParse = method === 'groupBy'
? `const parsedArgs = ${parsed}; let argsParsed = { ...parsedArgs, orderBy: parsedArgs.orderBy } as unknown as Prisma.${model.name}GroupByArgs & { orderBy: Prisma.${model.name}OrderByWithAggregationInput | Prisma.${model.name}OrderByWithAggregationInput[] };`
: `let argsParsed = ${parsed} as Prisma.${model.name}${prismaArgsType};`;
const tenantVarDecl = tenantKey
? `const _t = (this as unknown as { ctx: { tenantId?: string | number } }).ctx.tenantId;`
: '';
const tenantRead = tenantKey
? `if (_t !== undefined && !opts?.bypassTenant) {
const prevWhere = ('where' in argsParsed ? ( argsParsed as unknown as { where?: Record<string, unknown> } ).where : undefined) ?? {};
argsParsed = { ...( argsParsed as unknown as { where?: Record<string, unknown> } ), where: { AND: [ prevWhere, { ${tenantKey}: _t } ] } } as Prisma.${model.name}${prismaArgsType};
}`
: '';
const softRead = softKey
? `if (!opts?.withDeleted) {
const prevWhere = ('where' in argsParsed ? ( argsParsed as unknown as { where?: Record<string, unknown> } ).where : undefined) ?? {};
argsParsed = { ...( argsParsed as unknown as { where?: Record<string, unknown> } ), where: { AND: [ prevWhere, { ${softKey}: null } ] } } as Prisma.${model.name}${prismaArgsType};
}`
: '';
const tenantWriteData = tenantKey
? `if (_t !== undefined && !('data' in argsParsed && (${JSON.stringify(tenantKey)} in (argsParsed as unknown as { data: Record<string, unknown> }).data))) {
const prevData = ('data' in argsParsed ? ( argsParsed as unknown as { data: Record<string, unknown> } ).data : {}) as Record<string, unknown>;
prevData[${JSON.stringify(tenantKey)}] = _t;
argsParsed = { ...argsParsed, data: prevData } as Prisma.${model.name}${prismaArgsType};
}`
: '';
const tenantWriteWhere = tenantKey
? `if (_t !== undefined && !opts?.bypassTenant) {
const prevWhere = ('where' in argsParsed ? ( argsParsed as unknown as { where?: Record<string, unknown> } ).where : undefined) ?? {};
argsParsed = { ...( argsParsed as unknown as { where?: Record<string, unknown> } ), where: { AND: [ prevWhere, { ${tenantKey}: _t } ] } } as Prisma.${model.name}${prismaArgsType};
}`
: '';
const bodyLines = [];
if (isRead) {
bodyLines.push(baseParse);
if (tenantKey) {
bodyLines.push(tenantVarDecl);
// Do not inject AND tenant filter into findUnique/OrThrow (requires WhereUnique)
if (!(method === 'findUnique' || method === 'findUniqueOrThrow')) {
bodyLines.push(tenantRead);
}
}
if (softKey) {
// Do not inject soft-delete filter into findUnique/OrThrow (requires WhereUnique)
if (!(method === 'findUnique' || method === 'findUniqueOrThrow')) {
bodyLines.push(softRead);
}
}
return `${header}
${bodyLines.join('\n ')}
return this.prisma.${lc}.${prismaMethod}(argsParsed);
}`;
}
if (isDelete) {
if (softKey) {
// Soft delete translates to update
const softData = `{ ${softKey}: new Date() }`;
if (tenantKey) {
bodyLines.push(baseParse);
bodyLines.push(tenantVarDecl);
return `${header}
${bodyLines.join('\n ')}
const w = (argsParsed as Prisma.${model.name}DeleteArgs).where!;
return this.prisma.${lc}.update({ where: w, data: ${softData} });
}`;
}
return `${header}
${baseParse}
const w = (argsParsed as Prisma.${model.name}DeleteArgs).where!;
return this.prisma.${lc}.update({ where: w, data: ${softData} });
}`;
}
// Hard delete; scope by tenant if configured
bodyLines.push(baseParse);
if (tenantKey) {
bodyLines.push(tenantVarDecl);
}
return `${header}
${bodyLines.join('\n ')}
return this.prisma.${lc}.${prismaMethod}(argsParsed);
}`;
}
if (isDeleteMany) {
if (softKey) {
const softData = `{ ${softKey}: new Date() }`;
bodyLines.push(baseParse);
if (tenantKey) {
bodyLines.push(tenantVarDecl);
bodyLines.push(tenantWriteWhere);
}
return `${header}
${bodyLines.join('\n ')}
const w = (argsParsed as Prisma.${model.name}DeleteManyArgs).where;
return this.prisma.${lc}.updateMany({ where: w, data: ${softData} });
}`;
}
bodyLines.push(baseParse);
if (tenantKey) {
bodyLines.push(tenantVarDecl);
bodyLines.push(tenantWriteWhere);
}
return `${header}
${bodyLines.join('\n ')}
return this.prisma.${lc}.${prismaMethod}(argsParsed);
}`;
}
if (isCreate) {
bodyLines.push(baseParse);
if (tenantKey) {
bodyLines.push(tenantVarDecl);
bodyLines.push(tenantWriteData);
}
return `${header}
${bodyLines.join('\n ')}
return this.prisma.${lc}.${prismaMethod}(argsParsed);
}`;
}
if (isUpdate) {
bodyLines.push(baseParse);
if (tenantKey) {
bodyLines.push(tenantVarDecl);
if (method !== 'upsert')
bodyLines.push(tenantWriteData);
}
return `${header}
${bodyLines.join('\n ')}
return this.prisma.${lc}.${prismaMethod}(argsParsed);
}`;
}
return `${header}
${baseParse}
return this.prisma.${lc}.${prismaMethod}(argsParsed);
}`;
};
// Helper to check if a method's schema exists
const hasSchema = (method) => {
if (!config.withZod || !availableSchemaFiles)
return true;
const normalizeOp = (m) => {
switch (m) {
case 'findUniqueOrThrow':
return 'findUnique';
case 'findFirstOrThrow':
return 'findFirst';
case 'create':
return 'createOne';
case 'delete':
return 'deleteOne';
case 'update':
return 'updateOne';
case 'upsert':
return 'upsertOne';
default:
return m;
}
};
const op = normalizeOp(method);
const fileName = `${op}${model.name}.schema.ts`;
return availableSchemaFiles.has(fileName);
};
const lines = [];
lines.push(methodLine('findUnique', 'findUnique', 'FindUniqueArgs'));
lines.push(methodLine('findUniqueOrThrow', 'findUniqueOrThrow', 'FindUniqueOrThrowArgs'));
lines.push(methodLine('findFirst', 'findFirst', 'FindFirstArgs'));
lines.push(methodLine('findFirstOrThrow', 'findFirstOrThrow', 'FindFirstOrThrowArgs'));
lines.push(methodLine('findMany', 'findMany', 'FindManyArgs'));
lines.push(methodLine('aggregate', 'aggregate', 'AggregateArgs'));
lines.push(methodLine('groupBy', 'groupBy', 'GroupByArgs'));
lines.push(methodLine('count', 'count', 'CountArgs'));
lines.push(methodLine('create', 'create', 'CreateArgs'));
// Only include createManyAndReturn if schema exists
if (hasSchema('createManyAndReturn')) {
lines.push(methodLine('createManyAndReturn', 'createManyAndReturn', 'CreateManyAndReturnArgs'));
}
lines.push(methodLine('createMany', 'createMany', 'CreateManyArgs'));
lines.push(methodLine('delete', 'delete', 'DeleteArgs'));
lines.push(methodLine('update', 'update', 'UpdateArgs'));
lines.push(methodLine('deleteMany', 'deleteMany', 'DeleteManyArgs'));
// Only include updateManyAndReturn if schema exists
if (hasSchema('updateManyAndReturn')) {
lines.push(methodLine('updateManyAndReturn', 'updateManyAndReturn', 'UpdateManyAndReturnArgs'));
}
lines.push(methodLine('updateMany', 'updateMany', 'UpdateManyArgs'));
lines.push(methodLine('upsert', 'upsert', 'UpsertArgs'));
serviceBlocks.push(/* ts */ `
export class ${model.name}Service extends BaseService<'${model.name}', TenantContext> {
constructor(ctx: TenantContext) { super('${model.name}' as const, ctx); }
${lines.join('\n')}
}
`);
}
indexFile.addStatements(serviceBlocks.join('\n'));
// Services root factory
indexFile.addStatements(/* ts */ `
export function makeServices(ctx: Context) {
return {
${models
.filter((m) => !hiddenModels.includes(m.name))
.map((m) => `${m.name[0].toLowerCase() + m.name.slice(1)}: new ${m.name}Service(ctx)`) // e.g., user: new UserService(ctx)
.join(',\n ')}
} as const;
}
`);
}
// Skip heavy formatting for performance during tests/CI
const appRouter = project_1.project.createSourceFile(path_1.default.resolve(outputDir, 'routers', `index.ts`), undefined, { overwrite: true });
(0, helpers_1.generateCreateRouterImport)({
sourceFile: appRouter,
});
const routerStatements = [];
for (const modelOperation of modelOperations) {
const { model, ...operations } = modelOperation;
if (hiddenModels.includes(model))
continue;
// Start from Prisma-reported operations, then add known extras if requested
const reportedOps = Object.keys(operations);
// Extras we can synthesize even if not reported by DMMF
const extraOps = ['createManyAndReturn', 'updateManyAndReturn', 'count'];
let requestedExtras = extraOps.filter((extra) => config.generateModelActions.includes(extra));
// If withZod, ensure the schema exists for the op+model; skip otherwise (provider compatibility)
if (config.withZod && availableSchemaFiles) {
const filtered = [];
for (const op of requestedExtras) {
const fileOp = op === 'count' ? 'findMany' : op;
const fileName = `${fileOp}${model}.schema.ts`;
if (availableSchemaFiles.has(fileName))
filtered.push(op);
}
requestedExtras = filtered;
}
let modelActions = [
...reportedOps,
...requestedExtras.filter((op) => !reportedOps.includes(op)),
].filter((opType) => {
const baseOpType = opType.replace('One', '').replace('OrThrow', '');
return config.generateModelActions.some((action) => action === baseOpType);
});
// Filter out operations that don't have corresponding schema files
// (e.g., createManyAndReturn/updateManyAndReturn only exist for PostgreSQL/CockroachDB)
if (config.withZod && availableSchemaFiles) {
modelActions = modelActions.filter((opType) => {
const baseOpType = opType.replace('OrThrow', '');
let schemaOp = baseOpType;
if (baseOpType === 'findUniqueOrThrow')
schemaOp = 'findUnique';
if (baseOpType === 'findFirstOrThrow')
schemaOp = 'findFirst';
const fileName = `${schemaOp}${model}.schema.ts`;
return availableSchemaFiles.has(fileName);
});
}
// selected operations computed in modelActions
if (!modelActions.length)
continue;
const plural = (0, pluralize_1.default)(model.toLowerCase());
(0, helpers_1.generateRouterImport)(appRouter, plural, model);
const modelRouter = project_1.project.createSourceFile(path_1.default.resolve(outputDir, 'routers', `${model}.router.ts`), undefined, { overwrite: true });
(0, helpers_1.generateCreateRouterImport)({
sourceFile: modelRouter,
config,
});
// Add Prisma import for payload types/Args types used in casts
const routersDirAbs = path_1.default.resolve(outputDir, 'routers');
const prismaClientImportForRouters = resolveClientImportFrom(routersDirAbs);
modelRouter.addImportDeclaration({
moduleSpecifier: prismaClientImportForRouters,
namedImports: ['Prisma'],
});
if (config.withServices) {
// Import makeServices for delegation
const rel = path_1.default
.relative(path_1.default.resolve(outputDir, 'routers'), path_1.default.resolve(outputDir, config.serviceDir))
.split(path_1.default.sep)
.join(path_1.default.posix.sep);
modelRouter.addStatements(/* ts */ `
import { makeServices } from "${rel.startsWith('.') ? rel : `./${rel}`}";
`);
}
if (config.withZod) {
// Prefer schemas under our output directory; fallback to project-level generated/schemas
const schemasBase = path_1.default
.relative(path_1.default.resolve(outputDir, 'routers'), path_1.default.resolve(outputDir, 'schemas'))
.split(path_1.default.sep)
.join(path_1.default.posix.sep);
const schemasImportBase = schemasBase.startsWith('.')
? `${schemasBase}/${''}`.replace(/\/+/g, '/') // ensure trailing slash
: `./${schemasBase}/`;
(0, helpers_1.generateRouterSchemaImports)(modelRouter, model, modelActions,
// From a router file inside outputDir/routers, import base should be "../schemas"
// The above computation yields "../schemas", but normalize without trailing slash in helper
schemasImportBase.replace(/\/$/, '').replace(/^\.$/, './') ||
'../schemas');
}
modelRouter.addStatements(/* ts */ `
export const ${plural}Router = t.router({`);
for (const opType of modelActions) {
// Use mapping-provided name when available; otherwise synthesize
const opNameWithModel = (_l = operations[opType]) !== null && _l !== void 0 ? _l : `${opType}${model}`;
const baseOpType = opType.replace('OrThrow', '');
(0, helpers_1.generateProcedure)(modelRouter, opNameWithModel, (0, helpers_1.getInputTypeByOpName)(baseOpType, model), model, opType, baseOpType, config);
}
modelRouter.addStatements(/* ts */ `
})`);
modelRouter.formatText({ indentSize: 2 });
routerStatements.push(/* ts */ `
${model.toLowerCase()}: ${plural}Router`);
}
appRouter.addStatements(/* ts */ `
export const appRouter = t.router({${routerStatements}})
`);
// Skip heavy formatting for performance during tests/CI
await project_1.project.save();
let lastOpenApi = null;
// OpenAPI document
const openapiOpt = config.openapi;
const openapiEnabled = !!openapiOpt && openapiOpt !== false;
if (openapiEnabled) {
const enabled = typeof openapiOpt === 'object' && 'enabled' in openapiOpt
? !!openapiOpt.enabled
: true;
if (enabled) {
const oaTitle = (typeof openapiOpt === 'object' && 'title' in openapiOpt
? openapiOpt.title
: config.openapiTitle) || 'Prisma tRPC API';
const oaVersion = (typeof openapiOpt === 'object' && 'version' in openapiOpt
? openapiOpt.version
: config.openapiVersion) || '1.0.0';
const baseUrl = (typeof openapiOpt === 'object' && 'baseUrl' in openapiOpt
? openapiOpt.baseUrl
: config.openapiBaseUrl) || 'http://localhost:3000';
const pathPrefix = (typeof openapiOpt === 'object' && 'pathPrefix' in openapiOpt
? openapiOpt.pathPrefix
: config.openapiPathPrefix) || 'trpc';
const pathStyle = (typeof openapiOpt === 'object' && 'pathStyle' in openapiOpt
? openapiOpt.pathStyle
: config.openapiPathStyle) ||
'slash';
const includeExamples = typeof openapiOpt === 'object' && 'includeExamples' in openapiOpt
? !!openapiOpt.includeExamples
: ((_m = config.openapiIncludeExamples) !== null && _m !== void 0 ? _m : true);
const paths = {};
// Helpers (duplicated minimally from Postman section to avoid hoisting large blocks)
const getModelByName = (name) => models.find((m) => m.name === name);
const toWhereUnique = (m) => {
const model = getModelByName(m);
const idField = (model === null || model === void 0 ? void 0 : model.fields.find((f) => f.isId)) ||
(model === null || model === void 0 ? void 0 : model.fields.find((f) => f.isUnique));
if (!idField)
return {};
const sample = idField.type === 'Int' || idField.type === 'BigInt'
? 1
: idField.type === 'String'
? 'id'
: idField.type === 'DateTime'
? new Date().toISOString()
: 1;
return { [idField.name]: sample };
};
const sampleScalar = (field) => {
if (field.isList)
return [];
switch (field.type) {
case 'Int':
case 'BigInt':
return 1;
case 'Float':
case 'Decimal':
return 1.0;
case 'Boolean':
return true;
case 'String':
return `${field.name}`;
case 'DateTime':
return new Date().toISOString();
default:
return null;
}
};
const buildCreateData = (m) => {
const model = getModelByName(m);
if (!model)
return {};
const cfg = modelGenConfig[model.name] || {};
const data = {};
for (const f of model.fields) {
if (f.isId && f.hasDefaultValue)
continue;
if (f.isUpdatedAt)
continue;
if (f.isRequired && !f.relationName) {
data[f.name] = sampleScalar(f);
}
}
if (cfg.tenantKey)
delete data[cfg.tenantKey];
if (cfg.softDeleteKey)
delete data[cfg.softDeleteKey];
return data;
};
const buildUpdateData = (m) => {
const model = getModelByName(m);
if (!model)
return {};
const cfg = modelGenConfig[model.name] || {};
const data = {};
for (const f of model.fields) {
if (f.relationName)
continue;
if (f.isId)
continue;
if (f.isUpdatedAt)
continue;
if (cfg.tenantKey && f.name === cfg.tenantKey)
continue;
if (cfg.softDeleteKey && f.name === cfg.softDeleteKey)
continue;
data[f.name] = sampleScalar(f);
if (Object.keys(data).length >= 2)
break;
}
return data;
};
const pickGroupByField = (m) => {
var _a, _b, _c;
const model = getModelByName(m);
if (!model)
return 'id';
const idField = model.fields.find((f) => f.isId);
if (idField)
return idField.name;
const scalar = model.fields.find((f) => !f.relationName &&
(f.type === 'Int' ||
f.type === 'String' ||
f.type === 'Boolean' ||
f.type === 'DateTime'));
return (_c = (_a = scalar === null || scalar === void 0 ? void 0 : scalar.name) !== null && _a !== void 0 ? _a : (_b = model.fields[0]) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : 'id';
};
for (const modelOperation of modelOperations) {
const { model, ...operations } = modelOperation;
if (hiddenModels.includes(model))
continue;
const reportedOps = Object.keys(operations);
const extraOps = [
'createManyAndReturn',
'updateManyAndReturn',
'count',
];
let requestedExtras = extraOps.filter((extra) => config.generateModelActions.includes(extra));
const modelActions = [
...reportedOps,
...requestedExtras.filter((op) => !reportedOps.includes(op)),
].filter((opType) => {
const baseOpType = opType.replace('One', '').replace('OrThrow', '');
return config.generateModelActions.some((action) => action === baseOpType);
});
if (!modelActions.length)
continue;
const cfg = modelGenConfig[model] || {};
for (const opType of modelActions) {
const normalized = opType.replace('One', '');
const trpcPath = pathStyle === 'slash'
? `/${pathPrefix}/${model.toLowerCase()}/${normalized}`
: `/${pathPrefix}/${model.toLowerCase()}.${normalized}`;
let input = {};
const isUnique = [
'findUnique',
'findUniqueOrThrow',
'delete',
'update',
'upsert',