Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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>()
})
})
})