shuttlepro-shared 1.2.4 → 1.2.5
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.
|
@@ -1,85 +1,258 @@
|
|
|
1
|
-
const { getRedisData, setRedisData } = require("../../config/redis");
|
|
2
|
-
const Label = require("../../models/Label");
|
|
1
|
+
// const { getRedisData, setRedisData } = require("../../config/redis");
|
|
2
|
+
// const Label = require("../../models/Label");
|
|
3
3
|
|
|
4
|
-
const CACHE_KEY_ALL = "labels_all";
|
|
5
|
-
|
|
6
|
-
const getCachedLabels = async () => {
|
|
7
|
-
let labels = await getRedisData(CACHE_KEY_ALL);
|
|
8
|
-
if (!labels) {
|
|
9
|
-
labels = await Label.find().lean().exec();
|
|
10
|
-
await setRedisData(CACHE_KEY_ALL, JSON.stringify(labels));
|
|
11
|
-
} else {
|
|
12
|
-
labels = JSON.parse(labels);
|
|
13
|
-
}
|
|
14
|
-
return labels;
|
|
15
|
-
};
|
|
4
|
+
// const CACHE_KEY_ALL = "labels_all";
|
|
16
5
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
// const getCachedLabels = async () => {
|
|
7
|
+
// let labels = await getRedisData(CACHE_KEY_ALL);
|
|
8
|
+
// if (!labels) {
|
|
9
|
+
// labels = await Label.find().lean().exec();
|
|
10
|
+
// await setRedisData(CACHE_KEY_ALL, JSON.stringify(labels));
|
|
11
|
+
// } else {
|
|
12
|
+
// labels = JSON.parse(labels);
|
|
13
|
+
// }
|
|
14
|
+
// return labels;
|
|
15
|
+
// };
|
|
21
16
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return savedLabel;
|
|
27
|
-
};
|
|
17
|
+
// const updateCachedLabels = async () => {
|
|
18
|
+
// const labels = await Label.find().lean().exec();
|
|
19
|
+
// await setRedisData(CACHE_KEY_ALL, JSON.stringify(labels));
|
|
20
|
+
// };
|
|
28
21
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
// const createLabel = async (data) => {
|
|
23
|
+
// const newLabel = new Label(data);
|
|
24
|
+
// const savedLabel = await newLabel.save();
|
|
25
|
+
// await updateCachedLabels();
|
|
26
|
+
// return savedLabel;
|
|
27
|
+
// };
|
|
28
|
+
|
|
29
|
+
// const findLabelById = async (id) => {
|
|
30
|
+
// const labels = await getCachedLabels();
|
|
31
|
+
// return labels.find((l) => l._id.toString() === id) || null;
|
|
32
|
+
// };
|
|
33
|
+
|
|
34
|
+
// const findLabel = async (filter) => {
|
|
35
|
+
// const labels = await getCachedLabels();
|
|
36
|
+
// return (
|
|
37
|
+
// labels.find((l) =>
|
|
38
|
+
// Object.entries(filter).every(([key, value]) => l[key] === value)
|
|
39
|
+
// ) || null
|
|
40
|
+
// );
|
|
41
|
+
// };
|
|
42
|
+
|
|
43
|
+
// const findAllLabels = async (filter = {}) => {
|
|
44
|
+
// const labels = await getCachedLabels();
|
|
45
|
+
// return labels.filter((l) =>
|
|
46
|
+
// Object.entries(filter).every(([key, value]) => l[key] === value)
|
|
47
|
+
// );
|
|
48
|
+
// };
|
|
49
|
+
|
|
50
|
+
// const updateLabelById = async (id, data) => {
|
|
51
|
+
// const updatedLabel = await Label.findByIdAndUpdate(id, data, {
|
|
52
|
+
// new: true,
|
|
53
|
+
// }).exec();
|
|
54
|
+
// if (updatedLabel) {
|
|
55
|
+
// await updateCachedLabels();
|
|
56
|
+
// }
|
|
57
|
+
// return updatedLabel;
|
|
58
|
+
// };
|
|
59
|
+
// const updateLabel = async (filter, data) => {
|
|
60
|
+
// const updatedLabel = await Label.findOneAndUpdate(filter, data, {
|
|
61
|
+
// new: true,
|
|
62
|
+
// }).exec();
|
|
63
|
+
// if (updatedLabel) {
|
|
64
|
+
// await updateCachedLabels();
|
|
65
|
+
// }
|
|
66
|
+
// return updatedLabel;
|
|
67
|
+
// };
|
|
68
|
+
|
|
69
|
+
// const deleteLabel = async (id) => {
|
|
70
|
+
// const deletedLabel = await Label.findByIdAndDelete(id).exec();
|
|
71
|
+
// if (deletedLabel) {
|
|
72
|
+
// await updateCachedLabels();
|
|
73
|
+
// }
|
|
74
|
+
// return deletedLabel;
|
|
75
|
+
// };
|
|
33
76
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
77
|
+
// module.exports = {
|
|
78
|
+
// createLabel,
|
|
79
|
+
// findLabelById,
|
|
80
|
+
// findLabel,
|
|
81
|
+
// findAllLabels,
|
|
82
|
+
// updateLabel,
|
|
83
|
+
// deleteLabel,
|
|
84
|
+
// updateLabelById,
|
|
85
|
+
// };
|
|
86
|
+
|
|
87
|
+
const R = require("ramda");
|
|
88
|
+
const Label = require("../../models/Label");
|
|
89
|
+
const {
|
|
90
|
+
setRedisData,
|
|
91
|
+
getRedisData,
|
|
92
|
+
deleteRedisData,
|
|
93
|
+
} = require("../../config/redis");
|
|
94
|
+
|
|
95
|
+
// Configuration as a pure function
|
|
96
|
+
const createConfig = (env) => ({
|
|
97
|
+
cache: {
|
|
98
|
+
keys: {
|
|
99
|
+
ALL_LABELS: "labels:all",
|
|
100
|
+
WORKSPACE_LABELS: R.memoizeWith(
|
|
101
|
+
R.identity,
|
|
102
|
+
(workspaceId) => `labels:workspace:${workspaceId}`
|
|
103
|
+
),
|
|
104
|
+
LABEL_DETAILS: R.memoizeWith(
|
|
105
|
+
R.identity,
|
|
106
|
+
(labelId) => `labels:details:${labelId}`
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
expiry: 60 * 60, // 1 hour
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Pure query construction function
|
|
114
|
+
const constructQuery = R.curry((baseQuery, additionalQuery) =>
|
|
115
|
+
R.mergeDeepRight(baseQuery, additionalQuery)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Functional label repository
|
|
119
|
+
const createLabelRepository = (dependencies) => {
|
|
120
|
+
const { Label, redisConfig } = dependencies;
|
|
121
|
+
|
|
122
|
+
// Pure function for generating cache key
|
|
123
|
+
const generateCacheKey = R.pipe(
|
|
124
|
+
R.toPairs,
|
|
125
|
+
R.map(([key, value]) => `${key}:${JSON.stringify(value)}`),
|
|
126
|
+
R.join("|"),
|
|
127
|
+
R.concat("labels:query:")
|
|
40
128
|
);
|
|
41
|
-
};
|
|
42
129
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
130
|
+
// Caching Higher-Order Function
|
|
131
|
+
const withCache = R.curry(async (cacheKey, fetchFn, forceFresh = false) => {
|
|
132
|
+
if (!forceFresh) {
|
|
133
|
+
const cachedData = await getRedisData(cacheKey);
|
|
134
|
+
if (cachedData) return cachedData;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = await fetchFn();
|
|
138
|
+
|
|
139
|
+
if (data && data.length) {
|
|
140
|
+
await setRedisData(cacheKey, data, null, redisConfig.cache.expiry);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return data;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Pure validation function
|
|
147
|
+
const validateLabel = R.curry((existingLabels, newLabel) => {
|
|
148
|
+
const isDuplicate = R.any(
|
|
149
|
+
(label) =>
|
|
150
|
+
label.title.toLowerCase() === newLabel.title.toLowerCase() &&
|
|
151
|
+
R.equals(label.workspaceId, newLabel.workspaceId)
|
|
152
|
+
)(existingLabels);
|
|
153
|
+
|
|
154
|
+
if (isDuplicate) {
|
|
155
|
+
throw new Error("Label with this title already exists");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return newLabel;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Functional CRUD Operations
|
|
162
|
+
const createLabel = async (labelData) => {
|
|
163
|
+
// Compose validation and creation
|
|
164
|
+
const create = R.composeP(
|
|
165
|
+
(label) => label.save(),
|
|
166
|
+
validateLabel(await Label.find()),
|
|
167
|
+
Label.hydrate
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return create(labelData);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Advanced finding with functional composition
|
|
174
|
+
const findLabels = R.curry(async (options = {}, query = {}) => {
|
|
175
|
+
const {
|
|
176
|
+
lean = true,
|
|
177
|
+
populate = [],
|
|
178
|
+
sort = { createdAt: -1 },
|
|
179
|
+
limit = 100,
|
|
180
|
+
} = options;
|
|
181
|
+
|
|
182
|
+
// Compose query construction and execution
|
|
183
|
+
const executeQuery = R.composeP(
|
|
184
|
+
R.when(R.always(lean), R.map(R.prop("toObject"))),
|
|
185
|
+
(labels) => (populate.length ? Label.populate(labels, populate) : labels),
|
|
186
|
+
(labels) => R.take(limit, labels),
|
|
187
|
+
(labels) =>
|
|
188
|
+
R.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt), labels)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Use cache wrapper
|
|
192
|
+
const cachedFind = withCache(
|
|
193
|
+
generateCacheKey(query),
|
|
194
|
+
() => Label.find(query).exec(),
|
|
195
|
+
false
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return executeQuery(await cachedFind());
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Functional update with immutable updates
|
|
202
|
+
const updateLabel = R.curry(async (labelId, updateData) => {
|
|
203
|
+
const update = R.composeP(
|
|
204
|
+
R.tap(async (updatedLabel) => {
|
|
205
|
+
// Update cache atomically
|
|
206
|
+
await setRedisData(
|
|
207
|
+
redisConfig.cache.keys.LABEL_DETAILS(labelId),
|
|
208
|
+
updatedLabel.toObject()
|
|
209
|
+
);
|
|
210
|
+
}),
|
|
211
|
+
(label) => label.save(),
|
|
212
|
+
(label) => {
|
|
213
|
+
// Immutable update
|
|
214
|
+
const updatedFields = R.mergeDeepRight(label.toObject(), updateData);
|
|
215
|
+
return Label.hydrate(updatedFields);
|
|
216
|
+
},
|
|
217
|
+
() => Label.findById(labelId)
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return update();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Functional delete with side effect management
|
|
224
|
+
const deleteLabel = async (labelId) => {
|
|
225
|
+
const performDelete = R.composeP(
|
|
226
|
+
R.tap(async () => {
|
|
227
|
+
// Clear all related caches
|
|
228
|
+
await deleteRedisData(redisConfig.cache.keys.LABEL_DETAILS(labelId));
|
|
229
|
+
}),
|
|
230
|
+
(label) => label.remove(),
|
|
231
|
+
() => Label.findById(labelId)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return performDelete();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Advanced filtering with functional transformations
|
|
238
|
+
const filterLabels = R.curry((filterFn, labels) =>
|
|
239
|
+
R.filter(filterFn, labels)
|
|
47
240
|
);
|
|
48
|
-
};
|
|
49
241
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
const updateLabel = async (filter, data) => {
|
|
60
|
-
const updatedLabel = await Label.findOneAndUpdate(filter, data, {
|
|
61
|
-
new: true,
|
|
62
|
-
}).exec();
|
|
63
|
-
if (updatedLabel) {
|
|
64
|
-
await updateCachedLabels();
|
|
65
|
-
}
|
|
66
|
-
return updatedLabel;
|
|
242
|
+
// Public API
|
|
243
|
+
return {
|
|
244
|
+
createLabel,
|
|
245
|
+
findLabels,
|
|
246
|
+
updateLabel,
|
|
247
|
+
deleteLabel,
|
|
248
|
+
filterLabels,
|
|
249
|
+
};
|
|
67
250
|
};
|
|
68
251
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
return deletedLabel;
|
|
252
|
+
// Dependency Injection
|
|
253
|
+
const dependencies = {
|
|
254
|
+
Label,
|
|
255
|
+
redisConfig: createConfig(process.env),
|
|
75
256
|
};
|
|
76
257
|
|
|
77
|
-
module.exports =
|
|
78
|
-
createLabel,
|
|
79
|
-
findLabelById,
|
|
80
|
-
findLabel,
|
|
81
|
-
findAllLabels,
|
|
82
|
-
updateLabel,
|
|
83
|
-
deleteLabel,
|
|
84
|
-
updateLabelById,
|
|
85
|
-
};
|
|
258
|
+
module.exports = createLabelRepository(dependencies);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shuttlepro-shared",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"@babel/plugin-proposal-decorators": "^7.22.5",
|
|
17
17
|
"@babel/preset-env": "^7.22.5",
|
|
18
18
|
"@babel/register": "^7.22.5",
|
|
19
|
-
"
|
|
19
|
+
"bull": "^4.10.4",
|
|
20
|
+
"cors": "^2.8.5",
|
|
20
21
|
"express": "^4.17.1",
|
|
22
|
+
"helmet": "^8.0.0",
|
|
21
23
|
"jsonwebtoken": "^9.0.2",
|
|
22
24
|
"mongoose": "^8.7.1",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"bull": "^4.10.4",
|
|
25
|
+
"ramda": "^0.30.1",
|
|
26
|
+
"redis": "^4.6.14",
|
|
26
27
|
"socket.io": "^4.8.1"
|
|
27
28
|
}
|
|
28
29
|
}
|