Skip to content

Adding a New ORM

Codabra uses the Strategy Pattern for ORM integration. All ORM-specific code is isolated behind the OrmAdapter interface in packages/core/src/orm/index.ts.

Each ORM adapter also supports one or more database dialects via an internal DialectConfig object. This means you can add a new ORM that works with multiple databases without touching any existing code.

Step 1: Create the adapter file

bash
mkdir packages/core/src/orm/myorm
touch packages/core/src/orm/myorm/index.ts

Step 2: Implement OrmAdapter

The adapter receives the chosen database string in its constructor and uses it to dispatch to the correct dialect configuration.

ts
import type { OrmAdapter } from '../index';
import type { ModelDefinition } from '../../types';

// ── Dialect config (internal) ────────────────────────────────────────────────

interface MyOrmDialectConfig {
  generateSchemaFile(models: ModelDefinition[]): string;
  generateClientFile(): string;
  getDependencies(): Record<string, string>;
  getDevDependencies(): Record<string, string>;
}

const sqliteDialect: MyOrmDialectConfig = {
  generateSchemaFile: (models) =>
    `// schema for SQLite — ${models.map((m) => m.name).join(', ')}\n`,
  generateClientFile: () =>
    `// Auto-generated by Codabra\nexport const db = new MyOrm('sqlite.db');\n`,
  getDependencies: () => ({ 'my-orm': '^1.0.0' }),
  getDevDependencies: () => ({}),
};

const postgresqlDialect: MyOrmDialectConfig = {
  generateSchemaFile: (models) =>
    `// schema for PostgreSQL — ${models.map((m) => m.name).join(', ')}\n`,
  generateClientFile: () =>
    `// Auto-generated by Codabra\nexport const db = new MyOrm(process.env.DATABASE_URL!);\n`,
  getDependencies: () => ({ 'my-orm': '^1.0.0' }),
  getDevDependencies: () => ({}),
};

const DIALECTS: Record<string, MyOrmDialectConfig> = {
  sqlite: sqliteDialect,
  postgresql: postgresqlDialect,
};

// ── Adapter ──────────────────────────────────────────────────────────────────

export class MyOrmAdapter implements OrmAdapter {
  readonly name = 'myorm';
  readonly label = 'My ORM';

  private readonly dialect: MyOrmDialectConfig;

  constructor(database: string) {
    const dialect = DIALECTS[database];
    if (!dialect) {
      throw new Error(
        `My ORM does not support database "${database}". Supported: ${Object.keys(DIALECTS).join(', ')}`,
      );
    }
    this.dialect = dialect;
  }

  getSchemaFilePath(): string {
    return 'myorm/schema.ts';
  }
  getClientFilePath(): string {
    return 'src/lib/client.ts';
  }

  generateSchemaFile(models: ModelDefinition[]): string {
    return this.dialect.generateSchemaFile(models);
  }

  generateClientFile(): string {
    return this.dialect.generateClientFile();
  }

  getRouteImports(_modelName: string): string[] {
    return [`import { db } from '@/lib/client';`];
  }

  getFunctionImports(): string[] {
    return [`import { db } from '@/lib/client';`];
  }

  routeFindMany(modelName: string) {
    return `await db.findMany('${modelName}')`;
  }
  routeFindUnique(modelName: string) {
    return `await db.findOne('${modelName}', id)`;
  }
  routeCreate(modelName: string, dataExpr: string) {
    return `await db.create('${modelName}', ${dataExpr})`;
  }
  routeUpdate(modelName: string, _whereExpr: string, dataExpr: string) {
    return `await db.update('${modelName}', id, ${dataExpr})`;
  }
  routeDelete(modelName: string, _whereExpr: string) {
    return `await db.delete('${modelName}', id)`;
  }
  routeLoadResource(modelName: string) {
    return `await db.findOne('${modelName}', id)`;
  }
  getCreateBodyTypecast(_modelName: string) {
    return `body as Record<string, unknown>`;
  }
  getUpdateBodyTypecast(_modelName: string) {
    return `body as Record<string, unknown>`;
  }
  fnCreate(modelName: string, dataExpr: string) {
    return `await db.create('${modelName}', ${dataExpr})`;
  }
  fnUpdate(modelName: string, whereExpr: string, dataExpr: string) {
    return `await db.update('${modelName}', ${whereExpr}.id, ${dataExpr})`;
  }
  fnDelete(modelName: string, whereExpr: string) {
    return `await db.delete('${modelName}', ${whereExpr}.id)`;
  }
  fnFindMany(modelName: string, whereExpr?: string) {
    return `await db.findMany('${modelName}'${whereExpr ? `, ${whereExpr}` : ''})`;
  }

  getDependencies(): Record<string, string> {
    return this.dialect.getDependencies();
  }
  getDevDependencies(): Record<string, string> {
    return this.dialect.getDevDependencies();
  }
}

Step 3: Register it

Open packages/core/src/orm/index.ts. Add an ESM import at the top, then add an entry to ormRegistry:

ts
import { MyOrmAdapter } from './myorm';

export const ormRegistry: OrmRegistryEntry[] = [
  // ... existing entries ...
  {
    name: 'myorm',
    label: 'My ORM',
    supportedDatabases: [sqlite, postgresql], // import from ./dialects
    defaultDatabase: 'sqlite',
    create: (database) => new MyOrmAdapter(database),
  },
];

No require(). The project is ESM ("type": "module"). Always use static imports.

Step 4: Export it

Add to packages/core/src/index.ts:

ts
export { MyOrmAdapter } from './orm/myorm';

Step 5: Update the JSON Schema

Add "myorm" to the orm enum in packages/core/src/schemas/codabra.schema.json:

json
"orm": {
    "type": "string",
    "enum": ["drizzle", "prisma", "myorm"]
}

Step 6: Test it

bash
# In the monorepo root — create a project with your new ORM:
node packages/cli/bin/create-codabra.js my-test-app
# Select: My ORM → SQLite → ...

# Or generate directly:
node packages/cli/bin/codabra.js generate

Check the generated files in apps/nextjs/.


Want to support a new database for your ORM?

See Adding a New Database — it shows how to add a dialect config object without modifying any existing code.

Released under the Elastic License 2.0.