edinburgh 0.1.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,18 @@
1
- import { DatabaseError } from "olmdb";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { TypeWrapper } from "./types.js";
3
- import { BaseIndex, PrimaryIndex } from "./indexes.js";
3
+ export declare const txnStorage: AsyncLocalStorage<Transaction>;
4
+ /**
5
+ * Returns the current transaction from AsyncLocalStorage.
6
+ * Throws if called outside a transact() callback.
7
+ * @internal
8
+ */
9
+ export declare function currentTxn(): Transaction;
10
+ export interface Transaction {
11
+ id: number;
12
+ instances: Set<Model<unknown>>;
13
+ instancesByPk: Map<number, Model<unknown>>;
14
+ }
15
+ import { BaseIndex as BaseIndex, PrimaryIndex, IndexRangeIterator } from "./indexes.js";
4
16
  /**
5
17
  * Configuration interface for model fields.
6
18
  * @template T - The field type.
@@ -35,21 +47,10 @@ export interface FieldConfig<T> {
35
47
  */
36
48
  export declare function field<T>(type: TypeWrapper<T>, options?: Partial<FieldConfig<T>>): T;
37
49
  export declare const modelRegistry: Record<string, typeof Model>;
38
- export declare function resetModelCaches(): void;
39
- type OnSaveType = (model: InstanceType<typeof Model>, newKey: Uint8Array | undefined, oldKey: Uint8Array | undefined) => void;
40
- /**
41
- * Set a callback function to be called after a model is saved and committed.
42
- *
43
- * @param callback The callback function to set. As arguments, it receives the model instance, the new key (undefined in case of a delete), and the old key (undefined in case of a create).
44
- */
45
- export declare function setOnSaveCallback(callback: OnSaveType | undefined): void;
50
+ export type Change = Record<any, any> | "created" | "deleted";
46
51
  /**
47
52
  * Register a model class with the Edinburgh ORM system.
48
53
  *
49
- * This decorator function transforms the model class to use a proxy-based constructor
50
- * that enables change tracking and automatic field initialization. It also extracts
51
- * field metadata and sets up default values on the prototype.
52
- *
53
54
  * @template T - The model class type.
54
55
  * @param MyModel - The model class to register.
55
56
  * @returns The enhanced model class with ORM capabilities.
@@ -66,9 +67,6 @@ export declare function setOnSaveCallback(callback: OnSaveType | undefined): voi
66
67
  */
67
68
  export declare function registerModel<T extends typeof Model<unknown>>(MyModel: T): T;
68
69
  export declare function getMockModel<T extends typeof Model<unknown>>(OrgModel: T): T;
69
- /** @internal Symbol used to attach modified instances to running transaction */
70
- export declare const MODIFIED_INSTANCES_SYMBOL: unique symbol;
71
- /** @internal Symbol used to access the underlying model from a proxy */
72
70
  /**
73
71
  * Model interface that ensures proper typing for the constructor property.
74
72
  * @template SUB - The concrete model subclass.
@@ -83,62 +81,145 @@ export interface Model<SUB> {
83
81
  * change tracking, and relationship management. All model classes should extend
84
82
  * this base class and be decorated with `@registerModel`.
85
83
  *
84
+ * ### Schema Evolution
85
+ *
86
+ * Edinburgh tracks the schema version of each model automatically. When you add, remove, or
87
+ * change the types of fields, or add/remove indexes, Edinburgh detects the new schema version.
88
+ *
89
+ * **Lazy migration:** Changes to non-key field values are migrated lazily, when a row with an
90
+ * old schema version is read from disk, it is deserialized using the old schema and optionally
91
+ * transformed by the static `migrate()` function. This happens transparently on every read
92
+ * and requires no downtime or batch processing.
93
+ *
94
+ * **Batch migration (via `npx migrate-edinburgh` or `runMigration()`):** Certain schema changes
95
+ * require an explicit migration run:
96
+ * - Adding or removing secondary/unique indexes
97
+ * - Changing the fields or types of an existing index
98
+ * - A `migrate()` function that changes values used in secondary index fields
99
+ *
100
+ * The batch migration tool populates new indexes, deletes orphaned ones, and updates index
101
+ * entries whose values were changed by `migrate()`. It does *not* rewrite primary data rows
102
+ * (lazy migration handles that).
103
+ *
104
+ * ### Lifecycle Hooks
105
+ *
106
+ * - **`static migrate(record)`**: Called when deserializing rows written with an older schema
107
+ * version. Receives a plain record object; mutate it in-place to match the current schema.
108
+ * See {@link Model.migrate}.
109
+ *
110
+ * - **`preCommit()`**: Called on each modified instance right before the transaction commits.
111
+ * Useful for computing derived fields, enforcing cross-field invariants, or creating related
112
+ * instances. See {@link Model.preCommit}.
113
+ *
86
114
  * @template SUB - The concrete model subclass (for proper typing).
87
115
  *
88
116
  * @example
89
117
  * ```typescript
90
118
  * ⁣@E.registerModel
91
119
  * class User extends E.Model<User> {
92
- * static pk = E.index(User, ["id"], "primary");
120
+ * static pk = E.primary(User, "id");
93
121
  *
94
122
  * id = E.field(E.identifier);
95
123
  * name = E.field(E.string);
96
124
  * email = E.field(E.string);
97
125
  *
98
- * static byEmail = E.index(User, "email", "unique");
126
+ * static byEmail = E.unique(User, "email");
99
127
  * }
100
128
  * ```
101
129
  */
102
130
  export declare abstract class Model<SUB> {
103
- /** @internal Primary key index for this model. */
104
- static _pk?: PrimaryIndex<any, any>;
105
- /** @internal All indexes for this model, the primary key being first. */
106
- static _indexes?: BaseIndex<any, any>[];
131
+ static _primary: PrimaryIndex<any, any>;
132
+ /** @internal All non-primary indexes for this model. */
133
+ static _secondaries?: BaseIndex<any, readonly (keyof any & string)[]>[];
107
134
  /** The database table name (defaults to class name). */
108
135
  static tableName: string;
136
+ /** When true, registerModel replaces an existing model with the same tableName. */
137
+ static override?: boolean;
109
138
  /** Field configuration metadata. */
110
- static fields: Record<string, FieldConfig<unknown>>;
111
- /** @internal Field configuration for this instance. */
112
- _fields: Record<string, FieldConfig<unknown>>;
139
+ static fields: Record<string | symbol | number, FieldConfig<unknown>>;
140
+ /**
141
+ * Optional migration function called when deserializing rows written with an older schema version.
142
+ * Receives a plain record with all fields (primary key fields + value fields) and should mutate it
143
+ * in-place to match the current schema.
144
+ *
145
+ * This is called both during lazy loading (when a row is read from disk) and during batch
146
+ * migration (via `runMigration()` / `npx migrate-edinburgh`). The function's source code is hashed
147
+ * to detect changes. Modifying `migrate()` triggers a new schema version.
148
+ *
149
+ * If `migrate()` changes values of fields used in secondary or unique indexes, those indexes
150
+ * will only be updated when `runMigration()` is run (not during lazy loading).
151
+ *
152
+ * @param record - A plain object with all field values from the old schema version.
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * ⁣@E.registerModel
157
+ * class User extends E.Model<User> {
158
+ * static pk = E.primary(User, "id");
159
+ * id = E.field(E.identifier);
160
+ * name = E.field(E.string);
161
+ * role = E.field(E.string); // new field
162
+ *
163
+ * static migrate(record: Record<string, any>) {
164
+ * record.role ??= "user"; // default for rows that predate the 'role' field
165
+ * }
166
+ * }
167
+ * ```
168
+ */
169
+ static migrate?(record: Record<string, any>): void;
113
170
  /**
114
- * @internal State tracking for this model instance:
115
- * - undefined: new instance, unmodified
116
- * - 1: new instance, modified (and in modifiedInstances)
117
- * - 2: loaded from disk, unmodified
118
- * - 3: persistence disabled
119
- * - array: loaded from disk, modified (and in modifiedInstances), array values are original index buffers
171
+ * @internal
172
+ * - _oldValues===undefined: New instance, not yet saved.
173
+ * - _oldValues===null: Instance is to be deleted.
174
+ * - _oldValues is an object: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values
120
175
  */
121
- _state: undefined | 1 | 2 | 3 | Array<Uint8Array>;
176
+ _oldValues: Record<string, any> | undefined | null;
177
+ _primaryKey: Uint8Array | undefined;
178
+ _primaryKeyHash: number | undefined;
179
+ _txn: Transaction;
122
180
  constructor(initial?: Partial<Omit<SUB, "constructor">>);
123
- _save(): void;
124
181
  /**
125
- * Load a model instance by primary key.
126
- * @param args - Primary key field values.
127
- * @returns The model instance if found, undefined otherwise.
182
+ * Optional hook called on each modified instance right before the transaction commits.
183
+ * Runs before data is written to disk, so changes made here are included in the commit.
184
+ *
185
+ * Common use cases:
186
+ * - Computing derived or denormalized fields
187
+ * - Enforcing cross-field validation rules
188
+ * - Creating or updating related model instances (newly created instances will also
189
+ * have their `preCommit()` called)
128
190
  *
129
191
  * @example
130
192
  * ```typescript
131
- * const user = User.load("user123");
132
- * const post = Post.load("post456", "en");
193
+ * ⁣@E.registerModel
194
+ * class Post extends E.Model<Post> {
195
+ * static pk = E.primary(Post, "id");
196
+ * id = E.field(E.identifier);
197
+ * title = E.field(E.string);
198
+ * slug = E.field(E.string);
199
+ *
200
+ * preCommit() {
201
+ * this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
202
+ * }
203
+ * }
133
204
  * ```
134
205
  */
135
- static load<SUB>(this: typeof Model<SUB>, ...args: any[]): SUB | undefined;
206
+ preCommit?(): void;
207
+ static _delayedInit(cleared?: boolean): Promise<void>;
208
+ _setLoadedField(fieldName: string, value: any): void;
209
+ /**
210
+ * @returns The primary key for this instance.
211
+ */
212
+ getPrimaryKey(): Uint8Array;
213
+ _setPrimaryKey(key: Uint8Array, hash?: number): void;
214
+ /**
215
+ * @returns A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
216
+ */
217
+ getPrimaryKeyHash(): number;
218
+ isLazyField(field: keyof this): boolean;
219
+ _write(txn: Transaction): undefined | Change;
136
220
  /**
137
221
  * Prevent this instance from being persisted to the database.
138
222
  *
139
- * Removes the instance from the modified instances set and disables
140
- * automatic persistence at transaction commit.
141
- *
142
223
  * @returns This model instance for chaining.
143
224
  *
144
225
  * @example
@@ -149,6 +230,26 @@ export declare abstract class Model<SUB> {
149
230
  * ```
150
231
  */
151
232
  preventPersist(): this;
233
+ /**
234
+ * Find all instances of this model in the database, ordered by primary key.
235
+ * @param opts - Optional parameters.
236
+ * @param opts.reverse - If true, iterate in reverse order.
237
+ * @returns An iterator.
238
+ */
239
+ static findAll<T extends typeof Model<unknown>>(this: T, opts?: {
240
+ reverse?: boolean;
241
+ }): IndexRangeIterator<T>;
242
+ /**
243
+ * Load an existing instance by primary key and update it, or create a new one.
244
+ *
245
+ * The provided object must contain all primary key fields. If a matching row exists,
246
+ * the remaining properties from `obj` are set on the loaded instance. Otherwise a
247
+ * new instance is created with `obj` as its initial properties.
248
+ *
249
+ * @param obj - Partial model data that **must** include every primary key field.
250
+ * @returns The loaded-and-updated or newly created instance.
251
+ */
252
+ static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<Omit<InstanceType<T>, "constructor">>): InstanceType<T>;
152
253
  /**
153
254
  * Delete this model instance from the database.
154
255
  *
@@ -175,7 +276,7 @@ export declare abstract class Model<SUB> {
175
276
  * }
176
277
  * ```
177
278
  */
178
- validate(raise?: boolean): DatabaseError[];
279
+ validate(raise?: boolean): Error[];
179
280
  /**
180
281
  * Check if this model instance is valid.
181
282
  * @returns true if all validations pass.
@@ -187,6 +288,6 @@ export declare abstract class Model<SUB> {
187
288
  * ```
188
289
  */
189
290
  isValid(): boolean;
291
+ getState(): "deleted" | "created" | "loaded" | "lazy";
292
+ toString(): string;
190
293
  }
191
- export declare const modificationTracker: ProxyHandler<any>;
192
- export {};