Core Concepts

Understanding the key technologies and patterns behind DataBridge - CLI frameworks, Prisma, code generation, and type safety

Core Concepts

Understanding the key technologies and patterns used in DataBridge.


Table of Contents

  1. CLI Frameworks
  2. Database Introspection
  3. Code Generation
  4. Web Frameworks
  5. TypeScript Patterns

CLI Frameworks

What is a CLI?

CLI (Command-Line Interface) is a text-based program that runs in a terminal.

Examples:

  • git commit -m "message" - Git CLI
  • npm install package - npm CLI
  • databridge generate - DataBridge CLI

Anatomy of a CLI Command

databridge generate --db mysql://localhost:3306/mydb
│          │         │
│          │         └─ Flag (optional parameter)
│          └─ Command (action to perform)
└─ Program name

Why oclif?

oclif (Open CLI Framework) by Heroku is industry-standard for building CLIs.

Features:

  • Auto-discovery: Finds commands in src/commands/ automatically
  • Flag parsing: Handles --flag value and -f value
  • Help generation: Creates help text from your code
  • Plugin system: Extensible architecture
  • TypeScript: First-class TypeScript support

Alternatives:

  • Commander.js - Simpler but manual setup
  • Yargs - Good for Node.js scripts
  • Inquirer - Only for prompts (we use this with oclif)

oclif Command Structure

Every oclif command extends the Command class:

import { Command, Flags } from '@oclif/core';

export default class MyCommand extends Command {
  // Shown in help text
  static description = 'Does something cool';
  
  // Optional parameters
  static flags = {
    name: Flags.string({
      char: 'n',              // Short form: -n
      description: 'Your name',
      required: false,
      default: 'World',
    }),
    force: Flags.boolean({
      char: 'f',              // Short form: -f
      description: 'Force operation',
      default: false,
    }),
  };
  
  // Required arguments (positional)
  static args = [
    {
      name: 'file',
      description: 'File to process',
      required: true,
    },
  ];

  // Main function - runs when command is called
  async run(): Promise<void> {
    const { args, flags } = await this.parse(MyCommand);
    
    this.log(`Hello ${flags.name}!`);
    this.log(`Processing file: ${args.file}`);
    
    if (flags.force) {
      this.log('Force mode enabled');
    }
  }
}

Usage:

databridge mycommand myfile.txt --name Alice --force
# Output:
# Hello Alice!
# Processing file: myfile.txt
# Force mode enabled

DataBridge Commands

DataBridge has 5 main commands:

  1. databridge init - Initialize project (creates config files)
  2. databridge introspect - Read database structure into Prisma schema
  3. databridge generate - Generate API, OpenAPI spec, Swagger UI, and services
  4. databridge generate-sdk - Generate client SDKs in 50+ languages (Python, Go, etc.)
  5. databridge serve - Start the development server (deprecated - use npm run dev instead)

New in v0.2: OpenAPI 3.0 generation and multi-language SDK support. See API Tutorials for details.

Interactive Prompts with inquirer

inquirer asks users questions interactively:

import inquirer from 'inquirer';

const answers = await inquirer.prompt([
  {
    type: 'input',           // Text input
    name: 'username',        // Key in answers object
    message: 'Enter username:',
    default: 'admin',
    validate: (input) => {
      if (input.length < 3) {
        return 'Username must be at least 3 characters';
      }
      return true;
    },
  },
  {
    type: 'password',        // Hidden input
    name: 'password',
    message: 'Enter password:',
    mask: '*',
  },
  {
    type: 'list',            // Single choice
    name: 'role',
    message: 'Select role:',
    choices: ['admin', 'user', 'guest'],
  },
  {
    type: 'checkbox',        // Multiple choices
    name: 'permissions',
    message: 'Select permissions:',
    choices: ['read', 'write', 'delete'],
  },
  {
    type: 'confirm',         // Yes/No
    name: 'confirmed',
    message: 'Continue?',
    default: true,
  },
]);

console.log(answers);
// {
//   username: 'alice',
//   password: 'secret123',
//   role: 'admin',
//   permissions: ['read', 'write'],
//   confirmed: true
// }

Prompt Types:

  • input - Free text
  • number - Numeric input
  • password - Hidden text
  • list - Single selection
  • checkbox - Multiple selections
  • confirm - Yes/No
  • editor - Opens text editor

Database Introspection

What is Introspection?

Introspection = Reading database structure to generate code

Real-world analogy:

  • You walk into a library (database)
  • You look at the catalog (introspection)
  • You create an index card system (generated schema)

How Prisma Introspection Works

MySQL Database              Prisma reads structure        schema.prisma
┌─────────────────┐                                    ┌──────────────────┐
│  products       │                                    │ model products { │
│  - id (int PK)  │  ─────────────────────────────>   │   id Int @id     │
│  - name (text)  │      Prisma introspection         │   name String    │
│  - price (float)│                                    │   price Float    │
└─────────────────┘                                    │ }                │
                                                       └──────────────────┘

What Prisma reads:

  • Table names → Model names
  • Column names → Field names
  • Data types → Prisma types
  • Primary keys → @id
  • Foreign keys → @relation
  • Unique constraints → @unique
  • Default values → @default
  • Indexes → @@index

Prisma Schema Format

// Data source (database connection)
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// Client generator (creates Prisma Client for queries)
generator client {
  provider = "prisma-client-js"
}

// Model (represents a table)
model Product {
  id          Int       @id @default(autoincrement())
  name        String    @db.VarChar(255)
  description String?   @db.Text
  price       Decimal   @db.Decimal(10, 2)
  stock       Int       @default(0)
  categoryId  Int
  createdAt   DateTime  @default(now()) @map("created_at")
  
  // Relation to Category
  category    Category  @relation(fields: [categoryId], references: [id])
  
  @@index([categoryId])
  @@map("products")
}

model Category {
  id       Int       @id @default(autoincrement())
  name     String    @unique
  products Product[]
  
  @@map("categories")
}

Key concepts:

  • @id - Primary key
  • @default(autoincrement()) - Auto-increment
  • @unique - Unique constraint
  • @relation - Foreign key relationship
  • @map - Custom column name
  • @@map - Custom table name
  • @@index - Database index
  • ? after type - Optional field

Running Introspection

# Set database URL
export DATABASE_URL="mysql://root:password@localhost:3306/mydb"

# Run introspection (updates schema.prisma)
npx prisma db pull

# Generate Prisma Client (creates JavaScript/TypeScript code)
npx prisma generate

Type Mappings

MySQL → Prisma:

MySQL TypePrisma TypeTypeScript Type
INTIntnumber
BIGINTBigIntbigint
FLOATFloatnumber
DOUBLEFloatnumber
DECIMALDecimalPrisma.Decimal
VARCHARStringstring
TEXTStringstring
BOOLEANBooleanboolean
DATEDateTimeDate
TIMESTAMPDateTimeDate
JSONJsonany
ENUMEnumunion type

Code Generation

What is Code Generation?

Code generation = Writing code that writes code

Why?

  • Eliminate repetitive tasks
  • Ensure consistency
  • Save developer time
  • Reduce human errors

Examples:

  • React component generators
  • Prisma Client generation
  • GraphQL schema generators
  • DataBridge API/SDK generation

AST (Abstract Syntax Tree)

AST is a tree representation of code structure.

Example code:

function add(a: number, b: number): number {
  return a + b;
}

AST representation:

{
  "type": "FunctionDeclaration",
  "name": "add",
  "parameters": [
    { "name": "a", "type": "number" },
    { "name": "b", "type": "number" }
  ],
  "returnType": "number",
  "body": {
    "type": "ReturnStatement",
    "expression": {
      "type": "BinaryExpression",
      "operator": "+",
      "left": { "name": "a" },
      "right": { "name": "b" }
    }
  }
}

ts-morph (TypeScript Manipulation)

ts-morph lets you create/modify TypeScript code programmatically.

Basic example:

import { Project } from 'ts-morph';

// Create in-memory TypeScript project
const project = new Project();

// Add new file
const sourceFile = project.createSourceFile('example.ts');

// Add import
sourceFile.addImportDeclaration({
  namedImports: ['Component'],
  moduleSpecifier: '@angular/core',
});

// Add class with decorator
const myClass = sourceFile.addClass({
  name: 'MyComponent',
  isExported: true,
  decorators: [{
    name: 'Component',
    arguments: [`{ selector: 'app-my' }`],
  }],
});

// Add property
myClass.addProperty({
  name: 'title',
  type: 'string',
  initializer: '"Hello World"',
});

// Add method
myClass.addMethod({
  name: 'ngOnInit',
  returnType: 'void',
  statements: 'console.log(this.title);',
});

// Save to disk
await sourceFile.save();

Generated code:

import { Component } from '@angular/core';

@Component({ selector: 'app-my' })
export class MyComponent {
  title: string = "Hello World";
  
  ngOnInit(): void {
    console.log(this.title);
  }
}

Template-Based Generation

Alternative to AST: string templates

function generateService(modelName: string): string {
  return `
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ${modelName} } from '../models/${modelName.toLowerCase()}';

@Injectable({ providedIn: 'root' })
export class ${modelName}Service {
  private url = '/api/${modelName.toLowerCase()}';

  constructor(private http: HttpClient) {}

  list(): Observable<${modelName}[]> {
    return this.http.get<${modelName}[]>(this.url);
  }

  getById(id: number): Observable<${modelName}> {
    return this.http.get<${modelName}>(\`\${this.url}/\${id}\`);
  }
}
  `.trim();
}

// Usage
const code = generateService('Product');
fs.writeFileSync('product.service.ts', code);

When to use templates vs AST:

  • Templates: Quick, simple, fixed structure
  • AST: Complex logic, conditional generation, refactoring

DMMF (Data Model Meta Format)

DMMF is Prisma’s JSON representation of your schema.

import { getDMMF } from '@prisma/internals';

const schema = `
model Product {
  id    Int     @id @default(autoincrement())
  name  String
  price Float
}
`;

const dmmf = await getDMMF({ datamodel: schema });

console.log(JSON.stringify(dmmf, null, 2));

Output:

{
  "datamodel": {
    "models": [
      {
        "name": "Product",
        "dbName": null,
        "fields": [
          {
            "name": "id",
            "kind": "scalar",
            "type": "Int",
            "isRequired": true,
            "isId": true,
            "default": { "name": "autoincrement" }
          },
          {
            "name": "name",
            "kind": "scalar",
            "type": "String",
            "isRequired": true
          },
          {
            "name": "price",
            "kind": "scalar",
            "type": "Float",
            "isRequired": true
          }
        ],
        "primaryKey": null,
        "uniqueFields": [],
        "uniqueIndexes": []
      }
    ],
    "enums": []
  }
}

Using DMMF to generate code:

for (const model of dmmf.datamodel.models) {
  console.log(`Generating code for ${model.name}`);
  
  for (const field of model.fields) {
    const tsType = mapPrismaTypeToTs(field.type);
    console.log(`  ${field.name}: ${tsType}`);
  }
}

function mapPrismaTypeToTs(prismaType: string): string {
  const typeMap: Record<string, string> = {
    Int: 'number',
    Float: 'number',
    String: 'string',
    Boolean: 'boolean',
    DateTime: 'Date',
  };
  return typeMap[prismaType] || 'any';
}

Web Frameworks

REST API Basics

REST = Representational State Transfer

HTTP Methods:

  • GET - Read data (list or single item)
  • POST - Create new data
  • PUT / PATCH - Update existing data
  • DELETE - Delete data

Example endpoints:

GET    /products       - List all products
GET    /products/123   - Get product with ID 123
POST   /products       - Create new product
PATCH  /products/123   - Update product 123
DELETE /products/123   - Delete product 123

Fastify Framework

Fastify is a fast, low-overhead web framework.

Basic server:

import Fastify from 'fastify';

const fastify = Fastify({
  logger: true,
});

// Define route
fastify.get('/hello', async (request, reply) => {
  return { message: 'Hello World' };
});

// Start server
await fastify.listen({ port: 3000 });
console.log('Server running on http://localhost:3000');

Route with parameters:

fastify.get('/users/:id', async (request, reply) => {
  const { id } = request.params as { id: string };
  return { userId: id };
});

// GET /users/123 → { userId: "123" }

Route with body:

fastify.post('/users', async (request, reply) => {
  const body = request.body as { name: string; email: string };
  
  // Save to database
  const user = await db.user.create({ data: body });
  
  reply.code(201); // Created
  return user;
});

Query parameters:

fastify.get('/search', async (request, reply) => {
  const { q, limit } = request.query as { q: string; limit?: string };
  
  return {
    query: q,
    limit: parseInt(limit || '10'),
  };
});

// GET /search?q=laptop&limit=5

Fastify Plugins

Plugins extend Fastify with reusable functionality.

import fp from 'fastify-plugin';

// Create plugin
const myPlugin = fp(async (fastify, options) => {
  // Add method to fastify instance
  fastify.decorate('utility', () => {
    return 'Utility function';
  });
  
  // Add hook (runs on every request)
  fastify.addHook('onRequest', async (request, reply) => {
    console.log(`Request: ${request.method} ${request.url}`);
  });
});

// Register plugin
await fastify.register(myPlugin);

// Use in routes
fastify.get('/test', async (request, reply) => {
  return { result: fastify.utility() };
});

Common plugins:

  • @fastify/cors - Enable CORS
  • @fastify/jwt - JWT authentication
  • @fastify/multipart - File uploads
  • @fastify/rate-limit - Rate limiting
  • @fastify/swagger - API documentation

CORS (Cross-Origin Resource Sharing)

CORS allows your API to accept requests from different origins.

import cors from '@fastify/cors';

await fastify.register(cors, {
  origin: '*',                    // Allow all origins
  // origin: 'http://localhost:4200',  // Allow specific origin
  // origin: ['http://localhost:4200', 'https://myapp.com'],  // Multiple
  methods: ['GET', 'POST', 'PATCH', 'DELETE'],
  credentials: true,              // Allow cookies
});

Why CORS is needed:

Without CORS:
Angular (localhost:4200) → API (localhost:3000)
❌ Browser blocks: "No 'Access-Control-Allow-Origin' header"

With CORS:
Angular (localhost:4200) → API (localhost:3000)
✅ API responds with: Access-Control-Allow-Origin: *

TypeScript Patterns

Type-Safe API Routes

import { FastifyRequest, FastifyReply } from 'fastify';

interface CreateUserBody {
  name: string;
  email: string;
  age?: number;
}

interface UserParams {
  id: string;
}

fastify.post<{ Body: CreateUserBody }>(
  '/users',
  async (request, reply) => {
    const { name, email, age } = request.body;
    // TypeScript knows types!
  }
);

fastify.get<{ Params: UserParams }>(
  '/users/:id',
  async (request, reply) => {
    const { id } = request.params;
    // id is typed as string
  }
);

Generics for Reusable Code

// Generic service for any model
class BaseService<T> {
  constructor(private model: any) {}
  
  async list(): Promise<T[]> {
    return this.model.findMany();
  }
  
  async getById(id: number): Promise<T | null> {
    return this.model.findUnique({ where: { id } });
  }
  
  async create(data: Partial<T>): Promise<T> {
    return this.model.create({ data });
  }
}

// Usage
interface User {
  id: number;
  name: string;
  email: string;
}

const userService = new BaseService<User>(prisma.user);
const users = await userService.list(); // Type: User[]

Utility Types

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  stock: number;
}

// Make all properties optional
type PartialProduct = Partial<Product>;
// { id?: number; name?: string; ... }

// Pick specific properties
type ProductPreview = Pick<Product, 'id' | 'name' | 'price'>;
// { id: number; name: string; price: number; }

// Exclude specific properties
type ProductWithoutId = Omit<Product, 'id'>;
// { name: string; price: number; description: string; stock: number; }

// Make all properties required
type RequiredProduct = Required<Product>;

// Make all properties readonly
type ImmutableProduct = Readonly<Product>;

Summary

Key Technologies

TechnologyPurposeWhy
oclifCLI frameworkProfessional, feature-rich
inquirerUser promptsInteractive UX
PrismaORM + introspectionBest-in-class MySQL tooling
ts-morphCode generationAST manipulation for TypeScript
FastifyWeb serverFast, modern, TypeScript-friendly

Design Patterns

  • Command Pattern: Each CLI command is self-contained
  • Plugin Pattern: Fastify plugins for modularity
  • Factory Pattern: Generate code from templates
  • Repository Pattern: Prisma models abstract database
  • Decorator Pattern: Angular services use @Injectable

Next: Dive Deeper

Was this page helpful?