@jcbuisson/express-x-drizzle 3.1.6 → 3.1.11
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 +1 -1
- package/src/drizzle-plugins.mjs +99 -23
package/package.json
CHANGED
package/src/drizzle-plugins.mjs
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import { and, eq, gt, gte, lt, lte, isNull, getTableName } from "drizzle-orm";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { truncateString, computeSyncResult } from '@jcbuisson/express-x'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
////////////////////////// UTILITIES //////////////////////////
|
|
7
7
|
|
|
8
|
-
function stringifyWithSortedKeys(obj) {
|
|
9
|
-
return JSON.stringify(obj, (_, value) => {
|
|
10
|
-
if (value && typeof value === 'object' && !Array.isArray(value) && Object.prototype.toString.call(value) === '[object Object]') {
|
|
11
|
-
const sorted = {}
|
|
12
|
-
Object.keys(value).sort().forEach(k => { sorted[k] = value[k] })
|
|
13
|
-
return sorted
|
|
14
|
-
}
|
|
15
|
-
return value
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
|
|
19
8
|
function whereToDrizzleFilters(table, where) {
|
|
20
9
|
const conditions = Object.entries(where)
|
|
21
10
|
.filter(([_, value]) => value !== undefined)
|
|
@@ -35,8 +24,98 @@ function whereToDrizzleFilters(table, where) {
|
|
|
35
24
|
return conditions.length ? and(...conditions) : undefined;
|
|
36
25
|
}
|
|
37
26
|
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return value && typeof value === 'object' && !Array.isArray(value) && Object.prototype.toString.call(value) === '[object Object]'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasRangeOperator(value) {
|
|
32
|
+
return isPlainObject(value) && ['gte', 'gt', 'lte', 'lt'].some(key => key in value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function valueWithinRange(value, range) {
|
|
36
|
+
if (value === null || value === undefined) return false
|
|
37
|
+
if ('gte' in range && value < range.gte) return false
|
|
38
|
+
if ('gt' in range && value <= range.gt) return false
|
|
39
|
+
if ('lte' in range && value > range.lte) return false
|
|
40
|
+
if ('lt' in range && value >= range.lt) return false
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rangesOverlap(a, b) {
|
|
45
|
+
const lower = [
|
|
46
|
+
'gte' in a && { value: a.gte, inclusive: true },
|
|
47
|
+
'gt' in a && { value: a.gt, inclusive: false },
|
|
48
|
+
'gte' in b && { value: b.gte, inclusive: true },
|
|
49
|
+
'gt' in b && { value: b.gt, inclusive: false },
|
|
50
|
+
].filter(Boolean).sort((x, y) => x.value === y.value ? Number(x.inclusive) - Number(y.inclusive) : x.value > y.value ? -1 : 1)[0]
|
|
51
|
+
|
|
52
|
+
const upper = [
|
|
53
|
+
'lte' in a && { value: a.lte, inclusive: true },
|
|
54
|
+
'lt' in a && { value: a.lt, inclusive: false },
|
|
55
|
+
'lte' in b && { value: b.lte, inclusive: true },
|
|
56
|
+
'lt' in b && { value: b.lt, inclusive: false },
|
|
57
|
+
].filter(Boolean).sort((x, y) => x.value === y.value ? Number(x.inclusive) - Number(y.inclusive) : x.value < y.value ? -1 : 1)[0]
|
|
58
|
+
|
|
59
|
+
if (!lower || !upper) return true
|
|
60
|
+
if (lower.value < upper.value) return true
|
|
61
|
+
if (lower.value > upper.value) return false
|
|
62
|
+
return lower.inclusive && upper.inclusive
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function constraintsOverlap(a, b) {
|
|
66
|
+
if (a === undefined || b === undefined) return true
|
|
67
|
+
if (a === null || b === null) return a === null && b === null
|
|
68
|
+
|
|
69
|
+
const aRange = hasRangeOperator(a)
|
|
70
|
+
const bRange = hasRangeOperator(b)
|
|
71
|
+
|
|
72
|
+
if (aRange && bRange) return rangesOverlap(a, b)
|
|
73
|
+
if (aRange) return valueWithinRange(b, a)
|
|
74
|
+
if (bRange) return valueWithinRange(a, b)
|
|
75
|
+
if (isPlainObject(a) || isPlainObject(b)) return true
|
|
76
|
+
return a === b
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function whereScopesOverlap(a = {}, b = {}) {
|
|
80
|
+
const sharedKeys = Object.keys(a).filter(key => key in b)
|
|
81
|
+
for (const key of sharedKeys) {
|
|
82
|
+
if (!constraintsOverlap(a[key], b[key])) return false
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class OverlapLock {
|
|
88
|
+
constructor() {
|
|
89
|
+
this.active = []
|
|
90
|
+
this.queue = []
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
acquire(where) {
|
|
94
|
+
return new Promise(resolve => {
|
|
95
|
+
const entry = { where, resolve }
|
|
96
|
+
this.queue.push(entry)
|
|
97
|
+
this.pump()
|
|
98
|
+
})
|
|
99
|
+
}
|
|
38
100
|
|
|
39
|
-
|
|
101
|
+
pump() {
|
|
102
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
103
|
+
const entry = this.queue[i]
|
|
104
|
+
if (this.active.some(active => whereScopesOverlap(active.where, entry.where))) continue
|
|
105
|
+
this.queue.splice(i, 1)
|
|
106
|
+
i -= 1
|
|
107
|
+
this.active.push(entry)
|
|
108
|
+
entry.resolve(() => {
|
|
109
|
+
this.active = this.active.filter(active => active !== entry)
|
|
110
|
+
this.pump()
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get idle() {
|
|
116
|
+
return this.active.length === 0 && this.queue.length === 0
|
|
117
|
+
}
|
|
118
|
+
}
|
|
40
119
|
|
|
41
120
|
|
|
42
121
|
////////////////////////// DRIZZLE OFFLINE PLUGIN //////////////////////////
|
|
@@ -109,7 +188,7 @@ export function drizzleOfflinePlugin(app, db, metadata, models) {
|
|
|
109
188
|
})
|
|
110
189
|
}
|
|
111
190
|
|
|
112
|
-
const
|
|
191
|
+
const syncLocks = new Map()
|
|
113
192
|
|
|
114
193
|
// add a synchronization service
|
|
115
194
|
app.createService('sync', {
|
|
@@ -117,11 +196,10 @@ export function drizzleOfflinePlugin(app, db, metadata, models) {
|
|
|
117
196
|
// CUTOFFDATE INUTILE ?
|
|
118
197
|
go: async (modelName, where, cutoffDate, clientMetadataDict) => {
|
|
119
198
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await syncMutexes.get(mutexKey).acquire()
|
|
199
|
+
// overlap-aware lock so independent scopes can still run in parallel, but overlapping where predicates do not
|
|
200
|
+
if (!syncLocks.has(modelName)) syncLocks.set(modelName, new OverlapLock())
|
|
201
|
+
const syncLock = syncLocks.get(modelName)
|
|
202
|
+
const releaseSyncLock = await syncLock.acquire(where)
|
|
125
203
|
|
|
126
204
|
try {
|
|
127
205
|
console.log('>>>>> SYNC', modelName, where, cutoffDate)
|
|
@@ -166,10 +244,8 @@ export function drizzleOfflinePlugin(app, db, metadata, models) {
|
|
|
166
244
|
console.log('*** err sync', err)
|
|
167
245
|
throw err
|
|
168
246
|
} finally {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// Remove idle mutexes so the Map stays bounded for dynamic where clauses.
|
|
172
|
-
if (mutex.queue.length === 0) syncMutexes.delete(mutexKey)
|
|
247
|
+
releaseSyncLock()
|
|
248
|
+
if (syncLock.idle) syncLocks.delete(modelName)
|
|
173
249
|
}
|
|
174
250
|
},
|
|
175
251
|
})
|