UNPKG

prisma-trpc-generator

Version:

Prisma 2+ generator to emit fully implemented tRPC routers

516 lines (515 loc) 22.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getProcedureTypeByOpName = exports.getPrismaArgsTypeByOpName = exports.getInputTypeByOpName = exports.getRouterSchemaImportByOpName = exports.generateRouterImport = exports.generateMiddlewareImport = exports.generateShieldImport = exports.generatetRPCImport = exports.generateCreateRouterImport = void 0; exports.generateBaseRouter = generateBaseRouter; exports.generateProcedure = generateProcedure; exports.generateRouterSchemaImports = generateRouterSchemaImports; exports.resolveModelsComments = resolveModelsComments; exports.getModelsGenConfig = getModelsGenConfig; const internals_1 = require("@prisma/internals"); const getRelativePath_1 = __importDefault(require("./utils/getRelativePath")); const uncapitalizeFirstLetter_1 = require("./utils/uncapitalizeFirstLetter"); const getProcedureName = (config) => { return config.withShield ? 'shieldedProcedure' : config.withMiddleware ? 'protectedProcedure' : 'publicProcedure'; }; const generateCreateRouterImport = ({ sourceFile, config, }) => { const imports = ['t']; if (config) { imports.push(getProcedureName(config)); } sourceFile.addImportDeclaration({ moduleSpecifier: './helpers/createRouter', namedImports: imports, }); }; exports.generateCreateRouterImport = generateCreateRouterImport; const generatetRPCImport = (sourceFile) => { sourceFile.addImportDeclaration({ moduleSpecifier: '@trpc/server', namespaceImport: 'trpc', }); }; exports.generatetRPCImport = generatetRPCImport; const generateShieldImport = (sourceFile, options, value) => { const outputDir = (0, internals_1.parseEnvValue)(options.generator.output); let shieldPath = (0, getRelativePath_1.default)(outputDir, 'shield/shield'); if (typeof value === 'string') { shieldPath = (0, getRelativePath_1.default)(outputDir, value, true, options.schemaPath); } // Emit using single quotes to align with test expectations sourceFile.addStatements(/* ts */ ` import { permissions } from '${shieldPath}'; `); }; exports.generateShieldImport = generateShieldImport; const generateMiddlewareImport = (sourceFile, options) => { const outputDir = (0, internals_1.parseEnvValue)(options.generator.output); sourceFile.addImportDeclaration({ moduleSpecifier: (0, getRelativePath_1.default)(outputDir, 'middleware'), namedImports: ['permissions'], }); }; exports.generateMiddlewareImport = generateMiddlewareImport; const generateRouterImport = (sourceFile, modelNamePlural, modelNameCamelCase) => { sourceFile.addImportDeclaration({ moduleSpecifier: `./${modelNameCamelCase}.router`, namedImports: [`${modelNamePlural}Router`], }); }; exports.generateRouterImport = generateRouterImport; function generateBaseRouter(sourceFile, config, options) { const outputDir = (0, internals_1.parseEnvValue)(options.generator.output); sourceFile.addStatements(/* ts */ ` import type { Context } from '${(0, getRelativePath_1.default)(outputDir, config.contextPath, true, options.schemaPath)}'; `); // Re-export Context for downstream type-only imports from this helper module sourceFile.addStatements(/* ts */ ` export type { Context } from '${(0, getRelativePath_1.default)(outputDir, config.contextPath, true, options.schemaPath)}'; `); if (config.trpcOptionsPath) { sourceFile.addStatements(/* ts */ ` import trpcOptions from '${(0, getRelativePath_1.default)(outputDir, config.trpcOptionsPath, true, options.schemaPath)}'; `); } sourceFile.addStatements(/* ts */ ` export const t = trpc.initTRPC.context<Context>().create(${config.trpcOptionsPath ? 'trpcOptions' : ''}); `); // Request ID + simple logging middleware (optional) if (config.withRequestId || config.withLogging) { sourceFile.addStatements(/* ts */ ` const _rid = () => (Date.now().toString(36) + Math.random().toString(36).slice(2, 8)).toUpperCase(); export const requestIdMiddleware = t.middleware(async ({ ctx, next, path, type }) => { const requestId = ctx.requestId ?? _rid(); const start = Date.now(); const result = await next({ ctx: { ...ctx, requestId } }); const ms = Date.now() - start; ${ // Only log when withLogging config.withLogging ? `// eslint-disable-next-line no-console console.log(JSON.stringify({ level: 'info', msg: 'trpc', requestId, path, type, ms }));` : ''} return result; });`); } const middlewares = []; if (config.withMiddleware && typeof config.withMiddleware === 'boolean') { sourceFile.addStatements(/* ts */ ` export const globalMiddleware = t.middleware(async ({ ctx, next }) => { // Add your middleware logic here return next() });`); middlewares.push({ type: 'global', value: /* ts */ `.use(globalMiddleware)`, }); } if (config.withMiddleware && typeof config.withMiddleware === 'string') { sourceFile.addStatements(/* ts */ ` import defaultMiddleware from '${(0, getRelativePath_1.default)(outputDir, config.withMiddleware, true, options.schemaPath)}'; `); sourceFile.addStatements(/* ts */ ` export const globalMiddleware = t.middleware(defaultMiddleware);`); middlewares.push({ type: 'global', value: /* ts */ `.use(globalMiddleware)`, }); } if (config.withShield) { sourceFile.addStatements(/* ts */ ` export const permissionsMiddleware = t.middleware(permissions); `); middlewares.push({ type: 'shield', value: /* ts */ ` .use(permissions)`, }); } // Base public procedure (skip if auth is enabled, as it will be defined in the auth section) if (config.auth === false) { if (config.withRequestId || config.withLogging) { sourceFile.addStatements(/* ts */ ` export const publicProcedure = t.procedure.use(requestIdMiddleware); `); } else { sourceFile.addStatements(/* ts */ ` export const publicProcedure = t.procedure; `); } } if (middlewares.length > 0) { const procName = getProcedureName(config); middlewares.forEach((middleware, i) => { if (i === 0) { sourceFile.addStatements(/* ts */ ` export const ${procName} = t.procedure${config.withRequestId || config.withLogging ? '.use(requestIdMiddleware)' : ''} `); } sourceFile.addStatements(/* ts */ ` .use(${middleware.type === 'shield' ? 'permissionsMiddleware' : 'globalMiddleware'}) `); }); } } // We intentionally avoid manually annotating return types here and let // Prisma Client infer them from the actual call expression. This ensures // results match Prisma's computed types (e.g., BatchPayload for updateMany, // or $Result.GetResult for findMany with select/include), keeping parity with // node_modules/.prisma/client/index.d.ts without re-implementing the complex // conditional types Prisma uses internally. const getReturnTypeAnnotation = () => ''; // Helper function to generate tRPC metadata for operations const generateMetadata = (modelName, opType, baseOpType, config) => { if (!config.withMeta) { return ''; // No metadata if disabled } const metaConfig = typeof config.withMeta === 'object' ? config.withMeta : { openapi: true, auth: false, description: true, defaultMeta: {}, }; const metadata = { ...metaConfig.defaultMeta }; // Generate OpenAPI metadata if (metaConfig.openapi) { const isQuery = (0, exports.getProcedureTypeByOpName)(baseOpType) === 'query'; const method = isQuery ? 'GET' : 'POST'; const operationName = config.showModelNameInProcedure ? `${baseOpType}${modelName}` : baseOpType; metadata.openapi = { method, path: `/${modelName.toLowerCase()}/${operationName}`, tags: [modelName], summary: getOperationDescription(modelName, baseOpType), }; } // Generate auth metadata if (metaConfig.auth && config.auth !== false) { metadata.auth = { required: config.withMiddleware || config.withShield, }; } // Generate description if (metaConfig.description) { metadata.description = getOperationDescription(modelName, baseOpType); } return `.meta(${JSON.stringify(metadata, null, 2)})`; }; // Helper function to get human-readable operation descriptions const getOperationDescription = (modelName, opType) => { const model = modelName.toLowerCase(); switch (opType) { case 'findMany': return `Find multiple ${model} records`; case 'findUnique': return `Find a unique ${model} record`; case 'findFirst': return `Find the first ${model} record`; case 'createOne': return `Create a new ${model} record`; case 'createMany': return `Create multiple ${model} records`; case 'createManyAndReturn': return `Create multiple ${model} records and return them`; case 'updateOne': return `Update a ${model} record`; case 'updateMany': return `Update multiple ${model} records`; case 'updateManyAndReturn': return `Update multiple ${model} records and return them`; case 'deleteOne': return `Delete a ${model} record`; case 'deleteMany': return `Delete multiple ${model} records`; case 'upsertOne': return `Create or update a ${model} record`; case 'aggregate': return `Aggregate ${model} records`; case 'groupBy': return `Group ${model} records`; case 'count': return `Count ${model} records`; default: return `Perform ${opType} operation on ${model}`; } }; function generateProcedure(sourceFile, name, typeName, modelName, opType, baseOpType, config) { let input = 'input'; const nameWithoutModel = name.replace(modelName, ''); if (nameWithoutModel === 'groupBy' && config.withZod) { // Ensure 'orderBy' key exists in the argument type to satisfy Prisma's groupBy constraints input = '{ ...input, orderBy: input.orderBy }'; } // Let Prisma infer the return type for maximum compatibility with its client types const returnTypeAnnotation = getReturnTypeAnnotation(); // Get metadata for the procedure (if enabled) const metadataCall = generateMetadata(modelName, opType, baseOpType, config); // For groupBy, pass the full input (schema aligns with Prisma.GroupByArgs) const procHeader = `${config.showModelNameInProcedure ? name : nameWithoutModel}: ${getProcedureName(config)}${metadataCall} ${config.withZod ? `.input(${typeName})` : ''}.${(0, exports.getProcedureTypeByOpName)(baseOpType)}`; // Determine Prisma Args type for this operation to keep input strongly typed at call-site const prismaArgsType = (0, exports.getPrismaArgsTypeByOpName)(opType); const prismaMethod = opType === 'count' ? 'count' : opType.replace('One', ''); // Build a properly typed args expression; for groupBy we also re-expose orderBy const argsExpr = nameWithoutModel === 'groupBy' && config.withZod ? `{ ...(${input} as Prisma.${modelName}${prismaArgsType}), orderBy: (${input} as Prisma.${modelName}${prismaArgsType}).orderBy }` : `${input} as Prisma.${modelName}${prismaArgsType}`; if (!config.withServices) { sourceFile.addStatements(/* ts */ `${procHeader}(async ({ ctx, input })${returnTypeAnnotation} => { const ${name} = await ctx.prisma.${(0, uncapitalizeFirstLetter_1.uncapitalizeFirstLetter)(modelName)}.${prismaMethod}(${argsExpr}); return ${name}; }),`); } else { const methodName = prismaMethod; sourceFile.addStatements(/* ts */ `${procHeader}(async ({ ctx, input })${returnTypeAnnotation} => { const services = makeServices(ctx); const ${name} = await services.${(0, uncapitalizeFirstLetter_1.uncapitalizeFirstLetter)(modelName)}.${methodName}(${argsExpr}); return ${name}; }),`); } } function generateRouterSchemaImports(sourceFile, modelName, modelActions, schemasImportBase) { sourceFile.addStatements( /* ts */ [ // remove any duplicate import statements ...new Set(modelActions.map((opName) => (0, exports.getRouterSchemaImportByOpName)(opName, modelName, schemasImportBase))), ].join('\n')); } const getRouterSchemaImportByOpName = (opName, modelName, schemasImportBase) => { const opType = opName.replace('OrThrow', ''); const inputType = (0, exports.getInputTypeByOpName)(opType, modelName); if (!inputType) return ''; // Determine the actual schema filename prefix for this op // Most ops match opType, but some reuse other inputs let fileOp = opType; if (opType === 'count') fileOp = 'count'; return `import { ${inputType} } from "${schemasImportBase}/${fileOp}${modelName}.schema"; `; }; exports.getRouterSchemaImportByOpName = getRouterSchemaImportByOpName; const getInputTypeByOpName = (opName, modelName) => { let inputType; switch (opName) { case 'findUnique': inputType = `${modelName}FindUniqueSchema`; break; case 'findFirst': inputType = `${modelName}FindFirstSchema`; break; case 'findMany': inputType = `${modelName}FindManySchema`; break; case 'findRaw': inputType = `${modelName}FindRawObjectSchema`; break; case 'createOne': inputType = `${modelName}CreateOneSchema`; break; case 'createMany': inputType = `${modelName}CreateManySchema`; break; case 'createManyAndReturn': inputType = `${modelName}CreateManyAndReturnSchema`; break; case 'deleteOne': inputType = `${modelName}DeleteOneSchema`; break; case 'updateOne': inputType = `${modelName}UpdateOneSchema`; break; case 'deleteMany': inputType = `${modelName}DeleteManySchema`; break; case 'updateMany': inputType = `${modelName}UpdateManySchema`; break; case 'updateManyAndReturn': inputType = `${modelName}UpdateManyAndReturnSchema`; break; case 'upsertOne': inputType = `${modelName}UpsertOneSchema`; break; case 'aggregate': inputType = `${modelName}AggregateSchema`; break; case 'aggregateRaw': inputType = `${modelName}AggregateRawObjectSchema`; break; case 'groupBy': inputType = `${modelName}GroupBySchema`; break; case 'count': // Use dedicated Count args schema inputType = `${modelName}CountSchema`; break; default: // Fallback for unknown operation types } return inputType; }; exports.getInputTypeByOpName = getInputTypeByOpName; // Map an operation name to its corresponding Prisma Args type suffix const getPrismaArgsTypeByOpName = (opName) => { // Normalize names that end with "One" const isOrThrow = /OrThrow$/.test(opName); const norm = opName.replace(/One$/, '').replace(/OrThrow$/, ''); switch (norm) { case 'findUnique': return isOrThrow ? 'FindUniqueOrThrowArgs' : 'FindUniqueArgs'; case 'findFirst': return isOrThrow ? 'FindFirstOrThrowArgs' : 'FindFirstArgs'; case 'findMany': return 'FindManyArgs'; case 'aggregate': return 'AggregateArgs'; case 'groupBy': return 'GroupByArgs'; case 'count': return 'CountArgs'; case 'create': return 'CreateArgs'; case 'createMany': return 'CreateManyArgs'; case 'createManyAndReturn': return 'CreateManyAndReturnArgs'; case 'delete': return 'DeleteArgs'; case 'update': return 'UpdateArgs'; case 'deleteMany': return 'DeleteManyArgs'; case 'updateMany': return 'UpdateManyArgs'; case 'updateManyAndReturn': return 'UpdateManyAndReturnArgs'; case 'upsert': return 'UpsertArgs'; default: // Fallback to a safe, general args type (rare) return 'FindManyArgs'; } }; exports.getPrismaArgsTypeByOpName = getPrismaArgsTypeByOpName; const getProcedureTypeByOpName = (opName) => { let procType; switch (opName) { case 'findUnique': case 'findFirst': case 'findMany': case 'findRaw': case 'aggregate': case 'aggregateRaw': case 'groupBy': case 'count': procType = 'query'; break; case 'createOne': case 'createMany': case 'createManyAndReturn': case 'deleteOne': case 'updateOne': case 'deleteMany': case 'updateMany': case 'updateManyAndReturn': case 'upsertOne': procType = 'mutation'; break; default: // Fallback for unknown operation types } return procType; }; exports.getProcedureTypeByOpName = getProcedureTypeByOpName; function resolveModelsComments(models, hiddenModels) { var _a, _b, _c, _d, _e, _f; const modelAttributeRegex = /(@@Gen\.)+([A-z])+(\()+(.+)+(\))+/; const attributeNameRegex = /(?:\.)+([A-Za-z])+(?:\()+/; const attributeArgsRegex = /(?:\()+([A-Za-z])+:+(.+)+(?:\))+/; for (const model of models) { if (model.documentation) { const attribute = (_b = (_a = model.documentation) === null || _a === void 0 ? void 0 : _a.match(modelAttributeRegex)) === null || _b === void 0 ? void 0 : _b[0]; const attributeName = (_d = (_c = attribute === null || attribute === void 0 ? void 0 : attribute.match(attributeNameRegex)) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.slice(1, -1); if (attributeName !== 'model') continue; const rawAttributeArgs = (_f = (_e = attribute === null || attribute === void 0 ? void 0 : attribute.match(attributeArgsRegex)) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.slice(1, -1); const parsedAttributeArgs = {}; if (rawAttributeArgs) { const rawAttributeArgsParts = rawAttributeArgs .split(':') .map((it) => it.trim()) .map((part) => (part.startsWith('[') ? part : part.split(','))) .flat() .map((it) => it.trim()); for (let i = 0; i < rawAttributeArgsParts.length; i += 2) { const key = rawAttributeArgsParts[i]; const value = rawAttributeArgsParts[i + 1]; parsedAttributeArgs[key] = JSON.parse(value); } } if (parsedAttributeArgs.hide) { hiddenModels.push(model.name); } } } } function getModelsGenConfig(models) { var _a, _b, _c, _d, _e, _f; const modelAttributeRegex = /(@@Gen\.)+([A-z])+(\()+(.+)+(\))+/; const attributeNameRegex = /(?:\.)+([A-Za-z])+(?:\()+/; const attributeArgsRegex = /(?:\()+([A-Za-z])+:+(.+)+(?:\))+/; const result = {}; for (const model of models) { const cfg = {}; if (model.documentation) { const attribute = (_b = (_a = model.documentation) === null || _a === void 0 ? void 0 : _a.match(modelAttributeRegex)) === null || _b === void 0 ? void 0 : _b[0]; const attributeName = (_d = (_c = attribute === null || attribute === void 0 ? void 0 : attribute.match(attributeNameRegex)) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.slice(1, -1); if (attributeName === 'model') { const rawAttributeArgs = (_f = (_e = attribute === null || attribute === void 0 ? void 0 : attribute.match(attributeArgsRegex)) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.slice(1, -1); const parsed = {}; if (rawAttributeArgs) { const parts = rawAttributeArgs .split(':') .map((it) => it.trim()) .map((part) => (part.startsWith('[') ? part : part.split(','))) .flat() .map((it) => it.trim()); for (let i = 0; i < parts.length; i += 2) { const key = parts[i]; const value = parts[i + 1]; try { parsed[key] = JSON.parse(value); } catch { // ignore malformed } } } if (typeof parsed.hide === 'boolean' && parsed.hide) cfg.hide = true; if (typeof parsed.tenantKey === 'string') cfg.tenantKey = parsed.tenantKey; if (typeof parsed.softDelete === 'string') cfg.softDeleteKey = parsed.softDelete; } } result[model.name] = cfg; } return result; } //# sourceMappingURL=helpers.js.map