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() + }) + }) })