edinburgh 0.4.5 → 0.5.0
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.
- package/README.md +268 -374
- package/build/src/datapack.js +1 -1
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +5 -5
- package/build/src/edinburgh.js +8 -7
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +46 -116
- package/build/src/indexes.js +148 -180
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate.js +11 -31
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +74 -49
- package/build/src/models.js +112 -165
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +30 -21
- package/build/src/types.js +16 -30
- package/build/src/types.js.map +1 -1
- package/package.json +1 -3
- package/skill/BaseIndex_batchProcess.md +1 -1
- package/skill/BaseIndex_find.md +2 -2
- package/skill/BaseIndex_find_2.md +7 -0
- package/skill/BaseIndex_find_3.md +7 -0
- package/skill/BaseIndex_find_4.md +7 -0
- package/skill/Model.md +5 -7
- package/skill/Model_batchProcess.md +8 -0
- package/skill/Model_delete.md +1 -1
- package/skill/Model_migrate.md +2 -4
- package/skill/Model_preCommit.md +2 -4
- package/skill/Model_preventPersist.md +1 -1
- package/skill/Model_replaceInto.md +2 -2
- package/skill/NonPrimaryIndex.md +10 -0
- package/skill/SKILL.md +146 -144
- package/skill/SecondaryIndex.md +2 -2
- package/skill/UniqueIndex.md +2 -2
- package/skill/defineModel.md +22 -0
- package/skill/field.md +2 -2
- package/skill/link.md +11 -9
- package/skill/transact.md +2 -2
- package/src/datapack.ts +1 -1
- package/src/edinburgh.ts +8 -9
- package/src/indexes.ts +157 -276
- package/src/migrate.ts +9 -30
- package/src/models.ts +188 -174
- package/src/types.ts +31 -26
- package/skill/Model_findAll.md +0 -12
- package/skill/PrimaryIndex.md +0 -8
- package/skill/PrimaryIndex_get.md +0 -17
- package/skill/PrimaryIndex_getLazy.md +0 -13
- package/skill/UniqueIndex_get.md +0 -17
- package/skill/index.md +0 -32
- package/skill/primary.md +0 -26
- package/skill/registerModel.md +0 -26
- package/skill/unique.md +0 -32
package/src/migrate.ts
CHANGED
|
@@ -2,7 +2,6 @@ import * as lowlevel from "olmdb/lowlevel";
|
|
|
2
2
|
import DataPack from "./datapack.js";
|
|
3
3
|
import { modelRegistry, currentTxn, Transaction } from "./models.js";
|
|
4
4
|
import { dbDel, toBuffer, bytesEqual } from "./utils.js";
|
|
5
|
-
import { PrimaryIndex } from "./indexes.js";
|
|
6
5
|
import { deserializeType, TypeWrapper } from "./types.js";
|
|
7
6
|
import { transact } from "./edinburgh.js";
|
|
8
7
|
|
|
@@ -118,13 +117,12 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
118
117
|
|
|
119
118
|
// Build maps of known index IDs
|
|
120
119
|
const knownIndexIds = new Set<number>();
|
|
121
|
-
const
|
|
120
|
+
const modelByPkIndexId = new Map<number, typeof modelRegistry[string]>();
|
|
122
121
|
|
|
123
122
|
for (const model of Object.values(modelRegistry)) {
|
|
124
123
|
if (options.tables && !options.tables.includes(model.tableName)) continue;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
primaryByIndexId.set(primary._indexId!, { model, primary });
|
|
124
|
+
knownIndexIds.add(model._indexId!);
|
|
125
|
+
modelByPkIndexId.set(model._indexId!, model);
|
|
128
126
|
for (const sec of model._secondaries || []) {
|
|
129
127
|
knownIndexIds.add(sec._indexId!);
|
|
130
128
|
}
|
|
@@ -155,7 +153,7 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
155
153
|
|
|
156
154
|
// Phase 1: Populate secondary indexes and/or rewrite row data
|
|
157
155
|
if (populateSecondaries || rewriteData) {
|
|
158
|
-
for (const [indexId,
|
|
156
|
+
for (const [indexId, model] of modelByPkIndexId) {
|
|
159
157
|
let secondaryCount = 0;
|
|
160
158
|
let rewrittenCount = 0;
|
|
161
159
|
const migrateFn = (model as any).migrate as ((record: Record<string, any>) => void) | undefined;
|
|
@@ -164,15 +162,15 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
164
162
|
await forEachRow(indexId, (txn, keyBuf, valueBuf) => {
|
|
165
163
|
const valuePack = new DataPack(valueBuf);
|
|
166
164
|
const version = valuePack.readNumber();
|
|
167
|
-
if (version ===
|
|
165
|
+
if (version === model._currentVersion) return; // Already current
|
|
168
166
|
|
|
169
|
-
const versionInfo =
|
|
167
|
+
const versionInfo = model._loadVersionInfo(txn.id, version);
|
|
170
168
|
|
|
171
169
|
// Deserialize pre-migrate values from key + old-format value
|
|
172
170
|
const record: Record<string, any> = {};
|
|
173
171
|
const keyPack = new DataPack(keyBuf);
|
|
174
172
|
keyPack.readNumber(); // skip indexId
|
|
175
|
-
for (const [name, type] of
|
|
173
|
+
for (const [name, type] of model._pkFieldTypes.entries()) {
|
|
176
174
|
record[name] = type.deserialize(keyPack);
|
|
177
175
|
}
|
|
178
176
|
for (const [name, type] of versionInfo.nonKeyFields.entries()) {
|
|
@@ -191,33 +189,14 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
191
189
|
sec._write(txn, keyBuf, record as any);
|
|
192
190
|
secondaryCount++;
|
|
193
191
|
} else if (preMigrate) {
|
|
194
|
-
if (sec.
|
|
195
|
-
// Computed indexes: compare serialized keys to avoid unnecessary re-indexing
|
|
196
|
-
const oldKeyBytes = sec._serializeKeyFields(preMigrate).toUint8Array();
|
|
197
|
-
const newKeyBytes = sec._serializeKeyFields(record).toUint8Array();
|
|
198
|
-
if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
|
|
199
|
-
sec._delete(txn, keyBuf, preMigrate as any);
|
|
200
|
-
sec._write(txn, keyBuf, record as any);
|
|
201
|
-
secondaryCount++;
|
|
202
|
-
}
|
|
203
|
-
} else {
|
|
204
|
-
// Existing secondary, update if migrate changed any of its fields
|
|
205
|
-
for (const [field, type] of sec._fieldTypes.entries()) {
|
|
206
|
-
if (!type.equals(preMigrate[field], record[field])) {
|
|
207
|
-
sec._delete(txn, keyBuf, preMigrate as any);
|
|
208
|
-
sec._write(txn, keyBuf, record as any);
|
|
209
|
-
secondaryCount++;
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
192
|
+
if (sec._update(txn, keyBuf, record, preMigrate)) secondaryCount++;
|
|
214
193
|
}
|
|
215
194
|
}
|
|
216
195
|
}
|
|
217
196
|
|
|
218
197
|
// Rewrite primary row data to current version
|
|
219
198
|
if (rewriteData) {
|
|
220
|
-
|
|
199
|
+
model._writePrimary(txn, keyBuf, record);
|
|
221
200
|
rewrittenCount++;
|
|
222
201
|
}
|
|
223
202
|
});
|
package/src/models.ts
CHANGED
|
@@ -28,8 +28,8 @@ export interface Transaction {
|
|
|
28
28
|
instances: Set<Model<unknown>>;
|
|
29
29
|
instancesByPk: Map<number, Model<unknown>>;
|
|
30
30
|
}
|
|
31
|
-
import { BaseIndex
|
|
32
|
-
import { addErrorPath, logLevel, assert, dbGet, hashBytes
|
|
31
|
+
import { BaseIndex, NonPrimaryIndex, PrimaryIndex, IndexRangeIterator, UniqueIndex, SecondaryIndex, FindOptions } from "./indexes.js";
|
|
32
|
+
import { addErrorPath, logLevel, assert, dbGet, hashBytes } from "./utils.js";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Configuration interface for model fields.
|
|
@@ -58,10 +58,10 @@ export interface FieldConfig<T> {
|
|
|
58
58
|
*
|
|
59
59
|
* @example
|
|
60
60
|
* ```typescript
|
|
61
|
-
*
|
|
61
|
+
* const User = E.defineModel(class {
|
|
62
62
|
* name = E.field(E.string, {description: "User's full name"});
|
|
63
63
|
* age = E.field(E.opt(E.number), {description: "User's age", default: 25});
|
|
64
|
-
* }
|
|
64
|
+
* });
|
|
65
65
|
* ```
|
|
66
66
|
*/
|
|
67
67
|
export function field<T>(type: TypeWrapper<T>, options: Partial<FieldConfig<T>> = {}): T {
|
|
@@ -82,77 +82,161 @@ function isObjectEmpty(obj: object) {
|
|
|
82
82
|
|
|
83
83
|
export type Change = Record<any, any> | "created" | "deleted";
|
|
84
84
|
|
|
85
|
+
let autoTableNameId = 0;
|
|
86
|
+
|
|
87
|
+
type FieldsOf<T> = T extends new () => infer I ? I : never;
|
|
88
|
+
type ModelInstance<FIELDS> = FIELDS & Model<FIELDS>;
|
|
89
|
+
|
|
90
|
+
type PKArgs<FIELDS, PK> =
|
|
91
|
+
PK extends readonly (keyof FIELDS & string)[]
|
|
92
|
+
? { [I in keyof PK]: PK[I] extends keyof FIELDS ? FIELDS[PK[I]] : never }
|
|
93
|
+
: PK extends keyof FIELDS & string
|
|
94
|
+
? [FIELDS[PK]]
|
|
95
|
+
: [string];
|
|
96
|
+
|
|
97
|
+
type UniqueFor<SPEC> =
|
|
98
|
+
SPEC extends readonly string[] ? UniqueIndex<any, SPEC>
|
|
99
|
+
: SPEC extends string ? UniqueIndex<any, [SPEC]>
|
|
100
|
+
: SPEC extends (instance: any) => infer R
|
|
101
|
+
? R extends (infer V)[] ? UniqueIndex<any, [], [V]>
|
|
102
|
+
: UniqueIndex<any, [], [R]>
|
|
103
|
+
: never;
|
|
104
|
+
|
|
105
|
+
type SecondaryFor<SPEC> =
|
|
106
|
+
SPEC extends readonly string[] ? SecondaryIndex<any, SPEC>
|
|
107
|
+
: SPEC extends string ? SecondaryIndex<any, [SPEC]>
|
|
108
|
+
: SPEC extends (instance: any) => infer R
|
|
109
|
+
? R extends (infer V)[] ? SecondaryIndex<any, [], [V]>
|
|
110
|
+
: SecondaryIndex<any, [], [R]>
|
|
111
|
+
: never;
|
|
112
|
+
|
|
113
|
+
type RegisteredModel<FIELDS, PKA extends readonly any[], UNIQUE, INDEX> = {
|
|
114
|
+
new (initial?: Partial<FIELDS>): ModelInstance<FIELDS>;
|
|
115
|
+
tableName: string;
|
|
116
|
+
fields: Record<string | symbol | number, FieldConfig<unknown>>;
|
|
117
|
+
get(...args: PKA): ModelInstance<FIELDS> | undefined;
|
|
118
|
+
getLazy(...args: PKA): ModelInstance<FIELDS>;
|
|
119
|
+
find(opts: FindOptions<PKA, 'first'>): ModelInstance<FIELDS> | undefined;
|
|
120
|
+
find(opts: FindOptions<PKA, 'single'>): ModelInstance<FIELDS>;
|
|
121
|
+
find(opts?: FindOptions<PKA>): IndexRangeIterator<any>;
|
|
122
|
+
batchProcess(opts?: FindOptions<PKA> & { limitSeconds?: number; limitRows?: number }, callback?: (row: ModelInstance<FIELDS>) => any): Promise<void>;
|
|
123
|
+
replaceInto(obj: Partial<FIELDS>): ModelInstance<FIELDS>;
|
|
124
|
+
} &
|
|
125
|
+
{ [K in keyof UNIQUE]: UniqueFor<UNIQUE[K]> } &
|
|
126
|
+
{ [K in keyof INDEX]: SecondaryFor<INDEX[K]> };
|
|
127
|
+
|
|
85
128
|
/**
|
|
86
129
|
* Register a model class with the Edinburgh ORM system.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* @
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* }
|
|
100
|
-
* ```
|
|
130
|
+
*
|
|
131
|
+
* Converts a plain class into a fully-featured model with database persistence,
|
|
132
|
+
* typed fields, primary key access, and optional secondary and unique indexes.
|
|
133
|
+
*
|
|
134
|
+
* @param cls - A plain class whose properties use E.field().
|
|
135
|
+
* @param opts - Registration options.
|
|
136
|
+
* @param opts.pk - Primary key field name or array of field names.
|
|
137
|
+
* @param opts.unique - Named unique index specifications (field name, field array, or compute function).
|
|
138
|
+
* @param opts.index - Named secondary index specifications (field name, field array, or compute function).
|
|
139
|
+
* @param opts.tableName - Explicit database table name.
|
|
140
|
+
* @param opts.override - Replace a previous model with the same table name.
|
|
141
|
+
* @returns The enhanced model constructor.
|
|
101
142
|
*/
|
|
102
|
-
export function
|
|
103
|
-
|
|
143
|
+
export function defineModel<
|
|
144
|
+
T extends new () => any,
|
|
145
|
+
const PK extends (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[],
|
|
146
|
+
const UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
|
|
147
|
+
const INDEX extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
|
|
148
|
+
>(
|
|
149
|
+
cls: T,
|
|
150
|
+
opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, tableName?: string, override?: boolean }
|
|
151
|
+
): RegisteredModel<FieldsOf<T>, PKArgs<FieldsOf<T>, PK>, UNIQUE, INDEX>;
|
|
152
|
+
|
|
153
|
+
export function defineModel(cls: any, opts?: any): any {
|
|
154
|
+
Object.setPrototypeOf(cls.prototype, Model.prototype);
|
|
155
|
+
|
|
156
|
+
const MockModel = function(this: any, initial?: Record<string, any>, txn: Transaction = currentTxn()) {
|
|
157
|
+
this._txn = txn;
|
|
158
|
+
txn.instances.add(this);
|
|
159
|
+
if (initial) Object.assign(this, initial);
|
|
160
|
+
} as any;
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
cls.prototype.constructor = MockModel;
|
|
163
|
+
Object.setPrototypeOf(MockModel, Model);
|
|
164
|
+
MockModel.prototype = cls.prototype;
|
|
165
|
+
MockModel._original = cls;
|
|
166
|
+
|
|
167
|
+
for (const name of Object.getOwnPropertyNames(cls)) {
|
|
168
|
+
if (name !== 'length' && name !== 'prototype' && name !== 'name') {
|
|
169
|
+
MockModel[name] = cls[name];
|
|
109
170
|
}
|
|
110
171
|
}
|
|
111
|
-
MockModel.tableName ||= MyModel.name;
|
|
112
172
|
|
|
113
|
-
|
|
173
|
+
MockModel.tableName = opts?.tableName || cls.name || `Model${++autoTableNameId}`;
|
|
174
|
+
|
|
114
175
|
if (MockModel.tableName in modelRegistry) {
|
|
115
|
-
if (!
|
|
176
|
+
if (!opts?.override) {
|
|
116
177
|
throw new DatabaseError(`Model with table name '${MockModel.tableName}' already registered`, 'INIT_ERROR');
|
|
117
178
|
}
|
|
118
179
|
delete modelRegistry[MockModel.tableName];
|
|
119
180
|
}
|
|
120
|
-
modelRegistry[MockModel.tableName] = MockModel;
|
|
121
181
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const AnyOrgModel = OrgModel as any;
|
|
127
|
-
if (AnyOrgModel._isMock) return OrgModel;
|
|
128
|
-
if (AnyOrgModel._mock) return AnyOrgModel._mock;
|
|
182
|
+
const instance = new cls();
|
|
183
|
+
if (!opts?.pk && !instance.id) {
|
|
184
|
+
instance.id = { type: identifier };
|
|
185
|
+
}
|
|
129
186
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
187
|
+
MockModel.fields = {};
|
|
188
|
+
for (const key in instance) {
|
|
189
|
+
const value = instance[key] as FieldConfig<unknown>;
|
|
190
|
+
if (value && value.type instanceof TypeWrapper) {
|
|
191
|
+
MockModel.fields[key] = value;
|
|
192
|
+
|
|
193
|
+
const defObj = value.default === undefined ? value.type : value;
|
|
194
|
+
const def = defObj.default;
|
|
195
|
+
if (typeof def === 'function') {
|
|
196
|
+
Object.defineProperty(MockModel.prototype, key, {
|
|
197
|
+
get() {
|
|
198
|
+
return (this[key] = def.call(defObj, this));
|
|
199
|
+
},
|
|
200
|
+
set(val: any) {
|
|
201
|
+
Object.defineProperty(this, key, {
|
|
202
|
+
value: val,
|
|
203
|
+
configurable: true,
|
|
204
|
+
writable: true,
|
|
205
|
+
enumerable: true,
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
configurable: true,
|
|
209
|
+
});
|
|
210
|
+
} else if (def !== undefined) {
|
|
211
|
+
MockModel.prototype[key] = def;
|
|
212
|
+
}
|
|
137
213
|
}
|
|
138
|
-
}
|
|
214
|
+
}
|
|
139
215
|
|
|
140
|
-
|
|
141
|
-
|
|
216
|
+
if (opts?.pk) {
|
|
217
|
+
new PrimaryIndex(MockModel, Array.isArray(opts.pk) ? opts.pk : [opts.pk]);
|
|
218
|
+
} else {
|
|
219
|
+
new PrimaryIndex(MockModel, ['id']);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const normalizeSpec = (spec: any) => typeof spec === 'string' ? [spec] : spec;
|
|
223
|
+
|
|
224
|
+
if (opts?.unique) {
|
|
225
|
+
for (const [name, spec] of Object.entries<any>(opts.unique)) {
|
|
226
|
+
MockModel[name] = new UniqueIndex(MockModel, normalizeSpec(spec));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (opts?.index) {
|
|
230
|
+
for (const [name, spec] of Object.entries<any>(opts.index)) {
|
|
231
|
+
MockModel[name] = new SecondaryIndex(MockModel, normalizeSpec(spec));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
142
234
|
|
|
143
|
-
|
|
144
|
-
Object.setPrototypeOf(MockModel, Object.getPrototypeOf(OrgModel));
|
|
145
|
-
MockModel.prototype = OrgModel.prototype;
|
|
146
|
-
(MockModel as any)._isMock = true;
|
|
147
|
-
(MockModel as any)._original = OrgModel;
|
|
148
|
-
AnyOrgModel._mock = MockModel;
|
|
235
|
+
modelRegistry[MockModel.tableName] = MockModel;
|
|
149
236
|
scheduleInit();
|
|
150
237
|
return MockModel;
|
|
151
238
|
}
|
|
152
239
|
|
|
153
|
-
// Model base class and related symbols/state
|
|
154
|
-
const INIT_INSTANCE_SYMBOL = Symbol();
|
|
155
|
-
|
|
156
240
|
/**
|
|
157
241
|
* Model interface that ensures proper typing for the constructor property.
|
|
158
242
|
* @template SUB - The concrete model subclass.
|
|
@@ -165,8 +249,8 @@ export interface Model<SUB> {
|
|
|
165
249
|
* Base class for all database models in the Edinburgh ORM.
|
|
166
250
|
*
|
|
167
251
|
* Models represent database entities with typed fields, automatic serialization,
|
|
168
|
-
* change tracking, and relationship management.
|
|
169
|
-
*
|
|
252
|
+
* change tracking, and relationship management. Model classes are created using
|
|
253
|
+
* `E.defineModel()`.
|
|
170
254
|
*
|
|
171
255
|
* ### Schema Evolution
|
|
172
256
|
*
|
|
@@ -202,16 +286,14 @@ export interface Model<SUB> {
|
|
|
202
286
|
*
|
|
203
287
|
* @example
|
|
204
288
|
* ```typescript
|
|
205
|
-
*
|
|
206
|
-
* class User extends E.Model<User> {
|
|
207
|
-
* static pk = E.primary(User, "id");
|
|
208
|
-
*
|
|
289
|
+
* const User = E.defineModel(class {
|
|
209
290
|
* id = E.field(E.identifier);
|
|
210
291
|
* name = E.field(E.string);
|
|
211
292
|
* email = E.field(E.string);
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* }
|
|
293
|
+
* }, {
|
|
294
|
+
* pk: "id",
|
|
295
|
+
* unique: { byEmail: "email" },
|
|
296
|
+
* });
|
|
215
297
|
* ```
|
|
216
298
|
*/
|
|
217
299
|
|
|
@@ -220,17 +302,24 @@ export abstract class Model<SUB> {
|
|
|
220
302
|
static _primary: PrimaryIndex<any, any>;
|
|
221
303
|
|
|
222
304
|
/** @internal All non-primary indexes for this model. */
|
|
223
|
-
static _secondaries?:
|
|
305
|
+
static _secondaries?: NonPrimaryIndex<any, readonly (keyof any & string)[]>[];
|
|
224
306
|
|
|
225
307
|
/** The database table name (defaults to class name). */
|
|
226
308
|
static tableName: string;
|
|
227
309
|
|
|
228
|
-
/** When true,
|
|
310
|
+
/** When true, defineModel replaces an existing model with the same tableName. */
|
|
229
311
|
static override?: boolean;
|
|
230
312
|
|
|
231
313
|
/** Field configuration metadata. */
|
|
232
314
|
static fields: Record<string | symbol | number, FieldConfig<unknown>>;
|
|
233
315
|
|
|
316
|
+
// Alias statics that delegate to _primary, used by migrate.ts
|
|
317
|
+
static get _indexId() { return this._primary?._indexId; }
|
|
318
|
+
static get _currentVersion() { return this._primary._currentVersion; }
|
|
319
|
+
static get _pkFieldTypes() { return this._primary._fieldTypes; }
|
|
320
|
+
static _loadVersionInfo(txnId: number, version: number) { return this._primary._loadVersionInfo(txnId, version); }
|
|
321
|
+
static _writePrimary(txn: Transaction, pk: Uint8Array, data: Record<string, any>) { this._primary._write(txn, pk, data as any); }
|
|
322
|
+
|
|
234
323
|
/**
|
|
235
324
|
* Optional migration function called when deserializing rows written with an older schema version.
|
|
236
325
|
* Receives a plain record with all fields (primary key fields + value fields) and should mutate it
|
|
@@ -247,9 +336,7 @@ export abstract class Model<SUB> {
|
|
|
247
336
|
*
|
|
248
337
|
* @example
|
|
249
338
|
* ```typescript
|
|
250
|
-
*
|
|
251
|
-
* class User extends E.Model<User> {
|
|
252
|
-
* static pk = E.primary(User, "id");
|
|
339
|
+
* const User = E.defineModel(class {
|
|
253
340
|
* id = E.field(E.identifier);
|
|
254
341
|
* name = E.field(E.string);
|
|
255
342
|
* role = E.field(E.string); // new field
|
|
@@ -257,7 +344,7 @@ export abstract class Model<SUB> {
|
|
|
257
344
|
* static migrate(record: Record<string, any>) {
|
|
258
345
|
* record.role ??= "user"; // default for rows that predate the 'role' field
|
|
259
346
|
* }
|
|
260
|
-
* }
|
|
347
|
+
* }, { pk: "id" });
|
|
261
348
|
* ```
|
|
262
349
|
*/
|
|
263
350
|
static migrate?(record: Record<string, any>): void;
|
|
@@ -280,10 +367,7 @@ export abstract class Model<SUB> {
|
|
|
280
367
|
_txn!: Transaction;
|
|
281
368
|
|
|
282
369
|
constructor(initial: Partial<Omit<SUB, "constructor">> = {}) {
|
|
283
|
-
|
|
284
|
-
// be created by the 'fake' constructor. The typing for `initial` *is* important though.
|
|
285
|
-
if (initial as any === INIT_INSTANCE_SYMBOL) return;
|
|
286
|
-
throw new DatabaseError("The model needs a @E.registerModel decorator", 'INIT_ERROR');
|
|
370
|
+
throw new DatabaseError("Use defineModel() to create model classes", 'INIT_ERROR');
|
|
287
371
|
}
|
|
288
372
|
|
|
289
373
|
/**
|
|
@@ -298,9 +382,7 @@ export abstract class Model<SUB> {
|
|
|
298
382
|
*
|
|
299
383
|
* @example
|
|
300
384
|
* ```typescript
|
|
301
|
-
*
|
|
302
|
-
* class Post extends E.Model<Post> {
|
|
303
|
-
* static pk = E.primary(Post, "id");
|
|
385
|
+
* const Post = E.defineModel(class {
|
|
304
386
|
* id = E.field(E.identifier);
|
|
305
387
|
* title = E.field(E.string);
|
|
306
388
|
* slug = E.field(E.string);
|
|
@@ -308,79 +390,21 @@ export abstract class Model<SUB> {
|
|
|
308
390
|
* preCommit() {
|
|
309
391
|
* this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
|
|
310
392
|
* }
|
|
311
|
-
* }
|
|
393
|
+
* }, { pk: "id" });
|
|
312
394
|
* ```
|
|
313
395
|
*/
|
|
314
396
|
preCommit?(): void;
|
|
315
397
|
|
|
316
|
-
static
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
MockModel._primary._versions.clear();
|
|
322
|
-
for (const sec of MockModel._secondaries || []) sec._indexId = undefined;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!MockModel.fields) {
|
|
326
|
-
// First-time init: gather field configs from a temporary instance of the original class.
|
|
327
|
-
const OrgModel = (MockModel as any)._original || this;
|
|
328
|
-
const instance = new (OrgModel as any)(INIT_INSTANCE_SYMBOL);
|
|
329
|
-
|
|
330
|
-
// If no primary key exists, create one using 'id' field
|
|
331
|
-
if (!MockModel._primary) {
|
|
332
|
-
if (!instance.id) {
|
|
333
|
-
instance.id = { type: identifier };
|
|
334
|
-
}
|
|
335
|
-
// @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
|
|
336
|
-
new PrimaryIndex(MockModel, ['id']);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
MockModel.fields = {};
|
|
340
|
-
for (const key in instance) {
|
|
341
|
-
const value = instance[key] as FieldConfig<unknown>;
|
|
342
|
-
// Check if this property contains field metadata
|
|
343
|
-
if (value && value.type instanceof TypeWrapper) {
|
|
344
|
-
// Set the configuration on the constructor's `fields` property
|
|
345
|
-
MockModel.fields[key] = value;
|
|
346
|
-
|
|
347
|
-
// Set default value on the prototype
|
|
348
|
-
const defObj = value.default===undefined ? value.type : value;
|
|
349
|
-
const def = defObj.default;
|
|
350
|
-
if (typeof def === 'function') {
|
|
351
|
-
// The default is a function. We'll define a getter on the property in the model prototype,
|
|
352
|
-
// and once it is read, we'll run the function and set the value as a plain old property
|
|
353
|
-
// on the instance object.
|
|
354
|
-
Object.defineProperty(MockModel.prototype, key, {
|
|
355
|
-
get() {
|
|
356
|
-
// This will call set(), which will define the property on the instance.
|
|
357
|
-
return (this[key] = def.call(defObj, this));
|
|
358
|
-
},
|
|
359
|
-
set(val: any) {
|
|
360
|
-
Object.defineProperty(this, key, {
|
|
361
|
-
value: val,
|
|
362
|
-
configurable: true,
|
|
363
|
-
writable: true,
|
|
364
|
-
enumerable: true,
|
|
365
|
-
})
|
|
366
|
-
},
|
|
367
|
-
configurable: true,
|
|
368
|
-
});
|
|
369
|
-
} else if (def !== undefined) {
|
|
370
|
-
(MockModel.prototype as any)[key] = def;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (logLevel >= 1) {
|
|
376
|
-
console.log(`[edinburgh] Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
398
|
+
static _resetIndexes(): void {
|
|
399
|
+
this._primary._indexId = undefined;
|
|
400
|
+
this._primary._versions.clear();
|
|
401
|
+
for (const sec of this._secondaries || []) sec._indexId = undefined;
|
|
402
|
+
}
|
|
379
403
|
|
|
380
|
-
|
|
381
|
-
await
|
|
382
|
-
for (const sec of
|
|
383
|
-
await
|
|
404
|
+
static async _loadCreateIndexes(): Promise<void> {
|
|
405
|
+
await this._primary._delayedInit();
|
|
406
|
+
for (const sec of this._secondaries || []) await sec._delayedInit();
|
|
407
|
+
await this._primary._initVersioning();
|
|
384
408
|
}
|
|
385
409
|
|
|
386
410
|
_setLoadedField(fieldName: string, value: any) {
|
|
@@ -403,7 +427,7 @@ export abstract class Model<SUB> {
|
|
|
403
427
|
getPrimaryKey(): Uint8Array {
|
|
404
428
|
let key = this._primaryKey;
|
|
405
429
|
if (key === undefined) {
|
|
406
|
-
key = this.constructor._primary!.
|
|
430
|
+
key = this.constructor._primary!._serializeKey(this).toUint8Array();
|
|
407
431
|
this._setPrimaryKey(key);
|
|
408
432
|
}
|
|
409
433
|
return key;
|
|
@@ -468,8 +492,8 @@ export abstract class Model<SUB> {
|
|
|
468
492
|
// the whole object just to see if something changed.
|
|
469
493
|
|
|
470
494
|
// Add old values of changed fields to 'changed'.
|
|
495
|
+
const changed: Record<string, any> = {};
|
|
471
496
|
const fields = this.constructor.fields;
|
|
472
|
-
let changed : Record<any, any> = {};
|
|
473
497
|
for(const fieldName in oldValues) {
|
|
474
498
|
const oldValue = oldValues[fieldName];
|
|
475
499
|
const newValue = this[fieldName as keyof Model<SUB>];
|
|
@@ -498,23 +522,7 @@ export abstract class Model<SUB> {
|
|
|
498
522
|
|
|
499
523
|
// Update any secondaries with changed fields
|
|
500
524
|
for (const index of this.constructor._secondaries || []) {
|
|
501
|
-
|
|
502
|
-
// Computed indexes may depend on any field — compare serialized keys
|
|
503
|
-
const oldKeyBytes = index._serializeKeyFields(oldValues).toUint8Array();
|
|
504
|
-
const newKeyBytes = index._serializeKeyFields(this as any).toUint8Array();
|
|
505
|
-
if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
|
|
506
|
-
index._delete(txn, pk, oldValues);
|
|
507
|
-
index._write(txn, pk, this);
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
for (const field of index._fieldTypes.keys()) {
|
|
511
|
-
if (changed.hasOwnProperty(field)) {
|
|
512
|
-
index._delete(txn, pk, oldValues);
|
|
513
|
-
index._write(txn, pk, this);
|
|
514
|
-
break;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
525
|
+
index._update(txn, pk, this, oldValues);
|
|
518
526
|
}
|
|
519
527
|
return changed;
|
|
520
528
|
}
|
|
@@ -526,7 +534,7 @@ export abstract class Model<SUB> {
|
|
|
526
534
|
*
|
|
527
535
|
* @example
|
|
528
536
|
* ```typescript
|
|
529
|
-
* const user = User.
|
|
537
|
+
* const user = User.get("user123");
|
|
530
538
|
* user.name = "New Name";
|
|
531
539
|
* user.preventPersist(); // Changes won't be saved
|
|
532
540
|
* ```
|
|
@@ -538,16 +546,22 @@ export abstract class Model<SUB> {
|
|
|
538
546
|
return this;
|
|
539
547
|
}
|
|
540
548
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
549
|
+
static get(...args: any[]): any {
|
|
550
|
+
return this._primary!.get(...args);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
static getLazy(...args: any[]): any {
|
|
554
|
+
return this._primary!.getLazy(...args);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
static find(opts?: any): any {
|
|
548
558
|
return this._primary!.find(opts);
|
|
549
559
|
}
|
|
550
560
|
|
|
561
|
+
static batchProcess(opts: any, callback?: any): any {
|
|
562
|
+
return this._primary!.batchProcess(opts, callback);
|
|
563
|
+
}
|
|
564
|
+
|
|
551
565
|
/**
|
|
552
566
|
* Load an existing instance by primary key and update it, or create a new one.
|
|
553
567
|
*
|
|
@@ -558,7 +572,7 @@ export abstract class Model<SUB> {
|
|
|
558
572
|
* @param obj - Partial model data that **must** include every primary key field.
|
|
559
573
|
* @returns The loaded-and-updated or newly created instance.
|
|
560
574
|
*/
|
|
561
|
-
static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<
|
|
575
|
+
static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<Record<string, any>>): InstanceType<T> {
|
|
562
576
|
const pk = this._primary!;
|
|
563
577
|
const keyArgs = [];
|
|
564
578
|
for (const fieldName of pk._fieldTypes.keys()) {
|
|
@@ -587,7 +601,7 @@ export abstract class Model<SUB> {
|
|
|
587
601
|
*
|
|
588
602
|
* @example
|
|
589
603
|
* ```typescript
|
|
590
|
-
* const user = User.
|
|
604
|
+
* const user = User.get("user123");
|
|
591
605
|
* user.delete(); // Removes from database
|
|
592
606
|
* ```
|
|
593
607
|
*/
|
|
@@ -651,7 +665,7 @@ export abstract class Model<SUB> {
|
|
|
651
665
|
|
|
652
666
|
toString(): string {
|
|
653
667
|
const primary = this.constructor._primary;
|
|
654
|
-
const pk = primary._keyToArray(this._primaryKey || primary.
|
|
668
|
+
const pk = primary._keyToArray(this._primaryKey || primary._serializeKey(this).toUint8Array(false));
|
|
655
669
|
return `{Model:${this.constructor.tableName} ${this.getState()} ${pk}}`;
|
|
656
670
|
}
|
|
657
671
|
|