@tanstack/db 0.0.4 → 0.0.6
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/dist/cjs/collection.cjs +182 -113
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +43 -15
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/proxy.cjs +87 -248
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/proxy.d.cts +5 -5
- package/dist/cjs/query/compiled-query.cjs +23 -14
- package/dist/cjs/query/compiled-query.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.d.cts +3 -1
- package/dist/cjs/query/evaluators.cjs +35 -20
- package/dist/cjs/query/evaluators.cjs.map +1 -1
- package/dist/cjs/query/evaluators.d.cts +8 -3
- package/dist/cjs/query/extractors.cjs +20 -20
- package/dist/cjs/query/extractors.cjs.map +1 -1
- package/dist/cjs/query/extractors.d.cts +3 -3
- package/dist/cjs/query/group-by.cjs +12 -15
- package/dist/cjs/query/group-by.cjs.map +1 -1
- package/dist/cjs/query/group-by.d.cts +7 -7
- package/dist/cjs/query/joins.cjs +41 -55
- package/dist/cjs/query/joins.cjs.map +1 -1
- package/dist/cjs/query/joins.d.cts +3 -3
- package/dist/cjs/query/order-by.cjs +37 -84
- package/dist/cjs/query/order-by.cjs.map +1 -1
- package/dist/cjs/query/order-by.d.cts +2 -2
- package/dist/cjs/query/pipeline-compiler.cjs +13 -18
- package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
- package/dist/cjs/query/pipeline-compiler.d.cts +2 -1
- package/dist/cjs/query/query-builder.cjs +22 -29
- package/dist/cjs/query/query-builder.cjs.map +1 -1
- package/dist/cjs/query/query-builder.d.cts +16 -10
- package/dist/cjs/query/schema.d.cts +12 -11
- package/dist/cjs/query/select.cjs +47 -24
- package/dist/cjs/query/select.cjs.map +1 -1
- package/dist/cjs/query/select.d.cts +2 -2
- package/dist/cjs/query/types.d.cts +1 -0
- package/dist/cjs/transactions.cjs +20 -9
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +66 -7
- package/dist/esm/collection.d.ts +43 -15
- package/dist/esm/collection.js +183 -114
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/proxy.d.ts +5 -5
- package/dist/esm/proxy.js +87 -248
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/compiled-query.d.ts +3 -1
- package/dist/esm/query/compiled-query.js +23 -14
- package/dist/esm/query/compiled-query.js.map +1 -1
- package/dist/esm/query/evaluators.d.ts +8 -3
- package/dist/esm/query/evaluators.js +36 -21
- package/dist/esm/query/evaluators.js.map +1 -1
- package/dist/esm/query/extractors.d.ts +3 -3
- package/dist/esm/query/extractors.js +20 -20
- package/dist/esm/query/extractors.js.map +1 -1
- package/dist/esm/query/group-by.d.ts +7 -7
- package/dist/esm/query/group-by.js +14 -17
- package/dist/esm/query/group-by.js.map +1 -1
- package/dist/esm/query/joins.d.ts +3 -3
- package/dist/esm/query/joins.js +42 -56
- package/dist/esm/query/joins.js.map +1 -1
- package/dist/esm/query/order-by.d.ts +2 -2
- package/dist/esm/query/order-by.js +39 -86
- package/dist/esm/query/order-by.js.map +1 -1
- package/dist/esm/query/pipeline-compiler.d.ts +2 -1
- package/dist/esm/query/pipeline-compiler.js +14 -19
- package/dist/esm/query/pipeline-compiler.js.map +1 -1
- package/dist/esm/query/query-builder.d.ts +16 -10
- package/dist/esm/query/query-builder.js +22 -29
- package/dist/esm/query/query-builder.js.map +1 -1
- package/dist/esm/query/schema.d.ts +12 -11
- package/dist/esm/query/select.d.ts +2 -2
- package/dist/esm/query/select.js +48 -25
- package/dist/esm/query/select.js.map +1 -1
- package/dist/esm/query/types.d.ts +1 -0
- package/dist/esm/transactions.js +20 -9
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +66 -7
- package/package.json +2 -2
- package/src/collection.ts +286 -146
- package/src/proxy.ts +141 -358
- package/src/query/compiled-query.ts +30 -15
- package/src/query/evaluators.ts +49 -21
- package/src/query/extractors.ts +24 -21
- package/src/query/group-by.ts +24 -22
- package/src/query/joins.ts +88 -75
- package/src/query/order-by.ts +56 -106
- package/src/query/pipeline-compiler.ts +34 -37
- package/src/query/query-builder.ts +49 -46
- package/src/query/schema.ts +18 -15
- package/src/query/select.ts +68 -33
- package/src/query/types.ts +1 -0
- package/src/transactions.ts +30 -14
- package/src/types.ts +76 -7
- package/dist/cjs/query/key-by.cjs +0 -43
- package/dist/cjs/query/key-by.cjs.map +0 -1
- package/dist/cjs/query/key-by.d.cts +0 -3
- package/dist/esm/query/key-by.d.ts +0 -3
- package/dist/esm/query/key-by.js +0 -43
- package/dist/esm/query/key-by.js.map +0 -1
- package/src/query/key-by.ts +0 -61
package/src/collection.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Derived, Store, batch } from "@tanstack/store"
|
|
2
2
|
import { withArrayChangeTracking, withChangeTracking } from "./proxy"
|
|
3
|
-
import { getActiveTransaction } from "./transactions"
|
|
3
|
+
import { Transaction, getActiveTransaction } from "./transactions"
|
|
4
4
|
import { SortedMap } from "./SortedMap"
|
|
5
5
|
import type {
|
|
6
6
|
ChangeMessage,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
OptimisticChangeMessage,
|
|
11
11
|
PendingMutation,
|
|
12
12
|
StandardSchema,
|
|
13
|
-
Transaction,
|
|
13
|
+
Transaction as TransactionType,
|
|
14
14
|
} from "./types"
|
|
15
15
|
|
|
16
16
|
// Store collections in memory using Tanstack store
|
|
@@ -28,6 +28,19 @@ interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
|
28
28
|
operations: Array<OptimisticChangeMessage<T>>
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new Collection instance with the given configuration
|
|
33
|
+
*
|
|
34
|
+
* @template T - The type of items in the collection
|
|
35
|
+
* @param config - Configuration for the collection, including id and sync
|
|
36
|
+
* @returns A new Collection instance
|
|
37
|
+
*/
|
|
38
|
+
export function createCollection<T extends object = Record<string, unknown>>(
|
|
39
|
+
config: CollectionConfig<T>
|
|
40
|
+
): Collection<T> {
|
|
41
|
+
return new Collection<T>(config)
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/**
|
|
32
45
|
* Preloads a collection with the given configuration
|
|
33
46
|
* Returns a promise that resolves once the sync tool has done its first commit (initial sync is finished)
|
|
@@ -57,6 +70,10 @@ interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
|
57
70
|
export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
58
71
|
config: CollectionConfig<T>
|
|
59
72
|
): Promise<Collection<T>> {
|
|
73
|
+
if (!config.id) {
|
|
74
|
+
throw new Error(`The id property is required for preloadCollection`)
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
// If the collection is already fully loaded, return a resolved promise
|
|
61
78
|
if (
|
|
62
79
|
collectionsStore.state.has(config.id) &&
|
|
@@ -76,10 +93,14 @@ export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
|
76
93
|
if (!collectionsStore.state.has(config.id)) {
|
|
77
94
|
collectionsStore.setState((prev) => {
|
|
78
95
|
const next = new Map(prev)
|
|
96
|
+
if (!config.id) {
|
|
97
|
+
throw new Error(`The id property is required for preloadCollection`)
|
|
98
|
+
}
|
|
79
99
|
next.set(
|
|
80
100
|
config.id,
|
|
81
101
|
new Collection<T>({
|
|
82
102
|
id: config.id,
|
|
103
|
+
getId: config.getId,
|
|
83
104
|
sync: config.sync,
|
|
84
105
|
schema: config.schema,
|
|
85
106
|
})
|
|
@@ -100,6 +121,9 @@ export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
|
100
121
|
|
|
101
122
|
// Register a one-time listener for the first commit
|
|
102
123
|
collection.onFirstCommit(() => {
|
|
124
|
+
if (!config.id) {
|
|
125
|
+
throw new Error(`The id property is required for preloadCollection`)
|
|
126
|
+
}
|
|
103
127
|
if (loadingCollections.has(config.id)) {
|
|
104
128
|
loadingCollections.delete(config.id)
|
|
105
129
|
resolveFirstCommit()
|
|
@@ -145,7 +169,7 @@ export class SchemaValidationError extends Error {
|
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
export class Collection<T extends object = Record<string, unknown>> {
|
|
148
|
-
public transactions: Store<SortedMap<string,
|
|
172
|
+
public transactions: Store<SortedMap<string, TransactionType>>
|
|
149
173
|
public optimisticOperations: Derived<Array<OptimisticChangeMessage<T>>>
|
|
150
174
|
public derivedState: Derived<Map<string, T>>
|
|
151
175
|
public derivedArray: Derived<Array<T>>
|
|
@@ -157,9 +181,6 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
157
181
|
public config: CollectionConfig<T>
|
|
158
182
|
private hasReceivedFirstCommit = false
|
|
159
183
|
|
|
160
|
-
// WeakMap to associate objects with their keys
|
|
161
|
-
public objectKeyMap = new WeakMap<object, string>()
|
|
162
|
-
|
|
163
184
|
// Array to store one-time commit listeners
|
|
164
185
|
private onFirstCommitCallbacks: Array<() => void> = []
|
|
165
186
|
|
|
@@ -172,7 +193,7 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
172
193
|
this.onFirstCommitCallbacks.push(callback)
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
public id =
|
|
196
|
+
public id = ``
|
|
176
197
|
|
|
177
198
|
/**
|
|
178
199
|
* Creates a new Collection instance
|
|
@@ -180,13 +201,24 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
180
201
|
* @param config - Configuration object for the collection
|
|
181
202
|
* @throws Error if sync config is missing
|
|
182
203
|
*/
|
|
183
|
-
constructor(config
|
|
184
|
-
|
|
204
|
+
constructor(config: CollectionConfig<T>) {
|
|
205
|
+
// eslint-disable-next-line
|
|
206
|
+
if (!config) {
|
|
207
|
+
throw new Error(`Collection requires a config`)
|
|
208
|
+
}
|
|
209
|
+
if (config.id) {
|
|
210
|
+
this.id = config.id
|
|
211
|
+
} else {
|
|
212
|
+
this.id = crypto.randomUUID()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// eslint-disable-next-line
|
|
216
|
+
if (!config.sync) {
|
|
185
217
|
throw new Error(`Collection requires a sync config`)
|
|
186
218
|
}
|
|
187
219
|
|
|
188
220
|
this.transactions = new Store(
|
|
189
|
-
new SortedMap<string,
|
|
221
|
+
new SortedMap<string, TransactionType>(
|
|
190
222
|
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
191
223
|
)
|
|
192
224
|
)
|
|
@@ -232,11 +264,9 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
232
264
|
this.derivedState = new Derived({
|
|
233
265
|
fn: ({ currDepVals: [syncedData, operations] }) => {
|
|
234
266
|
const combined = new Map<string, T>(syncedData)
|
|
235
|
-
const optimisticKeys = new Set<string>()
|
|
236
267
|
|
|
237
268
|
// Apply the optimistic operations on top of the synced state.
|
|
238
269
|
for (const operation of operations) {
|
|
239
|
-
optimisticKeys.add(operation.key)
|
|
240
270
|
if (operation.isActive) {
|
|
241
271
|
switch (operation.type) {
|
|
242
272
|
case `insert`:
|
|
@@ -252,13 +282,6 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
252
282
|
}
|
|
253
283
|
}
|
|
254
284
|
|
|
255
|
-
// Update object => key mappings
|
|
256
|
-
optimisticKeys.forEach((key) => {
|
|
257
|
-
if (combined.has(key)) {
|
|
258
|
-
this.objectKeyMap.set(combined.get(key)!, key)
|
|
259
|
-
}
|
|
260
|
-
})
|
|
261
|
-
|
|
262
285
|
return combined
|
|
263
286
|
},
|
|
264
287
|
deps: [this.syncedData, this.optimisticOperations],
|
|
@@ -353,7 +376,7 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
353
376
|
operations: [],
|
|
354
377
|
})
|
|
355
378
|
},
|
|
356
|
-
write: (
|
|
379
|
+
write: (messageWithoutKey: Omit<ChangeMessage<T>, `key`>) => {
|
|
357
380
|
const pendingTransaction =
|
|
358
381
|
this.pendingSyncedTransactions[
|
|
359
382
|
this.pendingSyncedTransactions.length - 1
|
|
@@ -366,6 +389,30 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
366
389
|
`The pending sync transaction is already committed, you can't still write to it.`
|
|
367
390
|
)
|
|
368
391
|
}
|
|
392
|
+
const key = this.generateObjectKey(
|
|
393
|
+
this.config.getId(messageWithoutKey.value),
|
|
394
|
+
messageWithoutKey.value
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
// Check if an item with this ID already exists when inserting
|
|
398
|
+
if (messageWithoutKey.type === `insert`) {
|
|
399
|
+
if (
|
|
400
|
+
this.syncedData.state.has(key) &&
|
|
401
|
+
!pendingTransaction.operations.some(
|
|
402
|
+
(op) => op.key === key && op.type === `delete`
|
|
403
|
+
)
|
|
404
|
+
) {
|
|
405
|
+
const id = this.config.getId(messageWithoutKey.value)
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Cannot insert document with ID "${id}" from sync because it already exists in the collection "${this.id}"`
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const message: ChangeMessage<T> = {
|
|
413
|
+
...messageWithoutKey,
|
|
414
|
+
key,
|
|
415
|
+
}
|
|
369
416
|
pendingTransaction.operations.push(message)
|
|
370
417
|
},
|
|
371
418
|
commit: () => {
|
|
@@ -399,11 +446,9 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
399
446
|
({ state }) => state === `persisting`
|
|
400
447
|
)
|
|
401
448
|
) {
|
|
402
|
-
const keys = new Set<string>()
|
|
403
449
|
batch(() => {
|
|
404
450
|
for (const transaction of this.pendingSyncedTransactions) {
|
|
405
451
|
for (const operation of transaction.operations) {
|
|
406
|
-
keys.add(operation.key)
|
|
407
452
|
this.syncedKeys.add(operation.key)
|
|
408
453
|
this.syncedMetadata.setState((prevData) => {
|
|
409
454
|
switch (operation.type) {
|
|
@@ -443,13 +488,6 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
443
488
|
}
|
|
444
489
|
})
|
|
445
490
|
|
|
446
|
-
keys.forEach((key) => {
|
|
447
|
-
const curValue = this.state.get(key)
|
|
448
|
-
if (curValue) {
|
|
449
|
-
this.objectKeyMap.set(curValue, key)
|
|
450
|
-
}
|
|
451
|
-
})
|
|
452
|
-
|
|
453
491
|
this.pendingSyncedTransactions = []
|
|
454
492
|
|
|
455
493
|
// Call any registered one-time commit listeners
|
|
@@ -473,6 +511,29 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
473
511
|
)
|
|
474
512
|
}
|
|
475
513
|
|
|
514
|
+
private getKeyFromId(id: unknown): string {
|
|
515
|
+
if (typeof id === `undefined`) {
|
|
516
|
+
throw new Error(`id is undefined`)
|
|
517
|
+
}
|
|
518
|
+
if (typeof id === `string` && id.startsWith(`KEY::`)) {
|
|
519
|
+
return id
|
|
520
|
+
} else {
|
|
521
|
+
// if it's not a string, then it's some other
|
|
522
|
+
// primitive type and needs turned into a key.
|
|
523
|
+
return this.generateObjectKey(id, null)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
public generateObjectKey(id: any, item: any): string {
|
|
528
|
+
if (typeof id === `undefined`) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`An object was created without a defined id: ${JSON.stringify(item)}`
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return `KEY::${this.id}/${id}`
|
|
535
|
+
}
|
|
536
|
+
|
|
476
537
|
private validateData(
|
|
477
538
|
data: unknown,
|
|
478
539
|
type: `insert` | `update`,
|
|
@@ -539,22 +600,11 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
539
600
|
return result.value as T
|
|
540
601
|
}
|
|
541
602
|
|
|
542
|
-
private generateKey(data: unknown): string {
|
|
543
|
-
const str = JSON.stringify(data)
|
|
544
|
-
let h = 0
|
|
545
|
-
|
|
546
|
-
for (let i = 0; i < str.length; i++) {
|
|
547
|
-
h = (Math.imul(31, h) + str.charCodeAt(i)) | 0
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return `${this.id}/${Math.abs(h).toString(36)}`
|
|
551
|
-
}
|
|
552
|
-
|
|
553
603
|
/**
|
|
554
604
|
* Inserts one or more items into the collection
|
|
555
605
|
* @param items - Single item or array of items to insert
|
|
556
606
|
* @param config - Optional configuration including metadata and custom keys
|
|
557
|
-
* @returns A
|
|
607
|
+
* @returns A TransactionType object representing the insert operation(s)
|
|
558
608
|
* @throws {SchemaValidationError} If the data fails schema validation
|
|
559
609
|
* @example
|
|
560
610
|
* // Insert a single item
|
|
@@ -570,27 +620,22 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
570
620
|
* insert({ text: "Buy groceries" }, { key: "grocery-task" })
|
|
571
621
|
*/
|
|
572
622
|
insert = (data: T | Array<T>, config?: InsertConfig) => {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
623
|
+
const ambientTransaction = getActiveTransaction()
|
|
624
|
+
|
|
625
|
+
// If no ambient transaction exists, check for an onInsert handler early
|
|
626
|
+
if (!ambientTransaction && !this.config.onInsert) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`
|
|
629
|
+
)
|
|
576
630
|
}
|
|
577
631
|
|
|
578
632
|
const items = Array.isArray(data) ? data : [data]
|
|
579
633
|
const mutations: Array<PendingMutation<T>> = []
|
|
580
634
|
|
|
581
635
|
// Handle keys - convert to array if string, or generate if not provided
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
// If keys are provided, ensure we have the right number or allow sparse array
|
|
586
|
-
if (Array.isArray(config.key) && configKeys.length > items.length) {
|
|
587
|
-
throw new Error(`More keys provided than items to insert`)
|
|
588
|
-
}
|
|
589
|
-
keys = items.map((_, i) => configKeys[i] ?? this.generateKey(items[i]))
|
|
590
|
-
} else {
|
|
591
|
-
// No keys provided, generate for all items
|
|
592
|
-
keys = items.map((item) => this.generateKey(item))
|
|
593
|
-
}
|
|
636
|
+
const keys: Array<unknown> = items.map((item) =>
|
|
637
|
+
this.generateObjectKey(this.config.getId(item), item)
|
|
638
|
+
)
|
|
594
639
|
|
|
595
640
|
// Create mutations for each item
|
|
596
641
|
items.forEach((item, index) => {
|
|
@@ -598,6 +643,12 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
598
643
|
const validatedData = this.validateData(item, `insert`)
|
|
599
644
|
const key = keys[index]!
|
|
600
645
|
|
|
646
|
+
// Check if an item with this ID already exists in the collection
|
|
647
|
+
const id = this.config.getId(item)
|
|
648
|
+
if (this.state.has(this.getKeyFromId(id))) {
|
|
649
|
+
throw `Cannot insert document with ID "${id}" because it already exists in the collection`
|
|
650
|
+
}
|
|
651
|
+
|
|
601
652
|
const mutation: PendingMutation<T> = {
|
|
602
653
|
mutationId: crypto.randomUUID(),
|
|
603
654
|
original: {},
|
|
@@ -615,14 +666,37 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
615
666
|
mutations.push(mutation)
|
|
616
667
|
})
|
|
617
668
|
|
|
618
|
-
transaction
|
|
669
|
+
// If an ambient transaction exists, use it
|
|
670
|
+
if (ambientTransaction) {
|
|
671
|
+
ambientTransaction.applyMutations(mutations)
|
|
619
672
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
673
|
+
this.transactions.setState((sortedMap) => {
|
|
674
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
675
|
+
return sortedMap
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
return ambientTransaction
|
|
679
|
+
} else {
|
|
680
|
+
// Create a new transaction with a mutation function that calls the onInsert handler
|
|
681
|
+
const directOpTransaction = new Transaction({
|
|
682
|
+
mutationFn: async (params) => {
|
|
683
|
+
// Call the onInsert handler with the transaction
|
|
684
|
+
return this.config.onInsert!(params)
|
|
685
|
+
},
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
// Apply mutations to the new transaction
|
|
689
|
+
directOpTransaction.applyMutations(mutations)
|
|
690
|
+
directOpTransaction.commit()
|
|
624
691
|
|
|
625
|
-
|
|
692
|
+
// Add the transaction to the collection's transactions store
|
|
693
|
+
this.transactions.setState((sortedMap) => {
|
|
694
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
695
|
+
return sortedMap
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
return directOpTransaction
|
|
699
|
+
}
|
|
626
700
|
}
|
|
627
701
|
|
|
628
702
|
/**
|
|
@@ -645,54 +719,75 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
645
719
|
* update(todo, { metadata: { reason: "user update" } }, (draft) => { draft.text = "Updated text" })
|
|
646
720
|
*/
|
|
647
721
|
|
|
722
|
+
/**
|
|
723
|
+
* Updates one or more items in the collection using a callback function
|
|
724
|
+
* @param ids - Single ID or array of IDs to update
|
|
725
|
+
* @param configOrCallback - Either update configuration or update callback
|
|
726
|
+
* @param maybeCallback - Update callback if config was provided
|
|
727
|
+
* @returns A Transaction object representing the update operation(s)
|
|
728
|
+
* @throws {SchemaValidationError} If the updated data fails schema validation
|
|
729
|
+
* @example
|
|
730
|
+
* // Update a single item
|
|
731
|
+
* update("todo-1", (draft) => { draft.completed = true })
|
|
732
|
+
*
|
|
733
|
+
* // Update multiple items
|
|
734
|
+
* update(["todo-1", "todo-2"], (drafts) => {
|
|
735
|
+
* drafts.forEach(draft => { draft.completed = true })
|
|
736
|
+
* })
|
|
737
|
+
*
|
|
738
|
+
* // Update with metadata
|
|
739
|
+
* update("todo-1", { metadata: { reason: "user update" } }, (draft) => { draft.text = "Updated text" })
|
|
740
|
+
*/
|
|
648
741
|
update<TItem extends object = T>(
|
|
649
|
-
|
|
742
|
+
id: unknown,
|
|
650
743
|
configOrCallback: ((draft: TItem) => void) | OperationConfig,
|
|
651
744
|
maybeCallback?: (draft: TItem) => void
|
|
652
|
-
):
|
|
745
|
+
): TransactionType
|
|
653
746
|
|
|
654
747
|
update<TItem extends object = T>(
|
|
655
|
-
|
|
748
|
+
ids: Array<unknown>,
|
|
656
749
|
configOrCallback: ((draft: Array<TItem>) => void) | OperationConfig,
|
|
657
750
|
maybeCallback?: (draft: Array<TItem>) => void
|
|
658
|
-
):
|
|
751
|
+
): TransactionType
|
|
659
752
|
|
|
660
753
|
update<TItem extends object = T>(
|
|
661
|
-
|
|
754
|
+
ids: unknown | Array<unknown>,
|
|
662
755
|
configOrCallback: ((draft: TItem | Array<TItem>) => void) | OperationConfig,
|
|
663
756
|
maybeCallback?: (draft: TItem | Array<TItem>) => void
|
|
664
757
|
) {
|
|
665
|
-
if (typeof
|
|
758
|
+
if (typeof ids === `undefined`) {
|
|
666
759
|
throw new Error(`The first argument to update is missing`)
|
|
667
760
|
}
|
|
668
761
|
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
762
|
+
const ambientTransaction = getActiveTransaction()
|
|
763
|
+
|
|
764
|
+
// If no ambient transaction exists, check for an onUpdate handler early
|
|
765
|
+
if (!ambientTransaction && !this.config.onUpdate) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
|
|
768
|
+
)
|
|
672
769
|
}
|
|
673
770
|
|
|
674
|
-
const isArray = Array.isArray(
|
|
675
|
-
const
|
|
771
|
+
const isArray = Array.isArray(ids)
|
|
772
|
+
const idsArray = (Array.isArray(ids) ? ids : [ids]).map((id) =>
|
|
773
|
+
this.getKeyFromId(id)
|
|
774
|
+
)
|
|
676
775
|
const callback =
|
|
677
776
|
typeof configOrCallback === `function` ? configOrCallback : maybeCallback!
|
|
678
777
|
const config =
|
|
679
778
|
typeof configOrCallback === `function` ? {} : configOrCallback
|
|
680
779
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
780
|
+
// Get the current objects or empty objects if they don't exist
|
|
781
|
+
const currentObjects = idsArray.map((id) => {
|
|
782
|
+
const item = this.state.get(id)
|
|
783
|
+
if (!item) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`The id "${id}" was passed to update but an object for this ID was not found in the collection`
|
|
786
|
+
)
|
|
688
787
|
}
|
|
689
|
-
throw new Error(`Invalid item type for update - must be an object`)
|
|
690
|
-
})
|
|
691
788
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
...(this.state.get(key) || {}),
|
|
695
|
-
})) as Array<TItem>
|
|
789
|
+
return item
|
|
790
|
+
}) as unknown as Array<TItem>
|
|
696
791
|
|
|
697
792
|
let changesArray
|
|
698
793
|
if (isArray) {
|
|
@@ -710,29 +805,44 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
710
805
|
}
|
|
711
806
|
|
|
712
807
|
// Create mutations for each object that has changes
|
|
713
|
-
const mutations: Array<PendingMutation<T>> =
|
|
714
|
-
.map((
|
|
715
|
-
const
|
|
808
|
+
const mutations: Array<PendingMutation<T>> = idsArray
|
|
809
|
+
.map((id, index) => {
|
|
810
|
+
const itemChanges = changesArray[index] // User-provided changes for this specific item
|
|
716
811
|
|
|
717
812
|
// Skip items with no changes
|
|
718
|
-
if (!
|
|
813
|
+
if (!itemChanges || Object.keys(itemChanges).length === 0) {
|
|
719
814
|
return null
|
|
720
815
|
}
|
|
721
816
|
|
|
722
|
-
|
|
723
|
-
|
|
817
|
+
const originalItem = currentObjects[index] as unknown as T
|
|
818
|
+
// Validate the user-provided changes for this item
|
|
819
|
+
const validatedUpdatePayload = this.validateData(
|
|
820
|
+
itemChanges,
|
|
821
|
+
`update`,
|
|
822
|
+
id
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
// Construct the full modified item by applying the validated update payload to the original item
|
|
826
|
+
const modifiedItem = { ...originalItem, ...validatedUpdatePayload }
|
|
827
|
+
|
|
828
|
+
// Check if the ID of the item is being changed
|
|
829
|
+
const originalItemId = this.config.getId(originalItem)
|
|
830
|
+
const modifiedItemId = this.config.getId(modifiedItem)
|
|
831
|
+
|
|
832
|
+
if (originalItemId !== modifiedItemId) {
|
|
833
|
+
throw new Error(
|
|
834
|
+
`Updating the ID of an item is not allowed. Original ID: "${originalItemId}", Attempted new ID: "${modifiedItemId}". Please delete the old item and create a new one if an ID change is necessary.`
|
|
835
|
+
)
|
|
836
|
+
}
|
|
724
837
|
|
|
725
838
|
return {
|
|
726
839
|
mutationId: crypto.randomUUID(),
|
|
727
|
-
original:
|
|
728
|
-
modified:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
} as Record<string, unknown>,
|
|
732
|
-
changes: validatedData as Record<string, unknown>,
|
|
733
|
-
key,
|
|
840
|
+
original: originalItem as Record<string, unknown>,
|
|
841
|
+
modified: modifiedItem as Record<string, unknown>,
|
|
842
|
+
changes: validatedUpdatePayload as Record<string, unknown>,
|
|
843
|
+
key: id,
|
|
734
844
|
metadata: config.metadata as unknown,
|
|
735
|
-
syncMetadata: (this.syncedMetadata.state.get(
|
|
845
|
+
syncMetadata: (this.syncedMetadata.state.get(id) || {}) as Record<
|
|
736
846
|
string,
|
|
737
847
|
unknown
|
|
738
848
|
>,
|
|
@@ -749,69 +859,83 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
749
859
|
throw new Error(`No changes were made to any of the objects`)
|
|
750
860
|
}
|
|
751
861
|
|
|
752
|
-
transaction
|
|
862
|
+
// If an ambient transaction exists, use it
|
|
863
|
+
if (ambientTransaction) {
|
|
864
|
+
ambientTransaction.applyMutations(mutations)
|
|
865
|
+
|
|
866
|
+
this.transactions.setState((sortedMap) => {
|
|
867
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
868
|
+
return sortedMap
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
return ambientTransaction
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// No need to check for onUpdate handler here as we've already checked at the beginning
|
|
875
|
+
|
|
876
|
+
// Create a new transaction with a mutation function that calls the onUpdate handler
|
|
877
|
+
const directOpTransaction = new Transaction({
|
|
878
|
+
mutationFn: async (transaction) => {
|
|
879
|
+
// Call the onUpdate handler with the transaction
|
|
880
|
+
return this.config.onUpdate!(transaction)
|
|
881
|
+
},
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
// Apply mutations to the new transaction
|
|
885
|
+
directOpTransaction.applyMutations(mutations)
|
|
886
|
+
directOpTransaction.commit()
|
|
753
887
|
|
|
888
|
+
// Add the transaction to the collection's transactions store
|
|
754
889
|
this.transactions.setState((sortedMap) => {
|
|
755
|
-
sortedMap.set(
|
|
890
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
756
891
|
return sortedMap
|
|
757
892
|
})
|
|
758
893
|
|
|
759
|
-
return
|
|
894
|
+
return directOpTransaction
|
|
760
895
|
}
|
|
761
896
|
|
|
762
897
|
/**
|
|
763
898
|
* Deletes one or more items from the collection
|
|
764
|
-
* @param
|
|
899
|
+
* @param ids - Single ID or array of IDs to delete
|
|
765
900
|
* @param config - Optional configuration including metadata
|
|
766
|
-
* @returns A
|
|
901
|
+
* @returns A TransactionType object representing the delete operation(s)
|
|
767
902
|
* @example
|
|
768
903
|
* // Delete a single item
|
|
769
|
-
* delete(todo)
|
|
904
|
+
* delete("todo-1")
|
|
770
905
|
*
|
|
771
906
|
* // Delete multiple items
|
|
772
|
-
* delete([
|
|
907
|
+
* delete(["todo-1", "todo-2"])
|
|
773
908
|
*
|
|
774
909
|
* // Delete with metadata
|
|
775
|
-
* delete(todo, { metadata: { reason: "completed" } })
|
|
910
|
+
* delete("todo-1", { metadata: { reason: "completed" } })
|
|
776
911
|
*/
|
|
777
912
|
delete = (
|
|
778
|
-
|
|
913
|
+
ids: Array<string> | string,
|
|
779
914
|
config?: OperationConfig
|
|
780
|
-
) => {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
915
|
+
): TransactionType => {
|
|
916
|
+
const ambientTransaction = getActiveTransaction()
|
|
917
|
+
|
|
918
|
+
// If no ambient transaction exists, check for an onDelete handler early
|
|
919
|
+
if (!ambientTransaction && !this.config.onDelete) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
|
|
922
|
+
)
|
|
784
923
|
}
|
|
785
924
|
|
|
786
|
-
const
|
|
925
|
+
const idsArray = (Array.isArray(ids) ? ids : [ids]).map((id) =>
|
|
926
|
+
this.getKeyFromId(id)
|
|
927
|
+
)
|
|
787
928
|
const mutations: Array<PendingMutation<T>> = []
|
|
788
929
|
|
|
789
|
-
for (const
|
|
790
|
-
let key: string
|
|
791
|
-
if (typeof item === `object` && (item as unknown) !== null) {
|
|
792
|
-
const objectKey = this.objectKeyMap.get(item)
|
|
793
|
-
if (objectKey === undefined) {
|
|
794
|
-
throw new Error(
|
|
795
|
-
`Object not found in collection: ${JSON.stringify(item)}`
|
|
796
|
-
)
|
|
797
|
-
}
|
|
798
|
-
key = objectKey
|
|
799
|
-
} else if (typeof item === `string`) {
|
|
800
|
-
key = item
|
|
801
|
-
} else {
|
|
802
|
-
throw new Error(
|
|
803
|
-
`Invalid item type for delete - must be an object or string key`
|
|
804
|
-
)
|
|
805
|
-
}
|
|
806
|
-
|
|
930
|
+
for (const id of idsArray) {
|
|
807
931
|
const mutation: PendingMutation<T> = {
|
|
808
932
|
mutationId: crypto.randomUUID(),
|
|
809
|
-
original: (this.state.get(
|
|
810
|
-
modified: {
|
|
811
|
-
changes: {
|
|
812
|
-
key,
|
|
933
|
+
original: (this.state.get(id) || {}) as Record<string, unknown>,
|
|
934
|
+
modified: (this.state.get(id) || {}) as Record<string, unknown>,
|
|
935
|
+
changes: (this.state.get(id) || {}) as Record<string, unknown>,
|
|
936
|
+
key: id,
|
|
813
937
|
metadata: config?.metadata as unknown,
|
|
814
|
-
syncMetadata: (this.syncedMetadata.state.get(
|
|
938
|
+
syncMetadata: (this.syncedMetadata.state.get(id) || {}) as Record<
|
|
815
939
|
string,
|
|
816
940
|
unknown
|
|
817
941
|
>,
|
|
@@ -824,22 +948,38 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
824
948
|
mutations.push(mutation)
|
|
825
949
|
}
|
|
826
950
|
|
|
827
|
-
//
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
951
|
+
// If an ambient transaction exists, use it
|
|
952
|
+
if (ambientTransaction) {
|
|
953
|
+
ambientTransaction.applyMutations(mutations)
|
|
954
|
+
|
|
955
|
+
this.transactions.setState((sortedMap) => {
|
|
956
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
957
|
+
return sortedMap
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
return ambientTransaction
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Create a new transaction with a mutation function that calls the onDelete handler
|
|
964
|
+
const directOpTransaction = new Transaction({
|
|
965
|
+
autoCommit: true,
|
|
966
|
+
mutationFn: async (transaction) => {
|
|
967
|
+
// Call the onDelete handler with the transaction
|
|
968
|
+
return this.config.onDelete!(transaction)
|
|
969
|
+
},
|
|
833
970
|
})
|
|
834
971
|
|
|
835
|
-
transaction
|
|
972
|
+
// Apply mutations to the new transaction
|
|
973
|
+
directOpTransaction.applyMutations(mutations)
|
|
974
|
+
directOpTransaction.commit()
|
|
836
975
|
|
|
976
|
+
// Add the transaction to the collection's transactions store
|
|
837
977
|
this.transactions.setState((sortedMap) => {
|
|
838
|
-
sortedMap.set(
|
|
978
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
839
979
|
return sortedMap
|
|
840
980
|
})
|
|
841
981
|
|
|
842
|
-
return
|
|
982
|
+
return directOpTransaction
|
|
843
983
|
}
|
|
844
984
|
|
|
845
985
|
/**
|