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/README.md +450 -218
- package/build/src/datapack.d.ts +138 -0
- package/build/src/datapack.js +684 -0
- package/build/src/datapack.js.map +1 -0
- package/build/src/edinburgh.d.ts +41 -11
- package/build/src/edinburgh.js +163 -43
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +100 -111
- package/build/src/indexes.js +679 -369
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate-cli.d.ts +20 -0
- package/build/src/migrate-cli.js +122 -0
- package/build/src/migrate-cli.js.map +1 -0
- package/build/src/migrate.d.ts +33 -0
- package/build/src/migrate.js +225 -0
- package/build/src/migrate.js.map +1 -0
- package/build/src/models.d.ts +147 -46
- package/build/src/models.js +322 -268
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +209 -260
- package/build/src/types.js +423 -324
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +9 -9
- package/build/src/utils.js +32 -9
- package/build/src/utils.js.map +1 -1
- package/package.json +14 -11
- package/src/datapack.ts +726 -0
- package/src/edinburgh.ts +174 -43
- package/src/indexes.ts +722 -380
- package/src/migrate-cli.ts +138 -0
- package/src/migrate.ts +267 -0
- package/src/models.ts +415 -285
- package/src/types.ts +510 -391
- package/src/utils.ts +40 -12
- package/build/src/bytes.d.ts +0 -155
- package/build/src/bytes.js +0 -455
- package/build/src/bytes.js.map +0 -1
- package/src/bytes.ts +0 -500
package/build/src/types.js
CHANGED
|
@@ -1,483 +1,578 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
57
|
-
|
|
47
|
+
serialize(value, pack) {
|
|
48
|
+
pack.write(value);
|
|
58
49
|
}
|
|
59
|
-
deserialize(
|
|
60
|
-
|
|
50
|
+
deserialize(pack) {
|
|
51
|
+
return pack.readString();
|
|
61
52
|
}
|
|
62
|
-
|
|
63
|
-
if (typeof
|
|
64
|
-
return
|
|
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
|
-
|
|
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(
|
|
75
|
-
|
|
66
|
+
serialize(value, pack) {
|
|
67
|
+
pack.write(value);
|
|
76
68
|
}
|
|
77
|
-
deserialize(
|
|
78
|
-
|
|
69
|
+
deserialize(pack) {
|
|
70
|
+
return pack.readNumber();
|
|
79
71
|
}
|
|
80
|
-
|
|
81
|
-
const value = obj[prop];
|
|
72
|
+
getError(value) {
|
|
82
73
|
if (typeof value !== 'number' || isNaN(value)) {
|
|
83
|
-
return
|
|
74
|
+
return new DatabaseError(`Expected number, got ${typeof value}`, 'INVALID_TYPE');
|
|
84
75
|
}
|
|
85
|
-
return [];
|
|
86
76
|
}
|
|
87
77
|
}
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
94
|
-
|
|
98
|
+
serialize(value, pack) {
|
|
99
|
+
pack.write(value);
|
|
95
100
|
}
|
|
96
|
-
deserialize(
|
|
97
|
-
|
|
101
|
+
deserialize(pack) {
|
|
102
|
+
return pack.readBoolean();
|
|
98
103
|
}
|
|
99
|
-
|
|
100
|
-
if (typeof
|
|
101
|
-
return
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
125
|
-
|
|
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
|
|
131
|
+
this.inner.serialize(value[i], pack);
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
|
-
deserialize(
|
|
132
|
-
const length =
|
|
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(
|
|
138
|
+
result.push(this.inner.deserialize(pack));
|
|
136
139
|
}
|
|
137
|
-
|
|
140
|
+
return result;
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
const value = obj[prop];
|
|
142
|
+
getError(value) {
|
|
141
143
|
if (!Array.isArray(value)) {
|
|
142
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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(
|
|
159
|
-
serializeType(this.inner,
|
|
158
|
+
serializeType(pack) {
|
|
159
|
+
serializeType(this.inner, pack);
|
|
160
160
|
}
|
|
161
|
-
static deserializeType(
|
|
162
|
-
const inner = deserializeType(
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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(
|
|
281
|
+
_getChoiceIndex(value) {
|
|
182
282
|
for (const [i, choice] of this.choices.entries()) {
|
|
183
|
-
if (choice.
|
|
283
|
+
if (!choice.getError(value))
|
|
184
284
|
return i;
|
|
185
|
-
}
|
|
186
285
|
}
|
|
187
|
-
throw new DatabaseError(`Value does not match any union type: ${
|
|
286
|
+
throw new DatabaseError(`Value does not match any union type: ${value}`, 'INVALID_TYPE');
|
|
188
287
|
}
|
|
189
|
-
serialize(
|
|
190
|
-
const choiceIndex = this._getChoiceIndex(
|
|
191
|
-
|
|
192
|
-
this.choices[choiceIndex].serialize(
|
|
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(
|
|
195
|
-
const index =
|
|
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(
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
306
|
+
return new DatabaseError(`Value does not match any union type: ${value}`, 'INVALID_TYPE');
|
|
214
307
|
}
|
|
215
|
-
|
|
216
|
-
const choiceIndex = this._getChoiceIndex(
|
|
217
|
-
return this.choices[choiceIndex].
|
|
308
|
+
containsNull(value) {
|
|
309
|
+
const choiceIndex = this._getChoiceIndex(value);
|
|
310
|
+
return this.choices[choiceIndex].containsNull(value);
|
|
218
311
|
}
|
|
219
|
-
serializeType(
|
|
220
|
-
|
|
312
|
+
serializeType(pack) {
|
|
313
|
+
pack.write(this.choices.length);
|
|
221
314
|
for (const choice of this.choices) {
|
|
222
|
-
serializeType(choice,
|
|
315
|
+
serializeType(choice, pack);
|
|
223
316
|
}
|
|
224
317
|
}
|
|
225
|
-
static deserializeType(
|
|
226
|
-
const count =
|
|
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(
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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(
|
|
364
|
+
serialize(value, pack) {
|
|
250
365
|
// Literal values don't need to be serialized since they're constants
|
|
251
366
|
}
|
|
252
|
-
deserialize(
|
|
253
|
-
|
|
367
|
+
deserialize(pack) {
|
|
368
|
+
return this.value;
|
|
254
369
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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(
|
|
259
|
-
|
|
375
|
+
serializeType(pack) {
|
|
376
|
+
pack.write(this.value === undefined ? "" : JSON.stringify(this.value));
|
|
260
377
|
}
|
|
261
|
-
|
|
262
|
-
return
|
|
378
|
+
containsNull(value) {
|
|
379
|
+
return value == null;
|
|
263
380
|
}
|
|
264
|
-
static deserializeType(
|
|
265
|
-
const json =
|
|
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
|
-
|
|
386
|
+
toString() { return `literal<${JSON.stringify(this.value)}>`; }
|
|
387
|
+
default() {
|
|
270
388
|
return this.value;
|
|
271
389
|
}
|
|
272
390
|
}
|
|
273
|
-
const ID_SIZE =
|
|
391
|
+
const ID_SIZE = 8;
|
|
274
392
|
/**
|
|
275
|
-
|
|
276
|
-
|
|
393
|
+
* @internal Type wrapper for auto-generated unique identifier strings.
|
|
394
|
+
*/
|
|
277
395
|
class IdentifierType extends TypeWrapper {
|
|
278
396
|
kind = 'id';
|
|
279
|
-
serialize(
|
|
280
|
-
const value = obj[prop];
|
|
397
|
+
serialize(value, pack) {
|
|
281
398
|
assert(value.length === ID_SIZE);
|
|
282
|
-
|
|
399
|
+
pack.writeIdentifier(value);
|
|
283
400
|
}
|
|
284
|
-
deserialize(
|
|
285
|
-
|
|
401
|
+
deserialize(pack) {
|
|
402
|
+
return pack.readIdentifier();
|
|
286
403
|
}
|
|
287
|
-
|
|
288
|
-
const value = obj[prop];
|
|
404
|
+
getError(value) {
|
|
289
405
|
if (typeof value !== 'string' || value.length !== ID_SIZE)
|
|
290
|
-
return
|
|
291
|
-
return [];
|
|
406
|
+
return new DatabaseError(`Invalid ID format: ${value}`, 'VALUE_ERROR');
|
|
292
407
|
}
|
|
293
|
-
serializeType(
|
|
408
|
+
serializeType(pack) {
|
|
294
409
|
}
|
|
295
|
-
static deserializeType(
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
428
|
+
tableName;
|
|
323
429
|
/**
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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(
|
|
383
|
-
|
|
448
|
+
serializeType(pack) {
|
|
449
|
+
pack.write(this.tableName);
|
|
384
450
|
}
|
|
385
|
-
static deserializeType(
|
|
386
|
-
const tableName =
|
|
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
|
-
/**
|
|
463
|
+
/** Type wrapper instance for the string type. */
|
|
394
464
|
export const string = new StringType();
|
|
395
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
export function serializeType(arg,
|
|
493
|
-
|
|
494
|
-
arg.serializeType(
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
export function deserializeType(
|
|
513
|
-
const kind =
|
|
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(
|
|
615
|
+
return TypeWrapper.deserializeType(pack, featureFlags);
|
|
517
616
|
}
|
|
518
617
|
else {
|
|
519
618
|
return TypeWrapper;
|