edinburgh 0.4.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +403 -461
- package/build/src/datapack.d.ts +9 -9
- package/build/src/datapack.js +10 -10
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +21 -10
- package/build/src/edinburgh.js +33 -55
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +99 -288
- package/build/src/indexes.js +253 -636
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate.js +17 -39
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +177 -113
- package/build/src/models.js +487 -259
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +41 -51
- package/build/src/types.js +39 -52
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +4 -4
- package/build/src/utils.js +4 -4
- package/package.json +1 -3
- package/skill/AnyModelClass.md +7 -0
- package/skill/FindOptions.md +37 -0
- package/skill/Lifecycle Hooks.md +24 -0
- package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +2 -2
- package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
- package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
- package/skill/Lifecycle Hooks_migrate.md +26 -0
- package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +3 -5
- package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +2 -2
- package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
- package/skill/ModelBase.md +7 -0
- package/skill/ModelClass.md +8 -0
- package/skill/SKILL.md +253 -215
- package/skill/Schema Evolution.md +19 -0
- package/skill/TypeWrapper_containsNull.md +11 -0
- package/skill/TypeWrapper_deserialize.md +9 -0
- package/skill/TypeWrapper_getError.md +11 -0
- package/skill/TypeWrapper_serialize.md +10 -0
- package/skill/TypeWrapper_serializeType.md +9 -0
- package/skill/array.md +2 -2
- package/skill/defineModel.md +23 -0
- package/skill/deleteEverything.md +8 -0
- package/skill/field.md +4 -4
- package/skill/link.md +12 -10
- package/skill/literal.md +1 -1
- package/skill/opt.md +1 -1
- package/skill/or.md +1 -1
- package/skill/record.md +1 -1
- package/skill/set.md +2 -2
- package/skill/setOnSaveCallback.md +2 -2
- package/skill/transact.md +3 -3
- package/src/datapack.ts +10 -10
- package/src/edinburgh.ts +46 -58
- package/src/indexes.ts +338 -802
- package/src/migrate.ts +15 -37
- package/src/models.ts +617 -314
- package/src/types.ts +61 -54
- package/src/utils.ts +4 -4
- package/skill/BaseIndex.md +0 -16
- package/skill/BaseIndex_batchProcess.md +0 -10
- package/skill/BaseIndex_find.md +0 -7
- package/skill/Model.md +0 -22
- package/skill/Model_findAll.md +0 -12
- package/skill/Model_migrate.md +0 -34
- package/skill/Model_replaceInto.md +0 -16
- package/skill/PrimaryIndex.md +0 -8
- package/skill/PrimaryIndex_get.md +0 -17
- package/skill/PrimaryIndex_getLazy.md +0 -13
- package/skill/SecondaryIndex.md +0 -9
- package/skill/UniqueIndex.md +0 -9
- package/skill/UniqueIndex_get.md +0 -17
- package/skill/dump.md +0 -8
- package/skill/index.md +0 -32
- package/skill/primary.md +0 -26
- package/skill/registerModel.md +0 -26
- package/skill/unique.md +0 -32
package/skill/SKILL.md
CHANGED
|
@@ -26,85 +26,82 @@ import * as E from "edinburgh";
|
|
|
26
26
|
// Initialize the database (optional, defaults to ".edinburgh")
|
|
27
27
|
E.init("./my-database");
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
@E.registerModel
|
|
31
|
-
class User extends E.Model<User> {
|
|
32
|
-
// Define a primary key (optional, defaults to using the "id" field)
|
|
33
|
-
static pk = E.primary(User, "id");
|
|
34
|
-
// Define a unique index on the email field
|
|
35
|
-
static byEmail = E.unique(User, "email");
|
|
36
|
-
|
|
37
|
-
// Define fields with simple types -- they will be type-checked at compile time and validated at runtime.
|
|
29
|
+
const User = E.defineModel("User", class {
|
|
38
30
|
id = E.field(E.identifier);
|
|
39
31
|
name = E.field(E.string);
|
|
40
32
|
age = E.field(E.number);
|
|
41
|
-
email = E.field(E.opt(E.string));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
email = E.field(E.opt(E.string));
|
|
34
|
+
// Optional link to another instance of this model (needs a function as `User` is not defined yet at this point)
|
|
35
|
+
supervisor = E.field(E.opt(E.link(() => User)));
|
|
36
|
+
// A field with a more elaborate type. In TypeScript: `User | User[] | "unknown" | "whatever"`, defaulting to "unknown".
|
|
37
|
+
something = E.field(
|
|
38
|
+
E.or(
|
|
39
|
+
E.link(() => User),
|
|
40
|
+
E.array(E.link(() => User)),
|
|
41
|
+
E.literal("unknown"),
|
|
42
|
+
E.literal("whatever")
|
|
43
|
+
),
|
|
44
|
+
{ default: "unknown" }
|
|
45
|
+
);
|
|
46
|
+
}, {
|
|
47
|
+
pk: "id",
|
|
48
|
+
unique: {
|
|
49
|
+
email: "email",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
49
52
|
|
|
50
|
-
// Use in transactions
|
|
51
53
|
await E.transact(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const john = new User({ // Unique 'id' is automatically generated if not provided
|
|
57
|
-
name: "John Doe",
|
|
54
|
+
// Unique 'id' values are auto-generated if not provided
|
|
55
|
+
const boss = new User({ name: "Big Boss", age: 50 });
|
|
56
|
+
new User({
|
|
57
|
+
name: "John Doe",
|
|
58
58
|
age: 41,
|
|
59
59
|
email: "john@example.com",
|
|
60
60
|
supervisor: boss, // Link to another model instance
|
|
61
61
|
});
|
|
62
|
+
// Newly instantiated models are automatically saved to the database on transaction commit
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
await E.transact(() => {
|
|
65
66
|
// Query by unique index
|
|
66
|
-
|
|
67
|
+
const john = User.getBy("email", "john@example.com")!;
|
|
67
68
|
|
|
68
69
|
// The transaction will retry if there's a conflict, such as another transaction
|
|
69
70
|
// modifying the same user (from another async function or another process)
|
|
70
71
|
john.age++;
|
|
71
72
|
|
|
72
73
|
// The supervisor object is lazy loaded on first access
|
|
73
|
-
console.log(`${john.supervisor!.name} is ${john.name}'s supervisor`);
|
|
74
|
+
console.log(`${john.supervisor!.name} is ${john.name}'s supervisor`);
|
|
74
75
|
});
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
## Tutorial
|
|
78
79
|
|
|
79
|
-
### TypeScript Configuration
|
|
80
|
-
|
|
81
|
-
When using TypeScript to transpile to JavaScript, make sure to enable the following options in your `tsconfig.json`:
|
|
82
|
-
|
|
83
|
-
```json
|
|
84
|
-
{
|
|
85
|
-
"compilerOptions": {
|
|
86
|
-
"target": "es2022",
|
|
87
|
-
"experimentalDecorators": true
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
80
|
|
|
92
81
|
### Defining Models
|
|
93
82
|
|
|
94
|
-
|
|
83
|
+
A model is defined using the `E.defineModel()` function by passing it..
|
|
84
|
+
- a consistent table name,
|
|
85
|
+
- an (anonymous) class containing `E.field` database properties and optionally regular properties/methods, and
|
|
86
|
+
- optional key/index configuration.
|
|
95
87
|
|
|
96
88
|
```typescript
|
|
97
89
|
import * as E from "edinburgh";
|
|
98
90
|
|
|
99
|
-
|
|
100
|
-
class User extends E.Model<User> {
|
|
101
|
-
static pk = E.primary(User, "id");
|
|
102
|
-
|
|
91
|
+
const User = E.defineModel("User", class {
|
|
103
92
|
id = E.field(E.identifier);
|
|
104
93
|
name = E.field(E.string);
|
|
105
94
|
email = E.field(E.string);
|
|
106
95
|
age = E.field(E.number);
|
|
107
|
-
}
|
|
96
|
+
}, {
|
|
97
|
+
pk: "id",
|
|
98
|
+
unique: {
|
|
99
|
+
email: "email",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
// Add this if you want to use User as a type annotation (e.g. `let u: User`).
|
|
103
|
+
// Not needed just to call User.get(), User.find(), new User(), etc.
|
|
104
|
+
type User = InstanceType<typeof User>;
|
|
108
105
|
```
|
|
109
106
|
|
|
110
107
|
Instance fields are declared with `E.field(type, options?)`. Available types:
|
|
@@ -128,16 +125,13 @@ Instance fields are declared with `E.field(type, options?)`. Available types:
|
|
|
128
125
|
#### Defaults
|
|
129
126
|
|
|
130
127
|
```typescript
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
static pk = E.primary(Post, "id");
|
|
134
|
-
|
|
135
|
-
id = E.field(E.identifier); // auto-generated
|
|
128
|
+
const Post = E.defineModel("Post", class {
|
|
129
|
+
id = E.field(E.identifier); // auto-generated
|
|
136
130
|
title = E.field(E.string);
|
|
137
131
|
status = E.field(E.or("draft", "published"), {default: "draft"});
|
|
138
|
-
tags = E.field(E.array(E.string), {default: () => []});
|
|
139
|
-
createdAt = E.field(E.dateTime);
|
|
140
|
-
}
|
|
132
|
+
tags = E.field(E.array(E.string), {default: () => []}); // use function for mutable defaults
|
|
133
|
+
createdAt = E.field(E.dateTime); // dateTime defaults to new Date()
|
|
134
|
+
}, { pk: "id" });
|
|
141
135
|
```
|
|
142
136
|
|
|
143
137
|
### Transactions
|
|
@@ -150,19 +144,19 @@ E.init("./my-database");
|
|
|
150
144
|
|
|
151
145
|
// Create
|
|
152
146
|
await E.transact(() => {
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
// User.id is auto-generated
|
|
148
|
+
new User({name: "Alice", email: "alice@example.com", age: 30});
|
|
155
149
|
});
|
|
156
150
|
|
|
157
151
|
// Read + Update
|
|
158
152
|
await E.transact(() => {
|
|
159
|
-
const user = User.
|
|
153
|
+
const user = User.getBy("email", "alice@example.com");
|
|
160
154
|
if (user) user.age++;
|
|
161
155
|
});
|
|
162
156
|
|
|
163
157
|
// Return values from transactions
|
|
164
158
|
const name = await E.transact(() => {
|
|
165
|
-
const user = User.
|
|
159
|
+
const user = User.getBy("email", "alice@example.com");
|
|
166
160
|
return user?.name;
|
|
167
161
|
});
|
|
168
162
|
```
|
|
@@ -174,30 +168,29 @@ Transactions auto-retry on conflict (up to 6 times by default). Keep transaction
|
|
|
174
168
|
Edinburgh supports three index types:
|
|
175
169
|
|
|
176
170
|
```typescript
|
|
177
|
-
|
|
178
|
-
class Product extends E.Model<Product> {
|
|
179
|
-
static pk = E.primary(Product, "sku"); // primary: one per model, stores data
|
|
180
|
-
static byName = E.unique(Product, "name"); // unique: enforces uniqueness + fast lookup
|
|
181
|
-
static byCategory = E.index(Product, "category");// secondary: non-unique, for queries
|
|
182
|
-
|
|
171
|
+
const Product = E.defineModel("Product", class {
|
|
183
172
|
sku = E.field(E.string);
|
|
184
173
|
name = E.field(E.string);
|
|
185
174
|
category = E.field(E.string);
|
|
186
175
|
price = E.field(E.number);
|
|
187
|
-
}
|
|
176
|
+
}, {
|
|
177
|
+
pk: "sku",
|
|
178
|
+
unique: { name: "name" },
|
|
179
|
+
index: { category: "category" },
|
|
180
|
+
});
|
|
188
181
|
```
|
|
189
182
|
|
|
190
|
-
If no `
|
|
183
|
+
If no `pk` is provided, Edinburgh auto-creates one on an `id` field, adding it as an `E.identifier` field if needed.
|
|
191
184
|
|
|
192
185
|
#### Lookups
|
|
193
186
|
|
|
194
187
|
```typescript
|
|
195
188
|
await E.transact(() => {
|
|
196
189
|
// Primary key lookup
|
|
197
|
-
const
|
|
190
|
+
const p1 = Product.get("SKU-001");
|
|
198
191
|
|
|
199
192
|
// Unique index lookup
|
|
200
|
-
const p2 = Product.
|
|
193
|
+
const p2 = Product.getBy("name", "Widget");
|
|
201
194
|
|
|
202
195
|
// All return undefined if not found
|
|
203
196
|
});
|
|
@@ -205,54 +198,53 @@ await E.transact(() => {
|
|
|
205
198
|
|
|
206
199
|
#### Range Queries
|
|
207
200
|
|
|
208
|
-
|
|
201
|
+
Primary-key queries use `.find()`. Named unique and secondary indexes use `.findBy(name, ...)`:
|
|
209
202
|
|
|
210
203
|
```typescript
|
|
211
204
|
await E.transact(() => {
|
|
212
205
|
// Exact match
|
|
213
|
-
for (const p of Product.
|
|
206
|
+
for (const p of Product.findBy("category", {is: "electronics"})) {
|
|
214
207
|
console.log(p.name);
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
// Range (inclusive)
|
|
218
|
-
for (const p of Product.
|
|
211
|
+
for (const p of Product.find({from: "A", to: "M"})) {
|
|
219
212
|
console.log(p.sku);
|
|
220
213
|
}
|
|
221
214
|
|
|
222
215
|
// Exclusive bounds
|
|
223
|
-
for (const p of Product.
|
|
216
|
+
for (const p of Product.find({after: "A", before: "M"})) { ... }
|
|
224
217
|
|
|
225
218
|
// Open-ended
|
|
226
|
-
for (const p of Product.
|
|
219
|
+
for (const p of Product.find({from: "M"})) { ... }
|
|
227
220
|
|
|
228
221
|
// Reverse
|
|
229
|
-
for (const p of Product.
|
|
222
|
+
for (const p of Product.find({reverse: true})) { ... }
|
|
230
223
|
|
|
231
224
|
// Count and fetch helpers
|
|
232
|
-
const count = Product.
|
|
233
|
-
const first = Product.
|
|
225
|
+
const count = Product.findBy("category", {is: "electronics"}).count();
|
|
226
|
+
const first = Product.findBy("category", {is: "electronics"}).fetch(); // first match or undefined
|
|
234
227
|
});
|
|
235
228
|
```
|
|
236
229
|
|
|
237
|
-
#### Composite
|
|
230
|
+
#### Composite Primary Keys
|
|
238
231
|
|
|
239
232
|
```typescript
|
|
240
|
-
|
|
241
|
-
class Event extends E.Model<Event> {
|
|
242
|
-
static pk = E.primary(Event, ["year", "month", "id"]);
|
|
243
|
-
|
|
233
|
+
const Event = E.defineModel("Event", class {
|
|
244
234
|
year = E.field(E.number);
|
|
245
235
|
month = E.field(E.number);
|
|
246
236
|
id = E.field(E.identifier);
|
|
247
237
|
title = E.field(E.string);
|
|
248
|
-
}
|
|
238
|
+
}, {
|
|
239
|
+
pk: ["year", "month", "id"] as const,
|
|
240
|
+
});
|
|
249
241
|
|
|
250
242
|
await E.transact(() => {
|
|
251
243
|
// Prefix matching — find all events in 2025
|
|
252
|
-
for (const e of Event.
|
|
244
|
+
for (const e of Event.find({is: [2025]})) { ... }
|
|
253
245
|
|
|
254
246
|
// Find events in March 2025
|
|
255
|
-
for (const e of Event.
|
|
247
|
+
for (const e of Event.find({is: [2025, 3]})) { ... }
|
|
256
248
|
});
|
|
257
249
|
```
|
|
258
250
|
|
|
@@ -261,10 +253,7 @@ await E.transact(() => {
|
|
|
261
253
|
You can freely add regular methods, getters, and other non-persistent properties to model classes. These work normally in JavaScript but are **not stored in the database** and **not synchronized** across transactions or processes.
|
|
262
254
|
|
|
263
255
|
```typescript
|
|
264
|
-
|
|
265
|
-
class User extends E.Model<User> {
|
|
266
|
-
static pk = E.primary(User, "id");
|
|
267
|
-
id = E.field(E.identifier);
|
|
256
|
+
const User = E.defineModel("User", class {
|
|
268
257
|
firstName = E.field(E.string);
|
|
269
258
|
lastName = E.field(E.string);
|
|
270
259
|
|
|
@@ -279,70 +268,68 @@ class User extends E.Model<User> {
|
|
|
279
268
|
greet(): string {
|
|
280
269
|
return `Hello, ${this.fullName}!`;
|
|
281
270
|
}
|
|
282
|
-
}
|
|
271
|
+
});
|
|
283
272
|
```
|
|
284
273
|
|
|
285
274
|
#### Computed Indexes
|
|
286
275
|
|
|
287
|
-
Instead of naming fields, you can pass a
|
|
276
|
+
Instead of naming fields, you can pass a function as an index specification. The function receives a model instance and returns an **array** of index key values. Each element creates a separate index entry, enabling multi-value indexes. Return `[]` to skip indexing for that instance (partial index).
|
|
288
277
|
|
|
289
278
|
```typescript
|
|
290
|
-
|
|
291
|
-
class Article extends E.Model<Article> {
|
|
292
|
-
static pk = E.primary(Article, "id");
|
|
293
|
-
static byFullName = E.unique(Article, (a: Article) => [`${a.firstName} ${a.lastName}`]);
|
|
294
|
-
static byWord = E.index(Article, (a: Article) => a.title.toLowerCase().split(" "));
|
|
295
|
-
static byDomain = E.index(Article, (a: Article) => a.email ? [a.email.split("@")[1]] : []);
|
|
296
|
-
|
|
279
|
+
const Article = E.defineModel("Article", class {
|
|
297
280
|
id = E.field(E.identifier);
|
|
298
281
|
firstName = E.field(E.string);
|
|
299
282
|
lastName = E.field(E.string);
|
|
300
283
|
title = E.field(E.string);
|
|
301
284
|
email = E.field(E.opt(E.string));
|
|
302
|
-
}
|
|
285
|
+
}, {
|
|
286
|
+
pk: "id",
|
|
287
|
+
unique: {
|
|
288
|
+
fullName: (a: any) => [`${a.firstName} ${a.lastName}`], // computed covering unique index
|
|
289
|
+
},
|
|
290
|
+
index: {
|
|
291
|
+
domain: (a: any) => a.email ? [a.email.split("@")[1]] : [], // computed partial index
|
|
292
|
+
word: (a: any) => a.title.toLowerCase().split(" "), // computed multi-index
|
|
293
|
+
},
|
|
294
|
+
});
|
|
303
295
|
|
|
304
296
|
await E.transact(() => {
|
|
305
297
|
new Article({ firstName: "Jane", lastName: "Doe", title: "Hello World", email: "jane@acme.com" });
|
|
306
298
|
|
|
307
299
|
// Lookup via computed unique index
|
|
308
|
-
const jane = Article.
|
|
300
|
+
const jane = Article.getBy("fullName", "Jane Doe");
|
|
309
301
|
|
|
310
302
|
// Multi-value: each word in the title is indexed separately
|
|
311
|
-
for (const a of Article.
|
|
303
|
+
for (const a of Article.findBy("word", {is: "hello"})) { ... }
|
|
312
304
|
|
|
313
305
|
// Partial index: articles without email are skipped
|
|
314
|
-
for (const a of Article.
|
|
306
|
+
for (const a of Article.findBy("domain", {is: "acme.com"})) { ... }
|
|
315
307
|
});
|
|
316
308
|
```
|
|
317
309
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
### Relationships (Links)
|
|
310
|
+
### Relationships
|
|
321
311
|
|
|
322
|
-
Use `E.link(Model)` for foreign keys:
|
|
312
|
+
Use `E.link(Model)` for foreign keys. Use a thunk (a function that just returns a value) for forward references when needed:
|
|
323
313
|
|
|
324
314
|
```typescript
|
|
325
|
-
|
|
326
|
-
class Author extends E.Model<Author> {
|
|
327
|
-
static pk = E.primary(Author, "id");
|
|
315
|
+
const Author = E.defineModel("Author", class {
|
|
328
316
|
id = E.field(E.identifier);
|
|
329
317
|
name = E.field(E.string);
|
|
330
|
-
}
|
|
318
|
+
}, { pk: "id" });
|
|
331
319
|
|
|
332
|
-
|
|
333
|
-
class Book extends E.Model<Book> {
|
|
334
|
-
static pk = E.primary(Book, "id");
|
|
320
|
+
const Book = E.defineModel("Book", class {
|
|
335
321
|
id = E.field(E.identifier);
|
|
336
322
|
title = E.field(E.string);
|
|
337
323
|
author = E.field(E.link(Author));
|
|
338
|
-
}
|
|
324
|
+
}, { pk: "id" });
|
|
339
325
|
|
|
340
326
|
await E.transact(() => {
|
|
341
327
|
const author = new Author({name: "Tolkien"});
|
|
342
328
|
const book = new Book({title: "The Hobbit", author});
|
|
343
329
|
|
|
344
330
|
// Later: linked models are lazy-loaded on property access
|
|
345
|
-
const b = Book.
|
|
331
|
+
const b = Book.get(book.id)!;
|
|
332
|
+
console.log(b.author.id); // no need to load yet..
|
|
346
333
|
console.log(b.author.name); // loads Author automatically (~1µs)
|
|
347
334
|
});
|
|
348
335
|
```
|
|
@@ -351,7 +338,7 @@ await E.transact(() => {
|
|
|
351
338
|
|
|
352
339
|
```typescript
|
|
353
340
|
await E.transact(() => {
|
|
354
|
-
const user = User.
|
|
341
|
+
const user = User.get(someId);
|
|
355
342
|
if (user) user.delete();
|
|
356
343
|
});
|
|
357
344
|
```
|
|
@@ -369,10 +356,16 @@ await E.transact(() => {
|
|
|
369
356
|
user.preventPersist(); // exclude from commit
|
|
370
357
|
});
|
|
371
358
|
|
|
372
|
-
//
|
|
359
|
+
// find() iterates all instances (or use range options)
|
|
373
360
|
await E.transact(() => {
|
|
374
|
-
for (const user of User.
|
|
375
|
-
for (const user of User.
|
|
361
|
+
for (const user of User.find()) { ... }
|
|
362
|
+
for (const user of User.find({reverse: true})) { ... }
|
|
363
|
+
|
|
364
|
+
// {fetch: 'first'} returns a single instance or undefined
|
|
365
|
+
const first = User.find({fetch: 'first'});
|
|
366
|
+
|
|
367
|
+
// {fetch: 'single'} returns a single instance, or throws if there are none or more than one
|
|
368
|
+
const only = User.find({fetch: 'single'});
|
|
376
369
|
});
|
|
377
370
|
|
|
378
371
|
// replaceInto: upsert by primary key
|
|
@@ -386,7 +379,7 @@ await E.transact(() => {
|
|
|
386
379
|
For large datasets, `batchProcess` auto-commits in batches:
|
|
387
380
|
|
|
388
381
|
```typescript
|
|
389
|
-
await Product.
|
|
382
|
+
await Product.batchProcess({ limitRows: 1000 }, (product) => {
|
|
390
383
|
product.category = "archived";
|
|
391
384
|
});
|
|
392
385
|
// Commits every ~1 second or 4096 rows (configurable via limitSeconds, limitRows)
|
|
@@ -394,12 +387,10 @@ await Product.byCategory.batchProcess({is: "old"}, (product) => {
|
|
|
394
387
|
|
|
395
388
|
### Lazy Schema Migrations
|
|
396
389
|
|
|
397
|
-
When you change a model's schema, Edinburgh
|
|
390
|
+
When you change a model's schema, Edinburgh lazily migrates old records on access. You can provide a `static migrate(record)` function to transform old rows:
|
|
398
391
|
|
|
399
392
|
```typescript
|
|
400
|
-
|
|
401
|
-
class User extends E.Model<User> {
|
|
402
|
-
static pk = E.primary(User, "id");
|
|
393
|
+
const UserV2 = E.defineModel("User", class {
|
|
403
394
|
id = E.field(E.identifier);
|
|
404
395
|
name = E.field(E.string);
|
|
405
396
|
role = E.field(E.string); // newly added field
|
|
@@ -407,7 +398,7 @@ class User extends E.Model<User> {
|
|
|
407
398
|
static migrate(record: Record<string, any>) {
|
|
408
399
|
record.role ??= record.name.indexOf("admin") >= 0 ? "admin" : "user"; // set role based on name for old records
|
|
409
400
|
}
|
|
410
|
-
}
|
|
401
|
+
})
|
|
411
402
|
```
|
|
412
403
|
|
|
413
404
|
Edinburgh will lazily (re)run the `migrate` function on an instance whenever its implementation (the literal function code) has changed. For robustness, make sure that your `migrate` function...
|
|
@@ -442,9 +433,7 @@ console.log(result.secondaries); // { User: 1500 }
|
|
|
442
433
|
Compute derived fields before data is written:
|
|
443
434
|
|
|
444
435
|
```typescript
|
|
445
|
-
|
|
446
|
-
class Article extends E.Model<Article> {
|
|
447
|
-
static pk = E.primary(Article, "id");
|
|
436
|
+
const Article = E.defineModel("Article", class {
|
|
448
437
|
id = E.field(E.identifier);
|
|
449
438
|
title = E.field(E.string);
|
|
450
439
|
slug = E.field(E.string);
|
|
@@ -452,7 +441,7 @@ class Article extends E.Model<Article> {
|
|
|
452
441
|
preCommit() {
|
|
453
442
|
this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
|
|
454
443
|
}
|
|
455
|
-
}
|
|
444
|
+
});
|
|
456
445
|
```
|
|
457
446
|
|
|
458
447
|
### Change Tracking
|
|
@@ -493,9 +482,12 @@ ln -s ../../node_modules/edinburgh/skill .claude/skills/edinburgh
|
|
|
493
482
|
|
|
494
483
|
The following is auto-generated from `src/edinburgh.ts`:
|
|
495
484
|
|
|
496
|
-
###
|
|
485
|
+
### currentTxn · function
|
|
497
486
|
|
|
498
|
-
|
|
487
|
+
Returns the current transaction from AsyncLocalStorage.
|
|
488
|
+
Throws if called outside a transact() callback.
|
|
489
|
+
|
|
490
|
+
**Signature:** `() => Transaction`
|
|
499
491
|
|
|
500
492
|
### [init](init.md) · function
|
|
501
493
|
|
|
@@ -516,72 +508,53 @@ The default value is 6. Setting it to 0 will disable retries and cause transacti
|
|
|
516
508
|
|
|
517
509
|
Set a callback function to be called after a model is saved and committed.
|
|
518
510
|
|
|
519
|
-
###
|
|
520
|
-
|
|
521
|
-
**Signature:** `() => Promise<void>`
|
|
511
|
+
### Model · class
|
|
522
512
|
|
|
523
|
-
|
|
513
|
+
**Type:** `typeof ModelBase`
|
|
524
514
|
|
|
525
|
-
|
|
515
|
+
### [ModelClass](ModelClass.md) · class
|
|
526
516
|
|
|
527
|
-
|
|
517
|
+
Runtime base constructor for model classes returned by `defineModel()`.
|
|
528
518
|
|
|
529
|
-
|
|
519
|
+
### [AnyModelClass](AnyModelClass.md) · type
|
|
530
520
|
|
|
531
|
-
|
|
521
|
+
A model constructor with its generic information erased.
|
|
532
522
|
|
|
533
|
-
|
|
523
|
+
### [ModelBase](ModelBase.md) · abstract class
|
|
534
524
|
|
|
535
|
-
|
|
525
|
+
Base class for all database models in the Edinburgh ORM.
|
|
536
526
|
|
|
537
|
-
|
|
527
|
+
### [Schema Evolution](Schema Evolution.md)
|
|
538
528
|
|
|
539
|
-
|
|
529
|
+
Edinburgh tracks the schema version of each model automatically. When you add, remove, or
|
|
530
|
+
change the types of fields, or add/remove indexes, Edinburgh detects the new schema version.
|
|
540
531
|
|
|
541
|
-
|
|
532
|
+
### [Lifecycle Hooks](Lifecycle Hooks.md)
|
|
542
533
|
|
|
543
|
-
|
|
534
|
+
- **`static migrate(record)`**: Called when deserializing rows written with an older schema
|
|
535
|
+
version. Receives a plain record object; mutate it in-place to match the current schema.
|
|
544
536
|
|
|
545
|
-
#### [
|
|
537
|
+
#### [ModelBase.migrate](Lifecycle Hooks_migrate.md) · static method
|
|
546
538
|
|
|
547
539
|
Optional migration function called when deserializing rows written with an older schema version.
|
|
548
|
-
Receives a plain record with all fields
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
#### Model.initFields · static method
|
|
552
|
-
|
|
553
|
-
Transform the model's `E.field` properties into the appropriate JavaScript properties. Normally this is done
|
|
554
|
-
automatically when using `transact()`, but in case you need to access `Model.fields` directly before the first
|
|
555
|
-
transaction, you can call this method manually.
|
|
556
|
-
|
|
557
|
-
**Signature:** `(reset?: boolean) => void`
|
|
558
|
-
|
|
559
|
-
**Parameters:**
|
|
560
|
-
|
|
561
|
-
- `reset?: boolean`
|
|
540
|
+
Receives a plain record with all fields and should mutate it in-place to match the current schema.
|
|
541
|
+
It runs during lazy loading and during `runMigration()`. Changing this method creates a new schema version.
|
|
542
|
+
If it updates values used by secondary or unique indexes, those index entries are refreshed only by `runMigration()`.
|
|
562
543
|
|
|
563
|
-
#### [
|
|
564
|
-
|
|
565
|
-
Find all instances of this model in the database, ordered by primary key.
|
|
566
|
-
|
|
567
|
-
#### [Model.replaceInto](Model_replaceInto.md) · static method
|
|
568
|
-
|
|
569
|
-
Load an existing instance by primary key and update it, or create a new one.
|
|
570
|
-
|
|
571
|
-
#### [model.preCommit](Model_preCommit.md) · method
|
|
544
|
+
#### [modelBase.preCommit](Lifecycle Hooks_preCommit.md) · method
|
|
572
545
|
|
|
573
546
|
Optional hook called on each modified instance right before the transaction commits.
|
|
574
547
|
Runs before data is written to disk, so changes made here are included in the commit.
|
|
575
548
|
|
|
576
|
-
####
|
|
549
|
+
#### modelBase.getPrimaryKey · method
|
|
577
550
|
|
|
578
551
|
**Signature:** `() => Uint8Array<ArrayBufferLike>`
|
|
579
552
|
|
|
580
553
|
**Returns:** The primary key for this instance.
|
|
581
554
|
|
|
582
|
-
#### [
|
|
555
|
+
#### [modelBase.getPrimaryKeyHash](Lifecycle Hooks_getPrimaryKeyHash.md) · method
|
|
583
556
|
|
|
584
|
-
####
|
|
557
|
+
#### modelBase.isLazyField · method
|
|
585
558
|
|
|
586
559
|
**Signature:** `(field: keyof this) => boolean`
|
|
587
560
|
|
|
@@ -589,38 +562,42 @@ Runs before data is written to disk, so changes made here are included in the co
|
|
|
589
562
|
|
|
590
563
|
- `field: keyof this`
|
|
591
564
|
|
|
592
|
-
#### [
|
|
565
|
+
#### [modelBase.preventPersist](Lifecycle Hooks_preventPersist.md) · method
|
|
593
566
|
|
|
594
567
|
Prevent this instance from being persisted to the database.
|
|
595
568
|
|
|
596
|
-
#### [
|
|
569
|
+
#### [modelBase.delete](Lifecycle Hooks_delete.md) · method
|
|
597
570
|
|
|
598
571
|
Delete this model instance from the database.
|
|
599
572
|
|
|
600
|
-
#### [
|
|
573
|
+
#### [modelBase.validate](Lifecycle Hooks_validate.md) · method
|
|
601
574
|
|
|
602
575
|
Validate all fields in this model instance.
|
|
603
576
|
|
|
604
|
-
#### [
|
|
577
|
+
#### [modelBase.isValid](Lifecycle Hooks_isValid.md) · method
|
|
605
578
|
|
|
606
579
|
Check if this model instance is valid.
|
|
607
580
|
|
|
608
|
-
####
|
|
581
|
+
#### modelBase.getState · method
|
|
609
582
|
|
|
610
583
|
**Signature:** `() => "created" | "deleted" | "loaded" | "lazy"`
|
|
611
584
|
|
|
612
|
-
####
|
|
585
|
+
#### modelBase.toString · method
|
|
613
586
|
|
|
614
587
|
**Signature:** `() => string`
|
|
615
588
|
|
|
616
|
-
####
|
|
589
|
+
#### modelBase.[Symbol.for('nodejs.util.inspect.custom')] · method
|
|
617
590
|
|
|
618
591
|
**Signature:** `() => string`
|
|
619
592
|
|
|
620
|
-
### [
|
|
593
|
+
### [defineModel](defineModel.md) · function
|
|
621
594
|
|
|
622
595
|
Register a model class with the Edinburgh ORM system.
|
|
623
596
|
|
|
597
|
+
### [deleteEverything](deleteEverything.md) · function
|
|
598
|
+
|
|
599
|
+
Delete every key/value entry in the database and reinitialize all registered models.
|
|
600
|
+
|
|
624
601
|
### [field](field.md) · function
|
|
625
602
|
|
|
626
603
|
Create a field definition for a model property.
|
|
@@ -699,79 +676,126 @@ Create a literal type wrapper for a constant value.
|
|
|
699
676
|
|
|
700
677
|
Create a link type wrapper for model relationships.
|
|
701
678
|
|
|
702
|
-
###
|
|
679
|
+
### dump · function
|
|
703
680
|
|
|
704
|
-
|
|
681
|
+
**Signature:** `() => void`
|
|
705
682
|
|
|
706
|
-
### [
|
|
683
|
+
### [FindOptions](FindOptions.md) · type
|
|
707
684
|
|
|
708
|
-
|
|
685
|
+
Range-query options accepted by `find()`, `findBy()`, `batchProcess()`, and `batchProcessBy()`.
|
|
709
686
|
|
|
710
|
-
###
|
|
687
|
+
### IndexRangeIterator · class
|
|
711
688
|
|
|
712
|
-
|
|
689
|
+
Iterator for range queries on indexes.
|
|
690
|
+
Handles common iteration logic for both primary and unique indexes.
|
|
691
|
+
Extends built-in Iterator to provide map/filter/reduce/toArray/etc.
|
|
713
692
|
|
|
714
|
-
|
|
693
|
+
**Type Parameters:**
|
|
715
694
|
|
|
716
|
-
|
|
695
|
+
- `ITEM`
|
|
717
696
|
|
|
718
|
-
|
|
697
|
+
#### indexRangeIterator.[Symbol.iterator] · method
|
|
719
698
|
|
|
720
|
-
|
|
699
|
+
**Signature:** `() => this`
|
|
721
700
|
|
|
722
|
-
####
|
|
701
|
+
#### indexRangeIterator.next · method
|
|
723
702
|
|
|
724
|
-
|
|
703
|
+
**Signature:** `() => IteratorResult<ITEM, any>`
|
|
725
704
|
|
|
726
|
-
|
|
705
|
+
#### indexRangeIterator.count · method
|
|
727
706
|
|
|
728
|
-
|
|
707
|
+
**Signature:** `() => number`
|
|
729
708
|
|
|
730
|
-
|
|
709
|
+
#### indexRangeIterator.fetch · method
|
|
731
710
|
|
|
732
|
-
|
|
711
|
+
**Signature:** `() => ITEM`
|
|
733
712
|
|
|
734
|
-
|
|
713
|
+
### Change · type
|
|
735
714
|
|
|
736
|
-
|
|
715
|
+
**Type:** `Record<any, any> | "created" | "deleted"`
|
|
737
716
|
|
|
738
|
-
|
|
717
|
+
### FieldConfig · interface
|
|
739
718
|
|
|
740
|
-
|
|
719
|
+
Configuration interface for model fields.
|
|
741
720
|
|
|
742
|
-
|
|
721
|
+
**Type Parameters:**
|
|
743
722
|
|
|
744
|
-
|
|
723
|
+
- `T` - The field type.
|
|
745
724
|
|
|
746
|
-
|
|
725
|
+
#### fieldConfig.type · member
|
|
747
726
|
|
|
748
|
-
|
|
727
|
+
The type wrapper that defines how this field is serialized/validated.
|
|
749
728
|
|
|
750
|
-
|
|
751
|
-
property access. In case it turns out the instance doesn't exist, an error will be thrown
|
|
752
|
-
at that time.
|
|
729
|
+
**Type:** `TypeWrapper<T>`
|
|
753
730
|
|
|
754
|
-
|
|
731
|
+
#### fieldConfig.description · member
|
|
755
732
|
|
|
756
|
-
|
|
733
|
+
Optional human-readable description of the field.
|
|
757
734
|
|
|
758
|
-
|
|
735
|
+
**Type:** `string`
|
|
759
736
|
|
|
760
|
-
|
|
737
|
+
#### fieldConfig.default · member
|
|
761
738
|
|
|
762
|
-
|
|
739
|
+
Optional default value or function that generates default values.
|
|
763
740
|
|
|
764
|
-
|
|
741
|
+
**Type:** `T | ((model: Record<string, any>) => T)`
|
|
765
742
|
|
|
766
|
-
|
|
743
|
+
### TypeWrapper · abstract class
|
|
767
744
|
|
|
768
|
-
|
|
745
|
+
**Type Parameters:**
|
|
746
|
+
|
|
747
|
+
- `T` - The TypeScript type this wrapper represents.
|
|
748
|
+
|
|
749
|
+
#### typeWrapper.kind · abstract property
|
|
750
|
+
|
|
751
|
+
A string identifier for this type, used during serialization
|
|
752
|
+
|
|
753
|
+
**Type:** `string`
|
|
754
|
+
|
|
755
|
+
#### [typeWrapper.serialize](TypeWrapper_serialize.md) · abstract method
|
|
756
|
+
|
|
757
|
+
Serialize a value from an object property to a Pack.
|
|
758
|
+
|
|
759
|
+
#### [typeWrapper.deserialize](TypeWrapper_deserialize.md) · abstract method
|
|
760
|
+
|
|
761
|
+
Deserialize a value from a Pack into an object property.
|
|
762
|
+
|
|
763
|
+
#### [typeWrapper.getError](TypeWrapper_getError.md) · abstract method
|
|
764
|
+
|
|
765
|
+
Validate a value.
|
|
766
|
+
|
|
767
|
+
#### [typeWrapper.serializeType](TypeWrapper_serializeType.md) · method
|
|
768
|
+
|
|
769
|
+
Serialize type metadata to a Pack (for schema serialization).
|
|
770
|
+
|
|
771
|
+
#### [typeWrapper.containsNull](TypeWrapper_containsNull.md) · method
|
|
772
|
+
|
|
773
|
+
Check if indexing should be skipped for this field value.
|
|
774
|
+
|
|
775
|
+
#### typeWrapper.toString · method
|
|
769
776
|
|
|
770
|
-
**
|
|
777
|
+
**Signature:** `() => string`
|
|
778
|
+
|
|
779
|
+
#### typeWrapper.clone · method
|
|
780
|
+
|
|
781
|
+
**Signature:** `(value: T) => T`
|
|
782
|
+
|
|
783
|
+
**Parameters:**
|
|
784
|
+
|
|
785
|
+
- `value: T`
|
|
786
|
+
|
|
787
|
+
#### typeWrapper.equals · method
|
|
788
|
+
|
|
789
|
+
**Signature:** `(value1: T, value2: T) => boolean`
|
|
790
|
+
|
|
791
|
+
**Parameters:**
|
|
792
|
+
|
|
793
|
+
- `value1: T`
|
|
794
|
+
- `value2: T`
|
|
771
795
|
|
|
772
|
-
####
|
|
796
|
+
#### typeWrapper.getLinkedModel · method
|
|
773
797
|
|
|
774
|
-
**
|
|
798
|
+
**Signature:** `() => AnyModelClass`
|
|
775
799
|
|
|
776
800
|
### [DatabaseError](DatabaseError.md) · constant
|
|
777
801
|
|
|
@@ -853,3 +877,17 @@ Number of orphaned index entries deleted.
|
|
|
853
877
|
|
|
854
878
|
**Type:** `number`
|
|
855
879
|
|
|
880
|
+
### Transaction · interface
|
|
881
|
+
|
|
882
|
+
#### transaction.id · member
|
|
883
|
+
|
|
884
|
+
**Type:** `number`
|
|
885
|
+
|
|
886
|
+
#### transaction.instances · member
|
|
887
|
+
|
|
888
|
+
**Type:** `Map<number, ModelBase>`
|
|
889
|
+
|
|
890
|
+
### txnStorage · constant
|
|
891
|
+
|
|
892
|
+
**Value:** `AsyncLocalStorage<Transaction>`
|
|
893
|
+
|