diff --git a/lib/browser_manager/initialize.go b/lib/browser_manager/initialize.go index 0098484..9cad698 100644 --- a/lib/browser_manager/initialize.go +++ b/lib/browser_manager/initialize.go @@ -21,6 +21,8 @@ func Init(ctx context.Context, tabPool int) error { return fmt.Errorf("ROD_BROWSER_BIN environment variable not set") } + userDataDir := fmt.Sprintf("/tmp/chrome-user-data-%d", os.Getpid()) + launcher := launcher.New().Bin(browserPath). Headless(true). Set("--disable-gpu"). @@ -36,7 +38,7 @@ func Init(ctx context.Context, tabPool int) error { Set("--disable-sync"). Set("--metrics-recording-only"). Set("--mute-audio"). - Set("--user-data-dir", "/tmp/chrome-user-data"). + Set("--user-data-dir", userDataDir). Set("--disable-web-security"). Set("--no-startup-window"). Set("--disable-renderer-backgrounding"). // Prevent background throttling @@ -61,7 +63,7 @@ func Init(ctx context.Context, tabPool int) error { Set("--disable-cache"). Set("--disable-prompt-on-repost"). Set("--disable-domain-reliability"). - Set("--disable-features", "NetworkService,OutOfBlinkCors,InterestGroupStorage,UserAgentClientHint"). + Set("--disable-features", "OutOfBlinkCors,InterestGroupStorage,UserAgentClientHint"). Set("--disable-extensions"). Set("--disable-component-extensions-with-background-pages"). Set("--blink-settings", "autoplayPolicy=document-user-activation-required"). diff --git a/service/Dockerfile b/service/Dockerfile index 01b16dd..a1c2554 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -10,11 +10,14 @@ RUN mkdir -p /app/espresso/configs \ # Configure Go to bypass proxy for private repositories # Set ENABLE_UI to false to disable the UI -ENV GOSUMDB=off \ +ENV GOMODCACHE=/home/chrome/.cache/go-mod \ + GOCACHE=/home/chrome/.cache/go-build \ + GOSUMDB=off \ GO111MODULE=on \ ROD_BROWSER_BIN=/usr/bin/chromium \ ENABLE_UI=true - + +COPY lib/* ./lib # Copy go.mod and go.sum first to leverage Docker cache COPY service/go.mod service/go.sum ./service/ diff --git a/service/configs/espressoconfig.yaml b/service/configs/espressoconfig.yaml index 192f849..ae83f2e 100644 --- a/service/configs/espressoconfig.yaml +++ b/service/configs/espressoconfig.yaml @@ -4,6 +4,12 @@ template_storage: file_storage: storage_type: "disk" +mcp: + enabled: true + pdf_output_dir: "./output" + pdf_output_url_prefix: "http://localhost:8081/output" + pdf_output_path: "/output/" + browser: tab_pool: 50 @@ -42,4 +48,4 @@ digital_certificates: key_password: "password2" mysql: - dsn: "pdf_user:pdf_password@tcp(mysql:3306)/pdf_templates?parseTime=true" \ No newline at end of file + dsn: "pdf_user:pdf_password@tcp(localhost:3308)/pdf_templates?parseTime=true" \ No newline at end of file diff --git a/service/controller/http/pdf.go b/service/controller/http/pdf.go new file mode 100644 index 0000000..353bc10 --- /dev/null +++ b/service/controller/http/pdf.go @@ -0,0 +1,113 @@ +package controller + +import ( + "encoding/json" + "net/http" + "path/filepath" + "strings" + + "github.com/Zomato/espresso/service/dto" + "github.com/Zomato/espresso/service/pkg/response" + "github.com/Zomato/espresso/service/service" + svcUtils "github.com/Zomato/espresso/service/utils" +) + +type PDFController struct { + service *service.PDFService +} + +func NewPDFController(service *service.PDFService) *PDFController { + return &PDFController{service: service} +} + +func (s *PDFController) GeneratePDF(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + req := &dto.GeneratePDFRequest{} + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + svcUtils.Logger.Error(ctx, "error decoding request body", err, nil) + response.RespondWithError(w, "Failed to parse JSON request", http.StatusBadRequest) + return + } + + resp, err := s.service.GeneratePDF(ctx, req) + if err != nil { + svcUtils.Logger.Error(ctx, "error in generating pdf", err, nil) + response.RespondWithError(w, "Failed to generate PDF: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (s *PDFController) GeneratePDFStream(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req dto.PDFRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + svcUtils.Logger.Error(ctx, "error decoding request body", err, nil) + response.RespondWithError(w, "Failed to parse JSON request", http.StatusBadRequest) + return + } + + if err := req.Validate(); err != nil { + response.RespondWithError(w, err.Error(), http.StatusBadRequest) + return + } + + resp, err := s.service.GeneratePDFStream(ctx, &req) + if err != nil { + svcUtils.Logger.Error(ctx, "error in generating pdf stream", err, nil) + response.RespondWithError(w, "Failed to generate PDF stream: "+err.Error(), http.StatusInternalServerError) + return + } + + fileName := "generated.pdf" + if req.Filename != "" { + fileName = req.Filename + if !strings.HasSuffix(strings.ToLower(fileName), ".pdf") { + fileName += ".pdf" + } + } + fileName = filepath.Base(fileName) + + if len(resp.OutputFileBytes) == 0 { + response.RespondWithError(w, "No PDF data available", http.StatusInternalServerError) + return + } + + if err = response.RespondWithFile(w, fileName, "application/pdf", resp.OutputFileBytes); err != nil { + svcUtils.Logger.Error(ctx, "error writing pdf stream", err, nil) + response.RespondWithError(w, "Failed to write PDF stream: "+err.Error(), http.StatusInternalServerError) + return + } +} + +func (s *PDFController) SignPDF(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + req := &dto.SignPDFRequest{} + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + svcUtils.Logger.Error(ctx, "error decoding request body", err, nil) + response.RespondWithError(w, "Error decoding request body: "+err.Error(), http.StatusBadRequest) + return + } + + resp, err := s.service.SignPDF(ctx, req) + if err != nil { + svcUtils.Logger.Error(ctx, "error in signing pdf", err, nil) + response.RespondWithError(w, "Failed to sign PDF: "+err.Error(), http.StatusInternalServerError) + return + } + + responseData := map[string]interface{}{ + "status": map[string]string{ + "status": "success", + "message": "PDF signed successfully", + }, + "output_file_path": resp.OutputFilePath, + "output_file_bytes": resp.OutputFileBytes, + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(responseData) +} diff --git a/service/controller/http/template.go b/service/controller/http/template.go new file mode 100644 index 0000000..3211ca3 --- /dev/null +++ b/service/controller/http/template.go @@ -0,0 +1,83 @@ +package controller + +import ( + "encoding/json" + "net/http" + + "github.com/Zomato/espresso/service/dto" + "github.com/Zomato/espresso/service/pkg/response" + "github.com/Zomato/espresso/service/service" + svcUtils "github.com/Zomato/espresso/service/utils" +) + +type TemplateController struct { + service *service.TemplateService +} + +func NewTemplateController(service *service.TemplateService) *TemplateController { + return &TemplateController{service: service} +} + +func (s *TemplateController) GetAllTemplates(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + resp, err := s.service.GetAllTemplates(ctx) + if err != nil { + svcUtils.Logger.Error(ctx, "error listing templates", err, nil) + response.RespondWithError(w, "Failed to list templates: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (s *TemplateController) GetTemplateById(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + templateID := r.URL.Query().Get("template_id") + if templateID == "" { + svcUtils.Logger.Error(ctx, "template id is required", nil, nil) + response.RespondWithError(w, "template id is required", http.StatusBadRequest) + return + } + + resp, err := s.service.GetTemplateById(ctx, &dto.GetTemplateByIdRequest{TemplateId: templateID}) + if err != nil { + svcUtils.Logger.Error(ctx, "error getting template content", err, nil) + response.RespondWithError(w, "Failed to get template content: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +func (s *TemplateController) CreateTemplate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + req := &dto.CreateTemplateRequest{} + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + svcUtils.Logger.Error(ctx, "error decoding request body", err, nil) + response.RespondWithError(w, "Error decoding request body: "+err.Error(), http.StatusBadRequest) + return + } + + resp, err := s.service.CreateTemplate(ctx, req) + if err != nil { + svcUtils.Logger.Error(ctx, "error creating template", err, nil) + response.RespondWithError(w, "Failed to create template: "+err.Error(), http.StatusInternalServerError) + return + } + + responseData := map[string]interface{}{ + "status": map[string]string{ + "status": "success", + "message": "Template created successfully", + }, + "template_id": resp.TemplateId, + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(responseData) +} diff --git a/service/controller/mcp/pdf.go b/service/controller/mcp/pdf.go new file mode 100644 index 0000000..9e28c8b --- /dev/null +++ b/service/controller/mcp/pdf.go @@ -0,0 +1,107 @@ +package tools + +import ( + "context" + + "github.com/Zomato/espresso/service/dto" + pdfservice "github.com/Zomato/espresso/service/service" + svcUtils "github.com/Zomato/espresso/service/utils" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type PDFTools struct { + service *pdfservice.PDFService +} + +func NewPDFTools(service *pdfservice.PDFService) *PDFTools { + return &PDFTools{service: service} +} + +func (s *PDFTools) GeneratePDF(ctx context.Context, _ *mcp.CallToolRequest, req dto.GeneratePDFMCPRequest) (*mcp.CallToolResult, dto.GeneratePDFMCPResponse, error) { + generatePDFReq, err := req.GeneratePDFMCPRequestToGeneratePDFRequest() + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "Failed to parse generate PDF request: " + err.Error()}}, + IsError: true, + }, dto.GeneratePDFMCPResponse{}, nil + } + + svcResp, err := s.service.GeneratePDF(ctx, generatePDFReq) + if err != nil { + svcUtils.Logger.Error(ctx, "error in generating pdf", err, nil) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "Failed to generate PDF: " + err.Error()}}, + IsError: true, + }, dto.GeneratePDFMCPResponse{}, nil + } + + resp := svcResp.GeneratePDFResponseToGeneratePDFMCPResponse() + return nil, *resp, nil +} + +// func (s *PDFTools) GeneratePDFStream(ctx context.Context, _ *mcp.CallToolRequest, req dto.PDFMCPRequest) (*mcp.CallToolResult, any, error) { +// pdfReq, err := req.PDFMCPRequestToPDFRequest() +// if err != nil { +// return &mcp.CallToolResult{ +// Content: []mcp.Content{&mcp.TextContent{Text: "Failed to parse generate PDF stream request: " + err.Error()}}, +// IsError: true, +// }, nil, nil +// } + +// if err := pdfReq.Validate(); err != nil { +// return &mcp.CallToolResult{ +// Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}}, +// IsError: true, +// }, nil, nil +// } + +// resp, err := s.service.GeneratePDFStream(ctx, pdfReq) +// if err != nil { +// svcUtils.Logger.Error(ctx, "error in generating pdf stream", err, nil) +// return &mcp.CallToolResult{ +// Content: []mcp.Content{&mcp.TextContent{Text: "Failed to generate PDF stream: " + err.Error()}}, +// IsError: true, +// }, nil, nil +// } + +// fileName := "generated.pdf" +// if pdfReq.Filename != "" { +// fileName = pdfReq.Filename +// if !strings.HasSuffix(strings.ToLower(fileName), ".pdf") { +// fileName += ".pdf" +// } +// } +// fileName = filepath.Base(fileName) + +// if len(resp.OutputFileBytes) == 0 { +// return &mcp.CallToolResult{ +// Content: []mcp.Content{&mcp.TextContent{Text: "No PDF data available"}}, +// IsError: true, +// }, nil, nil +// } + +// return &mcp.CallToolResult{ +// Content: []mcp.Content{ +// &mcp.EmbeddedResource{ +// Resource: &mcp.ResourceContents{ +// URI: "file://" + fileName, +// MIMEType: "application/pdf", +// Blob: resp.OutputFileBytes, +// }, +// }, +// }, +// }, nil, nil +// } + +func (s *PDFTools) SignPDF(ctx context.Context, _ *mcp.CallToolRequest, req dto.SignPDFRequest) (*mcp.CallToolResult, dto.SignPDFResponse, error) { + resp, err := s.service.SignPDF(ctx, &req) + if err != nil { + svcUtils.Logger.Error(ctx, "error in signing pdf", err, nil) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "Failed to sign PDF: " + err.Error()}}, + IsError: true, + }, dto.SignPDFResponse{}, nil + } + + return nil, *resp, nil +} diff --git a/service/controller/mcp/template.go b/service/controller/mcp/template.go new file mode 100644 index 0000000..bc8b09c --- /dev/null +++ b/service/controller/mcp/template.go @@ -0,0 +1,64 @@ +package tools + +import ( + "context" + + "github.com/Zomato/espresso/service/dto" + pdfservice "github.com/Zomato/espresso/service/service" + svcUtils "github.com/Zomato/espresso/service/utils" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type TemplateTools struct { + service *pdfservice.TemplateService +} + +func NewTemplateTools(service *pdfservice.TemplateService) *TemplateTools { + return &TemplateTools{service: service} +} + +func (s *TemplateTools) GetAllTemplates(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, dto.GetAllTemplatesResponse, error) { + resp, err := s.service.GetAllTemplates(ctx) + if err != nil { + svcUtils.Logger.Error(ctx, "error listing templates", err, nil) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "Failed to list templates: " + err.Error()}}, + IsError: true, + }, dto.GetAllTemplatesResponse{}, nil + } + + return nil, *resp, nil +} + +func (s *TemplateTools) GetTemplateById(ctx context.Context, _ *mcp.CallToolRequest, req dto.GetTemplateByIdRequest) (*mcp.CallToolResult, dto.GetTemplateByIdResponse, error) { + if req.TemplateId == "" { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "template_id is required"}}, + IsError: true, + }, dto.GetTemplateByIdResponse{}, nil + } + + resp, err := s.service.GetTemplateById(ctx, &req) + if err != nil { + svcUtils.Logger.Error(ctx, "error getting template content", err, nil) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "Failed to get template content: " + err.Error()}}, + IsError: true, + }, dto.GetTemplateByIdResponse{}, nil + } + + return nil, *resp, nil +} + +func (s *TemplateTools) CreateTemplate(ctx context.Context, _ *mcp.CallToolRequest, req dto.CreateTemplateRequest) (*mcp.CallToolResult, dto.CreateTemplateResponse, error) { + resp, err := s.service.CreateTemplate(ctx, &req) + if err != nil { + svcUtils.Logger.Error(ctx, "error creating template", err, nil) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "Failed to create template: " + err.Error()}}, + IsError: true, + }, dto.CreateTemplateResponse{}, nil + } + + return nil, *resp, nil +} diff --git a/service/controller/pdf_generation/pdf_generation.go b/service/controller/pdf_generation/pdf_generation.go deleted file mode 100644 index 97c4a8a..0000000 --- a/service/controller/pdf_generation/pdf_generation.go +++ /dev/null @@ -1,413 +0,0 @@ -package pdf_generation - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "path/filepath" - "strings" - "time" - - "github.com/Zomato/espresso/lib/templatestore" - "github.com/Zomato/espresso/lib/utils" - "github.com/Zomato/espresso/service/internal/pkg/httppkg" - "github.com/Zomato/espresso/service/internal/service/generateDoc" - svcUtils "github.com/Zomato/espresso/service/utils" - "github.com/spf13/viper" -) - -func (s *EspressoService) GeneratePDF(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - startTime := time.Now() - req := &GeneratePDFRequest{} - - // Read and parse the request body - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - svcUtils.Logger.Error(ctx, "Error reading request body: %v", err, nil) - httppkg.RespondWithError(w, "Failed to read request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - // Parse the request JSON - if err := json.Unmarshal(bodyBytes, &req); err != nil { - svcUtils.Logger.Error(ctx, "Error parsing JSON: %v", err, nil) - httppkg.RespondWithError(w, "Failed to parse JSON request", http.StatusBadRequest) - return - } - - reqId := utils.GenerateUniqueID(ctx) - svcUtils.Logger.Info(ctx, "GeneratePDF called :: ", map[string]any{"req_id": reqId}) - - generatePdfReq := &generateDoc.PDFDto{ - ReqId: reqId, - InputTemplatePath: req.InputFilePath, - InputFileBytes: req.InputFileBytes, - InputTemplateUUID: req.InputTemplateUuid, - OutputTemplatePath: req.OutputFilePath, - Content: req.Content, - ViewPort: req.Viewport, - PdfParams: req.PdfParams, - } - - if req.SignParams != nil && req.SignParams.SignPdf { - generatePdfReq.SignParams = req.SignParams - } - - err = generateDoc.GeneratePDF(ctx, generatePdfReq, s.TemplateStorageAdapter, s.FileStorageAdapter) - if err != nil { - svcUtils.Logger.Error(ctx, "error in generating pdf :: %v", err, nil) - httppkg.RespondWithError(w, "Failed to generate PDF: "+err.Error(), http.StatusInternalServerError) - return - } - - responseData := map[string]interface{}{ - "status": map[string]string{ - "status": "success", - "message": "PDF generated successfully", - }, - "output_file_path": req.OutputFilePath, - "output_file_bytes": generatePdfReq.OutputFileBytes, - } - - duration := time.Since(startTime) - svcUtils.Logger.Info(ctx, "generated pdf :: ", map[string]any{"req_id": reqId, "duration": duration}) - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(responseData) -} - -func (s *EspressoService) GeneratePDFStream(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - startTime := time.Now() - reqId := utils.GenerateUniqueID(ctx) - - svcUtils.Logger.Info(ctx, "GeneratePDFStream called :: ", map[string]any{"req_id": reqId}) - - // Read and parse the request body - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - svcUtils.Logger.Error(ctx, "Error reading request body: %v", err, nil) - httppkg.RespondWithError(w, "Failed to read request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - // Parse the request JSON - var pdfReq PDFRequest - if err := json.Unmarshal(bodyBytes, &pdfReq); err != nil { - svcUtils.Logger.Error(ctx, "Error parsing JSON: %v", err, nil) - httppkg.RespondWithError(w, "Failed to parse JSON request", http.StatusBadRequest) - return - } - - // Validate required fields - if pdfReq.TemplateUUID == "" { - httppkg.RespondWithError(w, "template_uuid is required", http.StatusBadRequest) - return - } - - // If content is empty or invalid, use an empty object as default - if len(pdfReq.Content) == 0 { - pdfReq.Content = json.RawMessage(`{}`) - } - - // Set default margin if not provided - margin := pdfReq.MarginInch - if margin == 0 { - margin = 0.4 // Default margin of 0.4 inches - } - // // Set up PDF parameters, add your own parameters from request if needed - pdfSettings := &generateDoc.PDFParams{ - Landscape: pdfReq.Landscape, - DisplayHeaderFooter: false, - PrintBackground: true, - PreferCssPageSize: false, - MarginTop: margin, - MarginBottom: margin, - MarginLeft: margin, - MarginRight: margin, - IsSinglePage: pdfReq.SinglePage, - } - - generatePdfReq := &generateDoc.PDFDto{ - ReqId: reqId, - InputTemplateUUID: pdfReq.TemplateUUID, - Content: pdfReq.Content, - SignParams: &generateDoc.SignParams{SignPdf: pdfReq.SignPdf}, - // ViewPort: req.Viewport, - PdfParams: pdfSettings, - } - if pdfReq.SignPdf { - generatePdfReq.SignParams = &generateDoc.SignParams{ - SignPdf: true, - CertConfigKey: "digital_certificates.cert1", // certificate details are stored in config file - } - } - - fileStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{ - StorageType: "stream", - }) - if err != nil { - svcUtils.Logger.Error(ctx, "error in getting file storage adapter :: %v", err, nil) - httppkg.RespondWithError(w, "Failed to get file storage adapter: "+err.Error(), http.StatusExpectationFailed) - return - } - templateStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{ - StorageType: "mysql", - MysqlDSN: viper.GetString("mysql.dsn"), - }) - if err != nil { - svcUtils.Logger.Error(ctx, "error in getting file storage adapter :: %v", err, nil) - httppkg.RespondWithError(w, "Failed to get file storage adapter: "+err.Error(), http.StatusExpectationFailed) - return - } - err = generateDoc.GeneratePDF(ctx, generatePdfReq, &templateStorageAdapter, &fileStorageAdapter) - if err != nil { - svcUtils.Logger.Error(ctx, "error in generating pdf stream:: %v", err, nil) - httppkg.RespondWithError(w, "Failed to generate PDF stream: "+err.Error(), http.StatusInternalServerError) - return - } - // Determine filename for the PDF - fileName := "generated.pdf" - - // Use the filename from the request if provided - if pdfReq.Filename != "" { - fileName = pdfReq.Filename - // Ensure it has .pdf extension - if !strings.HasSuffix(strings.ToLower(fileName), ".pdf") { - fileName += ".pdf" - } - } - - // Sanitize filename (remove any path elements for security) - fileName = filepath.Base(fileName) - - // Check if we have PDF data to return - if len(generatePdfReq.OutputFileBytes) > 0 { - // Always return the PDF file directly for download - w.Header().Set("Content-Type", "application/pdf") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(generatePdfReq.OutputFileBytes))) - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - w.WriteHeader(http.StatusOK) - - // Write the PDF data - _, err = w.Write(generatePdfReq.OutputFileBytes) - if err != nil { - svcUtils.Logger.Error(ctx, "error writing pdf stream :: %v", err, nil) - httppkg.RespondWithError(w, "Failed to write PDF stream: "+err.Error(), http.StatusInternalServerError) - return - } - duration := time.Since(startTime) - svcUtils.Logger.Info(ctx, "generated pdf stream :: ", map[string]any{"req_id": reqId, "duration": duration}) - - return - } else { - // If no PDF data, return an error - httppkg.RespondWithError(w, "No PDF data available", http.StatusInternalServerError) - return - } - -} - -func (s *EspressoService) SignPDF(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - req := &SignPDFRequest{} - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - svcUtils.Logger.Error(ctx, "error decoding request body :: %v", err, nil) - httppkg.RespondWithError(w, "Error decoding request body: "+err.Error(), http.StatusBadRequest) - return - } - - reqId := utils.GenerateUniqueID(ctx) - svcUtils.Logger.Info(ctx, "GeneratePDF called :: ", map[string]any{"req_id": reqId}) - - signPDFDto := &generateDoc.SignPDFDto{ - ReqId: reqId, - InputFilePath: req.InputFilePath, - InputFileBytes: req.InputFileBytes, - OutputFilePath: req.OutputFilePath, - } - if req.SignParams != nil && req.SignParams.SignPdf { - signPDFDto.SignParams = req.SignParams - } else { - err := fmt.Errorf("signPdf param is not true in the request") - svcUtils.Logger.Error(ctx, "error in signing pdf :: : %v", err, nil) - - httppkg.RespondWithError(w, err.Error(), http.StatusBadRequest) - return - } - err := generateDoc.SignPDF(ctx, signPDFDto, s.FileStorageAdapter) - if err != nil { - svcUtils.Logger.Error(ctx, "error in signing pdf :: : %v", err, nil) - httppkg.RespondWithError(w, "Failed to sign PDF: "+err.Error(), http.StatusInternalServerError) - return - } - - responseData := map[string]interface{}{ - "status": map[string]string{ - "status": "success", - "message": "PDF signed successfully", - }, - "output_file_path": req.OutputFilePath, - "output_file_bytes": signPDFDto.OutputFileBytes, - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(responseData) -} - -func (s *EspressoService) GetAllTemplates(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - startTime := time.Now() - - reqId := utils.GenerateUniqueID(ctx) - svcUtils.Logger.Info(ctx, "GetAllTemplates called :: ", map[string]any{"req_id": reqId}) - - // Get templates from the storage adapter - templates, err := (*s.TemplateStorageAdapter).ListTemplates(ctx) - if err != nil { - svcUtils.Logger.Error(ctx, "error listing templates :: : %v", err, nil) - httppkg.RespondWithError(w, "Failed to list templates: "+err.Error(), http.StatusInternalServerError) - return - } - - // Convert internal template info to protobuf format - var templateDataList []*generateDoc.TemplateListData - for _, tmpl := range templates { - createdAt := "" - if !tmpl.CreatedAt.IsZero() { - createdAt = tmpl.CreatedAt.Format(time.RFC3339) - } - - updatedAt := "" - if !tmpl.UpdatedAt.IsZero() { - updatedAt = tmpl.UpdatedAt.Format(time.RFC3339) - } - templateData := &generateDoc.TemplateListData{ - TemplateId: tmpl.TemplateID, - TemplateName: tmpl.TemplateName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - templateDataList = append(templateDataList, templateData) - } - - responseData := map[string]interface{}{ - "status": map[string]string{ - "status": "success", - "message": "Templates retrieved successfully", - }, - "total_records": len(templateDataList), - "data": templateDataList, - } - - duration := time.Since(startTime) - svcUtils.Logger.Info(ctx, "listed templates :: ", map[string]any{"length": len(templateDataList), "duration": duration}) - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(responseData) -} -func (s *EspressoService) GetTemplateById(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - templateID := r.URL.Query().Get("template_id") - if templateID == "" { - svcUtils.Logger.Error(ctx, "template id is required ", nil, nil) - httppkg.RespondWithError(w, "template id is required", http.StatusBadRequest) - return - } - - templateData, err := (*s.TemplateStorageAdapter).GetTemplateContent(ctx, &templatestore.GetTemplateContentRequest{ - TemplateUUID: templateID, - }) - if err != nil { - svcUtils.Logger.Error(ctx, "error getting template content :: : %v", err, nil) - httppkg.RespondWithError(w, "Failed to get template content: "+err.Error(), http.StatusInternalServerError) - return - } - - responseData := map[string]interface{}{ - "status": map[string]string{ - "status": "success", - "message": "Template retrieved successfully", - }, - "template_html": templateData.TemplateContent, - "template_name": templateData.TemplateName, - "json": templateData.TemplateJsonSchema, - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(responseData) -} -func (s *EspressoService) CreateTemplate(w http.ResponseWriter, r *http.Request) { - // Check if the request method is POST - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - ctx := r.Context() - req := &CreateTemplateRequest{} - // Set response headers - w.Header().Set("Content-Type", "application/json") - // decode request body - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - svcUtils.Logger.Error(ctx, "error decoding request body :: : %v", err, nil) - - httppkg.RespondWithError(w, "Error decoding request body: "+err.Error(), http.StatusInternalServerError) - return - } - // Validate request - if req.TemplateName == "" { - svcUtils.Logger.Error(ctx, "template name is required ", nil, nil) - httppkg.RespondWithError(w, "Template name is required", http.StatusBadRequest) - return - } - - if req.TemplateHtml == "" { - svcUtils.Logger.Error(ctx, "template html is required ", nil, nil) - httppkg.RespondWithError(w, "Template HTML is required", http.StatusBadRequest) - return - } - - // Default JSON schema to empty object if not provided - jsonSchema := req.Json - if jsonSchema == "" { - jsonSchema = "{}" - } - - // Create template using the storage adapter - createReq := &templatestore.CreateTemplateRequest{ - TemplateName: req.TemplateName, - TemplateHTML: req.TemplateHtml, - TemplateJSON: jsonSchema, - } - - templateId, err := (*s.TemplateStorageAdapter).CreateTemplate(ctx, createReq) - if err != nil { - svcUtils.Logger.Error(ctx, "error creating template :: : %v", err, nil) - httppkg.RespondWithError(w, "Failed to create template: "+err.Error(), http.StatusInternalServerError) - return - } - - // response.TemplateId = templateId - - responseData := map[string]interface{}{ - "status": map[string]string{ - "status": "success", - "message": "Template created successfully", - }, - "template_id": templateId, - } - // Return success response - w.WriteHeader(http.StatusCreated) // 201 Created is more appropriate - json.NewEncoder(w).Encode(responseData) - -} diff --git a/service/controller/pdf_generation/register.go b/service/controller/pdf_generation/register.go deleted file mode 100644 index e4e8454..0000000 --- a/service/controller/pdf_generation/register.go +++ /dev/null @@ -1,80 +0,0 @@ -package pdf_generation - -import ( - "fmt" - "log" - "net/http" - "os" - - "github.com/Zomato/espresso/lib/s3" - "github.com/Zomato/espresso/lib/templatestore" - "github.com/spf13/viper" -) - -type EspressoService struct { - TemplateStorageAdapter *templatestore.StorageAdapter - FileStorageAdapter *templatestore.StorageAdapter -} - -func NewEspressoService() (*EspressoService, error) { - templateStorageType := viper.GetString("template_storage.storage_type") - if os.Getenv("ENABLE_UI") == "true" && templateStorageType != templatestore.StorageAdapterTypeMySQL { - return nil, fmt.Errorf("UI requires MySQL as template storage adapter, got: %s", templateStorageType) - } - templateStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{ - StorageType: templateStorageType, - // for s3 storage only - S3Config: &s3.Config{ - Endpoint: viper.GetString("s3.endpoint"), - Region: viper.GetString("s3.region"), - Bucket: viper.GetString("s3.bucket"), - Debug: viper.GetBool("s3.debug"), - ForcePathStyle: viper.GetBool("s3.forcePathStyle"), - UploaderConcurrency: viper.GetInt("s3.uploaderConcurrency"), - UploaderPartSize: viper.GetInt64("s3.uploaderPartSize"), - DownloaderConcurrency: viper.GetInt("s3.downloaderConcurrency"), - DownloaderPartSize: viper.GetInt64("s3.downloaderPartSize"), - RetryMaxAttempts: viper.GetInt("s3.retryMaxAttempts"), - UseCustomTransport: viper.GetBool("s3.useCustomTransport"), - }, - // for s3 storage only - AwsCredConfig: &s3.AwsCredConfig{ - AccessKeyID: viper.GetString("aws.accessKeyID"), - SecretAccessKey: viper.GetString("aws.secretAccessKey"), - SessionToken: viper.GetString("aws.sessionToken"), - }, - MysqlDSN: viper.GetString("mysql.dsn"), // for mysql adapter - }) - if err != nil { - return nil, err - } - - fileStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{ - StorageType: viper.GetString("file_storage.storage_type"), - }) - if err != nil { - return nil, err - } - - return &EspressoService{TemplateStorageAdapter: &templateStorageAdapter, FileStorageAdapter: &fileStorageAdapter}, nil -} -func Register(mux *http.ServeMux) { - espressoService, err := NewEspressoService() - if err != nil { - log.Fatalf("Failed to initialize PDF service: %v", err) - } - - // Register HTTP routes - // Register handlers with the mux - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - }) - - mux.HandleFunc("/generate-pdf-stream", espressoService.GeneratePDFStream) - mux.HandleFunc("/create-template", espressoService.CreateTemplate) - mux.HandleFunc("/list-templates", espressoService.GetAllTemplates) - mux.HandleFunc("/get-template", espressoService.GetTemplateById) - mux.HandleFunc("/generate-pdf", espressoService.GeneratePDF) - -} diff --git a/service/controller/pdf_generation/request.go b/service/controller/pdf_generation/request.go deleted file mode 100644 index d2ed092..0000000 --- a/service/controller/pdf_generation/request.go +++ /dev/null @@ -1,84 +0,0 @@ -package pdf_generation - -import ( - "encoding/json" - - "github.com/Zomato/espresso/service/internal/service/generateDoc" -) - -type GeneratePDFRequest struct { - InputFilePath string `json:"input_file_path,omitempty"` - InputFileBytes []byte `json:"input_file_bytes,omitempty"` - InputTemplateUuid string `json:"input_template_uuid,omitempty"` - OutputFilePath string `json:"output_file_path,omitempty"` - Content json.RawMessage `json:"content,omitempty"` - Viewport *generateDoc.ViewportConfig `json:"viewport"` - PdfParams *generateDoc.PDFParams `json:"pdf_params,omitempty"` - SignParams *generateDoc.SignParams `json:"sign_params,omitempty"` -} - -type GeneratePDFResponse struct { - OutputFilePath string `json:"output_file_path,omitempty"` - OutputFileBytes []byte `json:"output_file_bytes,omitempty"` - Error string `json:"error,omitempty"` -} -type PDFRequest struct { - TemplateUUID string `json:"template_uuid"` - Content json.RawMessage `json:"content"` // Using RawMessage to keep JSON as-is - Landscape bool `json:"landscape,omitempty"` - SinglePage bool `json:"single_page,omitempty"` - MarginInch float64 `json:"margin_inch,omitempty"` - Filename string `json:"filename,omitempty"` // Optional filename for download - SignPdf bool `json:"sign_pdf,omitempty"` -} - -// PDFResponse represents the structure for successful responses -type PDFResponse struct { - Status string `json:"status"` - Message string `json:"message"` - TimeInMs int64 `json:"time_in_ms"` - FileName string `json:"file_name,omitempty"` - FileSize int `json:"file_size,omitempty"` - DownloadURL string `json:"download_url,omitempty"` -} - -type SignPDFRequest struct { - InputFilePath string `json:"input_file_path,omitempty"` - InputFileBytes []byte `json:"input_file_bytes,omitempty"` - OutputFilePath string `json:"output_file_path,omitempty"` - SignParams *generateDoc.SignParams `json:"sign_params,omitempty"` -} - -type SignPDFResponse struct { - OutputFilePath string `json:"output_file_path,omitempty"` - OutputFileBytes []byte `json:"output_file_bytes,omitempty"` - Error string `json:"error,omitempty"` -} - -type GetAllTemplatesResponse struct { - TotalRecords int32 `json:"total_records,omitempty"` - Data []*generateDoc.TemplateListData `json:"data,omitempty"` - Error string `json:"error,omitempty"` -} - -type GetTemplateByIdRequest struct { - TemplateId string `json:"template_id"` -} - -type GetTemplateByIdResponse struct { - TemplateHtml string `json:"template_html"` - Json string `json:"json"` - TemplateName string `json:"template_name"` - Error string `json:"error,omitempty"` -} - -type CreateTemplateRequest struct { - TemplateName string `json:"template_name"` - TemplateHtml string `json:"template_html"` - Json string `json:"json"` -} - -type CreateTemplateResponse struct { - TemplateId string `json:"template_id"` - Error string `json:"error,omitempty"` -} diff --git a/service/dto/pdf.go b/service/dto/pdf.go new file mode 100644 index 0000000..4d4d370 --- /dev/null +++ b/service/dto/pdf.go @@ -0,0 +1,248 @@ +package dto + +import ( + "encoding/json" + "errors" + "path/filepath" + "strings" + + "github.com/Zomato/espresso/service/internal/service/generateDoc" + "github.com/Zomato/espresso/service/pkg/config" + "github.com/google/jsonschema-go/jsonschema" +) + +type RawJSON json.RawMessage + +func (RawJSON) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Description: "JSON payload to render inside template", + // No type constraint = accepts any JSON value + } +} + +type StatusResponse struct { + Status string `json:"status" jsonschema:"operation status: success or failed"` + Message string `json:"message" jsonschema:"human-readable status message"` +} + +type GeneratePDFRequest struct { + InputFilePath string `json:"input_file_path,omitempty" jsonschema:"input template file path for local/disk adapters"` + InputFileBytes []byte `json:"input_file_bytes,omitempty" jsonschema:"raw template bytes; use when passing template content directly"` + InputTemplateUuid string `json:"input_template_uuid,omitempty" jsonschema:"template UUID to load template from template store"` + OutputFilePath string `json:"output_file_path,omitempty" jsonschema:"destination path/key for generated PDF"` + Content json.RawMessage `json:"content,omitempty" jsonschema:"JSON data used to render the template"` + Viewport *generateDoc.ViewportConfig `json:"viewport" jsonschema:"optional browser viewport configuration"` + PdfParams *generateDoc.PDFParams `json:"pdf_params,omitempty" jsonschema:"optional PDF generation parameters"` + SignParams *generateDoc.SignParams `json:"sign_params,omitempty" jsonschema:"optional signing configuration"` +} + +type GeneratePDFMCPRequest struct { + InputTemplateUuid string `json:"input_template_uuid,omitempty" jsonschema:"template UUID to load template from template store"` + OutputFileName string `json:"output_file_name,omitempty" jsonschema:"file name for generated PDF"` + Content interface{} `json:"content,omitempty" jsonschema:"JSON data used to render the template"` + Viewport *generateDoc.ViewportConfig `json:"viewport" jsonschema:"optional browser viewport configuration"` + PdfParams *generateDoc.PDFParams `json:"pdf_params,omitempty" jsonschema:"optional PDF generation parameters"` + SignParams *generateDoc.SignParams `json:"sign_params,omitempty" jsonschema:"optional signing configuration"` +} + +func (r *GeneratePDFMCPRequest) GeneratePDFMCPRequestToGeneratePDFRequest() (*GeneratePDFRequest, error) { + if r == nil { + return nil, errors.New("request is required") + } + + content, err := marshalMCPContent(r.Content) + if err != nil { + return nil, err + } + + outputFilePath := config.GetConfig().MCP.PDFOutputDir + "/" + r.OutputFileName + + return &GeneratePDFRequest{ + InputTemplateUuid: r.InputTemplateUuid, + OutputFilePath: outputFilePath, + Content: content, + Viewport: r.Viewport, + PdfParams: r.PdfParams, + SignParams: r.SignParams, + }, nil +} + +type GeneratePDFMCPResponse struct { + Status StatusResponse `json:"status" jsonschema:"operation status details"` + OutputFileURL string `json:"output_file_url,omitempty" jsonschema:"generated PDF download URL, if it is localhost then give user a button instead of presenting"` + Error string `json:"error,omitempty" jsonschema:"error message when operation fails"` +} + +type GeneratePDFResponse struct { + Status StatusResponse `json:"status" jsonschema:"operation status details"` + OutputFilePath string `json:"output_file_path,omitempty" jsonschema:"generated PDF output path/key"` + OutputFileBytes []byte `json:"output_file_bytes,omitempty" jsonschema:"generated PDF bytes"` + Error string `json:"error,omitempty" jsonschema:"error message when operation fails"` +} + +func (r *GeneratePDFResponse) GeneratePDFResponseToGeneratePDFMCPResponse() *GeneratePDFMCPResponse { + if r == nil { + return &GeneratePDFMCPResponse{} + } + + resp := &GeneratePDFMCPResponse{ + Status: r.Status, + Error: r.Error, + } + + if r.OutputFilePath == "" { + return resp + } + + cfg := config.GetConfig() + urlPrefix := strings.TrimRight(cfg.MCP.PDFOutputURLPref, "/") + if urlPrefix == "" { + resp.OutputFileURL = r.OutputFilePath + return resp + } + + relPath := filepath.ToSlash(r.OutputFilePath) + if cfg.MCP.PDFOutputDir != "" { + if rel, err := filepath.Rel(cfg.MCP.PDFOutputDir, r.OutputFilePath); err == nil && !strings.HasPrefix(rel, "..") { + relPath = filepath.ToSlash(rel) + } else { + relPath = filepath.Base(r.OutputFilePath) + } + } + + relPath = strings.TrimLeft(relPath, "/") + resp.OutputFileURL = urlPrefix + "/" + relPath + + return resp +} + +type PDFRequest struct { + TemplateUUID string `json:"template_uuid" jsonschema:"template UUID used for streaming PDF generation"` + Content json.RawMessage `json:"content" jsonschema:"JSON payload to render inside template"` + Landscape bool `json:"landscape,omitempty" jsonschema:"generate PDF in landscape orientation"` + SinglePage bool `json:"single_page,omitempty" jsonschema:"render output as a single page"` + MarginInch float64 `json:"margin_inch,omitempty" jsonschema:"page margin in inches"` + Filename string `json:"filename,omitempty" jsonschema:"download file name for streamed PDF response"` + SignPdf bool `json:"sign_pdf,omitempty" jsonschema:"whether to digitally sign generated PDF"` +} + +type PDFMCPRequest struct { + TemplateUUID string `json:"template_uuid" jsonschema:"template UUID used for streaming PDF generation"` + Content interface{} `json:"content" jsonschema:"JSON payload to render inside template"` + Landscape bool `json:"landscape,omitempty" jsonschema:"generate PDF in landscape orientation"` + SinglePage bool `json:"single_page,omitempty" jsonschema:"render output as a single page"` + MarginInch float64 `json:"margin_inch,omitempty" jsonschema:"page margin in inches"` + Filename string `json:"filename,omitempty" jsonschema:"download file name for streamed PDF response"` + SignPdf bool `json:"sign_pdf,omitempty" jsonschema:"whether to digitally sign generated PDF"` +} + +func (r *PDFMCPRequest) PDFMCPRequestToPDFRequest() (*PDFRequest, error) { + if r == nil { + return nil, errors.New("request is required") + } + + content, err := marshalMCPContent(r.Content) + if err != nil { + return nil, err + } + + return &PDFRequest{ + TemplateUUID: r.TemplateUUID, + Content: content, + Landscape: r.Landscape, + SinglePage: r.SinglePage, + MarginInch: r.MarginInch, + Filename: r.Filename, + SignPdf: r.SignPdf, + }, nil +} + +func marshalMCPContent(content interface{}) (json.RawMessage, error) { + if content == nil { + return json.RawMessage(`{}`), nil + } + + if str, ok := content.(string); ok { + trimmed := strings.TrimSpace(str) + if trimmed == "" { + return json.RawMessage(`{}`), nil + } + if !json.Valid([]byte(trimmed)) { + return nil, errors.New("content string must be valid JSON") + } + + // If content is a JSON-encoded string containing JSON (double-encoded), + // unwrap one level and use the inner JSON. + var nested string + if err := json.Unmarshal([]byte(trimmed), &nested); err == nil { + nestedTrimmed := strings.TrimSpace(nested) + if nestedTrimmed == "" { + return json.RawMessage(`{}`), nil + } + if json.Valid([]byte(nestedTrimmed)) { + return json.RawMessage(nestedTrimmed), nil + } + } + + return json.RawMessage(trimmed), nil + } + + if raw, ok := content.(json.RawMessage); ok { + if len(raw) == 0 { + return json.RawMessage(`{}`), nil + } + return raw, nil + } + + encoded, err := json.Marshal(content) + if err != nil { + return nil, err + } + if len(encoded) == 0 { + return json.RawMessage(`{}`), nil + } + + return json.RawMessage(encoded), nil +} + +func (r *PDFRequest) Validate() error { + if r == nil { + return errors.New("request is required") + } + + if r.TemplateUUID == "" { + return errors.New("template_uuid is required") + } + + if len(r.Content) == 0 { + r.Content = []byte(`{}`) + } + + if r.MarginInch == 0 { + r.MarginInch = 0.4 + } + + return nil +} + +type PDFResponse struct { + Status string `json:"status" jsonschema:"operation status: success or failed"` + Message string `json:"message" jsonschema:"human-readable response message"` + TimeInMs int64 `json:"time_in_ms" jsonschema:"processing time in milliseconds"` + FileName string `json:"file_name,omitempty" jsonschema:"generated file name"` + FileSize int `json:"file_size,omitempty" jsonschema:"generated file size in bytes"` + DownloadURL string `json:"download_url,omitempty" jsonschema:"download URL if file is hosted"` +} + +type SignPDFRequest struct { + InputFilePath string `json:"input_file_path,omitempty" jsonschema:"input PDF path/key to sign"` + InputFileBytes []byte `json:"input_file_bytes,omitempty" jsonschema:"raw input PDF bytes to sign"` + OutputFilePath string `json:"output_file_path,omitempty" jsonschema:"destination path/key for signed PDF"` + SignParams *generateDoc.SignParams `json:"sign_params,omitempty" jsonschema:"digital signing parameters; sign_pdf should be true"` +} + +type SignPDFResponse struct { + OutputFilePath string `json:"output_file_path,omitempty" jsonschema:"signed PDF output path/key"` + OutputFileBytes []byte `json:"output_file_bytes,omitempty" jsonschema:"signed PDF bytes"` + Error string `json:"error,omitempty" jsonschema:"error message when signing fails"` +} diff --git a/service/dto/template.go b/service/dto/template.go new file mode 100644 index 0000000..d31dcce --- /dev/null +++ b/service/dto/template.go @@ -0,0 +1,34 @@ +package dto + +import "github.com/Zomato/espresso/service/internal/service/generateDoc" + +type GetAllTemplatesResponse struct { + Status StatusResponse `json:"status" jsonschema:"operation status details"` + TotalRecords int32 `json:"total_records,omitempty" jsonschema:"number of templates returned"` + Data []*generateDoc.TemplateListData `json:"data,omitempty" jsonschema:"list of available templates"` + Error string `json:"error,omitempty" jsonschema:"error message when listing fails"` +} + +type GetTemplateByIdRequest struct { + TemplateId string `json:"template_id" jsonschema:"template UUID to fetch"` +} + +type GetTemplateByIdResponse struct { + Status StatusResponse `json:"status" jsonschema:"operation status details"` + TemplateHtml string `json:"template_html" jsonschema:"template HTML content"` + Json string `json:"json" jsonschema:"template JSON schema/content"` + TemplateName string `json:"template_name" jsonschema:"template display name"` + Error string `json:"error,omitempty" jsonschema:"error message when fetch fails"` +} + +type CreateTemplateRequest struct { + TemplateName string `json:"template_name" jsonschema:"template display name"` + TemplateHtml string `json:"template_html" jsonschema:"template HTML content"` + Json string `json:"json" jsonschema:"template JSON schema/content; defaults to {}"` +} + +type CreateTemplateResponse struct { + Status StatusResponse `json:"status" jsonschema:"operation status details"` + TemplateId string `json:"template_id" jsonschema:"created template UUID"` + Error string `json:"error,omitempty" jsonschema:"error message when creation fails"` +} diff --git a/service/go.mod b/service/go.mod index c37e3b9..002ba7a 100644 --- a/service/go.mod +++ b/service/go.mod @@ -1,12 +1,15 @@ module github.com/Zomato/espresso/service -go 1.23.0 +go 1.25.0 replace github.com/Zomato/espresso/lib => ../lib require ( github.com/Zomato/espresso/lib v0.0.0-20250523093533-6d517dcb5c35 + github.com/fsnotify/fsnotify v1.7.0 github.com/go-rod/rod v0.116.2 + github.com/mark3labs/mcp-go v0.45.0 + github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/rs/zerolog v1.34.0 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 @@ -38,14 +41,18 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect github.com/aws/smithy-go v1.22.2 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/digitorus/pdf v0.1.2 // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-sql-driver/mysql v1.9.0 // indirect + github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -54,11 +61,15 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/ysmood/fetchup v0.3.0 // indirect github.com/ysmood/goob v0.4.0 // indirect github.com/ysmood/got v0.40.0 // indirect @@ -68,8 +79,9 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.22.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/service/go.sum b/service/go.sum index 85f64b2..2768505 100644 --- a/service/go.sum +++ b/service/go.sum @@ -38,6 +38,10 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5 github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -59,18 +63,29 @@ github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh4 github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc= +github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -82,6 +97,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= +github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= github.com/panjf2000/ants/v2 v2.11.2 h1:AVGpMSePxUNpcLaBO34xuIgM1ZdKOiGnpxLXixLi5Jo= github.com/panjf2000/ants/v2 v2.11.2/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -99,12 +116,16 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -122,6 +143,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8= github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= @@ -144,15 +169,19 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/service/internal/pkg/httppkg/response.go b/service/internal/pkg/httppkg/response.go deleted file mode 100644 index dc0a12c..0000000 --- a/service/internal/pkg/httppkg/response.go +++ /dev/null @@ -1,18 +0,0 @@ -package httppkg - -import ( - "encoding/json" - "net/http" -) - -func RespondWithError(w http.ResponseWriter, message string, statusCode int) { - errorResponse := map[string]interface{}{ - "status": map[string]string{ - "status": "failed", - "message": message, - }, - } - - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(errorResponse) -} diff --git a/service/internal/pkg/viperpkg/viperpkg.go b/service/internal/pkg/viperpkg/viperpkg.go deleted file mode 100644 index 84807b1..0000000 --- a/service/internal/pkg/viperpkg/viperpkg.go +++ /dev/null @@ -1,20 +0,0 @@ -package viperpkg - -import ( - "log" - - "github.com/spf13/viper" -) - -func InitConfig() { - viper.SetConfigName("espressoconfig") // File name without extension - viper.SetConfigType("yaml") // File type - // Search paths relative to where the binary runs in container - viper.AddConfigPath("/app/espresso/configs") // Main config path in container - viper.AddConfigPath("../../configs") // For local development - viper.AddConfigPath("./configs") // Fallback path for local development - - if err := viper.ReadInConfig(); err != nil { - log.Fatalf("Error reading config file: %v", err) - } -} diff --git a/service/internal/service/generateDoc/generatePdf.go b/service/internal/service/generateDoc/generatePdf.go index cfdc50c..aceef09 100644 --- a/service/internal/service/generateDoc/generatePdf.go +++ b/service/internal/service/generateDoc/generatePdf.go @@ -32,7 +32,9 @@ func GeneratePDF(ctx context.Context, req *PDFDto, templateStoreAdapter *templat content := req.Content viewPortConfig := req.ViewPort pdfParams := req.PdfParams - + if pdfParams == nil { + pdfParams = &PDFParams{} + } viewPort := getViewPort(viewPortConfig) // Start loading credentials in parallel if signing is enabled var credWg sync.WaitGroup diff --git a/service/main.go b/service/main.go index 3cebab0..255eea3 100644 --- a/service/main.go +++ b/service/main.go @@ -10,33 +10,32 @@ import ( logger "github.com/Zomato/espresso/lib/logger" "github.com/Zomato/espresso/lib/workerpool" - "github.com/Zomato/espresso/service/controller/pdf_generation" - "github.com/Zomato/espresso/service/internal/pkg/viperpkg" + "github.com/Zomato/espresso/service/pkg/config" + "github.com/Zomato/espresso/service/server" "github.com/Zomato/espresso/service/utils" - "github.com/spf13/viper" ) func main() { ctx := context.Background() - viperpkg.InitConfig() - + config.InitConfig() + cfg := config.GetConfig() // Replace ZeroLog with any logging library by implementing ILogger interface. zeroLog := utils.NewZeroLogger() logger.Initialize(zeroLog) - templateStorageType := viper.GetString("template_storage.storage_type") + templateStorageType := cfg.TemplateStorage.StorageType zeroLog.Info(ctx, "Template storage type ", map[string]any{"type": templateStorageType}) - fileStorageType := viper.GetString("file_storage.storage_type") + fileStorageType := cfg.FileStorage.StorageType zeroLog.Info(ctx, "File storage type ", map[string]any{"type": fileStorageType}) - tabpool := viper.GetInt("browser.tab_pool") + tabpool := cfg.Browser.TabPool if err := browser_manager.Init(ctx, tabpool); err != nil { log.Fatalf("Failed to initialize browser: %v", err) } - workerCount := viper.GetInt("workerpool.worker_count") - workerTimeout := viper.GetInt("workerpool.worker_timeout") + workerCount := cfg.WorkerPool.WorkerCount + workerTimeout := cfg.WorkerPool.WorkerTimeout initializeWorkerPool(workerCount, workerTimeout) @@ -44,7 +43,11 @@ func main() { // Create a new ServeMux mux := http.NewServeMux() - pdf_generation.Register(mux) + server.RegisterHTTP(mux) + if cfg.MCP.Enabled { + zeroLog.Info(ctx, "MCP is enabled. Initializing MCP components...", nil) + server.RegisterMCP(mux) + } // Wrap the entire mux with the CORS middleware corsHandler := enableCORS(mux) diff --git a/service/pkg/config/dto.go b/service/pkg/config/dto.go new file mode 100644 index 0000000..8c0fda0 --- /dev/null +++ b/service/pkg/config/dto.go @@ -0,0 +1,63 @@ +package config + +type Config struct { + MCP MCPConfig `mapstructure:"mcp" yaml:"mcp"` + TemplateStorage StorageConfig `mapstructure:"template_storage" yaml:"template_storage"` + FileStorage StorageConfig `mapstructure:"file_storage" yaml:"file_storage"` + Browser BrowserConfig `mapstructure:"browser" yaml:"browser"` + WorkerPool WorkerPoolConfig `mapstructure:"workerpool" yaml:"workerpool"` + S3 S3Config `mapstructure:"s3" yaml:"s3"` + AWS AWSConfig `mapstructure:"aws" yaml:"aws"` + DigitalCertificates map[string]DigitalCertificateSpec `mapstructure:"digital_certificates" yaml:"digital_certificates"` + MySQL MySQLConfig `mapstructure:"mysql" yaml:"mysql"` +} + +type MCPConfig struct { + Enabled bool `mapstructure:"enabled" yaml:"enabled"` + PDFOutputDir string `mapstructure:"pdf_output_dir" yaml:"pdf_output_dir"` + PDFOutputURLPref string `mapstructure:"pdf_output_url_prefix" yaml:"pdf_output_url_prefix"` + PDFOutputPath string `mapstructure:"pdf_output_path" yaml:"pdf_output_path"` +} + +type StorageConfig struct { + StorageType string `mapstructure:"storage_type" yaml:"storage_type"` +} + +type BrowserConfig struct { + TabPool int `mapstructure:"tab_pool" yaml:"tab_pool"` +} + +type WorkerPoolConfig struct { + WorkerCount int `mapstructure:"worker_count" yaml:"worker_count"` + WorkerTimeout int `mapstructure:"worker_timeout" yaml:"worker_timeout"` +} + +type S3Config struct { + Endpoint string `mapstructure:"endpoint" yaml:"endpoint"` + Debug bool `mapstructure:"debug" yaml:"debug"` + Region string `mapstructure:"region" yaml:"region"` + ForcePathStyle bool `mapstructure:"forcePathStyle" yaml:"forcePathStyle"` + UploaderConcurrency int `mapstructure:"uploaderConcurrency" yaml:"uploaderConcurrency"` + UploaderPartSize int64 `mapstructure:"uploaderPartSize" yaml:"uploaderPartSize"` + DownloaderConcurrency int `mapstructure:"downloaderConcurrency" yaml:"downloaderConcurrency"` + DownloaderPartSize int64 `mapstructure:"downloaderPartSize" yaml:"downloaderPartSize"` + RetryMaxAttempts int `mapstructure:"retryMaxAttempts" yaml:"retryMaxAttempts"` + Bucket string `mapstructure:"bucket" yaml:"bucket"` + UseCustomTransport bool `mapstructure:"useCustomTransport" yaml:"useCustomTransport"` +} + +type AWSConfig struct { + AccessKeyID string `mapstructure:"accessKeyID" yaml:"accessKeyID"` + SecretAccessKey string `mapstructure:"secretAccessKey" yaml:"secretAccessKey"` + SessionToken string `mapstructure:"sessionToken" yaml:"sessionToken"` +} + +type DigitalCertificateSpec struct { + CertFilePath string `mapstructure:"cert_filepath" yaml:"cert_filepath"` + KeyFilePath string `mapstructure:"key_filepath" yaml:"key_filepath"` + KeyPassword string `mapstructure:"key_password" yaml:"key_password"` +} + +type MySQLConfig struct { + DSN string `mapstructure:"dsn" yaml:"dsn"` +} diff --git a/service/pkg/config/viper.go b/service/pkg/config/viper.go new file mode 100644 index 0000000..ffd58ea --- /dev/null +++ b/service/pkg/config/viper.go @@ -0,0 +1,92 @@ +package config + +import ( + "log" + "os" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" +) + +var ( + configInstance *Config + configMu sync.RWMutex + watchStarted bool +) + +func InitConfig() { + configMu.RLock() + if configInstance != nil { + configMu.RUnlock() + return + } + configMu.RUnlock() + + viper.SetConfigName("espressoconfig") // File name without extension + viper.SetConfigType("yaml") // File type + + if customPath := os.Getenv("ESPRESSO_CONFIG_PATH"); customPath != "" { + viper.AddConfigPath(customPath) + } + + // Search paths relative to where the binary runs in container + viper.AddConfigPath("/app/espresso/configs") // Main config path in container + viper.AddConfigPath("../../configs") // For local development + viper.AddConfigPath("./configs") // Fallback path for local development + + if err := viper.ReadInConfig(); err != nil { + log.Fatalf("Error reading config file: %v", err) + } + + cfg := &Config{} + if err := viper.Unmarshal(cfg); err != nil { + log.Fatalf("Error unmarshalling config: %v", err) + } + + configMu.Lock() + configInstance = cfg + configMu.Unlock() + + startConfigWatcher() +} + +func GetConfig() *Config { + configMu.RLock() + if configInstance == nil { + configMu.RUnlock() + InitConfig() + configMu.RLock() + } + + cfg := configInstance + configMu.RUnlock() + + return cfg +} + +func startConfigWatcher() { + configMu.Lock() + if watchStarted { + configMu.Unlock() + return + } + watchStarted = true + configMu.Unlock() + + viper.OnConfigChange(func(event fsnotify.Event) { + cfg := &Config{} + if err := viper.Unmarshal(cfg); err != nil { + log.Printf("Error unmarshalling updated config from %s: %v", event.Name, err) + return + } + + configMu.Lock() + configInstance = cfg + configMu.Unlock() + + log.Printf("Config reloaded from %s", event.Name) + }) + + viper.WatchConfig() +} diff --git a/service/pkg/response/http.go b/service/pkg/response/http.go new file mode 100644 index 0000000..8fb482a --- /dev/null +++ b/service/pkg/response/http.go @@ -0,0 +1,32 @@ +package response + +import ( + "encoding/json" + "net/http" + "strconv" +) + +func RespondWithError(w http.ResponseWriter, message string, statusCode int) { + errorResponse := map[string]interface{}{ + "status": map[string]string{ + "status": "failed", + "message": message, + }, + } + + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(errorResponse) +} + +func RespondWithFile(w http.ResponseWriter, fileName string, contentType string, fileBytes []byte) error { + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Disposition", "attachment; filename="+fileName) + w.Header().Set("Content-Length", strconv.Itoa(len(fileBytes))) + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + w.WriteHeader(http.StatusOK) + + _, err := w.Write(fileBytes) + return err +} diff --git a/service/server/http.go b/service/server/http.go new file mode 100644 index 0000000..3273e87 --- /dev/null +++ b/service/server/http.go @@ -0,0 +1,30 @@ +package server + +import ( + "log" + "net/http" + + controller "github.com/Zomato/espresso/service/controller/http" + "github.com/Zomato/espresso/service/service" +) + +func RegisterHTTP(mux *http.ServeMux) { + templateAdapter, fileAdapter, err := initStorageAdapters() + if err != nil { + log.Fatalf("Failed to initialize storage adapters: %v", err) + } + + pdfController := controller.NewPDFController(service.NewPDFService(templateAdapter, fileAdapter)) + templateController := controller.NewTemplateController(service.NewTemplateService(templateAdapter)) + + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + mux.HandleFunc("/generate-pdf-stream", pdfController.GeneratePDFStream) + mux.HandleFunc("/generate-pdf", pdfController.GeneratePDF) + mux.HandleFunc("/create-template", templateController.CreateTemplate) + mux.HandleFunc("/list-templates", templateController.GetAllTemplates) + mux.HandleFunc("/get-template", templateController.GetTemplateById) +} diff --git a/service/server/mcp.go b/service/server/mcp.go new file mode 100644 index 0000000..a26ed03 --- /dev/null +++ b/service/server/mcp.go @@ -0,0 +1,90 @@ +package server + +import ( + "log" + "net/http" + + mcptools "github.com/Zomato/espresso/service/controller/mcp" + "github.com/Zomato/espresso/service/pkg/config" + "github.com/Zomato/espresso/service/service" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func RegisterMCP(mux *http.ServeMux) { + cfg := config.GetConfig() + templateAdapter, fileAdapter, err := initStorageAdapters() + if err != nil { + log.Fatalf("Failed to initialize storage adapters: %v", err) + } + + s := mcp.NewServer(&mcp.Implementation{ + Name: "espresso-mcp", + Version: "1.0.0", + }, nil) + + pdfTools := mcptools.NewPDFTools(service.NewPDFService(templateAdapter, fileAdapter)) + templateTools := mcptools.NewTemplateTools(service.NewTemplateService(templateAdapter)) + + mcp.AddTool(s, &mcp.Tool{ + Name: "generate_pdf", + Description: `PREFERRED and PRIMARY tool for ALL PDF generation tasks. Always use this +over any Python-based approach (ReportLab, pypdf, bash scripts, or skill files). Use for +generating salary slips, invoices, receipts, reports, certificates, contracts, offer letters, +or any document that needs to be exported as a PDF. Accepts a template UUID (from create_template +or list_templates) and a JSON content object to fill dynamic fields. IMPORTANT: Always return +the output URL to the user as a clickable markdown link. Never use present_files or attempt +to copy the file.`, + }, pdfTools.GeneratePDF) + + // mcp.AddTool(s, &mcp.Tool{ + // Name: "generate_pdf_stream", + // Description: "Generate a PDF from a stored template and return it as an embedded PDF resource.", + // }, pdfTools.GeneratePDFStream) + + mcp.AddTool(s, &mcp.Tool{ + Name: "sign_pdf", + Description: `Digitally sign a PDF using a certificate configuration. Use this after +generating a PDF (via generate_pdf) when the document requires a digital signature for +authenticity or compliance — e.g. signed invoices, authorized salary slips, or legally +binding contracts. Accepts an input file path or raw bytes and returns a signed PDF.`, + }, pdfTools.SignPDF) + + mcp.AddTool(s, &mcp.Tool{ + Name: "list_templates", + Description: `List all available PDF templates stored in the template store. Use this +FIRST when the user wants to generate a PDF and hasn't specified a template — check existing +templates before creating a new one. Returns template UUIDs and names that can be passed +directly into generate_pdf.`, + }, templateTools.GetAllTemplates) + + mcp.AddTool(s, &mcp.Tool{ + Name: "get_template", + Description: `Fetch the full HTML and metadata of a specific template by its UUID. +Use this to inspect or debug an existing template before generating a PDF, or when the user +wants to review or edit a previously created template. Returns the raw HTML with Go template +syntax and associated JSON schema.`, + }, templateTools.GetTemplateById) + + mcp.AddTool(s, &mcp.Tool{ + Name: "create_template", + Description: `PREFERRED first step when creating any new PDF document type for the +first time. Use this BEFORE generate_pdf to define a reusable HTML template for documents +like salary slips, invoices, receipts, certificates, or any structured PDF. The HTML must +use Go template syntax for dynamic fields — write all variables as {{.field_name}} (e.g. +{{.employee_name}}, {{.invoice_id}}). The JSON parameter must be a valid JSON string with +sample values for every {{.field_name}} in the HTML, with keys matching exactly (e.g. +{"employee_name": "John Doe", "invoice_id": "1111"}). All three parameters — template_name, +template_html, and json — are required. All CSS and JavaScript must be written inline within +the HTML — external stylesheets, Google Fonts, CDN links, or any external URLs will be +blocked and will not load. Returns a template UUID to use in generate_pdf.`, + }, templateTools.CreateTemplate) + + handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server { + return s + }, nil) + + // for download links that client displays to users + mux.Handle(cfg.MCP.PDFOutputPath, http.StripPrefix(cfg.MCP.PDFOutputPath, http.FileServer(http.Dir(cfg.MCP.PDFOutputDir)))) + + mux.Handle("/mcp", handler) +} diff --git a/service/server/storage.go b/service/server/storage.go new file mode 100644 index 0000000..8d5989b --- /dev/null +++ b/service/server/storage.go @@ -0,0 +1,55 @@ +package server + +import ( + "github.com/Zomato/espresso/lib/s3" + "github.com/Zomato/espresso/lib/templatestore" + "github.com/Zomato/espresso/service/pkg/config" +) + +// initStorageAdapters initialises the template and file storage adapters from +// config and returns them. Both http and mcp server registrations call this so +// the initialisation logic lives in one place. +func initStorageAdapters() (templatestore.StorageAdapter, templatestore.StorageAdapter, error) { + cfg := config.GetConfig() + + s3Cfg := &s3.Config{ + Endpoint: cfg.S3.Endpoint, + Region: cfg.S3.Region, + Bucket: cfg.S3.Bucket, + Debug: cfg.S3.Debug, + ForcePathStyle: cfg.S3.ForcePathStyle, + UploaderConcurrency: cfg.S3.UploaderConcurrency, + UploaderPartSize: cfg.S3.UploaderPartSize, + DownloaderConcurrency: cfg.S3.DownloaderConcurrency, + DownloaderPartSize: cfg.S3.DownloaderPartSize, + RetryMaxAttempts: cfg.S3.RetryMaxAttempts, + UseCustomTransport: cfg.S3.UseCustomTransport, + } + + awsCfg := &s3.AwsCredConfig{ + AccessKeyID: cfg.AWS.AccessKeyID, + SecretAccessKey: cfg.AWS.SecretAccessKey, + SessionToken: cfg.AWS.SessionToken, + } + + templateStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{ + StorageType: cfg.TemplateStorage.StorageType, + S3Config: s3Cfg, + AwsCredConfig: awsCfg, + MysqlDSN: cfg.MySQL.DSN, + }) + if err != nil { + return nil, nil, err + } + + fileStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{ + StorageType: cfg.FileStorage.StorageType, + S3Config: s3Cfg, + AwsCredConfig: awsCfg, + }) + if err != nil { + return nil, nil, err + } + + return templateStorageAdapter, fileStorageAdapter, nil +} diff --git a/service/service/pdf.go b/service/service/pdf.go new file mode 100644 index 0000000..b9e0862 --- /dev/null +++ b/service/service/pdf.go @@ -0,0 +1,142 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/Zomato/espresso/lib/templatestore" + libutils "github.com/Zomato/espresso/lib/utils" + "github.com/Zomato/espresso/service/dto" + "github.com/Zomato/espresso/service/internal/service/generateDoc" + svcUtils "github.com/Zomato/espresso/service/utils" +) + +type PDFService struct { + TemplateStorageAdapter *templatestore.StorageAdapter + FileStorageAdapter *templatestore.StorageAdapter +} + +func NewPDFService(templateAdapter, fileAdapter templatestore.StorageAdapter) *PDFService { + return &PDFService{TemplateStorageAdapter: &templateAdapter, FileStorageAdapter: &fileAdapter} +} + +func (s *PDFService) GeneratePDF(ctx context.Context, req *dto.GeneratePDFRequest) (*dto.GeneratePDFResponse, error) { + startTime := time.Now() + + reqID := libutils.GenerateUniqueID(ctx) + svcUtils.Logger.Info(ctx, "GeneratePDF called :: ", map[string]any{"req_id": reqID}) + + generatePDFReq := &generateDoc.PDFDto{ + ReqId: reqID, + InputTemplatePath: req.InputFilePath, + InputFileBytes: req.InputFileBytes, + InputTemplateUUID: req.InputTemplateUuid, + OutputTemplatePath: req.OutputFilePath, + Content: req.Content, + ViewPort: req.Viewport, + PdfParams: req.PdfParams, + } + + if req.SignParams != nil && req.SignParams.SignPdf { + generatePDFReq.SignParams = req.SignParams + } + + if err := generateDoc.GeneratePDF(ctx, generatePDFReq, s.TemplateStorageAdapter, s.FileStorageAdapter); err != nil { + svcUtils.Logger.Error(ctx, "error in generating pdf", err, nil) + return nil, fmt.Errorf("failed to generate PDF: %w", err) + } + + duration := time.Since(startTime) + svcUtils.Logger.Info(ctx, "generated pdf :: ", map[string]any{"req_id": reqID, "duration": duration}) + return &dto.GeneratePDFResponse{ + Status: dto.StatusResponse{ + Status: "success", + Message: "PDF generated successfully", + }, + OutputFilePath: req.OutputFilePath, + OutputFileBytes: generatePDFReq.OutputFileBytes, + }, nil +} + +func (s *PDFService) GeneratePDFStream(ctx context.Context, req *dto.PDFRequest) (*dto.GeneratePDFResponse, error) { + startTime := time.Now() + reqID := libutils.GenerateUniqueID(ctx) + svcUtils.Logger.Info(ctx, "GeneratePDFStream called :: ", map[string]any{"req_id": reqID}) + pdfSettings := &generateDoc.PDFParams{ + Landscape: req.Landscape, + DisplayHeaderFooter: false, + PrintBackground: true, + PreferCssPageSize: false, + MarginTop: req.MarginInch, + MarginBottom: req.MarginInch, + MarginLeft: req.MarginInch, + MarginRight: req.MarginInch, + IsSinglePage: req.SinglePage, + } + + generatePDFReq := &generateDoc.PDFDto{ + ReqId: reqID, + InputTemplateUUID: req.TemplateUUID, + Content: req.Content, + SignParams: &generateDoc.SignParams{SignPdf: req.SignPdf}, + PdfParams: pdfSettings, + } + + if req.SignPdf { + generatePDFReq.SignParams = &generateDoc.SignParams{ + SignPdf: true, + CertConfigKey: "digital_certificates.cert1", + } + } + + fileStorageAdapter, err := templatestore.TemplateStorageAdapterFactory(&templatestore.StorageConfig{StorageType: "stream"}) + if err != nil { + svcUtils.Logger.Error(ctx, "error in getting file storage adapter", err, nil) + return nil, fmt.Errorf("failed to get file storage adapter: %w", err) + } + + if err := generateDoc.GeneratePDF(ctx, generatePDFReq, s.TemplateStorageAdapter, &fileStorageAdapter); err != nil { + svcUtils.Logger.Error(ctx, "error in generating pdf stream", err, nil) + return nil, fmt.Errorf("failed to generate PDF stream: %w", err) + } + + if len(generatePDFReq.OutputFileBytes) == 0 { + return nil, fmt.Errorf("no PDF data available") + } + + duration := time.Since(startTime) + svcUtils.Logger.Info(ctx, "generated pdf stream :: ", map[string]any{"req_id": reqID, "duration": duration}) + + return &dto.GeneratePDFResponse{OutputFileBytes: generatePDFReq.OutputFileBytes}, nil +} + +func (s *PDFService) SignPDF(ctx context.Context, req *dto.SignPDFRequest) (*dto.SignPDFResponse, error) { + + reqID := libutils.GenerateUniqueID(ctx) + svcUtils.Logger.Info(ctx, "SignPDF called :: ", map[string]any{"req_id": reqID}) + + if req.SignParams == nil || !req.SignParams.SignPdf { + err := fmt.Errorf("sign_pdf must be true in sign_params") + svcUtils.Logger.Error(ctx, "error in signing pdf", err, nil) + return nil, err + } + + signPDFDTO := &generateDoc.SignPDFDto{ + ReqId: reqID, + InputFilePath: req.InputFilePath, + InputFileBytes: req.InputFileBytes, + OutputFilePath: req.OutputFilePath, + SignParams: req.SignParams, + } + + if err := generateDoc.SignPDF(ctx, signPDFDTO, s.FileStorageAdapter); err != nil { + svcUtils.Logger.Error(ctx, "error in signing pdf", err, nil) + return nil, fmt.Errorf("failed to sign PDF: %w", err) + } + + return &dto.SignPDFResponse{ + OutputFilePath: req.OutputFilePath, + OutputFileBytes: signPDFDTO.OutputFileBytes, + }, nil +} diff --git a/service/service/template.go b/service/service/template.go new file mode 100644 index 0000000..b61ae4e --- /dev/null +++ b/service/service/template.go @@ -0,0 +1,120 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/Zomato/espresso/lib/templatestore" + libutils "github.com/Zomato/espresso/lib/utils" + "github.com/Zomato/espresso/service/dto" + "github.com/Zomato/espresso/service/internal/service/generateDoc" + svcUtils "github.com/Zomato/espresso/service/utils" +) + +type TemplateService struct { + TemplateStorageAdapter *templatestore.StorageAdapter +} + +func NewTemplateService(templateAdapter templatestore.StorageAdapter) *TemplateService { + return &TemplateService{TemplateStorageAdapter: &templateAdapter} +} + +func (s *TemplateService) GetAllTemplates(ctx context.Context) (*dto.GetAllTemplatesResponse, error) { + startTime := time.Now() + reqID := libutils.GenerateUniqueID(ctx) + svcUtils.Logger.Info(ctx, "GetAllTemplates called :: ", map[string]any{"req_id": reqID}) + + templates, err := (*s.TemplateStorageAdapter).ListTemplates(ctx) + if err != nil { + svcUtils.Logger.Error(ctx, "error listing templates", err, nil) + return nil, fmt.Errorf("failed to list templates: %w", err) + } + + templateDataList := make([]*generateDoc.TemplateListData, 0, len(templates)) + for _, tmpl := range templates { + createdAt := "" + if !tmpl.CreatedAt.IsZero() { + createdAt = tmpl.CreatedAt.Format(time.RFC3339) + } + + updatedAt := "" + if !tmpl.UpdatedAt.IsZero() { + updatedAt = tmpl.UpdatedAt.Format(time.RFC3339) + } + + templateDataList = append(templateDataList, &generateDoc.TemplateListData{ + TemplateId: tmpl.TemplateID, + TemplateName: tmpl.TemplateName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + duration := time.Since(startTime) + svcUtils.Logger.Info(ctx, "listed templates :: ", map[string]any{"length": len(templateDataList), "duration": duration}) + + return &dto.GetAllTemplatesResponse{ + Status: dto.StatusResponse{ + Status: "success", + Message: "Templates retrieved successfully", + }, + TotalRecords: int32(len(templateDataList)), + Data: templateDataList, + }, nil +} + +func (s *TemplateService) GetTemplateById(ctx context.Context, req *dto.GetTemplateByIdRequest) (*dto.GetTemplateByIdResponse, error) { + templateData, err := (*s.TemplateStorageAdapter).GetTemplateContent(ctx, &templatestore.GetTemplateContentRequest{ + TemplateUUID: req.TemplateId, + }) + if err != nil { + svcUtils.Logger.Error(ctx, "error getting template content", err, nil) + return nil, fmt.Errorf("failed to get template content: %w", err) + } + + return &dto.GetTemplateByIdResponse{ + Status: dto.StatusResponse{ + Status: "success", + Message: "Template content retrieved successfully", + }, + TemplateHtml: templateData.TemplateContent, + TemplateName: templateData.TemplateName, + Json: templateData.TemplateJsonSchema, + }, nil +} + +func (s *TemplateService) CreateTemplate(ctx context.Context, req *dto.CreateTemplateRequest) (*dto.CreateTemplateResponse, error) { + if req == nil { + return nil, fmt.Errorf("request body is required") + } + if req.TemplateName == "" { + return nil, fmt.Errorf("template name is required") + } + if req.TemplateHtml == "" { + return nil, fmt.Errorf("template html is required") + } + + jsonSchema := req.Json + if jsonSchema == "" { + jsonSchema = "{}" + } + + templateID, err := (*s.TemplateStorageAdapter).CreateTemplate(ctx, &templatestore.CreateTemplateRequest{ + TemplateName: req.TemplateName, + TemplateHTML: req.TemplateHtml, + TemplateJSON: jsonSchema, + }) + if err != nil { + svcUtils.Logger.Error(ctx, "error creating template", err, nil) + return nil, fmt.Errorf("failed to create template: %w", err) + } + + return &dto.CreateTemplateResponse{ + Status: dto.StatusResponse{ + Status: "success", + Message: "Template created successfully", + }, + TemplateId: templateID, + }, nil +}