@koloseum/utils 0.1.9 → 0.1.10

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
@@ -1,4 +1,5 @@
1
- import type { CustomError, PronounsCheckboxes, SocialMediaPlatform } from "@koloseum/types/public/auth";
1
+ import type { CustomError } from "@koloseum/types/general";
2
+ import type { PronounsCheckboxes, SocialMediaPlatform } from "@koloseum/types/public-auth";
2
3
  import type { PostgrestError } from "@supabase/supabase-js";
3
4
  import type { MobilePhoneLocale } from "validator";
4
5
  export declare const Status: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koloseum/utils",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "author": "Koloseum Technologies Limited",
5
5
  "type": "module",
6
6
  "description": "Utility logic for use across Koloseum web apps (TypeScript)",
@@ -17,10 +17,9 @@
17
17
  },
18
18
  "main": "./dist/utils.js",
19
19
  "exports": {
20
- "./src": "./dist/utils.js"
20
+ ".": "./dist/utils.js"
21
21
  },
22
22
  "files": [
23
- "./src/**/*.ts",
24
23
  "./dist/**/*"
25
24
  ],
26
25
  "scripts": {
@@ -30,13 +29,13 @@
30
29
  "test": "vitest --run"
31
30
  },
32
31
  "dependencies": {
33
- "@koloseum/types": "^0.1.3",
34
32
  "@supabase/supabase-js": "^2.48.1",
35
33
  "@sveltejs/kit": "^2.17.1",
36
34
  "sanitize-html": "^2.14.0",
37
35
  "validator": "^13.12.0"
38
36
  },
39
37
  "devDependencies": {
38
+ "@koloseum/types": "^0.1.9",
40
39
  "@playwright/test": "^1.50.1",
41
40
  "@types/sanitize-html": "^2.13.0",
42
41
  "@types/validator": "^13.12.2",
package/src/utils.test.ts DELETED
@@ -1,191 +0,0 @@
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("31 December 2021")).toBe("31/12/2021");
50
- expect(formatDate("31 December 2021", true)).toBe("2021-12-31");
51
- expect(formatDate("31 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 DELETED
@@ -1,313 +0,0 @@
1
- import type { CustomError, PronounsCheckboxes, SocialMediaPlatform } from "@koloseum/types/public/auth";
2
- import type { PostgrestError } from "@supabase/supabase-js";
3
- import type { MobilePhoneLocale } from "validator";
4
-
5
- import { error as svelteError } from "@sveltejs/kit";
6
-
7
- import sanitize from "sanitize-html";
8
- import validator from "validator";
9
-
10
- /* Helper functions */
11
- const { isMobilePhone, isURL } = validator;
12
-
13
- /* Status messages */
14
- export const Status = {
15
- /**
16
- * A generic error message.
17
- */
18
- ERROR: "An unexpected error occurred. Kindly try again.",
19
- /**
20
- * A generic loading message.
21
- */
22
- LOADING: "Please wait…",
23
- /**
24
- * A generic password reset request message.
25
- */
26
- PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly."
27
- };
28
-
29
- /* Utility functions */
30
- export const Utility = {
31
- /**
32
- * Capitalises a given word.
33
- * @param {string} word - The word to be capitalised
34
- * @returns The capitalised word
35
- */
36
- capitalise: (word: string) =>
37
- word.charAt(0).toUpperCase() +
38
- word
39
- .slice(1)
40
- .split("")
41
- .map((c) => c.toLowerCase())
42
- .join(""),
43
- /**
44
- * Returns a custom error object.
45
- * @param {number} code - The error code; defaults to `500` if not provided or invalid
46
- * @param {string} message - The error message
47
- * @returns An object with the error `code` and `message`
48
- */
49
- customError: (code: number | undefined, message: string): CustomError =>
50
- !code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message },
51
- /**
52
- * A regular expression for E.164 phone numbers.
53
- *
54
- * - Source: https://www.twilio.com/docs/glossary/what-e164
55
- */
56
- e164Regex: /^\+?[1-9]\d{1,14}$/,
57
- /**
58
- * Formats a date in the format DD/MM/YYYY (by default) or YYYY-MM-DD (for client).
59
- * @param {string} date - Date to be formatted
60
- * @param {boolean} forClient - Specify whether the data is for displaying on the client; defaults to `false`
61
- * @returns The formatted date string, or `null` if invalid input or in case of an error
62
- */
63
- formatDate: (date: string, forClient: boolean = false) => {
64
- // Return null if no date is provided
65
- if (date === "" || typeof date !== "string") return null;
66
-
67
- // Handle input
68
- try {
69
- // Parse date string
70
- const parsedDate = Date.parse(date);
71
-
72
- // Return null if date string is not parseable
73
- if (isNaN(parsedDate)) return null;
74
-
75
- // Use the Intl.DateTimeFormat with explicit options to ensure correct formatting
76
- const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
77
- day: "2-digit",
78
- month: "2-digit",
79
- year: "numeric",
80
- timeZone: "Africa/Nairobi"
81
- }).format(parsedDate);
82
-
83
- // Return formatted date string
84
- return formattedDate;
85
- } catch (error) {
86
- console.log(error);
87
- return null;
88
- }
89
- },
90
- /**
91
- * Formats a social media platform for display.
92
- * @param {SocialMediaPlatform} platform - The social media platform to be formatted
93
- * @returns The formatted social media platform string
94
- */
95
- formatSocialMediaPlatform: (platform: SocialMediaPlatform) => {
96
- const platforms = ["facebook", "instagram", "kick", "tiktok", "twitch", "twitter", "youtube"];
97
- if (!platforms.includes(platform)) return "";
98
-
99
- if (platform === "tiktok") return "TikTok";
100
- if (platform === "twitter") return "X (formerly Twitter)";
101
- if (platform === "youtube") return "YouTube";
102
-
103
- return Utility.capitalise(platform);
104
- },
105
- /**
106
- * Returns an age in years given a birth date.
107
- * @param {string} dateOfBirth - The date of birth as a string, e.g. `DD-MM-YYYY`
108
- * @returns The age in years, or `NaN` if the input is invalid
109
- */
110
- getAge: (dateOfBirth: string) => {
111
- if (dateOfBirth === "" || typeof dateOfBirth !== "string") return NaN;
112
- dateOfBirth = Utility.formatDate(dateOfBirth, true) as string;
113
-
114
- const birthDate = new Date(dateOfBirth);
115
- const currentDate = new Date();
116
-
117
- let age = currentDate.getFullYear() - birthDate.getFullYear();
118
- const monthDiff = currentDate.getMonth() - birthDate.getMonth();
119
-
120
- return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
121
- },
122
- /**
123
- * Handles the click event for pronouns checkboxes.
124
- * @param {MouseEvent} e - The click event
125
- * @param {PronounsCheckboxes} checkboxes - The pronouns checkboxes
126
- */
127
- handlePronounsCheckboxClick: (e: MouseEvent, checkboxes: PronounsCheckboxes) => {
128
- const target = e.target as EventTarget & HTMLInputElement;
129
-
130
- if (target.value === "none") {
131
- for (let checkbox of Object.values(checkboxes))
132
- if (checkbox && checkbox !== target && checkbox.checked) {
133
- e.preventDefault();
134
- return;
135
- }
136
-
137
- for (let checkbox of Object.values(checkboxes))
138
- if (checkbox && checkbox !== target) {
139
- checkbox.checked = false;
140
- checkbox.disabled = !checkbox.disabled;
141
- }
142
- } else {
143
- if (checkboxes.none?.checked) {
144
- e.preventDefault();
145
- return;
146
- }
147
-
148
- if (target.value === "male" || target.value === "female") {
149
- const oppositeIndex = target.value === "male" ? "female" : "male";
150
- const oppositeCheckbox = checkboxes[oppositeIndex];
151
-
152
- if (oppositeCheckbox?.checked) {
153
- e.preventDefault();
154
- return;
155
- }
156
-
157
- if (oppositeCheckbox) {
158
- oppositeCheckbox.checked = false;
159
- oppositeCheckbox.disabled = !oppositeCheckbox.disabled;
160
- }
161
-
162
- if (checkboxes.none) {
163
- checkboxes.none.checked = false;
164
- if (!checkboxes.neutral?.checked) checkboxes.none.disabled = !checkboxes.none.disabled;
165
- }
166
- } else {
167
- if (checkboxes.none) {
168
- checkboxes.none.checked = false;
169
- if (checkboxes.neutral && !checkboxes.male?.checked && !checkboxes.female?.checked)
170
- checkboxes.none.disabled = checkboxes.neutral.checked;
171
- }
172
- }
173
- }
174
- },
175
- /**
176
- * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
177
- * - 9 characters long
178
- * - begins with "KLB" followed by 7 digits
179
- */
180
- loungeBranchIdRegex: /^KLB\d{7}$/,
181
- /**
182
- * A regular expression for Koloseum Lounge IDs. It covers the following rules:
183
- * - 9 characters long
184
- * - begins with "KL" followed by 7 digits
185
- */
186
- loungeIdRegex: /^KL\d{7}$/,
187
- /**
188
- * The minimum birth date (from today's date) for a user who is a minor, i.e. `YYYY-MM-DD`
189
- */
190
- minimumMinorBirthDate: (() => {
191
- const today = new Date();
192
- const tomorrow = new Date();
193
- tomorrow.setUTCDate(today.getDate() + 1);
194
-
195
- const tomorrow18YearsAgo = tomorrow.getFullYear() - 18;
196
- tomorrow.setFullYear(tomorrow18YearsAgo);
197
-
198
- return tomorrow.toISOString().split("T")[0];
199
- })(),
200
- /**
201
- * Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
202
- * @param {CustomError} error - The error object
203
- * @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
204
- */
205
- parseLoadError: (error: CustomError) =>
206
- error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message }),
207
- /**
208
- * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
209
- * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
210
- * @returns An object with an `error` if any has occurred
211
- */
212
- parsePostgrestError: (postgrestError: PostgrestError | null): { error?: CustomError } => {
213
- // Return undefined if no error occurred
214
- let error: CustomError | undefined;
215
- if (!postgrestError) return { error };
216
-
217
- // Return custom error if hint is a number between 400 and 599
218
- const customErrorCode = Number(postgrestError.hint);
219
- if (!isNaN(customErrorCode) && customErrorCode >= 400 && customErrorCode <= 599) {
220
- error = Utility.customError(customErrorCode, postgrestError.message);
221
- return { error };
222
- }
223
-
224
- // Map Postgrest error codes to custom error codes
225
- const errorMap = [
226
- { code: "08", status: 503 },
227
- { code: "09", status: 500 },
228
- { code: "0L", status: 403 },
229
- { code: "0P", status: 403 },
230
- { code: "23503", status: 409 },
231
- { code: "23505", status: 409 },
232
- { code: "25006", status: 405 },
233
- { code: "25", status: 500 },
234
- { code: "28", status: 403 },
235
- { code: "2D", status: 500 },
236
- { code: "38", status: 500 },
237
- { code: "39", status: 500 },
238
- { code: "3B", status: 500 },
239
- { code: "40", status: 500 },
240
- { code: "53400", status: 500 },
241
- { code: "53", status: 503 },
242
- { code: "54", status: 500 },
243
- { code: "55", status: 500 },
244
- { code: "57", status: 500 },
245
- { code: "58", status: 500 },
246
- { code: "F0", status: 500 },
247
- { code: "HV", status: 500 },
248
- { code: "P0001", status: 400 },
249
- { code: "P0", status: 500 },
250
- { code: "XX", status: 500 },
251
- { code: "42883", status: 404 },
252
- { code: "42P01", status: 404 },
253
- { code: "42P17", status: 500 },
254
- { code: "42501", status: 403 }
255
- ];
256
-
257
- // Return custom error if Postgrest error code is found
258
- for (const { code, status } of errorMap)
259
- if (postgrestError.code === code || postgrestError.code.startsWith(code)) {
260
- error = Utility.customError(status, Status.ERROR);
261
- return { error };
262
- }
263
-
264
- // Return generic error
265
- error = Utility.customError(500, Status.ERROR);
266
- return { error };
267
- },
268
- /**
269
- * A regular expression for user passwords to match during authentication. It covers the following rules:
270
- * - at least 8 characters long
271
- * - at least one digit
272
- * - at least one lowercase letter
273
- * - at least one uppercase letter
274
- * - at least one symbol
275
- */
276
- passwordRegex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()-_+=|{}[\]:;'<>,.?/~]).{8,}$/,
277
- /**
278
- * A regular expression for Koloseum Player IDs. It covers the following rules:
279
- * - 9 characters long
280
- * - begins with "KP" followed by 7 digits
281
- */
282
- playerIdRegex: /^KP\d{7}$/,
283
- /**
284
- * Sanitises any potential HTML injected into user input.
285
- * @param {string} input - The input to be sanitised
286
- * @returns A sanitised string, or an empty string if the input is invalid
287
- */
288
- sanitiseHtml: (input: string) =>
289
- typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
290
- /**
291
- * A regular expression for social media handles, without a leading slash or @ character.
292
- *
293
- * - Source: https://stackoverflow.com/a/74579651
294
- */
295
- socialMediaHandleRegex: /^([A-Za-z0-9_.]{3,25})$/gm,
296
- /**
297
- * Validates an E.164 phone number.
298
- * @param {string} phoneNumber - The phone number to be validated
299
- * @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
300
- * @returns `true` if the phone number is valid; `false` otherwise
301
- */
302
- validateE164: (phoneNumber: string, locale: "any" | MobilePhoneLocale | MobilePhoneLocale[] = "en-KE") =>
303
- isMobilePhone(phoneNumber, locale) && Boolean(phoneNumber.match(Utility.e164Regex)),
304
- /**
305
- * Validates a social media handle.
306
- * @param {string} handle - The social media handle to be validated
307
- * @returns `true` if the handle is valid; `false` otherwise
308
- */
309
- validateSocialMediaHandle: (handle: string) =>
310
- !isURL(handle, { require_protocol: false }) &&
311
- !handle.startsWith("@") &&
312
- Boolean(handle.match(Utility.socialMediaHandleRegex))
313
- };