@koloseum/utils 0.2.15 → 0.2.17

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/dist/utils.d.ts CHANGED
@@ -250,6 +250,16 @@ export declare const Utility: {
250
250
  * @returns A sanitised string, or an empty string if the input is invalid
251
251
  */
252
252
  sanitiseHtml: (input: string) => string;
253
+ /**
254
+ * Sends a welcome notification to a user.
255
+ * @param {SupabaseClient<Database>} supabase - The Supabase client
256
+ * @param {UserWithCustomMetadata} user - The user to send the notification to
257
+ * @param {MicroserviceGroup} microserviceGroup - The microservice group the user belongs to
258
+ * @returns An object with an `error` if any has occurred
259
+ */
260
+ sendWelcomeNotification: (supabase: SupabaseClient<Database>, user: UserWithCustomMetadata, microserviceGroup: MicroserviceGroup) => Promise<{
261
+ error?: CustomError;
262
+ }>;
253
263
  /**
254
264
  * A regular expression for social media handles, without a leading slash or @ character.
255
265
  *
@@ -279,3 +289,43 @@ export declare const Utility: {
279
289
  */
280
290
  validateSocialMediaHandle: (handle: string) => boolean;
281
291
  };
292
+ export declare const Cache: {
293
+ /**
294
+ * Delete cached data from Valkey.
295
+ * @param valkey - The Valkey client instance
296
+ * @param cachePrefix - The cache prefix
297
+ * @param key - The cache key
298
+ */
299
+ deleteData: (valkey: any, cachePrefix: string, key: string) => Promise<void>;
300
+ /**
301
+ * Generate cache key with consistent formatting
302
+ * @param prefix - The key prefix
303
+ * @param params - Additional parameters to include in the key
304
+ * @returns The formatted cache key
305
+ */
306
+ generateDataKey: (prefix: string, ...params: (string | number)[]) => string;
307
+ /**
308
+ * Get cached data from Valkey.
309
+ * @param valkey - The Valkey client instance
310
+ * @param cachePrefix - The cache prefix
311
+ * @param key - The cache key
312
+ * @returns The cached data, or `null` if not found
313
+ */
314
+ getData: <T>(valkey: any, cachePrefix: string, key: string) => Promise<T | null>;
315
+ /**
316
+ * Invalidate cache by pattern
317
+ * @param valkey - The Valkey client instance
318
+ * @param cachePrefix - The cache prefix
319
+ * @param pattern - The pattern to match keys against (e.g., "app-config*")
320
+ */
321
+ invalidateDataByPattern: (valkey: any, cachePrefix: string, pattern: string) => Promise<void>;
322
+ /**
323
+ * Set data in Valkey cache.
324
+ * @param valkey - The Valkey client instance
325
+ * @param cachePrefix - The cache prefix
326
+ * @param key - The cache key
327
+ * @param data - The data to cache
328
+ * @param ttl - The time to live in seconds; defaults to 1 hour
329
+ */
330
+ setData: <T>(valkey: any, cachePrefix: string, key: string, data: T, ttl?: number) => Promise<void>;
331
+ };
package/dist/utils.js CHANGED
@@ -6,6 +6,8 @@ import validator from "validator";
6
6
  const parsePgInterval = (await import("postgres-interval")).default;
7
7
  const sanitize = (await import("sanitize-html")).default;
8
8
  const { trim, escape, isMobilePhone, isURL } = validator;
9
+ // Cache constants
10
+ const DEFAULT_TTL = 3600; // 1 hour in seconds
9
11
  const { KenyaAdministrativeDivisions } = (await import("kenya-administrative-divisions")).default;
10
12
  /* DUMMY DATA */
11
13
  export const Data = {
@@ -703,6 +705,9 @@ export const Utility = {
703
705
  // Return true if microservice group is public
704
706
  if (microserviceGroup === "public")
705
707
  return { isAuthorised: true };
708
+ // Validate user metadata and app metadata
709
+ if (!user.user_metadata || !user.app_metadata)
710
+ return { error: Utility.customError(400, "User metadata is required.") };
706
711
  // Get user's roles and the role prefix
707
712
  const roles = [];
708
713
  const rolePrefix = microserviceGroup === "backroom" ? "backroom" : microserviceGroup.slice(0, -1);
@@ -748,43 +753,9 @@ export const Utility = {
748
753
  if (validationError)
749
754
  return { error: Utility.customError(code, validationError) };
750
755
  // Send welcome notification if needed
751
- if (!user.user_metadata.backroom || !user.user_metadata.backroom.welcome_notification_sent) {
752
- // Initialise backroom metadata if it doesn't exist
753
- if (!user.user_metadata.backroom) {
754
- // Update user metadata in database
755
- const { error: updateError } = await supabase.auth.updateUser({
756
- data: { backroom: { welcome_notification_sent: false } }
757
- });
758
- if (updateError)
759
- return { error: Utility.customError(updateError.status ?? 500, updateError.message) };
760
- // Update user object in memory
761
- user.user_metadata.backroom = { welcome_notification_sent: false };
762
- }
763
- // Define SuprSend workflow body
764
- const workflowBody = {
765
- name: "welcome-to-backroom",
766
- template: "welcome-to-backroom",
767
- notification_category: "system",
768
- users: [
769
- {
770
- distinct_id: user.id,
771
- $skip_create: true
772
- }
773
- ]
774
- };
775
- // Send welcome notification
776
- const { code, error: workflowError } = await Utility.callEdgeFunction(false, supabase, `suprsend/trigger-workflow?user-id=${user.id}`, { method: "POST", body: workflowBody });
777
- if (workflowError)
778
- return { error: Utility.customError(code, workflowError) };
779
- // Update user metadata in database
780
- const { error: updateError } = await supabase.auth.updateUser({
781
- data: { backroom: { welcome_notification_sent: true } }
782
- });
783
- if (updateError)
784
- return { error: Utility.customError(updateError.status ?? 500, updateError.message) };
785
- // Update user object in memory
786
- user.user_metadata.backroom.welcome_notification_sent = true;
787
- }
756
+ const { error: welcomeError } = await Utility.sendWelcomeNotification(supabase, user, microserviceGroup);
757
+ if (welcomeError)
758
+ return { error: welcomeError };
788
759
  // Return result
789
760
  return { isAuthorised };
790
761
  },
@@ -1357,6 +1328,67 @@ export const Utility = {
1357
1328
  * @returns A sanitised string, or an empty string if the input is invalid
1358
1329
  */
1359
1330
  sanitiseHtml: (input) => typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
1331
+ /**
1332
+ * Sends a welcome notification to a user.
1333
+ * @param {SupabaseClient<Database>} supabase - The Supabase client
1334
+ * @param {UserWithCustomMetadata} user - The user to send the notification to
1335
+ * @param {MicroserviceGroup} microserviceGroup - The microservice group the user belongs to
1336
+ * @returns An object with an `error` if any has occurred
1337
+ */
1338
+ sendWelcomeNotification: async (supabase, user, microserviceGroup) => {
1339
+ // Backroom
1340
+ if (microserviceGroup === "backroom" && !user.user_metadata.backroom?.welcome_notification_sent) {
1341
+ // Use atomic update with timestamp to prevent race conditions
1342
+ const currentTime = new Date().toISOString();
1343
+ const { data: updatedUser, error: updateError } = await supabase.auth.updateUser({
1344
+ data: {
1345
+ backroom: {
1346
+ ...user.user_metadata.backroom,
1347
+ welcome_notification_sent: true,
1348
+ welcome_notification_timestamp: currentTime
1349
+ }
1350
+ }
1351
+ });
1352
+ if (updateError)
1353
+ return { error: Utility.customError(updateError.status ?? 500, updateError.message) };
1354
+ // Only send notification if flag was successfully updated
1355
+ const updatedBackroom = updatedUser.user?.user_metadata?.backroom;
1356
+ if (updatedBackroom?.welcome_notification_sent === true &&
1357
+ updatedBackroom?.welcome_notification_timestamp === currentTime) {
1358
+ // Define SuprSend workflow body
1359
+ const workflowBody = {
1360
+ name: "welcome-to-backroom",
1361
+ template: "welcome-to-backroom",
1362
+ notification_category: "system",
1363
+ users: [
1364
+ {
1365
+ distinct_id: user.id,
1366
+ $skip_create: true
1367
+ }
1368
+ ]
1369
+ };
1370
+ // Send welcome notification
1371
+ const { code, error: workflowError } = await Utility.callEdgeFunction(false, supabase, `suprsend/trigger-workflow?user-id=${user.id}`, { method: "POST", body: workflowBody });
1372
+ // If notification fails, revert the flag and return error
1373
+ if (workflowError) {
1374
+ await supabase.auth.updateUser({
1375
+ data: {
1376
+ backroom: {
1377
+ ...user.user_metadata.backroom,
1378
+ welcome_notification_sent: false,
1379
+ welcome_notification_timestamp: undefined
1380
+ }
1381
+ }
1382
+ });
1383
+ return { error: Utility.customError(code, workflowError) };
1384
+ }
1385
+ // Update user object in memory
1386
+ user.user_metadata.backroom = updatedBackroom;
1387
+ }
1388
+ }
1389
+ // Return empty object
1390
+ return {};
1391
+ },
1360
1392
  /**
1361
1393
  * A regular expression for social media handles, without a leading slash or @ character.
1362
1394
  *
@@ -1477,3 +1509,76 @@ export const Utility = {
1477
1509
  !handle.startsWith("@") &&
1478
1510
  Boolean(handle.match(Utility.socialMediaHandleRegex))
1479
1511
  };
1512
+ /* CACHE FUNCTIONS */
1513
+ export const Cache = {
1514
+ /**
1515
+ * Delete cached data from Valkey.
1516
+ * @param valkey - The Valkey client instance
1517
+ * @param cachePrefix - The cache prefix
1518
+ * @param key - The cache key
1519
+ */
1520
+ deleteData: async (valkey, cachePrefix, key) => {
1521
+ try {
1522
+ await valkey.del(`${cachePrefix}${key}`);
1523
+ }
1524
+ catch (error) {
1525
+ console.error("Cache delete error:", error);
1526
+ }
1527
+ },
1528
+ /**
1529
+ * Generate cache key with consistent formatting
1530
+ * @param prefix - The key prefix
1531
+ * @param params - Additional parameters to include in the key
1532
+ * @returns The formatted cache key
1533
+ */
1534
+ generateDataKey: (prefix, ...params) => `${prefix}:${params.join(":")}`,
1535
+ /**
1536
+ * Get cached data from Valkey.
1537
+ * @param valkey - The Valkey client instance
1538
+ * @param cachePrefix - The cache prefix
1539
+ * @param key - The cache key
1540
+ * @returns The cached data, or `null` if not found
1541
+ */
1542
+ getData: async (valkey, cachePrefix, key) => {
1543
+ try {
1544
+ const cached = await valkey.get(`${cachePrefix}${key}`);
1545
+ return cached ? JSON.parse(cached) : null;
1546
+ }
1547
+ catch (error) {
1548
+ console.error("Cache get error:", error);
1549
+ return null;
1550
+ }
1551
+ },
1552
+ /**
1553
+ * Invalidate cache by pattern
1554
+ * @param valkey - The Valkey client instance
1555
+ * @param cachePrefix - The cache prefix
1556
+ * @param pattern - The pattern to match keys against (e.g., "app-config*")
1557
+ */
1558
+ invalidateDataByPattern: async (valkey, cachePrefix, pattern) => {
1559
+ try {
1560
+ const keys = await valkey.keys(`${cachePrefix}${pattern}`);
1561
+ if (keys.length > 0)
1562
+ await valkey.del(...keys);
1563
+ }
1564
+ catch (error) {
1565
+ console.error("Cache pattern invalidation error:", error);
1566
+ }
1567
+ },
1568
+ /**
1569
+ * Set data in Valkey cache.
1570
+ * @param valkey - The Valkey client instance
1571
+ * @param cachePrefix - The cache prefix
1572
+ * @param key - The cache key
1573
+ * @param data - The data to cache
1574
+ * @param ttl - The time to live in seconds; defaults to 1 hour
1575
+ */
1576
+ setData: async (valkey, cachePrefix, key, data, ttl = DEFAULT_TTL) => {
1577
+ try {
1578
+ await valkey.setex(`${cachePrefix}${key}`, ttl, JSON.stringify(data));
1579
+ }
1580
+ catch (error) {
1581
+ console.error("Cache set error:", error);
1582
+ }
1583
+ }
1584
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koloseum/utils",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "author": "Koloseum Technologies Limited",
5
5
  "type": "module",
6
6
  "description": "Utility logic for use across Koloseum web apps (TypeScript)",
@@ -38,7 +38,7 @@
38
38
  "validator": "^13.15.15"
39
39
  },
40
40
  "devDependencies": {
41
- "@koloseum/types": "^0.2.4",
41
+ "@koloseum/types": "^0.2.5",
42
42
  "@playwright/test": "^1.53.1",
43
43
  "@suprsend/web-components": "^0.2.1",
44
44
  "@types/sanitize-html": "^2.16.0",