Skip to content

Commit 77a9ed2

Browse files
authored
feat: added watch() to Drizzle and Kysely integrations (#414)
1 parent a919243 commit 77a9ed2

File tree

11 files changed

+702
-38
lines changed

11 files changed

+702
-38
lines changed

.changeset/calm-baboons-worry.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/drizzle-driver': minor
3+
---
4+
5+
Added `watch()` function to Drizzle wrapper to support watched queries. This function invokes `execute()` on the Drizzle query which improves support for complex queries such as those which are relational.

.changeset/empty-chefs-smell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/kysely-driver': minor
3+
---
4+
5+
Added `watch()` function to Kysely wrapper to support watched queries. This function invokes `execute()` on the Kysely query which improves support for complex queries and Kysely plugins.

.changeset/gold-beers-smoke.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/common': minor
3+
---
4+
5+
Added `compilableQueryWatch()` utility function which allows any compilable query to be watched.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { CompilableQuery } from './../types/types.js';
2+
import { AbstractPowerSyncDatabase, SQLWatchOptions } from './AbstractPowerSyncDatabase.js';
3+
import { runOnSchemaChange } from './runOnSchemaChange.js';
4+
5+
export interface CompilableQueryWatchHandler<T> {
6+
onResult: (results: T[]) => void;
7+
onError?: (error: Error) => void;
8+
}
9+
10+
export function compilableQueryWatch<T>(
11+
db: AbstractPowerSyncDatabase,
12+
query: CompilableQuery<T>,
13+
handler: CompilableQueryWatchHandler<T>,
14+
options?: SQLWatchOptions
15+
): void {
16+
const { onResult, onError = (e: Error) => {} } = handler ?? {};
17+
if (!onResult) {
18+
throw new Error('onResult is required');
19+
}
20+
21+
const watchQuery = async (abortSignal: AbortSignal) => {
22+
try {
23+
const toSql = query.compile();
24+
const resolvedTables = await db.resolveTables(toSql.sql, toSql.parameters as [], options);
25+
26+
// Fetch initial data
27+
const result = await query.execute();
28+
onResult(result);
29+
30+
db.onChangeWithCallback(
31+
{
32+
onChange: async () => {
33+
try {
34+
const result = await query.execute();
35+
onResult(result);
36+
} catch (error: any) {
37+
onError(error);
38+
}
39+
},
40+
onError
41+
},
42+
{
43+
...(options ?? {}),
44+
tables: resolvedTables,
45+
// Override the abort signal since we intercept it
46+
signal: abortSignal
47+
}
48+
);
49+
} catch (error: any) {
50+
onError(error);
51+
}
52+
};
53+
54+
runOnSchemaChange(watchQuery, db, options);
55+
}

packages/common/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './client/connection/PowerSyncBackendConnector.js';
55
export * from './client/connection/PowerSyncCredentials.js';
66
export * from './client/sync/bucket/BucketStorageAdapter.js';
77
export { runOnSchemaChange } from './client/runOnSchemaChange.js';
8+
export { CompilableQueryWatchHandler, compilableQueryWatch } from './client/compilableQueryWatch.js';
89
export { UpdateType, CrudEntry, OpId } from './client/sync/bucket/CrudEntry.js';
910
export * from './client/sync/bucket/SqliteBucketStorage.js';
1011
export * from './client/sync/bucket/CrudBatch.js';

packages/drizzle-driver/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { wrapPowerSyncWithDrizzle, type PowerSyncSQLiteDatabase } from './sqlite/db';
1+
import { wrapPowerSyncWithDrizzle, type DrizzleQuery, type PowerSyncSQLiteDatabase } from './sqlite/db';
22
import { toCompilableQuery } from './utils/compilableQuery';
33

4-
export { wrapPowerSyncWithDrizzle, toCompilableQuery, PowerSyncSQLiteDatabase };
4+
export { wrapPowerSyncWithDrizzle, toCompilableQuery, DrizzleQuery, PowerSyncSQLiteDatabase };
+52-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { AbstractPowerSyncDatabase, QueryResult } from '@powersync/common';
1+
import {
2+
AbstractPowerSyncDatabase,
3+
compilableQueryWatch,
4+
CompilableQueryWatchHandler,
5+
QueryResult,
6+
SQLWatchOptions
7+
} from '@powersync/common';
8+
import { Query } from 'drizzle-orm';
29
import { DefaultLogger } from 'drizzle-orm/logger';
310
import {
411
createTableRelationsHelpers,
@@ -11,42 +18,60 @@ import { SQLiteTransaction } from 'drizzle-orm/sqlite-core';
1118
import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core/db';
1219
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core/dialect';
1320
import type { DrizzleConfig } from 'drizzle-orm/utils';
21+
import { toCompilableQuery } from './../utils/compilableQuery';
1422
import { PowerSyncSQLiteSession, PowerSyncSQLiteTransactionConfig } from './sqlite-session';
1523

16-
export interface PowerSyncSQLiteDatabase<TSchema extends Record<string, unknown> = Record<string, never>>
17-
extends BaseSQLiteDatabase<'async', QueryResult, TSchema> {
18-
transaction<T>(
24+
export type DrizzleQuery<T> = { toSQL(): Query; execute(): Promise<T> };
25+
26+
export class PowerSyncSQLiteDatabase<
27+
TSchema extends Record<string, unknown> = Record<string, never>
28+
> extends BaseSQLiteDatabase<'async', QueryResult, TSchema> {
29+
private db: AbstractPowerSyncDatabase;
30+
31+
constructor(db: AbstractPowerSyncDatabase, config: DrizzleConfig<TSchema> = {}) {
32+
const dialect = new SQLiteAsyncDialect({ casing: config.casing });
33+
let logger;
34+
if (config.logger === true) {
35+
logger = new DefaultLogger();
36+
} else if (config.logger !== false) {
37+
logger = config.logger;
38+
}
39+
40+
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
41+
if (config.schema) {
42+
const tablesConfig = extractTablesRelationalConfig(config.schema, createTableRelationsHelpers);
43+
schema = {
44+
fullSchema: config.schema,
45+
schema: tablesConfig.tables,
46+
tableNamesMap: tablesConfig.tableNamesMap
47+
};
48+
}
49+
50+
const session = new PowerSyncSQLiteSession(db, dialect, schema, {
51+
logger
52+
});
53+
54+
super('async', dialect, session as any, schema as any);
55+
this.db = db;
56+
}
57+
58+
override transaction<T>(
1959
transaction: (
2060
tx: SQLiteTransaction<'async', QueryResult, TSchema, ExtractTablesWithRelations<TSchema>>
2161
) => Promise<T>,
2262
config?: PowerSyncSQLiteTransactionConfig
23-
): Promise<T>;
63+
): Promise<T> {
64+
return super.transaction(transaction, config);
65+
}
66+
67+
watch<T>(query: DrizzleQuery<T>, handler: CompilableQueryWatchHandler<T>, options?: SQLWatchOptions): void {
68+
compilableQueryWatch(this.db, toCompilableQuery(query), handler, options);
69+
}
2470
}
2571

2672
export function wrapPowerSyncWithDrizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
2773
db: AbstractPowerSyncDatabase,
2874
config: DrizzleConfig<TSchema> = {}
2975
): PowerSyncSQLiteDatabase<TSchema> {
30-
const dialect = new SQLiteAsyncDialect({casing: config.casing});
31-
let logger;
32-
if (config.logger === true) {
33-
logger = new DefaultLogger();
34-
} else if (config.logger !== false) {
35-
logger = config.logger;
36-
}
37-
38-
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
39-
if (config.schema) {
40-
const tablesConfig = extractTablesRelationalConfig(config.schema, createTableRelationsHelpers);
41-
schema = {
42-
fullSchema: config.schema,
43-
schema: tablesConfig.tables,
44-
tableNamesMap: tablesConfig.tableNamesMap
45-
};
46-
}
47-
48-
const session = new PowerSyncSQLiteSession(db, dialect, schema, {
49-
logger
50-
});
51-
return new BaseSQLiteDatabase('async', dialect, session, schema) as PowerSyncSQLiteDatabase<TSchema>;
76+
return new PowerSyncSQLiteDatabase<TSchema>(db, config);
5277
}

0 commit comments

Comments
 (0)