@jcbuisson/express-x-drizzle 1.0.2
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 +1 -0
- package/package.json +34 -0
- package/src/drizzle-plugins.mjs +186 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# express-x-drizzle
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jcbuisson/express-x-drizzle",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "drizzle-plugins.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">= 14.0.0",
|
|
13
|
+
"npm": ">= 6.0.0"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+ssh://git@github.com/jcbuisson/express-x-drizzle.git"
|
|
19
|
+
},
|
|
20
|
+
"author": "Jean-Christophe Buisson <buisson@enseeiht.fr> (jcbuisson.dev)",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": "",
|
|
23
|
+
"keywords": [],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "",
|
|
26
|
+
"test": ""
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@jcbuisson/express-x": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { and, eq, getTableName } from "drizzle-orm";
|
|
2
|
+
import { Mutex, truncateString } from '@jcbuisson/express-x'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
////////////////////////// UTILITIES //////////////////////////
|
|
6
|
+
|
|
7
|
+
function whereToDrizzleFilters(table, filters) {
|
|
8
|
+
const conditions = Object.entries(filters)
|
|
9
|
+
.filter(([_, value]) => value !== undefined)
|
|
10
|
+
.map(([key, value]) => eq(table[key], value));
|
|
11
|
+
return conditions.length ? and(...conditions) : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
////////////////////////// DRIZZLE OFFLINE PLUGIN //////////////////////////
|
|
15
|
+
|
|
16
|
+
const mutex = new Mutex()
|
|
17
|
+
|
|
18
|
+
export function drizzleOfflinePlugin(app, db, metadata, models) {
|
|
19
|
+
|
|
20
|
+
// add a database service for each model
|
|
21
|
+
for (const model of models) {
|
|
22
|
+
const modelName = getTableName(model)
|
|
23
|
+
|
|
24
|
+
app.createService(modelName, {
|
|
25
|
+
|
|
26
|
+
findUnique: async (where) => {
|
|
27
|
+
const rows = await db.select().from(model).where(whereToDrizzleFilters(model, where));
|
|
28
|
+
return rows[0] ?? null;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
findMany: async (where) => {
|
|
32
|
+
return await db.select().from(model).where(whereToDrizzleFilters(model, where));
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
createWithMeta: async (uid, data, created_at) => {
|
|
36
|
+
db.transaction(async (tx) => {
|
|
37
|
+
const value = await tx.insert(model).values({ uid, ...data }).returning();
|
|
38
|
+
const meta = await tx.insert(metadata).values({ uid, created_at }).returning();
|
|
39
|
+
return [value, meta]
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
updateWithMeta: async (uid, data, updated_at) => {
|
|
44
|
+
db.transaction(async (tx) => {
|
|
45
|
+
const value = await tx.update(model).values(data).where(eq(model.uid, uid)).returning();
|
|
46
|
+
const meta = await tx.update(metadata).set({ updated_at }).where(eq(metadata.uid, uid)).returning();
|
|
47
|
+
return [value, meta]
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
deleteWithMeta: async (uid, deleted_at) => {
|
|
52
|
+
db.transaction(async (tx) => {
|
|
53
|
+
const value = await tx.delete(model).where(eq(model.uid, uid)).returning();
|
|
54
|
+
const meta = await tx.update(metadata).set({ deleted_at }).where(eq(metadata.uid, uid)).returning();
|
|
55
|
+
return [value, meta]
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// add a synchronization service
|
|
62
|
+
app.createService('sync', {
|
|
63
|
+
|
|
64
|
+
// AMÉLIORER : ne pas avoir une exclusion mutuelle globale, mais seulement par model/where
|
|
65
|
+
go: async (modelName, where, cutoffDate, clientMetadataDict) => {
|
|
66
|
+
await mutex.acquire()
|
|
67
|
+
try {
|
|
68
|
+
console.log('>>>>> SYNC', modelName, where, cutoffDate)
|
|
69
|
+
const databaseService = app.service(modelName)
|
|
70
|
+
|
|
71
|
+
// STEP 1: get existing database `where` values
|
|
72
|
+
const databaseValues = await databaseService.findMany(where)
|
|
73
|
+
|
|
74
|
+
const databaseValuesDict = databaseValues.reduce((accu, value) => {
|
|
75
|
+
accu[value.uid] = value
|
|
76
|
+
return accu
|
|
77
|
+
}, {})
|
|
78
|
+
// console.log('clientMetadataDict', clientMetadataDict)
|
|
79
|
+
// console.log('databaseValuesDict', databaseValuesDict)
|
|
80
|
+
|
|
81
|
+
// STEP 2: compute intersections between client and database uids
|
|
82
|
+
const onlyDatabaseIds = new Set()
|
|
83
|
+
const onlyClientIds = new Set()
|
|
84
|
+
const databaseAndClientIds = new Set()
|
|
85
|
+
|
|
86
|
+
for (const uid in databaseValuesDict) {
|
|
87
|
+
if (uid in clientMetadataDict) {
|
|
88
|
+
databaseAndClientIds.add(uid)
|
|
89
|
+
} else {
|
|
90
|
+
onlyDatabaseIds.add(uid)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const uid in clientMetadataDict) {
|
|
95
|
+
if (uid in databaseValuesDict) {
|
|
96
|
+
databaseAndClientIds.add(uid)
|
|
97
|
+
} else {
|
|
98
|
+
onlyClientIds.add(uid)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// console.log('onlyDatabaseIds', onlyDatabaseIds)
|
|
102
|
+
// console.log('onlyClientIds', onlyClientIds)
|
|
103
|
+
// console.log('databaseAndClientIds', databaseAndClientIds)
|
|
104
|
+
|
|
105
|
+
// STEP 3: build add/update/delete sets
|
|
106
|
+
const addDatabase = []
|
|
107
|
+
const updateDatabase = []
|
|
108
|
+
const deleteDatabase = []
|
|
109
|
+
|
|
110
|
+
const addClient = []
|
|
111
|
+
const updateClient = []
|
|
112
|
+
const deleteClient = []
|
|
113
|
+
|
|
114
|
+
for (const uid of onlyDatabaseIds) {
|
|
115
|
+
const databaseValue = databaseValuesDict[uid]
|
|
116
|
+
const databaseMetaData = (await db.select().from(metadata).where(eq(metadata.uid, uid)))[0]
|
|
117
|
+
|| { uid, created_at: new Date() } // should not happen
|
|
118
|
+
addClient.push([databaseValue, databaseMetaData])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const uid of onlyClientIds) {
|
|
122
|
+
const clientMetaData = clientMetadataDict[uid]
|
|
123
|
+
if (clientMetaData.deleted_at) {
|
|
124
|
+
deleteClient.push([uid, clientMetaData.deleted_at])
|
|
125
|
+
} else if (new Date(clientMetaData.created_at) > cutoffDate) {
|
|
126
|
+
addDatabase.push(clientMetaData)
|
|
127
|
+
} else {
|
|
128
|
+
// ???
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const uid of databaseAndClientIds) {
|
|
133
|
+
const databaseValue = databaseValuesDict[uid]
|
|
134
|
+
const clientMetaData = clientMetadataDict[uid]
|
|
135
|
+
|| { uid, created_at: new Date() } // should not happen
|
|
136
|
+
if (clientMetaData.deleted_at) {
|
|
137
|
+
deleteDatabase.push(uid)
|
|
138
|
+
deleteClient.push([uid, clientMetaData.deleted_at])
|
|
139
|
+
} else {
|
|
140
|
+
const databaseMetaData = (await db.select().from(metadata).where(eq(metadata.uid, uid)))[0]
|
|
141
|
+
|| { uid, created_at: new Date() } // should not happen
|
|
142
|
+
const clientUpdatedAt = new Date(clientMetaData.updated_at || clientMetaData.created_at)
|
|
143
|
+
const databaseUpdatedAt = new Date(databaseMetaData.updated_at || databaseMetaData.created_at)
|
|
144
|
+
const dateDifference = clientUpdatedAt - databaseUpdatedAt
|
|
145
|
+
// console.log('databaseMetaData', databaseMetaData, 'clientMetaData', clientMetaData, 'dateDifference', dateDifference)
|
|
146
|
+
if (dateDifference > 0) {
|
|
147
|
+
updateDatabase.push(clientMetaData)
|
|
148
|
+
} else if (dateDifference < 0) {
|
|
149
|
+
updateClient.push(databaseValue)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
console.log('addDatabase', truncateString(JSON.stringify(addDatabase)))
|
|
154
|
+
console.log('deleteDatabase', truncateString(JSON.stringify(deleteDatabase)))
|
|
155
|
+
console.log('updateDatabase', truncateString(JSON.stringify(updateDatabase)))
|
|
156
|
+
|
|
157
|
+
console.log('addClient', truncateString(JSON.stringify(addClient)))
|
|
158
|
+
console.log('deleteClient', truncateString(JSON.stringify(deleteClient)))
|
|
159
|
+
console.log('updateClient', truncateString(JSON.stringify(updateClient)))
|
|
160
|
+
|
|
161
|
+
// STEP4: execute database deletions
|
|
162
|
+
for (const uid of deleteDatabase) {
|
|
163
|
+
const clientMetaData = clientMetadataDict[uid]
|
|
164
|
+
// console.log('---delete', uid, clientMetaData)
|
|
165
|
+
await databaseService.deleteWithMeta(uid, clientMetaData.deleted_at)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// STEP5: return to client the changes to perform on its cache, and create/update to perform on database with full data
|
|
169
|
+
// database creations & updates are done later by the client with complete data (this function only has client values's meta-data)
|
|
170
|
+
return {
|
|
171
|
+
toAdd: addClient,
|
|
172
|
+
toUpdate: updateClient,
|
|
173
|
+
toDelete: deleteClient,
|
|
174
|
+
|
|
175
|
+
addDatabase,
|
|
176
|
+
updateDatabase,
|
|
177
|
+
}
|
|
178
|
+
} catch(err) {
|
|
179
|
+
console.log('*** err sync', err)
|
|
180
|
+
} finally {
|
|
181
|
+
mutex.release()
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
}
|