From f9f24b484081160dc65c02f2dcfbd8657336d27a Mon Sep 17 00:00:00 2001 From: Amr Gad Date: Sat, 4 Apr 2026 02:14:38 +0200 Subject: [PATCH 1/3] fix(db-sqlite-persistence-core): add schema-aware overloads to persistedCollectionOptions persistedCollectionOptions was missing schema-aware overloads, causing TSchema to default to `never` when a schema was passed. This made the result incompatible with createCollection's schema overloads. Add four overloads matching the pattern used by localOnlyCollectionOptions and localStorageCollectionOptions: - schema provided + sync present - schema provided + sync absent - no schema + sync present - no schema + sync absent --- .../src/persisted.ts | 59 +++++- .../tests/persisted.test-d.ts | 193 +++++++++++++++++- 2 files changed, 249 insertions(+), 3 deletions(-) diff --git a/packages/db-sqlite-persistence-core/src/persisted.ts b/packages/db-sqlite-persistence-core/src/persisted.ts index 236eddb9c..25560d16b 100644 --- a/packages/db-sqlite-persistence-core/src/persisted.ts +++ b/packages/db-sqlite-persistence-core/src/persisted.ts @@ -14,6 +14,7 @@ import type { CollectionConfig, CollectionIndexMetadata, DeleteMutationFnParams, + InferSchemaOutput, InsertMutationFnParams, LoadSubsetOptions, PendingMutation, @@ -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, + TKey, + TSchema, + TUtils + > & { + schema: TSchema + }, +): PersistedSyncOptionsResult< + InferSchemaOutput, + 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, + TKey, + TSchema, + TUtils + > & { + schema: TSchema + }, +): PersistedLocalOnlyOptionsResult< + InferSchemaOutput, + 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, + options: PersistedSyncWrappedOptions & { + schema?: never // prohibit schema + }, ): PersistedSyncOptionsResult +// 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, + options: PersistedLocalOnlyOptions & { + schema?: never // prohibit schema + }, ): PersistedLocalOnlyOptionsResult export function persistedCollectionOptions< diff --git a/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts b/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts index e363a4954..e6ee0bf86 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts @@ -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 + +type ItemOf = T extends Array ? U : T type Todo = { id: string @@ -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() @@ -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 + type ExpectedInput = z.input + + const schemaAdapter: PersistenceAdapter = { + 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() + + const collection = createCollection(options) + + // Test that the collection has the correct inferred type from schema + expectTypeOf(collection.toArray).toEqualTypeOf< + Array> + >() + + // Test insert parameter type + type InsertParam = Parameters[0] + expectTypeOf>().toEqualTypeOf() + + // Check that the update method accepts the expected input type + collection.update(`1`, (draft) => { + expectTypeOf(draft).toEqualTypeOf() + }) + }) + + 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 + type ExpectedInput = z.input + + const schemaAdapter: PersistenceAdapter = { + 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> + >() + + // Test insert parameter type + type InsertParam = Parameters[0] + expectTypeOf>().toEqualTypeOf() + + // Check that the update method accepts the expected input type + collection.update(`1`, (draft) => { + expectTypeOf(draft).toEqualTypeOf() + }) + }) + + 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 + type ExpectedInput = z.input + + const schemaAdapter: PersistenceAdapter = { + 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() + + const collection = createCollection(options) + + // Test that the collection has the correct inferred type from schema + expectTypeOf(collection.toArray).toEqualTypeOf< + Array> + >() + + // Test insert parameter type + type InsertParam = Parameters[0] + expectTypeOf>().toEqualTypeOf() + + // Check that the update method accepts the expected input type + collection.update(`1`, (draft) => { + expectTypeOf(draft).toEqualTypeOf() + }) + }) + + 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 + type ExpectedInput = z.input + + const schemaAdapter: PersistenceAdapter = { + 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> + >() + + // Test insert parameter type + type InsertParam = Parameters[0] + expectTypeOf>().toEqualTypeOf() + + // Check that the update method accepts the expected input type + collection.update(`1`, (draft) => { + expectTypeOf(draft).toEqualTypeOf() + }) + }) }) From d77572e94288b1b04a88b6b05004631ffc7f9470 Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Tue, 7 Apr 2026 17:56:41 +0100 Subject: [PATCH 2/3] chore(changeset): add release note for persisted schema overloads Document the patch release metadata for the schema-aware persistedCollectionOptions typing fix so the PR can version the package correctly. Made-with: Cursor --- .changeset/calm-toes-approve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/calm-toes-approve.md diff --git a/.changeset/calm-toes-approve.md b/.changeset/calm-toes-approve.md new file mode 100644 index 000000000..71b9170b3 --- /dev/null +++ b/.changeset/calm-toes-approve.md @@ -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`. From 78af495a3137dd6fd9675413d7266564d71e3517 Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Tue, 7 Apr 2026 18:06:21 +0100 Subject: [PATCH 3/3] fix(db-sqlite-persistence-core): import generic adapter type in type tests Avoid the non-generic barrel type resolution seen in CI by importing PersistenceAdapter directly from the persisted module in the schema inference tests. Made-with: Cursor --- packages/db-sqlite-persistence-core/tests/persisted.test-d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts b/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts index e6ee0bf86..959637a3d 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted.test-d.ts @@ -2,7 +2,8 @@ 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 { PersistedCollectionUtils } from '../src' +import type { PersistenceAdapter } from '../src/persisted' import type { SyncConfig, UtilsRecord, WithVirtualProps } from '@tanstack/db' type OutputWithVirtual<