This guide shows how to create a typed OpenSya Persistence runtime, register your metadata and connect it to a database through an adapter.
OpenSya Persistence is database-independent. PostgreSQL with Drizzle is used throughout this guide because it is the first official adapter, but the core runtime is not coupled to PostgreSQL or Drizzle. Additional adapters can target databases such as MySQL, SQLite or MongoDB.
pnpm add @opensya/persistence drizzle-orm pg
pnpm add -D @types/pg
Persistence currently ships a Drizzle PostgreSQL adapter. Your physical tables must already exist through migrations.
Use defineTable() instead of annotating the object with TableMetadata. The
helper validates the shape while preserving literal names for type inference.
import { defineTable } from '@opensya/persistence'
export const usersMetadata = defineTable({
name: 'users',
collectionName: 'users',
columns: [
{
name: 'id',
columnName: 'id',
type: 'uuid',
nullable: false,
primaryKey: true,
unique: true,
default: () => crypto.randomUUID(),
validators: []
},
{
name: 'email',
columnName: 'email',
type: 'string',
nullable: false,
primaryKey: false,
unique: true,
validators: [
{
name: 'email-format',
validate(value) {
return typeof value === 'string' && value.includes('@')
? { valid: true }
: { valid: false, message: 'Enter a valid email address.' }
}
}
]
},
{
name: 'createdAt',
columnName: 'created_at',
type: 'timestamp',
nullable: false,
primaryKey: false,
unique: false,
default: () => new Date(),
validators: []
}
],
relations: [],
tableValidators: []
})
import {
createPostgreAdapter,
createHooksRegistry,
createMetadataRegistry,
createQueryEngine
} from '@opensya/persistence'
import { usersMetadata } from './users.metadata.js'
const registry = createMetadataRegistry(usersMetadata)
registry.lock()
const adapter = createPostgreAdapter(db)
const hooks = createHooksRegistry()
export const engine = createQueryEngine({ registry, adapter, hooks })
await engine.schema.createTables()
createTables() creates missing physical tables through the active adapter and
builds the runtime table objects used by queries. The operation is idempotent
and does not delete existing tables or data.
You can also keep registration chainable:
const registry = createMetadataRegistry()
.register(usersMetadata)
.register(postsMetadata)
registry.lock()
const user = await engine.create('users', {
email: 'john@example.com'
})
// user is inferred as:
// { id: string; email: string; createdAt: Date }
const saved = await engine.findOne('users', {
where: {
conditions: [
{ field: 'id', operator: 'eq', value: user.id }
]
}
})
No User interface or explicit generic is required. If an application needs a
custom projection, explicit generics remain supported.
const user = await engine.findOne<PublicUser>('users', { where })