@jcbuisson/express-x-plugins 4.0.7 → 4.0.8

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,7 +2,20 @@
2
2
  # express-x-plugins
3
3
 
4
4
 
5
- IMPORTANT: set lc_messages=C for the PostgreSQL chris role so Electric receives recognizable English errors.
5
+ IMPORTANT
6
+
7
+ # set lc_messages=C for the PostgreSQL role so Electric receives recognizable English errors
8
+ ALTER ROLE chris SET lc_messages = 'C';
9
+
10
+ # ALLOW POSTGRES LOGICAL REPLICATION
11
+ ALTER SYSTEM SET wal_level = 'logical';
12
+ ALTER SYSTEM SET max_replication_slots = 10;
13
+ ALTER SYSTEM SET max_wal_senders = 10;
14
+ ALTER ROLE chris WITH REPLICATION;
15
+
16
+ ## restart postgres
17
+ sudo systemctl restart postgresql
18
+
6
19
 
7
20
 
8
21
  Currently includes:
@@ -63,13 +76,16 @@ app.configure(electricOfflinePlugin, db, [
63
76
 
64
77
  This registers one Express-X service per model with the familiar API:
65
78
 
79
+ - `findUnique(where)`
80
+ - `findMany(where, queryOptions)`
66
81
  - `create(id, data)` for a client-generated primary key
67
82
  - `create(data)` for a database-generated primary key
68
83
  - `update(id, data)`
69
84
  - `delete(id)`
70
85
 
71
- Synchronized reads are provided client-side by `findMany(where)`, `findUnique(where)`, and `getObservable(where)` below;
72
- one-shot server reads would bypass Electric and are intentionally omitted.
86
+ The client model provides synchronized reads through `findMany(where)` and
87
+ `getObservable(where)`. The service also exposes direct one-shot server reads
88
+ when synchronization is not required.
73
89
 
74
90
  Mutation methods return the created, updated, or deleted row directly.
75
91
 
@@ -97,7 +113,6 @@ const numberedTodo = app.createElectricModel('numberedTodos', {
97
113
  })
98
114
 
99
115
  const incompleteTodos = await todo.findMany({ completed: false })
100
- const selectedTodo = await todo.findUnique({ uid })
101
116
 
102
117
  const subscription = todo.getObservable({ completed: false }).subscribe(rows => {
103
118
  console.log(rows)
@@ -116,9 +131,8 @@ subscription.unsubscribe()
116
131
 
117
132
  `findMany(where)` resolves with all matching rows from the first synchronized Shape emission.
118
133
 
119
- `findUnique(where)` resolves with the first matching row, or `null` when there is no match.
120
- Both unsubscribe after their first emission; if called within a Vue scope, they also unsubscribe
121
- if that scope is disposed before a result arrives.
134
+ It unsubscribes after the first emission; if called within a Vue scope, it also
135
+ unsubscribes if that scope is disposed before a result arrives.
122
136
 
123
137
  Object filters use parameterized Electric Shape predicates. Exact values,
124
138
  `null`, and `gt`/`gte`/`lt`/`lte` ranges are supported.
@@ -156,8 +170,16 @@ services:
156
170
  environment:
157
171
  DATABASE_URL: postgresql://user:password@host.docker.internal:5432/mydb
158
172
  ELECTRIC_INSECURE: "true"
173
+ ELECTRIC_STORAGE: FAST_FILE
174
+ ELECTRIC_PERSISTENT_STATE: FILE
175
+ ELECTRIC_STORAGE_DIR: /var/lib/electric
159
176
  ports:
160
177
  - "3001:3000"
178
+ volumes:
179
+ - electric_mydb_data:/var/lib/electric
180
+
181
+ volumes:
182
+ electric_mydb_data:
161
183
  ```
162
184
 
163
185
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x-plugins",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "Plugins for express-x",
5
5
  "type": "module",
6
6
  "main": "./src/electric-server-plugin.mjs",
@@ -2,20 +2,31 @@ import { Shape, ShapeStream } from '@electric-sql/client'
2
2
  import { firstValueFrom, Observable, Subject, takeUntil } from 'rxjs'
3
3
  import { getCurrentScope, onScopeDispose, ref } from 'vue'
4
4
 
5
+ export class DisposableShape extends Shape {
6
+ subscribe(callback) {
7
+ const unsubscribe = super.subscribe(callback)
8
+ return () => {
9
+ unsubscribe()
10
+ if (this.numSubscribers === 0) this.stream.unsubscribeAll()
11
+ }
12
+ }
13
+ }
14
+
5
15
 
6
16
  /**
7
17
  * Add Electric-backed reactive models to an Express-X client.
8
18
  *
9
19
  * Usage:
10
20
  * electricClientPlugin(app)
11
- * const todo = app.createElectricModel('todos')
21
+ * const todo = app.createElectricModel('todos') // primary key is provided by client
22
+ * const todo = app.createElectricModel('todos', { idGeneration: 'server' }) // primary key is server-generated
12
23
  * todo.getObservable({ completed: false }).subscribe(...)
13
24
  * const completedTodos = await todo.findMany({ completed: false })
14
25
  */
15
26
  export function electricClientPlugin(app, options = {}) {
16
27
  const shapePath = options.shapePath ?? '/electric/v1/shape'
17
28
  const ShapeStreamClass = options.ShapeStream ?? ShapeStream
18
- const ShapeClass = options.Shape ?? Shape
29
+ const ShapeClass = options.Shape ?? DisposableShape
19
30
  const ObservableClass = options.Observable ?? Observable
20
31
 
21
32
  function createElectricModel(modelName, modelOptions = {}) {
@@ -26,8 +26,10 @@ export function electricOfflinePlugin(app, db, models, options = {}) {
26
26
  }
27
27
  }
28
28
 
29
+ // for each model 'name', there is a service 'name' with methods 'findUnique', 'findMany', 'create', 'update', 'delete'
29
30
  for (const model of configuredModels) {
30
31
  app.createService(model.name, {
32
+
31
33
  findUnique: async function(where) {
32
34
  await authorize(this, model.name, 'findUnique', [where])
33
35
  const filter = buildWhere(where)
@@ -50,7 +52,8 @@ export function electricOfflinePlugin(app, db, models, options = {}) {
50
52
  return (await db.query(sql, values)).rows
51
53
  },
52
54
 
53
- // create(data) (primary key is server-generated) or create(uid, data) (uid is the primary key, client-generated)
55
+ // create(data): the primary key is server-generated
56
+ // create(uid, data): uid (the primary key) is provided by the client
54
57
  create: async function(idOrData, data) {
55
58
  const hasClientId = data !== undefined
56
59
  const mutationData = hasClientId ? data : idOrData
@@ -2,7 +2,11 @@ import assert from 'node:assert/strict'
2
2
  import test from 'node:test'
3
3
  import { effectScope, isRef } from 'vue'
4
4
 
5
- import { electricClientPlugin, whereToElectricParams } from '../src/electric-client-plugin.mjs'
5
+ import {
6
+ DisposableShape,
7
+ electricClientPlugin,
8
+ whereToElectricParams,
9
+ } from '../src/electric-client-plugin.mjs'
6
10
 
7
11
  class FakeStream {
8
12
  static instances = []
@@ -36,6 +40,19 @@ class EmptyShape {
36
40
  }
37
41
  }
38
42
 
43
+ test('DisposableShape tears down its stream with its last subscriber', () => {
44
+ const stream = {
45
+ subscribe() { return () => {} },
46
+ unsubscribeAll() { this.unsubscribed = true },
47
+ }
48
+ const shape = new DisposableShape(stream)
49
+ const unsubscribe = shape.subscribe(() => {})
50
+
51
+ unsubscribe()
52
+
53
+ assert.equal(stream.unsubscribed, true)
54
+ })
55
+
39
56
  test('translates where objects into parameterized Electric filters', () => {
40
57
  assert.deepEqual(whereToElectricParams({ completed: false, priority: { gte: 2, lt: 5 }, owner: null }), {
41
58
  where: '"completed" = $1 AND "priority" >= $2 AND "priority" < $3 AND "owner" IS NULL',