@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 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
+ }