Skip to content

Commit 5d602a4

Browse files
authored
Merge pull request #516 from powersync-ja/node
Add NodeJS implementation
2 parents 48f8632 + 87179ff commit 5d602a4

29 files changed

+1784
-172
lines changed

.changeset/serious-rice-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/node': minor
3+
---
4+
5+
Initial version

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ _[PowerSync](https://www.powersync.com) is a sync engine for building local-firs
1818

1919
- JS Web SDK implementation (extension of `packages/common`)
2020

21+
- [packages/node](./packages/node/README.md)
22+
23+
- Node.js client implementation (extension of `packages/common`)
24+
2125
- [packages/react](./packages/react/README.md)
2226

2327
- React integration for PowerSync.
@@ -80,6 +84,10 @@ Demo applications are located in the [`demos/`](./demos/) directory. Also see ou
8084

8185
- [demos/example-capacitor](./demos/example-capacitor/README.md) A Capacitor example app using the PowerSync Web SDK.
8286

87+
### Node
88+
89+
- [demos/example-node](./demos/example-node/README.md) A small CLI example built using the PowerSync SDK for Node.js.
90+
8391
## Tools
8492

8593
- [tools/diagnostics-app](./tools/diagnostics-app): A standalone web app that presents stats about a user's local database (incl. tables and sync buckets).

demos/example-node/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
BACKEND=http://localhost:6060
2+
SYNC_SERVICE=http://localhost:8080

demos/example-node/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Node.js Demo
2+
3+
This demonstrates a small Node.js client opening a database and connecting PowerSync.
4+
5+
This demo is configured to talk to an example backend [you can host yourself](https://github.com/powersync-ja/self-host-demo). To get started:
6+
7+
1. Start one of the Node.js backend examples from [the self-host-demo repository](https://github.com/powersync-ja/self-host-demo).
8+
2. If necessary, change `.env` to point to the started backend and sync service.
9+
3. Run `pnpm install` and `pnpm build:packages` in the root of this repo.
10+
4. In this directory, run `pnpm run start`.
11+
12+
This opens the local database, connects to PowerSync, waits for a first sync and then runs a simple query.
13+
Results from the query are printed every time it changes. Try:
14+
15+
1. Updating a row in the backend database and see changes reflected in the running client.
16+
2. Enter `add('my list')` and see the new list show up in the backend database.

demos/example-node/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "example-node",
3+
"version": "1.0.0",
4+
"description": "",
5+
"type": "module",
6+
"private": true,
7+
"scripts": {
8+
"build": "tsc -b",
9+
"watch": "tsc -b -w",
10+
"start": "node --loader ts-node/esm -r dotenv/config src/main.ts"
11+
},
12+
"dependencies": {
13+
"@powersync/node": "workspace:*",
14+
"dotenv": "^16.4.7"
15+
},
16+
"devDependencies": {
17+
"ts-node": "^10.9.2",
18+
"typescript": "^5.8.2"
19+
}
20+
}

demos/example-node/src/main.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import repl_factory from 'node:repl';
2+
import { once } from 'node:events';
3+
4+
import { PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node';
5+
import Logger from 'js-logger';
6+
import { AppSchema, DemoConnector } from './powersync.js';
7+
import { exit } from 'node:process';
8+
9+
const main = async () => {
10+
Logger.useDefaults({ defaultLevel: Logger.WARN });
11+
12+
if (!('BACKEND' in process.env) || !('SYNC_SERVICE' in process.env)) {
13+
console.warn(
14+
'Set the BACKEND and SYNC_SERVICE environment variables to point to a sync service and a running demo backend.'
15+
);
16+
return;
17+
}
18+
19+
const db = new PowerSyncDatabase({
20+
schema: AppSchema,
21+
database: {
22+
dbFilename: 'test.db'
23+
},
24+
logger: Logger
25+
});
26+
console.log(await db.get('SELECT powersync_rs_version();'));
27+
28+
await db.connect(new DemoConnector(), { connectionMethod: SyncStreamConnectionMethod.HTTP });
29+
await db.waitForFirstSync();
30+
console.log('First sync complete!');
31+
32+
let hasFirstRow: ((value: any) => void) | null = null;
33+
const firstRow = new Promise((resolve) => (hasFirstRow = resolve));
34+
const watchLists = async () => {
35+
for await (const rows of db.watch('SELECT * FROM lists')) {
36+
if (hasFirstRow) {
37+
hasFirstRow(null);
38+
hasFirstRow = null;
39+
}
40+
console.log('Has todo lists', rows.rows?._array);
41+
}
42+
};
43+
44+
watchLists();
45+
await firstRow;
46+
47+
console.log('Connected to PowerSync. Try updating the lists in the database and see it reflected here.');
48+
console.log("To upload a list here, enter `await add('name of new list');`");
49+
50+
const repl = repl_factory.start();
51+
repl.context.add = async (name: string) => {
52+
await db.execute(
53+
"INSERT INTO lists (id, created_at, name, owner_id) VALUEs (uuid(), datetime('now'), ?, uuid());",
54+
[name]
55+
);
56+
};
57+
58+
await once(repl, 'exit');
59+
console.log('shutting down');
60+
await db.disconnect();
61+
await db.close();
62+
exit(0);
63+
};
64+
65+
await main();
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { AbstractPowerSyncDatabase, column, PowerSyncBackendConnector, Schema, Table } from '@powersync/node';
2+
3+
export class DemoConnector implements PowerSyncBackendConnector {
4+
async fetchCredentials() {
5+
const response = await fetch(`${process.env.BACKEND}/api/auth/token`);
6+
if (response.status != 200) {
7+
throw 'Could not fetch token';
8+
}
9+
10+
const { token } = await response.json();
11+
12+
return {
13+
endpoint: process.env.SYNC_SERVICE!,
14+
token: token
15+
};
16+
}
17+
18+
async uploadData(database: AbstractPowerSyncDatabase) {
19+
const batch = await database.getCrudBatch();
20+
if (batch == null) {
21+
return;
22+
}
23+
24+
const entries: any[] = [];
25+
for (const op of batch.crud) {
26+
entries.push({
27+
table: op.table,
28+
op: op.op,
29+
id: op.id,
30+
data: op.opData
31+
});
32+
}
33+
34+
const response = await fetch(`${process.env.BACKEND}/api/data/`, {
35+
method: 'POST',
36+
headers: {'Content-Type': 'application/json'},
37+
body: JSON.stringify({batch: entries}),
38+
});
39+
if (response.status !== 200) {
40+
throw new Error(`Server returned HTTP ${response.status}: ${await response.text()}`);
41+
}
42+
43+
await batch?.complete();
44+
}
45+
}
46+
47+
export const LIST_TABLE = 'lists';
48+
export const TODO_TABLE = 'todos';
49+
50+
const todos = new Table(
51+
{
52+
list_id: column.text,
53+
created_at: column.text,
54+
completed_at: column.text,
55+
description: column.text,
56+
created_by: column.text,
57+
completed_by: column.text,
58+
completed: column.integer,
59+
photo_id: column.text
60+
},
61+
{ indexes: { list: ['list_id'] } }
62+
);
63+
64+
const lists = new Table({
65+
created_at: column.text,
66+
name: column.text,
67+
owner_id: column.text
68+
});
69+
70+
export const AppSchema = new Schema({
71+
lists,
72+
todos
73+
});
74+
75+
export type Database = (typeof AppSchema)['types'];
76+
export type TodoRecord = Database['todos'];
77+
export type ListRecord = Database['lists'];

demos/example-node/tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "../../tsconfig.base",
3+
"compilerOptions": {
4+
"baseUrl": ".",
5+
"rootDir": "src",
6+
"outDir": "lib",
7+
"strictNullChecks": true
8+
},
9+
"references": [
10+
{
11+
"path": "../../packages/node"
12+
}
13+
]
14+
}

docs/utils/packageMap.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ enum Packages {
66
VueSdk = 'vue-sdk',
77
AttachmentsSdk = 'attachments-sdk',
88
WebSdk = 'web-sdk',
9-
TanstackReactQuerySdk = 'tanstack-react-query-sdk'
9+
TanstackReactQuerySdk = 'tanstack-react-query-sdk',
10+
NodeSdk = 'node-sdk',
1011
}
1112

1213
interface Package {
@@ -63,5 +64,12 @@ export const packageMap: PackageMap = {
6364
entryPoints: ['../packages/attachments/src/index.ts'],
6465
tsconfig: '../packages/attachments/tsconfig.json',
6566
id: Packages.AttachmentsSdk
66-
}
67+
},
68+
[Packages.NodeSdk]: {
69+
name: 'Node SDK',
70+
dirName: Packages.NodeSdk,
71+
entryPoints: ['../packages/node/src/index.ts'],
72+
tsconfig: '../packages/node/tsconfig.json',
73+
id: Packages.NodeSdk
74+
},
6775
};

packages/node/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @powersync/node

0 commit comments

Comments
 (0)