Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Electron support #1859

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs-website/docs/docs/Installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,26 @@ You only need this if you want to use WatermelonDB in NodeJS with SQLite (e.g. f

---

## Electron (SQLite) setup

You only need this if you want to use WatermelonDB in Electron with SQLite.

1. Install [better-sqlite3](https://github.com/JoshuaWise/better-sqlite3) peer dependency

```sh
yarn add --dev better-sqlite3

# (or with npm:)
npm install -D better-sqlite3
```
2. Run electron rebuild on sqlite3. This step is necessary to ensure the sqlite native build (.node) is compatible with Electron's version of Node.js. If you're using Electron Forge, this step will be performed for you during build **but not development**.

```sh
npx electron-rebuild -f -w -t dev better-sqlite3
```

---

## Next steps

➡️ After Watermelon is installed, [**set it up**](./Setup.md)
79 changes: 77 additions & 2 deletions docs-website/docs/docs/Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ hide_title: true

Make sure you [installed Watermelon](./Installation.mdx) before proceeding.

## Common

Create `model/schema.js` in your project. You'll need it for [the next step](./Schema.md).

```js
Expand All @@ -32,7 +34,9 @@ export default schemaMigrations({
})
```

Now, in your `index.native.js`:
## React Native and Node.js (SQLite)

Now, in your `index.native.js` (React Native) or `index.js` (Node.js):

```js
import { Platform } from 'react-native'
Expand Down Expand Up @@ -68,7 +72,78 @@ const database = new Database({
})
```

The above will work on React Native (iOS/Android) and NodeJS. For the web, instead of `SQLiteAdapter` use `LokiJSAdapter`:
## Electron (SQLite)
Electron requires a little extra set up since we have to use IPC between our renderer and main processes to execute queries and return the response. However, if you'd like to use LokiJS instead of SQLite you can skip this section and go to the Web section below.

Let's set things up on the renderer side first.

```js
import RemoteAdapter from '@nozbe/watermelondb/adapters/remote'
import { Database } from '@nozbe/watermelondb'
import schema from './model/schema'
import migrations from './model/migrations'

const electronAPI = window.electronAPI

const adapter = new RemoteAdapter({
schema,
migrations,
handler: (op, args, callback) => {
electronAPI.handleAdapter({op, args}).then((res) => callback(res[0]))
}
})

const database = new Database({
adapter,
modelClasses: [
// Post, // ⬅️ You'll add Models to Watermelon here
],
})

export default database
```
Whenever Watermelon needs to interact with the database, it will do so through the remote adapter which in turn sends queries to sqlite over the Electron IPC bridge via the handler callback.

Now that our renderer is all set, let's set up the other side in `main.js`:
```js
import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite";
import schema from './model/schema';
import migrations from './model/migrations';

mainWindow.webContents.on('did-finish-load', () => {
const adapter = new SQLiteAdapter({
schema,
migrations
})

async function handleAdapter(_, dispatch) {
return new Promise((res) => {
const { op, args } = dispatch;
adapter[op](...args, (...resp) => res(resp))
})
}

ipcMain.removeHandler('db:handle')
ipcMain.handle('db:handle', handleAdapter)
})
```

Above, we've set up the adapter that will actually interact with our SQLite database. When the renderer sends the `db:handle` event, the handleAdapter callback function will be invoked with the required arguments. It will then return a promise with the data the renderer wants.

Note that we're re-instantiating the adapter inside a `'did-finish-load'` event handler. This ensures the cache maintained in the renderer and main is kept consistent during reloads (manually or due to HMR).

We still need to expose this event to our renderer so in `preload.js` we'll add the following:
```js
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
handleAdapter: (...args) => ipcRenderer.invoke('db:handle', ...args)
})
```

## Web (LokiJS)

This set up is suitable for web apps.

```js
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
Expand Down
8 changes: 4 additions & 4 deletions examples/typescript/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# yarn lockfile v1


"@babel/runtime@7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
"@babel/runtime@7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
dependencies:
regenerator-runtime "^0.14.0"

Expand Down
99 changes: 99 additions & 0 deletions src/adapters/remote/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// @flow

import { type ResultCallback } from '../../utils/fp/Result'

import type { RecordId } from '../../Model'
import type { SerializedQuery } from '../../Query'
import type { TableName, AppSchema } from '../../Schema'
import type { SchemaMigrations } from '../../Schema/migrations'
import type {
DatabaseAdapter,
CachedQueryResult,
CachedFindResult,
BatchOperation,
UnsafeExecuteOperations,
} from '../type'

import type {
RemoteHandler,
RemoteAdapterOptions
} from './type'

export default class RemoteAdapter implements DatabaseAdapter {
schema: AppSchema
dbName: string
migrations: ?SchemaMigrations
handler: RemoteHandler

constructor(options: RemoteAdapterOptions) {
const { schema, migrations, handler } = options;

this.schema = schema
this.migrations = migrations
this.handler = handler
}

find(table: TableName<any>, id: RecordId, callback: ResultCallback<CachedFindResult>) {
this.handler('find', [table, id], callback)
}

query(query: SerializedQuery, callback: ResultCallback<CachedQueryResult>) {
this.handler('query', [query], callback)
}

queryIds(query: SerializedQuery, callback: ResultCallback<RecordId[]>) {
this.handler('queryIds', [query], callback)
}

unsafeQueryRaw(query: SerializedQuery, callback: ResultCallback<any[]>) {
this.handler('unsafeQueryRaw', [query], callback)
}

count(query: SerializedQuery, callback: ResultCallback<number>) {
this.handler('count', [query], callback)
}

batch(operations: BatchOperation[], callback: ResultCallback<void>) {
this.handler('batch', [operations], callback)
}

getDeletedRecords(tableName: TableName<any>, callback: ResultCallback<RecordId[]>) {
this.handler('getDeletedRecords', [tableName], callback)
}

destroyDeletedRecords(
tableName: TableName<any>,
recordIds: RecordId[],
callback: ResultCallback<void>,
) {
this.handler('destroyDeletedRecords', [tableName, recordIds], callback)
}

unsafeLoadFromSync(jsonId: number, callback: ResultCallback<any>) {
this.handler('unsafeLoadFromSync', [jsonId], callback)
}

provideSyncJson(id: number, syncPullResultJson: string, callback: ResultCallback<void>) {
this.handler('provideSyncJson', [id, syncPullResultJson], callback)
}

unsafeResetDatabase(callback: ResultCallback<void>) {
this.handler('unsafeResetDatabase', [], callback)
}

unsafeExecute(work: UnsafeExecuteOperations, callback: ResultCallback<void>) {
this.handler('unsafeExecute', [work], callback)
}

getLocal(key: string, callback: ResultCallback<?string>) {
this.handler('getLocal', [key], callback)
}

setLocal(key: string, value: string, callback: ResultCallback<void>) {
this.handler('setLocal', [key, value], callback)
}

removeLocal(key: string, callback: ResultCallback<void>) {
this.handler('removeLocal', [key], callback)
}
}
11 changes: 11 additions & 0 deletions src/adapters/remote/type.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AppSchema } from "../../Schema";
import { SchemaMigrations } from "../../Schema/migrations";
import { ResultCallback } from "../../utils/fp/Result";

export type RemoteHandler = (op: string, args: any[], callback: ResultCallback<any>) => void;

export type RemoteAdapterOptions = {
schema: AppSchema,
migrations?: SchemaMigrations,
handler: RemoteHandler,
}
13 changes: 13 additions & 0 deletions src/adapters/remote/type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @flow

import { type ResultCallback } from '../../utils/fp/Result'
import type { AppSchema } from '../../Schema'
import type { SchemaMigrations } from '../../Schema/migrations'

export type RemoteHandler = (op: string, args: any[], callback: ResultCallback<any>) => void;

export type RemoteAdapterOptions = {
schema: AppSchema,
migrations?: SchemaMigrations,
handler: RemoteHandler,
}
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10102,4 +10102,4 @@ yocto-queue@^0.1.0:
yoctocolors-cjs@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242"
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
Loading