@koloseum/utils 0.1.1
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 +0 -0
- package/package.json +40 -0
- package/src/utils.test.ts +191 -0
- package/src/utils.ts +264 -0
package/README.md
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@koloseum/utils",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"author": "Koloseum Technologies Limited",
|
|
5
|
+
"description": "Utility logic for use across Koloseum web apps (TypeScript)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"Koloseum"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/koloseum-technologies/npm-utils#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/koloseum-technologies/npm-utils/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/koloseum-technologies/npm-utils.git"
|
|
16
|
+
},
|
|
17
|
+
"main": "./src/utils.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
"./src": "./src/utils.ts"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"./src/**/*.ts"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "vitest --run"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@koloseum/types": "^0.1.0",
|
|
29
|
+
"@supabase/supabase-js": "^2.48.1",
|
|
30
|
+
"@sveltejs/kit": "^2.17.1",
|
|
31
|
+
"any-date-parser": "^2.0.3",
|
|
32
|
+
"sanitize-html": "^2.14.0",
|
|
33
|
+
"validator": "^13.12.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/sanitize-html": "^2.13.0",
|
|
37
|
+
"@types/validator": "^13.12.2",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Status, Utility } from "./utils";
|
|
3
|
+
|
|
4
|
+
describe("Status helper", () => {
|
|
5
|
+
// Destructure the Status object
|
|
6
|
+
const { ERROR, LOADING } = Status;
|
|
7
|
+
|
|
8
|
+
// Test each helper
|
|
9
|
+
it("contains an error message", () => {
|
|
10
|
+
expect(ERROR).toBe("An unexpected error occurred. Kindly try again.");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("contains a loading message", () => {
|
|
14
|
+
expect(LOADING).toBe("Please wait…");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("Utility helper", () => {
|
|
19
|
+
// Destructure the Utility object
|
|
20
|
+
const {
|
|
21
|
+
capitalise,
|
|
22
|
+
customError,
|
|
23
|
+
formatDate,
|
|
24
|
+
formatSocialMediaPlatform,
|
|
25
|
+
getAge,
|
|
26
|
+
minimumMinorBirthDate,
|
|
27
|
+
parseLoadError,
|
|
28
|
+
parsePostgrestError,
|
|
29
|
+
sanitiseHtml,
|
|
30
|
+
validateE164,
|
|
31
|
+
validateSocialMediaHandle
|
|
32
|
+
} = Utility;
|
|
33
|
+
|
|
34
|
+
// Test each helper
|
|
35
|
+
it("capitalises a word", () => {
|
|
36
|
+
expect(capitalise("word")).toBe("Word");
|
|
37
|
+
expect(capitalise("WORD")).toBe("Word");
|
|
38
|
+
expect(capitalise("wOrD")).toBe("Word");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("generates a custom error", () => {
|
|
42
|
+
expect(customError(200, "OK")).toEqual({ code: 500, message: Status.ERROR });
|
|
43
|
+
expect(customError(400, "Bad Request")).toEqual({ code: 400, message: "Bad Request" });
|
|
44
|
+
expect(customError(500, "Internal Server Error")).toEqual({ code: 500, message: "Internal Server Error" });
|
|
45
|
+
expect(customError(700, "Unknown")).toEqual({ code: 500, message: Status.ERROR });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("formats dates", () => {
|
|
49
|
+
expect(formatDate("31st December 2021")).toBe("31/12/2021");
|
|
50
|
+
expect(formatDate("31st December 2021", true)).toBe("2021-12-31");
|
|
51
|
+
expect(formatDate("31st December 2021", false)).toBe("31/12/2021");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("formats social media platforms", () => {
|
|
55
|
+
expect(formatSocialMediaPlatform("facebook")).toBe("Facebook");
|
|
56
|
+
expect(formatSocialMediaPlatform("instagram")).toBe("Instagram");
|
|
57
|
+
expect(formatSocialMediaPlatform("kick")).toBe("Kick");
|
|
58
|
+
expect(formatSocialMediaPlatform("tiktok")).toBe("TikTok");
|
|
59
|
+
expect(formatSocialMediaPlatform("twitter")).toBe("X (formerly Twitter)");
|
|
60
|
+
expect(formatSocialMediaPlatform("youtube")).toBe("YouTube");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("gets the age of a person", () => {
|
|
64
|
+
// Set system time to 1 January 2024
|
|
65
|
+
vi.useFakeTimers();
|
|
66
|
+
vi.setSystemTime(new Date("2024-01-01"));
|
|
67
|
+
|
|
68
|
+
// Test age calculation
|
|
69
|
+
expect(getAge("2000-01-01")).toBe(24);
|
|
70
|
+
expect(getAge("2000-12-31")).toBe(23);
|
|
71
|
+
expect(getAge("2024-01-03")).toBe(-1);
|
|
72
|
+
|
|
73
|
+
// Reset timers
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("gets the minimum birth date for a minor", () => {
|
|
78
|
+
// Calculate expected minimum birth date
|
|
79
|
+
const today = new Date();
|
|
80
|
+
const tomorrow = new Date();
|
|
81
|
+
tomorrow.setUTCDate(today.getDate() + 1);
|
|
82
|
+
|
|
83
|
+
const tomorrow18YearsAgo = tomorrow.getFullYear() - 18;
|
|
84
|
+
tomorrow.setFullYear(tomorrow18YearsAgo);
|
|
85
|
+
|
|
86
|
+
const expectedMinimumMinorBirthDate = tomorrow.toISOString().split("T")[0];
|
|
87
|
+
|
|
88
|
+
// Test minimum birth date calculation
|
|
89
|
+
expect(minimumMinorBirthDate).toBe(expectedMinimumMinorBirthDate);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("parses a SvelteKit load error", () => {
|
|
93
|
+
const error = { code: 500, message: "Internal Server Error" };
|
|
94
|
+
expect(parseLoadError(error)).toEqual(new Error("Internal Server Error"));
|
|
95
|
+
// add assertion to check for SvelteKit `error` function?
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("parses a Postgrest error", () => {
|
|
99
|
+
expect(parsePostgrestError(null)).toEqual({ error: undefined });
|
|
100
|
+
expect(
|
|
101
|
+
parsePostgrestError({
|
|
102
|
+
code: "42501",
|
|
103
|
+
name: "insufficient_privilege",
|
|
104
|
+
message: 'Insufficient privileges: permission denied for relation "restricted_table"',
|
|
105
|
+
details: 'User does not have the necessary permissions to access the relation "restricted_table".',
|
|
106
|
+
hint: ""
|
|
107
|
+
})
|
|
108
|
+
).toEqual({ error: { code: 403, message: Status.ERROR } });
|
|
109
|
+
expect(
|
|
110
|
+
parsePostgrestError({
|
|
111
|
+
code: "42883",
|
|
112
|
+
name: "undefined_function",
|
|
113
|
+
message: "Undefined function: function non_existent_function() does not exist",
|
|
114
|
+
details: "Function non_existent_function() does not exist.",
|
|
115
|
+
hint: "No function matches the given name and argument types. You might need to add explicit type casts."
|
|
116
|
+
})
|
|
117
|
+
).toEqual({ error: { code: 404, message: Status.ERROR } });
|
|
118
|
+
expect(
|
|
119
|
+
parsePostgrestError({
|
|
120
|
+
code: "25006",
|
|
121
|
+
name: "read_only_sql_transaction",
|
|
122
|
+
message: "Read only SQL transaction: cannot execute INSERT in a read-only transaction",
|
|
123
|
+
details: "Cannot execute INSERT in a read-only transaction.",
|
|
124
|
+
hint: ""
|
|
125
|
+
})
|
|
126
|
+
).toEqual({ error: { code: 405, message: Status.ERROR } });
|
|
127
|
+
expect(
|
|
128
|
+
parsePostgrestError({
|
|
129
|
+
code: "23505",
|
|
130
|
+
name: "unique_violation",
|
|
131
|
+
message: 'Uniqueness violation: duplicate key value violates unique constraint "users_username_key"',
|
|
132
|
+
details: "Key (username)=(john_doe) already exists.",
|
|
133
|
+
hint: ""
|
|
134
|
+
})
|
|
135
|
+
).toEqual({ error: { code: 409, message: Status.ERROR } });
|
|
136
|
+
expect(
|
|
137
|
+
parsePostgrestError({
|
|
138
|
+
code: "53400",
|
|
139
|
+
name: "configuration_file_error",
|
|
140
|
+
message: "invalid page header in block 0 of relation base/12345/67890",
|
|
141
|
+
details: "The page header is corrupted. This may indicate hardware or file system issues.",
|
|
142
|
+
hint: ""
|
|
143
|
+
})
|
|
144
|
+
).toEqual({ error: { code: 500, message: Status.ERROR } });
|
|
145
|
+
expect(
|
|
146
|
+
parsePostgrestError({
|
|
147
|
+
code: "08003",
|
|
148
|
+
name: "connection_does_not_exist",
|
|
149
|
+
message: "connection does not exist",
|
|
150
|
+
details: "The connection to the database was lost or never established.",
|
|
151
|
+
hint: ""
|
|
152
|
+
})
|
|
153
|
+
).toEqual({ error: { code: 503, message: Status.ERROR } });
|
|
154
|
+
expect(
|
|
155
|
+
parsePostgrestError({
|
|
156
|
+
code: "P0001",
|
|
157
|
+
name: "raise_exception",
|
|
158
|
+
message: "I'm a teapot!",
|
|
159
|
+
details: "Pretty simple",
|
|
160
|
+
hint: "418"
|
|
161
|
+
})
|
|
162
|
+
).toEqual({ error: { code: 418, message: "I'm a teapot!" } });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("sanitises HTML", () => {
|
|
166
|
+
expect(sanitiseHtml("<script>alert('Hello!');</script>")).toBe("");
|
|
167
|
+
expect(sanitiseHtml("<p>Hello!</p>")).toBe("Hello!");
|
|
168
|
+
expect(sanitiseHtml("<p>Hello!</p><script>alert('Hello!');</script>")).toBe("Hello!");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("validates E.164 phone numbers", () => {
|
|
172
|
+
// Valid phone number
|
|
173
|
+
expect(validateE164("+254712345678")).toBe(true);
|
|
174
|
+
expect(validateE164("+254712345678", "en-KE")).toBe(true);
|
|
175
|
+
expect(validateE164("+254712345678", "en-RW")).toBe(false);
|
|
176
|
+
|
|
177
|
+
// Invalid phone numbers
|
|
178
|
+
expect(validateE164("+254812345678")).toBe(false);
|
|
179
|
+
expect(validateE164("254812345678")).toBe(false);
|
|
180
|
+
expect(validateE164("+2548123456789")).toBe(false);
|
|
181
|
+
expect(validateE164("2548123456789")).toBe(false);
|
|
182
|
+
expect(validateE164("08123456789")).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("validates social media handles", () => {
|
|
186
|
+
expect(validateSocialMediaHandle("username")).toBe(true);
|
|
187
|
+
expect(validateSocialMediaHandle("@username")).toBe(false);
|
|
188
|
+
expect(validateSocialMediaHandle("/username")).toBe(false);
|
|
189
|
+
expect(validateSocialMediaHandle("https://www.facebook.com/username")).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { Database } from "@koloseum/types/database";
|
|
2
|
+
import type { CustomError, SocialMediaPlatform } from "@koloseum/types/public/auth.js";
|
|
3
|
+
|
|
4
|
+
import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";
|
|
5
|
+
import type { MobilePhoneLocale } from "validator";
|
|
6
|
+
|
|
7
|
+
import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js";
|
|
8
|
+
import { error as svelteError } from "@sveltejs/kit";
|
|
9
|
+
|
|
10
|
+
import parser from "any-date-parser";
|
|
11
|
+
import sanitize from "sanitize-html";
|
|
12
|
+
import validator from "validator";
|
|
13
|
+
|
|
14
|
+
/* Helper functions */
|
|
15
|
+
const { isMobilePhone, isURL } = validator;
|
|
16
|
+
|
|
17
|
+
/* Status messages */
|
|
18
|
+
export const Status = {
|
|
19
|
+
/**
|
|
20
|
+
* A generic error message.
|
|
21
|
+
*/
|
|
22
|
+
ERROR: "An unexpected error occurred. Kindly try again.",
|
|
23
|
+
/**
|
|
24
|
+
* A generic loading message.
|
|
25
|
+
*/
|
|
26
|
+
LOADING: "Please wait…",
|
|
27
|
+
/**
|
|
28
|
+
* A generic password reset request message.
|
|
29
|
+
*/
|
|
30
|
+
PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly.",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/* Utility functions */
|
|
34
|
+
export const Utility = {
|
|
35
|
+
/**
|
|
36
|
+
* Calls an Edge function.
|
|
37
|
+
* @param {SupabaseClient} supabase - The Supabase client
|
|
38
|
+
* @param {string} path - The path to the Edge function
|
|
39
|
+
* @param {"GET" | "POST"} [method="GET"] - The HTTP method to use
|
|
40
|
+
* @returns The response from the Edge function
|
|
41
|
+
*/
|
|
42
|
+
callEdgeFunction: async (supabase: SupabaseClient<Database>, path: string, method: "GET" | "POST" = "GET"): Promise<{ data?: any; error?: string }> => {
|
|
43
|
+
// Fetch response
|
|
44
|
+
const { data, error } = await supabase.functions.invoke(path, { method });
|
|
45
|
+
|
|
46
|
+
// Return error if any
|
|
47
|
+
if (error instanceof FunctionsHttpError) return { error: await error.context.json() };
|
|
48
|
+
else if (error instanceof FunctionsRelayError) return { error: error.message };
|
|
49
|
+
else if (error instanceof FunctionsFetchError) return { error: error.message };
|
|
50
|
+
|
|
51
|
+
// Return response
|
|
52
|
+
return { data };
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Capitalises a given word.
|
|
56
|
+
* @param {string} word - The word to be capitalised
|
|
57
|
+
* @returns The capitalised word
|
|
58
|
+
*/
|
|
59
|
+
capitalise: (word: string) =>
|
|
60
|
+
word.charAt(0).toUpperCase() +
|
|
61
|
+
word
|
|
62
|
+
.slice(1)
|
|
63
|
+
.split("")
|
|
64
|
+
.map((c) => c.toLowerCase())
|
|
65
|
+
.join(""),
|
|
66
|
+
/**
|
|
67
|
+
* Returns a custom error object.
|
|
68
|
+
* @param {number} code - The error code; defaults to `500` if not provided or invalid
|
|
69
|
+
* @param {string} message - The error message
|
|
70
|
+
* @returns An object with the error `code` and `message`
|
|
71
|
+
*/
|
|
72
|
+
customError: (code: number | undefined, message: string): CustomError => (!code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message }),
|
|
73
|
+
/**
|
|
74
|
+
* A regular expression for E.164 phone numbers.
|
|
75
|
+
*
|
|
76
|
+
* - Source: https://www.twilio.com/docs/glossary/what-e164
|
|
77
|
+
*/
|
|
78
|
+
e164Regex: /^\+?[1-9]\d{1,14}$/,
|
|
79
|
+
/**
|
|
80
|
+
* Formats a date in the format DD/MM/YYYY (by default) or YYYY-MM-DD (for client).
|
|
81
|
+
* @param {string} date - Date to be formatted
|
|
82
|
+
* @param {boolean} forClient - Specify whether the data is for displaying on the client; defaults to `false`
|
|
83
|
+
* @returns The formatted date string, or `null` if invalid input or in case of an error
|
|
84
|
+
*/
|
|
85
|
+
formatDate: (date: string, forClient: boolean = false) => {
|
|
86
|
+
if (date === "") return null;
|
|
87
|
+
try {
|
|
88
|
+
const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
|
|
89
|
+
day: "2-digit",
|
|
90
|
+
month: "2-digit",
|
|
91
|
+
year: "numeric",
|
|
92
|
+
// @ts-ignore: .fromString method exists but is not typed in `any-date-parser` package
|
|
93
|
+
}).format(parser.fromString(date));
|
|
94
|
+
|
|
95
|
+
return formattedDate;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* Formats a social media platform for display.
|
|
102
|
+
* @param {SocialMediaPlatform} platform - The social media platform to be formatted
|
|
103
|
+
* @returns The formatted social media platform string
|
|
104
|
+
*/
|
|
105
|
+
formatSocialMediaPlatform: (platform: SocialMediaPlatform) => {
|
|
106
|
+
const platforms = ["facebook", "instagram", "kick", "tiktok", "twitch", "twitter", "youtube"];
|
|
107
|
+
if (!platforms.includes(platform)) return "";
|
|
108
|
+
|
|
109
|
+
if (platform === "tiktok") return "TikTok";
|
|
110
|
+
if (platform === "twitter") return "X (formerly Twitter)";
|
|
111
|
+
if (platform === "youtube") return "YouTube";
|
|
112
|
+
|
|
113
|
+
return Utility.capitalise(platform);
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* Returns an age in years given a birth date.
|
|
117
|
+
* @param {string} dateOfBirth - The date of birth as a string, e.g. `DD-MM-YYYY`
|
|
118
|
+
* @returns The age in years, or `NaN` if the input is invalid
|
|
119
|
+
*/
|
|
120
|
+
getAge: (dateOfBirth: string) => {
|
|
121
|
+
if (dateOfBirth === "" || typeof dateOfBirth !== "string") return NaN;
|
|
122
|
+
dateOfBirth = Utility.formatDate(dateOfBirth, true) as string;
|
|
123
|
+
|
|
124
|
+
const birthDate = new Date(dateOfBirth);
|
|
125
|
+
const currentDate = new Date();
|
|
126
|
+
|
|
127
|
+
let age = currentDate.getFullYear() - birthDate.getFullYear();
|
|
128
|
+
const monthDiff = currentDate.getMonth() - birthDate.getMonth();
|
|
129
|
+
|
|
130
|
+
return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
|
|
134
|
+
* - 9 characters long
|
|
135
|
+
* - begins with "KLB" followed by 7 digits
|
|
136
|
+
*/
|
|
137
|
+
loungeBranchIdRegex: /^KLB\d{7}$/,
|
|
138
|
+
/**
|
|
139
|
+
* A regular expression for Koloseum Lounge IDs. It covers the following rules:
|
|
140
|
+
* - 9 characters long
|
|
141
|
+
* - begins with "KL" followed by 7 digits
|
|
142
|
+
*/
|
|
143
|
+
loungeIdRegex: /^KL\d{7}$/,
|
|
144
|
+
/**
|
|
145
|
+
* The minimum birth date (from today's date) for a user who is a minor, i.e. `YYYY-MM-DD`
|
|
146
|
+
*/
|
|
147
|
+
minimumMinorBirthDate: (() => {
|
|
148
|
+
const today = new Date();
|
|
149
|
+
const tomorrow = new Date();
|
|
150
|
+
tomorrow.setUTCDate(today.getDate() + 1);
|
|
151
|
+
|
|
152
|
+
const tomorrow18YearsAgo = tomorrow.getFullYear() - 18;
|
|
153
|
+
tomorrow.setFullYear(tomorrow18YearsAgo);
|
|
154
|
+
|
|
155
|
+
return tomorrow.toISOString().split("T")[0];
|
|
156
|
+
})(),
|
|
157
|
+
/**
|
|
158
|
+
* Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
|
|
159
|
+
* @param {CustomError} error - The error object
|
|
160
|
+
* @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
|
|
161
|
+
*/
|
|
162
|
+
parseLoadError: (error: CustomError) => (error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message })),
|
|
163
|
+
/**
|
|
164
|
+
* Parses a `PostgrestError` object and returns a custom error object if any has occurred.
|
|
165
|
+
* @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
|
|
166
|
+
* @returns An object with an `error` if any has occurred
|
|
167
|
+
*/
|
|
168
|
+
parsePostgrestError: (postgrestError: PostgrestError | null): { error?: CustomError } => {
|
|
169
|
+
// Return undefined if no error occurred
|
|
170
|
+
let error: CustomError | undefined;
|
|
171
|
+
if (!postgrestError) return { error };
|
|
172
|
+
|
|
173
|
+
// Return custom error if hint is a number between 400 and 599
|
|
174
|
+
const customErrorCode = Number(postgrestError.hint);
|
|
175
|
+
if (!isNaN(customErrorCode) && customErrorCode >= 400 && customErrorCode <= 599) {
|
|
176
|
+
error = Utility.customError(customErrorCode, postgrestError.message);
|
|
177
|
+
return { error };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Map Postgrest error codes to custom error codes
|
|
181
|
+
const errorMap = [
|
|
182
|
+
{ code: "08", status: 503 },
|
|
183
|
+
{ code: "09", status: 500 },
|
|
184
|
+
{ code: "0L", status: 403 },
|
|
185
|
+
{ code: "0P", status: 403 },
|
|
186
|
+
{ code: "23503", status: 409 },
|
|
187
|
+
{ code: "23505", status: 409 },
|
|
188
|
+
{ code: "25006", status: 405 },
|
|
189
|
+
{ code: "25", status: 500 },
|
|
190
|
+
{ code: "28", status: 403 },
|
|
191
|
+
{ code: "2D", status: 500 },
|
|
192
|
+
{ code: "38", status: 500 },
|
|
193
|
+
{ code: "39", status: 500 },
|
|
194
|
+
{ code: "3B", status: 500 },
|
|
195
|
+
{ code: "40", status: 500 },
|
|
196
|
+
{ code: "53400", status: 500 },
|
|
197
|
+
{ code: "53", status: 503 },
|
|
198
|
+
{ code: "54", status: 500 },
|
|
199
|
+
{ code: "55", status: 500 },
|
|
200
|
+
{ code: "57", status: 500 },
|
|
201
|
+
{ code: "58", status: 500 },
|
|
202
|
+
{ code: "F0", status: 500 },
|
|
203
|
+
{ code: "HV", status: 500 },
|
|
204
|
+
{ code: "P0001", status: 400 },
|
|
205
|
+
{ code: "P0", status: 500 },
|
|
206
|
+
{ code: "XX", status: 500 },
|
|
207
|
+
{ code: "42883", status: 404 },
|
|
208
|
+
{ code: "42P01", status: 404 },
|
|
209
|
+
{ code: "42P17", status: 500 },
|
|
210
|
+
{ code: "42501", status: 403 },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
// Return custom error if Postgrest error code is found
|
|
214
|
+
for (const { code, status } of errorMap)
|
|
215
|
+
if (postgrestError.code === code || postgrestError.code.startsWith(code)) {
|
|
216
|
+
error = Utility.customError(status, Status.ERROR);
|
|
217
|
+
return { error };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Return generic error
|
|
221
|
+
error = Utility.customError(500, Status.ERROR);
|
|
222
|
+
return { error };
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* A regular expression for user passwords to match during authentication. It covers the following rules:
|
|
226
|
+
* - at least 8 characters long
|
|
227
|
+
* - at least one digit
|
|
228
|
+
* - at least one lowercase letter
|
|
229
|
+
* - at least one uppercase letter
|
|
230
|
+
* - at least one symbol
|
|
231
|
+
*/
|
|
232
|
+
passwordRegex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()-_+=|{}[\]:;'<>,.?/~]).{8,}$/,
|
|
233
|
+
/**
|
|
234
|
+
* A regular expression for Koloseum Player IDs. It covers the following rules:
|
|
235
|
+
* - 9 characters long
|
|
236
|
+
* - begins with "KP" followed by 7 digits
|
|
237
|
+
*/
|
|
238
|
+
playerIdRegex: /^KP\d{7}$/,
|
|
239
|
+
/**
|
|
240
|
+
* Sanitises any potential HTML injected into user input.
|
|
241
|
+
* @param {string} input - The input to be sanitised
|
|
242
|
+
* @returns A sanitised string, or an empty string if the input is invalid
|
|
243
|
+
*/
|
|
244
|
+
sanitiseHtml: (input: string) => (typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} })),
|
|
245
|
+
/**
|
|
246
|
+
* A regular expression for social media handles, without a leading slash or @ character.
|
|
247
|
+
*
|
|
248
|
+
* - Source: https://stackoverflow.com/a/74579651
|
|
249
|
+
*/
|
|
250
|
+
socialMediaHandleRegex: /^([A-Za-z0-9_.]{3,25})$/gm,
|
|
251
|
+
/**
|
|
252
|
+
* Validates an E.164 phone number.
|
|
253
|
+
* @param {string} phoneNumber - The phone number to be validated
|
|
254
|
+
* @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
|
|
255
|
+
* @returns `true` if the phone number is valid; `false` otherwise
|
|
256
|
+
*/
|
|
257
|
+
validateE164: (phoneNumber: string, locale: "any" | MobilePhoneLocale | MobilePhoneLocale[] = "en-KE") => isMobilePhone(phoneNumber, locale) && Boolean(phoneNumber.match(Utility.e164Regex)),
|
|
258
|
+
/**
|
|
259
|
+
* Validates a social media handle.
|
|
260
|
+
* @param {string} handle - The social media handle to be validated
|
|
261
|
+
* @returns `true` if the handle is valid; `false` otherwise
|
|
262
|
+
*/
|
|
263
|
+
validateSocialMediaHandle: (handle: string) => !isURL(handle, { require_protocol: false }) && !handle.startsWith("@") && Boolean(handle.match(Utility.socialMediaHandleRegex)),
|
|
264
|
+
};
|