UNPKG

prisma-trpc-generator

Version:

Prisma 2+ generator to emit fully implemented tRPC routers

934 lines (931 loc) 68.1 kB
"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',