Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-toes-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db-sqlite-persistence-core': patch
---

Add schema-aware overloads to `persistedCollectionOptions` so schema-based calls infer the correct types and remain compatible with `createCollection`.
59 changes: 57 additions & 2 deletions packages/db-sqlite-persistence-core/src/persisted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
CollectionConfig,
CollectionIndexMetadata,
DeleteMutationFnParams,
InferSchemaOutput,
InsertMutationFnParams,
LoadSubsetOptions,
PendingMutation,
Expand Down Expand Up @@ -2572,22 +2573,76 @@ function createLoopbackSyncConfig<
}
}

// Overload for when schema is provided and sync is present
export function persistedCollectionOptions<
TSchema extends StandardSchemaV1,
TKey extends string | number,
TUtils extends UtilsRecord = UtilsRecord,
>(
options: PersistedSyncWrappedOptions<
InferSchemaOutput<TSchema>,
TKey,
TSchema,
TUtils
> & {
schema: TSchema
},
): PersistedSyncOptionsResult<
InferSchemaOutput<TSchema>,
TKey,
TSchema,
TUtils
> & {
schema: TSchema
}

// Overload for when schema is provided and sync is absent
export function persistedCollectionOptions<
TSchema extends StandardSchemaV1,
TKey extends string | number,
TUtils extends UtilsRecord = UtilsRecord,
>(
options: PersistedLocalOnlyOptions<
InferSchemaOutput<TSchema>,
TKey,
TSchema,
TUtils
> & {
schema: TSchema
},
): PersistedLocalOnlyOptionsResult<
InferSchemaOutput<TSchema>,
TKey,
TSchema,
TUtils
> & {
schema: TSchema
}

// Overload for when no schema is provided and sync is present
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
export function persistedCollectionOptions<
T extends object,
TKey extends string | number,
TSchema extends StandardSchemaV1 = never,
TUtils extends UtilsRecord = UtilsRecord,
>(
options: PersistedSyncWrappedOptions<T, TKey, TSchema, TUtils>,
options: PersistedSyncWrappedOptions<T, TKey, TSchema, TUtils> & {
schema?: never // prohibit schema
},
): PersistedSyncOptionsResult<T, TKey, TSchema, TUtils>

// Overload for when no schema is provided and sync is absent
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
export function persistedCollectionOptions<
T extends object,
TKey extends string | number,
TSchema extends StandardSchemaV1 = never,
TUtils extends UtilsRecord = UtilsRecord,
>(
options: PersistedLocalOnlyOptions<T, TKey, TSchema, TUtils>,
options: PersistedLocalOnlyOptions<T, TKey, TSchema, TUtils> & {
schema?: never // prohibit schema
},
): PersistedLocalOnlyOptionsResult<T, TKey, TSchema, TUtils>

export function persistedCollectionOptions<
Expand Down
193 changes: 192 additions & 1 deletion packages/db-sqlite-persistence-core/tests/persisted.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { describe, expectTypeOf, it } from 'vitest'
import { z } from 'zod'
import { createCollection } from '@tanstack/db'
import { persistedCollectionOptions } from '../src'
import type { PersistedCollectionUtils, PersistenceAdapter } from '../src'
import type { SyncConfig, UtilsRecord } from '@tanstack/db'
import type { SyncConfig, UtilsRecord, WithVirtualProps } from '@tanstack/db'

type OutputWithVirtual<
T extends object,
TKey extends string | number = string | number,
> = WithVirtualProps<T, TKey>

type ItemOf<T> = T extends Array<infer U> ? U : T

type Todo = {
id: string
Expand Down Expand Up @@ -90,6 +98,11 @@ describe(`persisted collection types`, () => {
// @ts-expect-error persistedCollectionOptions requires a persistence config
persistedCollectionOptions({
getKey: (item: Todo) => item.id,
})

persistedCollectionOptions({
getKey: (item: Todo) => item.id,
// @ts-expect-error persistedCollectionOptions requires a persistence config when sync is provided
sync: {
sync: ({ markReady }: { markReady: () => void }) => {
markReady()
Expand All @@ -108,4 +121,182 @@ describe(`persisted collection types`, () => {
},
})
})

it(`should work with schema and infer correct types when saved to a variable in sync-absent mode`, () => {
const testSchema = z.object({
id: z.string(),
title: z.string(),
createdAt: z.date().optional().default(new Date()),
})

type ExpectedType = z.infer<typeof testSchema>
type ExpectedInput = z.input<typeof testSchema>

const schemaAdapter: PersistenceAdapter<ExpectedType, string> = {
loadSubset: () => Promise.resolve([]),
applyCommittedTx: () => Promise.resolve(),
ensureIndex: () => Promise.resolve(),
}

const options = persistedCollectionOptions({
id: `test-local-schema`,
schema: testSchema,
schemaVersion: 1,
getKey: (item) => item.id,
persistence: { adapter: schemaAdapter },
})

expectTypeOf(options.schema).toEqualTypeOf<typeof testSchema>()

const collection = createCollection(options)

// Test that the collection has the correct inferred type from schema
expectTypeOf(collection.toArray).toEqualTypeOf<
Array<OutputWithVirtual<ExpectedType, string>>
>()

// Test insert parameter type
type InsertParam = Parameters<typeof collection.insert>[0]
expectTypeOf<ItemOf<InsertParam>>().toEqualTypeOf<ExpectedInput>()

// Check that the update method accepts the expected input type
collection.update(`1`, (draft) => {
expectTypeOf(draft).toEqualTypeOf<ExpectedInput>()
})
})

it(`should work with schema and infer correct types when nested in createCollection in sync-absent mode`, () => {
const testSchema = z.object({
id: z.string(),
title: z.string(),
createdAt: z.date().optional().default(new Date()),
})

type ExpectedType = z.infer<typeof testSchema>
type ExpectedInput = z.input<typeof testSchema>

const schemaAdapter: PersistenceAdapter<ExpectedType, string> = {
loadSubset: () => Promise.resolve([]),
applyCommittedTx: () => Promise.resolve(),
ensureIndex: () => Promise.resolve(),
}

const collection = createCollection(
persistedCollectionOptions({
id: `test-local-schema-nested`,
schema: testSchema,
schemaVersion: 1,
getKey: (item) => item.id,
persistence: { adapter: schemaAdapter },
}),
)

// Test that the collection has the correct inferred type from schema
expectTypeOf(collection.toArray).toEqualTypeOf<
Array<OutputWithVirtual<ExpectedType, string>>
>()

// Test insert parameter type
type InsertParam = Parameters<typeof collection.insert>[0]
expectTypeOf<ItemOf<InsertParam>>().toEqualTypeOf<ExpectedInput>()

// Check that the update method accepts the expected input type
collection.update(`1`, (draft) => {
expectTypeOf(draft).toEqualTypeOf<ExpectedInput>()
})
})

it(`should work with schema and infer correct types when saved to a variable in sync-present mode`, () => {
const testSchema = z.object({
id: z.string(),
title: z.string(),
createdAt: z.date().optional().default(new Date()),
})

type ExpectedType = z.infer<typeof testSchema>
type ExpectedInput = z.input<typeof testSchema>

const schemaAdapter: PersistenceAdapter<ExpectedType, string> = {
loadSubset: () => Promise.resolve([]),
applyCommittedTx: () => Promise.resolve(),
ensureIndex: () => Promise.resolve(),
}

const options = persistedCollectionOptions({
id: `test-sync-schema`,
schema: testSchema,
schemaVersion: 1,
getKey: (item) => item.id,
sync: {
sync: ({ markReady }) => {
markReady()
},
},
persistence: { adapter: schemaAdapter },
})

expectTypeOf(options.schema).toEqualTypeOf<typeof testSchema>()

const collection = createCollection(options)

// Test that the collection has the correct inferred type from schema
expectTypeOf(collection.toArray).toEqualTypeOf<
Array<OutputWithVirtual<ExpectedType, string>>
>()

// Test insert parameter type
type InsertParam = Parameters<typeof collection.insert>[0]
expectTypeOf<ItemOf<InsertParam>>().toEqualTypeOf<ExpectedInput>()

// Check that the update method accepts the expected input type
collection.update(`1`, (draft) => {
expectTypeOf(draft).toEqualTypeOf<ExpectedInput>()
})
})

it(`should work with schema and infer correct types when nested in createCollection in sync-present mode`, () => {
const testSchema = z.object({
id: z.string(),
title: z.string(),
createdAt: z.date().optional().default(new Date()),
})

type ExpectedType = z.infer<typeof testSchema>
type ExpectedInput = z.input<typeof testSchema>

const schemaAdapter: PersistenceAdapter<ExpectedType, string> = {
loadSubset: () => Promise.resolve([]),
applyCommittedTx: () => Promise.resolve(),
ensureIndex: () => Promise.resolve(),
}

const collection = createCollection(
persistedCollectionOptions({
id: `test-sync-schema-nested`,
schema: testSchema,
schemaVersion: 1,
getKey: (item) => item.id,
sync: {
sync: ({ markReady }) => {
markReady()
},
},
persistence: { adapter: schemaAdapter },
}),
)

// Test that the collection has the correct inferred type from schema
expectTypeOf(collection.toArray).toEqualTypeOf<
Array<OutputWithVirtual<ExpectedType, string>>
>()

// Test insert parameter type
type InsertParam = Parameters<typeof collection.insert>[0]
expectTypeOf<ItemOf<InsertParam>>().toEqualTypeOf<ExpectedInput>()

// Check that the update method accepts the expected input type
collection.update(`1`, (draft) => {
expectTypeOf(draft).toEqualTypeOf<ExpectedInput>()
})
})
})
Loading