@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 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. It is analogous to `express-x-drizzle`, but deliberately has no
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
- # express-x-electric
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.1",
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 = {