Integración de Proto como Fuente Única de Verdad en Go con gorm.io/gen y para TypeScript

·

4 min read

Resumen

Este documento describe un flujo avanzado para utilizar Proto como la fuente única de verdad en sistemas que requieren consistencia entre los modelos de datos, la base de datos, el backend en Go y el cliente en TypeScript. El enfoque incluye:

  1. Definir modelos, relaciones (PK, FK) y servicios gRPC en un archivo Proto.

  2. Generar automáticamente modelos enriquecidos en Go con gorm.io/gen.

  3. Generar tipos en TypeScript para el cliente.

  4. Implementar un servicio gRPC que gestione las operaciones CRUD.


Introducción

La centralización de las definiciones de datos en un archivo Proto permite mantener consistencia entre las diferentes capas de una aplicación, desde la base de datos hasta el cliente. Este enfoque minimiza errores, acelera el desarrollo y asegura escalabilidad.

En este paper, implementaremos un flujo funcional y escalable que:

  • Usa Proto para definir esquemas y servicios.

  • Genera modelos compatibles con Gorm en Go.

  • Crea funciones enriquecidas con gorm.io/gen para optimizar las operaciones en la base de datos.

  • Genera tipos en TypeScript para mantener consistencia entre backend y cliente.


1. Definición del Esquema Proto

El archivo Proto incluye modelos, relaciones y un servicio gRPC para manejar las operaciones de User y Order.

Archivo models.proto

syntax = "proto3";

package example;

import "gorm.proto"; // Requiere el plugin `protoc-gen-gorm`

// Modelo User
message User {
  int32 id = 1 [(gorm.field).primary_key = true]; // Clave primaria
  string name = 2 [(gorm.field).size = 255];      // Nombre del usuario
  repeated Order orders = 3;                      // Relación uno-a-muchos
}

// Modelo Order
message Order {
  int32 id = 1 [(gorm.field).primary_key = true];          // Clave primaria
  int32 user_id = 2 [(gorm.field).foreign_key = "UserID"]; // Clave foránea hacia User
  string product = 3 [(gorm.field).size = 255];            // Producto asociado
}

// Servicio gRPC para manejar User
service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
}

// Mensajes para el servicio
message GetUserRequest {
  int32 id = 1;
}

message GetUserResponse {
  User user = 1;
}

message CreateUserRequest {
  string name = 1;
}

message CreateUserResponse {
  User user = 1;
}

2. Generación de Modelos en Go

Comando para Generar Modelos

Usa protoc-gen-gorm para generar modelos compatibles con Gorm:

protoc --gorm_out=. --go_out=. models.proto

Salida Generada: Modelos en Go

Modelo User

type User struct {
    ID     int32   `gorm:"primaryKey"`
    Name   string  `gorm:"size:255"`
    Orders []Order `gorm:"foreignKey:UserID"` // Relación uno-a-muchos
}

Modelo Order

type Order struct {
    ID      int32  `gorm:"primaryKey"`
    UserID  int32  `gorm:"index"` // FK hacia User.ID
    Product string `gorm:"size:255"`
}

3. Configurar gorm.io/gen

Instalar y Configurar

Instala gorm.io/gen y crea un archivo main.go para configurar el generador.

go get -u gorm.io/gen

Archivo main.go:

package main

import (
    "gorm.io/driver/postgres"
    "gorm.io/gen"
    "gorm.io/gorm"
)

func main() {
    db, err := gorm.Open(postgres.Open("host=localhost user=postgres dbname=mydb password=mypass sslmode=disable"))
    if err != nil {
        panic("failed to connect database")
    }

    g := gen.NewGenerator(gen.Config{
        OutPath: "./query",
        Mode:    gen.WithoutContext | gen.WithDefaultQuery,
    })

    g.UseDB(db)
    g.ApplyBasic("User", "Order") // Registra los modelos
    g.Execute()
}

Ejecutar Generador

go run main.go

Esto genera funciones enriquecidas en ./query.


4. Generar Modelos en TypeScript

Comando para Generar TS

Usa ts-proto para generar tipos TypeScript compatibles con el servicio gRPC:

protoc \
  --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=. \
  models.proto

Salida Generada: Tipos en TypeScript

Modelo User

export interface User {
  id: number;
  name: string;
  orders: Order[];
}

Modelo Order

export interface Order {
  id: number;
  userId: number;
  product: string;
}

Servicio UserService

export interface UserService {
  GetUser(request: GetUserRequest): Promise<GetUserResponse>;
  CreateUser(request: CreateUserRequest): Promise<CreateUserResponse>;
}

5. Implementar el Servicio gRPC en Go

Archivo service/user_service.go:

package service

import (
    "context"
    "query" // Código generado por gorm.io/gen
    pb "path/to/models" // Código generado por protoc
)

type UserService struct {
    db *query.Query
}

func NewUserService(db *query.Query) *UserService {
    return &UserService{db: db}
}

func (s *UserService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    user, err := s.db.User.Preload(s.db.User.Orders).Where(s.db.User.ID.Eq(req.Id)).First()
    if err != nil {
        return nil, err
    }

    return &pb.GetUserResponse{
        User: &pb.User{
            Id:     user.ID,
            Name:   user.Name,
            Orders: mapOrdersToProto(user.Orders),
        },
    }, nil
}

func (s *UserService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
    newUser := &query.User{
        Name: req.Name,
    }
    if err := s.db.User.Create(newUser); err != nil {
        return nil, err
    }

    return &pb.CreateUserResponse{
        User: &pb.User{
            Id:   newUser.ID,
            Name: newUser.Name,
        },
    }, nil
}

func mapOrdersToProto(orders []query.Order) []*pb.Order {
    var protoOrders []*pb.Order
    for _, o := range orders {
        protoOrders = append(protoOrders, &pb.Order{
            Id:      o.ID,
            UserId:  o.UserID,
            Product: o.Product,
        })
    }
    return protoOrders
}

6. Uso del Servicio en TypeScript

Cliente gRPC en TypeScript:

import { UserServiceClientImpl, GetUserRequest, CreateUserRequest } from './models';

const client = new UserServiceClientImpl(rpc);

async function fetchUser(id: number) {
  const req: GetUserRequest = { id };
  const res = await client.GetUser(req);
  console.log('User:', res.user);
}

async function createUser(name: string) {
  const req: CreateUserRequest = { name };
  const res = await client.CreateUser(req);
  console.log('New User:', res.user);
}

Conclusión

Este flujo:

  1. Centraliza el control en Proto, eliminando duplicaciones y manteniendo consistencia.

  2. Automatiza la generación de modelos, funciones enriquecidas y tipos.

  3. Escala perfectamente: Agregar nuevos modelos o servicios es sencillo y coherente.

Con estas herramientas, puedes desarrollar aplicaciones robustas, escalables y consistentes desde la base de datos hasta el cliente.