@jcbuisson/express-x-plugins 4.0.7 → 4.0.9
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,21 @@
|
|
|
2
2
|
# express-x-plugins
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
IMPORTANT
|
|
5
|
+
IMPORTANT FOR ELECTRIC
|
|
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: `sudo systemctl restart postgresql`)
|
|
17
|
+
|
|
18
|
+
## Use HTTP2
|
|
19
|
+
nginx: `listen 443 ssl http2;`
|
|
6
20
|
|
|
7
21
|
|
|
8
22
|
Currently includes:
|
|
@@ -63,13 +77,16 @@ app.configure(electricOfflinePlugin, db, [
|
|
|
63
77
|
|
|
64
78
|
This registers one Express-X service per model with the familiar API:
|
|
65
79
|
|
|
80
|
+
- `findUnique(where)`
|
|
81
|
+
- `findMany(where, queryOptions)`
|
|
66
82
|
- `create(id, data)` for a client-generated primary key
|
|
67
83
|
- `create(data)` for a database-generated primary key
|
|
68
84
|
- `update(id, data)`
|
|
69
85
|
- `delete(id)`
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
The client model provides synchronized reads through `findMany(where)` and
|
|
88
|
+
`getObservable(where)`. The service also exposes direct one-shot server reads
|
|
89
|
+
when synchronization is not required.
|
|
73
90
|
|
|
74
91
|
Mutation methods return the created, updated, or deleted row directly.
|
|
75
92
|
|
|
@@ -97,7 +114,6 @@ const numberedTodo = app.createElectricModel('numberedTodos', {
|
|
|
97
114
|
})
|
|
98
115
|
|
|
99
116
|
const incompleteTodos = await todo.findMany({ completed: false })
|
|
100
|
-
const selectedTodo = await todo.findUnique({ uid })
|
|
101
117
|
|
|
102
118
|
const subscription = todo.getObservable({ completed: false }).subscribe(rows => {
|
|
103
119
|
console.log(rows)
|
|
@@ -116,9 +132,8 @@ subscription.unsubscribe()
|
|
|
116
132
|
|
|
117
133
|
`findMany(where)` resolves with all matching rows from the first synchronized Shape emission.
|
|
118
134
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if that scope is disposed before a result arrives.
|
|
135
|
+
It unsubscribes after the first emission; if called within a Vue scope, it also
|
|
136
|
+
unsubscribes if that scope is disposed before a result arrives.
|
|
122
137
|
|
|
123
138
|
Object filters use parameterized Electric Shape predicates. Exact values,
|
|
124
139
|
`null`, and `gt`/`gte`/`lt`/`lte` ranges are supported.
|
|
@@ -156,8 +171,16 @@ services:
|
|
|
156
171
|
environment:
|
|
157
172
|
DATABASE_URL: postgresql://user:password@host.docker.internal:5432/mydb
|
|
158
173
|
ELECTRIC_INSECURE: "true"
|
|
174
|
+
ELECTRIC_STORAGE: FAST_FILE
|
|
175
|
+
ELECTRIC_PERSISTENT_STATE: FILE
|
|
176
|
+
ELECTRIC_STORAGE_DIR: /var/lib/electric
|
|
159
177
|
ports:
|
|
160
178
|
- "3001:3000"
|
|
179
|
+
volumes:
|
|
180
|
+
- electric_mydb_data:/var/lib/electric
|
|
181
|
+
|
|
182
|
+
volumes:
|
|
183
|
+
electric_mydb_data:
|
|
161
184
|
```
|
|
162
185
|
|
|
163
186
|
```
|
package/package.json
CHANGED
|
@@ -2,20 +2,20 @@ 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
|
-
|
|
6
5
|
/**
|
|
7
6
|
* Add Electric-backed reactive models to an Express-X client.
|
|
8
7
|
*
|
|
9
8
|
* Usage:
|
|
10
9
|
* electricClientPlugin(app)
|
|
11
|
-
* const todo = app.createElectricModel('todos')
|
|
10
|
+
* const todo = app.createElectricModel('todos') // primary key is provided by client
|
|
11
|
+
* const todo = app.createElectricModel('todos', { idGeneration: 'server' }) // primary key is server-generated
|
|
12
12
|
* todo.getObservable({ completed: false }).subscribe(...)
|
|
13
13
|
* const completedTodos = await todo.findMany({ completed: false })
|
|
14
14
|
*/
|
|
15
15
|
export function electricClientPlugin(app, options = {}) {
|
|
16
16
|
const shapePath = options.shapePath ?? '/electric/v1/shape'
|
|
17
17
|
const ShapeStreamClass = options.ShapeStream ?? ShapeStream
|
|
18
|
-
const ShapeClass = options.Shape ??
|
|
18
|
+
const ShapeClass = options.Shape ?? DisposableShape
|
|
19
19
|
const ObservableClass = options.Observable ?? Observable
|
|
20
20
|
|
|
21
21
|
function createElectricModel(modelName, modelOptions = {}) {
|
|
@@ -89,6 +89,16 @@ export function electricClientPlugin(app, options = {}) {
|
|
|
89
89
|
|
|
90
90
|
////////////////////// UTILITIES //////////////////////
|
|
91
91
|
|
|
92
|
+
export class DisposableShape extends Shape {
|
|
93
|
+
subscribe(callback) {
|
|
94
|
+
const unsubscribe = super.subscribe(callback)
|
|
95
|
+
return () => {
|
|
96
|
+
unsubscribe()
|
|
97
|
+
if (this.numSubscribers === 0) this.stream.unsubscribeAll()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
93
103
|
const RANGE_OPERATORS = { gt: '>', gte: '>=', lt: '<', lte: '<=' }
|
|
94
104
|
|
|
@@ -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',
|