@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
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
@@ -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 ??
|
|
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)
|
|
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 {
|
|
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',
|