Files
relspecgo/pkg/writers/graphql
2025-12-28 11:41:55 +02:00
..
2025-12-28 11:41:55 +02:00
2025-12-28 11:41:55 +02:00
2025-12-28 11:41:55 +02:00
2025-12-28 11:41:55 +02:00
2025-12-28 11:41:55 +02:00

GraphQL Schema Writer

The GraphQL writer converts RelSpec's internal database model into GraphQL Schema Definition Language (SDL) files.

Features

  • Table to Type mapping: Database tables become GraphQL types
  • Column to Field mapping: Table columns become type fields
  • Enum support: Database enums are preserved
  • Custom scalar declarations: Automatically declares DateTime, JSON, Date scalars
  • Implicit relationships: Generates relationship fields from foreign keys
  • Many-to-many support: Handles junction tables intelligently
  • Clean output: Proper formatting, field ordering, and comments

Type Mappings

SQL to GraphQL

SQL Type GraphQL Type Notes
bigint, integer, serial (PK) ID Primary keys map to ID
bigint, integer, int Int
text, varchar, char String
uuid (PK) ID UUID primary keys also map to ID
uuid String Non-PK UUIDs map to String
double precision, numeric, float Float
boolean Boolean
timestamp, timestamptz DateTime Custom scalar
jsonb, json JSON Custom scalar
date Date Custom scalar
Enum types Enum Preserves enum name
Arrays (e.g., text[]) [Type] Mapped to GraphQL lists

Relationship Handling

The writer intelligently generates relationship fields based on foreign key constraints:

Forward Relationships (FK on this table)

-- Post table has authorId FK to User.id
CREATE TABLE post (
  id bigint PRIMARY KEY,
  title text NOT NULL,
  author_id bigint NOT NULL REFERENCES user(id)
);
type Post {
  id: ID!
  title: String!
  author: User!  # Generated from authorId FK
}

Reverse Relationships (FK on other table)

type User {
  id: ID!
  email: String!
  posts: [Post!]!  # Reverse relationship (Post has FK to User)
}

Many-to-Many Relationships

Junction tables (tables with only PKs and FKs) are automatically detected and hidden:

CREATE TABLE post_tag (
  post_id bigint NOT NULL REFERENCES post(id),
  tag_id bigint NOT NULL REFERENCES tag(id),
  PRIMARY KEY (post_id, tag_id)
);
type Post {
  id: ID!
  tags: [Tag!]!  # Many-to-many through PostTag junction table
}

type Tag {
  id: ID!
  posts: [Post!]!  # Reverse many-to-many
}

# Note: PostTag junction table is NOT included in output

Usage

Basic Usage

import (
    "git.warky.dev/wdevs/relspecgo/pkg/models"
    "git.warky.dev/wdevs/relspecgo/pkg/writers"
    "git.warky.dev/wdevs/relspecgo/pkg/writers/graphql"
)

opts := &writers.WriterOptions{
    OutputPath: "schema.graphql",
}

writer := graphql.NewWriter(opts)
err := writer.WriteDatabase(db)

With Metadata Options

opts := &writers.WriterOptions{
    OutputPath: "schema.graphql",
    Metadata: map[string]any{
        "includeScalarDeclarations": true,   // Include scalar declarations
        "includeComments":            true,  // Include field/table comments
    },
}

writer := graphql.NewWriter(opts)
err := writer.WriteDatabase(db)

Write to Stdout

opts := &writers.WriterOptions{
    OutputPath: "",  // Empty path writes to stdout
}

writer := graphql.NewWriter(opts)
err := writer.WriteDatabase(db)

CLI Usage

# Convert PostgreSQL database to GraphQL
relspec convert --from pgsql \
                --from-conn "postgres://user:pass@localhost:5432/mydb" \
                --to graphql --to-path schema.graphql

# Convert GORM models to GraphQL
relspec convert --from gorm --from-path ./models \
                --to graphql --to-path schema.graphql

# Convert JSON to GraphQL
relspec convert --from json --from-path schema.json \
                --to graphql --to-path schema.graphql

Output Format

The generated GraphQL schema follows this structure:

  1. Header comment (if enabled)
  2. Custom scalar declarations (if any custom scalars are used)
  3. Enum definitions (alphabetically sorted)
  4. Type definitions (with fields ordered: ID first, then scalars alphabetically, then relationships)

Example Output

# Generated GraphQL Schema
# Database: myapp

scalar DateTime
scalar JSON
scalar Date

enum Role {
  ADMIN
  USER
  MODERATOR
}

type User {
  id: ID!
  createdAt: DateTime!
  email: String!
  name: String!
  role: Role!

  posts: [Post!]!
  profile: Profile
}

type Post {
  id: ID!
  content: String
  published: Boolean!
  publishedAt: Date
  title: String!

  author: User!
  tags: [Tag!]!
}

type Tag {
  id: ID!
  name: String!

  posts: [Post!]!
}

Metadata Options

Option Type Description Default
includeScalarDeclarations bool Include scalar DateTime, etc. declarations true
includeComments bool Include table/field descriptions as comments true
preservePKType bool Use Int/String for PKs instead of ID false

Field Naming Conventions

  • FK columns: Foreign key columns like authorId are removed from the output; instead, a relationship field author is generated
  • Relationship pluralization: Reverse one-to-many relationships are pluralized (e.g., posts, tags)
  • CamelCase: Field names are kept in their original casing from the database

Junction Table Detection

A table is considered a junction table if it:

  1. Has exactly 2 foreign key constraints
  2. All columns are either primary keys or foreign keys
  3. Has a composite primary key on the FK columns

Junction tables are automatically hidden from the GraphQL output, and many-to-many relationship fields are generated on the related types instead.

Limitations

  • All tables in all schemas are flattened into a single GraphQL schema
  • No support for GraphQL-specific features like directives, interfaces, or unions
  • Nullable vs non-nullable is determined solely by the NOT NULL constraint

Example Conversion

Input (Database Schema):

CREATE TABLE user (
  id bigint PRIMARY KEY,
  email text NOT NULL,
  created_at timestamp NOT NULL
);

CREATE TABLE post (
  id bigint PRIMARY KEY,
  title text NOT NULL,
  author_id bigint NOT NULL REFERENCES user(id)
);

Output (GraphQL Schema):

scalar DateTime

type User {
  id: ID!
  createdAt: DateTime!
  email: String!

  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!

  author: User!
}