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.
package/src/types.ts CHANGED
@@ -1,593 +1,709 @@
1
- import { Bytes } from "./bytes.js";
2
- import * as olmdb from "olmdb";
3
- import { DatabaseError } from "olmdb";
4
- import { Model, modelRegistry, getMockModel } from "./models.js";
5
- import { assert, addErrorPath } from "./utils.js";
1
+ import DataPack from "./datapack.js";
2
+ import { DatabaseError } from "olmdb/lowlevel";
3
+ import { Model, modelRegistry, FieldConfig, currentTxn } from "./models.js";
4
+ import { assert, addErrorPath, dbGet } from "./utils.js";
5
+ import { PrimaryIndex, BaseIndex, IndexRangeIterator } from "./indexes.js";
6
+
6
7
 
7
8
  /**
8
- * @internal Abstract base class for all type wrappers in the Edinburgh ORM system.
9
- *
10
- * This is an implementation detail and should not be referenced directly in user code.
11
- * Type wrappers define how values are serialized to/from the database and how they are validated.
12
- * Each type wrapper must implement serialization, deserialization, and validation logic.
13
- *
14
- * @template T - The TypeScript type this wrapper represents.
15
- */
9
+ * @internal Abstract base class for all type wrappers in the Edinburgh ORM system.
10
+ *
11
+ * This is an implementation detail and should not be referenced directly in user code.
12
+ * Type wrappers define how values are serialized to/from the database and how they are validated.
13
+ * Each type wrapper must implement serialization, deserialization, and validation logic.
14
+ *
15
+ * @template T - The TypeScript type this wrapper represents.
16
+ */
16
17
  export abstract class TypeWrapper<const T> {
17
18
  /** @internal Used for TypeScript type inference - this field is required for the type system */
18
19
  _T!: T;
19
20
 
20
21
  /** A string identifier for this type, used during serialization */
21
22
  abstract kind: string;
22
-
23
- constructor() {}
24
23
 
25
- /**
26
- * Serialize a value from an object property to bytes.
27
- * @param obj - The object containing the value.
28
- * @param prop - The property name or index.
29
- * @param bytes - The Bytes instance to write to.
30
- * @param model - Optional model instance for context.
31
- */
32
- abstract serialize(obj: any, prop: string|number, bytes: Bytes, model?: any): void;
24
+ constructor() {}
33
25
 
34
26
  /**
35
- * Deserialize a value from bytes into an object property.
36
- * @param obj - The object to set the value on.
37
- * @param prop - The property name or index.
38
- * @param bytes - The Bytes instance to read from.
39
- * @param model - Optional model instance for context.
40
- */
41
- abstract deserialize(obj: any, prop: string|number, bytes: Bytes, model?: any): void;
42
-
27
+ * Serialize a value from an object property to a Pack.
28
+ * @param value - The value to serialize.
29
+ * @param pack - The Pack instance to write to.
30
+ */
31
+ abstract serialize(value: T, pack: DataPack): void;
32
+
43
33
  /**
44
- * Validate a value and return any validation errors.
45
- * @param obj - The object containing the value.
46
- * @param prop - The property name or index.
47
- * @returns Array of validation errors (empty if valid).
48
- */
49
- abstract getErrors(obj: any, prop: string | number): DatabaseError[];
50
-
34
+ * Deserialize a value from a Pack into an object property.
35
+ * @param pack - The Pack instance to read from.
36
+ */
37
+ abstract deserialize(pack: DataPack): T;
38
+
51
39
  /**
52
- * Validate a value and serialize it, throwing on validation errors.
53
- * @param obj - The object containing the value.
54
- * @param prop - The property name or index.
55
- * @param bytes - The Bytes instance to write to.
56
- * @param model - Optional model instance for context.
57
- * @throws {DatabaseError} If validation fails.
58
- */
59
- validateAndSerialize(obj: any, prop: string|number, bytes: Bytes, model?: any): void {
60
- const errors = this.getErrors(obj, prop);
61
- if (errors.length) throw errors[0];
62
- this.serialize(obj, prop, bytes, model);
63
- }
40
+ * Validate a value.
41
+ * @param value - The value to validate.
42
+ * @returns - A DatabaseError if validation fails.
43
+ */
44
+ abstract getError(value: T): DatabaseError | void;
64
45
 
65
46
  /**
66
- * Serialize type metadata to bytes (for schema serialization).
67
- * @param bytes - The Bytes instance to write to.
68
- */
69
- serializeType(bytes: Bytes) {}
47
+ * Serialize type metadata to a Pack (for schema serialization).
48
+ * @param pack - The Pack instance to write to.
49
+ */
50
+ serializeType(pack: DataPack) {}
70
51
 
71
52
  /**
72
- * Check if indexing should be skipped for this field value.
73
- * @param obj - The object containing the value.
74
- * @param prop - The property name or index.
75
- * @returns true if indexing should be skipped.
76
- */
77
- checkSkipIndex(obj: any, prop: string | number): boolean {
53
+ * Check if indexing should be skipped for this field value.
54
+ * @param obj - The object containing the value.
55
+ * @param prop - The property name or index.
56
+ * @returns true if indexing should be skipped.
57
+ */
58
+ containsNull(value: T): boolean {
78
59
  return false;
79
60
  }
80
-
61
+
81
62
  toString(): string {
82
63
  return `${this.kind}`;
83
64
  }
65
+
66
+ clone(value: T): T {
67
+ return value;
68
+ }
69
+
70
+ equals(value1: T, value2: T): boolean {
71
+ return value1 === value2;
72
+ }
73
+
74
+ getLinkedModel(): (typeof Model<unknown>) | undefined {
75
+ return;
76
+ }
84
77
  }
85
78
 
86
- /**
87
- * Optional interface for type wrappers that can provide default values.
88
- * @template T - The TypeScript type this wrapper represents.
89
- */
79
+
90
80
  export interface TypeWrapper<T> {
91
81
  /**
92
- * Generate a default value for this type.
93
- * @param model - The model instance.
94
- * @returns The default value.
95
- */
82
+ * Generate a default value for this type.
83
+ * @param model - The model instance.
84
+ * @returns The default value.
85
+ */
96
86
  default?(model: any): T;
97
87
  }
98
88
 
99
- /**
100
- * @internal Type wrapper for string values.
101
- */
89
+
102
90
  class StringType extends TypeWrapper<string> {
103
91
  kind = 'string';
104
92
 
105
- serialize(obj: any, prop: string, bytes: Bytes, model?: any) {
106
- bytes.writeString(obj[prop]);
93
+ serialize(value: string, pack: DataPack) {
94
+ pack.write(value);
107
95
  }
108
96
 
109
- deserialize(obj: any, prop: string | number, bytes: Bytes, model?: any): void {
110
- obj[prop] = bytes.readString();
97
+ deserialize(pack: DataPack): string {
98
+ return pack.readString();
111
99
  }
112
-
113
- getErrors(obj: any, prop: string | number): DatabaseError[] {
114
- if (typeof obj[prop] !== 'string') {
115
- return [new DatabaseError(`Expected string, got ${typeof obj[prop]}`, 'INVALID_TYPE')];
100
+
101
+ getError(value: string) {
102
+ if (typeof value !== 'string') {
103
+ return new DatabaseError(`Expected string, got ${typeof value}`, 'INVALID_TYPE');
116
104
  }
117
- return [];
118
105
  }
119
106
  }
120
107
 
121
- /**
122
- * @internal Type wrapper for number values.
123
- */
108
+
109
+ class OrderedStringType extends StringType {
110
+ serialize(value: string, pack: DataPack) {
111
+ pack.writeOrderedString(value);
112
+ }
113
+ }
114
+
115
+
124
116
  class NumberType extends TypeWrapper<number> {
125
117
  kind = 'number';
126
-
127
- serialize(obj: any, prop: string, bytes: Bytes, model?: any) {
128
- bytes.writeNumber(obj[prop]);
118
+
119
+ serialize(value: number, pack: DataPack) {
120
+ pack.write(value);
129
121
  }
130
-
131
- deserialize(obj: any, prop: string | number, bytes: Bytes, model?: any): void {
132
- obj[prop] = bytes.readNumber();
122
+
123
+ deserialize(pack: DataPack): number {
124
+ return pack.readNumber();
133
125
  }
134
-
135
- getErrors(obj: any, prop: string | number): DatabaseError[] {
136
- const value = obj[prop];
126
+
127
+ getError(value: number) {
137
128
  if (typeof value !== 'number' || isNaN(value)) {
138
- return [new DatabaseError(`Expected number, got ${typeof value}`, 'INVALID_TYPE')];
129
+ return new DatabaseError(`Expected number, got ${typeof value}`, 'INVALID_TYPE');
139
130
  }
140
- return [];
141
131
  }
142
132
  }
143
133
 
144
- /**
145
- * @internal Type wrapper for boolean values.
146
- */
134
+ class DateTimeType extends TypeWrapper<Date> {
135
+ kind = 'dateTime';
136
+
137
+ serialize(value: Date, pack: DataPack) {
138
+ pack.write(value);
139
+ }
140
+
141
+ deserialize(pack: DataPack): Date {
142
+ return pack.readDate();;
143
+ }
144
+
145
+ getError(value: Date) {
146
+ if (!(value instanceof Date)) {
147
+ return new DatabaseError(`Expected Date, got ${typeof value}`, 'INVALID_TYPE');
148
+ }
149
+ }
150
+
151
+ default(): Date {
152
+ return new Date();
153
+ }
154
+
155
+ }
156
+
147
157
  class BooleanType extends TypeWrapper<boolean> {
148
158
  kind = 'boolean';
149
-
150
- serialize(obj: any, prop: string, bytes: Bytes, model?: any) {
151
- bytes.writeBits(obj[prop] ? 1 : 0, 1);
159
+
160
+ serialize(value: boolean, pack: DataPack) {
161
+ pack.write(value);
152
162
  }
153
-
154
- deserialize(obj: any, prop: string | number, bytes: Bytes, model?: any): void {
155
- obj[prop] = bytes.readBits(1) === 1;
163
+
164
+ deserialize(pack: DataPack): boolean {
165
+ return pack.readBoolean();
156
166
  }
157
-
158
- getErrors(obj: any, prop: string | number): DatabaseError[] {
159
- if (typeof obj[prop] !== 'boolean') {
160
- return [new DatabaseError(`Expected boolean, got ${typeof obj[prop]}`, 'INVALID_TYPE')];
167
+
168
+ getError(value: boolean) {
169
+ if (typeof value !== 'boolean') {
170
+ return new DatabaseError(`Expected boolean, got ${typeof value}`, 'INVALID_TYPE');
161
171
  }
162
- return [];
163
172
  }
164
173
  }
165
174
 
166
175
  /**
167
- * @internal Type wrapper for array values with optional length constraints.
168
- * @template T - The type of array elements.
169
- */
176
+ * @internal Type wrapper for array values with optional length constraints.
177
+ * @template T - The type of array elements.
178
+ */
170
179
  class ArrayType<T> extends TypeWrapper<T[]> {
171
180
  kind = 'array';
172
181
 
173
182
  /**
174
- * Create a new ArrayType.
175
- * @param inner - Type wrapper for array elements.
176
- * @param opts - Array constraints (min/max length).
177
- */
183
+ * Create a new ArrayType.
184
+ * @param inner - Type wrapper for array elements.
185
+ * @param opts - Array constraints (min/max length).
186
+ */
178
187
  constructor(public inner: TypeWrapper<T>, public opts: {min?: number, max?: number} = {}) {
179
188
  super();
180
189
  }
181
-
182
- serialize(obj: any, prop: string, bytes: Bytes, model?: any) {
183
- const value = obj[prop] as T[];
184
- bytes.writeNumber(value.length);
190
+
191
+ serialize(value: T[], pack: DataPack) {
192
+ pack.write(value.length);
185
193
  for(let i=0; i<value.length; i++) {
186
- this.inner.serialize(value, i, bytes, model);
194
+ this.inner.serialize(value[i], pack);
187
195
  }
188
196
  }
189
197
 
190
- deserialize(obj: any, prop: string | number, bytes: Bytes, model?: any): void {
191
- const length = bytes.readNumber();
198
+ deserialize(pack: DataPack): T[] {
199
+ const length = pack.readNumber();
192
200
  const result: T[] = [];
193
201
  for (let i = 0; i < length; i++) {
194
- this.inner.deserialize(result, i, bytes, model);
202
+ result.push(this.inner.deserialize(pack));
195
203
  }
196
- obj[prop] = result;
204
+ return result;
197
205
  }
198
-
199
- getErrors(obj: any, prop: string | number): DatabaseError[] {
200
- const value = obj[prop];
206
+
207
+ getError(value: T[]) {
201
208
  if (!Array.isArray(value)) {
202
- return [new DatabaseError(`Expected array, got ${typeof value}`, 'INVALID_TYPE')];
209
+ return new DatabaseError(`Expected array, got ${typeof value}`, 'INVALID_TYPE');
203
210
  }
204
- const errors: DatabaseError[] = [];
211
+
205
212
  if (this.opts.min !== undefined && value.length < this.opts.min) {
206
- errors.push(new DatabaseError(`Array length ${value.length} is less than minimum ${this.opts.min}`, 'OUT_OF_BOUNDS'));
213
+ return new DatabaseError(`Array length ${value.length} is less than minimum ${this.opts.min}`, 'OUT_OF_BOUNDS');
207
214
  }
208
215
  if (this.opts.max !== undefined && value.length > this.opts.max) {
209
- errors.push(new DatabaseError(`Array length ${value.length} is greater than maximum ${this.opts.max}`, 'OUT_OF_BOUNDS'));
216
+ return new DatabaseError(`Array length ${value.length} is greater than maximum ${this.opts.max}`, 'OUT_OF_BOUNDS');
210
217
  }
211
218
  for (let i = 0; i < value.length; i++) {
212
- for(let itemError of this.inner.getErrors(value, i)) {
213
- errors.push(addErrorPath(itemError, i));
214
- }
219
+ let error = this.inner.getError(value[i]);
220
+ if (error) return addErrorPath(error, i);
215
221
  }
216
- return errors;
217
222
  }
218
223
 
219
- serializeType(bytes: Bytes): void {
220
- serializeType(this.inner, bytes);
224
+ serializeType(pack: DataPack): void {
225
+ serializeType(this.inner, pack);
221
226
  }
222
227
 
223
- static deserializeType(bytes: Bytes, featureFlags: number): ArrayType<any> {
224
- const inner = deserializeType(bytes, featureFlags);
228
+ static deserializeType(pack: DataPack, featureFlags: number): ArrayType<any> {
229
+ const inner = deserializeType(pack, featureFlags);
225
230
  return new ArrayType(inner);
226
231
  }
232
+
233
+ default(): T[] {
234
+ return [];
235
+ }
236
+
237
+ clone(value: T[]): T[] {
238
+ return value.map(this.inner.clone.bind(this.inner));
239
+ }
240
+
241
+ equals(a: T[], b: T[]): boolean {
242
+ if (a.length !== b.length) return false;
243
+ for (let i = 0; i < a.length; i++) {
244
+ if (!this.inner.equals(a[i], b[i])) return false;
245
+ }
246
+ return true;
247
+ }
248
+
249
+ toString() { return `array<${this.inner}>`; }
250
+
251
+ getLinkedModel() {
252
+ return this.inner.getLinkedModel();
253
+ }
227
254
  }
228
255
 
229
256
  /**
230
- * @internal Type wrapper for union/discriminated union types.
231
- * @template T - The union type this wrapper represents.
232
- */
257
+ * @internal Type wrapper for array values with optional length constraints.
258
+ * @template T - The type of array elements.
259
+ */
260
+ export class SetType<T> extends TypeWrapper<Set<T>> {
261
+ kind = 'set';
262
+
263
+ /**
264
+ * Create a new SetType.
265
+ * @param inner - Type wrapper for set elements.
266
+ */
267
+ constructor(public inner: TypeWrapper<T>, public opts: {min?: number, max?: number} = {}) {
268
+ super();
269
+ }
270
+
271
+ serialize(value: Set<T>, pack: DataPack) {
272
+ pack.write(value.size);
273
+ for (const item of value) {
274
+ this.inner.serialize(item, pack);
275
+ }
276
+ }
277
+
278
+ deserialize(pack: DataPack): Set<T> {
279
+ const length = pack.readNumber();
280
+ const result = new Set<T>();
281
+ for (let i = 0; i < length; i++) {
282
+ result.add(this.inner.deserialize(pack));
283
+ }
284
+ return result;
285
+ }
286
+
287
+ getError(value: Set<T>) {
288
+ if (!(value instanceof Set)) {
289
+ return new DatabaseError(`Expected Set, got ${typeof value}`, 'INVALID_TYPE');
290
+ }
291
+
292
+ if (this.opts.min !== undefined && value.size < this.opts.min) {
293
+ return new DatabaseError(`Set size ${value.size} is less than minimum ${this.opts.min}`, 'OUT_OF_BOUNDS');
294
+ }
295
+ if (this.opts.max !== undefined && value.size > this.opts.max) {
296
+ return new DatabaseError(`Set size ${value.size} is greater than maximum ${this.opts.max}`, 'OUT_OF_BOUNDS');
297
+ }
298
+
299
+ try {
300
+ for (const item of value) {
301
+ this.inner.getError(item);
302
+ }
303
+ } catch (err) {
304
+ throw addErrorPath(err, 'item');
305
+ }
306
+ }
307
+
308
+ serializeType(pack: DataPack): void {
309
+ serializeType(this.inner, pack);
310
+ }
311
+
312
+ static deserializeType(pack: DataPack, featureFlags: number): SetType<any> {
313
+ const inner = deserializeType(pack, featureFlags);
314
+ return new SetType(inner);
315
+ }
316
+
317
+ default(): Set<T> {
318
+ return new Set<T>();
319
+ }
320
+
321
+ clone(value: Set<T>): Set<T> {
322
+ const cloned = new Set<T>();
323
+ for (const item of value) {
324
+ cloned.add(this.inner.clone(item));
325
+ }
326
+ return cloned;
327
+ }
328
+
329
+ equals(a: Set<T>, b: Set<T>): boolean {
330
+ if (a.size !== b.size) return false;
331
+ for(const v of a) {
332
+ if (!b.has(v)) return false;
333
+ }
334
+ return true;
335
+ }
336
+
337
+ toString() { return `set<${this.inner}>`; }
338
+
339
+ getLinkedModel() {
340
+ return this.inner.getLinkedModel();
341
+ }
342
+ }
343
+
344
+
345
+ /**
346
+ * @internal Type wrapper for union/discriminated union types.
347
+ * @template T - The union type this wrapper represents.
348
+ */
233
349
  class OrType<const T> extends TypeWrapper<T> {
234
350
  kind = 'or';
235
351
 
236
352
  /**
237
- * Create a new OrType.
238
- * @param choices - Array of type wrappers representing the union choices.
239
- */
353
+ * Create a new OrType.
354
+ * @param choices - Array of type wrappers representing the union choices.
355
+ */
240
356
  constructor(public choices: TypeWrapper<T>[]) {
241
357
  super();
242
358
  }
243
359
 
244
- _getChoiceIndex(obj: any, prop: string | number): number {
360
+ _getChoiceIndex(value: any): number {
245
361
  for (const [i, choice] of this.choices.entries()) {
246
- if (choice.getErrors(obj, prop).length === 0) {
247
- return i;
248
- }
362
+ if (!choice.getError(value)) return i;
249
363
  }
250
- throw new DatabaseError(`Value does not match any union type: ${obj[prop]}`, 'INVALID_TYPE');
364
+ throw new DatabaseError(`Value does not match any union type: ${value}`, 'INVALID_TYPE');
251
365
  }
252
366
 
253
- serialize(obj: any, prop: string, bytes: Bytes, model?: any) {
254
- const choiceIndex = this._getChoiceIndex(obj, prop);
255
- bytes.writeUIntN(choiceIndex, this.choices.length-1);
256
- this.choices[choiceIndex].serialize(obj, prop, bytes, model);
367
+ serialize(value: T, pack: DataPack) {
368
+ const choiceIndex = this._getChoiceIndex(value);
369
+ pack.write(choiceIndex);
370
+ this.choices[choiceIndex].serialize(value, pack);
257
371
  }
258
372
 
259
- deserialize(obj: any, prop: string | number, bytes: Bytes, model?: any): void {
260
- const index = bytes.readUIntN(this.choices.length-1);
373
+ deserialize(pack: DataPack) {
374
+ const index = pack.readNumber();
261
375
  if (index < 0 || index >= this.choices.length) {
262
376
  throw new DatabaseError(`Could not deserialize invalid union index ${index}`, 'DESERIALIZATION_ERROR');
263
377
  }
264
378
  const type = this.choices[index];
265
- type.deserialize(obj, prop, bytes, model);
379
+ return type.deserialize(pack);
266
380
  }
267
381
 
268
- getErrors(obj: any, prop: string | number): DatabaseError[] {
269
- const errors: DatabaseError[] = [];
270
- for (let i = 0; i < this.choices.length; i++) {
271
- const type = this.choices[i];
272
- const subErrors = type.getErrors(obj, prop);
273
- if (subErrors.length === 0) return [];
274
- for (let err of subErrors) {
275
- errors.push(addErrorPath(err, `opt${i+1}`));
276
- }
382
+ getError(value: any) {
383
+ for (const choice of this.choices.values()) {
384
+ if (!choice.getError(value)) return;
277
385
  }
278
- return errors;
386
+ return new DatabaseError(`Value does not match any union type: ${value}`, 'INVALID_TYPE');
279
387
  }
280
388
 
281
- checkSkipIndex(obj: any, prop: string | number): boolean {
282
- const choiceIndex = this._getChoiceIndex(obj, prop);
283
- return this.choices[choiceIndex].checkSkipIndex(obj, prop);
389
+ containsNull(value: T): boolean {
390
+ const choiceIndex = this._getChoiceIndex(value);
391
+ return this.choices[choiceIndex].containsNull(value);
284
392
  }
285
393
 
286
- serializeType(bytes: Bytes): void {
287
- bytes.writeNumber(this.choices.length);
394
+ serializeType(pack: DataPack): void {
395
+ pack.write(this.choices.length);
288
396
  for (const choice of this.choices) {
289
- serializeType(choice, bytes);
397
+ serializeType(choice, pack);
290
398
  }
291
399
  }
292
400
 
293
- static deserializeType(bytes: Bytes, featureFlags: number): OrType<any> {
294
- const count = bytes.readNumber();
401
+ static deserializeType(pack: DataPack, featureFlags: number): OrType<any> {
402
+ const count = pack.readNumber();
295
403
  const choices: TypeWrapper<unknown>[] = [];
296
404
  for (let i = 0; i < count; i++) {
297
- choices.push(deserializeType(bytes, featureFlags));
405
+ choices.push(deserializeType(pack, featureFlags));
298
406
  }
299
407
  return new OrType(choices);
300
408
  }
409
+
410
+ clone(value: T): T {
411
+ const choiceIndex = this._getChoiceIndex(value);
412
+ return this.choices[choiceIndex].clone(value);
413
+ }
414
+
415
+ equals(a: T, b: T): boolean {
416
+ const ca = this._getChoiceIndex(a);
417
+ const cb = this._getChoiceIndex(b);
418
+ return ca === cb && this.choices[ca].equals(a, b);
419
+ }
420
+
421
+ toString() { return `or<${this.choices.join('|')}>`; }
422
+
423
+ getLinkedModel() {
424
+ let model;
425
+ for (const choice of this.choices) {
426
+ const m = choice.getLinkedModel();
427
+ if (m) {
428
+ if (model && model !== m) throw new DatabaseError(`Union type has multiple linked models, unsupported by getLinkedModel()`, 'INVALID_TYPE');
429
+ model = m;
430
+ }
431
+ }
432
+ return model;
433
+ }
301
434
  }
302
435
 
303
436
  /**
304
- * @internal Type wrapper for literal values (constants).
305
- * @template T - The literal type this wrapper represents.
306
- */
437
+ * @internal Type wrapper for literal values (constants).
438
+ * @template T - The literal type this wrapper represents.
439
+ */
307
440
  class LiteralType<const T> extends TypeWrapper<T> {
308
441
  kind = 'literal';
309
442
 
310
443
  /**
311
- * Create a new LiteralType.
312
- * @param value - The literal value this type represents.
313
- */
444
+ * Create a new LiteralType.
445
+ * @param value - The literal value this type represents.
446
+ */
314
447
  constructor(public value: T) {
315
448
  super();
316
449
  }
317
450
 
318
- serialize(obj: any, prop: string | number, bytes: Bytes, model?: any) {
451
+ serialize(value: T, pack: DataPack) {
319
452
  // Literal values don't need to be serialized since they're constants
320
453
  }
321
454
 
322
- deserialize(obj: any, prop: string | number, bytes: Bytes, model?: any): void {
323
- obj[prop] = this.value;
455
+ deserialize(pack: DataPack) {
456
+ return this.value;
324
457
  }
325
458
 
326
- getErrors(obj: any, prop: string | number): DatabaseError[] {
327
- return this.value===obj[prop] ? [] : [new DatabaseError(`Invalid literal value ${obj[prop]} instead of ${this.value}`, 'INVALID_TYPE')];
459
+ getError(value: any) {
460
+ if (this.value!==value) {
461
+ return new DatabaseError(`Invalid literal value ${value} instead of ${this.value}`, 'INVALID_TYPE');
462
+ }
328
463
  }
329
464
 
330
- serializeType(bytes: Bytes): void {
331
- bytes.writeString(this.value===undefined ? "" : JSON.stringify(this.value));
465
+ serializeType(pack: DataPack): void {
466
+ pack.write(this.value===undefined ? "" : JSON.stringify(this.value));
332
467
  }
333
-
334
- checkSkipIndex(obj: any, prop: string | number): boolean {
335
- return obj[prop] == null;
468
+
469
+ containsNull(value: T): boolean {
470
+ return value == null;
336
471
  }
337
472
 
338
- static deserializeType(bytes: Bytes, featureFlags: number): LiteralType<any> {
339
- const json = bytes.readString();
473
+ static deserializeType(pack: DataPack, featureFlags: number): LiteralType<any> {
474
+ const json = pack.readString();
340
475
  const value = json==="" ? undefined : JSON.parse(json);
341
476
  return new LiteralType(value);
342
477
  }
478
+
479
+ toString() { return `literal<${JSON.stringify(this.value)}>`; }
343
480
 
344
- default(model: any): T {
481
+ default(): T {
345
482
  return this.value;
346
483
  }
347
484
  }
348
485
 
349
- const ID_SIZE = 7;
486
+ const ID_SIZE = 8;
350
487
 
351
488
  /**
352
- * @internal Type wrapper for auto-generated unique identifier strings.
353
- */
489
+ * @internal Type wrapper for auto-generated unique identifier strings.
490
+ */
354
491
  class IdentifierType extends TypeWrapper<string> {
355
492
  kind = 'id';
356
-
357
- serialize(obj: any, prop: string|number, bytes: Bytes): void {
358
- const value = obj[prop];
493
+
494
+ serialize(value: string, pack: DataPack): void {
359
495
  assert(value.length === ID_SIZE);
360
- bytes.writeBase64(value);
496
+ pack.writeIdentifier(value);
361
497
  }
362
-
363
- deserialize(obj: any, prop: string | number, bytes: Bytes): void {
364
- obj[prop] = bytes.readBase64(ID_SIZE);
498
+
499
+ deserialize(pack: DataPack) {
500
+ return pack.readIdentifier();
365
501
  }
366
502
 
367
- getErrors(obj: any, prop: string | number): DatabaseError[] {
368
- const value = obj[prop];
369
- if (typeof value !== 'string' || value.length !== ID_SIZE) return [new DatabaseError(`Invalid ID format: ${value}`, 'VALUE_ERROR')];
370
- return [];
503
+ getError(value: any) {
504
+ if (typeof value !== 'string' || value.length !== ID_SIZE) return new DatabaseError(`Invalid ID format: ${value}`, 'VALUE_ERROR');
371
505
  }
372
506
 
373
- serializeType(bytes: Bytes): void {
507
+ serializeType(pack: DataPack): void {
374
508
  }
375
509
 
376
- static deserializeType(bytes: Bytes, featureFlags: number): IdentifierType {
510
+ static deserializeType(pack: DataPack, featureFlags: number): IdentifierType {
377
511
  return new IdentifierType();
378
512
  }
379
-
513
+
380
514
  default(model: Model<any>): string {
381
515
  // Generate a random ID, and if it already exists in the database, retry.
382
516
  let id: string;
383
517
  do {
384
- // Combine a timestamp with randomness, to create locality of reference as well as a high chance of uniqueness.
385
- // Bits 9...42 are the date (wrapping about four times a year)
386
- // Bit 0...14 are random bits (partly overlapping with the date, adding up to 31ms of jitter)
387
- let num = Math.floor(+new Date() * (1<<9) + Math.random() * (1<<14));
388
-
389
- id = '';
390
- for(let i = 0; i < 7; i++) {
391
- id = Bytes.BASE64_CHARS[num & 0x3f] + id;
392
- num = Math.floor(num / 64);
393
- }
394
- } while (olmdb.get(new Bytes().writeNumber(model.constructor._pk!._cachedIndexId!).writeBase64(id).getBuffer()));
518
+ id = DataPack.generateIdentifier();
519
+ } while (dbGet(model._txn.id, new DataPack().write(model.constructor._primary!._indexId!).writeIdentifier(id).toUint8Array()));
395
520
  return id;
396
521
  }
397
522
  }
398
523
 
399
- const WANT_PK_ARRAY = {};
400
-
401
- type KeysOfType<T, TProp> = { [P in keyof T]: T[P] extends TProp? P : never}[keyof T];
402
-
403
524
  /**
404
- * @internal Type wrapper for model relationships (foreign keys).
405
- * @template T - The target model class type.
406
- */
407
- export class LinkType<T extends typeof Model<any>> extends TypeWrapper<InstanceType<T>> {
525
+ * @internal Type wrapper for model relationships (foreign keys).
526
+ * @template T - The target model class type.
527
+ */
528
+ export class LinkType<T extends typeof Model<unknown>> extends TypeWrapper<InstanceType<T>> {
408
529
  kind = 'link';
409
- TargetModel: T;
530
+ tableName: string;
410
531
 
411
532
  /**
412
- * Create a new LinkType.
413
- * @param TargetModel - The model class this link points to.
414
- */
533
+ * Create a new LinkType.
534
+ * @param TargetModel - The model class this link points to.
535
+ */
415
536
  constructor(TargetModel: T) {
416
537
  super();
417
- this.TargetModel = getMockModel(TargetModel);
418
- }
419
-
420
- serialize(obj: any, prop: string | number, bytes: Bytes, model: Model<InstanceType<T>>): void {
421
- let pkArray;
422
- const pk = this.TargetModel._pk!;
423
- // If obj[prop] is getter(), it will return the primary key array (based on WANT_PK_ARRAY
424
- // being the receiver). Otherwise, it will just return the value, which is a model instance.
425
- let value = Reflect.get(obj, prop, WANT_PK_ARRAY) as any[] | Model<InstanceType<T>>;
426
- if (value instanceof Array) {
427
- // It's a pk array, and the object has not been loaded. We can just serialize it.
428
- pk._serializeArgs(value, bytes);
429
- } else {
430
- // It's a model instance that has been loaded
431
- pk._serializeModel(value, bytes);
432
- }
538
+ this.tableName = (TargetModel as any).tableName || TargetModel.name;
539
+ }
540
+
541
+ serialize(model: InstanceType<T>, pack: DataPack) {
542
+ pack.write(model.getPrimaryKey());
543
+ }
544
+
545
+ deserialize(pack: DataPack) {
546
+ return modelRegistry[this.tableName]._primary!._get(currentTxn(), pack.readUint8Array(), false);
433
547
  }
434
548
 
435
- deserialize(obj: any, prop: string, bytes: Bytes, sourceModel: Model<unknown>) {
436
- const pk = this.TargetModel._pk!;
437
- const pkArray = pk._deserializeKey(bytes);
438
- const TargetModel = this.TargetModel;
439
-
440
- // Define a getter to load the model on first access
441
- Object.defineProperty(obj, prop, {
442
- get: function() {
443
- // Special case to return the primary key array instead of load the model, used by serialize.
444
- if (this === WANT_PK_ARRAY) return pkArray;
445
- const targetModel = TargetModel._pk!.get(...pkArray); // load by primary key Uint8Array
446
- if (!targetModel) {
447
- throw new DatabaseError(`Linked ${TargetModel.tableName} instance ${pkArray.join(', ')} not found`, 'BROKEN_LINK');
448
- }
449
- this[prop] = targetModel; // Cause set() to be called, so our property will be come a regular value
450
- return targetModel;
451
- },
452
- set: function(newValue) {
453
- // Convert back to a regular value property
454
- Object.defineProperty(this, prop, {
455
- value: newValue,
456
- writable: true,
457
- enumerable: true,
458
- configurable: true
459
- });
460
- },
461
- enumerable: true,
462
- configurable: true
463
- });
464
- }
465
-
466
- getErrors(obj: any, prop: string | number): DatabaseError[] {
467
- if (!(obj[prop] instanceof this.TargetModel)) {
468
- return [new DatabaseError(`Expected instance of ${this.TargetModel.tableName}, got ${typeof obj[prop]}`, 'VALUE_ERROR')];
549
+ getError(value: InstanceType<T>) {
550
+ if (!(value instanceof modelRegistry[this.tableName])) {
551
+ return new DatabaseError(`Expected instance of ${this.tableName}, got ${typeof value}`, 'VALUE_ERROR');
469
552
  }
470
- return [];
471
553
  }
472
554
 
473
- serializeType(bytes: Bytes): void {
474
- bytes.writeString(this.TargetModel.tableName);
555
+ serializeType(pack: DataPack): void {
556
+ pack.write(this.tableName);
475
557
  }
476
558
 
477
- static deserializeType(bytes: Bytes, featureFlags: number): LinkType<any> {
478
- const tableName = bytes.readString();
559
+ static deserializeType(pack: DataPack, featureFlags: number): LinkType<any> {
560
+ const tableName = pack.readString();
479
561
  const targetModel = modelRegistry[tableName];
480
562
  if (!targetModel) throw new DatabaseError(`Could not deserialize undefined model ${tableName}`, 'DESERIALIZATION_ERROR');
481
563
  return new LinkType(targetModel);
482
564
  }
565
+
566
+ toString() { return `link<${this.tableName}>`; }
567
+
568
+ getLinkedModel(): T {
569
+ return modelRegistry[this.tableName] as T;
570
+ }
483
571
  }
484
572
 
485
- /** Constant representing the string type. */
486
- export const string = new StringType();
573
+ /** Type wrapper instance for the string type. */
574
+ export const string = new StringType() as TypeWrapper<string>;
487
575
 
488
- /** Constant representing the number type. */
489
- export const number = new NumberType();
576
+ /** Type wrapper instance for the ordered string type, which is just like a string
577
+ * except that it sorts lexicographically in the database (instead of by incrementing
578
+ * length first), making it suitable for index fields that want lexicographic range
579
+ * scans. Ordered strings are implemented as null-terminated UTF-8 strings, so they
580
+ * may not contain null characters.
581
+ */
582
+ export const orderedString = new OrderedStringType() as TypeWrapper<string>;
490
583
 
491
- /** Constant representing the boolean type. */
492
- export const boolean = new BooleanType();
584
+ /** Type wrapper instance for the number type. */
585
+ export const number = new NumberType() as TypeWrapper<number>;
493
586
 
494
- /** Constant representing the identifier type. */
495
- export const identifier = new IdentifierType();
587
+ /** Type wrapper instance for the date/time type. */
588
+ export const dateTime = new DateTimeType() as TypeWrapper<Date>;
589
+
590
+ /** Type wrapper instance for the boolean type. */
591
+ export const boolean = new BooleanType() as TypeWrapper<boolean>;
592
+
593
+ /** Type wrapper instance for the identifier type. */
594
+ export const identifier = new IdentifierType() as TypeWrapper<string>;
595
+
596
+ /** Type wrapper instance for the 'undefined' type. */
597
+ export const undef = new LiteralType(undefined) as TypeWrapper<undefined>;
496
598
 
497
599
  /**
498
- * Create a literal type wrapper for a constant value.
499
- * @template T - The literal type.
500
- * @param value - The literal value.
501
- * @returns A literal type instance.
502
- *
503
- * @example
504
- * ```typescript
505
- * const statusType = E.literal("active");
506
- * const countType = E.literal(42);
507
- * ```
508
- */
509
- export function literal<const T>(value: T) {
510
- return new LiteralType<T>(value);
600
+ * Create a literal type wrapper for a constant value.
601
+ * @template T - The literal type.
602
+ * @param value - The literal value.
603
+ * @returns A literal type instance.
604
+ *
605
+ * @example
606
+ * ```typescript
607
+ * const statusType = E.literal("active");
608
+ * const countType = E.literal(42);
609
+ * ```
610
+ */
611
+ export function literal<const T>(value: T): TypeWrapper<T> {
612
+ return new LiteralType(value);
511
613
  }
512
614
 
513
615
  /**
514
- * Create a union type wrapper from multiple type choices.
515
- * @template T - Array of type wrapper or basic types.
516
- * @param choices - The type choices for the union.
517
- * @returns A union type instance.
518
- *
519
- * @example
520
- * ```typescript
521
- * const stringOrNumber = E.or(E.string, E.number);
522
- * const status = E.or("active", "inactive", "pending");
523
- * ```
524
- */
525
- export function or<const T extends (TypeWrapper<unknown>|BasicType)[]>(...choices: T) {
526
- return new OrType<UnwrapTypes<T>>(choices.map(wrapIfLiteral) as any);
616
+ * Create a union type wrapper from multiple type choices.
617
+ * @template T - Array of type wrapper or basic types.
618
+ * @param choices - The type choices for the union.
619
+ * @returns A union type instance.
620
+ *
621
+ * @example
622
+ * ```typescript
623
+ * const stringOrNumber = E.or(E.string, E.number);
624
+ * const status = E.or("active", "inactive", "pending");
625
+ * ```
626
+ */
627
+ export function or<const T extends (TypeWrapper<unknown>|BasicType)[]>(...choices: T): TypeWrapper<UnwrapTypes<T>> {
628
+ return new OrType(choices.map(wrapIfLiteral));
527
629
  }
528
630
 
529
- /** Constant representing the 'undefined' type. */
530
- export const undef = new LiteralType(undefined);
531
-
532
631
  /**
533
- * Create an optional type wrapper (allows undefined).
534
- * @template T - Type wrapper or basic type to make optional.
535
- * @param inner - The inner type to make optional.
536
- * @returns A union type that accepts the inner type or undefined.
537
- *
538
- * @example
539
- * ```typescript
540
- * const optionalString = E.opt(E.string);
541
- * const optionalNumber = E.opt(E.number);
542
- * ```
543
- */
544
- export function opt<const T extends TypeWrapper<unknown>|BasicType>(inner: T) {
632
+ * Create an optional type wrapper (allows undefined).
633
+ * @template T - Type wrapper or basic type to make optional.
634
+ * @param inner - The inner type to make optional.
635
+ * @returns A union type that accepts the inner type or undefined.
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * const optionalString = E.opt(E.string);
640
+ * const optionalNumber = E.opt(E.number);
641
+ * ```
642
+ */
643
+ export function opt<const T extends TypeWrapper<unknown>|BasicType>(inner: T): TypeWrapper<UnwrapTypes<[T, typeof undef]>> {
545
644
  return or(undef, inner);
546
645
  }
547
646
 
548
647
  /**
549
- * Create an array type wrapper with optional length constraints.
550
- * @template T - The element type.
551
- * @param inner - Type wrapper for array elements.
552
- * @param opts - Optional constraints (min/max length).
553
- * @returns An array type instance.
554
- *
555
- * @example
556
- * ```typescript
557
- * const stringArray = E.array(E.string);
558
- * const boundedArray = E.array(E.number, {min: 1, max: 10});
559
- * ```
560
- */
561
- export function array<const T>(inner: TypeWrapper<T>, opts: {min?: number, max?: number} = {}) {
562
- return new ArrayType<T>(wrapIfLiteral(inner), opts);
648
+ * Create an array type wrapper with optional length constraints.
649
+ * @template T - The element type.
650
+ * @param inner - Type wrapper for array elements.
651
+ * @param opts - Optional constraints (min/max length).
652
+ * @returns An array type instance.
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * const stringArray = E.array(E.string);
657
+ * const boundedArray = E.array(E.number, {min: 1, max: 10});
658
+ * ```
659
+ */
660
+ export function array<const T>(inner: TypeWrapper<T>, opts: {min?: number, max?: number} = {}): TypeWrapper<T[]> {
661
+ return new ArrayType(wrapIfLiteral(inner), opts);
563
662
  }
564
663
 
565
664
  /**
566
- * Create a link type wrapper for model relationships.
567
- * @template T - The target model class.
568
- * @param TargetModel - The model class this link points to.
569
- * @returns A link type instance.
570
- *
571
- * @example
572
- * ```typescript
573
- * class User extends E.Model<User> {
574
- * posts = E.field(E.array(E.link(Post, 'author')));
575
- * }
576
- *
577
- * class Post extends E.Model<Post> {
578
- * author = E.field(E.link(User));
579
- * }
580
- * ```
581
- */
582
- export function link<const T extends typeof Model<any>>(TargetModel: T) {
583
- return new LinkType<T>(TargetModel);
665
+ * Create a Set type wrapper with optional length constraints.
666
+ * @template T - The element type.
667
+ * @param inner - Type wrapper for set elements.
668
+ * @param opts - Optional constraints (min/max length).
669
+ * @returns A set type instance.
670
+ *
671
+ * @example
672
+ * ```typescript
673
+ * const stringSet = E.set(E.string);
674
+ * const boundedSet = E.set(E.number, {min: 1, max: 10});
675
+ * ```
676
+ */
677
+ export function set<const T>(inner: TypeWrapper<T>, opts: {min?: number, max?: number} = {}): TypeWrapper<Set<T>> {
678
+ return new SetType(wrapIfLiteral(inner), opts);
679
+ }
680
+
681
+ /**
682
+ * Create a link type wrapper for model relationships.
683
+ * @template T - The target model class.
684
+ * @param TargetModel - The model class this link points to.
685
+ * @returns A link type instance.
686
+ *
687
+ * @example
688
+ * ```typescript
689
+ * class User extends E.Model<User> {
690
+ * posts = E.field(E.array(E.link(Post, 'author')));
691
+ * }
692
+ *
693
+ * class Post extends E.Model<Post> {
694
+ * author = E.field(E.link(User));
695
+ * }
696
+ * ```
697
+ */
698
+ export function link<const T extends typeof Model<any>>(TargetModel: T): TypeWrapper<InstanceType<T>> {
699
+ return new LinkType(TargetModel);
584
700
  }
585
701
 
586
702
 
587
703
  // Utility types and functions
588
- export type BasicType = TypeWrapper<any> | string | number | boolean | undefined | null;
704
+ export type BasicType = string | number | boolean | undefined | null; // TypeWrapper<any>
589
705
 
590
- export type UnwrapTypes<T extends BasicType[]> = {
706
+ export type UnwrapTypes<T extends (TypeWrapper<unknown> | BasicType)[]> = {
591
707
  [K in keyof T]: T[K] extends TypeWrapper<infer U> ? U : T[K];
592
708
  }[number];
593
709
 
@@ -598,37 +714,40 @@ function wrapIfLiteral(type: any) {
598
714
  }
599
715
 
600
716
  /**
601
- * Serialize a type wrapper to bytes for schema persistence.
602
- * @param arg - The type wrapper to serialize.
603
- * @param bytes - The Bytes instance to write to.
604
- */
605
- export function serializeType(arg: TypeWrapper<any>, bytes: Bytes) {
606
- bytes.writeString(arg.kind);
607
- arg.serializeType(bytes);
717
+ * Serialize a type wrapper to a Pack for schema persistence.
718
+ * @param arg - The type wrapper to serialize.
719
+ * @param pack - The Pack instance to write to.
720
+ */
721
+ export function serializeType(arg: TypeWrapper<any>, pack: DataPack) {
722
+ pack.write(arg.kind);
723
+ arg.serializeType(pack);
608
724
  }
609
725
 
610
- const TYPE_WRAPPERS: Record<string, TypeWrapper<any> | {deserializeType: (bytes: Bytes, featureFlags: number) => TypeWrapper<any>}> = {
726
+ const TYPE_WRAPPERS: Record<string, TypeWrapper<any> | {deserializeType: (pack: DataPack, featureFlags: number) => TypeWrapper<any>}> = {
611
727
  string: string,
612
728
  number: number,
729
+ dateTime: dateTime,
730
+ boolean: boolean,
613
731
  array: ArrayType,
732
+ set: SetType,
614
733
  or: OrType,
615
734
  literal: LiteralType,
616
- boolean: boolean,
617
735
  id: identifier,
618
736
  link: LinkType,
619
737
  };
620
738
 
621
739
  /**
622
- * Deserialize a type wrapper from bytes.
623
- * @param bytes - The Bytes instance to read from.
624
- * @param featureFlags - Feature flags for version compatibility.
625
- * @returns The deserialized type wrapper.
626
- */
627
- export function deserializeType(bytes: Bytes, featureFlags: number): TypeWrapper<any> {
628
- const kind = bytes.readString();
740
+ * Deserialize a type wrapper from a Pack.
741
+ * @param pack - The Pack instance to read from.
742
+ * @param featureFlags - Feature flags for version compatibility.
743
+ * @returns The deserialized type wrapper.
744
+ */
745
+ export function deserializeType(pack: DataPack, featureFlags: number): TypeWrapper<any> {
746
+ const kind = pack.readString();
629
747
  const TypeWrapper = TYPE_WRAPPERS[kind];
748
+ if (!TypeWrapper) throw new DatabaseError(`Unknown field type in database: ${kind}`, 'CONSISTENCY_ERROR');
630
749
  if ('deserializeType' in TypeWrapper) {
631
- return TypeWrapper.deserializeType(bytes, featureFlags);
750
+ return TypeWrapper.deserializeType(pack, featureFlags);
632
751
  } else {
633
752
  return TypeWrapper;
634
753
  }