import DataPack from "./datapack.js"; import { TypeWrapper, type FieldQueryArg, type FieldValue } from "./types.js"; import { type Transaction } from "./edinburgh.js"; import { PrimaryKey, NonPrimaryIndex, IndexRangeIterator, FindOptions, VersionInfo } from "./indexes.js"; /** * Configuration interface for model fields. * @template T - The field type. */ export interface FieldConfig { /** The type wrapper that defines how this field is serialized/validated. */ type: TypeWrapper; /** Optional human-readable description of the field. */ description?: string; /** Optional default value or function that generates default values. */ default?: T | ((model: Record) => T); } /** * Create a field definition for a model property. * * This function uses TypeScript magic to return the field configuration object * while appearing to return the actual field value type to the type system. * This allows for both runtime introspection and compile-time type safety. * * @template T - The field type. * @param type The type wrapper for this field. * @param options Additional field configuration options. * @returns The field value (typed as T, but actually returns FieldConfig). * * @example * ```typescript * const User = E.defineModel("User", class { * name = E.field(E.string, {description: "User's full name"}); * age = E.field(E.opt(E.number), {description: "User's age", default: 25}); * }); * ``` */ export declare function field>(type: TYPE, options?: Partial>>): FieldValue; export type Change = Record | "created" | "deleted"; type FieldsOf = T extends new () => infer I ? I : never; type ModelFields any> = FieldsOf; export interface ModelLookup { get(...args: PKA): any; } type ModelInstance = Model>; type PublicModelOf any> = Model>; type IndexSpec any> = (keyof ModelFields & string) | readonly (keyof ModelFields & string)[] | ((instance: PublicModelOf) => any); type PKArgs = PK extends readonly (keyof FIELDS & string)[] ? { [I in keyof PK]: PK[I] extends keyof FIELDS ? FieldQueryArg : never; } : PK extends keyof FIELDS & string ? [FieldQueryArg] : [string]; type IndexArgs = SPEC extends readonly (keyof FIELDS & string)[] ? { [I in keyof SPEC]: SPEC[I] extends keyof FIELDS ? FieldQueryArg : never; } : SPEC extends keyof FIELDS & string ? [FieldQueryArg] : SPEC extends (instance: any) => infer R ? R extends (infer V)[] ? [V] : [R] : never; type PublicIndexSpec = SPEC extends (instance: infer INSTANCE) => infer R ? (instance: INSTANCE) => R : SPEC; type PublicIndexSpecs = { [K in keyof SPECS]: PublicIndexSpec; }; /** * A model constructor with its generic information erased. * * Useful when accepting or storing arbitrary registered model classes. */ export type AnyModelClass = ModelClass; type StaticMembers any> = Pick>; type SecondaryRegistry = Record, readonly (keyof FIELDS & string)[], readonly any[]>>; export declare const modelRegistry: Record; export declare const pendingModelInits: Set; declare class ModelClassRuntime extends PrimaryKey, readonly (keyof FIELDS & string)[], PKA> { tableName: string; fields: Record>; _secondaries?: SecondaryRegistry; _nonKeyFields: (keyof FIELDS & string)[]; _lazyDescriptors: Record; _resetDescriptors: Record; _freezePrimaryKeyDescriptors: Record; _currentVersion: number; _currentMigrateHash: number; _versions: Map; _serializeVersionValue(): Uint8Array; _initialize(reset?: boolean): Promise; _getSecondary(name: string): NonPrimaryIndex, readonly (keyof FIELDS & string)[], readonly any[]>; _get(txn: Transaction, args: PKA | Uint8Array, loadNow: false | Uint8Array): ModelInstance; _get(txn: Transaction, args: PKA | Uint8Array, loadNow: true): ModelInstance | undefined; _lazyLoad(model: ModelInstance): void; /** * Load a model by primary key inside the current transaction. * * For `link(...)` primary-key fields, each argument may be the linked model * instance or the linked model's primary key. Composite linked primary keys * are passed as a tuple in that argument slot. * * @returns The matching model, or `undefined` if no row exists. */ get(...args: PKA): ModelInstance | undefined; /** * Load a model by primary key without fetching its non-key fields immediately. * * Link-valued primary-key fields accept the same shorthand as `get()`. * * Accessing a lazy field later will load the remaining fields transparently. */ getLazy(...args: PKA): ModelInstance; _pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): ModelInstance; /** * Load an existing instance by primary key and update it, or create a new one. * If a row already exists, its non-primary-key fields are updated in place. * Otherwise, a new instance is created with `obj` as its initial properties. * * @param obj Partial model data that **must** include every primary key field. * @returns The loaded-and-updated or newly created instance. */ replaceInto(obj: Partial): ModelInstance; /** * Look up a model through a named unique index. * * @param name The name from the model's `unique` definition. * @param args The unique-index key values. For `link(...)` fields, pass * either the linked model instance or the linked model's primary key. If the * linked model uses a composite primary key, pass the full tuple in that slot. * @returns The matching model instance, if any. */ getBy(name: K, ...args: IndexArgs): ModelInstance | undefined; /** * Query rows through a named unique or secondary index. * * This mirrors `find()`, but targets a named entry from the model's `unique` * or `index` registration. Link-valued index fields accept either the linked * model instance or the linked model's primary key tuple/value. */ findBy(name: K, opts: FindOptions, 'first'>): ModelInstance | undefined; findBy(name: K, opts: FindOptions, 'single'>): ModelInstance; findBy(name: K, opts?: FindOptions>): IndexRangeIterator>; /** * Process rows from a named unique or secondary index in batched transactions. * * Uses the same range options as `findBy()`, plus batch limits. */ batchProcessBy(name: K, opts: FindOptions> & { limitSeconds?: number; limitRows?: number; }, callback: (row: ModelInstance) => any): Promise; _loadValueFields(model: ModelInstance, valueArray: Uint8Array): void; _loadVersionInfo(txnId: number, version: number): VersionInfo; _migrateValueFields(model: ModelInstance, version: number, valuePack: DataPack): void; _serializeValue(data: Record): Uint8Array; } /** * Runtime base constructor for model classes returned by `defineModel()`. * * Prefer the `ModelClass` type alias for annotations and the result of * `defineModel()` for concrete model classes. */ export declare const ModelClass: typeof ModelClassRuntime; /** * The static side of a model class returned by `defineModel()`. * * Besides the class constructor itself, this includes primary-key lookup * helpers like `get()` and `getLazy()`, range-query helpers like `find()`, and * named-index helpers like `getBy()` and `findBy()`. * * @template FIELDS - The user-defined fields of the model instance. * @template PKA - Tuple of primary-key argument types. * @template UNIQUE - Named unique-index specifications. * @template INDEX - Named secondary-index specifications. */ export type ModelClass = STATICS & ModelClassRuntime & { new (initial?: Partial, txn?: Transaction): ModelInstance; }; /** * Minimal instance-side model shape used for typing the constructor property. */ export interface ModelBase { constructor: LOOKUP & AnyModelClass; } /** * Register a model class with the Edinburgh ORM system. * * Converts a plain class into a fully-featured model with database persistence, * typed fields, primary key access, and optional secondary and unique indexes. * * @param tableName The database table name for this model. * @param cls A plain class whose properties use E.field(). * @param opts Registration options. * @param opts.pk Primary key field name or array of field names. * @param opts.unique Named unique index specifications (field name, field array, or compute function). * @param opts.index Named secondary index specifications (field name, field array, or compute function). * @param opts.override Replace a previous model with the same table name. * @returns The enhanced model constructor. */ export declare function defineModel any, const PK extends (keyof ModelFields & string) | readonly (keyof ModelFields & string)[], const UNIQUE extends Record>, const INDEX extends Record>>(tableName: string, cls: T, opts?: { pk?: PK; unique?: UNIQUE; index?: INDEX; override?: boolean; }): ModelClass, ModelFields, PKArgs, PK>, PublicIndexSpecs, PublicIndexSpecs>; /** * Base class for all database models in the Edinburgh ORM. * * Models represent database entities with typed fields, automatic serialization, * change tracking, and relationship management. Model classes are created using * `E.defineModel()`. * * ### Schema Evolution * * Edinburgh tracks the schema version of each model automatically. When you add, remove, or * change the types of fields, or add/remove indexes, Edinburgh detects the new schema version. * * **Lazy migration:** Changes to non-key field values are migrated lazily, when a row with an * old schema version is read from disk, it is deserialized using the old schema and optionally * transformed by the static `migrate()` function. This happens transparently on every read * and requires no downtime or batch processing. * * **Batch migration (via `npx migrate-edinburgh` or `runMigration()`):** Certain schema changes * require an explicit migration run: * - Adding or removing secondary/unique indexes * - Changing the fields or types of an existing index * - A `migrate()` function that changes values used in secondary index fields * * The batch migration tool populates new indexes, deletes orphaned ones, and updates index * entries whose values were changed by `migrate()`. It does *not* rewrite primary data rows * (lazy migration handles that). * * ### Lifecycle Hooks * * - **`static migrate(record)`**: Called when deserializing rows written with an older schema * version. Receives a plain record object; mutate it in-place to match the current schema. * * - **`preCommit()`**: Called on each modified instance right before the transaction commits. * Useful for computing derived fields, enforcing cross-field invariants, or creating related * instances. * * @example * ```typescript * const User = E.defineModel("User", class { * id = E.field(E.identifier); * name = E.field(E.string); * email = E.field(E.string); * }, { * pk: "id", * unique: { email: "email" }, * }); * // Optional: declare a companion type so `let u: User` works. * // Not needed if you only use `new User()`, `User.find()`, etc. * type User = InstanceType; * ``` */ export declare abstract class ModelBase { /** * Optional migration function called when deserializing rows written with an older schema version. * Receives a plain record with all fields and should mutate it in-place to match the current schema. * It runs during lazy loading and during `runMigration()`. Changing this method creates a new schema version. * If it updates values used by secondary or unique indexes, those index entries are refreshed only by `runMigration()`. * * @param record A plain object containing the row's field values from the older schema version. * * @example * ```typescript * const User = E.defineModel("User", class { * id = E.field(E.identifier); * name = E.field(E.string); * role = E.field(E.string); * * static migrate(record: Record) { * record.role ??= "user"; * } * }, { pk: "id" }); * ``` */ static migrate?(record: Record): void; /** * @internal * - _oldValues===undefined: New instance, not yet saved. * - _oldValues===null: Instance is to be deleted. * - _oldValues===false: Instance excluded from persistence (preventPersist). * - _oldValues is an object: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values */ _oldValues: Record | undefined | null | false; _primaryKey: Uint8Array | undefined; _primaryKeyHash: number | undefined; _txn: Transaction; /** * Optional hook called on each modified instance right before the transaction commits. * Runs before data is written to disk, so changes made here are included in the commit. * * Common use cases: * - Computing derived or denormalized fields * - Enforcing cross-field validation rules * - Creating or updating related model instances (newly created instances will also * have their `preCommit()` called) * * @example * ```typescript * const Post = E.defineModel("Post", class { * id = E.field(E.identifier); * title = E.field(E.string); * slug = E.field(E.string); * * preCommit() { * this.slug = this.title.toLowerCase().replace(/\s+/g, "-"); * } * }, { pk: "id" }); * ``` */ preCommit?(): void; _setLoadedField(fieldName: string, value: any): void; _restoreLazyFields(): void; /** * @returns The primary key for this instance. */ getPrimaryKey(): Uint8Array; _setPrimaryKey(key: Uint8Array, hash?: number): void; /** * @returns A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved. */ getPrimaryKeyHash(): number; isLazyField(field: keyof this): boolean; _write(txn: Transaction): undefined | Change; /** * Prevent this instance from being persisted to the database. * * @returns This model instance for chaining. * * @example * ```typescript * const user = User.get("user123"); * user.name = "New Name"; * user.preventPersist(); // Changes won't be saved * ``` */ preventPersist(): this; /** * Delete this model instance from the database. * * Removes the instance and all its index entries from the database and prevents further persistence. * * @example * ```typescript * const user = User.get("user123"); * user.delete(); // Removes from database * ``` */ delete(): void; /** * Validate all fields in this model instance. * @param raise If true, throw on first validation error. * @returns Array of validation errors (empty if valid). * * @example * ```typescript * const user = new User(); * const errors = user.validate(); * if (errors.length > 0) { * console.log("Validation failed:", errors); * } * ``` */ validate(raise?: boolean): Error[]; /** * Check if this model instance is valid. * @returns true if all validations pass. * * @example * ```typescript * const user = new User({name: "John"}); * if (!user.isValid()) shoutAtTheUser(); * ``` */ isValid(): boolean; getState(): "deleted" | "created" | "loaded" | "lazy"; toString(): string; } /** * Delete every key/value entry in the database and reinitialize all registered models. * * This clears rows, index metadata, and schema-version records. It is mainly useful * for tests, local resets, or tooling that needs a completely empty database. */ export declare function deleteEverything(): Promise; /** * A model instance, including its user-defined fields. * @template FIELDS - The fields defined on this model. */ export type Model = FIELDS & ModelBase; export declare const Model: typeof ModelBase; export {};