-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add query builder package (#77)
Co-authored-by: Steven Ontong <[email protected]> Co-authored-by: stevensJourney <[email protected]> Co-authored-by: DominicGBauer <[email protected]>
- Loading branch information
1 parent
749dc80
commit 0ff3228
Showing
23 changed files
with
935 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@powersync/kysely-driver": minor | ||
--- | ||
|
||
Initial release of Kysely driver |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
<p align="center"> | ||
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/19345049/602bafa0-41ce-4cee-a432-56848c278722"/></a> | ||
</p> | ||
|
||
# PowerSync Kysely Driver | ||
|
||
[PowerSync](https://powersync.com) is a service and set of SDKs that keeps Postgres databases in sync with on-device SQLite databases. | ||
|
||
This package (`packages/kysely-driver`) brings the benefits of an ORM through our maintained [Kysely](https://kysely.dev/) driver to PowerSync. | ||
|
||
## Getting started | ||
|
||
Setup the PowerSync Database and wrap it with Kysely. | ||
|
||
```js | ||
import { wrapPowerSyncWithKysely } from '@powersync/kysely-driver'; | ||
import { WASQLitePowerSyncDatabaseOpenFactory } from "@journeyapps/powersync-sdk-web"; | ||
import { appSchema } from "./schema"; | ||
import { Database } from "./types"; | ||
|
||
const factory = new WASQLitePowerSyncDatabaseOpenFactory({ | ||
schema: appSchema, | ||
dbFilename: "test.sqlite", | ||
}); | ||
|
||
export const powerSyncDb = factory.getInstance(); | ||
|
||
export const db = wrapPowerSyncWithKysely<Database>(powerSyncDb) | ||
``` | ||
|
||
For more information on Kysely typing [here](https://kysely.dev/docs/getting-started#types). | ||
|
||
Now you are able to use Kysely queries: | ||
|
||
### Select | ||
|
||
* In Kysely | ||
|
||
```js | ||
const result = await db.selectFrom('users').selectAll().execute(); | ||
|
||
// {id: '1', name: 'user1', id: '2', name: 'user2'} | ||
``` | ||
|
||
* In PowerSync | ||
|
||
```js | ||
const result = await powerSyncDb.getAll('SELECT * from users') | ||
|
||
// {id: '1', name: 'user1', id: '2', name: 'user2'} | ||
``` | ||
|
||
### Insert | ||
|
||
* In Kysely | ||
|
||
```js | ||
await db.insertInto('users').values({ id: '1', name: 'John' }).execute(); | ||
const result = await db.selectFrom('users').selectAll().execute(); | ||
|
||
// {id: '1', name: 'John'} | ||
``` | ||
|
||
* In PowerSync | ||
|
||
```js | ||
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(1, ?)', ['John']); | ||
const result = await powerSyncDb.getAll('SELECT * from users') | ||
|
||
// {id: '1', name: 'John'} | ||
``` | ||
|
||
### Delete | ||
|
||
* In Kysely | ||
|
||
```js | ||
await db.insertInto('users').values({ id: '2', name: 'Ben' }).execute(); | ||
await db.deleteFrom('users').where('name', '=', 'Ben').execute(); | ||
const result = await db.selectFrom('users').selectAll().execute(); | ||
|
||
// { } | ||
``` | ||
|
||
* In PowerSync | ||
|
||
```js | ||
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(2, ?)', ['Ben']); | ||
await powerSyncDb.execute(`DELETE FROM users WHERE name = ?`, ['Ben']); | ||
const result = await powerSyncDb.getAll('SELECT * from users') | ||
|
||
// { } | ||
``` | ||
|
||
### Update | ||
|
||
* In Kysely | ||
|
||
```js | ||
await db.insertInto('users').values({ id: '3', name: 'Lucy' }).execute(); | ||
await db.updateTable('users').where('name', '=', 'Lucy').set('name', 'Lucy Smith').execute(); | ||
const result = await db.selectFrom('users').select('name').executeTakeFirstOrThrow(); | ||
|
||
// { id: '3', name: 'Lucy Smith' } | ||
``` | ||
|
||
* In PowerSync | ||
|
||
```js | ||
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(3, ?)', ['Lucy']); | ||
await powerSyncDb.execute("UPDATE users SET name = ? WHERE name = ?", ['Lucy Smith', 'Lucy']); | ||
const result = await powerSyncDb.getAll('SELECT * from users') | ||
|
||
// { id: '3', name: 'Lucy Smith' } | ||
``` | ||
|
||
### Transaction | ||
|
||
* In Kysely | ||
|
||
```js | ||
await db.transaction().execute(async (transaction) => { | ||
await transaction.insertInto('users').values({ id: '4', name: 'James' }).execute(); | ||
await transaction.updateTable('users').where('name', '=', 'James').set('name', 'James Smith').execute(); | ||
}); | ||
const result = await db.selectFrom('users').select('name').executeTakeFirstOrThrow(); | ||
|
||
// { id: '4', name: 'James Smith' } | ||
``` | ||
|
||
* In PowerSync | ||
|
||
```js | ||
await powerSyncDb.writeTransaction((transaction) => { | ||
await transaction.execute('INSERT INTO users (id, name) VALUES(4, ?)', ['James']); | ||
await transaction.execute("UPDATE users SET name = ? WHERE name = ?", ['James Smith', 'James']); | ||
}) | ||
const result = await powerSyncDb.getAll('SELECT * from users') | ||
|
||
// { id: '4', name: 'James Smith' } | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{ | ||
"name": "@powersync/kysely-driver", | ||
"version": "0.0.0", | ||
"description": "Kysely driver for PowerSync", | ||
"main": "lib/src/index.js", | ||
"types": "lib/src/index.d.ts", | ||
"author": "JOURNEYAPPS", | ||
"license": "Apache-2.0", | ||
"files": [ | ||
"lib" | ||
], | ||
"repository": "https://github.com/powersync-ja/powersync-js", | ||
"bugs": { | ||
"url": "https://github.com/powersync-ja/powersync-js/issues" | ||
}, | ||
"publishConfig": { | ||
"registry": "https://registry.npmjs.org/", | ||
"access": "public" | ||
}, | ||
"homepage": "https://docs.powersync.com", | ||
"scripts": { | ||
"build": "tsc --build", | ||
"clean": "rm -rf dist tsconfig.tsbuildinfo", | ||
"watch": "tsc --build -w", | ||
"test": "pnpm build && vitest" | ||
}, | ||
"dependencies": { | ||
"@journeyapps/powersync-sdk-common": "workspace:*", | ||
"kysely": "^0.27.2" | ||
}, | ||
"devDependencies": { | ||
"@journeyapps/powersync-sdk-web": "workspace:*", | ||
"@journeyapps/wa-sqlite": "^0.1.1", | ||
"@types/node": "^20.11.17", | ||
"@vitest/browser": "^1.3.1", | ||
"ts-loader": "^9.5.1", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.3.3", | ||
"vite": "^5.1.1", | ||
"vite-plugin-top-level-await": "^1.4.1", | ||
"vite-plugin-wasm": "^3.3.0", | ||
"vitest": "^1.3.0", | ||
"webdriverio": "^8.32.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { wrapPowerSyncWithKysely } from './sqlite/db'; | ||
import { | ||
type ColumnType, | ||
type Insertable, | ||
type Selectable, | ||
type Updateable, | ||
type JSONColumnType, | ||
type KyselyConfig, | ||
type Kysely, | ||
sql | ||
} from 'kysely'; | ||
|
||
export { | ||
ColumnType, | ||
Insertable, | ||
Selectable, | ||
Updateable, | ||
JSONColumnType, | ||
KyselyConfig, | ||
sql, | ||
Kysely, | ||
wrapPowerSyncWithKysely | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { PowerSyncDialect } from './sqlite-dialect'; | ||
import { Kysely, type KyselyConfig } from 'kysely'; | ||
import { type AbstractPowerSyncDatabase } from '@journeyapps/powersync-sdk-common'; | ||
|
||
export const wrapPowerSyncWithKysely = <T>(db: AbstractPowerSyncDatabase, options?: KyselyConfig) => { | ||
return new Kysely<T>({ | ||
dialect: new PowerSyncDialect({ | ||
db | ||
}), | ||
...options | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { type AbstractPowerSyncDatabase, type Transaction } from '@journeyapps/powersync-sdk-common'; | ||
import { CompiledQuery, DatabaseConnection, QueryResult } from 'kysely'; | ||
|
||
/** | ||
* Represent a Kysely connection to the PowerSync database. | ||
* | ||
* The actual locks are acquired on-demand when a transaction is started. | ||
* | ||
* When not using transactions, we rely on the automatic locks. | ||
* | ||
* This allows us to bypass write locks when doing pure select queries outside a transaction. | ||
*/ | ||
export class PowerSyncConnection implements DatabaseConnection { | ||
readonly #db: AbstractPowerSyncDatabase; | ||
#completeTransaction: (() => void) | null; | ||
#tx: Transaction | null; | ||
|
||
constructor(db: AbstractPowerSyncDatabase) { | ||
this.#db = db; | ||
this.#tx = null; | ||
this.#completeTransaction = null; | ||
} | ||
|
||
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> { | ||
const { sql, parameters, query } = compiledQuery; | ||
|
||
const context = this.#tx ?? this.#db; | ||
|
||
if (query.kind === 'SelectQueryNode') { | ||
// Optimizaton: use getAll() instead of execute() if it's a select query | ||
const rows = await context.getAll(sql, parameters as unknown[]); | ||
return { | ||
rows: rows as O[] | ||
}; | ||
} | ||
|
||
const result = await context.execute(sql, parameters as unknown[]); | ||
|
||
return { | ||
insertId: result.insertId ? BigInt(result.insertId!) : undefined, | ||
numAffectedRows: BigInt(result.rowsAffected), | ||
rows: result.rows?._array ?? [] | ||
}; | ||
} | ||
|
||
async *streamQuery<R>(compiledQuery: CompiledQuery): AsyncIterableIterator<QueryResult<R>> { | ||
// Not actually streamed | ||
const results = await this.executeQuery<R>(compiledQuery); | ||
yield { | ||
rows: results.rows | ||
}; | ||
} | ||
|
||
async beginTransaction(): Promise<void> { | ||
// TODO: Check if there is already an active transaction? | ||
|
||
/** | ||
* Returns a promise which resolves once a transaction has been started. | ||
* Rejects if any errors occur in obtaining the lock. | ||
*/ | ||
return new Promise<void>((resolve, reject) => { | ||
/** | ||
* Starts a transaction, resolves the `beginTransaction` promise | ||
* once it's started. The transaction waits until the `this.#release` | ||
* callback is executed. | ||
*/ | ||
this.#db | ||
.writeTransaction(async (tx) => { | ||
// Set the current active transaction | ||
this.#tx = tx; | ||
|
||
/** | ||
* Wait for this transaction to be completed | ||
* Rejecting would cause any uncommitted changes to be | ||
* rolled back. | ||
*/ | ||
const transactionCompleted = new Promise<void>((resolve) => { | ||
this.#completeTransaction = resolve; | ||
}); | ||
|
||
// Allow this transaction to be used externally | ||
resolve(); | ||
|
||
await transactionCompleted; | ||
}) | ||
.catch(reject); | ||
}); | ||
} | ||
|
||
async commitTransaction(): Promise<void> { | ||
if (!this.#tx) { | ||
throw new Error('Transaction is not defined'); | ||
} | ||
|
||
await this.#tx.commit(); | ||
this.releaseTransaction(); | ||
} | ||
|
||
async rollbackTransaction(): Promise<void> { | ||
if (!this.#tx) { | ||
throw new Error('Transaction is not defined'); | ||
} | ||
|
||
await this.#tx.rollback(); | ||
this.releaseTransaction(); | ||
} | ||
|
||
async releaseConnection(): Promise<void> { | ||
this.#db.close(); | ||
} | ||
|
||
private releaseTransaction() { | ||
if (!this.#completeTransaction) { | ||
throw new Error(`Not able to release transaction`); | ||
} | ||
|
||
this.#completeTransaction(); | ||
this.#completeTransaction = null; | ||
this.#tx = null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { | ||
DatabaseIntrospector, | ||
Dialect, | ||
DialectAdapter, | ||
Driver, | ||
Kysely, | ||
QueryCompiler, | ||
SqliteAdapter, | ||
SqliteIntrospector, | ||
SqliteQueryCompiler | ||
} from 'kysely'; | ||
import { PowerSyncDialectConfig, PowerSyncDriver } from './sqlite-driver'; | ||
|
||
export class PowerSyncDialect implements Dialect { | ||
readonly #config: PowerSyncDialectConfig; | ||
|
||
constructor(config: PowerSyncDialectConfig) { | ||
this.#config = Object.freeze({ ...config }); | ||
} | ||
|
||
createDriver(): Driver { | ||
return new PowerSyncDriver(this.#config); | ||
} | ||
|
||
createQueryCompiler(): QueryCompiler { | ||
return new SqliteQueryCompiler(); | ||
} | ||
|
||
createAdapter(): DialectAdapter { | ||
return new SqliteAdapter(); | ||
} | ||
|
||
createIntrospector(db: Kysely<unknown>): DatabaseIntrospector { | ||
return new SqliteIntrospector(db); | ||
} | ||
} |
Oops, something went wrong.