@jcbuisson/express-x-plugins 4.0.12 → 4.1.0
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/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { Shape, ShapeStream } from '@electric-sql/client'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Observable } from 'rxjs'
|
|
3
|
+
// import { firstValueFrom, Subject, takeUntil } from 'rxjs'
|
|
4
|
+
// import { getCurrentScope, onScopeDispose } from 'vue'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Add Electric-backed reactive models to an Express-X client.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
10
|
* electricClientPlugin(app)
|
|
10
|
-
* const
|
|
11
|
-
* const
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* const todoModel = app.createElectricModel('todos') // primary key is automatically provided by server (auto-increment)
|
|
12
|
+
* const todoMilk = await todoModel.create({ label: "Buy milk" })
|
|
13
|
+
* todoMilk.update({ completed: true })
|
|
14
|
+
* todoModel.getObservable({ completed: false }).subscribe(...) // subscribe to an observable for todos with completed===false
|
|
14
15
|
*/
|
|
15
16
|
export function electricClientPlugin(app, options = {}) {
|
|
16
17
|
const shapePath = options.shapePath ?? '/electric/v1/shape'
|
|
@@ -23,10 +24,10 @@ export function electricClientPlugin(app, options = {}) {
|
|
|
23
24
|
const service = app.service(modelName)
|
|
24
25
|
const url = modelOptions.url ?? modelPath(shapePath, modelName)
|
|
25
26
|
const streamOptions = modelOptions.streamOptions ?? {}
|
|
26
|
-
const idGeneration = modelOptions.idGeneration ?? 'client'
|
|
27
|
-
if (!['client', 'server'].includes(idGeneration)) {
|
|
28
|
-
|
|
29
|
-
}
|
|
27
|
+
// const idGeneration = modelOptions.idGeneration ?? 'client'
|
|
28
|
+
// if (!['client', 'server'].includes(idGeneration)) {
|
|
29
|
+
// throw new TypeError("idGeneration must be 'client' or 'server'")
|
|
30
|
+
// }
|
|
30
31
|
|
|
31
32
|
function getObservable(where = {}) {
|
|
32
33
|
// Validate eagerly
|
|
@@ -52,24 +53,35 @@ export function electricClientPlugin(app, options = {}) {
|
|
|
52
53
|
})
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function findMany(where = {}) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function create(data) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
// function findMany(where = {}) {
|
|
57
|
+
// const observable = getObservable(where)
|
|
58
|
+
// if (!getCurrentScope()) return firstValueFrom(observable)
|
|
59
|
+
|
|
60
|
+
// const scopeDisposed = new Subject()
|
|
61
|
+
// onScopeDispose(() => {
|
|
62
|
+
// scopeDisposed.next()
|
|
63
|
+
// scopeDisposed.complete()
|
|
64
|
+
// })
|
|
65
|
+
// return firstValueFrom(observable.pipe(takeUntil(scopeDisposed)))
|
|
66
|
+
// }
|
|
67
|
+
|
|
68
|
+
// async function create(data) {
|
|
69
|
+
// assertPlainObject(data, 'mutation data')
|
|
70
|
+
// if (idGeneration === 'server') return service.create(data)
|
|
71
|
+
// const uid = globalThis.crypto?.randomUUID?.()
|
|
72
|
+
// if (!uid) throw new Error('crypto.randomUUID() is required')
|
|
73
|
+
// return service.create(uid, data)
|
|
74
|
+
// }
|
|
75
|
+
|
|
76
|
+
async function create(uidOrData, data) {
|
|
77
|
+
const hasUid = data !== undefined
|
|
78
|
+
const mutationData = hasUid ? data : uidOrData
|
|
79
|
+
assertPlainObject(mutationData, 'mutation data')
|
|
80
|
+
if (hasUid) {
|
|
81
|
+
return service.create(uidOrData, data)
|
|
82
|
+
} else {
|
|
83
|
+
return service.create(data)
|
|
84
|
+
}
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
async function update(id, data) {
|
|
@@ -80,7 +92,7 @@ export function electricClientPlugin(app, options = {}) {
|
|
|
80
92
|
return service.delete(id)
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
return { getObservable, findMany
|
|
95
|
+
return { getObservable, /*findMany,*/ create, update, remove }
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
return Object.assign(app, { createElectricModel })
|
|
@@ -16,9 +16,11 @@ export function electricOfflinePlugin(app, db, models, options = {}) {
|
|
|
16
16
|
const shapePath = options.shapePath ?? '/electric/v1/shape/:model'
|
|
17
17
|
const fetchImpl = options.fetch ?? globalThis.fetch
|
|
18
18
|
if (typeof fetchImpl !== 'function') throw new TypeError('a fetch implementation is required')
|
|
19
|
+
const authorizeFunc = options.authorize || (() => true)
|
|
20
|
+
if (typeof authorizeFunc !== 'function') throw new TypeError('an authorize function is required')
|
|
19
21
|
|
|
20
22
|
async function authorize(context, modelName, action, args) {
|
|
21
|
-
const allowed = await
|
|
23
|
+
const allowed = await authorizeFunc(context, { modelName, action, args })
|
|
22
24
|
if (!allowed) {
|
|
23
25
|
const error = new Error(`not authorized to ${action} '${modelName}'`)
|
|
24
26
|
error.code = 'forbidden'
|
|
@@ -54,13 +56,13 @@ export function electricOfflinePlugin(app, db, models, options = {}) {
|
|
|
54
56
|
|
|
55
57
|
// create(data): the primary key is server-generated
|
|
56
58
|
// create(uid, data): uid (the primary key) is provided by the client
|
|
57
|
-
create: async function(
|
|
59
|
+
create: async function(uidOrData, data) {
|
|
58
60
|
const hasClientId = data !== undefined
|
|
59
|
-
const mutationData = hasClientId ? data :
|
|
60
|
-
await authorize(this, model.name, 'create', hasClientId ? [
|
|
61
|
+
const mutationData = hasClientId ? data : uidOrData
|
|
62
|
+
await authorize(this, model.name, 'create', hasClientId ? [uidOrData, mutationData] : [mutationData])
|
|
61
63
|
assertPlainObject(mutationData, 'mutation data')
|
|
62
64
|
const safeData = hasClientId
|
|
63
|
-
? { ...mutationData, [model.primaryKey]:
|
|
65
|
+
? { ...mutationData, [model.primaryKey]: uidOrData }
|
|
64
66
|
: { ...mutationData }
|
|
65
67
|
const entries = Object.entries(safeData).filter(([, value]) => value !== undefined)
|
|
66
68
|
const columns = entries.map(([column]) => quoteIdentifier(column, 'data column'))
|
|
@@ -74,7 +74,7 @@ export async function reloadPlugin(app, options = {}) {
|
|
|
74
74
|
// copy rooms
|
|
75
75
|
for (const room of fromSocketRooms) {
|
|
76
76
|
if (room === fromSocketId) continue // do not include room associated to socket#id
|
|
77
|
-
toSocket.join(room)
|
|
77
|
+
await toSocket.join(room)
|
|
78
78
|
}
|
|
79
79
|
// copy data
|
|
80
80
|
toSocket.data = {
|
|
@@ -12,11 +12,15 @@ class FakeSocket {
|
|
|
12
12
|
this.emitted = []
|
|
13
13
|
this.sockets = sockets
|
|
14
14
|
this.disconnectingListener = disconnectingListener
|
|
15
|
+
this.joinDelay = Promise.resolve()
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
on(event, handler) { this.handlers.set(event, handler) }
|
|
18
19
|
emit(event, ...args) { this.emitted.push([event, ...args]) }
|
|
19
|
-
join(room) {
|
|
20
|
+
async join(room) {
|
|
21
|
+
await this.joinDelay
|
|
22
|
+
this.rooms.add(room)
|
|
23
|
+
}
|
|
20
24
|
disconnect() {
|
|
21
25
|
this.disconnectingListener(this, 'server namespace disconnect')
|
|
22
26
|
this.sockets.delete(this.id)
|
|
@@ -46,9 +50,16 @@ test('transfers state when the replacement connects before the old socket discon
|
|
|
46
50
|
const transferToken = oldSocket.emitted.find(([event]) => event === 'cnx-transfer-token')[1]
|
|
47
51
|
|
|
48
52
|
const newSocket = new FakeSocket('new', sockets, (...args) => disconnectingListener(...args))
|
|
53
|
+
let finishJoin
|
|
54
|
+
newSocket.joinDelay = new Promise(resolve => { finishJoin = resolve })
|
|
49
55
|
sockets.set(newSocket.id, newSocket)
|
|
50
56
|
connectListener(newSocket)
|
|
51
|
-
|
|
57
|
+
const transfer = newSocket.handlers.get('cnx-transfer')('old', 'new', transferToken)
|
|
58
|
+
|
|
59
|
+
await Promise.resolve()
|
|
60
|
+
assert.equal(newSocket.emitted.some(([event]) => event === 'cnx-transfer-ack'), false)
|
|
61
|
+
finishJoin()
|
|
62
|
+
await transfer
|
|
52
63
|
|
|
53
64
|
assert.equal(newSocket.data.user, 'Ada')
|
|
54
65
|
assert.equal(newSocket.rooms.has('project:1'), true)
|