Skip to content

Commit 7030117

Browse files
Update doc comments. Code cleanup.
1 parent fb45f02 commit 7030117

File tree

8 files changed

+93
-174
lines changed

8 files changed

+93
-174
lines changed

docs/collections/powersync-collection.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ const APP_SCHEMA = new Schema({
4444
}),
4545
})
4646

47-
type Document = (typeof APP_SCHEMA)["types"]["documents"]
48-
4947
// Initialize PowerSync database
5048
const db = new PowerSyncDatabase({
5149
database: {
@@ -87,7 +85,7 @@ There are two ways to create a collection: using type inference or using schema
8785

8886
#### Option 1: Using Table Type Inference
8987

90-
The collection types are automatically inferred from the PowerSync Schema Table definition. The table is used to construct a default StandardSchema validator which is used internally to validate collection data and operations.
88+
The collection types are automatically inferred from the PowerSync Schema Table definition. The table is used to construct a default StandardSchema validator which is used internally to validate collection operations.
9189

9290
```ts
9391
import { createCollection } from "@tanstack/react-db"
@@ -103,7 +101,7 @@ const documentsCollection = createCollection(
103101

104102
#### Option 2: Using Advanced Schema Validation
105103

106-
Additional validations can be performed by supplying a Standard Schema.
104+
Additional validations can be performed by supplying a Standard Schema. The typing of the validator is constrained to match the typing of the SQLite table.
107105

108106
```ts
109107
import { createCollection } from "@tanstack/react-db"
@@ -128,8 +126,6 @@ const documentsCollection = createCollection(
128126
)
129127
```
130128

131-
With schema validation, the collection will validate all inputs at runtime to ensure they match the PowerSync schema types. This provides an extra layer of type safety beyond TypeScript's compile-time checks.
132-
133129
## Features
134130

135131
### Offline-First

packages/powersync-db-collection/src/PowerSyncTransactor.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { sanitizeSQL } from "@powersync/common"
22
import DebugModule from "debug"
33
import { PendingOperationStore } from "./PendingOperationStore"
44
import { asPowerSyncRecord, mapOperationToPowerSync } from "./helpers"
5-
import type { AbstractPowerSyncDatabase, LockContext } from "@powersync/common"
6-
import type { PendingMutation, Transaction } from "@tanstack/db"
7-
import type { PendingOperation } from "./PendingOperationStore"
85
import type { EnhancedPowerSyncCollectionConfig } from "./definitions"
6+
import type { PendingOperation } from "./PendingOperationStore"
7+
import type { PendingMutation, Transaction } from "@tanstack/db"
8+
import type { AbstractPowerSyncDatabase, LockContext } from "@powersync/common"
99

1010
const debug = DebugModule.debug(`ts/db:powersync`)
1111

@@ -24,7 +24,7 @@ export type TransactorOptions = {
2424
* const collection = createCollection(
2525
* powerSyncCollectionOptions<Document>({
2626
* database: db,
27-
* tableName: "documents",
27+
* table: APP_SCHEMA.props.documents,
2828
* })
2929
* )
3030
*
@@ -239,15 +239,17 @@ export class PowerSyncTransactor<T extends object = Record<string, unknown>> {
239239
waitForCompletion: boolean,
240240
handler: (tableName: string, mutation: PendingMutation<T>) => Promise<void>
241241
): Promise<PendingOperation | null> {
242-
const { tableName, trackedTableName } = (
243-
mutation.collection.config as EnhancedPowerSyncCollectionConfig
244-
).utils.getMeta()
245-
246-
if (!tableName) {
242+
if (
243+
typeof (mutation.collection.config as any).utils?.getMeta != `function`
244+
) {
247245
throw new Error(`Could not get tableName from mutation's collection config.
248246
The provided mutation might not have originated from PowerSync.`)
249247
}
250248

249+
const { tableName, trackedTableName } = (
250+
mutation.collection.config as unknown as EnhancedPowerSyncCollectionConfig
251+
).utils.getMeta()
252+
251253
await handler(sanitizeSQL`${tableName}`, mutation)
252254

253255
if (!waitForCompletion) {

packages/powersync-db-collection/src/definitions.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ExtractedTable } from "./helpers"
55

66
/**
77
* Configuration interface for PowerSync collection options
8-
* @template T - The type of items in the collection
8+
* @template TTable - The PowerSync table schema definition
99
* @template TSchema - The schema type for validation
1010
*/
1111
/**
@@ -19,8 +19,6 @@ import type { ExtractedTable } from "./helpers"
1919
* }),
2020
* })
2121
*
22-
* type Document = (typeof APP_SCHEMA)["types"]["documents"]
23-
*
2422
* const db = new PowerSyncDatabase({
2523
* database: {
2624
* dbFilename: "test.sqlite",
@@ -29,9 +27,9 @@ import type { ExtractedTable } from "./helpers"
2927
* })
3028
*
3129
* const collection = createCollection(
32-
* powerSyncCollectionOptions<Document>({
30+
* powerSyncCollectionOptions({
3331
* database: db,
34-
* tableName: "documents",
32+
* table: APP_SCHEMA.props.documents
3533
* })
3634
* )
3735
* ```
@@ -62,6 +60,9 @@ export type PowerSyncCollectionConfig<
6260
syncBatchSize?: number
6361
}
6462

63+
/**
64+
* Meta data for the PowerSync Collection
65+
*/
6566
export type PowerSyncCollectionMeta = {
6667
/**
6768
* The SQLite table representing the collection.
@@ -73,6 +74,9 @@ export type PowerSyncCollectionMeta = {
7374
trackedTableName: string
7475
}
7576

77+
/**
78+
* A CollectionConfig which includes utilities for PowerSync
79+
*/
7680
export type EnhancedPowerSyncCollectionConfig<
7781
TTable extends Table = Table,
7882
TSchema extends StandardSchemaV1 = never,
@@ -82,6 +86,9 @@ export type EnhancedPowerSyncCollectionConfig<
8286
schema?: TSchema
8387
}
8488

89+
/**
90+
* Collection level utilities for PowerSync
91+
*/
8592
export type PowerSyncCollectionUtils = {
8693
getMeta: () => PowerSyncCollectionMeta
8794
}

packages/powersync-db-collection/src/helpers.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@ export type PowerSyncRecord = {
99
[key: string]: unknown
1010
}
1111

12+
/**
13+
* Utility type: If T includes null, add undefined.
14+
* PowerSync records typically are typed as `string | null`, where insert
15+
* and update operations also allow not specifying a value at all (optional)
16+
* */
17+
type WithUndefinedIfNull<T> = null extends T ? T | undefined : T
18+
type OptionalIfUndefined<T> = {
19+
[K in keyof T as undefined extends T[K] ? K : never]?: T[K]
20+
} & {
21+
[K in keyof T as undefined extends T[K] ? never : K]: T[K]
22+
}
23+
1224
/**
1325
* Utility type that extracts the typed structure of a table based on its column definitions.
1426
* Maps each column to its corresponding TypeScript type using ExtractColumnValueType.
1527
*
16-
* @template Columns - The ColumnsType definition containing column configurations
28+
* @template TTable - The PowerSync table definition
1729
* @example
1830
* ```typescript
1931
* const table = new Table({
@@ -24,11 +36,11 @@ export type PowerSyncRecord = {
2436
* // Results in: { name: string | null, age: number | null }
2537
* ```
2638
*/
27-
export type ExtractedTable<TTable extends Table> = {
28-
[K in keyof TTable[`columnMap`]]: ExtractColumnValueType<
29-
TTable[`columnMap`][K]
39+
export type ExtractedTable<TTable extends Table> = OptionalIfUndefined<{
40+
[K in keyof TTable[`columnMap`]]: WithUndefinedIfNull<
41+
ExtractColumnValueType<TTable[`columnMap`][K]>
3042
>
31-
} & {
43+
}> & {
3244
id: string
3345
}
3446

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from "./definitions"
22
export * from "./powersync"
33
export * from "./PowerSyncTransactor"
4-
export * from "./schema"
Lines changed: 11 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ColumnType } from "@powersync/common"
2-
import type { ColumnsType, Schema, Table } from "@powersync/common"
2+
import type { Table } from "@powersync/common"
33
import type { StandardSchemaV1 } from "@standard-schema/spec"
44
import type { ExtractedTable } from "./helpers"
55

@@ -8,7 +8,7 @@ import type { ExtractedTable } from "./helpers"
88
* Creates a schema that validates the structure and types of table records
99
* according to the PowerSync table definition.
1010
*
11-
* @template Columns - The ColumnsType definition containing column configurations
11+
* @template TTable - The PowerSync schema typed Table definition
1212
* @param table - The PowerSync Table instance to convert
1313
* @returns A StandardSchemaV1 compatible schema with proper type validation
1414
*
@@ -18,26 +18,17 @@ import type { ExtractedTable } from "./helpers"
1818
* name: column.text,
1919
* age: column.integer
2020
* })
21-
*
22-
* const schema = convertTableToSchema(usersTable)
23-
* // Now you can use this schema with powerSyncCollectionOptions
24-
* const collection = createCollection(
25-
* powerSyncCollectionOptions({
26-
* database: db,
27-
* tableName: "users",
28-
* schema: schema
29-
* })
30-
* )
3121
* ```
3222
*/
33-
export function convertTableToSchema<Columns extends ColumnsType>(
34-
table: Table<Columns>
35-
): StandardSchemaV1<ExtractedTable<Columns>> {
23+
export function convertTableToSchema<TTable extends Table>(
24+
table: TTable
25+
): StandardSchemaV1<ExtractedTable<TTable>> {
26+
type TExtracted = ExtractedTable<TTable>
3627
// Create validate function that checks types according to column definitions
3728
const validate = (
3829
value: unknown
3930
):
40-
| StandardSchemaV1.SuccessResult<ExtractedTable<Columns>>
31+
| StandardSchemaV1.SuccessResult<TExtracted>
4132
| StandardSchemaV1.FailureResult => {
4233
if (typeof value != `object` || value == null) {
4334
return {
@@ -61,7 +52,7 @@ export function convertTableToSchema<Columns extends ColumnsType>(
6152

6253
// Check each column
6354
for (const column of table.columns) {
64-
const val = (value as ExtractedTable<Columns>)[column.name]
55+
const val = (value as TExtracted)[column.name as keyof TExtracted]
6556

6657
if (val == null) {
6758
continue
@@ -92,7 +83,7 @@ export function convertTableToSchema<Columns extends ColumnsType>(
9283
return { issues }
9384
}
9485

95-
return { value: { ...value } as ExtractedTable<Columns> }
86+
return { value: { ...value } as TExtracted }
9687
}
9788

9889
return {
@@ -101,72 +92,9 @@ export function convertTableToSchema<Columns extends ColumnsType>(
10192
vendor: `powersync`,
10293
validate,
10394
types: {
104-
input: {} as ExtractedTable<Columns>,
105-
output: {} as ExtractedTable<Columns>,
95+
input: {} as TExtracted,
96+
output: {} as TExtracted,
10697
},
10798
},
10899
}
109100
}
110-
111-
/**
112-
* Converts an entire PowerSync Schema (containing multiple tables) into a collection of StandardSchemaV1 schemas.
113-
* Each table in the schema is converted to its own StandardSchemaV1 schema while preserving all type information.
114-
*
115-
* @template Tables - A record type mapping table names to their Table definitions
116-
* @param schema - The PowerSync Schema containing multiple table definitions
117-
* @returns An object where each key is a table name and each value is that table's StandardSchemaV1 schema
118-
*
119-
* @example
120-
* ```typescript
121-
* const mySchema = new Schema({
122-
* users: new Table({
123-
* name: column.text,
124-
* age: column.integer
125-
* }),
126-
* posts: new Table({
127-
* title: column.text,
128-
* views: column.integer
129-
* })
130-
* })
131-
*
132-
* const standardizedSchemas = convertSchemaToSpecs(mySchema)
133-
* // Result has type:
134-
* // {
135-
* // users: StandardSchemaV1<{ name: string | null, age: number | null }>,
136-
* // posts: StandardSchemaV1<{ title: string | null, views: number | null }>
137-
* // }
138-
*
139-
* // Can be used with collections:
140-
* const usersCollection = createCollection(
141-
* powerSyncCollectionOptions({
142-
* database: db,
143-
* tableName: "users",
144-
* schema: standardizedSchemas.users
145-
* })
146-
* )
147-
* ```
148-
*/
149-
export function convertPowerSyncSchemaToSpecs<
150-
Tables extends Record<string, Table<ColumnsType>>,
151-
>(
152-
schema: Schema<Tables>
153-
): {
154-
[TableName in keyof Tables]: StandardSchemaV1<
155-
ExtractedTable<Tables[TableName][`columnMap`]>
156-
>
157-
} {
158-
// Create a map to store the standardized schemas
159-
const standardizedSchemas = {} as {
160-
[TableName in keyof Tables]: StandardSchemaV1<
161-
ExtractedTable<Tables[TableName][`columnMap`]>
162-
>
163-
}
164-
165-
// Iterate through each table in the schema
166-
schema.tables.forEach((table) => {
167-
// Convert each table to a StandardSchemaV1 and store it in the result map
168-
;(standardizedSchemas as any)[table.name] = convertTableToSchema(table)
169-
})
170-
171-
return standardizedSchemas
172-
}

packages/powersync-db-collection/tests/powersync.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const APP_SCHEMA = new Schema({
2727
documents: new Table(
2828
{
2929
name: column.text,
30+
author: column.text,
3031
},
3132
{ viewName: `documents` }
3233
),
@@ -110,7 +111,11 @@ describe(`PowerSync Integration`, () => {
110111
const errorMessage = `Name must be at least 3 characters`
111112
const schema = z.object({
112113
id: z.string(),
113-
name: z.string().min(3, { message: errorMessage }).nullable(),
114+
name: z
115+
.string()
116+
.min(3, { message: errorMessage })
117+
.nullable()
118+
.optional(),
114119
})
115120

116121
const collection = createCollection(
@@ -258,12 +263,14 @@ describe(`PowerSync Integration`, () => {
258263
const tx = collection.insert({
259264
id,
260265
name: `new`,
266+
author: `somebody`,
261267
})
262268

263269
// The insert should optimistically update the collection
264270
const newDoc = collection.get(id)
265271
expect(newDoc).toBeDefined()
266272
expect(newDoc!.name).toBe(`new`)
273+
expect(newDoc!.author).toBe(`somebody`)
267274

268275
await tx.isPersisted.promise
269276
// The item should now be present in PowerSync
@@ -276,6 +283,8 @@ describe(`PowerSync Integration`, () => {
276283
const updatedDoc = collection.get(id)
277284
expect(updatedDoc).toBeDefined()
278285
expect(updatedDoc!.name).toBe(`updatedNew`)
286+
// Only the updated field should be updated
287+
expect(updatedDoc!.author).toBe(`somebody`)
279288

280289
await collection.delete(id).isPersisted.promise
281290

@@ -511,7 +520,7 @@ describe(`PowerSync Integration`, () => {
511520
})
512521
})
513522

514-
describe(`Multiple Clients`, async () => {
523+
describe(`Multiple Clients`, () => {
515524
it(`should sync updates between multiple clients`, async () => {
516525
const db = await createDatabase()
517526

@@ -535,7 +544,7 @@ describe(`PowerSync Integration`, () => {
535544
})
536545
})
537546

538-
describe(`Lifecycle`, async () => {
547+
describe(`Lifecycle`, () => {
539548
it(`should cleanup resources`, async () => {
540549
const db = await createDatabase()
541550
const collectionOptions = powerSyncCollectionOptions({

0 commit comments

Comments
 (0)