@jcbuisson/express-x-plugins 4.0.1 → 4.0.3

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.3",
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
- import { Observable } from 'rxjs'
2
+ import { firstValueFrom, Observable } from 'rxjs'
3
+ import { getCurrentScope, onScopeDispose, ref } from 'vue'
3
4
 
4
5
 
5
6
  /**
@@ -46,6 +47,17 @@ 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
+
57
+ function firstResult(where = {}) {
58
+ return firstValueFrom(getObservable(where))
59
+ }
60
+
49
61
  async function create(data) {
50
62
  assertPlainObject(data, 'mutation data')
51
63
  const uid = globalThis.crypto?.randomUUID?.()
@@ -65,7 +77,7 @@ export function electricClientPlugin(app, options = {}) {
65
77
  return value
66
78
  }
67
79
 
68
- return { getObservable, create, update, remove }
80
+ return { getObservable, getVueRef, firstResult, create, update, remove }
69
81
  }
70
82
 
71
83
  return Object.assign(app, { createElectricModel })
@@ -105,7 +117,7 @@ function serializeValue(value, path) {
105
117
  }
106
118
 
107
119
  /** Convert the Express-X object filter into Electric's parameterized SQL filter. */
108
- function whereToElectricParams(where = {}) {
120
+ export function whereToElectricParams(where = {}) {
109
121
  assertPlainObject(where, 'where')
110
122
  const clauses = []
111
123
  const params = []
@@ -139,3 +151,5 @@ function modelPath(shapePath, modelName) {
139
151
  const base = shapePath.replace(/\/$/, '')
140
152
  return `${base}/${encodeURIComponent(modelName)}`
141
153
  }
154
+
155
+ 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,38 @@ 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
+
64
+ test('firstResult resolves with the first Shape rows and cleans up its subscription', async () => {
65
+ const app = { service: () => ({}) }
66
+ electricClientPlugin(app, { ShapeStream: FakeStream, Shape: FakeShape })
67
+ const todo = app.createElectricModel('todos')
68
+
69
+ const rows = await todo.firstResult({ completed: false })
70
+
71
+ assert.deepEqual(rows, [{ uid: 'one', completed: false }])
72
+ const stream = FakeStream.instances.at(-1)
73
+ assert.deepEqual(stream.options.params, {
74
+ where: '"completed" = $1', params: ['false'],
75
+ })
76
+ assert.equal(stream.shape.unsubscribed, true)
77
+ })
78
+
46
79
  test('model mutations retain the simple Express-X API', async () => {
47
80
  const calls = []
48
81
  const service = {