Skip to main 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

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.

packages/core/src/orm/myorm/index.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:

packages/core/src/orm/index.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:

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:

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

Step 6: Test it

# 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/web/.


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.