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