@starklabs/backend-core 1.1.0 → 1.2.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/.env.example +21 -0
- package/README.md +7 -7
- package/dist/js/config/cloudinary.js +18 -0
- package/dist/js/config/config.js +11 -0
- package/dist/js/config/duration.js +22 -0
- package/dist/js/core/app.js +122 -0
- package/dist/js/core/auth/OTP.js +115 -0
- package/dist/js/core/auth/auth.controller.js +63 -0
- package/dist/js/core/auth/auth.service.js +290 -0
- package/dist/js/core/auth/auth.validation.js +95 -0
- package/dist/js/core/crud/crud.controller.js +95 -0
- package/dist/js/core/crud/crud.service.js +296 -0
- package/dist/js/core/index.js +3 -0
- package/dist/js/index.js +44 -55
- package/dist/js/lib/db.js +40 -0
- package/dist/js/lib/field.types.js +174 -0
- package/dist/js/lib/model.factory.js +19 -0
- package/dist/js/lib/model.registry.js +4 -0
- package/dist/js/lib/schema.builder.js +35 -0
- package/dist/js/lib/zod.validations.js +247 -0
- package/dist/js/middleware/auth.middleware.js +51 -0
- package/dist/js/middleware/error.middleware.js +28 -0
- package/dist/js/middleware/socket.middleware.js +29 -0
- package/dist/js/utils/AppLog.js +2 -1
- package/dist/js/utils/deleteFile.js +22 -0
- package/dist/js/utils/index.js +10 -1
- package/dist/js/utils/jwt.js +12 -20
- package/dist/js/utils/libsodium.js +19 -3
- package/dist/js/utils/rateLimiter.js +25 -0
- package/dist/js/utils/uploadFile.js +43 -0
- package/handlerMap.js +33 -0
- package/package.json +18 -5
- package/test.js +36 -0
- package/dist/cjs/db.cjs +0 -17
- package/dist/cjs/index.cjs +0 -59
- package/dist/cjs/utils/AppError.cjs +0 -13
- package/dist/cjs/utils/AppLog.cjs +0 -13
- package/dist/cjs/utils/asyncHandler.cjs +0 -6
- package/dist/cjs/utils/jwt.cjs +0 -38
- package/dist/cjs/utils/libsodium.cjs +0 -145
- package/dist/cjs/utils/successResponse.cjs +0 -13
- package/dist/js/db.js +0 -19
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { getConfig } from "../../config/config.js";
|
|
2
|
+
import { hash } from "../../utils/libsodium.js";
|
|
3
|
+
import { AppError, uploadFile, deleteFile } from "../../utils/index.js";
|
|
4
|
+
|
|
5
|
+
const sanitize = (doc) => {
|
|
6
|
+
if (!doc) return doc;
|
|
7
|
+
const { __v, password, ...clean } = doc;
|
|
8
|
+
return clean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const authorizeAccess = ({ ownerEmail, dataEmail, role }) => {
|
|
12
|
+
const { internalRoles } = getConfig();
|
|
13
|
+
|
|
14
|
+
if (!internalRoles?.length)
|
|
15
|
+
throw new AppError("internalRoles is missing in StarkCore({})");
|
|
16
|
+
|
|
17
|
+
const isInternalRuler = internalRoles.includes(role);
|
|
18
|
+
const isOwner = ownerEmail === dataEmail;
|
|
19
|
+
|
|
20
|
+
if (!isInternalRuler && !isOwner) throw new AppError("Unauthorized", 401);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const assertInternalRole = (role) => {
|
|
24
|
+
const { internalRoles } = getConfig();
|
|
25
|
+
|
|
26
|
+
const isInternalRuler = internalRoles.includes(role);
|
|
27
|
+
if (!isInternalRuler) throw new AppError("Unauthorized", 401);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const showHealth = () => {
|
|
31
|
+
return {
|
|
32
|
+
msg: "All clear!",
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getAll = async ({ Model, modelName }) => {
|
|
37
|
+
const data = await Model.find().lean();
|
|
38
|
+
|
|
39
|
+
if (!data.length) {
|
|
40
|
+
throw new AppError(`${modelName} not found`, 404);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
data: data.map(sanitize),
|
|
45
|
+
msg: "Fetched all successfully",
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getById = async ({ Model, modelName, id, userData, body }) => {
|
|
50
|
+
const data = await Model.findById(id).lean();
|
|
51
|
+
|
|
52
|
+
if (!data) {
|
|
53
|
+
throw new AppError(`${modelName} with id ${id} not found`, 404);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
authorizeAccess({
|
|
57
|
+
ownerEmail: userData.email,
|
|
58
|
+
dataEmail: data.email,
|
|
59
|
+
role: userData.role,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
data: sanitize(data),
|
|
64
|
+
msg: "Fetched successfully",
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const create = async ({ Model, modelName, userData, body }) => {
|
|
69
|
+
assertInternalRole(userData.role);
|
|
70
|
+
|
|
71
|
+
const { cloudinaryFolderName } = getConfig();
|
|
72
|
+
|
|
73
|
+
// if image
|
|
74
|
+
let imageMetaData = undefined;
|
|
75
|
+
if (body.image) {
|
|
76
|
+
imageMetaData = await uploadFile(body.image, cloudinaryFolderName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// if user
|
|
80
|
+
if (body.email) {
|
|
81
|
+
const exists = await Model.findOne({ email: body.email });
|
|
82
|
+
if (exists) {
|
|
83
|
+
throw new AppError(`${modelName} with this email already exists`, 409);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (body.password) body.password = await hash(body.password);
|
|
88
|
+
|
|
89
|
+
const newItem = await Model.create({
|
|
90
|
+
...body,
|
|
91
|
+
role: "user",
|
|
92
|
+
imageMetaData,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
data: sanitize(newItem.toObject()),
|
|
97
|
+
msg: "Created successfully",
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const update = async ({ Model, modelName, id, body, userData }) => {
|
|
102
|
+
assertInternalRole(userData.role);
|
|
103
|
+
|
|
104
|
+
if (body.password) {
|
|
105
|
+
body.password = await hash(body.password);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const existing = await Model.findById(id);
|
|
109
|
+
|
|
110
|
+
if (!existing) {
|
|
111
|
+
throw new AppError(`${modelName} not found for update`, 404);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { cloudinaryFolderName } = getConfig();
|
|
115
|
+
|
|
116
|
+
let imageMetaData;
|
|
117
|
+
if (body.image) {
|
|
118
|
+
imageMetaData = await uploadFile(body.image, cloudinaryFolderName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const replacementDoc = {
|
|
123
|
+
...body,
|
|
124
|
+
...(imageMetaData && { imageMetaData }),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Prevent storing raw file object/base64/etc.
|
|
128
|
+
delete replacementDoc.image;
|
|
129
|
+
|
|
130
|
+
const updated = await Model.findOneAndReplace({ _id: id }, replacementDoc, {
|
|
131
|
+
returnDocument: "after",
|
|
132
|
+
}).lean();
|
|
133
|
+
|
|
134
|
+
// Delete old image after successful replacement
|
|
135
|
+
if (imageMetaData && existing.imageMetaData?.public_id) {
|
|
136
|
+
await deleteFile(existing.imageMetaData.public_id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
data: sanitize(updated),
|
|
141
|
+
msg: "Updated successfully",
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Rollback newly uploaded image
|
|
145
|
+
if (imageMetaData?.public_id) {
|
|
146
|
+
await deleteFile(imageMetaData.public_id).catch(() => {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (error.message.includes("duplicate key error")) {
|
|
150
|
+
throw new AppError(`${modelName} with these detail already exists`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const patch = async ({ Model, modelName, id, body, userData }) => {
|
|
158
|
+
assertInternalRole(userData.role);
|
|
159
|
+
|
|
160
|
+
if (body.password) {
|
|
161
|
+
body.password = await hash(body.password);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const existing = await Model.findById(id);
|
|
165
|
+
|
|
166
|
+
if (!existing) {
|
|
167
|
+
throw new AppError(`${modelName} not found for patch`, 404);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { cloudinaryFolderName } = getConfig();
|
|
171
|
+
|
|
172
|
+
let imageMetaData;
|
|
173
|
+
if (body.image) {
|
|
174
|
+
// Upload new image first
|
|
175
|
+
imageMetaData = await uploadFile(body.image, cloudinaryFolderName);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const updated = await Model.findByIdAndUpdate(
|
|
180
|
+
id,
|
|
181
|
+
{
|
|
182
|
+
$set: {
|
|
183
|
+
...body,
|
|
184
|
+
...(imageMetaData && { imageMetaData }),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{ returnDocument: "after" },
|
|
188
|
+
).lean();
|
|
189
|
+
|
|
190
|
+
// Delete old image only after successful DB update
|
|
191
|
+
if (imageMetaData && existing.imageMetaData?.public_id) {
|
|
192
|
+
await deleteFile(existing.imageMetaData.public_id);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
data: sanitize(updated),
|
|
197
|
+
msg: "Updated successfully!",
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// Rollback newly uploaded image if DB update fails
|
|
201
|
+
if (imageMetaData?.public_id) {
|
|
202
|
+
await deleteFile(imageMetaData.public_id).catch(() => {});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (error.message.includes("duplicate key error")) {
|
|
206
|
+
throw new AppError(`${modelName} with these detail already exists`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const remove = async ({ Model, modelName, id, userData }) => {
|
|
214
|
+
assertInternalRole(userData.role);
|
|
215
|
+
|
|
216
|
+
const deleted = await Model.findByIdAndDelete(id).lean();
|
|
217
|
+
|
|
218
|
+
if (!deleted) {
|
|
219
|
+
throw new AppError(`${modelName} not found for delete`, 404);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
data: sanitize(deleted),
|
|
224
|
+
msg: "Deleted successfully",
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const removeFile = async ({
|
|
229
|
+
Model,
|
|
230
|
+
modelName,
|
|
231
|
+
id,
|
|
232
|
+
userData,
|
|
233
|
+
fileType,
|
|
234
|
+
metaDataName,
|
|
235
|
+
}) => {
|
|
236
|
+
if (!fileType)
|
|
237
|
+
throw new AppError(
|
|
238
|
+
`fileType is required in ${modelName.toLowerCase()} collection`,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (!metaDataName)
|
|
242
|
+
throw new AppError(
|
|
243
|
+
`metaDataName is required in ${modelName.toLowerCase()} collection`,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
assertInternalRole(userData.role);
|
|
247
|
+
|
|
248
|
+
const item = await Model.findById(id);
|
|
249
|
+
if (!item) throw new AppError(`${modelName} not found to delete file`);
|
|
250
|
+
|
|
251
|
+
if (Object.keys(item[metaDataName]).length === 0)
|
|
252
|
+
throw new AppError(`${fileType} not found to delete`);
|
|
253
|
+
|
|
254
|
+
if (Object.keys(item[metaDataName].toObject()).length === 0)
|
|
255
|
+
throw new AppError(`${fileType} not found to delete`);
|
|
256
|
+
|
|
257
|
+
const deleted = await deleteFile(item[metaDataName].public_id, fileType);
|
|
258
|
+
|
|
259
|
+
item[metaDataName] = undefined;
|
|
260
|
+
|
|
261
|
+
item.save();
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
data: item,
|
|
265
|
+
msg: "File deleted successfully",
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const removeAll = async ({ Model, modelName, id, userData }) => {
|
|
270
|
+
assertInternalRole(userData.role);
|
|
271
|
+
|
|
272
|
+
const deleted = await Model.deleteMany();
|
|
273
|
+
|
|
274
|
+
if (!deleted) {
|
|
275
|
+
throw new AppError(`${modelName} not found for delete`, 404);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
data: sanitize(deleted),
|
|
280
|
+
msg: "Deleted all successfully",
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// by deleting all, also delete the images from cloudinary 1/1
|
|
285
|
+
|
|
286
|
+
export default {
|
|
287
|
+
showHealth,
|
|
288
|
+
getAll,
|
|
289
|
+
getById,
|
|
290
|
+
create,
|
|
291
|
+
update,
|
|
292
|
+
patch,
|
|
293
|
+
remove,
|
|
294
|
+
removeFile,
|
|
295
|
+
removeAll,
|
|
296
|
+
};
|
package/dist/js/index.js
CHANGED
|
@@ -1,66 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { setConfig } from "./config/config.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} StarkCoreConfig
|
|
5
|
+
* @property {number} port - *Server listening port number*
|
|
6
|
+
* @property {Array<object>} collections - *Routes detail*
|
|
7
|
+
* @property {string} apiVersion - *Current API version*
|
|
8
|
+
* @property {string} jwtSecret - *Used for JSON Web Token functionalities*
|
|
9
|
+
* @property {boolean} isOffline - *Connects to local MongoDB instance*
|
|
10
|
+
* @property {string} masterKey - *Used for encryption/decryption operations*
|
|
11
|
+
* @property {string} ENV - *Current application environment (e.g. development, production)*
|
|
12
|
+
* @property {string} tokenExpiry - *JWT/token expiration duration*
|
|
13
|
+
* @property {Array<string>} internalRoles - *List of internal roles with elevated permissions*
|
|
14
|
+
* @property {string} resendAPIKey - *API key used for Resend email services*
|
|
15
|
+
* @property {string|number} rateLimitDuration - *Duration for rate limiting window*
|
|
16
|
+
* @property {number} maxReqLimit - *Maximum allowed requests within rate limit duration*
|
|
17
|
+
* @property {string} rateLimitMsg - *Message returned when rate limit is exceeded*
|
|
18
|
+
* @property {string} cloudinaryAPIKey - *Cloudinary API key*
|
|
19
|
+
* @property {string} cloudinaryCloudName - *Cloudinary cloud name*
|
|
20
|
+
* @property {string} cloudinaryAPISecret - *Cloudinary API secret*
|
|
21
|
+
* @property {string} cloudinaryFolderName - *Default Cloudinary folder for uploads*
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
class StarkCore {
|
|
25
|
+
/**
|
|
26
|
+
* @param {StarkCoreConfig} config
|
|
27
|
+
*/
|
|
3
28
|
|
|
4
|
-
// seal/unSeal - encrypt and decrypt data
|
|
5
|
-
import { seal, unSeal } from "./utils/libsodium.js";
|
|
6
|
-
import { signJWT, verifyJWT } from "./utils/jwt.js";
|
|
7
|
-
|
|
8
|
-
class StarkAuth {
|
|
9
|
-
#_masterKey;
|
|
10
|
-
#_JWT_SECRET;
|
|
11
|
-
#_JWT_EXPIRY;
|
|
12
|
-
|
|
13
|
-
// constructor - store keys etc.
|
|
14
29
|
constructor(config) {
|
|
15
|
-
|
|
16
|
-
this.#_JWT_SECRET = config.JWT_SECRET;
|
|
17
|
-
this.#_JWT_EXPIRY = config.JWT_EXPIRY;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// create - initializes DB and auth instance
|
|
21
|
-
static async create(config) {
|
|
22
|
-
await connectDB(config.MONGO_URI, config.DB_NAME);
|
|
23
|
-
return new StarkAuth(config);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// encrypt - encrypts string using master key
|
|
27
|
-
async encrypt(str) {
|
|
28
|
-
return seal(str, this.#_masterKey);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// decrypt - decrypts string using master key
|
|
32
|
-
async decrypt(str, nonce, publicKey, securedPrivateKey) {
|
|
33
|
-
return unSeal(str, nonce, publicKey, securedPrivateKey, this.#_masterKey);
|
|
30
|
+
setConfig(config);
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @param {StarkCoreConfig} config
|
|
35
|
+
* @returns {StarkCore}
|
|
36
|
+
*/
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return verifyJWT(token, this.#_JWT_SECRET);
|
|
38
|
+
static create(config) {
|
|
39
|
+
return new StarkCore(config);
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
|
|
47
|
-
export default
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
generateMasterKey,
|
|
43
|
+
export default StarkCore;
|
|
44
|
+
export { startServer } from "./core/index.js";
|
|
45
|
+
export { default as fieldTypes } from "./lib/field.types.js";
|
|
46
|
+
export { default as zodValidations } from "./lib/zod.validations.js";
|
|
47
|
+
export {
|
|
48
|
+
AppLog,
|
|
54
49
|
generateJWTSecret,
|
|
50
|
+
generateMasterKey,
|
|
55
51
|
hash,
|
|
52
|
+
seal,
|
|
53
|
+
unSeal,
|
|
56
54
|
verifyHash,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// AppError - custom application error class
|
|
60
|
-
export { default as AppError } from "./utils/AppError.js";
|
|
61
|
-
|
|
62
|
-
// AppLog - structured logging utility
|
|
63
|
-
export { default as AppLog } from "./utils/AppLog.js";
|
|
64
|
-
|
|
65
|
-
// async handler - AppError in async functions
|
|
66
|
-
export { default as asyncHandler } from "./utils/asyncHandler.js";
|
|
55
|
+
} from "./utils/index.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// module imports
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
|
|
4
|
+
import AppLog from "../utils/AppLog.js";
|
|
5
|
+
|
|
6
|
+
const connectLocally = async () => {
|
|
7
|
+
await mongoose.connect(`mongodb://localhost:27017/offline-db`);
|
|
8
|
+
AppLog("check", "db", "Connected to OFFLINE-DB!");
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// connecting to db
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {"MONGODB_URI"} uri
|
|
15
|
+
* @param {"DATABASE_NAME"} database
|
|
16
|
+
* @param {true | false} NETWORK
|
|
17
|
+
* @returns
|
|
18
|
+
*/
|
|
19
|
+
const connectDB = async (
|
|
20
|
+
MONGODB_URI = "",
|
|
21
|
+
DATABASE_NAME = "starklabs",
|
|
22
|
+
NETWORK = false,
|
|
23
|
+
) => {
|
|
24
|
+
const mongodbUri = MONGODB_URI || "mongodb://localhost:27017";
|
|
25
|
+
try {
|
|
26
|
+
if (!NETWORK) {
|
|
27
|
+
await connectLocally();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await mongoose.connect(`${mongodbUri}/${DATABASE_NAME}`);
|
|
32
|
+
AppLog("check", "db", "Connected successfully!");
|
|
33
|
+
} catch (error) {
|
|
34
|
+
AppLog("X", "db", "Error while connecting!");
|
|
35
|
+
AppLog("X", "db", error.message);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// export
|
|
40
|
+
export default connectDB;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
const { ObjectId } = mongoose.Schema.Types;
|
|
4
|
+
|
|
5
|
+
const requiredString = {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
trim: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const optionalString = {
|
|
12
|
+
type: String,
|
|
13
|
+
default: "",
|
|
14
|
+
trim: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const requiredUniqueString = {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true,
|
|
20
|
+
unique: true,
|
|
21
|
+
trim: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const email = {
|
|
25
|
+
type: String,
|
|
26
|
+
required: true,
|
|
27
|
+
unique: true,
|
|
28
|
+
lowercase: true,
|
|
29
|
+
trim: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const password = {
|
|
33
|
+
type: String,
|
|
34
|
+
required: true,
|
|
35
|
+
minlength: 6,
|
|
36
|
+
select: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const requiredNumber = {
|
|
40
|
+
type: Number,
|
|
41
|
+
required: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const optionalNumber = {
|
|
45
|
+
type: Number,
|
|
46
|
+
default: 0,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const booleanTrue = {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: true,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const booleanFalse = {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: false,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const dateNow = {
|
|
60
|
+
type: Date,
|
|
61
|
+
default: Date.now,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const stringArray = {
|
|
65
|
+
type: [String],
|
|
66
|
+
default: [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const objectArray = {
|
|
70
|
+
type: [Object],
|
|
71
|
+
default: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const objectId = {
|
|
75
|
+
type: ObjectId,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const requiredObjectId = {
|
|
79
|
+
type: ObjectId,
|
|
80
|
+
required: true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const userRef = {
|
|
84
|
+
type: ObjectId,
|
|
85
|
+
ref: "User",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const requiredUserRef = {
|
|
89
|
+
type: ObjectId,
|
|
90
|
+
ref: "User",
|
|
91
|
+
required: true,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const userRefArray = [
|
|
95
|
+
{
|
|
96
|
+
type: ObjectId,
|
|
97
|
+
ref: "User",
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const timestamps = {
|
|
102
|
+
createdAt: dateNow,
|
|
103
|
+
updatedAt: dateNow,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const provider = {
|
|
107
|
+
type: String,
|
|
108
|
+
enum: ["local", "google"],
|
|
109
|
+
default: "local",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const role = {
|
|
113
|
+
...requiredString,
|
|
114
|
+
enum: ["user", ...JSON.parse(process.env.INTERNAL_ROLES || "[]")],
|
|
115
|
+
default: "user",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const otp = {
|
|
119
|
+
type: String,
|
|
120
|
+
required: true,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const otpExpiry = {
|
|
124
|
+
type: Number,
|
|
125
|
+
required: true,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const otpCount = {
|
|
129
|
+
type: Number,
|
|
130
|
+
required: true,
|
|
131
|
+
default: 0,
|
|
132
|
+
max: [10, "OTP verification limit reached. Please try again later."],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const otpStatus = {
|
|
136
|
+
type: String,
|
|
137
|
+
enum: ["pending", "verified", "blocked"],
|
|
138
|
+
default: "pending",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const imageMetaData = {
|
|
142
|
+
public_id: { type: String, required: false },
|
|
143
|
+
secure_url: { type: String, required: false },
|
|
144
|
+
width: { type: Number, required: false },
|
|
145
|
+
height: { type: Number, required: false },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default {
|
|
149
|
+
requiredString,
|
|
150
|
+
optionalString,
|
|
151
|
+
requiredUniqueString,
|
|
152
|
+
email,
|
|
153
|
+
password,
|
|
154
|
+
requiredNumber,
|
|
155
|
+
optionalNumber,
|
|
156
|
+
booleanTrue,
|
|
157
|
+
booleanFalse,
|
|
158
|
+
dateNow,
|
|
159
|
+
stringArray,
|
|
160
|
+
objectArray,
|
|
161
|
+
objectId,
|
|
162
|
+
requiredObjectId,
|
|
163
|
+
userRef,
|
|
164
|
+
requiredUserRef,
|
|
165
|
+
userRefArray,
|
|
166
|
+
timestamps,
|
|
167
|
+
provider,
|
|
168
|
+
role,
|
|
169
|
+
otp,
|
|
170
|
+
otpExpiry,
|
|
171
|
+
otpCount,
|
|
172
|
+
otpStatus,
|
|
173
|
+
imageMetaData,
|
|
174
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
import buildSchema from "./schema.builder.js";
|
|
3
|
+
import AppLog from "../utils/AppLog.js";
|
|
4
|
+
import AppError from "../utils/AppError.js";
|
|
5
|
+
|
|
6
|
+
const createModel = (name, definition) => {
|
|
7
|
+
if (!name) throw new AppError("modelName is required");
|
|
8
|
+
if (!definition) throw new AppError("mongooseSchema is required");
|
|
9
|
+
|
|
10
|
+
const schema = buildSchema(definition);
|
|
11
|
+
|
|
12
|
+
if (mongoose.modelNames().includes(name)) {
|
|
13
|
+
return mongoose.model(name);
|
|
14
|
+
}
|
|
15
|
+
AppLog("db", "modelFactory", `${name} model created!`);
|
|
16
|
+
return mongoose.model(name, schema);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default createModel;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
const buildSchema = (definition) => {
|
|
4
|
+
const schemaObject = {};
|
|
5
|
+
|
|
6
|
+
for (const key in definition) {
|
|
7
|
+
const value = definition[key];
|
|
8
|
+
|
|
9
|
+
// CASE 1: primitive shorthand (String, Number, etc.)
|
|
10
|
+
if (
|
|
11
|
+
typeof value === "string" ||
|
|
12
|
+
typeof value === "number" ||
|
|
13
|
+
typeof value === "boolean"
|
|
14
|
+
) {
|
|
15
|
+
schemaObject[key] = { type: value };
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// CASE 2: already mongoose-style object
|
|
20
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
21
|
+
schemaObject[key] = value;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// CASE 3: array schema
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
schemaObject[key] = value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new mongoose.Schema(schemaObject, { timestamps: true });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default buildSchema;
|