Skip to content

Commit 0ff3228

Browse files
DominicGBauerstevensJourneyDominicGBauer
authored
feat: add query builder package (#77)
Co-authored-by: Steven Ontong <steven@journeyapps.com> Co-authored-by: stevensJourney <51082125+stevensJourney@users.noreply.github.com> Co-authored-by: DominicGBauer <dominic@nomanini.com>
1 parent 749dc80 commit 0ff3228

23 files changed

+935
-39
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@powersync/kysely-driver": minor
3+
---
4+
5+
Initial release of Kysely driver

packages/kysely-driver/README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<p align="center">
2+
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/19345049/602bafa0-41ce-4cee-a432-56848c278722"/></a>
3+
</p>
4+
5+
# PowerSync Kysely Driver
6+
7+
[PowerSync](https://powersync.com) is a service and set of SDKs that keeps Postgres databases in sync with on-device SQLite databases.
8+
9+
This package (`packages/kysely-driver`) brings the benefits of an ORM through our maintained [Kysely](https://kysely.dev/) driver to PowerSync.
10+
11+
## Getting started
12+
13+
Setup the PowerSync Database and wrap it with Kysely.
14+
15+
```js
16+
import { wrapPowerSyncWithKysely } from '@powersync/kysely-driver';
17+
import { WASQLitePowerSyncDatabaseOpenFactory } from "@journeyapps/powersync-sdk-web";
18+
import { appSchema } from "./schema";
19+
import { Database } from "./types";
20+
21+
const factory = new WASQLitePowerSyncDatabaseOpenFactory({
22+
schema: appSchema,
23+
dbFilename: "test.sqlite",
24+
});
25+
26+
export const powerSyncDb = factory.getInstance();
27+
28+
export const db = wrapPowerSyncWithKysely<Database>(powerSyncDb)
29+
```
30+
31+
For more information on Kysely typing [here](https://kysely.dev/docs/getting-started#types).
32+
33+
Now you are able to use Kysely queries:
34+
35+
### Select
36+
37+
* In Kysely
38+
39+
```js
40+
const result = await db.selectFrom('users').selectAll().execute();
41+
42+
// {id: '1', name: 'user1', id: '2', name: 'user2'}
43+
```
44+
45+
* In PowerSync
46+
47+
```js
48+
const result = await powerSyncDb.getAll('SELECT * from users')
49+
50+
// {id: '1', name: 'user1', id: '2', name: 'user2'}
51+
```
52+
53+
### Insert
54+
55+
* In Kysely
56+
57+
```js
58+
await db.insertInto('users').values({ id: '1', name: 'John' }).execute();
59+
const result = await db.selectFrom('users').selectAll().execute();
60+
61+
// {id: '1', name: 'John'}
62+
```
63+
64+
* In PowerSync
65+
66+
```js
67+
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(1, ?)', ['John']);
68+
const result = await powerSyncDb.getAll('SELECT * from users')
69+
70+
// {id: '1', name: 'John'}
71+
```
72+
73+
### Delete
74+
75+
* In Kysely
76+
77+
```js
78+
await db.insertInto('users').values({ id: '2', name: 'Ben' }).execute();
79+
await db.deleteFrom('users').where('name', '=', 'Ben').execute();
80+
const result = await db.selectFrom('users').selectAll().execute();
81+
82+
// { }
83+
```
84+
85+
* In PowerSync
86+
87+
```js
88+
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(2, ?)', ['Ben']);
89+
await powerSyncDb.execute(`DELETE FROM users WHERE name = ?`, ['Ben']);
90+
const result = await powerSyncDb.getAll('SELECT * from users')
91+
92+
// { }
93+
```
94+
95+
### Update
96+
97+
* In Kysely
98+
99+
```js
100+
await db.insertInto('users').values({ id: '3', name: 'Lucy' }).execute();
101+
await db.updateTable('users').where('name', '=', 'Lucy').set('name', 'Lucy Smith').execute();
102+
const result = await db.selectFrom('users').select('name').executeTakeFirstOrThrow();
103+
104+
// { id: '3', name: 'Lucy Smith' }
105+
```
106+
107+
* In PowerSync
108+
109+
```js
110+
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(3, ?)', ['Lucy']);
111+
await powerSyncDb.execute("UPDATE users SET name = ? WHERE name = ?", ['Lucy Smith', 'Lucy']);
112+
const result = await powerSyncDb.getAll('SELECT * from users')
113+
114+
// { id: '3', name: 'Lucy Smith' }
115+
```
116+
117+
### Transaction
118+
119+
* In Kysely
120+
121+
```js
122+
await db.transaction().execute(async (transaction) => {
123+
await transaction.insertInto('users').values({ id: '4', name: 'James' }).execute();
124+
await transaction.updateTable('users').where('name', '=', 'James').set('name', 'James Smith').execute();
125+
});
126+
const result = await db.selectFrom('users').select('name').executeTakeFirstOrThrow();
127+
128+
// { id: '4', name: 'James Smith' }
129+
```
130+
131+
* In PowerSync
132+
133+
```js
134+
await powerSyncDb.writeTransaction((transaction) => {
135+
await transaction.execute('INSERT INTO users (id, name) VALUES(4, ?)', ['James']);
136+
await transaction.execute("UPDATE users SET name = ? WHERE name = ?", ['James Smith', 'James']);
137+
})
138+
const result = await powerSyncDb.getAll('SELECT * from users')
139+
140+
// { id: '4', name: 'James Smith' }
141+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@powersync/kysely-driver",
3+
"version": "0.0.0",
4+
"description": "Kysely driver for PowerSync",
5+
"main": "lib/src/index.js",
6+
"types": "lib/src/index.d.ts",
7+
"author": "JOURNEYAPPS",
8+
"license": "Apache-2.0",
9+
"files": [
10+
"lib"
11+
],
12+
"repository": "https://github.com/powersync-ja/powersync-js",
13+
"bugs": {
14+
"url": "https://github.com/powersync-ja/powersync-js/issues"
15+
},
16+
"publishConfig": {
17+
"registry": "https://registry.npmjs.org/",
18+
"access": "public"
19+
},
20+
"homepage": "https://docs.powersync.com",
21+
"scripts": {
22+
"build": "tsc --build",
23+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
24+
"watch": "tsc --build -w",
25+
"test": "pnpm build && vitest"
26+
},
27+
"dependencies": {
28+
"@journeyapps/powersync-sdk-common": "workspace:*",
29+
"kysely": "^0.27.2"
30+
},
31+
"devDependencies": {
32+
"@journeyapps/powersync-sdk-web": "workspace:*",
33+
"@journeyapps/wa-sqlite": "^0.1.1",
34+
"@types/node": "^20.11.17",
35+
"@vitest/browser": "^1.3.1",
36+
"ts-loader": "^9.5.1",
37+
"ts-node": "^10.9.2",
38+
"typescript": "^5.3.3",
39+
"vite": "^5.1.1",
40+
"vite-plugin-top-level-await": "^1.4.1",
41+
"vite-plugin-wasm": "^3.3.0",
42+
"vitest": "^1.3.0",
43+
"webdriverio": "^8.32.3"
44+
}
45+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { wrapPowerSyncWithKysely } from './sqlite/db';
2+
import {
3+
type ColumnType,
4+
type Insertable,
5+
type Selectable,
6+
type Updateable,
7+
type JSONColumnType,
8+
type KyselyConfig,
9+
type Kysely,
10+
sql
11+
} from 'kysely';
12+
13+
export {
14+
ColumnType,
15+
Insertable,
16+
Selectable,
17+
Updateable,
18+
JSONColumnType,
19+
KyselyConfig,
20+
sql,
21+
Kysely,
22+
wrapPowerSyncWithKysely
23+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PowerSyncDialect } from './sqlite-dialect';
2+
import { Kysely, type KyselyConfig } from 'kysely';
3+
import { type AbstractPowerSyncDatabase } from '@journeyapps/powersync-sdk-common';
4+
5+
export const wrapPowerSyncWithKysely = <T>(db: AbstractPowerSyncDatabase, options?: KyselyConfig) => {
6+
return new Kysely<T>({
7+
dialect: new PowerSyncDialect({
8+
db
9+
}),
10+
...options
11+
});
12+
};
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { type AbstractPowerSyncDatabase, type Transaction } from '@journeyapps/powersync-sdk-common';
2+
import { CompiledQuery, DatabaseConnection, QueryResult } from 'kysely';
3+
4+
/**
5+
* Represent a Kysely connection to the PowerSync database.
6+
*
7+
* The actual locks are acquired on-demand when a transaction is started.
8+
*
9+
* When not using transactions, we rely on the automatic locks.
10+
*
11+
* This allows us to bypass write locks when doing pure select queries outside a transaction.
12+
*/
13+
export class PowerSyncConnection implements DatabaseConnection {
14+
readonly #db: AbstractPowerSyncDatabase;
15+
#completeTransaction: (() => void) | null;
16+
#tx: Transaction | null;
17+
18+
constructor(db: AbstractPowerSyncDatabase) {
19+
this.#db = db;
20+
this.#tx = null;
21+
this.#completeTransaction = null;
22+
}
23+
24+
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
25+
const { sql, parameters, query } = compiledQuery;
26+
27+
const context = this.#tx ?? this.#db;
28+
29+
if (query.kind === 'SelectQueryNode') {
30+
// Optimizaton: use getAll() instead of execute() if it's a select query
31+
const rows = await context.getAll(sql, parameters as unknown[]);
32+
return {
33+
rows: rows as O[]
34+
};
35+
}
36+
37+
const result = await context.execute(sql, parameters as unknown[]);
38+
39+
return {
40+
insertId: result.insertId ? BigInt(result.insertId!) : undefined,
41+
numAffectedRows: BigInt(result.rowsAffected),
42+
rows: result.rows?._array ?? []
43+
};
44+
}
45+
46+
async *streamQuery<R>(compiledQuery: CompiledQuery): AsyncIterableIterator<QueryResult<R>> {
47+
// Not actually streamed
48+
const results = await this.executeQuery<R>(compiledQuery);
49+
yield {
50+
rows: results.rows
51+
};
52+
}
53+
54+
async beginTransaction(): Promise<void> {
55+
// TODO: Check if there is already an active transaction?
56+
57+
/**
58+
* Returns a promise which resolves once a transaction has been started.
59+
* Rejects if any errors occur in obtaining the lock.
60+
*/
61+
return new Promise<void>((resolve, reject) => {
62+
/**
63+
* Starts a transaction, resolves the `beginTransaction` promise
64+
* once it's started. The transaction waits until the `this.#release`
65+
* callback is executed.
66+
*/
67+
this.#db
68+
.writeTransaction(async (tx) => {
69+
// Set the current active transaction
70+
this.#tx = tx;
71+
72+
/**
73+
* Wait for this transaction to be completed
74+
* Rejecting would cause any uncommitted changes to be
75+
* rolled back.
76+
*/
77+
const transactionCompleted = new Promise<void>((resolve) => {
78+
this.#completeTransaction = resolve;
79+
});
80+
81+
// Allow this transaction to be used externally
82+
resolve();
83+
84+
await transactionCompleted;
85+
})
86+
.catch(reject);
87+
});
88+
}
89+
90+
async commitTransaction(): Promise<void> {
91+
if (!this.#tx) {
92+
throw new Error('Transaction is not defined');
93+
}
94+
95+
await this.#tx.commit();
96+
this.releaseTransaction();
97+
}
98+
99+
async rollbackTransaction(): Promise<void> {
100+
if (!this.#tx) {
101+
throw new Error('Transaction is not defined');
102+
}
103+
104+
await this.#tx.rollback();
105+
this.releaseTransaction();
106+
}
107+
108+
async releaseConnection(): Promise<void> {
109+
this.#db.close();
110+
}
111+
112+
private releaseTransaction() {
113+
if (!this.#completeTransaction) {
114+
throw new Error(`Not able to release transaction`);
115+
}
116+
117+
this.#completeTransaction();
118+
this.#completeTransaction = null;
119+
this.#tx = null;
120+
}
121+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
DatabaseIntrospector,
3+
Dialect,
4+
DialectAdapter,
5+
Driver,
6+
Kysely,
7+
QueryCompiler,
8+
SqliteAdapter,
9+
SqliteIntrospector,
10+
SqliteQueryCompiler
11+
} from 'kysely';
12+
import { PowerSyncDialectConfig, PowerSyncDriver } from './sqlite-driver';
13+
14+
export class PowerSyncDialect implements Dialect {
15+
readonly #config: PowerSyncDialectConfig;
16+
17+
constructor(config: PowerSyncDialectConfig) {
18+
this.#config = Object.freeze({ ...config });
19+
}
20+
21+
createDriver(): Driver {
22+
return new PowerSyncDriver(this.#config);
23+
}
24+
25+
createQueryCompiler(): QueryCompiler {
26+
return new SqliteQueryCompiler();
27+
}
28+
29+
createAdapter(): DialectAdapter {
30+
return new SqliteAdapter();
31+
}
32+
33+
createIntrospector(db: Kysely<unknown>): DatabaseIntrospector {
34+
return new SqliteIntrospector(db);
35+
}
36+
}

0 commit comments

Comments
 (0)