@xenterprises/fastify-xconfig 1.0.1 → 1.1.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/README.md +171 -18
- package/dist/integrations/cloudinary.d.ts +1 -0
- package/dist/integrations/cloudinary.js +25 -0
- package/dist/integrations/cloudinary.js.map +1 -0
- package/dist/integrations/prisma.d.ts +1 -0
- package/dist/integrations/prisma.js +13 -0
- package/dist/integrations/prisma.js.map +1 -0
- package/dist/integrations/sendgrid.d.ts +1 -0
- package/dist/integrations/sendgrid.js +22 -0
- package/dist/integrations/sendgrid.js.map +1 -0
- package/dist/integrations/stripe.d.ts +1 -0
- package/dist/integrations/stripe.js +15 -0
- package/dist/integrations/stripe.js.map +1 -0
- package/dist/integrations/twilio.d.ts +1 -0
- package/dist/integrations/twilio.js +17 -0
- package/dist/integrations/twilio.js.map +1 -0
- package/dist/middleware/bugsnag.d.ts +2 -0
- package/dist/middleware/bugsnag.js +9 -0
- package/dist/middleware/bugsnag.js.map +1 -0
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.js +11 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +2 -0
- package/dist/middleware/errorHandler.js +19 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/multipart.d.ts +2 -0
- package/dist/middleware/multipart.js +7 -0
- package/dist/middleware/multipart.js.map +1 -0
- package/dist/middleware/rateLimit.d.ts +2 -0
- package/dist/middleware/rateLimit.js +7 -0
- package/dist/middleware/rateLimit.js.map +1 -0
- package/dist/middleware/underPressure.d.ts +2 -0
- package/dist/middleware/underPressure.js +7 -0
- package/dist/middleware/underPressure.js.map +1 -0
- package/dist/utils/colorize.d.ts +4 -0
- package/dist/utils/colorize.js +33 -0
- package/dist/utils/colorize.js.map +1 -0
- package/dist/utils/formatBytes.d.ts +1 -0
- package/dist/utils/formatBytes.js +10 -0
- package/dist/utils/formatBytes.js.map +1 -0
- package/dist/utils/randomUUID.d.ts +1 -0
- package/dist/utils/randomUUID.js +3 -0
- package/dist/utils/randomUUID.js.map +1 -0
- package/dist/utils/statAsync.d.ts +2 -0
- package/dist/utils/statAsync.js +4 -0
- package/dist/utils/statAsync.js.map +1 -0
- package/dist/xConfig.d.ts +3 -0
- package/dist/xConfig.js +9 -0
- package/dist/xConfig.js.map +1 -0
- package/package.json +39 -33
- package/server/app.js +78 -0
- package/src/auth/admin.js +181 -0
- package/src/auth/portal.js +177 -0
- package/src/integrations/cloudinary.js +98 -0
- package/src/integrations/geocode.js +43 -0
- package/src/integrations/prisma.js +30 -0
- package/src/integrations/sendgrid.js +58 -0
- package/src/integrations/twilio.js +146 -0
- package/src/lifecycle/xFastifyAfter.js +27 -0
- package/src/middleware/bugsnag.js +10 -0
- package/src/middleware/cors.js +10 -0
- package/src/middleware/fancyErrors.js +26 -0
- package/src/middleware/multipart.js +6 -0
- package/src/middleware/rateLimit.js +6 -0
- package/src/middleware/underPressure.js +6 -0
- package/src/utils/colorize.js +37 -0
- package/src/utils/cookie.js +5 -0
- package/src/utils/formatBytes.js +16 -0
- package/src/utils/health.js +126 -0
- package/src/utils/xEcho.js +12 -0
- package/src/utils/xSlugify.js +20 -0
- package/src/utils/xUUID.js +14 -0
- package/src/xConfig.js +117 -0
- package/test/index.js +17 -0
- package/ts-reference/integrations/cloudinary.ts +26 -0
- package/ts-reference/integrations/prisma.ts +13 -0
- package/ts-reference/integrations/sendgrid.ts +27 -0
- package/ts-reference/integrations/stripe.ts +15 -0
- package/ts-reference/integrations/twilio.ts +20 -0
- package/ts-reference/middleware/bugsnag.ts +10 -0
- package/ts-reference/middleware/cors.ts +13 -0
- package/ts-reference/middleware/errorHandler.ts +24 -0
- package/ts-reference/middleware/multipart.ts +8 -0
- package/ts-reference/middleware/rateLimit.ts +8 -0
- package/ts-reference/middleware/underPressure.ts +11 -0
- package/ts-reference/utils/colorize.ts +45 -0
- package/ts-reference/utils/formatBytes.ts +8 -0
- package/ts-reference/utils/randomUUID.ts +3 -0
- package/ts-reference/utils/statAsync.ts +4 -0
- package/tsconfig.json +13 -8
- package/xConfigReference.js +1526 -0
- package/xConfigWorkingList.js +720 -0
- package/.github/workflows/ci.yml +0 -19
- package/.taprc +0 -3
- package/index.d.ts +0 -13
- package/index.js +0 -9
- package/test/index.test-d.ts +0 -13
- package/test/index.test.js +0 -14
- package/test/xConfig.js +0 -115
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Sendgrid from '@sendgrid/mail';
|
|
2
|
+
import sgClient from '@sendgrid/client';
|
|
3
|
+
export async function setupSendgrid(fastify, options) {
|
|
4
|
+
if (options.active !== false) {
|
|
5
|
+
if (!options.apiKey)
|
|
6
|
+
throw new Error("SendGrid API key must be provided.");
|
|
7
|
+
|
|
8
|
+
Sendgrid.setApiKey(options.apiKey);
|
|
9
|
+
sgClient.setApiKey(options.apiKeyEmailValidation);
|
|
10
|
+
// Decorator to group SendGrid-related methods under fastify.sendGrid
|
|
11
|
+
fastify.decorate('sendgrid', {
|
|
12
|
+
sendEmail: async (to, subject, templateId, dynamicTemplateData) => {
|
|
13
|
+
try {
|
|
14
|
+
|
|
15
|
+
const msg = {
|
|
16
|
+
to: to,
|
|
17
|
+
from: options.fromEmail,
|
|
18
|
+
subject: subject,
|
|
19
|
+
templateId: templateId,
|
|
20
|
+
dynamicTemplateData: { ...dynamicTemplateData, subject: subject },
|
|
21
|
+
};
|
|
22
|
+
await Sendgrid.send(msg);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
fastify.log.error("SendGrid send email failed:", error);
|
|
26
|
+
throw new Error("Failed to send email.");
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
validateEmail: async (email) => {
|
|
30
|
+
const data = {
|
|
31
|
+
email: email,
|
|
32
|
+
// source: 'signup',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const request = {
|
|
36
|
+
url: `/v3/validations/email`,
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: data,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const [response, body] = await sgClient.request(request);
|
|
43
|
+
console.log(body)
|
|
44
|
+
if (response.statusCode === 200) {
|
|
45
|
+
return body?.result; // Return the validation result
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(body.errors ? body.errors.map(err => err.message).join(', ') : 'Failed to validate email.');
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
fastify.log.error('SendGrid email validation failed:', error);
|
|
51
|
+
throw new Error('Failed to validate email.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.info(" ✅ SendGrid Enabled");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import Twilio from "twilio";
|
|
2
|
+
export async function setupTwilio(fastify, options) {
|
|
3
|
+
if (options.active !== false) {
|
|
4
|
+
// Return if missing Twilio credentials
|
|
5
|
+
if (
|
|
6
|
+
!options.accountSid ||
|
|
7
|
+
!options.authToken ||
|
|
8
|
+
!options.phoneNumber
|
|
9
|
+
)
|
|
10
|
+
throw new Error(
|
|
11
|
+
"Twilio accountSid, authToken, and phoneNumber must be provided."
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const twilioClient = Twilio(
|
|
15
|
+
options.accountSid,
|
|
16
|
+
options.authToken
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Decorator to group Twilio-related methods under fastify.twilio
|
|
20
|
+
fastify.decorate('twilio', {
|
|
21
|
+
sendSMS: async (to, body) => {
|
|
22
|
+
try {
|
|
23
|
+
const message = await twilioClient.messages.create({
|
|
24
|
+
body,
|
|
25
|
+
to,
|
|
26
|
+
from: options.phoneNumber, // Use the Twilio phone number from options
|
|
27
|
+
});
|
|
28
|
+
return message;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
fastify.log.error("Twilio send SMS failed:", error);
|
|
31
|
+
throw new Error("Failed to send SMS.");
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
sendMMS: async (to, body, mediaUrl) => {
|
|
35
|
+
try {
|
|
36
|
+
const message = await twilioClient.messages.create({
|
|
37
|
+
body,
|
|
38
|
+
to,
|
|
39
|
+
from: options.phoneNumber,
|
|
40
|
+
mediaUrl: [mediaUrl], // Array of media URLs
|
|
41
|
+
});
|
|
42
|
+
return message;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
fastify.log.error("Twilio send MMS failed:", error);
|
|
45
|
+
throw new Error("Failed to send MMS.");
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
makeVoiceCall: async (to, twimlUrl) => {
|
|
49
|
+
try {
|
|
50
|
+
const call = await twilioClient.calls.create({
|
|
51
|
+
to,
|
|
52
|
+
from: options.phoneNumber,
|
|
53
|
+
url: twimlUrl, // URL that provides TwiML instructions
|
|
54
|
+
});
|
|
55
|
+
return call;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
fastify.log.error("Twilio make voice call failed:", error);
|
|
58
|
+
throw new Error("Failed to make voice call.");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
sendVerificationCode: async (to, channel = 'sms') => {
|
|
62
|
+
try {
|
|
63
|
+
const verification = await twilioClient.verify.services(options.verifyServiceSid)
|
|
64
|
+
.verifications
|
|
65
|
+
.create({ to, channel }); // channel can be 'sms' or 'call'
|
|
66
|
+
return verification;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
fastify.log.error("Twilio send verification code failed:", error);
|
|
69
|
+
throw new Error("Failed to send verification code.");
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
verifyCode: async (to, code) => {
|
|
73
|
+
try {
|
|
74
|
+
const verificationCheck = await twilioClient.verify.services(options.verifyServiceSid)
|
|
75
|
+
.verificationChecks
|
|
76
|
+
.create({ to, code });
|
|
77
|
+
return verificationCheck;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
fastify.log.error("Twilio verify code failed:", error);
|
|
80
|
+
throw new Error("Failed to verify code.");
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
fetchSMSHistory: async (fromDate, toDate) => {
|
|
84
|
+
try {
|
|
85
|
+
const messages = await twilioClient.messages.list({
|
|
86
|
+
dateSentAfter: fromDate,
|
|
87
|
+
dateSentBefore: toDate,
|
|
88
|
+
limit: 100,
|
|
89
|
+
});
|
|
90
|
+
return messages;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
fastify.log.error("Twilio fetch SMS history failed:", error);
|
|
93
|
+
throw new Error("Failed to fetch SMS history.");
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
handleIncomingSMS: (req, reply) => {
|
|
97
|
+
const { Body, From } = req.body;
|
|
98
|
+
|
|
99
|
+
// Process incoming message
|
|
100
|
+
console.log(`Message received from ${From}: ${Body}`);
|
|
101
|
+
|
|
102
|
+
reply.type('text/xml');
|
|
103
|
+
reply.send('<Response><Message>Thanks for your message!</Message></Response>');
|
|
104
|
+
},
|
|
105
|
+
checkCallStatus: async (callSid) => {
|
|
106
|
+
try {
|
|
107
|
+
const call = await twilioClient.calls(callSid).fetch();
|
|
108
|
+
return call;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
fastify.log.error("Twilio check call status failed:", error);
|
|
111
|
+
throw new Error("Failed to check call status.");
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
validatePhoneNumber: async (phoneNumber) => {
|
|
115
|
+
try {
|
|
116
|
+
// Perform phone number validation with Twilio
|
|
117
|
+
const validation = await twilioClient.lookups.v2
|
|
118
|
+
.phoneNumbers(phoneNumber)
|
|
119
|
+
.fetch();
|
|
120
|
+
|
|
121
|
+
// Check if the phone number is valid (Twilio may return "valid": true/false)
|
|
122
|
+
const isValid = validation.valid !== undefined ? validation.valid : true;
|
|
123
|
+
|
|
124
|
+
// Return the required structure
|
|
125
|
+
return {
|
|
126
|
+
sms: phoneNumber, // The phone number
|
|
127
|
+
smsValidate: validation, // The full validation payload from Twilio
|
|
128
|
+
isSmsValidated: isValid // Set true/false based on the validation
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
fastify.log.error("Twilio phone number validation failed:", error);
|
|
132
|
+
|
|
133
|
+
// Return the structure with failed validation
|
|
134
|
+
return {
|
|
135
|
+
sms: phoneNumber, // The phone number
|
|
136
|
+
smsValidate: {}, // Empty object as validation failed
|
|
137
|
+
isSmsValidated: false // Validation failed
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
console.info(" ✅ Twilio Enabled");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/lifecycle/xFastifyAfter.js
|
|
2
|
+
import { printRoutes } from "../utils/colorize.js";
|
|
3
|
+
export async function xFastifyAfter(fastify, options) {
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
===== LIST ROUTES AFTER ALL PLUGINS =====
|
|
7
|
+
Use the after() method to ensure this runs after all plugins are registered.
|
|
8
|
+
*/
|
|
9
|
+
fastify.after(() => {
|
|
10
|
+
if (options.professional !== true) {
|
|
11
|
+
console.info(" ✅ Listing Routes:");
|
|
12
|
+
fastify.ready(() => {
|
|
13
|
+
printRoutes(options.routes, options.colors !== false);
|
|
14
|
+
// Add rocket emoji
|
|
15
|
+
console.info(
|
|
16
|
+
`🚀 Server is ready on port ${process.env.PORT || 3000}\n\n`
|
|
17
|
+
);
|
|
18
|
+
// Add goodbye emoji for server shutting down
|
|
19
|
+
fastify.addHook("onClose", () =>
|
|
20
|
+
console.info("Server shutting down... Goodbye 👋")
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function setupBugsnag(fastify, options) {
|
|
2
|
+
if (options.active !== false) {
|
|
3
|
+
if (!options.apiKey)
|
|
4
|
+
throw new Error("Bugsnag API key must be provided.");
|
|
5
|
+
fastify.register(import("fastify-bugsnag"), {
|
|
6
|
+
apiKey: options.apiKey,
|
|
7
|
+
});
|
|
8
|
+
console.info(" ✅ BugSnag Enabled");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function setupCors(fastify, options) {
|
|
2
|
+
if (options.active !== false) {
|
|
3
|
+
await fastify.register(await import("@fastify/cors"), {
|
|
4
|
+
origin: options.origin || ["https://getx.io", "http://localhost:3000"],
|
|
5
|
+
credentials: options.credentials !== undefined ? options.credentials : true,
|
|
6
|
+
methods: options.methods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
7
|
+
});
|
|
8
|
+
console.info(" ✅ CORS Enabled");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export async function setupFancyErrors(fastify, options) {
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
===== EXTEND ERRORS =====
|
|
5
|
+
*/
|
|
6
|
+
if (options.fancyErrors !== false) {
|
|
7
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
8
|
+
const statusCode = error.statusCode || 500;
|
|
9
|
+
const response = {
|
|
10
|
+
status: statusCode,
|
|
11
|
+
message: error.message || "Internal Server Error",
|
|
12
|
+
// Only show stack in development mode
|
|
13
|
+
stack: process.env.NODE_ENV === "development" ? error.stack : undefined,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Optional Bugsnag error reporting
|
|
17
|
+
if (fastify.bugsnag) {
|
|
18
|
+
fastify.bugsnag.notify(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fastify.log.error(response);
|
|
22
|
+
reply.status(statusCode).send(response);
|
|
23
|
+
});
|
|
24
|
+
console.info(" ✅ Fancy Errors Enabled");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/utils/colorize.js
|
|
2
|
+
const colors = {
|
|
3
|
+
POST: 33,
|
|
4
|
+
GET: 32,
|
|
5
|
+
PUT: 34,
|
|
6
|
+
DELETE: 31,
|
|
7
|
+
PATCH: 90,
|
|
8
|
+
clear: 39,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// Function to colorize method and path names
|
|
13
|
+
function colorize(method, text) {
|
|
14
|
+
const colorCode = colors[method] || colors.clear;
|
|
15
|
+
return `\u001b[${colorCode}m${text}\u001b[${colors.clear}m`;
|
|
16
|
+
}
|
|
17
|
+
// Function to print the collected routes
|
|
18
|
+
function printRoutes(routes, colors = true) {
|
|
19
|
+
routes
|
|
20
|
+
.sort((a, b) => a.url.localeCompare(b.url))
|
|
21
|
+
.forEach(({ method, url }) => {
|
|
22
|
+
const methodsArray = Array.isArray(method) ? method : [method];
|
|
23
|
+
methodsArray
|
|
24
|
+
.filter((m) => m !== "HEAD")
|
|
25
|
+
.forEach((m) =>
|
|
26
|
+
console.info(
|
|
27
|
+
`${colors ? colorize(m, m) : m}\t${colors ? colorize(m, url) : url}`
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
colors,
|
|
35
|
+
colorize,
|
|
36
|
+
printRoutes,
|
|
37
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
|
|
3
|
+
async function formatBytesPlugin(fastify, options) {
|
|
4
|
+
fastify.decorate("formatBytes", (bytes, decimals = 2) => {
|
|
5
|
+
if (bytes === 0) return "0 Bytes";
|
|
6
|
+
|
|
7
|
+
const k = 1024;
|
|
8
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
9
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
|
10
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
11
|
+
|
|
12
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default fp(formatBytesPlugin);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Import necessary modules
|
|
2
|
+
import os from "os";
|
|
3
|
+
import process from "process";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import util from "util";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Promisify fs functions
|
|
9
|
+
const statAsync = util.promisify(fs.stat);
|
|
10
|
+
|
|
11
|
+
// Record the server start time
|
|
12
|
+
const serverStartTime = Date.now();
|
|
13
|
+
|
|
14
|
+
// Helper function to format bytes into human-readable format
|
|
15
|
+
function formatBytes(bytes, decimals = 2) {
|
|
16
|
+
if (bytes === 0) return "0 Bytes";
|
|
17
|
+
const k = 1024;
|
|
18
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
19
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
|
20
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
21
|
+
const formattedNumber = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
|
22
|
+
return `${formattedNumber} ${sizes[i]}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function setupHealth(fastify, options) {
|
|
26
|
+
fastify.get("/health", async (request, reply) => {
|
|
27
|
+
const status = {
|
|
28
|
+
status: "healthy",
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
uptime: process.uptime(), // Uptime in seconds
|
|
31
|
+
version: "1.0.0", // Fetch dynamically if possible
|
|
32
|
+
environment: process.env.NODE_ENV || "development",
|
|
33
|
+
dependencies: {},
|
|
34
|
+
resources: {},
|
|
35
|
+
details: {},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Database connectivity check
|
|
40
|
+
if (fastify.prisma) {
|
|
41
|
+
await fastify.prisma.$queryRaw`SELECT 1`;
|
|
42
|
+
status.dependencies.database = "up";
|
|
43
|
+
} else {
|
|
44
|
+
status.dependencies.database = "not configured";
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
status.dependencies.database = "down";
|
|
48
|
+
status.status = "degraded";
|
|
49
|
+
status.details.databaseError = error.message;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Redis connectivity check
|
|
54
|
+
if (fastify.redis) {
|
|
55
|
+
await fastify.redis.ping();
|
|
56
|
+
status.dependencies.redis = "up";
|
|
57
|
+
} else {
|
|
58
|
+
status.dependencies.redis = "not configured";
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
status.dependencies.redis = "down";
|
|
62
|
+
status.status = "degraded";
|
|
63
|
+
status.details.redisError = error.message;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resource usage
|
|
67
|
+
const memoryUsage = process.memoryUsage();
|
|
68
|
+
const loadAverage = os.loadavg();
|
|
69
|
+
|
|
70
|
+
status.resources.memory = {
|
|
71
|
+
rss: formatBytes(memoryUsage.rss),
|
|
72
|
+
heapTotal: formatBytes(memoryUsage.heapTotal),
|
|
73
|
+
heapUsed: formatBytes(memoryUsage.heapUsed),
|
|
74
|
+
external: formatBytes(memoryUsage.external),
|
|
75
|
+
arrayBuffers: formatBytes(memoryUsage.arrayBuffers),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
status.resources.cpu = {
|
|
79
|
+
loadAverage, // 1, 5, and 15 minute load averages
|
|
80
|
+
cpus: os.cpus().length,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Disk space availability
|
|
84
|
+
try {
|
|
85
|
+
// Implement disk space check using 'check-disk-space' package
|
|
86
|
+
// Install it via 'npm install check-disk-space'
|
|
87
|
+
const checkDiskSpace = (await import("check-disk-space")).default;
|
|
88
|
+
const diskSpace = await checkDiskSpace("/"); // Replace '/' with your drive or mount point
|
|
89
|
+
status.resources.disk = {
|
|
90
|
+
free: formatBytes(diskSpace.free),
|
|
91
|
+
size: formatBytes(diskSpace.size),
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
status.resources.disk = "unknown";
|
|
95
|
+
status.details.diskError = error.message;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Application-specific checks
|
|
99
|
+
// Example: Check if a scheduled job is running
|
|
100
|
+
if (typeof isJobRunning === "function" && !isJobRunning()) {
|
|
101
|
+
status.status = "degraded";
|
|
102
|
+
status.details.jobStatus = "Scheduled job is not running";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Configuration validation
|
|
106
|
+
const requiredEnvVars = [
|
|
107
|
+
"DATABASE_URL",
|
|
108
|
+
"ADMIN_JWT_SECRET",
|
|
109
|
+
"USER_JWT_SECRET",
|
|
110
|
+
];
|
|
111
|
+
const missingEnvVars = requiredEnvVars.filter(
|
|
112
|
+
(varName) => !process.env[varName]
|
|
113
|
+
);
|
|
114
|
+
if (missingEnvVars.length > 0) {
|
|
115
|
+
status.status = "degraded";
|
|
116
|
+
status.details.missingEnvVars = missingEnvVars;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Determine overall health
|
|
120
|
+
if (status.status === "healthy") {
|
|
121
|
+
reply.send(status);
|
|
122
|
+
} else {
|
|
123
|
+
reply.status(500).send(status);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/utils/xSlugify.js
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
|
|
4
|
+
async function xSlugify(fastify, options) {
|
|
5
|
+
fastify.decorate('slugify', (string) => {
|
|
6
|
+
return string
|
|
7
|
+
.toString()
|
|
8
|
+
.toLowerCase() // Convert to lowercase
|
|
9
|
+
.trim() // Trim leading and trailing spaces
|
|
10
|
+
.replace(/\s+/g, '-') // Replace spaces with -
|
|
11
|
+
.replace(/[^\w\-]+/g, '') // Remove all non-word characters (allow only letters, numbers, and dashes)
|
|
12
|
+
.replace(/\-\-+/g, '-') // Replace multiple dashes with a single dash
|
|
13
|
+
.replace(/^-+/, '') // Trim leading dashes
|
|
14
|
+
.replace(/-+$/, ''); // Trim trailing dashes
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default fp(xSlugify, {
|
|
19
|
+
name: "xSlugify",
|
|
20
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { randomUUID } from "uncrypto";
|
|
3
|
+
|
|
4
|
+
async function xUUID(fastify, options) {
|
|
5
|
+
fastify.decorate("generateUUID", randomUUID);
|
|
6
|
+
|
|
7
|
+
fastify.decorate('randomUUID', () => {
|
|
8
|
+
return randomUUID(); // Generate a UUID using uncrypto's randomUUID
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default fp(xUUID, {
|
|
13
|
+
name: "xUUID",
|
|
14
|
+
});
|
package/src/xConfig.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/xConfig.js
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
4
|
+
/*
|
|
5
|
+
* Auth
|
|
6
|
+
*/
|
|
7
|
+
import { setupAdminAuth } from "./auth/admin.js";
|
|
8
|
+
import { setupAuth } from "./auth/portal.js";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Integrations
|
|
13
|
+
*/
|
|
14
|
+
import { setupPrisma } from "./integrations/prisma.js";
|
|
15
|
+
import { setupSendgrid } from "./integrations/sendgrid.js";
|
|
16
|
+
import { setupTwilio } from "./integrations/twilio.js";
|
|
17
|
+
import { setupCloudinary } from "./integrations/cloudinary.js";
|
|
18
|
+
import { setupGeocode } from "./integrations/geocode.js";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Lifecycle
|
|
22
|
+
*/
|
|
23
|
+
import { xFastifyAfter } from "./lifecycle/xFastifyAfter.js";
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Middleware
|
|
28
|
+
*/
|
|
29
|
+
import { setupCors } from './middleware/cors.js';
|
|
30
|
+
import { setupUnderPressure } from './middleware/underPressure.js';
|
|
31
|
+
import { setupRateLimit } from './middleware/rateLimit.js';
|
|
32
|
+
import { setupMultipart } from './middleware/multipart.js';
|
|
33
|
+
import { setupBugsnag } from './middleware/bugsnag.js';
|
|
34
|
+
import { setupFancyErrors } from './middleware/fancyErrors.js'
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Utils
|
|
38
|
+
*/
|
|
39
|
+
import xEcho from "./utils/xEcho.js";
|
|
40
|
+
import { setupHealth } from "./utils/health.js";
|
|
41
|
+
import xSlugify from "./utils/xSlugify.js";
|
|
42
|
+
import xUUID from "./utils/xUUID.js";
|
|
43
|
+
import { setupCookie } from "./utils/cookie.js";
|
|
44
|
+
|
|
45
|
+
async function xConfig(fastify, options) {
|
|
46
|
+
const {
|
|
47
|
+
professional = false,
|
|
48
|
+
fancyErrors = true,
|
|
49
|
+
prisma: prismaOptions = {},
|
|
50
|
+
bugsnag: bugsnagOptions = {},
|
|
51
|
+
stripe: stripeOptions = {},
|
|
52
|
+
sendGrid: sendGridOptions = {},
|
|
53
|
+
twilio: twilioOptions = {},
|
|
54
|
+
cloudinary: cloudinaryOptions = {},
|
|
55
|
+
auth: authOptions = {},
|
|
56
|
+
cors: corsOptions = {},
|
|
57
|
+
underPressure: underPressureOptions = {},
|
|
58
|
+
multipart: multipartOptions = {},
|
|
59
|
+
rateLimit: rateLimitOptions = {},
|
|
60
|
+
geocode: geocodeOptions = {},
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
// add starting console with emoji
|
|
64
|
+
console.info("\n 🌮 Starting xConfig...\n");
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
===== LIST ROUTES =====
|
|
68
|
+
Moved the onRoute hook to the top to capture all routes.
|
|
69
|
+
*/
|
|
70
|
+
const routes = [];
|
|
71
|
+
fastify.addHook("onRoute", (r) => routes.push(r));
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Integrations
|
|
75
|
+
*/
|
|
76
|
+
await setupPrisma(fastify, prismaOptions);
|
|
77
|
+
await setupSendgrid(fastify, sendGridOptions);
|
|
78
|
+
await setupTwilio(fastify, twilioOptions);
|
|
79
|
+
await setupCloudinary(fastify, cloudinaryOptions);
|
|
80
|
+
await setupGeocode(fastify, geocodeOptions);
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Middleware
|
|
84
|
+
*/
|
|
85
|
+
await setupCors(fastify, corsOptions);
|
|
86
|
+
await setupUnderPressure(fastify, underPressureOptions);
|
|
87
|
+
await setupRateLimit(fastify, rateLimitOptions);
|
|
88
|
+
await setupMultipart(fastify, multipartOptions);
|
|
89
|
+
await setupBugsnag(fastify, bugsnagOptions);
|
|
90
|
+
await setupFancyErrors(fastify, fancyErrors);
|
|
91
|
+
fastify.register(import('@fastify/sensible'))
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* Utils
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
fastify.register(xEcho);
|
|
98
|
+
fastify.register(xSlugify);
|
|
99
|
+
fastify.register(xUUID);
|
|
100
|
+
await setupHealth(fastify);
|
|
101
|
+
await setupCookie(fastify, authOptions);
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* Auth
|
|
105
|
+
*/
|
|
106
|
+
await setupAuth(fastify, authOptions);
|
|
107
|
+
await setupAdminAuth(fastify, authOptions);
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
//.after() method to ensure this runs after all plugins are registered.
|
|
111
|
+
await xFastifyAfter(fastify, { professional, routes });
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default fp(xConfig, {
|
|
116
|
+
name: "xConfig",
|
|
117
|
+
});
|
package/test/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// test.js
|
|
2
|
+
const fastify = require('fastify')();
|
|
3
|
+
const myPlugin = require('../src/xConfig');
|
|
4
|
+
|
|
5
|
+
fastify.register(myPlugin);
|
|
6
|
+
|
|
7
|
+
fastify.get('/', async (request, reply) => {
|
|
8
|
+
return { message: fastify.myPluginMethod() };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
fastify.listen(3000, (err, address) => {
|
|
12
|
+
if (err) {
|
|
13
|
+
console.error(err);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
console.log(`Server running at ${address}`);
|
|
17
|
+
});
|