diff --git a/docs-website/docs/docs/Installation.mdx b/docs-website/docs/docs/Installation.mdx index 7fb85d359..bbd2ce468 100644 --- a/docs-website/docs/docs/Installation.mdx +++ b/docs-website/docs/docs/Installation.mdx @@ -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) diff --git a/docs-website/docs/docs/Setup.md b/docs-website/docs/docs/Setup.md index 88b940c2e..428346e23 100644 --- a/docs-website/docs/docs/Setup.md +++ b/docs-website/docs/docs/Setup.md @@ -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 @@ -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' @@ -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' diff --git a/examples/typescript/yarn.lock b/examples/typescript/yarn.lock index 5803d3ab8..fb4b4d795 100644 --- a/examples/typescript/yarn.lock +++ b/examples/typescript/yarn.lock @@ -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" diff --git a/src/adapters/remote/index.js b/src/adapters/remote/index.js new file mode 100644 index 000000000..d576f08be --- /dev/null +++ b/src/adapters/remote/index.js @@ -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, id: RecordId, callback: ResultCallback) { + this.handler('find', [table, id], callback) + } + + query(query: SerializedQuery, callback: ResultCallback) { + this.handler('query', [query], callback) + } + + queryIds(query: SerializedQuery, callback: ResultCallback) { + this.handler('queryIds', [query], callback) + } + + unsafeQueryRaw(query: SerializedQuery, callback: ResultCallback) { + this.handler('unsafeQueryRaw', [query], callback) + } + + count(query: SerializedQuery, callback: ResultCallback) { + this.handler('count', [query], callback) + } + + batch(operations: BatchOperation[], callback: ResultCallback) { + this.handler('batch', [operations], callback) + } + + getDeletedRecords(tableName: TableName, callback: ResultCallback) { + this.handler('getDeletedRecords', [tableName], callback) + } + + destroyDeletedRecords( + tableName: TableName, + recordIds: RecordId[], + callback: ResultCallback, + ) { + this.handler('destroyDeletedRecords', [tableName, recordIds], callback) + } + + unsafeLoadFromSync(jsonId: number, callback: ResultCallback) { + this.handler('unsafeLoadFromSync', [jsonId], callback) + } + + provideSyncJson(id: number, syncPullResultJson: string, callback: ResultCallback) { + this.handler('provideSyncJson', [id, syncPullResultJson], callback) + } + + unsafeResetDatabase(callback: ResultCallback) { + this.handler('unsafeResetDatabase', [], callback) + } + + unsafeExecute(work: UnsafeExecuteOperations, callback: ResultCallback) { + this.handler('unsafeExecute', [work], callback) + } + + getLocal(key: string, callback: ResultCallback) { + this.handler('getLocal', [key], callback) + } + + setLocal(key: string, value: string, callback: ResultCallback) { + this.handler('setLocal', [key, value], callback) + } + + removeLocal(key: string, callback: ResultCallback) { + this.handler('removeLocal', [key], callback) + } +} \ No newline at end of file diff --git a/src/adapters/remote/type.d.ts b/src/adapters/remote/type.d.ts new file mode 100644 index 000000000..b23630e39 --- /dev/null +++ b/src/adapters/remote/type.d.ts @@ -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) => void; + +export type RemoteAdapterOptions = { + schema: AppSchema, + migrations?: SchemaMigrations, + handler: RemoteHandler, +} \ No newline at end of file diff --git a/src/adapters/remote/type.js b/src/adapters/remote/type.js new file mode 100644 index 000000000..a35c39bb0 --- /dev/null +++ b/src/adapters/remote/type.js @@ -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) => void; + +export type RemoteAdapterOptions = { + schema: AppSchema, + migrations?: SchemaMigrations, + handler: RemoteHandler, +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ad9234b3a..ff1b38b53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== \ No newline at end of file