Skip to content

Commit 43ac7e7

Browse files
committed
improve schema types
1 parent bbed2f2 commit 43ac7e7

File tree

9 files changed

+355
-106
lines changed

9 files changed

+355
-106
lines changed

packages/db/src/local-only.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DeleteMutationFnParams,
44
InsertMutationFnParams,
55
OperationType,
6+
ResolveInsertInput,
67
ResolveType,
78
SyncConfig,
89
UpdateMutationFnParams,
@@ -26,7 +27,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
2627
* You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
2728
*/
2829
export interface LocalOnlyCollectionConfig<
29-
TExplicit = unknown,
30+
TExplicit extends object = Record<string, unknown>,
3031
TSchema extends StandardSchemaV1 = never,
3132
TFallback extends Record<string, unknown> = Record<string, unknown>,
3233
TKey extends string | number = string | number,
@@ -51,7 +52,7 @@ export interface LocalOnlyCollectionConfig<
5152
*/
5253
onInsert?: (
5354
params: InsertMutationFnParams<
54-
ResolveType<TExplicit, TSchema, TFallback>,
55+
ResolveInsertInput<TExplicit, TSchema, TFallback>,
5556
TKey,
5657
LocalOnlyCollectionUtils
5758
>
@@ -136,33 +137,56 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
136137
* )
137138
*/
138139
export function localOnlyCollectionOptions<
139-
TExplicit = unknown,
140+
TExplicit extends object = Record<string, unknown>,
140141
TSchema extends StandardSchemaV1 = never,
141142
TFallback extends Record<string, unknown> = Record<string, unknown>,
142143
TKey extends string | number = string | number,
143144
>(
144145
config: LocalOnlyCollectionConfig<TExplicit, TSchema, TFallback, TKey>
145-
): CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>, TKey> & {
146+
): CollectionConfig<
147+
ResolveType<TExplicit, TSchema, TFallback>,
148+
TKey,
149+
TSchema,
150+
ResolveInsertInput<TExplicit, TSchema, TFallback>
151+
> & {
146152
utils: LocalOnlyCollectionUtils
147153
} {
148-
type ResolvedType = ResolveType<TExplicit, TSchema, TFallback>
154+
type TItem = ResolveType<TExplicit, TSchema, TFallback>
155+
type TInsertInput = ResolveInsertInput<TExplicit, TSchema, TFallback>
149156

150157
const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
151158

152159
// Create the sync configuration with transaction confirmation capability
153-
const syncResult = createLocalOnlySync<ResolvedType, TKey>(initialData)
160+
const syncResult = createLocalOnlySync<TItem, TKey>(
161+
initialData as Array<TItem> | undefined
162+
)
154163

155164
/**
156165
* Create wrapper handlers that call user handlers first, then confirm transactions
157166
* Wraps the user's onInsert handler to also confirm the transaction immediately
158167
*/
159168
const wrappedOnInsert = async (
160-
params: InsertMutationFnParams<ResolvedType, TKey, LocalOnlyCollectionUtils>
169+
params: InsertMutationFnParams<TItem, TKey, LocalOnlyCollectionUtils>
161170
) => {
162171
// Call user handler first if provided
163172
let handlerResult
164173
if (onInsert) {
165-
handlerResult = (await onInsert(params)) ?? {}
174+
handlerResult =
175+
(await (
176+
onInsert as (
177+
p: InsertMutationFnParams<
178+
TInsertInput,
179+
TKey,
180+
LocalOnlyCollectionUtils
181+
>
182+
) => Promise<any>
183+
)(
184+
params as unknown as InsertMutationFnParams<
185+
TInsertInput,
186+
TKey,
187+
LocalOnlyCollectionUtils
188+
>
189+
)) ?? {}
166190
}
167191

168192
// Then synchronously confirm the transaction by looping through mutations
@@ -175,7 +199,7 @@ export function localOnlyCollectionOptions<
175199
* Wrapper for onUpdate handler that also confirms the transaction immediately
176200
*/
177201
const wrappedOnUpdate = async (
178-
params: UpdateMutationFnParams<ResolvedType, TKey, LocalOnlyCollectionUtils>
202+
params: UpdateMutationFnParams<TItem, TKey, LocalOnlyCollectionUtils>
179203
) => {
180204
// Call user handler first if provided
181205
let handlerResult
@@ -193,7 +217,7 @@ export function localOnlyCollectionOptions<
193217
* Wrapper for onDelete handler that also confirms the transaction immediately
194218
*/
195219
const wrappedOnDelete = async (
196-
params: DeleteMutationFnParams<ResolvedType, TKey, LocalOnlyCollectionUtils>
220+
params: DeleteMutationFnParams<TItem, TKey, LocalOnlyCollectionUtils>
197221
) => {
198222
// Call user handler first if provided
199223
let handlerResult

packages/db/src/local-storage.ts

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
import type {
1010
CollectionConfig,
1111
DeleteMutationFnParams,
12+
InsertMutationFn,
1213
InsertMutationFnParams,
14+
ResolveInsertInput,
1315
ResolveType,
1416
SyncConfig,
1517
UpdateMutationFnParams,
@@ -62,6 +64,7 @@ export interface LocalStorageCollectionConfig<
6264
TExplicit = unknown,
6365
TSchema extends StandardSchemaV1 = never,
6466
TFallback extends object = Record<string, unknown>,
67+
TKey extends string | number = string | number,
6568
> {
6669
/**
6770
* The key to use for storing the collection data in localStorage/sessionStorage
@@ -85,16 +88,29 @@ export interface LocalStorageCollectionConfig<
8588
*/
8689
id?: string
8790
schema?: TSchema
88-
getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`]
89-
sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`]
91+
getKey: CollectionConfig<
92+
ResolveType<TExplicit, TSchema, TFallback>,
93+
TKey,
94+
TSchema,
95+
ResolveInsertInput<TExplicit, TSchema, TFallback>
96+
>[`getKey`]
97+
sync?: CollectionConfig<
98+
ResolveType<TExplicit, TSchema, TFallback>,
99+
TKey,
100+
TSchema,
101+
ResolveInsertInput<TExplicit, TSchema, TFallback>
102+
>[`sync`]
90103

91104
/**
92105
* Optional asynchronous handler function called before an insert operation
93106
* @param params Object containing transaction and collection information
94107
* @returns Promise resolving to any value
95108
*/
96109
onInsert?: (
97-
params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
110+
params: InsertMutationFnParams<
111+
ResolveInsertInput<TExplicit, TSchema, TFallback>,
112+
TKey
113+
>
98114
) => Promise<any>
99115

100116
/**
@@ -103,7 +119,10 @@ export interface LocalStorageCollectionConfig<
103119
* @returns Promise resolving to any value
104120
*/
105121
onUpdate?: (
106-
params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
122+
params: UpdateMutationFnParams<
123+
ResolveType<TExplicit, TSchema, TFallback>,
124+
TKey
125+
>
107126
) => Promise<any>
108127

109128
/**
@@ -112,7 +131,10 @@ export interface LocalStorageCollectionConfig<
112131
* @returns Promise resolving to any value
113132
*/
114133
onDelete?: (
115-
params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
134+
params: DeleteMutationFnParams<
135+
ResolveType<TExplicit, TSchema, TFallback>,
136+
TKey
137+
>
116138
) => Promise<any>
117139
}
118140

@@ -206,13 +228,23 @@ export function localStorageCollectionOptions<
206228
TExplicit = unknown,
207229
TSchema extends StandardSchemaV1 = never,
208230
TFallback extends object = Record<string, unknown>,
231+
TKey extends string | number = string | number,
209232
>(
210-
config: LocalStorageCollectionConfig<TExplicit, TSchema, TFallback>
211-
): Omit<CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>, `id`> & {
233+
config: LocalStorageCollectionConfig<TExplicit, TSchema, TFallback, TKey>
234+
): Omit<
235+
CollectionConfig<
236+
ResolveType<TExplicit, TSchema, TFallback>,
237+
TKey,
238+
TSchema,
239+
ResolveInsertInput<TExplicit, TSchema, TFallback>
240+
>,
241+
`id`
242+
> & {
212243
id: string
213244
utils: LocalStorageCollectionUtils
214245
} {
215-
type ResolvedType = ResolveType<TExplicit, TSchema, TFallback>
246+
type TItem = ResolveType<TExplicit, TSchema, TFallback>
247+
type TInsertInput = ResolveInsertInput<TExplicit, TSchema, TFallback>
216248

217249
// Validate required parameters
218250
if (!config.storageKey) {
@@ -237,14 +269,14 @@ export function localStorageCollectionOptions<
237269
}
238270

239271
// Track the last known state to detect changes
240-
const lastKnownData = new Map<string | number, StoredItem<ResolvedType>>()
272+
const lastKnownData = new Map<string | number, StoredItem<TItem>>()
241273

242274
// Create the sync configuration
243-
const sync = createLocalStorageSync<ResolvedType>(
275+
const sync = createLocalStorageSync<TItem, TKey>(
244276
config.storageKey,
245277
storage,
246278
storageEventApi,
247-
config.getKey,
279+
config.getKey as (item: TItem) => TKey,
248280
lastKnownData
249281
)
250282

@@ -263,11 +295,11 @@ export function localStorageCollectionOptions<
263295
* @param dataMap - Map of items with version tracking to save to storage
264296
*/
265297
const saveToStorage = (
266-
dataMap: Map<string | number, StoredItem<ResolvedType>>
298+
dataMap: Map<string | number, StoredItem<TItem>>
267299
): void => {
268300
try {
269301
// Convert Map to object format for storage
270-
const objectData: Record<string, StoredItem<ResolvedType>> = {}
302+
const objectData: Record<string, StoredItem<TItem>> = {}
271303
dataMap.forEach((storedItem, key) => {
272304
objectData[String(key)] = storedItem
273305
})
@@ -303,7 +335,7 @@ export function localStorageCollectionOptions<
303335
* Wraps the user's onInsert handler to also save changes to localStorage
304336
*/
305337
const wrappedOnInsert = async (
306-
params: InsertMutationFnParams<ResolvedType>
338+
params: InsertMutationFnParams<TItem, TKey>
307339
) => {
308340
// Validate that all values in the transaction can be JSON serialized
309341
params.transaction.mutations.forEach((mutation) => {
@@ -313,20 +345,20 @@ export function localStorageCollectionOptions<
313345
// Call the user handler BEFORE persisting changes (if provided)
314346
let handlerResult: any = {}
315347
if (config.onInsert) {
316-
handlerResult = (await config.onInsert(params)) ?? {}
348+
handlerResult =
349+
(await (config.onInsert as InsertMutationFn<TInsertInput, TKey>)(
350+
params as unknown as InsertMutationFnParams<TInsertInput, TKey>
351+
)) ?? {}
317352
}
318353

319354
// Always persist to storage
320355
// Load current data from storage
321-
const currentData = loadFromStorage<ResolvedType>(
322-
config.storageKey,
323-
storage
324-
)
356+
const currentData = loadFromStorage<TItem>(config.storageKey, storage)
325357

326358
// Add new items with version keys
327359
params.transaction.mutations.forEach((mutation) => {
328-
const key = config.getKey(mutation.modified)
329-
const storedItem: StoredItem<ResolvedType> = {
360+
const key = (config.getKey as (item: TItem) => TKey)(mutation.modified)
361+
const storedItem: StoredItem<TItem> = {
330362
versionKey: generateUuid(),
331363
data: mutation.modified,
332364
}
@@ -343,7 +375,7 @@ export function localStorageCollectionOptions<
343375
}
344376

345377
const wrappedOnUpdate = async (
346-
params: UpdateMutationFnParams<ResolvedType>
378+
params: UpdateMutationFnParams<TItem, TKey>
347379
) => {
348380
// Validate that all values in the transaction can be JSON serialized
349381
params.transaction.mutations.forEach((mutation) => {
@@ -358,15 +390,12 @@ export function localStorageCollectionOptions<
358390

359391
// Always persist to storage
360392
// Load current data from storage
361-
const currentData = loadFromStorage<ResolvedType>(
362-
config.storageKey,
363-
storage
364-
)
393+
const currentData = loadFromStorage<TItem>(config.storageKey, storage)
365394

366395
// Update items with new version keys
367396
params.transaction.mutations.forEach((mutation) => {
368-
const key = config.getKey(mutation.modified)
369-
const storedItem: StoredItem<ResolvedType> = {
397+
const key = (config.getKey as (item: TItem) => TKey)(mutation.modified)
398+
const storedItem: StoredItem<TItem> = {
370399
versionKey: generateUuid(),
371400
data: mutation.modified,
372401
}
@@ -383,7 +412,7 @@ export function localStorageCollectionOptions<
383412
}
384413

385414
const wrappedOnDelete = async (
386-
params: DeleteMutationFnParams<ResolvedType>
415+
params: DeleteMutationFnParams<TItem, TKey>
387416
) => {
388417
// Call the user handler BEFORE persisting changes (if provided)
389418
let handlerResult: any = {}
@@ -393,15 +422,14 @@ export function localStorageCollectionOptions<
393422

394423
// Always persist to storage
395424
// Load current data from storage
396-
const currentData = loadFromStorage<ResolvedType>(
397-
config.storageKey,
398-
storage
399-
)
425+
const currentData = loadFromStorage<TItem>(config.storageKey, storage)
400426

401427
// Remove items
402428
params.transaction.mutations.forEach((mutation) => {
403429
// For delete operations, mutation.original contains the full object
404-
const key = config.getKey(mutation.original as ResolvedType)
430+
const key = (config.getKey as (item: TItem) => TKey)(
431+
mutation.original as TItem
432+
)
405433
currentData.delete(key)
406434
})
407435

@@ -433,7 +461,10 @@ export function localStorageCollectionOptions<
433461
...restConfig,
434462
id: collectionId,
435463
sync,
436-
onInsert: wrappedOnInsert,
464+
onInsert: wrappedOnInsert as unknown as InsertMutationFn<
465+
TInsertInput,
466+
TKey
467+
>,
437468
onUpdate: wrappedOnUpdate,
438469
onDelete: wrappedOnDelete,
439470
utils: {
@@ -506,14 +537,17 @@ function loadFromStorage<T extends object>(
506537
* @param lastKnownData - Map tracking the last known state for change detection
507538
* @returns Sync configuration with manual trigger capability
508539
*/
509-
function createLocalStorageSync<T extends object>(
540+
function createLocalStorageSync<
541+
T extends object,
542+
TKey extends string | number = string | number,
543+
>(
510544
storageKey: string,
511545
storage: StorageApi,
512546
storageEventApi: StorageEventApi,
513-
_getKey: (item: T) => string | number,
547+
_getKey: (item: T) => TKey,
514548
lastKnownData: Map<string | number, StoredItem<T>>
515-
): SyncConfig<T> & { manualTrigger?: () => void } {
516-
let syncParams: Parameters<SyncConfig<T>[`sync`]>[0] | null = null
549+
): SyncConfig<T, TKey> & { manualTrigger?: () => void } {
550+
let syncParams: Parameters<SyncConfig<T, TKey>[`sync`]>[0] | null = null
517551

518552
/**
519553
* Compare two Maps to find differences using version keys
@@ -588,8 +622,8 @@ function createLocalStorageSync<T extends object>(
588622
}
589623
}
590624

591-
const syncConfig: SyncConfig<T> & { manualTrigger?: () => void } = {
592-
sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {
625+
const syncConfig: SyncConfig<T, TKey> & { manualTrigger?: () => void } = {
626+
sync: (params: Parameters<SyncConfig<T, TKey>[`sync`]>[0]) => {
593627
const { begin, write, commit, markReady } = params
594628

595629
// Store sync params for later use

0 commit comments

Comments
 (0)