Skip to content

Commit bec8620

Browse files
fix: arktype schemas (#279)
Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
1 parent 581cd43 commit bec8620

File tree

6 files changed

+128
-6
lines changed

6 files changed

+128
-6
lines changed

.changeset/chubby-ties-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
fix arktype schemas for collections

packages/db/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"@standard-schema/spec": "^1.0.0"
88
},
99
"devDependencies": {
10-
"@vitest/coverage-istanbul": "^3.0.9"
10+
"@vitest/coverage-istanbul": "^3.0.9",
11+
"arktype": "^2.1.20"
1112
},
1213
"exports": {
1314
".": {

packages/db/src/collection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { withArrayChangeTracking, withChangeTracking } from "./proxy"
2-
import { createTransaction, getActiveTransaction } from "./transactions"
32
import { SortedMap } from "./SortedMap"
3+
import { createTransaction, getActiveTransaction } from "./transactions"
44
import type { Transaction } from "./transactions"
5+
import type { StandardSchemaV1 } from "@standard-schema/spec"
56
import type {
67
ChangeListener,
78
ChangeMessage,
@@ -19,7 +20,6 @@ import type {
1920
TransactionWithMutations,
2021
UtilsRecord,
2122
} from "./types"
22-
import type { StandardSchemaV1 } from "@standard-schema/spec"
2323

2424
// Store collections in memory
2525
export const collectionsStore = new Map<string, CollectionImpl<any, any, any>>()
@@ -1242,7 +1242,7 @@ export class CollectionImpl<
12421242

12431243
private ensureStandardSchema(schema: unknown): StandardSchema<T> {
12441244
// If the schema already implements the standard-schema interface, return it
1245-
if (schema && typeof schema === `object` && `~standard` in schema) {
1245+
if (schema && `~standard` in (schema as {})) {
12461246
return schema as StandardSchema<T>
12471247
}
12481248

packages/db/tests/collection.test.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { describe, expect, expectTypeOf, it, vi } from "vitest"
1+
import { type } from "arktype"
22
import mitt from "mitt"
3+
import { describe, expect, expectTypeOf, it, vi } from "vitest"
34
import { z } from "zod"
45
import { SchemaValidationError, createCollection } from "../src/collection"
56
import { createTransaction } from "../src/transactions"
@@ -956,6 +957,99 @@ describe(`Collection`, () => {
956957
})
957958

958959
describe(`Collection with schema validation`, () => {
960+
it(`should validate data against arktype schema on insert`, () => {
961+
// Create a Zod schema for a user
962+
const userSchema = type({
963+
name: `string > 0`,
964+
age: `number.integer > 0`,
965+
"email?": `string.email`,
966+
})
967+
968+
// Create a collection with the schema
969+
const collection = createCollection<typeof userSchema.infer>({
970+
id: `test`,
971+
getKey: (item) => item.name,
972+
startSync: true,
973+
sync: {
974+
sync: ({ begin, commit }) => {
975+
begin()
976+
commit()
977+
},
978+
},
979+
schema: userSchema,
980+
})
981+
const mutationFn = async () => {}
982+
983+
// Valid data should work
984+
const validUser = {
985+
name: `Alice`,
986+
age: 30,
987+
email: `alice@example.com`,
988+
}
989+
990+
const tx1 = createTransaction({ mutationFn })
991+
tx1.mutate(() => collection.insert(validUser))
992+
993+
// Invalid data should throw SchemaValidationError
994+
const invalidUser = {
995+
name: ``, // Empty name (fails min length)
996+
age: -5, // Negative age (fails positive)
997+
email: `not-an-email`, // Invalid email
998+
}
999+
1000+
try {
1001+
const tx2 = createTransaction({ mutationFn })
1002+
tx2.mutate(() => collection.insert(invalidUser))
1003+
// Should not reach here
1004+
expect(true).toBe(false)
1005+
} catch (error) {
1006+
expect(error).toBeInstanceOf(SchemaValidationError)
1007+
if (error instanceof SchemaValidationError) {
1008+
expect(error.type).toBe(`insert`)
1009+
expect(error.issues.length).toBeGreaterThan(0)
1010+
// Check that we have validation errors for each invalid field
1011+
expect(error.issues.some((issue) => issue.path?.includes(`name`))).toBe(
1012+
true
1013+
)
1014+
expect(error.issues.some((issue) => issue.path?.includes(`age`))).toBe(
1015+
true
1016+
)
1017+
expect(
1018+
error.issues.some((issue) => issue.path?.includes(`email`))
1019+
).toBe(true)
1020+
}
1021+
}
1022+
1023+
// Partial updates should work with valid data
1024+
const tx3 = createTransaction({ mutationFn })
1025+
tx3.mutate(() =>
1026+
collection.update(`Alice`, (draft) => {
1027+
draft.age = 31
1028+
})
1029+
)
1030+
1031+
// Partial updates should fail with invalid data
1032+
try {
1033+
const tx4 = createTransaction({ mutationFn })
1034+
tx4.mutate(() =>
1035+
collection.update(`Alice`, (draft) => {
1036+
draft.age = -1
1037+
})
1038+
)
1039+
// Should not reach here
1040+
expect(true).toBe(false)
1041+
} catch (error) {
1042+
expect(error).toBeInstanceOf(SchemaValidationError)
1043+
if (error instanceof SchemaValidationError) {
1044+
expect(error.type).toBe(`update`)
1045+
expect(error.issues.length).toBeGreaterThan(0)
1046+
expect(error.issues.some((issue) => issue.path?.includes(`age`))).toBe(
1047+
true
1048+
)
1049+
}
1050+
}
1051+
})
1052+
9591053
it(`should validate data against schema on insert`, () => {
9601054
// Create a Zod schema for a user
9611055
const userSchema = z.object({

packages/query-db-collection/src/query.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ export function queryCollectionOptions<
265265
throw new Error(`[QueryCollection] queryClient must be provided.`)
266266
}
267267

268-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
269268
if (!getKey) {
270269
throw new Error(`[QueryCollection] getKey must be provided.`)
271270
}

pnpm-lock.yaml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)