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
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 CLInpm install package- npm CLIdatabridge 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 valueand-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:
databridge init- Initialize project (creates config files)databridge introspect- Read database structure into Prisma schemadatabridge generate- Generate API, OpenAPI spec, Swagger UI, and servicesdatabridge generate-sdk- Generate client SDKs in 50+ languages (Python, Go, etc.)databridge serve- Start the development server (deprecated - usenpm run devinstead)
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 textnumber- Numeric inputpassword- Hidden textlist- Single selectioncheckbox- Multiple selectionsconfirm- Yes/Noeditor- 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 Type | Prisma Type | TypeScript Type |
|---|---|---|
| INT | Int | number |
| BIGINT | BigInt | bigint |
| FLOAT | Float | number |
| DOUBLE | Float | number |
| DECIMAL | Decimal | Prisma.Decimal |
| VARCHAR | String | string |
| TEXT | String | string |
| BOOLEAN | Boolean | boolean |
| DATE | DateTime | Date |
| TIMESTAMP | DateTime | Date |
| JSON | Json | any |
| ENUM | Enum | union 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 dataPUT/PATCH- Update existing dataDELETE- 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
| Technology | Purpose | Why |
|---|---|---|
| oclif | CLI framework | Professional, feature-rich |
| inquirer | User prompts | Interactive UX |
| Prisma | ORM + introspection | Best-in-class MySQL tooling |
| ts-morph | Code generation | AST manipulation for TypeScript |
| Fastify | Web server | Fast, 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?
Thank you for your feedback!