Skip to content

Schema Guide

Platform Users — Engineers & Low-code Ops Users (ORA / Panel Builder) OR Platform ORA — AI Planning Interface Agent Workflows Plan Visualisation ADK Integration SDK UI — Frontend Shell FDK Architecture Low code Config-driven DDK Schema Definition Code Generator Generated Server MDK WEM DAL Experiment Manager Nexus Deployment Control Live Monitoring Registry Browser SCDK Source Control Pipeline Mgmt Azure DevOps deploys ↓ SDK API — GraphQL Federation Gateway Federation Gateway Component Resolvers Auth & Licensing Plugins: gql-autogeneration Migrator Helm KinD Boilerplate GenAI ··· Microservices — Domain IP Services Data Pipeline Core Platform Metrics & Analytics Spatial & Geo Simulation Event Detection Camera & Device Fire & Resource Opt. Satellite Modelling ↓ Nexus deploys Deployed OR Applications Rail Ops Dashboard Mine Mgmt Dashboard Port Ops Dashboard ··· FDK-built · DDK-backed · MDK-powered · deployed via Nexus ↑ Application Users — Operations Teams (shift managers, analysts, planners)

This guide covers everything you need to know about writing DDK schemas, including directives, constraints, relationships, and best practices.

Table of Contents


Basic Schema Structure

DDK schemas use GraphQL Schema Definition Language (SDL) with custom directives for database mapping. The directives are defined in internal.graphqls which is automatically included during validation and generation.

Minimal Type Definition

Every DDK type needs the @required directive:

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  email: String!
}

Key Elements:

  • @required directive controls CRUD generation and table creation
  • Primary key is mandatory for database-backed types
  • ! indicates non-nullable fields
  • Field names become database column names directly (column name matches field name via gorm:"column:{fieldName};")

Schema File Organization

DDK schemas are organized into two types of files:

  • {name}.orm.graphqls — ORM-backed types with auto-generated CRUD operations
  • {name}.custom.graphqls — Custom operations marked with @resolver(type: "CUSTOM")

During regeneration, the DDK reads all .graphqls files (excluding *.custom.graphqls) to generate default CRUD resolvers, then processes all files together through gqlgen.


Type Directive: @required

The @required directive defines CRUD operations and table creation for a type.

Syntax

graphql
@required(type: "OPERATIONS", table: "true|false")

Parameters

type (string)

Comma-separated list of operations to generate. The DDK's default_resolver_builder.go parses these flags and generates corresponding Query/Mutation fields:

  • CREATE — Generates create{Type}({allFields}): {Type} mutation
  • READ — Generates get{Type}(id: ID!): {Type} query and get{Type}List(limit, offset, sortBy, sortOrder, ...filters): [{Type}] query
  • UPDATE — Generates update{Type}(id: ID!, {allFields}): {Type} mutation
  • DELETE — Generates delete{Type}(id: ID!): Boolean mutation

Examples:

graphql
# Full CRUD
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
}

# Read-only
type Category @required(type: "READ", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
}

# No auto-generated operations (custom resolvers only)
type SearchResult @required(type: "", table: "false") {
  id: ID!
  score: Float!
}

table (string)

Whether to create a database table. This is the key flag checked by the DDDPlugin — when table: "true", the plugin generates:

  • An application repository interface (application/repository/{type}.application.repository.go)
  • An infrastructure repository implementation (infrastructure/repository/{type}.infrastructure.repository.go)
  • A field in RepositoryHolder for dependency injection
  • Mock repository implementations via mockgen

When table: "false", the type is added to an ignoreModels map and excluded from migration and DDD generation.

When to use table: "false":

graphql
# Type without database persistence (DTO)
type Statistics @required(type: "", table: "false") {
  totalUsers: Int!
  activeUsers: Int!
  averageAge: Float!
}

# Type assembled from multiple sources
type UserProfile @required(type: "", table: "false") {
  user: User!
  preferences: JSON!
  activityScore: Float!
}

Links a non-table type to a model implementation:

graphql
type UserDTO @required(type: "READ", table: "false", link: "User") {
  id: ID!
  name: String!
}

This creates a mapping tracked in the generator's dtoImplementations map.

view and viewType (string, optional)

Creates database views instead of tables. See Views and Materialized Views.


Constraint Directive: @constraint

The @constraint directive adds database constraints to fields. The model generator (hooks/model_generator.go) processes these into GORM struct tags.

Supported Constraint Types

The generator maps each constraint type to a GORM tag:

Constraint TypeGORM TagDescription
primarykeygorm:"primaryKey;"Primary key field
uniquegorm:"uniqueIndex;"Unique index
indexgorm:"index;"Database index
checkgorm:"check:{value};"Check constraint with SQL expression
defaultgorm:"default:{value};"Default value
notnullgorm:"not null;"NOT NULL constraint
jsongorm:"serializer:json;"JSON serialization for complex types
ignoregorm:"-;"Field ignored by GORM
excludeAllgorm:"-:all;"Field excluded from all GORM operations
create-readgorm:"<-:create;"Only writable on create
update-readgorm:"<-:update;"Only writable on update
create-updategorm:"<-;"Writable on create and update
create-onlygorm:"<-:false;"Only writable on create, no reads
read-onlygorm:">:false;<:create;"Read-only field
updatetime-nanogorm:"autoUpdateTime:nano;"Auto-update time in nanoseconds
updatetime-milligorm:"autoUpdateTime:milli;"Auto-update time in milliseconds
embeddedgorm:"embedded"Embedded struct in GORM
sizegorm:"type:varchar({value});"Fixed-size varchar
numericgorm:"type:numeric{value};"Numeric precision (e.g. (10,2))

Primary Key

Required for every database-backed type. The DDDPlugin extracts primary keys from @constraint(type: "primarykey") to generate the GetPrimaryKey() method on the repository implementation.

graphql
id: ID! @constraint(type: "primarykey")

Composite Primary Key (for junction tables):

graphql
type UserRole @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  UserID: ID! @constraint(type: "primarykey")
  RoleID: ID! @constraint(type: "primarykey")
  assignedAt: String
}

Check Constraint

The field name in the check expression is automatically wrapped in double quotes by the generator for PostgreSQL compatibility:

go
// In model_generator.go:
value = strings.ReplaceAll(value, field.Name, `\"`+field.Name+`\"`)

Examples:

graphql
# Age validation
age: Int @constraint(type: "check", value: "age >= 18")

# Price validation
price: Float! @constraint(type: "check", value: "price > 0")

# String length
description: String @constraint(type: "check", value: "LENGTH(description) <= 500")

# Range validation
rating: Int @constraint(type: "check", value: "rating >= 1 AND rating <= 5")

Multiple Constraints

Fields can have multiple @constraint directives. The generator processes all of them and concatenates the GORM tags:

graphql
type Account @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  email: String! @constraint(type: "unique")
  balance: Float!
    @constraint(type: "default", value: "0.0")
    @constraint(type: "check", value: "balance >= 0")
  status: String
    @constraint(type: "default", value: "active")
    @constraint(
      type: "check"
      value: "status IN ('active', 'suspended', 'closed')"
    )
}

The generated Go struct for balance would have:

go
Balance float64 `json:"balance" gorm:"default:0.0;check:\"balance\" >= 0;column:balance;"`

Mapping Directive: @mapping

The @mapping directive defines relationships between types. The model generator reads these to produce GORM association struct tags.

Syntax

graphql
fieldName: Type @mapping(
  type: "relationship_type",
  foreignKey: "field_name",
  foreignKeyReference: "referenced_field",
  mappingTable: "table_name"  # only for many-to-many
)

Parameters

  • type: Relationship type — one2one, one2many, many2many, backRef
  • foreignKey: Field containing the foreign key
  • foreignKeyReference: Field being referenced
  • mappingTable: Junction table name (many-to-many only)

Important: The schema validator enforces that all non-basic field types (types that aren't ID, Int, String, Boolean, Float, Time, JSON, DateTime, Hex, or custom scalars) must have a @mapping directive. This ensures the generator knows how to handle relationships.


Field Types

Standard Scalar Types

GraphQL TypeGo TypePostgreSQL TypeDescription
IDstringVARCHARUnique identifier
StringstringTEXTVariable-length text
IntintINTEGER32-bit integer
Floatfloat64DOUBLE PRECISIONFloating-point number
BooleanboolBOOLEANTrue/false value
Timeint64BIGINTUnix timestamp
DateTimestringTEXTDate-time string
JSONjson.RawMessageJSONBJSON data (type:jsonb GORM tag)
HexstringTEXTHexadecimal string

Custom Scalars with Database Mapping

These scalars have automatic PostgreSQL type mapping defined in the generator's customScalarToGormTag map:

ScalarGo TypePostgreSQL TypeGORM Tag
UUIDstringuuidtype:uuid;default:uuid_generate_v4()
ULIDstringuuidtype:uuid;default:generate_ulid()
Pointmodel.Pointgeometry(POINT,4326)type:geometry(POINT,4326)
LineStringmodel.LineStringgeometry(LINESTRING,4326)type:geometry(LINESTRING,4326)
Polygonmodel.Polygongeometry(POLYGON,4326)type:geometry(POLYGON,4326)
Collectionmodel.Collectiongeometry(GEOMETRYCOLLECTION)type:geometry(GEOMETRYCOLLECTION)

Array Types

The DDK supports both 1D and 2D PostgreSQL arrays:

ScalarPostgreSQL TypeGORM Tag
Int1DArrayinteger[]type:integer[]
String1DArraytext[]type:text[]
Float1DArrayfloat[]type:float[]
Bool1DArraybool[]type:bool[]
Int2DArrayinteger[][]type:integer[][]
String2DArraytext[][]type:text[][]
Float2DArrayfloat[][]type:float[][]
Bool2DArraybool[][]type:bool[][]

Usage:

graphql
type Sensor @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  tags: String1DArray
  readings: Float2DArray
  location: Point
  boundary: Polygon
}

Nullable vs Non-Nullable

graphql
# Required field (non-nullable)
name: String!

# Optional field (nullable)
bio: String

# Required list (list can't be null, but can be empty)
tags: [String]!

# Optional list
categories: [String]

# Required list of required items
emails: [String!]!

Field Naming

  • Use camelCase for field names: firstName, emailAddress
  • Field names become column names in the database (usually snake_case via GORM)
  • Avoid SQL keywords: user, order, select, etc.

Enums

Define enumeration types for fields with fixed value sets.

Basic Enum

graphql
enum UserRole {
  ADMIN
  USER
  GUEST
}

type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  role: UserRole!
}

Multiple Enums

graphql
enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

enum PaymentStatus {
  UNPAID
  PAID
  REFUNDED
  FAILED
}

type Order @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  orderStatus: OrderStatus!
  paymentStatus: PaymentStatus!
  total: Float!
}

Enum Best Practices

  1. Use UPPERCASE for enum values
  2. Be descriptive but concise
  3. Consider future values when designing
  4. Don't remove values in production (add new ones instead)

Custom Scalars

JSON Scalar

Store arbitrary JSON data:

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  metadata: JSON
  preferences: JSON
}

Usage in Mutations:

graphql
mutation {
  createUser(
    id: "1"
    name: "John"
    metadata: "{\"age\": 30, \"city\": \"Sydney\"}"
    preferences: "{\"theme\": \"dark\", \"notifications\": true}"
  ) {
    id
    metadata
    preferences
  }
}

Custom Scalar Definition

You can extend with custom scalars in your schema:

graphql
scalar DateTime
scalar URL
scalar Email

type Event @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  title: String!
  startsAt: DateTime!
  website: URL
}

Note: You'll need to implement custom scalar handling in generated code.


Relationships

One-to-One

One entity relates to exactly one other entity.

Example: User has one Profile

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  email: String!
  profile: Profile!
    @mapping(type: "one2one", foreignKey: "userId", foreignKeyReference: "id")
}

type Profile @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  userId: String! @constraint(type: "unique")
  bio: String
  avatar: String
}

Critical: The foreign key (userId) must have a unique constraint.

One-to-Many

One entity relates to many other entities.

Example: User has many Posts

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  posts: [Post]!
    @mapping(
      type: "one2many"
      foreignKey: "authorId"
      foreignKeyReference: "id"
    )
}

type Post @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  title: String!
  content: String!
  authorId: String
}

Notes:

  • Foreign key is on the "many" side (Post)
  • Foreign key should match the referenced field type
  • Foreign key field is typically nullable

Many-to-Many

Many entities relate to many other entities through a junction table.

Example: Users have many Roles, Roles have many Users

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  roles: [Role]!
    @mapping(
      type: "many2many"
      foreignKey: "id"
      foreignKeyReference: "id"
      mappingTable: "UserRole"
    )
}

type Role @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  description: String
  users: [User]!
    @mapping(
      type: "many2many"
      foreignKey: "id"
      foreignKeyReference: "id"
      mappingTable: "UserRole"
    )
}

type UserRole @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  UserID: ID! @constraint(type: "primarykey")
  RoleID: ID! @constraint(type: "primarykey")
}

Critical:

  • Define explicit junction table
  • Use composite primary key
  • Field names in junction table follow pattern: {Type}ID

Back Reference

Navigate relationships in reverse direction.

Example: Profile can navigate back to User

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  contact: Contact!
    @mapping(type: "one2one", foreignKey: "userId", foreignKeyReference: "id")
}

type Contact @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  userId: String! @constraint(type: "unique")
  phone: String
  address: String
  # Back reference to user
  user: User!
    @mapping(type: "backRef", foreignKey: "id", foreignKeyReference: "userId")
}

Usage:

graphql
query {
  getContact(id: "1") {
    phone
    address
    user {
      id
      name
      email
    }
  }
}

Views and Materialized Views

The DDK supports creating PostgreSQL views and materialized views directly from schema directives. The model generator's checkifTableRequired function processes these.

View Syntax

graphql
type ActiveUsers
  @required(
    type: "READ"
    table: "false"
    view: "true"
    viewType: "VIEW"
    query: "SELECT * FROM users WHERE is_active = true"
  ) {
  id: ID!
  name: String!
  email: String!
}

Materialized View Syntax

graphql
type UserStats
  @required(
    type: "READ"
    table: "false"
    view: "true"
    viewType: "MVIEW"
    query: "SELECT role, COUNT(*) as user_count FROM users GROUP BY role"
  ) {
  role: String!
  userCount: Int!
}

Implementation: The MigrateModels function in service/model_migrator.go creates views after table migration:

  • Views: CREATE OR REPLACE VIEW {schema}."{name}" AS {query}
  • Materialized Views: CREATE MATERIALIZED VIEW {schema}."{name}" AS {query}

Types with view: "true" are added to the ignoreModels map (no table creation) and tracked in the views list for SQL generation.

Refreshing Materialized Views: The legacy repository's QueryParams.Refresh flag triggers REFRESH MATERIALIZED VIEW CONCURRENTLY when set to true.


Schema Validation Rules

The DDK enforces validation rules through schema_validator.go (1347 lines). Validation is two-phase:

Phase 1: Standard gqlgen Validation

Loads the gqlgen configuration, adds internal.graphqls directive declarations, and validates through config.LoadSchema(). This catches:

  • Invalid GraphQL syntax
  • Undefined type references
  • Invalid directive usage
  • Duplicate definitions

Phase 2: Custom DDK Validation

CustomValidation() enforces DDK-specific rules:

RuleLimitError
Type name length≤ 60 characters"type name exceeds 60 characters"
Enum value length≤ 200 characters"enum value exceeds 200 characters"
Field name length≤ 60 characters"field name exceeds 60 characters"
Go keyword checkAgainst keywords.json goList"field/type is a Go keyword"
PostgreSQL keyword checkAgainst keywords.json postgresList"field/type is a PostgreSQL keyword"
@required presenceAll non-Q/M/S types"type must have @required directive"
@mapping for complex typesNon-scalar field types"non-basic types require @mapping"
JSON mandatory checkJSON fields with !"JSON fields must not be mandatory"

Predefined Data Types

The validator recognizes these as built-in scalar types (in initDatatypes()): ID, Int, String, Boolean, Float, Time, JSON, DateTime, Hex

Fields using any other type (except enums and other defined types) require a @mapping directive.


Auto-Generated Resolvers

The DDK auto-generates default CRUD resolvers from schema types using CreateDefaultResolvers() in default_resolver_builder.go. This happens before gqlgen runs.

How Resolvers Are Generated

  1. The schema text is parsed to extract types with @required directive
  2. For each type, the CRUD flags are checked (CREATE, READ, UPDATE, DELETE)
  3. Query/Mutation fields are generated with appropriate parameters

Generated Query Format

For a type with READ:

graphql
type Query {
  getUser(id: ID!): User
  getUserList(
    limit: Int
    offset: Int
    sortBy: String
    sortOrder: String
    # Filter parameters for each scalar field:
    name: String
    email: String
    age: Int
    isActive: Boolean
  ): [User]
}

Generated Mutation Format

For CREATE:

graphql
type Mutation {
  createUser(id: ID!, name: String!, email: String!, age: Int): User
}

For UPDATE:

graphql
type Mutation {
  updateUser(id: ID!, name: String, email: String, age: Int): User
}

For DELETE:

graphql
type Mutation {
  deleteUser(id: ID!): Boolean
}

Type Mapping in Resolvers

The resolver builder maps GraphQL types to query parameter types:

  • Enum types are used directly as parameters
  • Geospatial types are converted to input types: PointInputPoint, LineStringInputLineString, PolygonInputPolygon, CollectionInputCollection
  • @format(type: "...") directive can specify custom type mappings like ULID, UUID, Hex
  • Array types and object references are excluded from query filters

Best Practices

Schema Design

  1. Start Simple: Begin with core entities, add relationships later
  2. Primary Keys Always: Every database table needs a primary key
  3. Use Enums: For fixed value sets instead of strings
  4. Nullable Wisely: Required fields (!) enforce data integrity
  5. Unique Constraints: Use for fields that must be unique (emails, usernames)

Relationship Design

  1. One-to-One: Always use unique constraint on foreign key
  2. One-to-Many: Foreign key on the "many" side
  3. Many-to-Many: Always create explicit junction table with composite primary key
  4. Back References: Use sparingly to avoid circular dependencies

Constraints

  1. Validate at Database Level: Use check constraints for data integrity
  2. Default Values: Set sensible defaults for optional fields
  3. Check Constraints: Enforce business rules at database level
  4. Multiple Constraints: Combine for comprehensive validation

Naming Conventions

  1. Types: PascalCase (User, OrderItem)
  2. Fields: camelCase (firstName, createdAt)
  3. Enums: UPPERCASE (ADMIN, PENDING)
  4. Junction Tables: Combine type names (UserRole, PostTag)

Performance

  1. Indexes: Add unique constraints which create indexes
  2. Eager Loading: Use @mapping for efficient relationship queries
  3. Pagination: Always provide list queries with limit/offset
  4. JSON Fields: Use sparingly for unstructured data

Schema Evolution

  1. Adding Fields: Safe, no migration issues
  2. Removing Fields: Create migration to drop column
  3. Changing Types: Requires careful migration
  4. Adding Relationships: Add foreign key constraints carefully

Testing Schema Changes

  1. Validate schema before generation
  2. Test in development environment first
  3. Review generated migrations
  4. Test queries with new schema
  5. Check for breaking changes in API

Common Patterns

Audit Fields

The DDK model generator automatically recognizes three special field names (createdAt, updatedAt, deletedAt) defined in autoDBFields. When these fields appear in your schema, they receive standard GORM tags:

  • createdAtCreatedAt int with gorm:"column:createdAt;" — Set to current time on creation
  • updatedAtUpdatedAt int with gorm:"column:updatedAt;" — Set to current unix seconds on update
  • deletedAtDeletedAt int with gorm:"column:deletedAt;" — Populated on soft delete
graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  email: String!
  createdAt: String @constraint(type: "default", value: "now()")
  updatedAt: String @constraint(type: "default", value: "now()")
  createdBy: String
  updatedBy: String
}

Soft Deletes

graphql
type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  email: String!
  deletedAt: String # Populated on delete
}

Note: DDK automatically handles soft deletes via GORM.

Multi-tenant Schema

graphql
type Organization @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  users: [User]!
    @mapping(
      type: "one2many"
      foreignKey: "organizationId"
      foreignKeyReference: "id"
    )
}

type User @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  organizationId: String!
}

Hierarchical Data

graphql
type Category @required(type: "CREATE,READ,UPDATE,DELETE", table: "true") {
  id: ID! @constraint(type: "primarykey")
  name: String!
  parentId: String
  parent: Category
    @mapping(type: "backRef", foreignKey: "id", foreignKeyReference: "parentId")
}

Validation Checklist

Before generating your server, verify:

  • [ ] All types have @required directive
  • [ ] All database tables have primary key
  • [ ] Foreign keys match referenced field types
  • [ ] One-to-one relationships have unique constraint
  • [ ] Many-to-many relationships have junction table
  • [ ] Junction tables have composite primary keys
  • [ ] Check constraints use valid SQL
  • [ ] Default values are appropriate for field types
  • [ ] Enum values are uppercase
  • [ ] No circular dependencies in back references
  • [ ] Field names follow conventions
  • [ ] Required fields use ! notation

User documentation for Optimal Reality