@jcbuisson/express-x-plugins 4.0.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/package.json +8 -3
- package/src/electric-client-plugin.mjs +12 -2
- package/test/electric-client-plugin.test.mjs +19 -1
package/README.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The smallest useful ElectricSQL integration for Express-X. Express-X handles
|
|
4
4
|
authorized PostgreSQL mutations; Electric's Shape API streams those changes to
|
|
5
|
-
clients
|
|
6
|
-
metadata table and no custom `sync.go`: Electric is the sync engine.
|
|
5
|
+
clients: Electric is the sync engine.
|
|
7
6
|
|
|
8
7
|
## Install
|
|
9
8
|
|
|
@@ -11,8 +10,7 @@ metadata table and no custom `sync.go`: Electric is the sync engine.
|
|
|
11
10
|
npm install @jcbuisson/express-x-electric pg
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
This server-only installation does not install the browser Electric client or
|
|
15
|
-
RxJS.
|
|
13
|
+
This server-only installation does not install the browser Electric client or RxJS.
|
|
16
14
|
|
|
17
15
|
## Server
|
|
18
16
|
|
|
@@ -102,4 +100,23 @@ as query parameters; range filters support `gt`, `gte`, `lt`, and `lte`.
|
|
|
102
100
|
Requires Node 18+ for the built-in Fetch API. The PostgreSQL client only needs a
|
|
103
101
|
`query(sql, values)` method; a `pg.Pool` is recommended so each mutation and its
|
|
104
102
|
transaction ID are captured in the same transaction.
|
|
105
|
-
|
|
103
|
+
|
|
104
|
+
## Run Electric from Docker
|
|
105
|
+
|
|
106
|
+
A local install of the Electric sync engine requires Elixir and Erlang; it is simpler to use a pre-built Docker image.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
services:
|
|
110
|
+
electric:
|
|
111
|
+
image: electricsql/electric:latest
|
|
112
|
+
environment:
|
|
113
|
+
DATABASE_URL: postgresql://user:password@host.docker.internal:5432/mydb
|
|
114
|
+
ELECTRIC_INSECURE: "true"
|
|
115
|
+
ports:
|
|
116
|
+
- "3001:3000"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
docker compose pull electric
|
|
121
|
+
docker compose up electric
|
|
122
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jcbuisson/express-x-plugins",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Plugins for express-x",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/electric-server-plugin.mjs",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@electric-sql/client": "^1.5.24",
|
|
38
|
-
"rxjs": "^7.8.2"
|
|
38
|
+
"rxjs": "^7.8.2",
|
|
39
|
+
"vue": "^3.5.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependenciesMeta": {
|
|
41
42
|
"@electric-sql/client": {
|
|
@@ -43,10 +44,14 @@
|
|
|
43
44
|
},
|
|
44
45
|
"rxjs": {
|
|
45
46
|
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"vue": {
|
|
49
|
+
"optional": true
|
|
46
50
|
}
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
53
|
"@electric-sql/client": "^1.5.24",
|
|
50
|
-
"rxjs": "^7.8.2"
|
|
54
|
+
"rxjs": "^7.8.2",
|
|
55
|
+
"vue": "^3.5.0"
|
|
51
56
|
}
|
|
52
57
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Shape, ShapeStream } from '@electric-sql/client'
|
|
2
2
|
import { Observable } from 'rxjs'
|
|
3
|
+
import { getCurrentScope, onScopeDispose, ref } from 'vue'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -46,6 +47,13 @@ export function electricClientPlugin(app, options = {}) {
|
|
|
46
47
|
})
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
function getVueRef(where = {}) {
|
|
51
|
+
const value = ref([])
|
|
52
|
+
const subscription = getObservable(where).subscribe(rows => { value.value = rows })
|
|
53
|
+
if (getCurrentScope()) onScopeDispose(() => subscription.unsubscribe())
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
async function create(data) {
|
|
50
58
|
assertPlainObject(data, 'mutation data')
|
|
51
59
|
const uid = globalThis.crypto?.randomUUID?.()
|
|
@@ -65,7 +73,7 @@ export function electricClientPlugin(app, options = {}) {
|
|
|
65
73
|
return value
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
return { getObservable, create, update, remove }
|
|
76
|
+
return { getObservable, getVueRef, create, update, remove }
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
return Object.assign(app, { createElectricModel })
|
|
@@ -105,7 +113,7 @@ function serializeValue(value, path) {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
/** Convert the Express-X object filter into Electric's parameterized SQL filter. */
|
|
108
|
-
function whereToElectricParams(where = {}) {
|
|
116
|
+
export function whereToElectricParams(where = {}) {
|
|
109
117
|
assertPlainObject(where, 'where')
|
|
110
118
|
const clauses = []
|
|
111
119
|
const params = []
|
|
@@ -139,3 +147,5 @@ function modelPath(shapePath, modelName) {
|
|
|
139
147
|
const base = shapePath.replace(/\/$/, '')
|
|
140
148
|
return `${base}/${encodeURIComponent(modelName)}`
|
|
141
149
|
}
|
|
150
|
+
|
|
151
|
+
export default electricClientPlugin
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
+
import { effectScope, isRef } from 'vue'
|
|
3
4
|
|
|
4
5
|
import { electricClientPlugin, whereToElectricParams } from '../src/electric-client-plugin.mjs'
|
|
5
6
|
|
|
@@ -12,7 +13,7 @@ class FakeStream {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
class FakeShape {
|
|
15
|
-
constructor(stream) { this.stream = stream }
|
|
16
|
+
constructor(stream) { this.stream = stream; stream.shape = this }
|
|
16
17
|
subscribe(callback) {
|
|
17
18
|
callback({ rows: [{ uid: 'one', completed: false }] })
|
|
18
19
|
return () => { this.unsubscribed = true }
|
|
@@ -43,6 +44,23 @@ test('getObservable emits Shape rows and cleans up its subscription', () => {
|
|
|
43
44
|
subscription.unsubscribe()
|
|
44
45
|
})
|
|
45
46
|
|
|
47
|
+
test('getVueRef returns Shape rows in a Vue ref and cleans up with its scope', () => {
|
|
48
|
+
const app = { service: () => ({}) }
|
|
49
|
+
electricClientPlugin(app, { ShapeStream: FakeStream, Shape: FakeShape })
|
|
50
|
+
const todo = app.createElectricModel('todos')
|
|
51
|
+
const scope = effectScope()
|
|
52
|
+
let rows
|
|
53
|
+
|
|
54
|
+
scope.run(() => { rows = todo.getVueRef({ completed: false }) })
|
|
55
|
+
|
|
56
|
+
assert.equal(isRef(rows), true)
|
|
57
|
+
assert.deepEqual(rows.value, [{ uid: 'one', completed: false }])
|
|
58
|
+
const shape = FakeStream.instances.at(-1).shape
|
|
59
|
+
assert.equal(shape.unsubscribed, undefined)
|
|
60
|
+
scope.stop()
|
|
61
|
+
assert.equal(shape.unsubscribed, true)
|
|
62
|
+
})
|
|
63
|
+
|
|
46
64
|
test('model mutations retain the simple Express-X API', async () => {
|
|
47
65
|
const calls = []
|
|
48
66
|
const service = {
|