@koloseum/utils 0.2.14 → 0.2.16

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
@@ -223,9 +223,10 @@ export declare const Utility: {
223
223
  /**
224
224
  * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
225
225
  * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
226
+ * @param {boolean} clientSafe - Whether to clamp 5xx errors down to 422 to prevent Sentry from capturing them as unhandled; defaults to `true`
226
227
  * @returns An object with an `error` if any has occurred
227
228
  */
228
- parsePostgrestError: (postgrestError: PostgrestError | null) => {
229
+ parsePostgrestError: (postgrestError: PostgrestError | null, clientSafe?: boolean) => {
229
230
  error?: CustomError;
230
231
  };
231
232
  /**
@@ -249,6 +250,16 @@ export declare const Utility: {
249
250
  * @returns A sanitised string, or an empty string if the input is invalid
250
251
  */
251
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
+ }>;
252
263
  /**
253
264
  * A regular expression for social media handles, without a leading slash or @ character.
254
265
  *
package/dist/utils.js CHANGED
@@ -703,6 +703,9 @@ export const Utility = {
703
703
  // Return true if microservice group is public
704
704
  if (microserviceGroup === "public")
705
705
  return { isAuthorised: true };
706
+ // Validate user metadata and app metadata
707
+ if (!user.user_metadata || !user.app_metadata)
708
+ return { error: Utility.customError(400, "User metadata is required.") };
706
709
  // Get user's roles and the role prefix
707
710
  const roles = [];
708
711
  const rolePrefix = microserviceGroup === "backroom" ? "backroom" : microserviceGroup.slice(0, -1);
@@ -748,43 +751,9 @@ export const Utility = {
748
751
  if (validationError)
749
752
  return { error: Utility.customError(code, validationError) };
750
753
  // 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
- }
754
+ const { error: welcomeError } = await Utility.sendWelcomeNotification(supabase, user, microserviceGroup);
755
+ if (welcomeError)
756
+ return { error: welcomeError };
788
757
  // Return result
789
758
  return { isAuthorised };
790
759
  },
@@ -1268,17 +1237,21 @@ export const Utility = {
1268
1237
  /**
1269
1238
  * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
1270
1239
  * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
1240
+ * @param {boolean} clientSafe - Whether to clamp 5xx errors down to 422 to prevent Sentry from capturing them as unhandled; defaults to `true`
1271
1241
  * @returns An object with an `error` if any has occurred
1272
1242
  */
1273
- parsePostgrestError: (postgrestError) => {
1243
+ parsePostgrestError: (postgrestError, clientSafe = true) => {
1274
1244
  // Return undefined if no error occurred
1275
1245
  let error;
1276
1246
  if (!postgrestError)
1277
1247
  return { error };
1278
- // Return custom error if hint is a number between 400 and 599
1248
+ // Get custom error code from hint
1279
1249
  const customErrorCode = Number(postgrestError.hint);
1250
+ // Clamp 5xx errors down to 422 to prevent Sentry from capturing them as unhandled; see https://koloseum-technologies.sentry.io/issues/6766267685/
1251
+ let statusCode = clientSafe ? (customErrorCode >= 500 ? 422 : customErrorCode) : customErrorCode;
1252
+ // Return custom error if hint is a number between 400 and 599
1280
1253
  if (!isNaN(customErrorCode) && customErrorCode >= 400 && customErrorCode <= 599) {
1281
- error = Utility.customError(customErrorCode, postgrestError.message);
1254
+ error = Utility.customError(statusCode, postgrestError.message);
1282
1255
  return { error };
1283
1256
  }
1284
1257
  // Map Postgrest error codes to custom error codes
@@ -1316,11 +1289,12 @@ export const Utility = {
1316
1289
  // Return custom error if Postgrest error code is found
1317
1290
  for (const { code, status } of errorMap)
1318
1291
  if (postgrestError.code === code || postgrestError.code.startsWith(code)) {
1319
- error = Utility.customError(status, Status.ERROR);
1292
+ statusCode = clientSafe ? (status >= 500 ? 422 : status) : status;
1293
+ error = Utility.customError(statusCode, Status.ERROR);
1320
1294
  return { error };
1321
1295
  }
1322
1296
  // Return generic error
1323
- error = Utility.customError(500, Status.ERROR);
1297
+ error = Utility.customError(clientSafe ? 422 : 500, Status.ERROR);
1324
1298
  return { error };
1325
1299
  },
1326
1300
  /**
@@ -1352,6 +1326,67 @@ export const Utility = {
1352
1326
  * @returns A sanitised string, or an empty string if the input is invalid
1353
1327
  */
1354
1328
  sanitiseHtml: (input) => typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
1329
+ /**
1330
+ * Sends a welcome notification to a user.
1331
+ * @param {SupabaseClient<Database>} supabase - The Supabase client
1332
+ * @param {UserWithCustomMetadata} user - The user to send the notification to
1333
+ * @param {MicroserviceGroup} microserviceGroup - The microservice group the user belongs to
1334
+ * @returns An object with an `error` if any has occurred
1335
+ */
1336
+ sendWelcomeNotification: async (supabase, user, microserviceGroup) => {
1337
+ // Backroom
1338
+ if (microserviceGroup === "backroom" && !user.user_metadata.backroom?.welcome_notification_sent) {
1339
+ // Use atomic update with timestamp to prevent race conditions
1340
+ const currentTime = new Date().toISOString();
1341
+ const { data: updatedUser, error: updateError } = await supabase.auth.updateUser({
1342
+ data: {
1343
+ backroom: {
1344
+ ...user.user_metadata.backroom,
1345
+ welcome_notification_sent: true,
1346
+ welcome_notification_timestamp: currentTime
1347
+ }
1348
+ }
1349
+ });
1350
+ if (updateError)
1351
+ return { error: Utility.customError(updateError.status ?? 500, updateError.message) };
1352
+ // Only send notification if flag was successfully updated
1353
+ const updatedBackroom = updatedUser.user?.user_metadata?.backroom;
1354
+ if (updatedBackroom?.welcome_notification_sent === true &&
1355
+ updatedBackroom?.welcome_notification_timestamp === currentTime) {
1356
+ // Define SuprSend workflow body
1357
+ const workflowBody = {
1358
+ name: "welcome-to-backroom",
1359
+ template: "welcome-to-backroom",
1360
+ notification_category: "system",
1361
+ users: [
1362
+ {
1363
+ distinct_id: user.id,
1364
+ $skip_create: true
1365
+ }
1366
+ ]
1367
+ };
1368
+ // Send welcome notification
1369
+ const { code, error: workflowError } = await Utility.callEdgeFunction(false, supabase, `suprsend/trigger-workflow?user-id=${user.id}`, { method: "POST", body: workflowBody });
1370
+ // If notification fails, revert the flag and return error
1371
+ if (workflowError) {
1372
+ await supabase.auth.updateUser({
1373
+ data: {
1374
+ backroom: {
1375
+ ...user.user_metadata.backroom,
1376
+ welcome_notification_sent: false,
1377
+ welcome_notification_timestamp: undefined
1378
+ }
1379
+ }
1380
+ });
1381
+ return { error: Utility.customError(code, workflowError) };
1382
+ }
1383
+ // Update user object in memory
1384
+ user.user_metadata.backroom = updatedBackroom;
1385
+ }
1386
+ }
1387
+ // Return empty object
1388
+ return {};
1389
+ },
1355
1390
  /**
1356
1391
  * A regular expression for social media handles, without a leading slash or @ character.
1357
1392
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koloseum/utils",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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",