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