@tanstack/query-db-collection 0.1.0 → 0.1.2

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.
@@ -0,0 +1,236 @@
1
+ import {
2
+ DeleteOperationItemNotFoundError,
3
+ DuplicateKeyInBatchError,
4
+ SyncNotInitializedError,
5
+ UpdateOperationItemNotFoundError,
6
+ } from "./errors"
7
+ import type { QueryClient } from "@tanstack/query-core"
8
+ import type { ChangeMessage, Collection } from "@tanstack/db"
9
+
10
+ // Types for sync operations
11
+ export type SyncOperation<
12
+ TRow extends object,
13
+ TKey extends string | number = string | number,
14
+ TInsertInput extends object = TRow,
15
+ > =
16
+ | { type: `insert`; data: TInsertInput | Array<TInsertInput> }
17
+ | { type: `update`; data: Partial<TRow> | Array<Partial<TRow>> }
18
+ | { type: `delete`; key: TKey | Array<TKey> }
19
+ | { type: `upsert`; data: Partial<TRow> | Array<Partial<TRow>> }
20
+
21
+ export interface SyncContext<
22
+ TRow extends object,
23
+ TKey extends string | number = string | number,
24
+ > {
25
+ collection: Collection<TRow>
26
+ queryClient: QueryClient
27
+ queryKey: Array<unknown>
28
+ getKey: (item: TRow) => TKey
29
+ begin: () => void
30
+ write: (message: Omit<ChangeMessage<TRow>, `key`>) => void
31
+ commit: () => void
32
+ }
33
+
34
+ interface NormalizedOperation<
35
+ TRow extends object,
36
+ TKey extends string | number = string | number,
37
+ > {
38
+ type: `insert` | `update` | `delete` | `upsert`
39
+ key: TKey
40
+ data?: TRow | Partial<TRow>
41
+ }
42
+
43
+ // Normalize operations into a consistent format
44
+ function normalizeOperations<
45
+ TRow extends object,
46
+ TKey extends string | number = string | number,
47
+ TInsertInput extends object = TRow,
48
+ >(
49
+ ops:
50
+ | SyncOperation<TRow, TKey, TInsertInput>
51
+ | Array<SyncOperation<TRow, TKey, TInsertInput>>,
52
+ ctx: SyncContext<TRow, TKey>
53
+ ): Array<NormalizedOperation<TRow, TKey>> {
54
+ const operations = Array.isArray(ops) ? ops : [ops]
55
+ const normalized: Array<NormalizedOperation<TRow, TKey>> = []
56
+
57
+ for (const op of operations) {
58
+ if (op.type === `delete`) {
59
+ const keys = Array.isArray(op.key) ? op.key : [op.key]
60
+ for (const key of keys) {
61
+ normalized.push({ type: `delete`, key })
62
+ }
63
+ } else {
64
+ const items = Array.isArray(op.data) ? op.data : [op.data]
65
+ for (const item of items) {
66
+ let key: TKey
67
+ if (op.type === `update`) {
68
+ // For updates, we need to get the key from the partial data
69
+ key = ctx.getKey(item as TRow)
70
+ } else {
71
+ // For insert/upsert, validate and resolve the full item first
72
+ const resolved = ctx.collection.validateData(
73
+ item,
74
+ op.type === `upsert` ? `insert` : op.type
75
+ )
76
+ key = ctx.getKey(resolved)
77
+ }
78
+ normalized.push({ type: op.type, key, data: item })
79
+ }
80
+ }
81
+ }
82
+
83
+ return normalized
84
+ }
85
+
86
+ // Validate operations before executing
87
+ function validateOperations<
88
+ TRow extends object,
89
+ TKey extends string | number = string | number,
90
+ >(
91
+ operations: Array<NormalizedOperation<TRow, TKey>>,
92
+ ctx: SyncContext<TRow, TKey>
93
+ ): void {
94
+ const seenKeys = new Set<TKey>()
95
+
96
+ for (const op of operations) {
97
+ // Check for duplicate keys within the batch
98
+ if (seenKeys.has(op.key)) {
99
+ throw new DuplicateKeyInBatchError(op.key)
100
+ }
101
+ seenKeys.add(op.key)
102
+
103
+ // Validate operation-specific requirements
104
+ if (op.type === `update`) {
105
+ if (!ctx.collection.has(op.key)) {
106
+ throw new UpdateOperationItemNotFoundError(op.key)
107
+ }
108
+ } else if (op.type === `delete`) {
109
+ if (!ctx.collection.has(op.key)) {
110
+ throw new DeleteOperationItemNotFoundError(op.key)
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // Execute a batch of operations
117
+ export function performWriteOperations<
118
+ TRow extends object,
119
+ TKey extends string | number = string | number,
120
+ TInsertInput extends object = TRow,
121
+ >(
122
+ operations:
123
+ | SyncOperation<TRow, TKey, TInsertInput>
124
+ | Array<SyncOperation<TRow, TKey, TInsertInput>>,
125
+ ctx: SyncContext<TRow, TKey>
126
+ ): void {
127
+ const normalized = normalizeOperations(operations, ctx)
128
+ validateOperations(normalized, ctx)
129
+
130
+ ctx.begin()
131
+
132
+ for (const op of normalized) {
133
+ switch (op.type) {
134
+ case `insert`: {
135
+ const resolved = ctx.collection.validateData(op.data, `insert`)
136
+ ctx.write({
137
+ type: `insert`,
138
+ value: resolved,
139
+ })
140
+ break
141
+ }
142
+ case `update`: {
143
+ const currentItem = ctx.collection.get(op.key)!
144
+ const updatedItem = {
145
+ ...currentItem,
146
+ ...op.data,
147
+ }
148
+ const resolved = ctx.collection.validateData(
149
+ updatedItem,
150
+ `update`,
151
+ op.key
152
+ )
153
+ ctx.write({
154
+ type: `update`,
155
+ value: resolved,
156
+ })
157
+ break
158
+ }
159
+ case `delete`: {
160
+ const currentItem = ctx.collection.get(op.key)!
161
+ ctx.write({
162
+ type: `delete`,
163
+ value: currentItem,
164
+ })
165
+ break
166
+ }
167
+ case `upsert`: {
168
+ const resolved = ctx.collection.validateData(
169
+ op.data,
170
+ ctx.collection.has(op.key) ? `update` : `insert`,
171
+ op.key
172
+ )
173
+ if (ctx.collection.has(op.key)) {
174
+ ctx.write({
175
+ type: `update`,
176
+ value: resolved,
177
+ })
178
+ } else {
179
+ ctx.write({
180
+ type: `insert`,
181
+ value: resolved,
182
+ })
183
+ }
184
+ break
185
+ }
186
+ }
187
+ }
188
+
189
+ ctx.commit()
190
+
191
+ // Update query cache after successful commit
192
+ const updatedData = ctx.collection.toArray
193
+ ctx.queryClient.setQueryData(ctx.queryKey, updatedData)
194
+ }
195
+
196
+ // Factory function to create write utils
197
+ export function createWriteUtils<
198
+ TRow extends object,
199
+ TKey extends string | number = string | number,
200
+ TInsertInput extends object = TRow,
201
+ >(getContext: () => SyncContext<TRow, TKey> | null) {
202
+ function ensureContext(): SyncContext<TRow, TKey> {
203
+ const context = getContext()
204
+ if (!context) {
205
+ throw new SyncNotInitializedError()
206
+ }
207
+ return context
208
+ }
209
+
210
+ return {
211
+ writeInsert(data: TInsertInput | Array<TInsertInput>) {
212
+ const ctx = ensureContext()
213
+ performWriteOperations({ type: `insert`, data }, ctx)
214
+ },
215
+
216
+ writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {
217
+ const ctx = ensureContext()
218
+ performWriteOperations({ type: `update`, data }, ctx)
219
+ },
220
+
221
+ writeDelete(key: TKey | Array<TKey>) {
222
+ const ctx = ensureContext()
223
+ performWriteOperations({ type: `delete`, key }, ctx)
224
+ },
225
+
226
+ writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {
227
+ const ctx = ensureContext()
228
+ performWriteOperations({ type: `upsert`, data }, ctx)
229
+ },
230
+
231
+ writeBatch(operations: Array<SyncOperation<TRow, TKey, TInsertInput>>) {
232
+ const ctx = ensureContext()
233
+ performWriteOperations(operations, ctx)
234
+ },
235
+ }
236
+ }
package/src/query.ts CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  QueryFnRequiredError,
6
6
  QueryKeyRequiredError,
7
7
  } from "./errors"
8
+ import { createWriteUtils } from "./manual-sync"
9
+ import type { SyncOperation } from "./manual-sync"
8
10
  import type {
9
11
  QueryClient,
10
12
  QueryFunctionContext,
@@ -12,6 +14,7 @@ import type {
12
14
  QueryObserverOptions,
13
15
  } from "@tanstack/query-core"
14
16
  import type {
17
+ ChangeMessage,
15
18
  CollectionConfig,
16
19
  DeleteMutationFn,
17
20
  DeleteMutationFnParams,
@@ -23,6 +26,9 @@ import type {
23
26
  UtilsRecord,
24
27
  } from "@tanstack/db"
25
28
 
29
+ // Re-export for external use
30
+ export type { SyncOperation } from "./manual-sync"
31
+
26
32
  export interface QueryCollectionConfig<
27
33
  TItem extends object,
28
34
  TError = unknown,
@@ -222,8 +228,22 @@ export type RefetchFn = () => Promise<void>
222
228
  /**
223
229
  * Query collection utilities type
224
230
  */
225
- export interface QueryCollectionUtils extends UtilsRecord {
231
+ /**
232
+ * Write operation types for batch operations
233
+ */
234
+ export interface QueryCollectionUtils<
235
+ TItem extends object = Record<string, unknown>,
236
+ TKey extends string | number = string | number,
237
+ TInsertInput extends object = TItem,
238
+ > extends UtilsRecord {
226
239
  refetch: RefetchFn
240
+ writeInsert: (data: TInsertInput | Array<TInsertInput>) => void
241
+ writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void
242
+ writeDelete: (keys: TKey | Array<TKey>) => void
243
+ writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void
244
+ writeBatch: (
245
+ operations: Array<SyncOperation<TItem, TKey, TInsertInput>>
246
+ ) => void
227
247
  }
228
248
 
229
249
  /**
@@ -236,9 +256,13 @@ export function queryCollectionOptions<
236
256
  TItem extends object,
237
257
  TError = unknown,
238
258
  TQueryKey extends QueryKey = QueryKey,
259
+ TKey extends string | number = string | number,
260
+ TInsertInput extends object = TItem,
239
261
  >(
240
262
  config: QueryCollectionConfig<TItem, TError, TQueryKey>
241
- ): CollectionConfig<TItem> & { utils: QueryCollectionUtils } {
263
+ ): CollectionConfig<TItem> & {
264
+ utils: QueryCollectionUtils<TItem, TKey, TInsertInput>
265
+ } {
242
266
  const {
243
267
  queryKey,
244
268
  queryFn,
@@ -407,6 +431,41 @@ export function queryCollectionOptions<
407
431
  })
408
432
  }
409
433
 
434
+ // Create write context for manual write operations
435
+ let writeContext: {
436
+ collection: any
437
+ queryClient: QueryClient
438
+ queryKey: Array<unknown>
439
+ getKey: (item: TItem) => TKey
440
+ begin: () => void
441
+ write: (message: Omit<ChangeMessage<TItem>, `key`>) => void
442
+ commit: () => void
443
+ } | null = null
444
+
445
+ // Enhanced internalSync that captures write functions for manual use
446
+ const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {
447
+ const { begin, write, commit, collection } = params
448
+
449
+ // Store references for manual write operations
450
+ writeContext = {
451
+ collection,
452
+ queryClient,
453
+ queryKey: queryKey as unknown as Array<unknown>,
454
+ getKey: getKey as (item: TItem) => TKey,
455
+ begin,
456
+ write,
457
+ commit,
458
+ }
459
+
460
+ // Call the original internalSync logic
461
+ return internalSync(params)
462
+ }
463
+
464
+ // Create write utils using the manual-sync module
465
+ const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(
466
+ () => writeContext
467
+ )
468
+
410
469
  // Create wrapper handlers for direct persistence operations that handle refetching
411
470
  const wrappedOnInsert = onInsert
412
471
  ? async (params: InsertMutationFnParams<TItem>) => {
@@ -453,12 +512,13 @@ export function queryCollectionOptions<
453
512
  return {
454
513
  ...baseCollectionConfig,
455
514
  getKey,
456
- sync: { sync: internalSync },
515
+ sync: { sync: enhancedInternalSync },
457
516
  onInsert: wrappedOnInsert,
458
517
  onUpdate: wrappedOnUpdate,
459
518
  onDelete: wrappedOnDelete,
460
519
  utils: {
461
520
  refetch,
521
+ ...writeUtils,
462
522
  },
463
523
  }
464
524
  }