diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..b5c68e5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve title: '' labels: '' assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cbe842a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "semi": false, + "singleQuote": true +} diff --git a/.vscode/launch.json b/.vscode/launch.json index e221d2b..6858a77 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,53 +1,52 @@ { - "version": "0.2.0", - "configurations": [ - { - "type": "bun", - "request": "launch", - "name": "Debug Bun", - - // The path to a JavaScript or TypeScript file to run. - "program": "${file}", - - // The arguments to pass to the program, if any. - "args": [], - - // The working directory of the program. - "cwd": "${workspaceFolder}", - - // The environment variables to pass to the program. - "env": {}, - - // If the environment variables should not be inherited from the parent process. - "strictEnv": false, - - // If the program should be run in watch mode. - // This is equivalent to passing `--watch` to the `bun` executable. - // You can also set this to "hot" to enable hot reloading using `--hot`. - "watchMode": false, - - // If the debugger should stop on the first line of the program. - "stopOnEntry": false, - - // If the debugger should be disabled. (for example, breakpoints will not be hit) - "noDebug": false, - - // The path to the `bun` executable, defaults to your `PATH` environment variable. - "runtime": "bun", - - // The arguments to pass to the `bun` executable, if any. - // Unlike `args`, these are passed to the executable itself, not the program. - "runtimeArgs": [], - }, - { - "type": "bun", - "request": "attach", - "name": "Attach to Bun", - - // The URL of the WebSocket inspector to attach to. - // This value can be retrieved by using `bun --inspect`. - "url": "ws://localhost:6499/", - } - ] - } - \ No newline at end of file + "version": "0.2.0", + "configurations": [ + { + "type": "bun", + "request": "launch", + "name": "Debug Bun", + + // The path to a JavaScript or TypeScript file to run. + "program": "${file}", + + // The arguments to pass to the program, if any. + "args": [], + + // The working directory of the program. + "cwd": "${workspaceFolder}", + + // The environment variables to pass to the program. + "env": {}, + + // If the environment variables should not be inherited from the parent process. + "strictEnv": false, + + // If the program should be run in watch mode. + // This is equivalent to passing `--watch` to the `bun` executable. + // You can also set this to "hot" to enable hot reloading using `--hot`. + "watchMode": false, + + // If the debugger should stop on the first line of the program. + "stopOnEntry": false, + + // If the debugger should be disabled. (for example, breakpoints will not be hit) + "noDebug": false, + + // The path to the `bun` executable, defaults to your `PATH` environment variable. + "runtime": "bun", + + // The arguments to pass to the `bun` executable, if any. + // Unlike `args`, these are passed to the executable itself, not the program. + "runtimeArgs": [] + }, + { + "type": "bun", + "request": "attach", + "name": "Attach to Bun", + + // The URL of the WebSocket inspector to attach to. + // This value can be retrieved by using `bun --inspect`. + "url": "ws://localhost:6499/" + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 964897f..4ab7d6d 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/CREATING_A_MODULE.md b/CREATING_A_MODULE.md index 92bb051..207add6 100644 --- a/CREATING_A_MODULE.md +++ b/CREATING_A_MODULE.md @@ -5,49 +5,64 @@ Certainly! Let's create a README document to guide users through the process of # Creating a Module in BNK (Bun Nookit) ## Overview + This guide provides step-by-step instructions on how to create a new module for the BNK framework. Whether you're adding functionality like OAuth integration or something entirely different, these guidelines will help align the module with BNK's design philosophy and standards to ensure consistency. ## Prerequisites + - Familiarity with TypeScript and BNK's core concepts. - Understanding of the problem domain the module will address. ## Step 1: Research and Requirements Gathering + Before coding, understand the scope and requirements of the module. For instance, if you're building an OAuth module, research the OAuth 2.0 protocol, and identify the primary use cases you want to support. ## Step 2: Designing the Module + ### 2.1 Define the API + Design a clear and intuitive API for the module. Consider the functions and interfaces users will interact with. ### 2.2 Plan the Architecture + Ensure the module aligns with BNK's architecture. Use factory functions, avoid global state, and adhere to strong typing. ### 2.3 Security and Performance + Plan for security and performance from the start. This is especially important for modules handling sensitive data or requiring high efficiency. ## Step 3: Implementation + ### 3.1 Setup + Set up the basic structure of the module. Create a new directory and files as needed within the BNK project structure. ### 3.2 Core Functionality + Develop the core functionality of the module. Keep functions short and focused, and use descriptive names. ### 3.3 Integration + Ensure that the module integrates seamlessly with other BNK components. ### 3.4 Error Handling + Implement robust error handling to make the module resilient and reliable. ## Step 4: Testing + Write comprehensive tests for the module. Cover unit testing for individual functions and integration testing for the module as a whole. ## Step 5: Documentation + Document the module thoroughly. Include a usage guide, example implementations, and a detailed API reference. ## Step 6: Community Feedback and Iteration + Release a beta version of the module and encourage feedback from the BNK community. Iterate based on the feedback received. ## Best Practices + - **Follow BNK's Coding Style**: Adhere to the principles outlined in BNK's coding guidelines, such as using `const`, writing pure functions, and avoiding premature optimization. - **Use Descriptive Names**: Choose clear and descriptive names for functions, variables, and modules. - **Write Efficient Code**: Focus on practicality and optimization where necessary. Prioritize clarity and simplicity. - diff --git a/README.md b/README.md index 55cdfa2..5a7041a 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,15 @@ **Bun Nookit (BNK)** is a comprehensive toolkit for software development, leveraging the power of Bun and TypeScript. With zero third-party dependencies, strong TypeScript inferencing, and a focus on Web API standards, BNK offers a modular, type-safe, and efficient way to build robust applications. - ![GitHub License](https://img.shields.io/github/license/nookit-dev/bnkit) -![npm](https://img.shields.io/npm/v/bnkit?logo=npm) ![GitHub release (with filter)](https://img.shields.io/github/v/release/nookit-dev/bnkit) ![Stars](https://img.shields.io/github/stars/nookit-dev/bnkit) -![npm bundle size](https://img.shields.io/bundlephobia/min/bnkit) ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/bnkit) +![npm](https://img.shields.io/npm/v/bnkit?logo=npm) ![GitHub release (with filter)](https://img.shields.io/github/v/release/nookit-dev/bnkit) ![Stars](https://img.shields.io/github/stars/nookit-dev/bnkit) +![npm bundle size](https://img.shields.io/bundlephobia/min/bnkit) ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/npm/bnkit) -![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/bun_nook_kit). ![Discord](https://img.shields.io/discord/1164699087543746560)' +![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/bun_nook_kit). ![Discord](https://img.shields.io/discord/1164699087543746560)' +## BNK Server Quickstart -## BNK Server Quickstart ```bash bash <(curl -fsSL https://raw.githubusercontent.com/nookit-dev/bnkit/main/scripts/quickstart.sh) ``` @@ -22,21 +21,25 @@ Visit `http://localhost:3000` in your browser and you should see Hello world and `http://localhost:3000/json` for the json --- -### + +### + # [📋 Documentation](https://nookit.dev/readme) + #### [🧩 Modules Docs](https://nookit.dev/modules) + #### [🖥️ BNK CLI Docs](https://nookit.dev/bnk-cli/bnk-cli-readme) -#### [🔌 Plugin Docs](https://nookit.dev/plugins/BNK+Plugins) -### +#### [🔌 Plugin Docs](https://nookit.dev/plugins/BNK+Plugins) +### ## Bun Nookit Package Installation -Install in your project: +Install in your project: `bun add bnkit` -Plugin install example: +Plugin install example: `bun add @bnk/react` Use any an all Bun Nookit modules - server example with json response (similar to starter project) @@ -44,26 +47,26 @@ Use any an all Bun Nookit modules - server example with json response (similar t `index.ts` ```typescript -import { jsonRes, serverFactory } from "bnkit/server"; -import { middleware, RoutesWithMiddleware } from "./middlewares"; +import { jsonRes, serverFactory } from 'bnkit/server' +import { middleware, RoutesWithMiddleware } from './middlewares' const routes = { - "/": { + '/': { // parse from request if neeeded - get: (request) => new Response("Hello World!") + get: (request) => new Response('Hello World!'), + }, + '/json': { + get: (request) => + bnk.server.jsonRes({ + message: 'Hello JSON Response!', + }), }, - "/json": { - get: request => bnk.server.jsonRes({ - message: "Hello JSON Response!" - }) - } } satisfies RoutesWithMiddleware const { start, routes } = bnk.server.serverFactory({ routes, - middleware -}); - + middleware, +}) // start on default port 3000 start() @@ -76,7 +79,6 @@ Join our [Discord Server]("https://discord.gg/rQyWN7V6") https://discord.gg/rQyW ## Key Highlights - **Zero Third Paty Dependencies** - BNK uses nothin' but Bun - - **Unit Tested** - To ensure BNK is reliable, changeable, and upgradeable. - **TypeSafe with Strong TypeScript type Inferencing** - Strong types tell you where things are incorrect, strong type inferrence allows you to utilize the advantages of strong types and not having to deal with too much TypeScript. @@ -138,6 +140,7 @@ Close To Final For V1: ### Better handling for Server Sent Events in Server, Fetcher, etc ## Screenshots + (if you made it this far) Create typesafe server routes and middleware! @@ -146,8 +149,8 @@ Create typesafe server routes and middleware! Xnapper-2023-11-14-19 47 14 - ### Sponsors + None! Be the first to sponsor BNK :) ## License @@ -157,4 +160,5 @@ Bun Nookit is licensed under the MIT License. Enjoy the freedom to use, modify, Jumpstart your journey to revolutionary software development with Bun Nookit! Contribute to the docs: + ### [Docs Repo](https://github.com/nookit-dev/bnkit-docs) diff --git a/auth/example/google-oauth-server-example.ts b/auth/example/google-oauth-server-example.ts index e229f87..268f07c 100644 --- a/auth/example/google-oauth-server-example.ts +++ b/auth/example/google-oauth-server-example.ts @@ -1,68 +1,68 @@ -import { oAuthFactory } from "auth/oauth"; -import { initGoogleOAuth } from "auth/oauth-providers"; -import type { Routes } from "server"; -import { serverFactory } from "server"; +import { oAuthFactory } from 'auth/oauth' +import { initGoogleOAuth } from 'auth/oauth-providers' +import type { Routes } from 'server' +import { serverFactory } from 'server' -const googleClientId = Bun.env.GOOGLE_OAUTH_CLIENT_ID || ""; -const googleClientSecret = Bun.env.GOOGLE_OAUTH_CLIENT_SECRET || ""; +const googleClientId = Bun.env.GOOGLE_OAUTH_CLIENT_ID || '' +const googleClientSecret = Bun.env.GOOGLE_OAUTH_CLIENT_SECRET || '' const googleOAuthConfig = initGoogleOAuth({ clientId: googleClientId, clientSecret: googleClientSecret, -}); +}) -const googleOAuth = oAuthFactory(googleOAuthConfig); +const googleOAuth = oAuthFactory(googleOAuthConfig) const routes = { - "/login": { + '/login': { get: () => { // you could pass a param for the provider - const authUrl = googleOAuth.initiateOAuthFlow(); + const authUrl = googleOAuth.initiateOAuthFlow() return new Response(null, { headers: { Location: authUrl }, status: 302, - }); + }) }, }, - "/callback": { + '/callback': { get: async (req) => { try { - const host = req.headers.get("host"); + const host = req.headers.get('host') // Parse the URL and query parameters - const url = new URL(req.url, `http://${host}`); - const queryParams = new URLSearchParams(url.search); - const code = queryParams.get("code"); + const url = new URL(req.url, `http://${host}`) + const queryParams = new URLSearchParams(url.search) + const code = queryParams.get('code') if (!code) { - return new Response("No code provided in query", { status: 400 }); + return new Response('No code provided in query', { status: 400 }) } - const tokenInfo = await googleOAuth.handleRedirect(code); + const tokenInfo = await googleOAuth.handleRedirect(code) - console.log({ tokenInfo }); + console.log({ tokenInfo }) // Logic after successful authentication - return new Response("Login Successful!"); + return new Response('Login Successful!') } catch (error) { - console.error(error); - return new Response("Authentication failed", { status: 403 }); + console.error(error) + return new Response('Authentication failed', { status: 403 }) } }, }, - "/": { + '/': { get: () => { // HTML content for the login page - const htmlContent = `

Login with Google

`; + const htmlContent = `

Login with Google

` return new Response(htmlContent, { - headers: { "Content-Type": "text/html" }, - }); + headers: { 'Content-Type': 'text/html' }, + }) }, }, -} satisfies Routes; +} satisfies Routes const server = serverFactory({ routes, -}); +}) -server.start(3000); +server.start(3000) diff --git a/auth/example/setup-oauth-providers.md b/auth/example/setup-oauth-providers.md index 85898d3..9b6ec2b 100644 --- a/auth/example/setup-oauth-providers.md +++ b/auth/example/setup-oauth-providers.md @@ -3,29 +3,35 @@ ## Google OAuth Setup #### Step 1: Create a Google Cloud Project + - **Access Google Cloud Console**: Go to [Google Cloud Console](https://console.cloud.google.com/). - **New Project**: Click 'New Project', name it, and create. #### Step 2: Configure OAuth Consent Screen + - **Credentials Page**: Navigate to 'Credentials' under 'APIs & Services'. - **Consent Screen Setup**: Click 'Configure Consent Screen', select 'External', and create. - **Details**: Enter app name, support email, and developer email. Add optional details like logo and policy links. - **Save**: Click 'Save and Continue'. #### Step 3: Create OAuth 2.0 Credentials + - **Credentials Creation**: Back on 'Credentials' page, select 'Create Credentials' > 'OAuth client ID'. - **Application Type**: Choose 'Web application'. - **Redirect URIs**: Add your redirect URI (/callback). - **Client ID & Secret**: After clicking 'Create', note down the client ID and secret. #### Step 4: Enable Required APIs + - **API Library**: In 'Library', search and enable needed Google APIs. #### Step 5: Implement OAuth in Your App + - **Integrate Credentials**: Use client ID and secret in your app's OAuth config. - **Handle Redirects**: Ensure handling of Google's redirects and token exchange. #### Step 6: Test and Deploy + - **Testing**: Thoroughly test the OAuth flow. - **Verification and Deployment**: Submit for verification if needed and deploy. @@ -40,12 +46,10 @@ This guide provides a condensed overview of setting up Google OAuth. Adapt it ba 5. **Scopes**: Decide on the scopes you need, like `user:email` for email access. 6. **Callback URL**: Set your callback URL that GitHub will redirect to after authentication. - #### Don't Forget: 1. **Submit for Verification**: If your application will be used by users outside your organization, you must submit your OAuth consent screen for verification by Google. - ## Meta (Facebook) OAuth 1. **Facebook App**: Create a new app in the Facebook Developer portal. @@ -78,4 +82,3 @@ This guide provides a condensed overview of setting up Google OAuth. Adapt it ba 2. **Redirect to Authorization URL**: On your login page, add buttons for each service that redirects to their respective authorization URL with the necessary query parameters. 3. **Handle Callbacks**: Implement routes in your server to handle the callbacks, exchanging the authorization code for tokens. 4. **User Authentication**: Use the tokens to fetch user details and authenticate or register them in your system. - diff --git a/auth/index.ts b/auth/index.ts index 6d7d8cb..3d3edff 100644 --- a/auth/index.ts +++ b/auth/index.ts @@ -1,10 +1,5 @@ -export { - createSecurityToken, - createToken, - getTokenExpireEpoch, - verifyToken, -} from "./security-token"; +export { createSecurityToken, createToken, getTokenExpireEpoch, verifyToken } from './security-token' -export { oAuthFactory } from "./oauth"; +export { oAuthFactory } from './oauth' -export { initGoogleOAuth, oAuthProviders } from "./oauth-providers"; +export { initGoogleOAuth, oAuthProviders } from './oauth-providers' diff --git a/auth/oauth-providers.ts b/auth/oauth-providers.ts index b9d71c1..e480b64 100644 --- a/auth/oauth-providers.ts +++ b/auth/oauth-providers.ts @@ -1,41 +1,41 @@ -import { OAuthConfig, OAuthProviderFn } from "./oauth-types"; +import { OAuthConfig, OAuthProviderFn } from './oauth-types' -export type ProvidersConfigRecord = Record>; +export type ProvidersConfigRecord = Record> export const oAuthProviders = { google: { - redirectUri: "http://localhost:3000/callback", // just a default placeholder - authReqUrl: "https://accounts.google.com/o/oauth2/v2/auth", - tokenUrl: "https://oauth2.googleapis.com/token", + redirectUri: 'http://localhost:3000/callback', // just a default placeholder + authReqUrl: 'https://accounts.google.com/o/oauth2/v2/auth', + tokenUrl: 'https://oauth2.googleapis.com/token', }, microsoft: { - redirectUri: "http://localhost:3000/callback", - authReqUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", - tokenUrl: "http://needtofind", + redirectUri: 'http://localhost:3000/callback', + authReqUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + tokenUrl: 'http://needtofind', }, github: { - redirectUri: "http://localhost:3000/callback", - authReqUrl: "https://github.com/login/oauth/authorize", - tokenUrl: "http://https://github.com/login/oauth/access_token", + redirectUri: 'http://localhost:3000/callback', + authReqUrl: 'https://github.com/login/oauth/authorize', + tokenUrl: 'http://https://github.com/login/oauth/access_token', }, -} satisfies ProvidersConfigRecord; +} satisfies ProvidersConfigRecord export const initGoogleOAuth: OAuthProviderFn = ({ clientId, clientSecret }, options) => { - const redirectUrl = options?.redirectUrl; + const redirectUrl = options?.redirectUrl return { ...oAuthProviders.google, redirectUri: redirectUrl ? redirectUrl : oAuthProviders.google.redirectUri, clientId, clientSecret, - }; -}; + } +} export const initGithubOAuth: OAuthProviderFn = ({ clientId, clientSecret }, options) => { - const redirectUrl = options?.redirectUrl; + const redirectUrl = options?.redirectUrl return { ...oAuthProviders.github, redirectUri: redirectUrl ? redirectUrl : oAuthProviders.github.redirectUri, clientId, clientSecret, - }; -}; + } +} diff --git a/auth/oauth-types.ts b/auth/oauth-types.ts index 2888918..d21fc16 100644 --- a/auth/oauth-types.ts +++ b/auth/oauth-types.ts @@ -1,35 +1,35 @@ export type OAuthHelpers = { - getAuthorizationUrl(config: OAuthConfig): string; - getToken(code: string, config: OAuthConfig): Promise; // Simplified for demonstration -}; + getAuthorizationUrl(config: OAuthConfig): string + getToken(code: string, config: OAuthConfig): Promise // Simplified for demonstration +} export type OAuthConfig = { - clientId: string; - clientSecret: string; + clientId: string + clientSecret: string // the server route that handles the redirect from the OAuth provider - redirectUri: string; + redirectUri: string // the url that handles the token request from the OAuth provider - tokenUrl: string; + tokenUrl: string // the server route that handles the token request from the OAuth provider - authReqUrl: string; - headers?: Record; -}; + authReqUrl: string + headers?: Record +} export type OAuthToken = { - accessToken: string; - tokenType: string; - expiresIn: number; // Time in seconds after which the token expires - refreshToken?: string; // Optional, not all flows return a refresh token - scope?: string; // Optional, scope of the access granted - idToken?: string; // Optional, used in OpenID Connect (OIDC) + accessToken: string + tokenType: string + expiresIn: number // Time in seconds after which the token expires + refreshToken?: string // Optional, not all flows return a refresh token + scope?: string // Optional, scope of the access granted + idToken?: string // Optional, used in OpenID Connect (OIDC) // Additional fields can be added here depending on the OAuth provider -}; +} export type OAuthProviderOptions = { - redirectUrl: string; -}; + redirectUrl: string +} -export type OAuthProviderCreds = Pick; -export type OAuthProviderFn = (config: OAuthProviderCreds, options?: OAuthProviderOptions) => OAuthConfig; +export type OAuthProviderCreds = Pick +export type OAuthProviderFn = (config: OAuthProviderCreds, options?: OAuthProviderOptions) => OAuthConfig -export type OAuthProviderInitializer = (config: OAuthConfig) => OAuthHelpers; +export type OAuthProviderInitializer = (config: OAuthConfig) => OAuthHelpers diff --git a/auth/oauth.ts b/auth/oauth.ts index 3fb313a..cda7818 100644 --- a/auth/oauth.ts +++ b/auth/oauth.ts @@ -1,27 +1,27 @@ -import { OAuthConfig, OAuthProviderInitializer, OAuthToken } from "./oauth-types"; +import { OAuthConfig, OAuthProviderInitializer, OAuthToken } from './oauth-types' type FetcherResponse = T & { - error?: string; -}; + error?: string +} // Generic OAuth fetcher export async function oAuthFetcher( url: string, options: { - params: Record; - headers?: Record; + params: Record + headers?: Record }, ): Promise> { const response = await fetch(url, { - method: "post", + method: 'post', headers: { ...options.headers, - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams(options.params).toString(), - }); + }) - return response.json(); + return response.json() } // Wrapper function for getting token @@ -29,66 +29,66 @@ export async function getOAuthToken({ code, config: { clientId, clientSecret, redirectUri, tokenUrl }, }: { - code: string; - config: Omit; + code: string + config: Omit }): Promise> { const params = { code, client_id: clientId, client_secret: clientSecret, redirect_uri: redirectUri, - grant_type: "authorization_code", - }; + grant_type: 'authorization_code', + } - return oAuthFetcher(tokenUrl, params); + return oAuthFetcher(tokenUrl, params) } export const initProvider: OAuthProviderInitializer = ({ clientId, authReqUrl, redirectUri, headers }) => { return { // TODO add options to be able to change response_type/scope, etc getAuthorizationUrl: () => { - const authUrl = authReqUrl; + const authUrl = authReqUrl const queryParams = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, - response_type: "code", - scope: "email profile", - }); + response_type: 'code', + scope: 'email profile', + }) - return `${authUrl}?${queryParams.toString()}`; + return `${authUrl}?${queryParams.toString()}` }, getToken: async (code: string) => { return getOAuthToken({ code, config: { clientId, - clientSecret: "", + clientSecret: '', redirectUri, - tokenUrl: "https://oauth2.googleapis.com/token", + tokenUrl: 'https://oauth2.googleapis.com/token', headers, }, }).then((response) => { if (response.error) { - console.error("Error fetching token:", response.error); - throw new Error(response.error); + console.error('Error fetching token:', response.error) + throw new Error(response.error) } else { - console.log("Access Token:", response.accessToken); - return response; + console.log('Access Token:', response.accessToken) + return response } - }); + }) }, - }; -}; + } +} export const oAuthFactory = (config: OAuthConfig) => { - const provider = initProvider(config); + const provider = initProvider(config) return { handleRedirect: async (code: string) => { - return await provider.getToken(code, config); + return await provider.getToken(code, config) }, initiateOAuthFlow: () => { - return provider.getAuthorizationUrl(config); + return provider.getAuthorizationUrl(config) }, - }; -}; + } +} diff --git a/auth/security-token.test.ts b/auth/security-token.test.ts index 89e0d50..ec61b1e 100644 --- a/auth/security-token.test.ts +++ b/auth/security-token.test.ts @@ -1,52 +1,52 @@ -import { describe, it } from "bun:test"; -import { expect } from "bun:test"; -import { getTokenExpireEpoch, createToken, verifyToken, createSecurityToken } from "."; +import { describe, it } from 'bun:test' +import { expect } from 'bun:test' +import { getTokenExpireEpoch, createToken, verifyToken, createSecurityToken } from '.' -describe("Token Utilities", () => { - describe("getTokenExpireEpoch", () => { - it("should return the correct expiration epoch", () => { - const date = new Date("2023-10-26T12:00:00Z"); - const tokenValidTimeSec = 3600; // 1 hour in seconds - const expected = date.getTime() + tokenValidTimeSec * 1000; // convert seconds to milliseconds - const result = getTokenExpireEpoch(date, tokenValidTimeSec); +describe('Token Utilities', () => { + describe('getTokenExpireEpoch', () => { + it('should return the correct expiration epoch', () => { + const date = new Date('2023-10-26T12:00:00Z') + const tokenValidTimeSec = 3600 // 1 hour in seconds + const expected = date.getTime() + tokenValidTimeSec * 1000 // convert seconds to milliseconds + const result = getTokenExpireEpoch(date, tokenValidTimeSec) - expect(result).toEqual(expected); - }); - }); - describe("verifyToken", () => { - it("should verify the token correctly", async () => { - const salt = "randomSalt"; - const originalString = "testToken"; - const hashedToken = await createToken(originalString, salt); - const isVerified = await verifyToken(originalString, salt, hashedToken); - expect(isVerified).toBe(true); - }); - }); + expect(result).toEqual(expected) + }) + }) + describe('verifyToken', () => { + it('should verify the token correctly', async () => { + const salt = 'randomSalt' + const originalString = 'testToken' + const hashedToken = await createToken(originalString, salt) + const isVerified = await verifyToken(originalString, salt, hashedToken) + expect(isVerified).toBe(true) + }) + }) - describe("createToken", () => { - it("should create a hashed token", async () => { - const salt = "randomSalt"; - const originalString = "testToken"; - const hashedToken = await createToken(originalString, salt); - expect(hashedToken).toBeTruthy(); - expect(hashedToken).not.toEqual(originalString); // Ensure the hashed token is not the same as the original string - }); - }); + describe('createToken', () => { + it('should create a hashed token', async () => { + const salt = 'randomSalt' + const originalString = 'testToken' + const hashedToken = await createToken(originalString, salt) + expect(hashedToken).toBeTruthy() + expect(hashedToken).not.toEqual(originalString) // Ensure the hashed token is not the same as the original string + }) + }) - describe("createSecurityToken", () => { - it("should create a security token with default expiration time", async () => { - const result = await createSecurityToken(5000); - expect(result.securityToken).toBeTruthy(); - expect(result.tokenId).toBeTruthy(); - expect(result.tokenExpireEpoch).toBeGreaterThan(Date.now()); - }); + describe('createSecurityToken', () => { + it('should create a security token with default expiration time', async () => { + const result = await createSecurityToken(5000) + expect(result.securityToken).toBeTruthy() + expect(result.tokenId).toBeTruthy() + expect(result.tokenExpireEpoch).toBeGreaterThan(Date.now()) + }) - it("should create a security token with specified expiration time", async () => { - const currentTime = new Date(); - const tokenValidTime = 60 * 15; // 15 minutes - const result = await createSecurityToken(tokenValidTime, currentTime); - const expectedExpiration = currentTime.getTime() + tokenValidTime * 1000; - expect(result.tokenExpireEpoch).toBeCloseTo(expectedExpiration, -2); // -2 is for a precision of 10 milliseconds - }); - }); -}); + it('should create a security token with specified expiration time', async () => { + const currentTime = new Date() + const tokenValidTime = 60 * 15 // 15 minutes + const result = await createSecurityToken(tokenValidTime, currentTime) + const expectedExpiration = currentTime.getTime() + tokenValidTime * 1000 + expect(result.tokenExpireEpoch).toBeCloseTo(expectedExpiration, -2) // -2 is for a precision of 10 milliseconds + }) + }) +}) diff --git a/auth/security-token.ts b/auth/security-token.ts index 5ec972d..b7d497b 100644 --- a/auth/security-token.ts +++ b/auth/security-token.ts @@ -1,40 +1,40 @@ -import { v7 as uuid } from "../uuid"; +import { v7 as uuid } from '../uuid' export const getTokenExpireEpoch = (date: Date, tokenValidTimeSec: number) => { - const expireEpoch = date.getTime() + tokenValidTimeSec * 1000; + const expireEpoch = date.getTime() + tokenValidTimeSec * 1000 - return expireEpoch; -}; + return expireEpoch +} export async function verifyToken(tokenString: string, salt: string, storedHash: string) { - const fullPassword = tokenString + salt; - const isMatch = await Bun.password.verify(fullPassword, storedHash); + const fullPassword = tokenString + salt + const isMatch = await Bun.password.verify(fullPassword, storedHash) - return isMatch; + return isMatch } export async function createToken(string: string, salt: string) { - const fullPassword = string + salt; + const fullPassword = string + salt return await Bun.password.hash(fullPassword, { - algorithm: "argon2id", + algorithm: 'argon2id', memoryCost: 65536, timeCost: 3, - }); + }) } export const createSecurityToken = async (tokenValidTime: number, currentDate?: Date) => { - const salt = uuid(); + const salt = uuid() const [tokenId, timestamp] = uuid({ returnTimestamp: true, dateTime: currentDate, - }); + }) - const securityToken = await createToken(tokenId, salt); - const tokenExpireEpoch = getTokenExpireEpoch(timestamp, tokenValidTime); + const securityToken = await createToken(tokenId, salt) + const tokenExpireEpoch = getTokenExpireEpoch(timestamp, tokenValidTime) return { securityToken, tokenId, tokenExpireEpoch, - }; -}; + } +} diff --git a/biome.json b/biome.json deleted file mode 100644 index 56b1523..0000000 --- a/biome.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.4.0/schema.json", - "organizeImports": { - "enabled": false - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "formatter": { - "enabled": true, - "lineWidth": 120, - "indentStyle": "space" - } -} diff --git a/cli/cli-utils.test.ts b/cli/cli-utils.test.ts index 72dc1f6..8e9e367 100644 --- a/cli/cli-utils.test.ts +++ b/cli/cli-utils.test.ts @@ -1,56 +1,56 @@ -import { describe, expect, test } from "bun:test"; -import { OptionDefinition, getOptionValue, parseArgument } from "./cli-utils"; +import { describe, expect, test } from 'bun:test' +import { OptionDefinition, getOptionValue, parseArgument } from './cli-utils' -describe("getOptionValue", () => { - test("should return correct value for boolean type", () => { - const arg = "--testArg"; - const nextArg = "true"; +describe('getOptionValue', () => { + test('should return correct value for boolean type', () => { + const arg = '--testArg' + const nextArg = 'true' const optionDef: OptionDefinition = { default: false, - types: ["boolean"], - }; + types: ['boolean'], + } - const value = getOptionValue(arg, nextArg, optionDef); - expect(value).toBe(true); - }); + const value = getOptionValue(arg, nextArg, optionDef) + expect(value).toBe(true) + }) test("should return default value if nextArg starts with '--'", () => { - const arg = "--testArg"; - const nextArg = "--anotherArg"; + const arg = '--testArg' + const nextArg = '--anotherArg' const optionDef: OptionDefinition = { - default: "default", - types: ["string"], - }; + default: 'default', + types: ['string'], + } - const value = getOptionValue(arg, nextArg, optionDef); - expect(value).toBe("default"); - }); -}); + const value = getOptionValue(arg, nextArg, optionDef) + expect(value).toBe('default') + }) +}) -describe("parseArgument", () => { - test("should return correct key and value", () => { - const arg = "--testArg"; - const nextArg = "true"; +describe('parseArgument', () => { + test('should return correct key and value', () => { + const arg = '--testArg' + const nextArg = 'true' - const { key, value } = parseArgument(arg, nextArg); - expect(key).toBe("testArg"); - expect(value).toBe(true); - }); + const { key, value } = parseArgument(arg, nextArg) + expect(key).toBe('testArg') + expect(value).toBe(true) + }) test("should throw error if arg does not start with '--'", async () => { - const arg = "testArg"; - const nextArg = "true"; + const arg = 'testArg' + const nextArg = 'true' - let error: Error | null = null; + let error: Error | null = null try { - parseArgument(arg, nextArg); + parseArgument(arg, nextArg) } catch (e) { if (e instanceof Error) { - error = e; + error = e } } - expect(error).toBeDefined(); - expect(error!.message).toBe(`Invalid parameter: ${arg}`); - }); -}); + expect(error).toBeDefined() + expect(error!.message).toBe(`Invalid parameter: ${arg}`) + }) +}) diff --git a/cli/cli-utils.ts b/cli/cli-utils.ts index 98a6c8e..fdd6ea5 100644 --- a/cli/cli-utils.ts +++ b/cli/cli-utils.ts @@ -1,32 +1,32 @@ -import readline from "readline"; +import readline from 'readline' const cliLog = (...args: any[]) => { - console.info(...args); -}; + console.info(...args) +} // Get user input asynchronously export async function getUserInput(): Promise { - const proc = Bun.spawn([]); - return await new Response(proc.stdout).text(); + const proc = Bun.spawn([]) + return await new Response(proc.stdout).text() } // Interface for parsed command line arguments export interface ParsedArgs { - [key: string]: string | boolean | undefined; + [key: string]: string | boolean | undefined } export interface OptionDefinition { - default?: string | boolean; - types: (string | boolean)[]; + default?: string | boolean + types: (string | boolean)[] } const optionDefinitions: { [key: string]: OptionDefinition } = { // Define available options here -}; +} // Parse command line arguments export function getArguments(): string[] { - return process.argv.slice(2); + return process.argv.slice(2) } export function getOptionValue( @@ -34,66 +34,66 @@ export function getOptionValue( nextArg: string, optionDef: OptionDefinition, ): string | boolean | undefined { - let value = optionDef.default; - - if (nextArg && !nextArg.startsWith("--")) { - const type = optionDef.types.find((type) => type === typeof nextArg || type === typeof value); - cliLog("Type found: ", type); // Debug log - if (type === "boolean") { - if (nextArg.toLowerCase() === "true") { - value = true; - } else if (nextArg.toLowerCase() === "false") { - value = false; + let value = optionDef.default + + if (nextArg && !nextArg.startsWith('--')) { + const type = optionDef.types.find((type) => type === typeof nextArg || type === typeof value) + cliLog('Type found: ', type) // Debug log + if (type === 'boolean') { + if (nextArg.toLowerCase() === 'true') { + value = true + } else if (nextArg.toLowerCase() === 'false') { + value = false } } else { - value = nextArg; + value = nextArg } - } else if (typeof value === "boolean") { - value = true; + } else if (typeof value === 'boolean') { + value = true } - cliLog("Returned value: ", value); // Debug log - return value; + cliLog('Returned value: ', value) // Debug log + return value } export function parseArgument( arg: string, nextArg: string, ): { key: string | undefined; value: string | boolean | undefined } { - let key: string | undefined = undefined; - let value: string | boolean | undefined; + let key: string | undefined = undefined + let value: string | boolean | undefined - if (arg.startsWith("--")) { - key = arg.slice(2); + if (arg.startsWith('--')) { + key = arg.slice(2) if (optionDefinitions.hasOwnProperty(key)) { - const optionDef = optionDefinitions[key]; - value = getOptionValue(arg, nextArg, optionDef); + const optionDef = optionDefinitions[key] + value = getOptionValue(arg, nextArg, optionDef) } else { - value = true; + value = true } } else { - throw new Error(`Invalid parameter: ${arg}`); + throw new Error(`Invalid parameter: ${arg}`) } - return { key, value }; + return { key, value } } export async function parseCliArgs(): Promise { try { - const args = getArguments(); - const parsedArgs: ParsedArgs = {}; + const args = getArguments() + const parsedArgs: ParsedArgs = {} for (let i = 0; i < args.length; i++) { - const { key, value } = parseArgument(args[i], args[i + 1]); + const { key, value } = parseArgument(args[i], args[i + 1]) if (key) { - parsedArgs[key] = value; + parsedArgs[key] = value } } - return parsedArgs; + return parsedArgs } catch (error) { - throw error; + throw error } } @@ -102,40 +102,40 @@ export const getAdditionalPrompt = () => const rl = readline.createInterface({ input: process.stdin, output: process.stdout, - }); - rl.question("Do you want to add anything else...", (additionalPrompt) => { - rl.close(); - resolve(additionalPrompt); - }); - }); + }) + rl.question('Do you want to add anything else...', (additionalPrompt) => { + rl.close() + resolve(additionalPrompt) + }) + }) export const chooseActions = async (actionsConfig: Record): Promise> => { - cliLog("\nChoose actions (separated by commas):"); - const actions = Object.keys(actionsConfig); + cliLog('\nChoose actions (separated by commas):') + const actions = Object.keys(actionsConfig) actions.forEach((action, index) => { - cliLog(`${index + 1}. ${action}`); - }); + cliLog(`${index + 1}. ${action}`) + }) const rl = readline.createInterface({ input: process.stdin, output: process.stdout, - }); + }) const actionIndexes = await new Promise((resolve) => { - rl.question("Enter the numbers corresponding to the actions: ", (actionIndexes) => { - rl.close(); - resolve(actionIndexes); - }); - }); + rl.question('Enter the numbers corresponding to the actions: ', (actionIndexes) => { + rl.close() + resolve(actionIndexes) + }) + }) - const selectedIndexes = actionIndexes.split(",").map((index) => parseInt(index.trim()) - 1); + const selectedIndexes = actionIndexes.split(',').map((index) => parseInt(index.trim()) - 1) - const validSelection = selectedIndexes.every((index) => index >= 0 && index < actions.length); + const validSelection = selectedIndexes.every((index) => index >= 0 && index < actions.length) if (validSelection) { - return selectedIndexes.map((index) => actions[index] as keyof typeof actionsConfig); + return selectedIndexes.map((index) => actions[index] as keyof typeof actionsConfig) } else { - cliLog("Invalid input, please try again."); - return chooseActions(actionsConfig); + cliLog('Invalid input, please try again.') + return chooseActions(actionsConfig) } -}; +} diff --git a/cli/create-cli-factory.test.ts b/cli/create-cli-factory.test.ts index f458729..aa9ce05 100644 --- a/cli/create-cli-factory.test.ts +++ b/cli/create-cli-factory.test.ts @@ -1 +1 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from 'bun:test' diff --git a/cli/create-cli-factory.ts b/cli/create-cli-factory.ts index 050014f..ec2b42d 100644 --- a/cli/create-cli-factory.ts +++ b/cli/create-cli-factory.ts @@ -1,76 +1,70 @@ -import { fileFactory } from "../files-folders"; -import { defaultLogger } from "../logger"; -import { BaseError } from "../utils/base-error"; -import { chooseActions, getAdditionalPrompt, getUserInput, parseCliArgs } from "./cli-utils"; +import { fileFactory } from '../files-folders' +import { defaultLogger } from '../logger' +import { BaseError } from '../utils/base-error' +import { chooseActions, getAdditionalPrompt, getUserInput, parseCliArgs } from './cli-utils' export type CLIOptions = { - inputPrompt?: string; - actionsConfig?: Record; - logger?: typeof defaultLogger; + inputPrompt?: string + actionsConfig?: Record + logger?: typeof defaultLogger // fileConfig?: { // filePath: string; // fileContent: string; // }; -}; +} export function createCliFactory>({ - inputPrompt = "Please input your command", + inputPrompt = 'Please input your command', actionsConfig = {}, logger, }: CLIOptions) { // const actionsConfig = options.actionsConfig ?? {}; const factory = fileFactory({ - baseDirectory: ".", // Replace with actual path - }); + baseDirectory: '.', // Replace with actual path + }) const processInput = async () => { try { - const commandLineArgs = await parseCliArgs(); + const commandLineArgs = await parseCliArgs() - const userInput = await getUserInput(); + const userInput = await getUserInput() // Handle user input and command line arguments... - return { commandLineArgs, userInput }; + return { commandLineArgs, userInput } } catch (error) { - throw error; + throw error } - }; + } const executeActions = async () => { try { - const additionalPrompt = await getAdditionalPrompt(); + const additionalPrompt = await getAdditionalPrompt() - const chosenActions = await chooseActions(actionsConfig); + const chosenActions = await chooseActions(actionsConfig) // Execute chosen actions... - return { additionalPrompt, chosenActions }; + return { additionalPrompt, chosenActions } } catch (error) { - throw error; + throw error } - }; + } - const handleFiles = ({ - filePath, - fileContent, - }: { - filePath: string; - fileContent: string; - }) => { + const handleFiles = ({ filePath, fileContent }: { filePath: string; fileContent: string }) => { try { - factory.directoryExists({ path: filePath }); + factory.directoryExists({ path: filePath }) - factory.createFile(filePath, fileContent); + factory.createFile(filePath, fileContent) } catch (error) { - throw error; + throw error } - }; + } return { inputPrompt, processInput, executeActions, handleFiles, - }; + } } diff --git a/cli/index.ts b/cli/index.ts index 92c9bc5..2758838 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1 +1 @@ -export { createCliFactory } from "./create-cli-factory"; +export { createCliFactory } from './create-cli-factory' diff --git a/client.tsx b/client.tsx index f84240e..68664a9 100644 --- a/client.tsx +++ b/client.tsx @@ -1,10 +1,10 @@ -export { clientCookieFactory } from "./cookies/client-cookie-factory"; +export { clientCookieFactory } from './cookies/client-cookie-factory' -export * as cookieUtils from "./cookies/cookie-utils"; +export * as cookieUtils from './cookies/cookie-utils' -export type { CookieOptions } from "./cookies/cookie-types.ts"; +export type { CookieOptions } from './cookies/cookie-types.ts' -export { createFetchFactory } from "./fetcher/create-fetch-factory.ts"; +export { createFetchFactory } from './fetcher/create-fetch-factory.ts' export type { APIConfig, EventHandlerMap, @@ -12,6 +12,6 @@ export type { FileDownloadConfig, MappedApiConfig, TypeMap, -} from "./fetcher/fetch-types.ts"; +} from './fetcher/fetch-types.ts' -export {} from "./auth/security-token.ts"; +export {} from './auth/security-token.ts' diff --git a/cookies/client-cookie-factory.test.ts b/cookies/client-cookie-factory.test.ts index 938414e..f0d7651 100644 --- a/cookies/client-cookie-factory.test.ts +++ b/cookies/client-cookie-factory.test.ts @@ -1,68 +1,68 @@ -import { beforeEach, describe, expect, jest, test } from "bun:test"; -import { clientCookieFactory } from "./client-cookie-factory"; +import { beforeEach, describe, expect, jest, test } from 'bun:test' +import { clientCookieFactory } from './client-cookie-factory' declare var document: { - cookie: any; -}; + cookie: any +} const mockDocument = { - _cookie: "", + _cookie: '', get cookie() { - return this._cookie; + return this._cookie }, set cookie(value) { - this._cookie = value; + this._cookie = value }, -}; +} -Object.defineProperty(globalThis, "document", { +Object.defineProperty(globalThis, 'document', { value: mockDocument, writable: true, configurable: true, -}); +}) -describe("createClientCookieFactory", () => { - const cookieFactory = clientCookieFactory("test"); +describe('createClientCookieFactory', () => { + const cookieFactory = clientCookieFactory('test') // Mock document.cookie - let mockCookie = ""; + let mockCookie = '' beforeEach(() => { // Reset the mock document.cookie before each test - mockCookie = ""; - }); + mockCookie = '' + }) - Object.defineProperty(document, "cookie", { + Object.defineProperty(document, 'cookie', { get: jest.fn(() => mockCookie), set: jest.fn((newCookie) => { - mockCookie = newCookie; + mockCookie = newCookie }), - }); + }) - test("setCookie sets a cookie", () => { - cookieFactory.setCookie("value"); - expect(document.cookie).toBe("test=value"); - }); + test('setCookie sets a cookie', () => { + cookieFactory.setCookie('value') + expect(document.cookie).toBe('test=value') + }) - test("getCookie returns the value of a cookie", () => { - document.cookie = "test=value"; - const value = cookieFactory.getRawCookie(); - expect(value).toBe("value"); - }); + test('getCookie returns the value of a cookie', () => { + document.cookie = 'test=value' + const value = cookieFactory.getRawCookie() + expect(value).toBe('value') + }) - test("deleteCookie sets a cookie with Max-Age=-1", () => { - cookieFactory.deleteCookie(); - expect(document.cookie).toBe("test=; max-age=-1"); - }); + test('deleteCookie sets a cookie with Max-Age=-1', () => { + cookieFactory.deleteCookie() + expect(document.cookie).toBe('test=; max-age=-1') + }) - test("checkCookie returns true if a cookie exists", () => { - document.cookie = "test=value"; - const exists = cookieFactory.checkCookie(); - expect(exists).toBe(true); - }); + test('checkCookie returns true if a cookie exists', () => { + document.cookie = 'test=value' + const exists = cookieFactory.checkCookie() + expect(exists).toBe(true) + }) - test("checkCookie returns false if a cookie does not exist", () => { - const exists = cookieFactory.checkCookie(); - expect(exists).toBe(false); - }); -}); + test('checkCookie returns false if a cookie does not exist', () => { + const exists = cookieFactory.checkCookie() + expect(exists).toBe(false) + }) +}) diff --git a/cookies/client-cookie-factory.ts b/cookies/client-cookie-factory.ts index 4317aa3..29b6b2b 100644 --- a/cookies/client-cookie-factory.ts +++ b/cookies/client-cookie-factory.ts @@ -1,31 +1,31 @@ -import { CookieOptions } from "./cookie-types"; -import { parseCookieData, retrieveRawCookieValue, setCookie } from "./cookie-utils"; +import { CookieOptions } from './cookie-types' +import { parseCookieData, retrieveRawCookieValue, setCookie } from './cookie-utils' declare var document: { - cookie: any; -}; + cookie: any +} export function clientCookieFactory(cookieKey: string, options?: CookieOptions) { const handleSetCookie = (value: T, cookieSetOptions: CookieOptions = {}) => { - setCookie(cookieKey, value, cookieSetOptions || options || {}); - }; + setCookie(cookieKey, value, cookieSetOptions || options || {}) + } const getRawCookie = () => { - return retrieveRawCookieValue(cookieKey); - }; + return retrieveRawCookieValue(cookieKey) + } const deleteCookie = () => { - handleSetCookie("" as T, { maxAge: -1 }); - }; + handleSetCookie('' as T, { maxAge: -1 }) + } const checkCookie = () => { - return getRawCookie() !== null; - }; + return getRawCookie() !== null + } const getParsedCookie = (): T | null => { - const rawCookie = getRawCookie(); - return parseCookieData(rawCookie); - }; + const rawCookie = getRawCookie() + return parseCookieData(rawCookie) + } return { setCookie: handleSetCookie, @@ -33,5 +33,5 @@ export function clientCookieFactory(cookieKey: string, options?: Coo checkCookie, getParsedCookie, getRawCookie, - }; + } } diff --git a/cookies/cookie-types.ts b/cookies/cookie-types.ts index 3e345ee..15dc2eb 100644 --- a/cookies/cookie-types.ts +++ b/cookies/cookie-types.ts @@ -1,8 +1,8 @@ export type CookieOptions = { - maxAge?: number; - path?: string; - domain?: string; - secure?: boolean; - httpOnly?: boolean; - sameSite?: "Strict" | "Lax" | "None"; -}; + maxAge?: number + path?: string + domain?: string + secure?: boolean + httpOnly?: boolean + sameSite?: 'Strict' | 'Lax' | 'None' +} diff --git a/cookies/cookie-utils.test.ts b/cookies/cookie-utils.test.ts index 1b04052..4a1c8eb 100644 --- a/cookies/cookie-utils.test.ts +++ b/cookies/cookie-utils.test.ts @@ -1,66 +1,66 @@ -import { afterEach, describe, expect, it } from "bun:test"; -import { parseCookieData, retrieveRawCookieValue, stringifyCookieData } from "./cookie-utils"; +import { afterEach, describe, expect, it } from 'bun:test' +import { parseCookieData, retrieveRawCookieValue, stringifyCookieData } from './cookie-utils' declare var document: { - cookie: any; -}; + cookie: any +} -describe("Cookie Helpers", () => { - describe("parseCookieData", () => { - it("should parse JSON string to object", () => { - const jsonString = '{"key": "value"}'; - expect(parseCookieData(jsonString)).toEqual({ key: "value" }); - }); +describe('Cookie Helpers', () => { + describe('parseCookieData', () => { + it('should parse JSON string to object', () => { + const jsonString = '{"key": "value"}' + expect(parseCookieData(jsonString)).toEqual({ key: 'value' }) + }) - it("should return string if parsing fails", () => { - const invalidJson = "{key: 'value'}"; - expect(parseCookieData(invalidJson)).toBe(invalidJson); - }); + it('should return string if parsing fails', () => { + const invalidJson = "{key: 'value'}" + expect(parseCookieData(invalidJson)).toBe(invalidJson) + }) - it("should return null if input is null", () => { - expect(parseCookieData(null)).toBeNull(); - }); - }); + it('should return null if input is null', () => { + expect(parseCookieData(null)).toBeNull() + }) + }) - describe("stringifyCookieData", () => { - it("should stringify object to JSON string", () => { - const obj = { key: "value" }; - expect(stringifyCookieData(obj)).toBe('{"key":"value"}'); - }); + describe('stringifyCookieData', () => { + it('should stringify object to JSON string', () => { + const obj = { key: 'value' } + expect(stringifyCookieData(obj)).toBe('{"key":"value"}') + }) - it("should return string as is", () => { - const str = "testString"; - expect(stringifyCookieData(str)).toBe(str); - }); - }); -}); + it('should return string as is', () => { + const str = 'testString' + expect(stringifyCookieData(str)).toBe(str) + }) + }) +}) -describe("retrieveRawCookieValue", () => { +describe('retrieveRawCookieValue', () => { // Save original document.cookie - const originalDocumentCookie = document.cookie; + const originalDocumentCookie = document.cookie afterEach(() => { // Restore original document.cookie after each test - document.cookie = originalDocumentCookie; - }); + document.cookie = originalDocumentCookie + }) - it("should return the correct cookie value", () => { - document.cookie = "testCookie=testValue; anotherCookie=anotherValue"; - expect(retrieveRawCookieValue("testCookie")).toBe("testValue"); - }); + it('should return the correct cookie value', () => { + document.cookie = 'testCookie=testValue; anotherCookie=anotherValue' + expect(retrieveRawCookieValue('testCookie')).toBe('testValue') + }) - it("should return null if cookie is not found", () => { - document.cookie = "testCookie=testValue; anotherCookie=anotherValue"; - expect(retrieveRawCookieValue("nonExistentCookie")).toBeNull(); - }); + it('should return null if cookie is not found', () => { + document.cookie = 'testCookie=testValue; anotherCookie=anotherValue' + expect(retrieveRawCookieValue('nonExistentCookie')).toBeNull() + }) - it("should decode URI encoded cookie names and values", () => { - document.cookie = "encodedName%3D=encodedValue%3D; anotherCookie=anotherValue"; - expect(retrieveRawCookieValue("encodedName=")).toBe("encodedValue="); - }); + it('should decode URI encoded cookie names and values', () => { + document.cookie = 'encodedName%3D=encodedValue%3D; anotherCookie=anotherValue' + expect(retrieveRawCookieValue('encodedName=')).toBe('encodedValue=') + }) - it("should handle cookies with no value", () => { - document.cookie = "emptyCookie=; anotherCookie=anotherValue"; - expect(retrieveRawCookieValue("emptyCookie")).toBe(""); - }); -}); + it('should handle cookies with no value', () => { + document.cookie = 'emptyCookie=; anotherCookie=anotherValue' + expect(retrieveRawCookieValue('emptyCookie')).toBe('') + }) +}) diff --git a/cookies/cookie-utils.ts b/cookies/cookie-utils.ts index 9fb64bc..6636782 100644 --- a/cookies/cookie-utils.ts +++ b/cookies/cookie-utils.ts @@ -1,92 +1,92 @@ -import { CookieOptions } from "./cookie-types"; +import { CookieOptions } from './cookie-types' declare var document: { - cookie: any; -}; + cookie: any +} export const parseCookieData = (data: string | null): T | null => { - if (data === null) return null; - if (typeof data === "undefined") return null; + if (data === null) return null + if (typeof data === 'undefined') return null try { - return JSON.parse(data) as T; + return JSON.parse(data) as T } catch (e) { // If parsing fails, assume the data is a string - return data as unknown as T; + return data as unknown as T } -}; +} export const stringifyCookieData = (data: T): string => { - if (typeof data === "string") { - return data; + if (typeof data === 'string') { + return data } else { - return JSON.stringify(data); + return JSON.stringify(data) } -}; +} export const retrieveRawCookieValue = (name: string): string | null => { - const cookieArr = document.cookie.split("; "); + const cookieArr = document.cookie.split('; ') for (let i = 0; i < cookieArr.length; i++) { - const cookiePair = cookieArr[i].split("="); + const cookiePair = cookieArr[i].split('=') if (name === decodeURIComponent(cookiePair[0])) { - return decodeURIComponent(cookiePair[1]); + return decodeURIComponent(cookiePair[1]) } } - return null; -}; + return null +} export const encodeCookie = (cookieKey: string, value: T, options: CookieOptions): string => { let cookieString = `${encodeURIComponent(cookieKey)}=${encodeURIComponent( - typeof value === "string" ? value : JSON.stringify(value), - )}`; + typeof value === 'string' ? value : JSON.stringify(value), + )}` if (options.maxAge) { - cookieString += `; max-age=${options.maxAge}`; + cookieString += `; max-age=${options.maxAge}` } if (options.path) { - cookieString += `; path=${options.path}`; + cookieString += `; path=${options.path}` } if (options.domain) { - cookieString += `; domain=${options.domain}`; + cookieString += `; domain=${options.domain}` } if (options.secure) { - cookieString += `; secure`; + cookieString += `; secure` } if (options.httpOnly) { - cookieString += `; httpOnly`; + cookieString += `; httpOnly` } - return cookieString; -}; + return cookieString +} export const setCookie = (cookieKey: string, value: T, options: CookieOptions) => { - document.cookie = encodeCookie(cookieKey, value, options); -}; + document.cookie = encodeCookie(cookieKey, value, options) +} export function parseCookies(cookiesString: string) { - const cookies: { [name: string]: string } = {}; - const pairs = cookiesString.split(";"); + const cookies: { [name: string]: string } = {} + const pairs = cookiesString.split(';') pairs.forEach((pair) => { - const [name, ...rest] = pair.split("="); - cookies[name.trim()] = rest.join("=").trim(); - }); + const [name, ...rest] = pair.split('=') + cookies[name.trim()] = rest.join('=').trim() + }) - return cookies; + return cookies } export const getAllCookies = (req: Request): T => { - const cookies = parseCookies(req?.headers.get("Cookie") || ""); - const parsedCookies: any = {}; + const cookies = parseCookies(req?.headers.get('Cookie') || '') + const parsedCookies: any = {} for (const [name, value] of Object.entries(cookies)) { - parsedCookies[name] = parseCookieData(value) as any; + parsedCookies[name] = parseCookieData(value) as any } - return parsedCookies as T; -}; + return parsedCookies as T +} diff --git a/cookies/index.ts b/cookies/index.ts index 22f5db7..fb33c97 100644 --- a/cookies/index.ts +++ b/cookies/index.ts @@ -1,5 +1,5 @@ -export { clientCookieFactory as createClientCookieFactory } from "./client-cookie-factory"; -export type { CookieOptions } from "./cookie-types"; +export { clientCookieFactory as createClientCookieFactory } from './client-cookie-factory' +export type { CookieOptions } from './cookie-types' export { encodeCookie, getAllCookies, @@ -8,5 +8,5 @@ export { retrieveRawCookieValue, setCookie, stringifyCookieData, -} from "./cookie-utils"; -export { serverCookieFactory as createServerCookieFactory } from "./server-side-cookie-factory"; +} from './cookie-utils' +export { serverCookieFactory as createServerCookieFactory } from './server-side-cookie-factory' diff --git a/cookies/server-side-cookie-factory.test.ts b/cookies/server-side-cookie-factory.test.ts index 4717d3c..b950ca8 100644 --- a/cookies/server-side-cookie-factory.test.ts +++ b/cookies/server-side-cookie-factory.test.ts @@ -1,53 +1,53 @@ -import { beforeEach, describe, expect, jest, test } from "bun:test"; -import { serverCookieFactory } from "./server-side-cookie-factory"; +import { beforeEach, describe, expect, jest, test } from 'bun:test' +import { serverCookieFactory } from './server-side-cookie-factory' -describe("createServerCookieFactory", () => { - const cookieFactory = serverCookieFactory("test"); +describe('createServerCookieFactory', () => { + const cookieFactory = serverCookieFactory('test') // Mock response and request objects - let mockRes = { headers: { append: jest.fn() } }; - let mockReq = { headers: { get: jest.fn() } }; + let mockRes = { headers: { append: jest.fn() } } + let mockReq = { headers: { get: jest.fn() } } beforeEach(() => { // Reset the mock functions before each test - mockRes.headers.append.mockReset(); - mockReq.headers.get.mockReset(); - }); + mockRes.headers.append.mockReset() + mockReq.headers.get.mockReset() + }) - test("setCookie appends a Set-Cookie header", () => { - cookieFactory.setCookie("test", { res: mockRes as unknown as Response }); + test('setCookie appends a Set-Cookie header', () => { + cookieFactory.setCookie('test', { res: mockRes as unknown as Response }) // bun doesn't currently support toHaveBeenCalledWith // expect(mockRes.headers.append).toHaveBeenCalledWith( // "Set-Cookie", // "test=value" // ); - expect(mockRes.headers.append).toHaveBeenCalled(); - }); + expect(mockRes.headers.append).toHaveBeenCalled() + }) - test("getCookie returns the value of a cookie", () => { - mockReq.headers.get.mockReturnValue("test=value"); - const value = cookieFactory.getCookie(false, mockReq as any as Request); - expect(value).toBe("value"); - }); + test('getCookie returns the value of a cookie', () => { + mockReq.headers.get.mockReturnValue('test=value') + const value = cookieFactory.getCookie(false, mockReq as any as Request) + expect(value).toBe('value') + }) - test("deleteCookie sets a cookie with Max-Age=-1", () => { - cookieFactory.deleteCookie(mockRes as any as Response); + test('deleteCookie sets a cookie with Max-Age=-1', () => { + cookieFactory.deleteCookie(mockRes as any as Response) // expect(mockRes.headers.append).toHaveBeenCalledWith( // "Set-Cookie", // "test=; Max-Age=-1" // ); - expect(mockRes.headers.append).toHaveBeenCalled(); - }); - - test("checkCookie returns true if a cookie exists", () => { - mockReq.headers.get.mockReturnValue("test=value"); - const exists = cookieFactory.checkCookie(mockReq as unknown as Request); - expect(exists).toBe(true); - }); - - test("checkCookie returns false if a cookie does not exist", () => { - mockReq.headers.get.mockReturnValue(""); - const exists = cookieFactory.checkCookie(mockReq as unknown as Request); - expect(exists).toBe(false); - }); -}); + expect(mockRes.headers.append).toHaveBeenCalled() + }) + + test('checkCookie returns true if a cookie exists', () => { + mockReq.headers.get.mockReturnValue('test=value') + const exists = cookieFactory.checkCookie(mockReq as unknown as Request) + expect(exists).toBe(true) + }) + + test('checkCookie returns false if a cookie does not exist', () => { + mockReq.headers.get.mockReturnValue('') + const exists = cookieFactory.checkCookie(mockReq as unknown as Request) + expect(exists).toBe(false) + }) +}) diff --git a/cookies/server-side-cookie-factory.ts b/cookies/server-side-cookie-factory.ts index 8561fad..86f6574 100644 --- a/cookies/server-side-cookie-factory.ts +++ b/cookies/server-side-cookie-factory.ts @@ -1,5 +1,5 @@ -import { CookieOptions } from "./cookie-types"; -import { encodeCookie, parseCookieData, parseCookies, stringifyCookieData } from "./cookie-utils"; +import { CookieOptions } from './cookie-types' +import { encodeCookie, parseCookieData, parseCookies, stringifyCookieData } from './cookie-utils' export function serverCookieFactory< T = string, @@ -12,9 +12,9 @@ export function serverCookieFactory< request, response, }: { - request?: FactoryRequest; - response?: FactoryRes; - options?: CookieOptions; + request?: FactoryRequest + response?: FactoryRes + options?: CookieOptions } = {}, ) { const setCookie = ( @@ -23,52 +23,52 @@ export function serverCookieFactory< options = optionsCfg || {}, res = response, }: { - options?: CookieOptions; - res?: Response | undefined; + options?: CookieOptions + res?: Response | undefined } = {}, ) => { - let cookieValue = typeof value === "string" ? value : stringifyCookieData(value); + let cookieValue = typeof value === 'string' ? value : stringifyCookieData(value) - const cookieString = encodeCookie(cookieKey, cookieValue, options); + const cookieString = encodeCookie(cookieKey, cookieValue, options) if (!res) { - throw new Error("No response object provided"); + throw new Error('No response object provided') } - res?.headers.append("Set-Cookie", cookieString); - }; + res?.headers.append('Set-Cookie', cookieString) + } const deleteCookie = (res = response) => { if (!res) { - throw new Error("No response object provided"); + throw new Error('No response object provided') } - setCookie("" as unknown as T, { + setCookie('' as unknown as T, { options: { maxAge: -1 }, res, - }); - }; + }) + } const getCookie = (uriDecode = false, req = request): T | null => { if (!req) { - throw new Error("No request object provided"); + throw new Error('No request object provided') } - const cookies = parseCookies(req.headers.get("Cookie") || ""); - const cookie = cookies[cookieKey]; - return parseCookieData(uriDecode ? decodeURIComponent(cookie) : cookie); - }; + const cookies = parseCookies(req.headers.get('Cookie') || '') + const cookie = cookies[cookieKey] + return parseCookieData(uriDecode ? decodeURIComponent(cookie) : cookie) + } const checkCookie = (req = request) => { - return getCookie(false, req) !== null; - }; + return getCookie(false, req) !== null + } const getRawCookie = (req = request): string | null => { if (!req) { - throw new Error("No request object provided"); + throw new Error('No request object provided') } - const cookies = parseCookies(req.headers.get("Cookie") || ""); - return cookies[cookieKey] || null; - }; + const cookies = parseCookies(req.headers.get('Cookie') || '') + return cookies[cookieKey] || null + } return { setCookie, @@ -76,5 +76,5 @@ export function serverCookieFactory< deleteCookie, checkCookie, getRawCookie, - }; + } } diff --git a/data-gen/cities.ts b/data-gen/cities.ts index a445aab..a45768e 100644 --- a/data-gen/cities.ts +++ b/data-gen/cities.ts @@ -1,112 +1,112 @@ -import { randFromArray } from "./rand-num"; +import { randFromArray } from './rand-num' const cities = [ - "Albany", - "Austin", - "Baltimore", - "Boston", - "Charlotte", - "Chicago", - "Dallas", - "Denver", - "Detroit", - "Houston", - "Indianapolis", - "Jacksonville", - "Kansas City", - "Las Vegas", - "Los Angeles", - "Louisville", - "Memphis", - "Miami", - "Minneapolis", - "Nashville", - "New Orleans", - "New York", - "Oklahoma City", - "Orlando", - "Philadelphia", - "Phoenix", - "Portland", - "Raleigh", - "Sacramento", - "San Antonio", - "San Diego", - "San Francisco", - "San Jose", - "Seattle", - "St. Louis", - "Tampa", - "Washington, D.C.", - "Albuquerque", - "Atlanta", - "Boise", - "Buffalo", - "Cincinnati", - "Cleveland", - "Columbus", - "Des Moines", - "El Paso", - "Fort Worth", - "Fresno", - "Honolulu", - "Indianapolis", - "Jacksonville", - "Kansas City", - "Las Vegas", - "Little Rock", - "Madison", - "Manchester", - "Milwaukee", - "Mobile", - "Montgomery", - "Newark", - "Oakland", - "Omaha", - "Pittsburgh", - "Reno", - "Richmond", - "Riverside", - "Salt Lake City", - "Santa Ana", - "Savannah", - "Syracuse", - "Tacoma", - "Toledo", - "Tucson", - "Tulsa", - "Virginia Beach", - "Wichita", - "Wilmington", - "Anchorage", - "Arlington", - "Aurora", - "Bakersfield", - "Chandler", - "Chesapeake", - "Chula Vista", - "Durham", - "Fremont", - "Garland", - "Gilbert", - "Glendale", - "Hialeah", - "Irvine", - "Irving", - "Laredo", - "Mesa", - "Norfolk", - "North Las Vegas", - "Plano", - "Raleigh", - "Riverside", - "Rochester", - "Scottsdale", - "Spokane", - "Stockton", - "Toledo", -]; + 'Albany', + 'Austin', + 'Baltimore', + 'Boston', + 'Charlotte', + 'Chicago', + 'Dallas', + 'Denver', + 'Detroit', + 'Houston', + 'Indianapolis', + 'Jacksonville', + 'Kansas City', + 'Las Vegas', + 'Los Angeles', + 'Louisville', + 'Memphis', + 'Miami', + 'Minneapolis', + 'Nashville', + 'New Orleans', + 'New York', + 'Oklahoma City', + 'Orlando', + 'Philadelphia', + 'Phoenix', + 'Portland', + 'Raleigh', + 'Sacramento', + 'San Antonio', + 'San Diego', + 'San Francisco', + 'San Jose', + 'Seattle', + 'St. Louis', + 'Tampa', + 'Washington, D.C.', + 'Albuquerque', + 'Atlanta', + 'Boise', + 'Buffalo', + 'Cincinnati', + 'Cleveland', + 'Columbus', + 'Des Moines', + 'El Paso', + 'Fort Worth', + 'Fresno', + 'Honolulu', + 'Indianapolis', + 'Jacksonville', + 'Kansas City', + 'Las Vegas', + 'Little Rock', + 'Madison', + 'Manchester', + 'Milwaukee', + 'Mobile', + 'Montgomery', + 'Newark', + 'Oakland', + 'Omaha', + 'Pittsburgh', + 'Reno', + 'Richmond', + 'Riverside', + 'Salt Lake City', + 'Santa Ana', + 'Savannah', + 'Syracuse', + 'Tacoma', + 'Toledo', + 'Tucson', + 'Tulsa', + 'Virginia Beach', + 'Wichita', + 'Wilmington', + 'Anchorage', + 'Arlington', + 'Aurora', + 'Bakersfield', + 'Chandler', + 'Chesapeake', + 'Chula Vista', + 'Durham', + 'Fremont', + 'Garland', + 'Gilbert', + 'Glendale', + 'Hialeah', + 'Irvine', + 'Irving', + 'Laredo', + 'Mesa', + 'Norfolk', + 'North Las Vegas', + 'Plano', + 'Raleigh', + 'Riverside', + 'Rochester', + 'Scottsdale', + 'Spokane', + 'Stockton', + 'Toledo', +] export const getRandomCity = () => { - return randFromArray(cities); -}; + return randFromArray(cities) +} diff --git a/data-gen/countries.ts b/data-gen/countries.ts index 4b537b5..2c25bf2 100644 --- a/data-gen/countries.ts +++ b/data-gen/countries.ts @@ -1,204 +1,204 @@ -import { randFromArray } from "./rand-num"; +import { randFromArray } from './rand-num' const countries = [ - "Afghanistan", - "Albania", - "Algeria", - "Andorra", - "Angola", - "Antigua and Barbuda", - "Argentina", - "Armenia", - "Australia", - "Austria", - "Azerbaijan", - "Bahamas", - "Bahrain", - "Bangladesh", - "Barbados", - "Belarus", - "Belgium", - "Belize", - "Benin", - "Bhutan", - "Bolivia", - "Bosnia and Herzegovina", - "Botswana", - "Brazil", - "Brunei", - "Bulgaria", - "Burkina Faso", - "Burundi", - "Cabo Verde", - "Cambodia", - "Cameroon", - "Canada", - "Central African Republic", - "Chad", - "Chile", - "China", - "Colombia", - "Comoros", - "Congo", - "Costa Rica", - "Croatia", - "Cuba", - "Cyprus", - "Czech Republic", - "Democratic Republic of the Congo", - "Denmark", - "Djibouti", - "Dominica", - "Dominican Republic", - "East Timor", - "Ecuador", - "Egypt", - "El Salvador", - "Equatorial Guinea", - "Eritrea", - "Estonia", - "Eswatini", - "Ethiopia", - "Fiji", - "Finland", - "France", - "Gabon", - "Gambia", - "Georgia", - "Germany", - "Ghana", - "Greece", - "Grenada", - "Guatemala", - "Guinea", - "Guinea-Bissau", - "Guyana", - "Haiti", - "Honduras", - "Hungary", - "Iceland", - "India", - "Indonesia", - "Iran", - "Iraq", - "Ireland", - "Israel", - "Italy", - "Ivory Coast", - "Jamaica", - "Japan", - "Jordan", - "Kazakhstan", - "Kenya", - "Kiribati", - "Kosovo", - "Kuwait", - "Kyrgyzstan", - "Laos", - "Latvia", - "Lebanon", - "Lesotho", - "Liberia", - "Libya", - "Liechtenstein", - "Lithuania", - "Luxembourg", - "Madagascar", - "Malawi", - "Malaysia", - "Maldives", - "Mali", - "Malta", - "Marshall Islands", - "Mauritania", - "Mauritius", - "Mexico", - "Micronesia", - "Moldova", - "Monaco", - "Mongolia", - "Montenegro", - "Morocco", - "Mozambique", - "Myanmar", - "Namibia", - "Nauru", - "Nepal", - "Netherlands", - "New Zealand", - "Nicaragua", - "Niger", - "Nigeria", - "North Macedonia", - "Norway", - "Oman", - "Pakistan", - "Palau", - "Palestine", - "Panama", - "Papua New Guinea", - "Paraguay", - "Peru", - "Philippines", - "Poland", - "Portugal", - "Qatar", - "Romania", - "Russia", - "Rwanda", - "Saint Kitts and Nevis", - "Saint Lucia", - "Saint Vincent and the Grenadines", - "Samoa", - "San Marino", - "Sao Tome and Principe", - "Saudi Arabia", - "Senegal", - "Serbia", - "Seychelles", - "Sierra Leone", - "Singapore", - "Slovakia", - "Slovenia", - "Solomon Islands", - "Somalia", - "South Africa", - "South Korea", - "South Sudan", - "Spain", - "Sri Lanka", - "Sudan", - "Suriname", - "Sweden", - "Switzerland", - "Syria", - "Taiwan", - "Tajikistan", - "Tanzania", - "Thailand", - "Togo", - "Tonga", - "Trinidad and Tobago", - "Tunisia", - "Turkey", - "Turkmenistan", - "Tuvalu", - "Uganda", - "Ukraine", - "United Arab Emirates", - "United Kingdom", - "United States of America", - "Uruguay", - "Uzbekistan", - "Vanuatu", - "Vatican City", - "Venezuela", - "Vietnam", - "Yemen", - "Zambia", - "Zimbabwe", -]; + 'Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Antigua and Barbuda', + 'Argentina', + 'Armenia', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bhutan', + 'Bolivia', + 'Bosnia and Herzegovina', + 'Botswana', + 'Brazil', + 'Brunei', + 'Bulgaria', + 'Burkina Faso', + 'Burundi', + 'Cabo Verde', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Central African Republic', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Comoros', + 'Congo', + 'Costa Rica', + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Democratic Republic of the Congo', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'East Timor', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Eswatini', + 'Ethiopia', + 'Fiji', + 'Finland', + 'France', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Greece', + 'Grenada', + 'Guatemala', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Honduras', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland', + 'Israel', + 'Italy', + 'Ivory Coast', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Kosovo', + 'Kuwait', + 'Kyrgyzstan', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Morocco', + 'Mozambique', + 'Myanmar', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'North Macedonia', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Palestine', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Qatar', + 'Romania', + 'Russia', + 'Rwanda', + 'Saint Kitts and Nevis', + 'Saint Lucia', + 'Saint Vincent and the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome and Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Korea', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Sweden', + 'Switzerland', + 'Syria', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Togo', + 'Tonga', + 'Trinidad and Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States of America', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Vatican City', + 'Venezuela', + 'Vietnam', + 'Yemen', + 'Zambia', + 'Zimbabwe', +] export const getRandomCountry = () => { - return randFromArray(countries); -}; + return randFromArray(countries) +} diff --git a/data-gen/create-random-data.test.ts b/data-gen/create-random-data.test.ts index b982360..d0f4227 100644 --- a/data-gen/create-random-data.test.ts +++ b/data-gen/create-random-data.test.ts @@ -1,36 +1,36 @@ -import { describe, expect, it } from "bun:test"; -import { dataGenerators } from "./object-gen"; -import { createRandomData } from "./create-random-data"; +import { describe, expect, it } from 'bun:test' +import { dataGenerators } from './object-gen' +import { createRandomData } from './create-random-data' -describe("createRandomData", () => { - it("should return an object with the correct keys", () => { +describe('createRandomData', () => { + it('should return an object with the correct keys', () => { const config = { - firstName: { type: "firstName" }, - lastName: { type: "lastName" }, - age: { type: "num", min: 18, max: 65 }, - } as const; - const result = createRandomData(config); - expect(result).toHaveProperty("firstName"); - expect(result).toHaveProperty("lastName"); - expect(result).toHaveProperty("age"); - }); + firstName: { type: 'firstName' }, + lastName: { type: 'lastName' }, + age: { type: 'num', min: 18, max: 65 }, + } as const + const result = createRandomData(config) + expect(result).toHaveProperty('firstName') + expect(result).toHaveProperty('lastName') + expect(result).toHaveProperty('age') + }) - it("should return a number within the specified range for num type config", () => { + it('should return a number within the specified range for num type config', () => { const config = { - age: { type: "num", min: 18, max: 65 }, - } as const; - const result = createRandomData(config); - expect(result.age).toBeGreaterThanOrEqual(18); - expect(result.age).toBeLessThanOrEqual(65); - }); + age: { type: 'num', min: 18, max: 65 }, + } as const + const result = createRandomData(config) + expect(result.age).toBeGreaterThanOrEqual(18) + expect(result.age).toBeLessThanOrEqual(65) + }) - it("should return a string for non-num type config", () => { + it('should return a string for non-num type config', () => { const config = { - firstName: { type: "firstName" }, - lastName: { type: "lastName" }, - } as const; - const result = createRandomData(config); - expect(typeof result.firstName).toBe("string"); - expect(typeof result.lastName).toBe("string"); - }); -}); + firstName: { type: 'firstName' }, + lastName: { type: 'lastName' }, + } as const + const result = createRandomData(config) + expect(typeof result.firstName).toBe('string') + expect(typeof result.lastName).toBe('string') + }) +}) diff --git a/data-gen/create-random-data.ts b/data-gen/create-random-data.ts index b1f3af6..9c067af 100644 --- a/data-gen/create-random-data.ts +++ b/data-gen/create-random-data.ts @@ -1,22 +1,22 @@ -import { getRandomCity } from "./cities"; -import { getRandomCountry } from "./countries"; -import { getRandomFirstName, getRandomFullName, getRandomLastName } from "./names"; -import { randNum } from "./rand-num"; -import { getRandState } from "./states"; +import { getRandomCity } from './cities' +import { getRandomCountry } from './countries' +import { getRandomFirstName, getRandomFullName, getRandomLastName } from './names' +import { randNum } from './rand-num' +import { getRandState } from './states' type NumConfig = { - type: "num"; - min?: number; - max?: number; -}; + type: 'num' + min?: number + max?: number +} type OtherConfig = { - type: Exclude; -}; + type: Exclude +} -type OutputT = T extends { type: "num" } ? number : ReturnType; +type OutputT = T extends { type: 'num' } ? number : ReturnType -type DataConfigItem = NumConfig | OtherConfig; +type DataConfigItem = NumConfig | OtherConfig export const dataGeneratorMap = { city: getRandomCity, @@ -26,39 +26,41 @@ export const dataGeneratorMap = { country: getRandomCountry, state: getRandState, num: randNum, -}; +} export type DataGeneratorMapConfig = { - city: {}; - firstName: {}; - lastName: {}; - fullName: {}; - country: {}; - state: {}; - num: { min: number; max: number }; -}; + city: {} + firstName: {} + lastName: {} + fullName: {} + country: {} + state: {} + num: { min: number; max: number } +} -type DataGenMap = typeof dataGeneratorMap; +type DataGenMap = typeof dataGeneratorMap -export function createRandomData>(config: T): { - [K in keyof T]: OutputT; +export function createRandomData>( + config: T, +): { + [K in keyof T]: OutputT } { - const result: any = {}; + const result: any = {} for (const key in config) { - const itemConfig = config[key]; - if (itemConfig.type === "num") { - const { min = 0, max = 100 } = itemConfig; - result[key] = dataGeneratorMap[itemConfig.type](min, max); + const itemConfig = config[key] + if (itemConfig.type === 'num') { + const { min = 0, max = 100 } = itemConfig + result[key] = dataGeneratorMap[itemConfig.type](min, max) } else { - result[key] = dataGeneratorMap[itemConfig.type](); + result[key] = dataGeneratorMap[itemConfig.type]() } } - return result; + return result } const dataGen = { - first: { type: "firstName" }, // Explicitly type this as OtherConfig - age: { type: "num", min: 18, max: 65 }, // Explicitly type this as NumConfig -} as const; + first: { type: 'firstName' }, // Explicitly type this as OtherConfig + age: { type: 'num', min: 18, max: 65 }, // Explicitly type this as NumConfig +} as const diff --git a/data-gen/index.ts b/data-gen/index.ts index 5772eb5..e97836a 100644 --- a/data-gen/index.ts +++ b/data-gen/index.ts @@ -1,13 +1,9 @@ -export { getRandomCity } from "./cities"; -export { getRandomCountry } from "./countries"; -export { createRandomData, dataGeneratorMap } from "./create-random-data"; -export type { DataGeneratorMapConfig } from "./create-random-data"; -export { - getRandomFirstName, - getRandomFullName, - getRandomLastName, -} from "./names"; -export { dataGenerators, inferTypeAndGenerate } from "./object-gen"; -export { randDateRange } from "./rand-date-range"; -export { randFromArray, randNum } from "./rand-num"; -export { getRandState } from "./states"; +export { getRandomCity } from './cities' +export { getRandomCountry } from './countries' +export { createRandomData, dataGeneratorMap } from './create-random-data' +export type { DataGeneratorMapConfig } from './create-random-data' +export { getRandomFirstName, getRandomFullName, getRandomLastName } from './names' +export { dataGenerators, inferTypeAndGenerate } from './object-gen' +export { randDateRange } from './rand-date-range' +export { randFromArray, randNum } from './rand-num' +export { getRandState } from './states' diff --git a/data-gen/names.ts b/data-gen/names.ts index 3217199..e9a9d07 100644 --- a/data-gen/names.ts +++ b/data-gen/names.ts @@ -1,198 +1,198 @@ -import { randFromArray } from "./rand-num"; +import { randFromArray } from './rand-num' export const firstNames = [ - "Adam", - "Alex", - "Alice", - "Andrew", - "Anna", - "Anthony", - "Barbara", - "Betty", - "Brandon", - "Brian", - "Carol", - "Charles", - "Christine", - "Christopher", - "Cynthia", - "Daniel", - "David", - "Deborah", - "Donald", - "Donna", - "Dorothy", - "Edward", - "Elizabeth", - "Emily", - "Eric", - "Evelyn", - "Frank", - "George", - "Grace", - "Helen", - "Henry", - "Jack", - "James", - "Jane", - "Jason", - "Jennifer", - "Jessica", - "John", - "Joseph", - "Joshua", - "Julie", - "Karen", - "Katherine", - "Kathleen", - "Kenneth", - "Kevin", - "Laura", - "Linda", - "Lisa", - "Margaret", - "Maria", - "Mark", - "Mary", - "Matthew", - "Melissa", - "Michael", - "Michelle", - "Nancy", - "Nicole", - "Patricia", - "Patrick", - "Paul", - "Peter", - "Rachel", - "Raymond", - "Richard", - "Robert", - "Roger", - "Ronald", - "Ryan", - "Sandra", - "Sarah", - "Scott", - "Sharon", - "Stephen", - "Susan", - "Teresa", - "Thomas", - "Timothy", - "Virginia", - "Walter", - "William", -]; + 'Adam', + 'Alex', + 'Alice', + 'Andrew', + 'Anna', + 'Anthony', + 'Barbara', + 'Betty', + 'Brandon', + 'Brian', + 'Carol', + 'Charles', + 'Christine', + 'Christopher', + 'Cynthia', + 'Daniel', + 'David', + 'Deborah', + 'Donald', + 'Donna', + 'Dorothy', + 'Edward', + 'Elizabeth', + 'Emily', + 'Eric', + 'Evelyn', + 'Frank', + 'George', + 'Grace', + 'Helen', + 'Henry', + 'Jack', + 'James', + 'Jane', + 'Jason', + 'Jennifer', + 'Jessica', + 'John', + 'Joseph', + 'Joshua', + 'Julie', + 'Karen', + 'Katherine', + 'Kathleen', + 'Kenneth', + 'Kevin', + 'Laura', + 'Linda', + 'Lisa', + 'Margaret', + 'Maria', + 'Mark', + 'Mary', + 'Matthew', + 'Melissa', + 'Michael', + 'Michelle', + 'Nancy', + 'Nicole', + 'Patricia', + 'Patrick', + 'Paul', + 'Peter', + 'Rachel', + 'Raymond', + 'Richard', + 'Robert', + 'Roger', + 'Ronald', + 'Ryan', + 'Sandra', + 'Sarah', + 'Scott', + 'Sharon', + 'Stephen', + 'Susan', + 'Teresa', + 'Thomas', + 'Timothy', + 'Virginia', + 'Walter', + 'William', +] export const lastNames = [ - "Adams", - "Allen", - "Anderson", - "Bailey", - "Baker", - "Barnes", - "Bell", - "Bennett", - "Brooks", - "Brown", - "Bryant", - "Campbell", - "Carter", - "Clark", - "Collins", - "Cook", - "Cooper", - "Cox", - "Davis", - "Diaz", - "Edwards", - "Evans", - "Foster", - "Flores", - "Garcia", - "Gonzales", - "Gonzalez", - "Gray", - "Green", - "Griffin", - "Hall", - "Harris", - "Hayes", - "Henderson", - "Hernandez", - "Hill", - "Howard", - "Hughes", - "Jackson", - "James", - "Jenkins", - "Johnson", - "Jones", - "Kelly", - "King", - "Lee", - "Lewis", - "Lopez", - "Long", - "Martinez", - "Martin", - "Miller", - "Mitchell", - "Moore", - "Morgan", - "Morris", - "Murphy", - "Nelson", - "Parker", - "Patterson", - "Perez", - "Perry", - "Peterson", - "Phillips", - "Powell", - "Price", - "Ramirez", - "Reed", - "Richardson", - "Rivera", - "Roberts", - "Robinson", - "Rodriguez", - "Rogers", - "Ross", - "Russell", - "Sanchez", - "Sanders", - "Scott", - "Simmons", - "Smith", - "Stewart", - "Taylor", - "Thomas", - "Thompson", - "Torres", - "Turner", - "Walker", - "Washington", - "Watson", - "Ward", - "White", - "Williams", - "Wilson", - "Wood", - "Wright", - "Young", -]; + 'Adams', + 'Allen', + 'Anderson', + 'Bailey', + 'Baker', + 'Barnes', + 'Bell', + 'Bennett', + 'Brooks', + 'Brown', + 'Bryant', + 'Campbell', + 'Carter', + 'Clark', + 'Collins', + 'Cook', + 'Cooper', + 'Cox', + 'Davis', + 'Diaz', + 'Edwards', + 'Evans', + 'Foster', + 'Flores', + 'Garcia', + 'Gonzales', + 'Gonzalez', + 'Gray', + 'Green', + 'Griffin', + 'Hall', + 'Harris', + 'Hayes', + 'Henderson', + 'Hernandez', + 'Hill', + 'Howard', + 'Hughes', + 'Jackson', + 'James', + 'Jenkins', + 'Johnson', + 'Jones', + 'Kelly', + 'King', + 'Lee', + 'Lewis', + 'Lopez', + 'Long', + 'Martinez', + 'Martin', + 'Miller', + 'Mitchell', + 'Moore', + 'Morgan', + 'Morris', + 'Murphy', + 'Nelson', + 'Parker', + 'Patterson', + 'Perez', + 'Perry', + 'Peterson', + 'Phillips', + 'Powell', + 'Price', + 'Ramirez', + 'Reed', + 'Richardson', + 'Rivera', + 'Roberts', + 'Robinson', + 'Rodriguez', + 'Rogers', + 'Ross', + 'Russell', + 'Sanchez', + 'Sanders', + 'Scott', + 'Simmons', + 'Smith', + 'Stewart', + 'Taylor', + 'Thomas', + 'Thompson', + 'Torres', + 'Turner', + 'Walker', + 'Washington', + 'Watson', + 'Ward', + 'White', + 'Williams', + 'Wilson', + 'Wood', + 'Wright', + 'Young', +] export const getRandomFirstName = () => { - return randFromArray(firstNames); -}; + return randFromArray(firstNames) +} export const getRandomLastName = () => { - return randFromArray(lastNames); -}; + return randFromArray(lastNames) +} export const getRandomFullName = () => { - return `${getRandomFirstName()} ${getRandomLastName()}`; -}; + return `${getRandomFirstName()} ${getRandomLastName()}` +} diff --git a/data-gen/object-gen.test.ts b/data-gen/object-gen.test.ts index 9104f81..6b1c8ce 100644 --- a/data-gen/object-gen.test.ts +++ b/data-gen/object-gen.test.ts @@ -1,89 +1,89 @@ -import { describe, expect, it } from "bun:test"; -import { dataGenerators, inferTypeAndGenerate } from "./object-gen"; +import { describe, expect, it } from 'bun:test' +import { dataGenerators, inferTypeAndGenerate } from './object-gen' -describe("dataGenerators", () => { - it("should generate a random string", () => { - const result = dataGenerators.string(); - expect(typeof result).toBe("string"); - }); +describe('dataGenerators', () => { + it('should generate a random string', () => { + const result = dataGenerators.string() + expect(typeof result).toBe('string') + }) - it("should generate a random number", () => { - const result = dataGenerators.number(); - expect(typeof result).toBe("number"); - }); + it('should generate a random number', () => { + const result = dataGenerators.number() + expect(typeof result).toBe('number') + }) - it("should generate a random boolean", () => { - const result = dataGenerators.boolean(); - expect(typeof result).toBe("boolean"); - }); + it('should generate a random boolean', () => { + const result = dataGenerators.boolean() + expect(typeof result).toBe('boolean') + }) - it("should generate a random date", () => { - const result = dataGenerators.date(); - expect(result instanceof Date).toBe(true); - }); + it('should generate a random date', () => { + const result = dataGenerators.date() + expect(result instanceof Date).toBe(true) + }) - it("should generate a random object", () => { + it('should generate a random object', () => { const result = dataGenerators.object({ name: dataGenerators.string, age: dataGenerators.number, isStudent: dataGenerators.boolean, - })(); - expect(typeof result.name).toBe("string"); - expect(typeof result.age).toBe("number"); - expect(typeof result.isStudent).toBe("boolean"); - }); + })() + expect(typeof result.name).toBe('string') + expect(typeof result.age).toBe('number') + expect(typeof result.isStudent).toBe('boolean') + }) - it("should generate a random array", () => { - const result = dataGenerators.array(dataGenerators.number, 5)(); + it('should generate a random array', () => { + const result = dataGenerators.array(dataGenerators.number, 5)() - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(5); - expect(typeof result[0]).toBe("number"); - }); -}); + expect(Array.isArray(result)).toBe(true) + expect(result.length).toBe(5) + expect(typeof result[0]).toBe('number') + }) +}) -describe("inferTypeAndGenerate", () => { - it("should generate a string for a string input", () => { - const result = inferTypeAndGenerate("hello"); - expect(typeof result).toBe("string"); - }); +describe('inferTypeAndGenerate', () => { + it('should generate a string for a string input', () => { + const result = inferTypeAndGenerate('hello') + expect(typeof result).toBe('string') + }) - it("should generate a number for a number input", () => { - const result = inferTypeAndGenerate(42); + it('should generate a number for a number input', () => { + const result = inferTypeAndGenerate(42) // expect(result).toEqual(dataGenerators.number()); - expect(typeof result).toBe("number"); - }); + expect(typeof result).toBe('number') + }) - it("should generate a boolean for a boolean input", () => { - const result = inferTypeAndGenerate(true); - expect(typeof result).toBe("boolean"); - }); + it('should generate a boolean for a boolean input', () => { + const result = inferTypeAndGenerate(true) + expect(typeof result).toBe('boolean') + }) - it("should generate a date for a Date input", () => { - const date = new Date(); - const result = inferTypeAndGenerate(date); - expect(result instanceof Date).toBe(true); - }); + it('should generate a date for a Date input', () => { + const date = new Date() + const result = inferTypeAndGenerate(date) + expect(result instanceof Date).toBe(true) + }) - it("should generate an array for an array input", () => { - const input = [1, 2, 3]; - const result = inferTypeAndGenerate(input); - const inferredTypeGenerator = inferTypeAndGenerate(input[0]); + it('should generate an array for an array input', () => { + const input = [1, 2, 3] + const result = inferTypeAndGenerate(input) + const inferredTypeGenerator = inferTypeAndGenerate(input[0]) - expect(result.length).toEqual(input.length); + expect(result.length).toEqual(input.length) - expect(typeof result[0]).toBe(typeof inferredTypeGenerator); - expect(typeof input[0]).toEqual(typeof result[0]); - }); + expect(typeof result[0]).toBe(typeof inferredTypeGenerator) + expect(typeof input[0]).toEqual(typeof result[0]) + }) - it("should generate an object for an object input", () => { - const input = { name: "John", age: 30 }; - const result = inferTypeAndGenerate(input); - const expected = dataGenerators.object(input)(); + it('should generate an object for an object input', () => { + const input = { name: 'John', age: 30 } + const result = inferTypeAndGenerate(input) + const expected = dataGenerators.object(input)() // validate that the types of each key value, match the expected key/value types for (const key in expected) { // @ts-ignore - expect(typeof result[key]).toBe(typeof expected[key]); + expect(typeof result[key]).toBe(typeof expected[key]) } - }); -}); + }) +}) diff --git a/data-gen/object-gen.ts b/data-gen/object-gen.ts index 2ab0586..bc4c673 100644 --- a/data-gen/object-gen.ts +++ b/data-gen/object-gen.ts @@ -1,19 +1,19 @@ -type DataGenerator = () => T; +type DataGenerator = () => T interface DataGenerators { - string: (generator?: DataGenerator) => string; - number: (generator?: DataGenerator) => number; - boolean: (generator?: DataGenerator) => boolean; - date: (generator?: DataGenerator) => Date; + string: (generator?: DataGenerator) => string + number: (generator?: DataGenerator) => number + boolean: (generator?: DataGenerator) => boolean + date: (generator?: DataGenerator) => Date object: >( shape: T, generatorMap?: Partial>>, - ) => DataGenerator; - array: (generator: DataGenerator, length?: number) => DataGenerator; + ) => DataGenerator + array: (generator: DataGenerator, length?: number) => DataGenerator } export const dataGenerators: DataGenerators = { - string: (generator = () => "Some random string") => generator(), + string: (generator = () => 'Some random string') => generator(), number: (generator = () => Math.random() * 100) => generator(), boolean: (generator = () => Math.random() < 0.5) => generator(), date: (generator = () => new Date()) => generator(), @@ -22,49 +22,49 @@ export const dataGenerators: DataGenerators = { generatorMap: Partial>> = {}, ) => { return () => { - const result = {} as any; + const result = {} as any for (const key in shape) { - const generator = generatorMap[key as keyof T] || shape[key]; - if (typeof generator === "function") { - result[key] = generator(); + const generator = generatorMap[key as keyof T] || shape[key] + if (typeof generator === 'function') { + result[key] = generator() } else { - result[key] = inferTypeAndGenerate(generator); + result[key] = inferTypeAndGenerate(generator) } } - return result as T; - }; + return result as T + } }, array: (generator: DataGenerator, length = 10) => { return () => { - const result = []; + const result = [] for (let i = 0; i < length; i++) { - result.push(generator()); + result.push(generator()) } - return result; - }; + return result + } }, -}; +} export const inferTypeAndGenerate = (value: Val): any => { switch (typeof value) { - case "string": - return dataGenerators.string(); - case "number": - return dataGenerators.number(); - case "boolean": - return dataGenerators.boolean(); + case 'string': + return dataGenerators.string() + case 'number': + return dataGenerators.number() + case 'boolean': + return dataGenerators.boolean() default: if (value instanceof Date) { - return dataGenerators.date(); + return dataGenerators.date() } else if (Array.isArray(value)) { // Use first item in the array to infer type for array items // This is a simplification and assumes uniform array types - const inferredTypeGenerator = inferTypeAndGenerate(value[0]); - return dataGenerators.array(() => inferredTypeGenerator, value.length)(); - } else if (typeof value === "object" && value !== null) { - return dataGenerators.object(value as Record)(); + const inferredTypeGenerator = inferTypeAndGenerate(value[0]) + return dataGenerators.array(() => inferredTypeGenerator, value.length)() + } else if (typeof value === 'object' && value !== null) { + return dataGenerators.object(value as Record)() } } - return null; -}; + return null +} diff --git a/data-gen/rand-date-range.test.ts b/data-gen/rand-date-range.test.ts index e60fd62..a63c87b 100644 --- a/data-gen/rand-date-range.test.ts +++ b/data-gen/rand-date-range.test.ts @@ -1,13 +1,13 @@ -import { describe, expect, it } from "bun:test"; -import { randDateRange } from "./rand-date-range"; +import { describe, expect, it } from 'bun:test' +import { randDateRange } from './rand-date-range' -describe("randDateRange", () => { - it("should return a date between the start and end dates", () => { - const startDate = new Date("2021-01-01"); - const endDate = new Date("2021-12-31"); - const result = randDateRange(startDate, endDate); - expect(result).toBeInstanceOf(Date); - expect(result.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); - expect(result.getTime()).toBeLessThanOrEqual(endDate.getTime()); - }); -}); +describe('randDateRange', () => { + it('should return a date between the start and end dates', () => { + const startDate = new Date('2021-01-01') + const endDate = new Date('2021-12-31') + const result = randDateRange(startDate, endDate) + expect(result).toBeInstanceOf(Date) + expect(result.getTime()).toBeGreaterThanOrEqual(startDate.getTime()) + expect(result.getTime()).toBeLessThanOrEqual(endDate.getTime()) + }) +}) diff --git a/data-gen/rand-date-range.ts b/data-gen/rand-date-range.ts index c6b933a..e999580 100644 --- a/data-gen/rand-date-range.ts +++ b/data-gen/rand-date-range.ts @@ -1,6 +1,6 @@ export const randDateRange = (startDate: Date, endDate: Date): Date => { - const start = startDate.getTime(); - const end = endDate.getTime(); - const randomDate = new Date(start + Math.random() * (end - start)); - return randomDate; -}; + const start = startDate.getTime() + const end = endDate.getTime() + const randomDate = new Date(start + Math.random() * (end - start)) + return randomDate +} diff --git a/data-gen/rand-num.test.ts b/data-gen/rand-num.test.ts index 4cc9beb..ab2631b 100644 --- a/data-gen/rand-num.test.ts +++ b/data-gen/rand-num.test.ts @@ -1,18 +1,18 @@ -import { describe, expect, it } from "bun:test"; -import { randFromArray, randNum } from "./rand-num"; +import { describe, expect, it } from 'bun:test' +import { randFromArray, randNum } from './rand-num' -describe("randNum", () => { - it("should return a number between the min and max values", () => { - const result = randNum(1, 10); - expect(result).toBeGreaterThanOrEqual(1); - expect(result).toBeLessThanOrEqual(10); - }); -}); +describe('randNum', () => { + it('should return a number between the min and max values', () => { + const result = randNum(1, 10) + expect(result).toBeGreaterThanOrEqual(1) + expect(result).toBeLessThanOrEqual(10) + }) +}) -describe("randFromArray", () => { - it("should return a random element from the array", () => { - const arr = [1, 2, 3, 4, 5]; - const result = randFromArray(arr); - expect(arr).toContain(result); - }); -}); +describe('randFromArray', () => { + it('should return a random element from the array', () => { + const arr = [1, 2, 3, 4, 5] + const result = randFromArray(arr) + expect(arr).toContain(result) + }) +}) diff --git a/data-gen/rand-num.ts b/data-gen/rand-num.ts index 835cfbb..4b74e94 100644 --- a/data-gen/rand-num.ts +++ b/data-gen/rand-num.ts @@ -1,7 +1,7 @@ export const randNum = (min: number, max: number) => { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; + return Math.floor(Math.random() * (max - min + 1)) + min +} export const randFromArray = (arr: T[]) => { - return arr[randNum(0, arr.length - 1)]; -}; + return arr[randNum(0, arr.length - 1)] +} diff --git a/data-gen/states.ts b/data-gen/states.ts index ce5c8db..50b5934 100644 --- a/data-gen/states.ts +++ b/data-gen/states.ts @@ -1,113 +1,113 @@ -import { randFromArray } from "./rand-num"; +import { randFromArray } from './rand-num' const uniqueStates = [ - "Alabama", - "Alaska", - "Arizona", - "Arkansas", - "California", - "Colorado", - "Connecticut", - "Delaware", - "Florida", - "Georgia", - "Hawaii", - "Idaho", - "Illinois", - "Indiana", - "Iowa", - "Kansas", - "Kentucky", - "Louisiana", - "Maine", - "Maryland", - "Massachusetts", - "Michigan", - "Minnesota", - "Mississippi", - "Missouri", - "Montana", - "Nebraska", - "Nevada", - "New Hampshire", - "New Jersey", - "New Mexico", - "New York", - "North Carolina", - "North Dakota", - "Ohio", - "Oklahoma", - "Oregon", - "Pennsylvania", - "Rhode Island", - "South Carolina", - "South Dakota", - "Tennessee", - "Texas", - "Utah", - "Vermont", - "Virginia", - "Washington", - "West Virginia", - "Wisconsin", - "Wyoming", - "American Samoa", - "District of Columbia", - "Guam", - "Northern Mariana Islands", - "Puerto Rico", - "U.S. Virgin Islands", - "Federated States of Micronesia", - "Marshall Islands", - "Palau", - "Armed Forces Africa", - "Armed Forces Americas", - "Armed Forces Canada", - "Armed Forces Europe", - "Armed Forces Middle East", - "Armed Forces Pacific", - "Alberta", - "British Columbia", - "Manitoba", - "New Brunswick", - "Newfoundland and Labrador", - "Northwest Territories", - "Nova Scotia", - "Nunavut", - "Ontario", - "Prince Edward Island", - "Quebec", - "Saskatchewan", - "Yukon", - "Johor", - "Kedah", - "Kelantan", - "Melaka", - "Negeri Sembilan", - "Pahang", - "Perak", - "Perlis", - "Pulau Pinang", - "Sabah", - "Sarawak", - "Selangor", - "Terengganu", - "Baden-Württemberg", - "Bavaria", - "Berlin", - "Brandenburg", - "Bremen", - "Hamburg", - "Hesse", - "Lower Saxony", - "Mecklenburg-Vorpommern", - "North Rhine-Westphalia", - "Rhineland-Palatinate", - "Saarland", - "Saxony", - "Saxony-Anhalt", - "Schleswig-Holstein", - "Thuringia", -]; + 'Alabama', + 'Alaska', + 'Arizona', + 'Arkansas', + 'California', + 'Colorado', + 'Connecticut', + 'Delaware', + 'Florida', + 'Georgia', + 'Hawaii', + 'Idaho', + 'Illinois', + 'Indiana', + 'Iowa', + 'Kansas', + 'Kentucky', + 'Louisiana', + 'Maine', + 'Maryland', + 'Massachusetts', + 'Michigan', + 'Minnesota', + 'Mississippi', + 'Missouri', + 'Montana', + 'Nebraska', + 'Nevada', + 'New Hampshire', + 'New Jersey', + 'New Mexico', + 'New York', + 'North Carolina', + 'North Dakota', + 'Ohio', + 'Oklahoma', + 'Oregon', + 'Pennsylvania', + 'Rhode Island', + 'South Carolina', + 'South Dakota', + 'Tennessee', + 'Texas', + 'Utah', + 'Vermont', + 'Virginia', + 'Washington', + 'West Virginia', + 'Wisconsin', + 'Wyoming', + 'American Samoa', + 'District of Columbia', + 'Guam', + 'Northern Mariana Islands', + 'Puerto Rico', + 'U.S. Virgin Islands', + 'Federated States of Micronesia', + 'Marshall Islands', + 'Palau', + 'Armed Forces Africa', + 'Armed Forces Americas', + 'Armed Forces Canada', + 'Armed Forces Europe', + 'Armed Forces Middle East', + 'Armed Forces Pacific', + 'Alberta', + 'British Columbia', + 'Manitoba', + 'New Brunswick', + 'Newfoundland and Labrador', + 'Northwest Territories', + 'Nova Scotia', + 'Nunavut', + 'Ontario', + 'Prince Edward Island', + 'Quebec', + 'Saskatchewan', + 'Yukon', + 'Johor', + 'Kedah', + 'Kelantan', + 'Melaka', + 'Negeri Sembilan', + 'Pahang', + 'Perak', + 'Perlis', + 'Pulau Pinang', + 'Sabah', + 'Sarawak', + 'Selangor', + 'Terengganu', + 'Baden-Württemberg', + 'Bavaria', + 'Berlin', + 'Brandenburg', + 'Bremen', + 'Hamburg', + 'Hesse', + 'Lower Saxony', + 'Mecklenburg-Vorpommern', + 'North Rhine-Westphalia', + 'Rhineland-Palatinate', + 'Saarland', + 'Saxony', + 'Saxony-Anhalt', + 'Schleswig-Holstein', + 'Thuringia', +] -export const getRandState = () => randFromArray(uniqueStates); +export const getRandState = () => randFromArray(uniqueStates) diff --git a/deploy/github-actions.ts b/deploy/github-actions.ts index 74a5189..d72de47 100644 --- a/deploy/github-actions.ts +++ b/deploy/github-actions.ts @@ -1,87 +1,87 @@ -import { SyncSubprocess } from "bun"; -import { exit } from "process"; -import { ulog } from "../utils/ulog"; +import { SyncSubprocess } from 'bun' +import { exit } from 'process' +import { ulog } from '../utils/ulog' export async function logStdOutput(proc: SyncSubprocess) { try { - const stdout = await new Response(proc.stdout).text(); - const stderr = proc?.stderr?.toString().trim(); + const stdout = await new Response(proc.stdout).text() + const stderr = proc?.stderr?.toString().trim() if (stdout) { - ulog(stdout); + ulog(stdout) } if (stderr) { - ulog(stderr); + ulog(stderr) } return { stdout, stderr, - }; + } } catch (error) { - console.error(error); + console.error(error) } } type GHActions = { - branch: "main" | "release"; - eventName: "push" | "pull_request"; -} & Omit; + branch: 'main' | 'release' + eventName: 'push' | 'pull_request' +} & Omit export function createGitHubActionsFactory({ sshRepoUrl, }: { - sshRepoUrl: string; // for now just ssh based + sshRepoUrl: string // for now just ssh based }) { const actionsEnv = { eventName: Bun.env.GITHUB_EVENT_NAME, runNumber: Bun.env.GITHUB_RUN_NUMBER, - branch: Bun.env.GITHUB_REF?.split("/")?.[2], + branch: Bun.env.GITHUB_REF?.split('/')?.[2], actor: Bun.env.GITHUB_ACTOR, job: Bun.env.GITHUB_JOB, refType: Bun.env.GITHUB_REF_TYPE, - } as const; + } as const const gitCmd = async (commands: string[], log = true) => { - const commandArray = ["git", ...commands]; + const commandArray = ['git', ...commands] try { - if (log) ulog(`Running command: ${commandArray.join(" ")}`); + if (log) ulog(`Running command: ${commandArray.join(' ')}`) - const proc = Bun.spawnSync(commandArray); + const proc = Bun.spawnSync(commandArray) - const stdout = await new Response(proc.stdout).text(); + const stdout = await new Response(proc.stdout).text() if (stdout.trim()) { - ulog(stdout.trim()); + ulog(stdout.trim()) } - const stderr = await new Response(proc.stderr).text(); + const stderr = await new Response(proc.stderr).text() if (stderr.trim()) { - console.error(stderr.trim()); + console.error(stderr.trim()) } } catch (error) { - console.error(`Failed to run command: ${commandArray}:`, error); - exit(1); + console.error(`Failed to run command: ${commandArray}:`, error) + exit(1) } - }; + } const commitAndPush = async (commitMsg: string) => { - ulog("*** Running git commands ***"); + ulog('*** Running git commands ***') // Use the SSH to set the remote URL with authentication - await gitCmd(["remote", "set-url", "origin", sshRepoUrl]); - ulog("Configured GitHub User"); - await gitCmd(["config", "user.name"]); + await gitCmd(['remote', 'set-url', 'origin', sshRepoUrl]) + ulog('Configured GitHub User') + await gitCmd(['config', 'user.name']) - ulog("Configured GitHub Email"); - await gitCmd(["config", "user.email"]); + ulog('Configured GitHub Email') + await gitCmd(['config', 'user.email']) - await gitCmd(["add", "."]); - await gitCmd(["commit", "-m", `[skip ci] ${commitMsg}`]); - await gitCmd(["push", "origin", "HEAD:main"]); - }; + await gitCmd(['add', '.']) + await gitCmd(['commit', '-m', `[skip ci] ${commitMsg}`]) + await gitCmd(['push', 'origin', 'HEAD:main']) + } const setupGitConfig = async ( { @@ -89,31 +89,31 @@ export function createGitHubActionsFactory({ githubUsername, }: { // optional username and email for the git config, but defaults to the github-actions bot - githubUsername: string; - githubEmail: string; + githubUsername: string + githubEmail: string } = { - githubUsername: "github-actions[bot]", - githubEmail: "41898282+github-actions[bot]@users.noreply.github.com", + githubUsername: 'github-actions[bot]', + githubEmail: '41898282+github-actions[bot]@users.noreply.github.com', }, ) => { - ulog("*** Configuring Git ***"); + ulog('*** Configuring Git ***') // Configure user name and email - ulog("Configuring GitHub User"); - await gitCmd(["config", "--global", "user.name", githubUsername]); + ulog('Configuring GitHub User') + await gitCmd(['config', '--global', 'user.name', githubUsername]) - ulog("Configured Git User: ", await gitCmd(["config", "user.name"])); + ulog('Configured Git User: ', await gitCmd(['config', 'user.name'])) - ulog("Configuring GitHub Email"); - await gitCmd(["config", "--global", "user.email", githubEmail]); + ulog('Configuring GitHub Email') + await gitCmd(['config', '--global', 'user.email', githubEmail]) - ulog("Configured Git Email: ", await gitCmd(["config", "user.email"])); - }; + ulog('Configured Git Email: ', await gitCmd(['config', 'user.email'])) + } return { actionsEnv: actionsEnv as GHActions, gitCmd, commitAndPush, setupGitConfig, - }; + } } diff --git a/deploy/index.ts b/deploy/index.ts index fb630dc..7262f28 100644 --- a/deploy/index.ts +++ b/deploy/index.ts @@ -1 +1 @@ -export { createGitHubActionsFactory, logStdOutput } from "./github-actions"; +export { createGitHubActionsFactory, logStdOutput } from './github-actions' diff --git a/fetcher/create-fetch-factory.test.ts b/fetcher/create-fetch-factory.test.ts index 2b73a29..684d0d4 100644 --- a/fetcher/create-fetch-factory.test.ts +++ b/fetcher/create-fetch-factory.test.ts @@ -1,66 +1,66 @@ -import { describe, expect, test } from "bun:test"; -import { createFetchFactory } from "./create-fetch-factory"; +import { describe, expect, test } from 'bun:test' +import { createFetchFactory } from './create-fetch-factory' declare var global: { - fetch: any; -}; + fetch: any +} type FetchArgs = { - url: string; - options: RequestInit; -}; + url: string + options: RequestInit +} -describe("post method", () => { - test("should make a post request to the correct URL with JSON body", async () => { +describe('post method', () => { + test('should make a post request to the correct URL with JSON body', async () => { let fetchArgs: FetchArgs = { - url: "", + url: '', options: {}, - }; + } global.fetch = async (url: string, options: RequestInit) => { - fetchArgs = { url, options }; + fetchArgs = { url, options } return { ok: true, - json: async () => ({ message: "Success" }), - }; - }; + json: async () => ({ message: 'Success' }), + } + } - const headers = new Headers(); + const headers = new Headers() - headers.set("Authorization", "Bearer token"); + headers.set('Authorization', 'Bearer token') const fetchFactory = createFetchFactory({ - baseUrl: "https://api.example.com", + baseUrl: 'https://api.example.com', defaultHeaders: headers, config: { test: { - endpoint: "/test", - method: "POST", + endpoint: '/test', + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }, }, - }); - const postData = { key: "value" }; - await fetchFactory.post({ endpoint: "test", body: postData }); + }) + const postData = { key: 'value' } + await fetchFactory.post({ endpoint: 'test', body: postData }) - expect(fetchArgs.url).toBe("https://api.example.com/test"); - expect(fetchArgs.options.method).toBe("POST"); + expect(fetchArgs.url).toBe('https://api.example.com/test') + expect(fetchArgs.options.method).toBe('POST') // TODO: fix header tests // expect(fetchArgs.options.headers.get("Content-Type")).toBe( // "application/json" // ); // expect(fetchArgs.options.headers.get("Authorization")).toBe("Bearer token"); - expect(fetchArgs.options.body).toBe(JSON.stringify(postData)); - }); -}); + expect(fetchArgs.options.body).toBe(JSON.stringify(postData)) + }) +}) -describe("postForm method", () => { - test("should make a post request to the correct URL with FormData body", async () => { +describe('postForm method', () => { + test('should make a post request to the correct URL with FormData body', async () => { let fetchArgs: FetchArgs = { - url: "", + url: '', options: {}, - }; + } global.fetch = async (url: string, options: RequestInit) => { fetchArgs = { url, @@ -68,60 +68,60 @@ describe("postForm method", () => { ...options, headers: new Headers(options.headers), }, - }; + } return { ok: true, - json: async () => ({ message: "Success" }), - }; - }; + json: async () => ({ message: 'Success' }), + } + } const fetchFactory = createFetchFactory({ - baseUrl: "https://api.example.com", + baseUrl: 'https://api.example.com', config: { test: { - endpoint: "/test", - method: "POST", + endpoint: '/test', + method: 'POST', }, }, - }); - const formData = new FormData(); - formData.append("key", "value"); + }) + const formData = new FormData() + formData.append('key', 'value') // @ts-expect-error - await fetchFactory.postForm({ endpoint: "test", bodyData: formData }); + await fetchFactory.postForm({ endpoint: 'test', bodyData: formData }) - expect(fetchArgs.url).toBe("https://api.example.com/test"); - expect(fetchArgs.options.method).toBe("POST"); + expect(fetchArgs.url).toBe('https://api.example.com/test') + expect(fetchArgs.options.method).toBe('POST') // @ts-expect-error - expect(fetchArgs?.options?.headers?.get(["content-type"])).toContain("multipart/form-data"); - }); -}); + expect(fetchArgs?.options?.headers?.get(['content-type'])).toContain('multipart/form-data') + }) +}) -describe("delete method", () => { - test("should make a delete request to the correct URL", async () => { +describe('delete method', () => { + test('should make a delete request to the correct URL', async () => { let fetchArgs: FetchArgs = { - url: "", + url: '', options: {}, - }; + } global.fetch = async (url: string, options: RequestInit) => { - fetchArgs = { url, options }; + fetchArgs = { url, options } return { ok: true, - json: async () => ({ message: "Success" }), - }; - }; + json: async () => ({ message: 'Success' }), + } + } const fetchFactory = createFetchFactory({ - baseUrl: "https://api.example.com", + baseUrl: 'https://api.example.com', config: { test: { - endpoint: "/test", - method: "delete", + endpoint: '/test', + method: 'delete', }, }, - }); - await fetchFactory.delete({ endpoint: "test" }); + }) + await fetchFactory.delete({ endpoint: 'test' }) - expect(fetchArgs.url).toBe("https://api.example.com/test"); - expect(fetchArgs.options.method).toBe("DELETE"); - }); -}); + expect(fetchArgs.url).toBe('https://api.example.com/test') + expect(fetchArgs.options.method).toBe('DELETE') + }) +}) diff --git a/fetcher/create-fetch-factory.ts b/fetcher/create-fetch-factory.ts index f3d2f9a..7aad073 100644 --- a/fetcher/create-fetch-factory.ts +++ b/fetcher/create-fetch-factory.ts @@ -1,105 +1,105 @@ -declare var window: any; +declare var window: any declare var document: { - createElement: any; + createElement: any body: { - appendChild: any; - removeChild: any; - }; -}; -import { HTTPMethod } from "../utils/http-types"; -import { ExternalFetchConfig, MappedApiConfig, TypeMap } from "./fetch-types"; -import { computeHeaders, createEventStream, fetcher, fileDownload } from "./fetch-utils"; + appendChild: any + removeChild: any + } +} +import { HTTPMethod } from '../utils/http-types' +import { ExternalFetchConfig, MappedApiConfig, TypeMap } from './fetch-types' +import { computeHeaders, createEventStream, fetcher, fileDownload } from './fetch-utils' -export type FactoryMethods = keyof ReturnType; +export type FactoryMethods = keyof ReturnType export function createFetchFactory({ - baseUrl = "", + baseUrl = '', config, defaultHeaders, debug = false, }: { - baseUrl?: string; - debug?: boolean; - config: Record>; - defaultHeaders?: Headers; // Headers can be strings or functions returning strings + baseUrl?: string + debug?: boolean + config: Record> + defaultHeaders?: Headers // Headers can be strings or functions returning strings }) { return { fetcher: ( fetcherConfig: ExternalFetchConfig, - ): Promise => { - const headers = computeHeaders(defaultHeaders || {}, fetcherConfig.headers || {}); + ): Promise => { + const headers = computeHeaders(defaultHeaders || {}, fetcherConfig.headers || {}) return fetcher( { ...fetcherConfig, headers, - method: fetcherConfig.method || "GET", + method: fetcherConfig.method || 'GET', }, config, baseUrl, - ); + ) }, get: ( - fetcherConfig: ExternalFetchConfig, - ): Promise => { - const headers = computeHeaders(defaultHeaders || {}, fetcherConfig.headers || {}); + fetcherConfig: ExternalFetchConfig, + ): Promise => { + const headers = computeHeaders(defaultHeaders || {}, fetcherConfig.headers || {}) return fetcher( { ...fetcherConfig, headers, - method: "GET", + method: 'GET', }, config, baseUrl, - ); + ) }, post: ( - fetchConfig: ExternalFetchConfig & { - endpoint: Endpoint; + fetchConfig: ExternalFetchConfig & { + endpoint: Endpoint }, - ): Promise => { - const headers = computeHeaders(defaultHeaders || {}, fetchConfig.headers || {}); + ): Promise => { + const headers = computeHeaders(defaultHeaders || {}, fetchConfig.headers || {}) - return fetcher({ ...fetchConfig, headers, method: "POST" }, config, baseUrl); + return fetcher({ ...fetchConfig, headers, method: 'POST' }, config, baseUrl) }, postForm: ( - fetchConfig: ExternalFetchConfig & { - endpoint: Endpoint; - boundary?: string; + fetchConfig: ExternalFetchConfig & { + endpoint: Endpoint + boundary?: string }, - ): Promise => { + ): Promise => { const defaultContentType = fetchConfig.boundary ? `multipart/form-data; boundary=${fetchConfig.boundary}` - : "multipart/form-data"; + : 'multipart/form-data' const formHeaders = { - "Content-Type": defaultContentType, + 'Content-Type': defaultContentType, ...fetchConfig.headers, - }; - const headers = computeHeaders(defaultHeaders || {}, formHeaders || {}); + } + const headers = computeHeaders(defaultHeaders || {}, formHeaders || {}) return fetcher( { ...fetchConfig, headers, - method: "POST", + method: 'POST', }, config, baseUrl, - ); + ) }, delete: ( - fetchConfig: ExternalFetchConfig & { - endpoint: Endpoint; + fetchConfig: ExternalFetchConfig & { + endpoint: Endpoint }, - ): Promise => { - const headers = computeHeaders(defaultHeaders || {}, fetchConfig.headers || {}); - return fetcher({ ...fetchConfig, headers, method: "DELETE" }, config, baseUrl); + ): Promise => { + const headers = computeHeaders(defaultHeaders || {}, fetchConfig.headers || {}) + return fetcher({ ...fetchConfig, headers, method: 'DELETE' }, config, baseUrl) }, createEventStream, fileDownload, - }; + } } diff --git a/fetcher/fetch-types.ts b/fetcher/fetch-types.ts index 0663f42..6727ca7 100644 --- a/fetcher/fetch-types.ts +++ b/fetcher/fetch-types.ts @@ -1,38 +1,38 @@ -import { HTTPMethod, RouteMethods } from "../utils/http-types"; +import { HTTPMethod, RouteMethods } from '../utils/http-types' -export type EventHandlerMap = { [event: string]: (ev: MessageEvent) => void }; +export type EventHandlerMap = { [event: string]: (ev: MessageEvent) => void } export type APIConfig = { - method: RouteMethods; - endpoint: string; - response?: TRes; - params?: TParams; - body?: TBody; - headers?: THeaders; -}; + method: RouteMethods + endpoint: string + response?: TRes + params?: TParams + body?: TBody + headers?: THeaders +} export type TypeMap = { - [endpoint: string | number]: APIConfig; -}; + [endpoint: string | number]: APIConfig +} export type FileDownloadConfig = { - endpoint: string; - headers?: HeadersInit; - filename?: string; - params?: Record; -}; + endpoint: string + headers?: HeadersInit + filename?: string + params?: Record +} export type MappedApiConfig = APIConfig< - TMap[keyof TMap]["response"], - TMap[keyof TMap]["params"], - TMap[keyof TMap]["body"], + TMap[keyof TMap]['response'], + TMap[keyof TMap]['params'], + TMap[keyof TMap]['body'], HeadersInit ->; +> export type ExternalFetchConfig = Omit< MappedApiConfig, - "response" | "method" + 'response' | 'method' > & { - endpoint: Endpoint; - method?: Method; -}; + endpoint: Endpoint + method?: Method +} diff --git a/fetcher/fetch-utils.ts b/fetcher/fetch-utils.ts index d7ef2bd..55ceeb8 100644 --- a/fetcher/fetch-utils.ts +++ b/fetcher/fetch-utils.ts @@ -1,47 +1,47 @@ -import { EventHandlerMap, ExternalFetchConfig, FileDownloadConfig, MappedApiConfig, TypeMap } from "./fetch-types"; +import { EventHandlerMap, ExternalFetchConfig, FileDownloadConfig, MappedApiConfig, TypeMap } from './fetch-types' declare var window: { - fetch: any; -}; + fetch: any +} declare var document: { - createElement: any; - body: any; -}; + createElement: any + body: any +} export function appendURLParameters(url: string, params: Record = {}): string { - const urlWithParams = new URLSearchParams(); + const urlWithParams = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { - urlWithParams.append(key, value); - }); - return urlWithParams.toString() ? `${url}?${urlWithParams.toString()}` : url; + urlWithParams.append(key, value) + }) + return urlWithParams.toString() ? `${url}?${urlWithParams.toString()}` : url } async function handleResponse(response: Response): Promise { if (!response.ok) { - throw new Error(JSON.stringify(response)); + throw new Error(JSON.stringify(response)) } - return await response.json(); + return await response.json() } export function computeHeaders(defaultHeaders: HeadersInit, customHeaders?: HeadersInit): HeadersInit { - const resultHeaders = new Headers(defaultHeaders); + const resultHeaders = new Headers(defaultHeaders) if (customHeaders instanceof Headers) { for (const [key, value] of customHeaders.entries()) { - resultHeaders.set(key, value); + resultHeaders.set(key, value) } } else if (Array.isArray(customHeaders)) { customHeaders.forEach(([key, value]) => { - resultHeaders.set(key, value); - }); + resultHeaders.set(key, value) + }) } else { for (const [key, value] of Object.entries(customHeaders || {})) { - resultHeaders.set(key, value as string); + resultHeaders.set(key, value as string) } } - return resultHeaders; + return resultHeaders } export async function fetcher( @@ -49,58 +49,58 @@ export async function fetcher config: Record>, baseUrl: string, // computeHeadersFunction: (headers?: HeadersInit) => HeadersInit -): Promise { - const endpointConfig = config[fetcherConfig.endpoint]; - const finalUrl = appendURLParameters(baseUrl + endpointConfig.endpoint, fetcherConfig.params); +): Promise { + const endpointConfig = config[fetcherConfig.endpoint] + const finalUrl = appendURLParameters(baseUrl + endpointConfig.endpoint, fetcherConfig.params) - const method = endpointConfig.method; - let bodyData = ""; + const method = endpointConfig.method + let bodyData = '' if (fetcherConfig.body) { - bodyData = JSON.stringify(fetcherConfig.body); + bodyData = JSON.stringify(fetcherConfig.body) } const response = await fetch(finalUrl, { method: method.toUpperCase(), headers: fetcherConfig?.headers, body: bodyData, - }); + }) - return handleResponse(response); + return handleResponse(response) } export function fileDownload(config: FileDownloadConfig, baseUrl: string): void { - if (typeof window === "undefined") return; - const finalUrl = new URL(baseUrl + config.endpoint); + if (typeof window === 'undefined') return + const finalUrl = new URL(baseUrl + config.endpoint) if (config.params) { Object.keys(config.params).forEach((key) => { - finalUrl.searchParams.append(key, config.params![key]); - }); + finalUrl.searchParams.append(key, config.params![key]) + }) } - const a = document.createElement("a"); - a.href = finalUrl.toString(); - a.download = config.filename || ""; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + const a = document.createElement('a') + a.href = finalUrl.toString() + a.download = config.filename || '' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) } export function createEventStream(endpoint: string, eventHandlers: EventHandlerMap, baseUrl: string): EventSource { - const url = baseUrl + endpoint; - const es = new EventSource(url); + const url = baseUrl + endpoint + const es = new EventSource(url) es.onopen = (event) => { - console.info("Stream opened:", event); - }; + console.info('Stream opened:', event) + } es.onerror = (error) => { - console.error("Stream Error:", error); - }; + console.error('Stream Error:', error) + } for (const [event, handler] of Object.entries(eventHandlers)) { - es.addEventListener(event as string, handler); + es.addEventListener(event as string, handler) } - return es; + return es } diff --git a/fetcher/index.ts b/fetcher/index.ts index 88a0151..df883df 100644 --- a/fetcher/index.ts +++ b/fetcher/index.ts @@ -1 +1 @@ -export { createFetchFactory } from "./create-fetch-factory"; +export { createFetchFactory } from './create-fetch-factory' diff --git a/files-folders/file-editing-utils.test.ts b/files-folders/file-editing-utils.test.ts index fdd07a1..79c48e3 100644 --- a/files-folders/file-editing-utils.test.ts +++ b/files-folders/file-editing-utils.test.ts @@ -1,30 +1,30 @@ -import { describe, expect, it } from "bun:test"; -import { saveOrUpdateFile } from "./file-editing-utils"; +import { describe, expect, it } from 'bun:test' +import { saveOrUpdateFile } from './file-editing-utils' -import { readFileContent } from "./file-reading-utils"; -import { deletePath } from "./file-validation-utils"; +import { readFileContent } from './file-reading-utils' +import { deletePath } from './file-validation-utils' -const savePath = process.env.PWD + "/files-folders/test"; +const savePath = process.env.PWD + '/files-folders/test' -describe("saveResultToFile", () => { - it("should save and update content to file", async () => { - const saveFile = savePath + "/test.text"; +describe('saveResultToFile', () => { + it('should save and update content to file', async () => { + const saveFile = savePath + '/test.text' - const content = "Hello, world!"; + const content = 'Hello, world!' - await saveOrUpdateFile({ filePath: saveFile, content }); + await saveOrUpdateFile({ filePath: saveFile, content }) - const fileContent = await readFileContent(saveFile); + const fileContent = await readFileContent(saveFile) - expect(fileContent).toEqual(content); + expect(fileContent).toEqual(content) - const newFileContent = "Goodbye, world!"; - await saveOrUpdateFile({ filePath: saveFile, content: newFileContent }); + const newFileContent = 'Goodbye, world!' + await saveOrUpdateFile({ filePath: saveFile, content: newFileContent }) - const newReadFileContent = await readFileContent(saveFile); + const newReadFileContent = await readFileContent(saveFile) - expect(newReadFileContent).toEqual(newFileContent); + expect(newReadFileContent).toEqual(newFileContent) - await deletePath(saveFile); - }); -}); + await deletePath(saveFile) + }) +}) diff --git a/files-folders/file-editing-utils.ts b/files-folders/file-editing-utils.ts index fe3801b..46ca858 100644 --- a/files-folders/file-editing-utils.ts +++ b/files-folders/file-editing-utils.ts @@ -1,30 +1,30 @@ -import fsPromise from "fs/promises"; -import path from "path"; -import { directoryExists } from "./file-validation-utils"; +import fsPromise from 'fs/promises' +import path from 'path' +import { directoryExists } from './file-validation-utils' type SaveOptions = { - filePath: string; - content: string | object; - isJson?: boolean; -}; + filePath: string + content: string | object + isJson?: boolean +} export async function saveOrUpdateFile({ filePath, content, isJson = false }: SaveOptions): Promise { try { - const dirPath = path.dirname(filePath); - await directoryExists({ path: dirPath, createMissingDirs: true }); + const dirPath = path.dirname(filePath) + await directoryExists({ path: dirPath, createMissingDirs: true }) - let dataToWrite = content; + let dataToWrite = content if (isJson) { - dataToWrite = JSON.stringify(content, null, 2); + dataToWrite = JSON.stringify(content, null, 2) } - await fsPromise.writeFile(filePath, dataToWrite as string); + await fsPromise.writeFile(filePath, dataToWrite as string) } catch (err: any) { - throw new Error(`saveOrUpdateFile: Failed to save or update file at path ${filePath}: ${err?.message}`); + throw new Error(`saveOrUpdateFile: Failed to save or update file at path ${filePath}: ${err?.message}`) } } export async function updateMultipleFiles(filePaths: string[], content: string): Promise { - return Promise.all(filePaths.map((filePath) => saveOrUpdateFile({ filePath, content }))); + return Promise.all(filePaths.map((filePath) => saveOrUpdateFile({ filePath, content }))) } diff --git a/files-folders/file-extension-map.ts b/files-folders/file-extension-map.ts index 66699a3..d26c763 100644 --- a/files-folders/file-extension-map.ts +++ b/files-folders/file-extension-map.ts @@ -1,274 +1,274 @@ export type FileExtensionType = { - name: string; - description: string; - logoUrl: string; - mime: string; - encoding: string; -}; + name: string + description: string + logoUrl: string + mime: string + encoding: string +} const defaultExtensionInfo: FileExtensionType = { - name: "Unknown", - description: "Unknown", - logoUrl: "", - mime: "", - encoding: "", -}; + name: 'Unknown', + description: 'Unknown', + logoUrl: '', + mime: '', + encoding: '', +} type ExtensionMap = { - [key: string]: FileExtensionType; -}; + [key: string]: FileExtensionType +} export const fileExtensionMap = { ts: { - name: "TypeScript", - description: "TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.", - logoUrl: "https://raw.githubusercontent.com/github/explore/master/topics/typescript/typescript.png", - mime: "application/typescript", - encoding: "utf-8", + name: 'TypeScript', + description: 'TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.', + logoUrl: 'https://raw.githubusercontent.com/github/explore/master/topics/typescript/typescript.png', + mime: 'application/typescript', + encoding: 'utf-8', }, tsx: { - name: "TypeScript JSX", - description: "Used with React in TypeScript projects.", - logoUrl: "", - mime: "application/typescript", - encoding: "utf-8", + name: 'TypeScript JSX', + description: 'Used with React in TypeScript projects.', + logoUrl: '', + mime: 'application/typescript', + encoding: 'utf-8', }, js: { - name: "JavaScript", - description: "A lightweight, interpreted, or just-in-time compiled programming language.", - logoUrl: "", - mime: "application/javascript", - encoding: "utf-8", + name: 'JavaScript', + description: 'A lightweight, interpreted, or just-in-time compiled programming language.', + logoUrl: '', + mime: 'application/javascript', + encoding: 'utf-8', }, jsx: { - name: "JavaScript JSX", - description: "Used with React in JavaScript projects.", - logoUrl: "", - mime: "application/javascript", - encoding: "utf-8", + name: 'JavaScript JSX', + description: 'Used with React in JavaScript projects.', + logoUrl: '', + mime: 'application/javascript', + encoding: 'utf-8', }, json: { - name: "JSON", - description: "JavaScript Object Notation.", - logoUrl: "", - mime: "application/json", - encoding: "utf-8", + name: 'JSON', + description: 'JavaScript Object Notation.', + logoUrl: '', + mime: 'application/json', + encoding: 'utf-8', }, toml: { - name: "TOML", + name: 'TOML', description: "Tom's Obvious, Minimal Language.", - logoUrl: "", - mime: "application/toml", - encoding: "utf-8", + logoUrl: '', + mime: 'application/toml', + encoding: 'utf-8', }, md: { - name: "Markdown", - description: "A lightweight markup language.", - logoUrl: "", - mime: "text/markdown", - encoding: "utf-8", + name: 'Markdown', + description: 'A lightweight markup language.', + logoUrl: '', + mime: 'text/markdown', + encoding: 'utf-8', }, html: { - name: "HTML", - description: "Hypertext Markup Language.", - logoUrl: "", - mime: "text/html", - encoding: "utf-8", + name: 'HTML', + description: 'Hypertext Markup Language.', + logoUrl: '', + mime: 'text/html', + encoding: 'utf-8', }, css: { - name: "CSS", - description: "Cascading Style Sheets.", - logoUrl: "", - mime: "text/css", - encoding: "utf-8", + name: 'CSS', + description: 'Cascading Style Sheets.', + logoUrl: '', + mime: 'text/css', + encoding: 'utf-8', }, cpp: { - name: "C++", - description: "C++ programming language.", - logoUrl: "", - mime: "text/x-c++", - encoding: "utf-8", + name: 'C++', + description: 'C++ programming language.', + logoUrl: '', + mime: 'text/x-c++', + encoding: 'utf-8', }, rs: { - name: "Rust", - description: "A language empowering everyone to build reliable and efficient software.", - logoUrl: "", - mime: "text/rust", - encoding: "utf-8", + name: 'Rust', + description: 'A language empowering everyone to build reliable and efficient software.', + logoUrl: '', + mime: 'text/rust', + encoding: 'utf-8', }, c: { - name: "C", - description: "C programming language.", - logoUrl: "", - mime: "text/x-c", - encoding: "utf-8", + name: 'C', + description: 'C programming language.', + logoUrl: '', + mime: 'text/x-c', + encoding: 'utf-8', }, py: { - name: "Python", - description: "A high-level, interpreted scripting language.", - logoUrl: "", - mime: "text/x-python", - encoding: "utf-8", + name: 'Python', + description: 'A high-level, interpreted scripting language.', + logoUrl: '', + mime: 'text/x-python', + encoding: 'utf-8', }, txt: { - name: "Text", - description: "Plain text file.", - logoUrl: "", - mime: "text/plain", - encoding: "utf-8", + name: 'Text', + description: 'Plain text file.', + logoUrl: '', + mime: 'text/plain', + encoding: 'utf-8', }, jpeg: { - name: "JPEG", - description: "JPEG image format.", - logoUrl: "", - mime: "image/jpeg", - encoding: "", + name: 'JPEG', + description: 'JPEG image format.', + logoUrl: '', + mime: 'image/jpeg', + encoding: '', }, jpg: { - name: "JPG", - description: "JPG image format.", - logoUrl: "", - mime: "image/jpeg", - encoding: "", + name: 'JPG', + description: 'JPG image format.', + logoUrl: '', + mime: 'image/jpeg', + encoding: '', }, png: { - name: "PNG", - description: "Portable Network Graphics.", - logoUrl: "", - mime: "image/png", - encoding: "", + name: 'PNG', + description: 'Portable Network Graphics.', + logoUrl: '', + mime: 'image/png', + encoding: '', }, zip: { - name: "ZIP", - description: "ZIP file format.", - logoUrl: "", - mime: "application/zip", - encoding: "", + name: 'ZIP', + description: 'ZIP file format.', + logoUrl: '', + mime: 'application/zip', + encoding: '', }, gzip: { - name: "GZIP", - description: "GNU zip, a file compression format.", - logoUrl: "", - mime: "application/gzip", - encoding: "", + name: 'GZIP', + description: 'GNU zip, a file compression format.', + logoUrl: '', + mime: 'application/gzip', + encoding: '', }, xml: { - name: "XML", - description: "eXtensible Markup Language.", - logoUrl: "", - mime: "application/xml", - encoding: "utf-8", + name: 'XML', + description: 'eXtensible Markup Language.', + logoUrl: '', + mime: 'application/xml', + encoding: 'utf-8', }, svg: { - name: "SVG", - description: "Scalable Vector Graphics.", - logoUrl: "", - mime: "image/svg+xml", - encoding: "utf-8", + name: 'SVG', + description: 'Scalable Vector Graphics.', + logoUrl: '', + mime: 'image/svg+xml', + encoding: 'utf-8', }, gif: { - name: "GIF", - description: "Graphics Interchange Format.", - logoUrl: "", - mime: "image/gif", - encoding: "", + name: 'GIF', + description: 'Graphics Interchange Format.', + logoUrl: '', + mime: 'image/gif', + encoding: '', }, pdf: { - name: "PDF", - description: "Portable Document Format.", - logoUrl: "", - mime: "application/pdf", - encoding: "", + name: 'PDF', + description: 'Portable Document Format.', + logoUrl: '', + mime: 'application/pdf', + encoding: '', }, mp3: { - name: "MP3", - description: "Audio file format.", - logoUrl: "", - mime: "audio/mpeg", - encoding: "", + name: 'MP3', + description: 'Audio file format.', + logoUrl: '', + mime: 'audio/mpeg', + encoding: '', }, mp4: { - name: "MP4", - description: "Video file format.", - logoUrl: "", - mime: "video/mp4", - encoding: "", + name: 'MP4', + description: 'Video file format.', + logoUrl: '', + mime: 'video/mp4', + encoding: '', }, doc: { - name: "DOC", - description: "Microsoft Word document.", - logoUrl: "", - mime: "application/msword", - encoding: "", + name: 'DOC', + description: 'Microsoft Word document.', + logoUrl: '', + mime: 'application/msword', + encoding: '', }, docx: { - name: "DOCX", - description: "Microsoft Word Open XML document.", - logoUrl: "", - mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - encoding: "", + name: 'DOCX', + description: 'Microsoft Word Open XML document.', + logoUrl: '', + mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + encoding: '', }, xls: { - name: "XLS", - description: "Microsoft Excel spreadsheet.", - logoUrl: "", - mime: "application/vnd.ms-excel", - encoding: "", + name: 'XLS', + description: 'Microsoft Excel spreadsheet.', + logoUrl: '', + mime: 'application/vnd.ms-excel', + encoding: '', }, xlsx: { - name: "XLSX", - description: "Microsoft Excel Open XML spreadsheet.", - logoUrl: "", - mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - encoding: "", + name: 'XLSX', + description: 'Microsoft Excel Open XML spreadsheet.', + logoUrl: '', + mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + encoding: '', }, ppt: { - name: "PPT", - description: "Microsoft PowerPoint presentation.", - logoUrl: "", - mime: "application/vnd.ms-powerpoint", - encoding: "", + name: 'PPT', + description: 'Microsoft PowerPoint presentation.', + logoUrl: '', + mime: 'application/vnd.ms-powerpoint', + encoding: '', }, pptx: { - name: "PPTX", - description: "Microsoft PowerPoint Open XML presentation.", - logoUrl: "", - mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation", - encoding: "", + name: 'PPTX', + description: 'Microsoft PowerPoint Open XML presentation.', + logoUrl: '', + mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + encoding: '', }, odt: { - name: "ODT", - description: "OpenDocument Text Document.", - logoUrl: "", - mime: "application/vnd.oasis.opendocument.text", - encoding: "", + name: 'ODT', + description: 'OpenDocument Text Document.', + logoUrl: '', + mime: 'application/vnd.oasis.opendocument.text', + encoding: '', }, ods: { - name: "ODS", - description: "OpenDocument Spreadsheet Document.", - logoUrl: "", - mime: "application/vnd.oasis.opendocument.spreadsheet", - encoding: "", + name: 'ODS', + description: 'OpenDocument Spreadsheet Document.', + logoUrl: '', + mime: 'application/vnd.oasis.opendocument.spreadsheet', + encoding: '', }, odp: { - name: "ODP", - description: "OpenDocument Presentation Document.", - logoUrl: "", - mime: "application/vnd.oasis.opendocument.presentation", - encoding: "", + name: 'ODP', + description: 'OpenDocument Presentation Document.', + logoUrl: '', + mime: 'application/vnd.oasis.opendocument.presentation', + encoding: '', }, -} satisfies ExtensionMap; +} satisfies ExtensionMap -export type ExtensionMapKeys = keyof typeof fileExtensionMap; +export type ExtensionMapKeys = keyof typeof fileExtensionMap export const fullMimeForExtension = (extension: ExtensionMapKeys) => { - let extensionType = fileExtensionMap[extension] ?? defaultExtensionInfo; + let extensionType = fileExtensionMap[extension] ?? defaultExtensionInfo // If there's no encoding specified, return just the MIME type. if (!extensionType.encoding) { - return extensionType.mime; + return extensionType.mime } // Otherwise, return MIME type with encoding. - return `${extensionType.mime};charset=${extensionType.encoding}`; -}; + return `${extensionType.mime};charset=${extensionType.encoding}` +} diff --git a/files-folders/file-factory.ts b/files-folders/file-factory.ts index de0dae2..0c4f74f 100644 --- a/files-folders/file-factory.ts +++ b/files-folders/file-factory.ts @@ -1,8 +1,8 @@ -import { saveOrUpdateFile } from "./file-editing-utils"; -import { getFullPath } from "./file-path-utils"; -import { listFilesAndFolderInPath, readJson, readTextFromMultipleFiles } from "./file-reading-utils"; -import { searchDirForFileName } from "./file-search-utils"; -import { deletePath, directoryExists, fileExists } from "./file-validation-utils"; +import { saveOrUpdateFile } from './file-editing-utils' +import { getFullPath } from './file-path-utils' +import { listFilesAndFolderInPath, readJson, readTextFromMultipleFiles } from './file-reading-utils' +import { searchDirForFileName } from './file-search-utils' +import { deletePath, directoryExists, fileExists } from './file-validation-utils' export function fileFactory({ baseDirectory }: { baseDirectory: string }) { return { @@ -11,45 +11,45 @@ export function fileFactory({ baseDirectory }: { baseDirectory: string }) { const fullPath = await getFullPath({ baseDir: baseDirectory, filePath: relativePath, - }); + }) return saveOrUpdateFile({ filePath: fullPath, content: data, - }); - }); - return Promise.all(promises); + }) + }) + return Promise.all(promises) }, readTextFromMultipleFiles: (relativePaths: string[]) => { const fullPaths = relativePaths.map((relativePath) => getFullPath({ baseDir: baseDirectory, filePath: relativePath }), - ); - return readTextFromMultipleFiles(fullPaths); + ) + return readTextFromMultipleFiles(fullPaths) }, searchDirForFile: (fileName: string, relativePath?: string) => { - let fullPath = baseDirectory; + let fullPath = baseDirectory if (relativePath) { fullPath = getFullPath({ baseDir: baseDirectory, filePath: relativePath, - }); + }) } - return searchDirForFileName(fullPath, fileName); + return searchDirForFileName(fullPath, fileName) }, fileExists: (relativePath: string) => { const fullPath = getFullPath({ baseDir: baseDirectory, filePath: relativePath, - }); + }) - return fileExists(fullPath); + return fileExists(fullPath) }, deleteFile: (relativePath: string) => { - return deletePath(getFullPath({ baseDir: baseDirectory, filePath: relativePath })); + return deletePath(getFullPath({ baseDir: baseDirectory, filePath: relativePath })) }, readJson: (relativePath: string) => { - return readJson(getFullPath({ baseDir: baseDirectory, filePath: relativePath })); + return readJson(getFullPath({ baseDir: baseDirectory, filePath: relativePath })) }, saveJson: (content: object, relativePath: string) => { return saveOrUpdateFile({ @@ -59,7 +59,7 @@ export function fileFactory({ baseDirectory }: { baseDirectory: string }) { }), content, isJson: true, - }); + }) }, directoryExists, createFile: (content: string, relativePath: string) => { @@ -69,16 +69,16 @@ export function fileFactory({ baseDirectory }: { baseDirectory: string }) { filePath: relativePath, }), content, - }); + }) }, listFilesAndFolderInPath: (dirPath: string) => { const fullPath = getFullPath({ baseDir: baseDirectory, filePath: dirPath, - }); + }) - return listFilesAndFolderInPath(fullPath); + return listFilesAndFolderInPath(fullPath) }, - }; + } } diff --git a/files-folders/file-path-utils.test.ts b/files-folders/file-path-utils.test.ts index 7affbbe..865accf 100644 --- a/files-folders/file-path-utils.test.ts +++ b/files-folders/file-path-utils.test.ts @@ -1,11 +1,11 @@ -import { describe, expect, it } from "bun:test"; -import { getFullPath } from "./file-path-utils"; +import { describe, expect, it } from 'bun:test' +import { getFullPath } from './file-path-utils' -describe("getFullPath", () => { - it("should return an absolute path when given an absolute path", () => { - const baseDirectory = "/Users/brandon"; - const filePath = "/Users/brandon/Documents/file.txt"; - const result = getFullPath({ baseDir: baseDirectory, filePath }); - expect(result).toEqual(filePath); - }); -}); +describe('getFullPath', () => { + it('should return an absolute path when given an absolute path', () => { + const baseDirectory = '/Users/brandon' + const filePath = '/Users/brandon/Documents/file.txt' + const result = getFullPath({ baseDir: baseDirectory, filePath }) + expect(result).toEqual(filePath) + }) +}) diff --git a/files-folders/file-path-utils.ts b/files-folders/file-path-utils.ts index 45f5060..f38b109 100644 --- a/files-folders/file-path-utils.ts +++ b/files-folders/file-path-utils.ts @@ -1,21 +1,21 @@ -import path from "path"; +import path from 'path' export function getFullPath({ baseDir, filePath, isAbsolute = false, }: { - baseDir: string; - filePath: string; - isAbsolute?: boolean; + baseDir: string + filePath: string + isAbsolute?: boolean }): string { try { if (isAbsolute || path.isAbsolute(filePath)) { - return filePath; + return filePath } else { - return path.resolve(baseDir, filePath); + return path.resolve(baseDir, filePath) } } catch (e) { - throw e; + throw e } } diff --git a/files-folders/file-reading-utils.ts b/files-folders/file-reading-utils.ts index cdedb49..96323bc 100644 --- a/files-folders/file-reading-utils.ts +++ b/files-folders/file-reading-utils.ts @@ -1,27 +1,27 @@ -import fsPromise from "fs/promises"; -import path from "path"; -import { FileDirInfo } from "./file-types"; +import fsPromise from 'fs/promises' +import path from 'path' +import { FileDirInfo } from './file-types' export async function readFileContent(filePath: string): Promise { try { - return Bun.file(filePath).text(); + return Bun.file(filePath).text() } catch (error) { - throw error; + throw error } } export async function listFilesAndFolderInPath(fullPath: string): Promise { const entries = await fsPromise.readdir(fullPath, { withFileTypes: true, - }); + }) const filesAndFolders = entries.map((entry) => { - const name = entry.name; - const entryFullPath = path.join(fullPath, name); - const bunFileInfo = Bun.file(entryFullPath); - const type = entry.isFile() ? "file" : entry.isDirectory() ? "directory" : "other"; - let fileExtension = "folder"; - if (type === "file") { - const regex = /(?:\.([^.]+))?$/; - fileExtension = regex.exec(entryFullPath)?.[1] ?? "file"; + const name = entry.name + const entryFullPath = path.join(fullPath, name) + const bunFileInfo = Bun.file(entryFullPath) + const type = entry.isFile() ? 'file' : entry.isDirectory() ? 'directory' : 'other' + let fileExtension = 'folder' + if (type === 'file') { + const regex = /(?:\.([^.]+))?$/ + fileExtension = regex.exec(entryFullPath)?.[1] ?? 'file' } return { type, @@ -29,19 +29,19 @@ export async function listFilesAndFolderInPath(fullPath: string): Promise { const promises = filePaths.map(async (filePath) => { - return readFileContent(filePath); - }); - return Promise.all(promises); + return readFileContent(filePath) + }) + return Promise.all(promises) } export async function readJson(filePath: string): Promise { - const rawText = await Bun.file(filePath).text(); - return JSON.parse(rawText); + const rawText = await Bun.file(filePath).text() + return JSON.parse(rawText) } diff --git a/files-folders/file-search-utils.test.ts b/files-folders/file-search-utils.test.ts index 6bb05a6..eb810b2 100644 --- a/files-folders/file-search-utils.test.ts +++ b/files-folders/file-search-utils.test.ts @@ -1,36 +1,36 @@ -import { describe, expect, it } from "bun:test"; -import { saveOrUpdateFile } from "./file-editing-utils"; -import { recursiveDirSearch, searchDirForFileName } from "./file-search-utils"; -import { deletePath } from "./file-validation-utils"; +import { describe, expect, it } from 'bun:test' +import { saveOrUpdateFile } from './file-editing-utils' +import { recursiveDirSearch, searchDirForFileName } from './file-search-utils' +import { deletePath } from './file-validation-utils' -const testDir = process.env.PWD + "/files-folders/test"; -const nestedDir = testDir + "/nestedDir"; +const testDir = process.env.PWD + '/files-folders/test' +const nestedDir = testDir + '/nestedDir' -describe("Search Utilities", () => { - it("should search directory and return matched files", async () => { +describe('Search Utilities', () => { + it('should search directory and return matched files', async () => { // Setup - const testFile1 = testDir + "/test1.txt"; - const testFile2 = nestedDir + "/test2.txt"; + const testFile1 = testDir + '/test1.txt' + const testFile2 = nestedDir + '/test2.txt' - await saveOrUpdateFile({ filePath: testFile1, content: "Hello, world!" }); - await saveOrUpdateFile({ filePath: testFile2, content: "Goodbye, world!" }); + await saveOrUpdateFile({ filePath: testFile1, content: 'Hello, world!' }) + await saveOrUpdateFile({ filePath: testFile2, content: 'Goodbye, world!' }) // Test recursiveDirSearch const results = await recursiveDirSearch({ directory: testDir, - searchString: "test", - }); + searchString: 'test', + }) - expect(results.length).toEqual(2); - expect(results.map((r) => r.fullPath)).toContain(testFile1); - expect(results.map((r) => r.fullPath)).toContain(testFile2); + expect(results.length).toEqual(2) + expect(results.map((r) => r.fullPath)).toContain(testFile1) + expect(results.map((r) => r.fullPath)).toContain(testFile2) // Test searchDirForFile - const foundPath = await searchDirForFileName(testDir, "test2.txt"); - expect(foundPath).toEqual(testFile2); + const foundPath = await searchDirForFileName(testDir, 'test2.txt') + expect(foundPath).toEqual(testFile2) // Cleanup - await deletePath(testFile1); - await deletePath(testFile2); - }); -}); + await deletePath(testFile1) + await deletePath(testFile2) + }) +}) diff --git a/files-folders/file-search-utils.ts b/files-folders/file-search-utils.ts index e527f52..71f25f4 100644 --- a/files-folders/file-search-utils.ts +++ b/files-folders/file-search-utils.ts @@ -1,48 +1,48 @@ -import fsPromise from "fs/promises"; +import fsPromise from 'fs/promises' -import path from "path"; -import { FileDirInfo, FileWithContent } from "./file-types"; +import path from 'path' +import { FileDirInfo, FileWithContent } from './file-types' export type FileSearchConfig = { - [key: string]: boolean; -}; + [key: string]: boolean +} export const defaultDirIgnore = { node_modules: true, - ".git": true, - ".vscode": true, - ".idea": true, + '.git': true, + '.vscode': true, + '.idea': true, cache: true, -}; +} export const defaultExtIgnore = { log: true, localstorage: true, DS_Store: true, testing: true, -}; +} export const readFileInfo = (filePath: string): FileDirInfo => { - const bunFileInfo = Bun.file(filePath); + const bunFileInfo = Bun.file(filePath) - const extension = path.extname(bunFileInfo?.name || "").slice(1); + const extension = path.extname(bunFileInfo?.name || '').slice(1) return { - type: "file", - name: filePath.split("/").pop() || "", + type: 'file', + name: filePath.split('/').pop() || '', fullPath: filePath, size: bunFileInfo.size, extension, - }; -}; + } +} export type FileSearchParams = { - directory: string; - searchString: string; - ignoreDirectories?: object; - ignoreFileTypes?: object; - searchContent?: T; -}; + directory: string + searchString: string + ignoreDirectories?: object + ignoreFileTypes?: object + searchContent?: T +} export const recursiveDirSearch = async ( params: FileSearchParams, @@ -53,68 +53,68 @@ export const recursiveDirSearch = async ( ignoreDirectories = defaultDirIgnore, ignoreFileTypes = defaultExtIgnore, searchContent = false, - } = params; + } = params - const results = []; - const entries = await fsPromise.readdir(directory, { withFileTypes: true }); + const results = [] + const entries = await fsPromise.readdir(directory, { withFileTypes: true }) for (const entry of entries) { - const fullPath = path.join(directory, entry.name); + const fullPath = path.join(directory, entry.name) if (entry.isDirectory() && !ignoreDirectories[entry.name as keyof typeof ignoreDirectories]) { const nestedResults = await recursiveDirSearch({ ...params, directory: fullPath, - }); - results.push(...nestedResults); + }) + results.push(...nestedResults) } else if (entry.isFile()) { - const extension = path.extname(entry.name).slice(1); + const extension = path.extname(entry.name).slice(1) - if (ignoreFileTypes[extension as keyof typeof ignoreFileTypes]) continue; + if (ignoreFileTypes[extension as keyof typeof ignoreFileTypes]) continue if (searchContent) { - const content = await Bun.file(fullPath).text(); + const content = await Bun.file(fullPath).text() if (content.includes(searchString)) { - const bunFileInfo = Bun.file(fullPath); + const bunFileInfo = Bun.file(fullPath) results.push({ - type: "file", + type: 'file', name: entry.name, fullPath, size: bunFileInfo.size, extension, content, - }); + }) } } else if (entry.name.includes(searchString)) { - const bunFileInfo = Bun.file(fullPath); + const bunFileInfo = Bun.file(fullPath) results.push({ - type: "file", + type: 'file', name: entry.name, fullPath, size: bunFileInfo.size, extension, - }); + }) } } } - return results as T extends true ? FileWithContent[] : FileDirInfo[]; -}; + return results as T extends true ? FileWithContent[] : FileDirInfo[] +} export async function searchDirForFileName(startingDir: string, fileName: string): Promise { async function searchFn(dir: string): Promise { - const entries = await fsPromise.readdir(dir, { withFileTypes: true }); + const entries = await fsPromise.readdir(dir, { withFileTypes: true }) for (let entry of entries) { - const fullPath = path.join(dir, entry.name); + const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { if (await searchFn(fullPath)) { - return fullPath + "/" + fileName; + return fullPath + '/' + fileName } } else if (entry.name === fileName) { - return fullPath + "/" + fileName; + return fullPath + '/' + fileName } } - return null; + return null } - return searchFn(startingDir); + return searchFn(startingDir) } diff --git a/files-folders/file-types.ts b/files-folders/file-types.ts index 35b4ea3..3fc6e4a 100644 --- a/files-folders/file-types.ts +++ b/files-folders/file-types.ts @@ -1,12 +1,12 @@ export type FileDirInfo = { - type: "file" | "directory" | "other"; - name: string; - fullPath: string; - size: number; - extension: string; -}; + type: 'file' | 'directory' | 'other' + name: string + fullPath: string + size: number + extension: string +} export type FileWithContent = FileDirInfo & { - content: string; - type: "file"; -}; + content: string + type: 'file' +} diff --git a/files-folders/file-validation-utils.test.ts b/files-folders/file-validation-utils.test.ts index d0be3ef..501f052 100644 --- a/files-folders/file-validation-utils.test.ts +++ b/files-folders/file-validation-utils.test.ts @@ -1,56 +1,56 @@ -import { describe, expect, it } from "bun:test"; -import { saveOrUpdateFile } from "./file-editing-utils"; -import { getFullPath } from "./file-path-utils"; -import { deletePath, directoryExists, fileExists } from "./file-validation-utils"; - -const savePath = process.env.PWD + "/files-folders/test"; -const saveFile = savePath + "/test.txt"; - -describe("fileExists", () => { - it("should return true and false", async () => { - await saveOrUpdateFile({ filePath: saveFile, content: "test test test" }); - expect(await fileExists(saveFile)).toBe(true); - await deletePath(saveFile); - expect(await fileExists(saveFile)).toBe(false); - }); - - it("should return false if file does not exist", async () => { - const filePath = "/path/to/fake-file.txt"; - - const result = await fileExists(filePath); - expect(result).toBe(false); - }); -}); - -describe("deleteFile", () => { - it("should delete file", async () => { - await saveOrUpdateFile({ filePath: saveFile, content: "test test test" }); - expect(await fileExists(saveFile)).toBe(true); - await deletePath(saveFile); - expect(await fileExists(saveFile)).toBe(false); - }); -}); - -describe("directoryExists", () => { - it("should create directory if it does not exist", async () => { - expect(await directoryExists({ path: savePath, createMissingDirs: true })).toBe(true); - }); - - it("should not create directory if it already exists", async () => { - const directoryPath = "files-folders/fake"; +import { describe, expect, it } from 'bun:test' +import { saveOrUpdateFile } from './file-editing-utils' +import { getFullPath } from './file-path-utils' +import { deletePath, directoryExists, fileExists } from './file-validation-utils' + +const savePath = process.env.PWD + '/files-folders/test' +const saveFile = savePath + '/test.txt' + +describe('fileExists', () => { + it('should return true and false', async () => { + await saveOrUpdateFile({ filePath: saveFile, content: 'test test test' }) + expect(await fileExists(saveFile)).toBe(true) + await deletePath(saveFile) + expect(await fileExists(saveFile)).toBe(false) + }) + + it('should return false if file does not exist', async () => { + const filePath = '/path/to/fake-file.txt' + + const result = await fileExists(filePath) + expect(result).toBe(false) + }) +}) + +describe('deleteFile', () => { + it('should delete file', async () => { + await saveOrUpdateFile({ filePath: saveFile, content: 'test test test' }) + expect(await fileExists(saveFile)).toBe(true) + await deletePath(saveFile) + expect(await fileExists(saveFile)).toBe(false) + }) +}) + +describe('directoryExists', () => { + it('should create directory if it does not exist', async () => { + expect(await directoryExists({ path: savePath, createMissingDirs: true })).toBe(true) + }) + + it('should not create directory if it already exists', async () => { + const directoryPath = 'files-folders/fake' const fullPath = getFullPath({ - baseDir: ".", + baseDir: '.', filePath: directoryPath, - }); + }) - Bun.spawnSync({ cmd: ["rm", "-rf", directoryPath] }); + Bun.spawnSync({ cmd: ['rm', '-rf', directoryPath] }) - expect(await directoryExists({ path: fullPath })).toBe(false); + expect(await directoryExists({ path: fullPath })).toBe(false) - await directoryExists({ path: fullPath, createMissingDirs: true }); + await directoryExists({ path: fullPath, createMissingDirs: true }) - expect(await directoryExists({ path: fullPath })).toBe(true); + expect(await directoryExists({ path: fullPath })).toBe(true) - Bun.spawnSync({ cmd: ["rm", "-rf", directoryPath] }); - }); -}); + Bun.spawnSync({ cmd: ['rm', '-rf', directoryPath] }) + }) +}) diff --git a/files-folders/file-validation-utils.ts b/files-folders/file-validation-utils.ts index 5630b87..91e2813 100644 --- a/files-folders/file-validation-utils.ts +++ b/files-folders/file-validation-utils.ts @@ -1,62 +1,62 @@ -import fsPromise from "fs/promises"; -import path from "path"; +import fsPromise from 'fs/promises' +import path from 'path' export async function fileExists(filePath: string): Promise { try { - return await Bun.file(filePath).exists(); + return await Bun.file(filePath).exists() } catch (error) { - return false; + return false } } export async function deletePath(targetPath: string) { - let dirData; + let dirData try { - dirData = await fsPromise.stat(targetPath); + dirData = await fsPromise.stat(targetPath) } catch (error) { - console.error(error); + console.error(error) } if (dirData && dirData.isDirectory()) { for (const file of await fsPromise.readdir(targetPath)) { - await deletePath(path.join(targetPath, file)); + await deletePath(path.join(targetPath, file)) } - await fsPromise.rm(targetPath); + await fsPromise.rm(targetPath) } else if (dirData && dirData.isFile()) { // delete file - await fsPromise.unlink(targetPath); + await fsPromise.unlink(targetPath) } else { - console.info(`deletePath: Path does not exist: ${targetPath}`); + console.info(`deletePath: Path does not exist: ${targetPath}`) } } export async function directoryExists({ createMissingDirs = false, path, }: { - path: string; - createMissingDirs?: boolean; + path: string + createMissingDirs?: boolean }): Promise { try { - const stat = await fsPromise.stat(path); - return stat.isDirectory(); + const stat = await fsPromise.stat(path) + return stat.isDirectory() } catch (error: any) { - if (error?.code === "ENOENT") { - if (!createMissingDirs && typeof createMissingDirs !== "boolean") { + if (error?.code === 'ENOENT') { + if (!createMissingDirs && typeof createMissingDirs !== 'boolean') { console.error( `directoryExists: Directory does not exist: ${path}, but createMissingDirs is false, set to true to create the directory.`, - ); - return false; + ) + return false } if (createMissingDirs) { - await fsPromise.mkdir(path, { recursive: true }); - console.info(`directoryExists: Created directory: ${path}`); - return true; + await fsPromise.mkdir(path, { recursive: true }) + console.info(`directoryExists: Created directory: ${path}`) + return true } - return false; + return false } else { - throw error; + throw error } } } diff --git a/files-folders/files-folder.test.ts b/files-folders/files-folder.test.ts index f764009..da4de1b 100644 --- a/files-folders/files-folder.test.ts +++ b/files-folders/files-folder.test.ts @@ -1,199 +1,199 @@ -import fsPromise from "fs/promises"; +import fsPromise from 'fs/promises' -import { afterEach, describe, expect, it, jest, spyOn } from "bun:test"; -import path from "path"; -import { saveOrUpdateFile } from "./file-editing-utils"; -import { fileFactory } from "./file-factory"; +import { afterEach, describe, expect, it, jest, spyOn } from 'bun:test' +import path from 'path' +import { saveOrUpdateFile } from './file-editing-utils' +import { fileFactory } from './file-factory' const getTestFactory = () => { return fileFactory({ - baseDirectory: ".", - }); -}; + baseDirectory: '.', + }) +} // Tests that saveResultToFile saves the given content to a file at the specified file path -it("saves content to file", async () => { - const filePath = "./test/test-utils/test.txt"; - const content = "Hello, world!"; - await saveOrUpdateFile({ filePath, content }); - const fileContent = await fsPromise.readFile(filePath, "utf-8"); - expect(fileContent).toEqual(content); +it('saves content to file', async () => { + const filePath = './test/test-utils/test.txt' + const content = 'Hello, world!' + await saveOrUpdateFile({ filePath, content }) + const fileContent = await fsPromise.readFile(filePath, 'utf-8') + expect(fileContent).toEqual(content) - const factory = getTestFactory(); + const factory = getTestFactory() // delete file - await factory.deleteFile(filePath); + await factory.deleteFile(filePath) // expect file to no longer be there try { - await fsPromise.access(filePath, fsPromise.constants.F_OK); - throw new Error("File should not exist"); + await fsPromise.access(filePath, fsPromise.constants.F_OK) + throw new Error('File should not exist') } catch (e: any) { - if (e.code === "ENOENT") { + if (e.code === 'ENOENT') { // This is expected as the file should not exist } else { // If the error is something else, re-throw it - throw e; + throw e } } -}); +}) -describe("fileFactory", async () => { +describe('fileFactory', async () => { afterEach(() => { - jest.restoreAllMocks(); - }); + jest.restoreAllMocks() + }) // Test that readFilesRawText reads the content of multiple files correctly - it("reads multiple files", async () => { - const factory = getTestFactory(); + it('reads multiple files', async () => { + const factory = getTestFactory() - const filePaths = ["./files-folders/test/test1.txt", "./files-folders/test/test2.txt"]; + const filePaths = ['./files-folders/test/test1.txt', './files-folders/test/test2.txt'] - const contents = ["Hello, world!", "Goodbye, world!"]; + const contents = ['Hello, world!', 'Goodbye, world!'] for (let i = 0; i < filePaths.length; i++) { - await factory.createFile(contents[i], filePaths[i]); + await factory.createFile(contents[i], filePaths[i]) } - const fileContents = await factory.readTextFromMultipleFiles(filePaths); - expect(fileContents).toEqual(contents); + const fileContents = await factory.readTextFromMultipleFiles(filePaths) + expect(fileContents).toEqual(contents) for (let filePath of filePaths) { - await factory.deleteFile(filePath); + await factory.deleteFile(filePath) } - }); + }) // Test that updateFiles updates the content of multiple files correctly - it("updates multiple files", async () => { - const factory = getTestFactory(); - const filePaths = ["./test/test-utils/test1.txt", "./test/test-utils/test2.txt"]; - const initialContents = ["Hello, world!", "Goodbye, world!"]; - const newContents = "Updated content"; + it('updates multiple files', async () => { + const factory = getTestFactory() + const filePaths = ['./test/test-utils/test1.txt', './test/test-utils/test2.txt'] + const initialContents = ['Hello, world!', 'Goodbye, world!'] + const newContents = 'Updated content' for (let i = 0; i < filePaths.length; i++) { await saveOrUpdateFile({ filePath: filePaths[i], content: initialContents[i], - }); + }) } - await factory.updateFiles(filePaths, newContents); + await factory.updateFiles(filePaths, newContents) for (let filePath of filePaths) { - const fileContent = await fsPromise.readFile(filePath, "utf-8"); - expect(fileContent).toEqual(newContents); - await factory.deleteFile(filePath); + const fileContent = await fsPromise.readFile(filePath, 'utf-8') + expect(fileContent).toEqual(newContents) + await factory.deleteFile(filePath) } - }); + }) // Test that searchDirectory can correctly find a file in the specified directory - it("recursively searches directory for file", async () => { - const factory = getTestFactory(); - const fileName = "test.txt"; - const filePath = `./test/test-utils/${fileName}`; - const content = "Hello, world!"; - await saveOrUpdateFile({ filePath, content }); - const fileExists = await factory.searchDirForFile(fileName); - expect(fileExists).toEqual("test/test.txt"); - await factory.deleteFile(filePath); - }); + it('recursively searches directory for file', async () => { + const factory = getTestFactory() + const fileName = 'test.txt' + const filePath = `./test/test-utils/${fileName}` + const content = 'Hello, world!' + await saveOrUpdateFile({ filePath, content }) + const fileExists = await factory.searchDirForFile(fileName) + expect(fileExists).toEqual('test/test.txt') + await factory.deleteFile(filePath) + }) // Test that fileExists correctly identifies if a file exists at the given path - it("checks if file exists", async () => { - const factory = getTestFactory(); - const fileName = "test.txt"; - const filePath = `./test/test-utils/${fileName}`; - const content = "Hello, world!"; - await saveOrUpdateFile({ filePath, content }); - const fileExists = await factory.fileExists(filePath); - expect(fileExists).toEqual(true); - await factory.deleteFile(filePath); - }); + it('checks if file exists', async () => { + const factory = getTestFactory() + const fileName = 'test.txt' + const filePath = `./test/test-utils/${fileName}` + const content = 'Hello, world!' + await saveOrUpdateFile({ filePath, content }) + const fileExists = await factory.fileExists(filePath) + expect(fileExists).toEqual(true) + await factory.deleteFile(filePath) + }) // Test that deleteFile deletes a file correctly - it("deletes a file", async () => { - const factory = getTestFactory(); - const fileName = "test.txt"; - const filePath = `./test/test-utils/${fileName}`; - const content = "Hello, world!"; - await saveOrUpdateFile({ filePath, content }); - await factory.deleteFile(filePath); - const fileExistsAfterDeletion = await factory.fileExists(filePath); - expect(fileExistsAfterDeletion).toEqual(false); - }); + it('deletes a file', async () => { + const factory = getTestFactory() + const fileName = 'test.txt' + const filePath = `./test/test-utils/${fileName}` + const content = 'Hello, world!' + await saveOrUpdateFile({ filePath, content }) + await factory.deleteFile(filePath) + const fileExistsAfterDeletion = await factory.fileExists(filePath) + expect(fileExistsAfterDeletion).toEqual(false) + }) // Test that readJson reads a JSON file correctly - it("reads JSON file", async () => { - const factory = getTestFactory(); - const fileName = "test.json"; - const filePath = `./test/test-utils/${fileName}`; - const jsonContent = { greeting: "Hello, world!" }; - await fsPromise.writeFile(filePath, JSON.stringify(jsonContent)); - const content = await factory.readJson(filePath); - expect(content).toEqual(jsonContent); - await factory.deleteFile(filePath); - }); + it('reads JSON file', async () => { + const factory = getTestFactory() + const fileName = 'test.json' + const filePath = `./test/test-utils/${fileName}` + const jsonContent = { greeting: 'Hello, world!' } + await fsPromise.writeFile(filePath, JSON.stringify(jsonContent)) + const content = await factory.readJson(filePath) + expect(content).toEqual(jsonContent) + await factory.deleteFile(filePath) + }) // Test that writeJson writes a JSON object to a file correctly - it("writes JSON file", async () => { - const factory = getTestFactory(); - const fileName = "test.json"; - const filePath = `./${fileName}`; + it('writes JSON file', async () => { + const factory = getTestFactory() + const fileName = 'test.json' + const filePath = `./${fileName}` - const jsonContent = { farewell: "Goodbye, world!" }; + const jsonContent = { farewell: 'Goodbye, world!' } try { - await factory.saveJson(jsonContent, filePath); + await factory.saveJson(jsonContent, filePath) } catch (error) { - console.error(error); + console.error(error) } - let fileContent = ""; + let fileContent = '' try { - fileContent = JSON.parse(await fsPromise.readFile(filePath, "utf-8")); + fileContent = JSON.parse(await fsPromise.readFile(filePath, 'utf-8')) } catch (e) { - console.error(e); + console.error(e) } - expect(fileContent).toEqual(jsonContent); + expect(fileContent).toEqual(jsonContent) try { - await factory.deleteFile(filePath); + await factory.deleteFile(filePath) } catch (error) { - console.error(error); + console.error(error) } - }); + }) - it("should list files and directories", async () => { - const readdirSpy = spyOn(fsPromise, "readdir"); + it('should list files and directories', async () => { + const readdirSpy = spyOn(fsPromise, 'readdir') //@ts-ignore readdirSpy.mockResolvedValue([ { isFile: () => true, isDirectory: () => false, - name: "some-file.txt", + name: 'some-file.txt', }, { isFile: () => false, isDirectory: () => true, - name: "some-dir", + name: 'some-dir', }, - ]); + ]) - const factory = fileFactory({ baseDirectory: "path/to" }); + const factory = fileFactory({ baseDirectory: 'path/to' }) - const result = await factory.listFilesAndFolderInPath(""); + const result = await factory.listFilesAndFolderInPath('') expect(result).toEqual([ { - extension: "txt", - type: "file", - name: "some-file.txt", - fullPath: path.resolve("path/to", "some-file.txt"), + extension: 'txt', + type: 'file', + name: 'some-file.txt', + fullPath: path.resolve('path/to', 'some-file.txt'), size: 0, }, { - type: "directory", - extension: "folder", - name: "some-dir", - fullPath: path.resolve("path/to", "some-dir"), + type: 'directory', + extension: 'folder', + name: 'some-dir', + fullPath: path.resolve('path/to', 'some-dir'), size: 0, }, - ]); - }); -}); + ]) + }) +}) diff --git a/files-folders/index.ts b/files-folders/index.ts index 2299774..a8ceb86 100644 --- a/files-folders/index.ts +++ b/files-folders/index.ts @@ -1,19 +1,14 @@ -export { saveOrUpdateFile, updateMultipleFiles } from "./file-editing-utils"; -export { fileExtensionMap, fullMimeForExtension } from "./file-extension-map"; -export type { ExtensionMapKeys, FileExtensionType } from "./file-extension-map"; -export { fileFactory } from "./file-factory"; -export { getFullPath } from "./file-path-utils"; -export { - listFilesAndFolderInPath, - readFileContent, - readJson, - readTextFromMultipleFiles, -} from "./file-reading-utils"; +export { saveOrUpdateFile, updateMultipleFiles } from './file-editing-utils' +export { fileExtensionMap, fullMimeForExtension } from './file-extension-map' +export type { ExtensionMapKeys, FileExtensionType } from './file-extension-map' +export { fileFactory } from './file-factory' +export { getFullPath } from './file-path-utils' +export { listFilesAndFolderInPath, readFileContent, readJson, readTextFromMultipleFiles } from './file-reading-utils' export { defaultDirIgnore, defaultExtIgnore, readFileInfo, recursiveDirSearch, searchDirForFileName, -} from "./file-search-utils"; -export type { FileSearchConfig, FileSearchParams } from "./file-search-utils"; +} from './file-search-utils' +export type { FileSearchConfig, FileSearchParams } from './file-search-utils' diff --git a/htmlody/constants.ts b/htmlody/constants.ts index 22448c1..303315a 100644 --- a/htmlody/constants.ts +++ b/htmlody/constants.ts @@ -1,116 +1,116 @@ export const SELF_CLOSING_TAGS = new Set([ - "area", - "base", - "br", - "col", - "command", - "embed", - "hr", - "img", - "input", - "keygen", - "link", - "meta", - "param", - "source", - "track", - "wbr", -]); + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', +]) export const htmlTags = [ // Metadata and scripting - "base", - "head", - "link", - "meta", - "noscript", - "script", - "style", - "title", + 'base', + 'head', + 'link', + 'meta', + 'noscript', + 'script', + 'style', + 'title', // Sections - "body", - "footer", - "header", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "nav", - "section", + 'body', + 'footer', + 'header', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'nav', + 'section', // Grouping content - "blockquote", - "div", - "figure", - "hr", - "li", - "main", - "ol", - "p", - "pre", - "ul", + 'blockquote', + 'div', + 'figure', + 'hr', + 'li', + 'main', + 'ol', + 'p', + 'pre', + 'ul', // Text-level semantics - "a", - "abbr", - "b", - "br", - "cite", - "code", - "data", - "em", - "i", - "span", - "strong", - "time", - "u", + 'a', + 'abbr', + 'b', + 'br', + 'cite', + 'code', + 'data', + 'em', + 'i', + 'span', + 'strong', + 'time', + 'u', // Forms - "button", - "datalist", - "fieldset", - "form", - "input", - "label", - "legend", - "meter", - "optgroup", - "option", - "output", - "progress", - "select", - "textarea", + 'button', + 'datalist', + 'fieldset', + 'form', + 'input', + 'label', + 'legend', + 'meter', + 'optgroup', + 'option', + 'output', + 'progress', + 'select', + 'textarea', // Embedded content - "audio", - "canvas", - "embed", - "iframe", - "img", - "map", - "object", - "picture", - "source", - "track", - "video", + 'audio', + 'canvas', + 'embed', + 'iframe', + 'img', + 'map', + 'object', + 'picture', + 'source', + 'track', + 'video', // Tables - "caption", - "col", - "colgroup", - "table", - "tbody", - "td", - "tfoot", - "th", - "thead", - "tr", - "turbo-frame", -] as const; + 'caption', + 'col', + 'colgroup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr', + 'turbo-frame', +] as const -export type HtmlTags = (typeof htmlTags)[number]; +export type HtmlTags = (typeof htmlTags)[number] -export const htmlTagsSet = new Set(htmlTags); +export const htmlTagsSet = new Set(htmlTags) diff --git a/htmlody/css-engine.test.ts b/htmlody/css-engine.test.ts index 16e38f2..458f9af 100644 --- a/htmlody/css-engine.test.ts +++ b/htmlody/css-engine.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "bun:test"; -import { JsonHtmlNodeTree, JsonTagElNode } from "."; +import { describe, expect, it } from 'bun:test' +import { JsonHtmlNodeTree, JsonTagElNode } from '.' import { adjustBrightness, generateCSS, @@ -8,208 +8,208 @@ import { generateVariablesForColor, hexToRgb, rgbToHex, -} from "./css-engine"; // Update this import path -import { ClassRecordAttributes } from "./htmlody-plugins"; +} from './css-engine' // Update this import path +import { ClassRecordAttributes } from './htmlody-plugins' -describe("generateCSS", () => { - it("should generate correct CSS from nodeMap", () => { +describe('generateCSS', () => { + it('should generate correct CSS from nodeMap', () => { const mockNodeMap: JsonHtmlNodeTree> = { exampleDiv: { - tag: "div", + tag: 'div', cr: { - "*": { + '*': { flex: true, - "m-1": true, + 'm-1': true, grid: false, - "w-1/2": true, + 'w-1/2': true, }, }, }, exampleSpan: { - tag: "span", + tag: 'span', cr: { - "*": { - "h-1/2": true, - "p-1": true, + '*': { + 'h-1/2': true, + 'p-1': true, flex: false, }, }, }, - }; + } - const result = generateCSS(mockNodeMap); + const result = generateCSS(mockNodeMap) const expectedCss = `.flex { display: flex; }\n` + `.m-1 { margin: 0.25rem; }\n` + `.w-1/2 { width: 50%; }\n` + `.h-1/2 { height: 50%; }\n` + - `.p-1 { padding: 0.25rem; }\n`; + `.p-1 { padding: 0.25rem; }\n` - expect(result).toEqual(expectedCss); - }); - it("should return empty string for empty nodeMap", () => { - const mockNodeMap: JsonHtmlNodeTree> = {}; + expect(result).toEqual(expectedCss) + }) + it('should return empty string for empty nodeMap', () => { + const mockNodeMap: JsonHtmlNodeTree> = {} - const result = generateCSS(mockNodeMap); - expect(result).toEqual(null); - }); + const result = generateCSS(mockNodeMap) + expect(result).toEqual(null) + }) - it("should handle nodes without the cr property", () => { + it('should handle nodes without the cr property', () => { const mockNodeMap: JsonHtmlNodeTree> = { exampleDiv: { - tag: "div", + tag: 'div', }, - }; + } - const result = generateCSS(mockNodeMap); - expect(result).toEqual(null); - }); + const result = generateCSS(mockNodeMap) + expect(result).toEqual(null) + }) - it("should ignore invalid class names", () => { + it('should ignore invalid class names', () => { const mockNodeMap: JsonHtmlNodeTree> = { exampleDiv: { - tag: "div", + tag: 'div', cr: { - "*": { - "invalid-class": true, + '*': { + 'invalid-class': true, }, }, }, - }; + } - const result = generateCSS(mockNodeMap); - expect(result).toEqual(null); - }); + const result = generateCSS(mockNodeMap) + expect(result).toEqual(null) + }) - it("should not generate CSS if all classes are set to false", () => { + it('should not generate CSS if all classes are set to false', () => { const mockNodeMap: JsonHtmlNodeTree> = { exampleDiv: { - tag: "div", + tag: 'div', cr: { - "*": { + '*': { flex: false, - "m-1": false, + 'm-1': false, }, }, }, - }; - - const result = generateCSS(mockNodeMap); - expect(result).toEqual(null); // No CSS generated - }); -}); - -import { bgColorGen, borderColorGen, textColorGen } from "./css-engine"; - -describe("textColorGen", () => { - it("should generate a CSS property value for text color", () => { - const result = textColorGen("red", 500); - expect(result["text-red-500"]).toBe("color: var(--red-500);"); - }); - - it("should generate a CSS property value for text color with a different shade", () => { - const result = textColorGen("red", 600); - expect(result["text-red-600"]).toBe("color: var(--red-600);"); - }); -}); - -describe("bgColorGen", () => { - it("should generate a CSS property value for background color", () => { - const result = bgColorGen("red", 500); - expect(result["bg-red-500"]).toBe("background-color: var(--red-500);"); - }); -}); - -describe("borderColorGen", () => { - it("should generate a CSS property value for border color", () => { - const result = borderColorGen("red", 500); - expect(result["border-red-500"]).toBe("border-color: var(--red-500);"); - }); - - it("should generate a CSS property value for border color with a different shade", () => { - const result = borderColorGen("red", 600); - expect(result["border-red-600"]).toBe("border-color: var(--red-600);"); - }); -}); - -describe("generateColorVariables", () => { - it("should generate CSS variables for colors", () => { - const result = generateColorVariables(); - - expect(result).toContain(`--red-50`); - - expect(result).toContain(":root {\n--red-50: #1A0000;"); - expect(result).toContain("--slate-300: #4E5A65;"); - }); -}); - -describe("generateShades", () => { - it("should generate shades of a color", () => { - const result = generateShades("red"); - expect(result).toHaveLength(10); + } + + const result = generateCSS(mockNodeMap) + expect(result).toEqual(null) // No CSS generated + }) +}) + +import { bgColorGen, borderColorGen, textColorGen } from './css-engine' + +describe('textColorGen', () => { + it('should generate a CSS property value for text color', () => { + const result = textColorGen('red', 500) + expect(result['text-red-500']).toBe('color: var(--red-500);') + }) + + it('should generate a CSS property value for text color with a different shade', () => { + const result = textColorGen('red', 600) + expect(result['text-red-600']).toBe('color: var(--red-600);') + }) +}) + +describe('bgColorGen', () => { + it('should generate a CSS property value for background color', () => { + const result = bgColorGen('red', 500) + expect(result['bg-red-500']).toBe('background-color: var(--red-500);') + }) +}) + +describe('borderColorGen', () => { + it('should generate a CSS property value for border color', () => { + const result = borderColorGen('red', 500) + expect(result['border-red-500']).toBe('border-color: var(--red-500);') + }) + + it('should generate a CSS property value for border color with a different shade', () => { + const result = borderColorGen('red', 600) + expect(result['border-red-600']).toBe('border-color: var(--red-600);') + }) +}) + +describe('generateColorVariables', () => { + it('should generate CSS variables for colors', () => { + const result = generateColorVariables() + + expect(result).toContain(`--red-50`) + + expect(result).toContain(':root {\n--red-50: #1A0000;') + expect(result).toContain('--slate-300: #4E5A65;') + }) +}) + +describe('generateShades', () => { + it('should generate shades of a color', () => { + const result = generateShades('red') + expect(result).toHaveLength(10) result.forEach((shade) => { // validate that each shade is a valid hex color - expect(shade).toHaveLength(7); - }); + expect(shade).toHaveLength(7) + }) - expect(result[0][0]).toBe("#"); // first shade, first charact - }); -}); + expect(result[0][0]).toBe('#') // first shade, first charact + }) +}) -describe("generateVariablesForColor", () => { - it("should generate CSS variables for shades of a color", () => { - const result = generateVariablesForColor("red"); +describe('generateVariablesForColor', () => { + it('should generate CSS variables for shades of a color', () => { + const result = generateVariablesForColor('red') expect(result).toBe( - "--red-50: #1A0000;\n--red-100: #4D0000;\n--red-200: #800000;\n--red-300: #B30000;\n--red-400: #E60000;\n--red-500: #FF0000;\n--red-600: #FF1A1A;\n--red-700: #FF4D4D;\n--red-800: #FF8080;\n--red-900: #FFB3B3;\n", - ); - }); + '--red-50: #1A0000;\n--red-100: #4D0000;\n--red-200: #800000;\n--red-300: #B30000;\n--red-400: #E60000;\n--red-500: #FF0000;\n--red-600: #FF1A1A;\n--red-700: #FF4D4D;\n--red-800: #FF8080;\n--red-900: #FFB3B3;\n', + ) + }) - it("should generate CSS variables for shades of a different color", () => { - const result = generateVariablesForColor("blue"); + it('should generate CSS variables for shades of a different color', () => { + const result = generateVariablesForColor('blue') expect(result).toBe( - "--blue-50: #00001A;\n--blue-100: #00004D;\n--blue-200: #000080;\n--blue-300: #0000B3;\n--blue-400: #0000E6;\n--blue-500: #0000FF;\n--blue-600: #1A1AFF;\n--blue-700: #4D4DFF;\n--blue-800: #8080FF;\n--blue-900: #B3B3FF;\n", - ); - }); -}); - -describe("hexToRgb", () => { - it("should convert a hex color to an RGB object", () => { - const result = hexToRgb("#ff0000"); - expect(result).toEqual({ r: 255, g: 0, b: 0 }); - }); - - it("should return null for an invalid hex color", () => { - const result = hexToRgb("invalid"); - expect(result).toBeNull(); - }); -}); - -describe("rgbToHex", () => { - it("should convert an RGB color to a hex string", () => { - const result = rgbToHex(255, 0, 0); - expect(result).toBe("#FF0000"); - }); - - it("should handle zero values correctly", () => { - const result = rgbToHex(0, 0, 0); - expect(result).toBe("#000000"); - }); -}); - -describe("adjustBrightness", () => { - it("should adjust the brightness of an RGB color", () => { - const result = adjustBrightness({ r: 255, g: 0, b: 0 }, 1.5); - expect(result).toEqual({ r: 255, g: 0, b: 0 }); - }); - - it("should not exceed the maximum brightness value", () => { - const result = adjustBrightness({ r: 255, g: 255, b: 255 }, 10); - expect(result).toEqual({ r: 255, g: 255, b: 255 }); - }); - - it("should not go below the minimum brightness value", () => { - const result = adjustBrightness({ r: 0, g: 0, b: 0 }, 0.1); - expect(result).toEqual({ r: 0, g: 0, b: 0 }); - }); -}); + '--blue-50: #00001A;\n--blue-100: #00004D;\n--blue-200: #000080;\n--blue-300: #0000B3;\n--blue-400: #0000E6;\n--blue-500: #0000FF;\n--blue-600: #1A1AFF;\n--blue-700: #4D4DFF;\n--blue-800: #8080FF;\n--blue-900: #B3B3FF;\n', + ) + }) +}) + +describe('hexToRgb', () => { + it('should convert a hex color to an RGB object', () => { + const result = hexToRgb('#ff0000') + expect(result).toEqual({ r: 255, g: 0, b: 0 }) + }) + + it('should return null for an invalid hex color', () => { + const result = hexToRgb('invalid') + expect(result).toBeNull() + }) +}) + +describe('rgbToHex', () => { + it('should convert an RGB color to a hex string', () => { + const result = rgbToHex(255, 0, 0) + expect(result).toBe('#FF0000') + }) + + it('should handle zero values correctly', () => { + const result = rgbToHex(0, 0, 0) + expect(result).toBe('#000000') + }) +}) + +describe('adjustBrightness', () => { + it('should adjust the brightness of an RGB color', () => { + const result = adjustBrightness({ r: 255, g: 0, b: 0 }, 1.5) + expect(result).toEqual({ r: 255, g: 0, b: 0 }) + }) + + it('should not exceed the maximum brightness value', () => { + const result = adjustBrightness({ r: 255, g: 255, b: 255 }, 10) + expect(result).toEqual({ r: 255, g: 255, b: 255 }) + }) + + it('should not go below the minimum brightness value', () => { + const result = adjustBrightness({ r: 0, g: 0, b: 0 }, 0.1) + expect(result).toEqual({ r: 0, g: 0, b: 0 }) + }) +}) diff --git a/htmlody/css-engine.ts b/htmlody/css-engine.ts index 2124bb9..83ee180 100644 --- a/htmlody/css-engine.ts +++ b/htmlody/css-engine.ts @@ -1,130 +1,130 @@ -import { ClassRecordAttributes } from "./htmlody-plugins"; -import { ClassRecord, JsonHtmlNodeTree, JsonTagElNode, ResponsiveClassRecord } from "./htmlody-types"; +import { ClassRecordAttributes } from './htmlody-plugins' +import { ClassRecord, JsonHtmlNodeTree, JsonTagElNode, ResponsiveClassRecord } from './htmlody-types' const fractionPercentMap = { - "1/2": 50, - "1/3": 33.333333, - "2/3": 66.666667, - "1/4": 25, - "2/4": 50, - "3/4": 75, - "1/5": 20, - "2/5": 40, - "3/5": 60, - "4/5": 80, - "1/6": 16.666667, - "2/6": 33.333333, - "3/6": 50, - "4/6": 66.666667, - "5/6": 83.333333, - "1/12": 8.333333, - "2/12": 16.666667, - "3/12": 25, - "4/12": 33.333333, - "5/12": 41.666667, - "6/12": 50, - "7/12": 58.333333, - "8/12": 66.666667, - "9/12": 75, - "10/12": 83.333333, - "11/12": 91.666667, -} as const; + '1/2': 50, + '1/3': 33.333333, + '2/3': 66.666667, + '1/4': 25, + '2/4': 50, + '3/4': 75, + '1/5': 20, + '2/5': 40, + '3/5': 60, + '4/5': 80, + '1/6': 16.666667, + '2/6': 33.333333, + '3/6': 50, + '4/6': 66.666667, + '5/6': 83.333333, + '1/12': 8.333333, + '2/12': 16.666667, + '3/12': 25, + '4/12': 33.333333, + '5/12': 41.666667, + '6/12': 50, + '7/12': 58.333333, + '8/12': 66.666667, + '9/12': 75, + '10/12': 83.333333, + '11/12': 91.666667, +} as const export const breakpoints = { - sm: "640px", - md: "768px", - lg: "1024px", - xl: "1280px", -} as const; + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', +} as const -type FractionPercentMapT = typeof fractionPercentMap; +type FractionPercentMapT = typeof fractionPercentMap -export type CSSUnits = "rem" | "px" | "%" | "em"; +export type CSSUnits = 'rem' | 'px' | '%' | 'em' export const cc = (keys: Keys) => { - const keyString = typeof keys === "string" ? keys : keys.join(" "); + const keyString = typeof keys === 'string' ? keys : keys.join(' ') const composition = { - "*": { + '*': { [keyString]: true, }, - }; + } - return composition; -}; + return composition +} export const uClass = (keys: CSSMapKeys[]) => { // type casted so the resulting string doesn't throw a type error, // and keys are validated on the input - return keys.join(" ") as "u-class"; -}; + return keys.join(' ') as 'u-class' +} -export const textAlign = (val: Val) => `text-align: ${val};` as const; -export const fontSize = (val: Val) => `font-size: ${val};` as const; -export const textColor = (val: Val) => `color: ${val};` as const; -export const bgColor = (val: Val) => `background-color: ${val};` as const; +export const textAlign = (val: Val) => `text-align: ${val};` as const +export const fontSize = (val: Val) => `font-size: ${val};` as const +export const textColor = (val: Val) => `color: ${val};` as const +export const bgColor = (val: Val) => `background-color: ${val};` as const export const border = ( width: string, style: string, color: string, -) => `border: ${width} ${style} ${color};` as const; +) => `border: ${width} ${style} ${color};` as const function extractClassNames(classRecord: ClassRecord): string[] { return Object.entries(classRecord) .filter(([_key, value]) => value) - .flatMap(([key]) => key.split(" ")); + .flatMap(([key]) => key.split(' ')) } function generateCssSelector(breakpoint: string, className: string): string { - const fullClassName = breakpoint === "*" ? className : `${breakpoint}_${className}`; - const cssRule = CSS_MAP[className]; + const fullClassName = breakpoint === '*' ? className : `${breakpoint}_${className}` + const cssRule = CSS_MAP[className] - if (breakpoint === "*") { - return `.${fullClassName} { ${cssRule} }`; + if (breakpoint === '*') { + return `.${fullClassName} { ${cssRule} }` } - return `@media (min-width: ${breakpoints[breakpoint]}) { .${fullClassName} { ${cssRule} } }`; + return `@media (min-width: ${breakpoints[breakpoint]}) { .${fullClassName} { ${cssRule} } }` } function processClassRecords(classRecords: ResponsiveClassRecord, usedClasses: Set): string | null { - let cssStr = ""; + let cssStr = '' Object.entries(classRecords).forEach(([breakpoint, classRecord]) => { - const classNames = extractClassNames(classRecord); + const classNames = extractClassNames(classRecord) classNames.forEach((className) => { - const fullClassName = breakpoint === "*" ? className : `${breakpoint}_${className}`; + const fullClassName = breakpoint === '*' ? className : `${breakpoint}_${className}` if (!usedClasses.has(fullClassName)) { - usedClasses.add(fullClassName); + usedClasses.add(fullClassName) - if (typeof CSS_MAP[className] === "string") { - const selector = generateCssSelector(breakpoint, className); - cssStr += `${selector}\n`; + if (typeof CSS_MAP[className] === 'string') { + const selector = generateCssSelector(breakpoint, className) + cssStr += `${selector}\n` } } - }); - }); + }) + }) - if (!cssStr) return null; - return cssStr; + if (!cssStr) return null + return cssStr } export function processNode(node: JsonTagElNode, usedClasses: Set): string | null { - let cssStr = ""; + let cssStr = '' if (node.cr) { - const classRecords = processClassRecords(node.cr, usedClasses); - if (classRecords) cssStr += classRecords; + const classRecords = processClassRecords(node.cr, usedClasses) + if (classRecords) cssStr += classRecords } if (node.child) { Object.values(node.child).forEach((childNode) => { - const childNodeStr = processNode(childNode, usedClasses); - if (childNodeStr) cssStr += childNodeStr; - }); + const childNodeStr = processNode(childNode, usedClasses) + if (childNodeStr) cssStr += childNodeStr + }) } - return cssStr || null; + return cssStr || null } export function generateCSS< @@ -132,28 +132,28 @@ export function generateCSS< JsonTagElNode >, >(nodeMap: NodeMap): string | null { - const usedClasses = new Set(); - let cssStr = ""; + const usedClasses = new Set() + let cssStr = '' Object.values(nodeMap).forEach((node) => { - const nodeStr = processNode(node, usedClasses); + const nodeStr = processNode(node, usedClasses) - if (nodeStr) cssStr += nodeStr; - }); + if (nodeStr) cssStr += nodeStr + }) - if (!cssStr) return null; - return cssStr; + if (!cssStr) return null + return cssStr } export const createKeyVal = (key: Key, val: Val) => { const obj = { [key]: val, } as { - [K in Key]: Val; - }; + [K in Key]: Val + } - return obj; -}; + return obj +} export const cssPropertyValueGen = < ClassAbbrevKey extends string, @@ -166,14 +166,14 @@ export const cssPropertyValueGen = < value: Value, unit: Unit, ) => { - const cssGen = `${property}: ${value}${unit};` as const; + const cssGen = `${property}: ${value}${unit};` as const return { [classAbbrevKey]: cssGen, } as { - [K in ClassAbbrevKey]: typeof cssGen; - }; -}; + [K in ClassAbbrevKey]: typeof cssGen + } +} export const sizingHelper = ( classValKey: ClassValKey, @@ -181,10 +181,10 @@ export const sizingHelper = { return { - ...cssPropertyValueGen(`w-${classValKey}`, "width", value, unit), - ...cssPropertyValueGen(`h-${classValKey}`, "height", value, unit), - } as const; -}; + ...cssPropertyValueGen(`w-${classValKey}`, 'width', value, unit), + ...cssPropertyValueGen(`h-${classValKey}`, 'height', value, unit), + } as const +} const fractionHelper = < ClassAbbrevKey extends string, @@ -195,17 +195,17 @@ const fractionHelper = < fraction: Fraction, property: Property, ) => { - const percentageValue = fractionPercentMap[fraction]; + const percentageValue = fractionPercentMap[fraction] - return cssPropertyValueGen(`${classAbbrevKey}-${fraction}`, property, percentageValue, "%"); -}; + return cssPropertyValueGen(`${classAbbrevKey}-${fraction}`, property, percentageValue, '%') +} export const sizeFractions = (fraction: Fraction) => { return { - ...fractionHelper("w", fraction, "width"), - ...fractionHelper("h", fraction, "height"), - }; -}; + ...fractionHelper('w', fraction, 'width'), + ...fractionHelper('h', fraction, 'height'), + } +} export const spacingHelper = ( marginFactorKey: ClassAbbrevKey, @@ -228,48 +228,48 @@ export const spacingHelper = { - let adjustedColor; + let adjustedColor if (index < 5) { // Darken the color for shades 50-400 - adjustedColor = adjustBrightness(baseColor, factor); + adjustedColor = adjustBrightness(baseColor, factor) } else { // Lighten the color for shades 600-900 - adjustedColor = lightenColor(baseColor, factor); + adjustedColor = lightenColor(baseColor, factor) } - const shade = rgbToHex(adjustedColor.r, adjustedColor.g, adjustedColor.b); - shades.push(shade); - }); + const shade = rgbToHex(adjustedColor.r, adjustedColor.g, adjustedColor.b) + shades.push(shade) + }) - return shades; + return shades } function lightenColor(color: { r: number; g: number; b: number }, factor: number): { r: number; g: number; b: number } { @@ -317,93 +317,93 @@ function lightenColor(color: { r: number; g: number; b: number }, factor: number r: Math.round(clamp(color.r + (255 - color.r) * (factor - 1), 0, 255)), g: Math.round(clamp(color.g + (255 - color.g) * (factor - 1), 0, 255)), b: Math.round(clamp(color.b + (255 - color.b) * (factor - 1), 0, 255)), - }; + } } export const generateVariablesForColor = (color: Color) => { - const cssVariables: string[] = []; + const cssVariables: string[] = [] - const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900] as const; + const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900] as const - const shadeColorArray = generateShades(color); + const shadeColorArray = generateShades(color) for (let i = 0; i < shades.length; i++) { - const shade = shades[i]; - const colorCode = shadeColorArray[i]; + const shade = shades[i] + const colorCode = shadeColorArray[i] - cssVariables.push(`--${color}-${shade}: ${colorCode};\n`); + cssVariables.push(`--${color}-${shade}: ${colorCode};\n`) } - return cssVariables.join(""); -}; + return cssVariables.join('') +} export const generateColorVariables = () => { - let cssVariables = ":root {\n"; + let cssVariables = ':root {\n' const colors = [ - "red", - "orange", - "yellow", - "green", - "blue", - "indigo", - "purple", - "pink", - "slate", - "gray", - "lightgray", - "powderblue", - "whitesmoke", - ] as const; + 'red', + 'orange', + 'yellow', + 'green', + 'blue', + 'indigo', + 'purple', + 'pink', + 'slate', + 'gray', + 'lightgray', + 'powderblue', + 'whitesmoke', + ] as const for (let i = 0; i < colors.length; i++) { - const color = colors[i]; - const typedColor = color; - cssVariables += generateVariablesForColor(typedColor); + const color = colors[i] + const typedColor = color + cssVariables += generateVariablesForColor(typedColor) } - cssVariables += "}\n"; - return cssVariables; -}; + cssVariables += '}\n' + return cssVariables +} export const textColorGen = (color: Color, shade: Shade) => { - return cssPropertyValueGen(`text-${color}-${shade}`, "color", `var(--${color}-${shade})`, ""); -}; + return cssPropertyValueGen(`text-${color}-${shade}`, 'color', `var(--${color}-${shade})`, '') +} export const bgColorGen = (color: Color, shade: Shade) => { - return cssPropertyValueGen(`bg-${color}-${shade}`, "background-color", `var(--${color}-${shade})`, ""); -}; + return cssPropertyValueGen(`bg-${color}-${shade}`, 'background-color', `var(--${color}-${shade})`, '') +} export const borderColorGen = (color: Color, shade: Shade) => { - return cssPropertyValueGen(`border-${color}-${shade}`, "border-color", `var(--${color}-${shade})`, ""); -}; + return cssPropertyValueGen(`border-${color}-${shade}`, 'border-color', `var(--${color}-${shade})`, '') +} export const textColorStrokeGen = (color: Color, shade: Shade) => { return cssPropertyValueGen( `text-stroke-${color}-${shade}`, - "-webkit-text-stroke-color", + '-webkit-text-stroke-color', `var(--${color}-${shade})`, - "", - ); -}; + '', + ) +} const shadowSizes = { - "shadow-sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)", - shadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - "shadow-md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - "shadow-lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)", - "shadow-xl": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)", - "shadow-2xl": "0 25px 50px -12px rgb(0 0 0 / 0.25)", - "shadow-inner": "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)", - "shadow-none": "0 0 #0000", -} as const; - -type ShadowSize = keyof typeof shadowSizes; + 'shadow-sm': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + shadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'shadow-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + 'shadow-lg': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', + 'shadow-xl': '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', + 'shadow-2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)', + 'shadow-inner': 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', + 'shadow-none': '0 0 #0000', +} as const + +type ShadowSize = keyof typeof shadowSizes export const shadowGen = (size: Size) => { - const shadowValue = shadowSizes[size]; - return cssPropertyValueGen(size, "box-shadow", shadowValue, ""); -}; + const shadowValue = shadowSizes[size] + return cssPropertyValueGen(size, 'box-shadow', shadowValue, '') +} export const generatePropertiesForColor = (colorKey: Color) => { return { @@ -447,63 +447,63 @@ export const generatePropertiesForColor = (colorKey: Co ...textColorStrokeGen(colorKey, 700), ...textColorStrokeGen(colorKey, 800), ...textColorStrokeGen(colorKey, 900), - }; -}; + } +} // gaps are based on rem by default const baseGapSizes = { - "0": "0", - "0.5": "0.125", // 2px - "1": "0.25", // 4px - "1.5": "0.375", // 6px - "2": "0.5", // 8px - "2.5": "0.625", // 10px - "3": "0.75", // 12px - "3.5": "0.875", // 14px - "4": "1", // 16px - "5": "1.25", // 20px - "6": "1.5", // 24px - "7": "1.75", // 28px - "8": "2", // 32px - "9": "2.25", // 36px - "10": "2.5", // 40px - "11": "2.75", // 44px - "12": "3", // 48px - "14": "3.5", // 56px - "16": "4", // 64px - "20": "5", // 80px - "24": "6", // 96px - "28": "7", // 112px - "32": "8", // 128px - "36": "9", // 144px - "40": "10", // 160px - "44": "11", // 176px - "48": "12", // 192px - "52": "13", // 208px - "56": "14", // 224px - "60": "15", // 240px - "64": "16", // 256px - "72": "18", // 288px - "80": "20", // 320px - "96": "24", // 384px -} as const; - -type GapSize = keyof typeof baseGapSizes; - -const gapHelper = (key: Key) => cssPropertyValueGen(key, "gap", baseGapSizes[key], "rem"); - -export const generateGapHelper = (gap: Gap, unit: Unit) => { - const gapValue = baseGapSizes[gap]; - - const baseGap = `gap-${gap}` as const; - const xGap = `gap-x-${gap}` as const; - const yGap = `gap-y-${gap}` as const; + '0': '0', + '0.5': '0.125', // 2px + '1': '0.25', // 4px + '1.5': '0.375', // 6px + '2': '0.5', // 8px + '2.5': '0.625', // 10px + '3': '0.75', // 12px + '3.5': '0.875', // 14px + '4': '1', // 16px + '5': '1.25', // 20px + '6': '1.5', // 24px + '7': '1.75', // 28px + '8': '2', // 32px + '9': '2.25', // 36px + '10': '2.5', // 40px + '11': '2.75', // 44px + '12': '3', // 48px + '14': '3.5', // 56px + '16': '4', // 64px + '20': '5', // 80px + '24': '6', // 96px + '28': '7', // 112px + '32': '8', // 128px + '36': '9', // 144px + '40': '10', // 160px + '44': '11', // 176px + '48': '12', // 192px + '52': '13', // 208px + '56': '14', // 224px + '60': '15', // 240px + '64': '16', // 256px + '72': '18', // 288px + '80': '20', // 320px + '96': '24', // 384px +} as const + +type GapSize = keyof typeof baseGapSizes + +const gapHelper = (key: Key) => cssPropertyValueGen(key, 'gap', baseGapSizes[key], 'rem') + +export const generateGapHelper = (gap: Gap, unit: Unit) => { + const gapValue = baseGapSizes[gap] + + const baseGap = `gap-${gap}` as const + const xGap = `gap-x-${gap}` as const + const yGap = `gap-y-${gap}` as const return { - ...cssPropertyValueGen(baseGap, "gap", gapValue, unit), - ...cssPropertyValueGen(xGap, "column-gap", gapValue, unit), - ...cssPropertyValueGen(yGap, "row-gap", gapValue, unit), - }; + ...cssPropertyValueGen(baseGap, 'gap', gapValue, unit), + ...cssPropertyValueGen(xGap, 'column-gap', gapValue, unit), + ...cssPropertyValueGen(yGap, 'row-gap', gapValue, unit), + } // return { // [baseGap]: `gap: ${gapValue}${unit};`, @@ -513,383 +513,383 @@ export const generateGapHelper = { - it("should add classes to attributes based on ClassRecord", () => { +describe('classRecordPluginHandler', () => { + it('should add classes to attributes based on ClassRecord', () => { const node: CRNode = { - tag: "div", + tag: 'div', cr: { - "*": { - "border-gray-50": true, - "border-blue-100": false, - "bg-blue-100": true, + '*': { + 'border-gray-50': true, + 'border-blue-100': false, + 'bg-blue-100': true, }, }, attributes: { - id: "sample-id", + id: 'sample-id', }, - content: "Sample Content", - }; - const processedNode = classRecordPluginHandler(node); - expect(processedNode.attributes?.class).toBe("border-gray-50 bg-blue-100"); - }); + content: 'Sample Content', + } + const processedNode = classRecordPluginHandler(node) + expect(processedNode.attributes?.class).toBe('border-gray-50 bg-blue-100') + }) - it("should not add classes to attributes if ClassRecord is not present", () => { + it('should not add classes to attributes if ClassRecord is not present', () => { const node: CRNode = { - tag: "div", + tag: 'div', attributes: { - id: "sample-id", + id: 'sample-id', }, - content: "Sample Content", - }; - const processedNode = classRecordPluginHandler(node); - expect(processedNode.attributes?.class).toBe(undefined); - }); -}); + content: 'Sample Content', + } + const processedNode = classRecordPluginHandler(node) + expect(processedNode.attributes?.class).toBe(undefined) + }) +}) -describe("classRecordPlugin", () => { +describe('classRecordPlugin', () => { const sampleNodeMap: JsonHtmlNodeTree = { div1: { - tag: "div", + tag: 'div', cr: { - "*": { - "bg-blue-100": true, - "bg-gray-100": false, - "bg-red-100": true, + '*': { + 'bg-blue-100': true, + 'bg-gray-100': false, + 'bg-red-100': true, }, }, - content: "Hello World!", + content: 'Hello World!', }, div2: { - tag: "div", + tag: 'div', cr: { - "*": { - "bg-blue-100": true, + '*': { + 'bg-blue-100': true, }, }, - content: "Another div", + content: 'Another div', }, - }; + } - it("should add classes based on the ClassRecord", () => { - const renderedHtml = jsonToHtml(sampleNodeMap, [classRecordPlugin]); - expect(renderedHtml).toContain('
Hello World!
'); - expect(renderedHtml).toContain('
Another div
'); - }); + it('should add classes based on the ClassRecord', () => { + const renderedHtml = jsonToHtml(sampleNodeMap, [classRecordPlugin]) + expect(renderedHtml).toContain('
Hello World!
') + expect(renderedHtml).toContain('
Another div
') + }) - it("should not add classes with a value of false", () => { - const renderedHtml = jsonToHtml(sampleNodeMap, [classRecordPlugin]); - expect(renderedHtml).not.toContain("class-two"); - }); -}); + it('should not add classes with a value of false', () => { + const renderedHtml = jsonToHtml(sampleNodeMap, [classRecordPlugin]) + expect(renderedHtml).not.toContain('class-two') + }) +}) -describe("Markdown Plugin with jsonToHtml", () => { - it("should convert a simple markdown text to HTML", () => { +describe('Markdown Plugin with jsonToHtml', () => { + it('should convert a simple markdown text to HTML', () => { const input = { sampleId: { - tag: "div", - markdown: "# Hello\nThis is **bold**.", + tag: 'div', + markdown: '# Hello\nThis is **bold**.', }, - }; + } - const output = jsonToHtml(input, [markdownPlugin]); + const output = jsonToHtml(input, [markdownPlugin]) - const expectedOutput = `

Hello

\n

This is bold.

`; - expect(output).toBe(expectedOutput); - }); + const expectedOutput = `

Hello

\n

This is bold.

` + expect(output).toBe(expectedOutput) + }) - it("should handle nodes without markdown", () => { + it('should handle nodes without markdown', () => { const input = { sampleId: { - tag: "div", - content: "Just a regular div.", + tag: 'div', + content: 'Just a regular div.', }, - }; + } - const output = jsonToHtml(input, [markdownPlugin]); + const output = jsonToHtml(input, [markdownPlugin]) - const expectedOutput = `
Just a regular div.
`; - expect(output).toBe(expectedOutput); - }); -}); + const expectedOutput = `
Just a regular div.
` + expect(output).toBe(expectedOutput) + }) +}) diff --git a/htmlody/htmlody-plugins.ts b/htmlody/htmlody-plugins.ts index f980f31..62ee3c4 100644 --- a/htmlody/htmlody-plugins.ts +++ b/htmlody/htmlody-plugins.ts @@ -1,70 +1,70 @@ -import { convertMarkdownToHTML } from "../utils/text-utils"; -import { ExtensionRec, JsonTagElNode, ResponsiveClassRecord } from "./htmlody-types"; +import { convertMarkdownToHTML } from '../utils/text-utils' +import { ExtensionRec, JsonTagElNode, ResponsiveClassRecord } from './htmlody-types' // this will be the node that will be attached to our json node export type ClassRecordAttributes = { - cr?: ResponsiveClassRecord; -}; + cr?: ResponsiveClassRecord +} -export type CRNode = JsonTagElNode; -export type MDNode = JsonTagElNode; +export type CRNode = JsonTagElNode +export type MDNode = JsonTagElNode export interface HTMLodyPlugin { // need to figure out if finall return type should be with or without the type - processNode: (node: JsonTagElNode) => JsonTagElNode; + processNode: (node: JsonTagElNode) => JsonTagElNode } export const classRecordPluginHandler = (node: Node) => { - let classes = ""; + let classes = '' if (node.cr) { const responsiveClasses = Object.entries(node.cr) .map(([breakpoint, classRecord]) => { - const breakpointPrefix = breakpoint === "*" ? "" : `${breakpoint}_`; + const breakpointPrefix = breakpoint === '*' ? '' : `${breakpoint}_` const classList = Object.entries(classRecord || {}) .filter(([, value]) => value) .map(([key]) => `${breakpointPrefix}${key}`) - .join(" "); - return classList; + .join(' ') + return classList }) - .join(" "); + .join(' ') - classes = responsiveClasses; + classes = responsiveClasses } - if (classes === "") { - return node; + if (classes === '') { + return node } return { ...node, attributes: { ...node.attributes, class: classes }, - }; -}; + } +} export const classRecordPlugin: HTMLodyPlugin = { processNode: classRecordPluginHandler, -}; +} export type MarkdownAttributes = { - markdown?: string; -}; + markdown?: string +} export const markdownPluginHandler = (node: Node): JsonTagElNode => { if (node.markdown) { - const htmlContent = convertMarkdownToHTML(node.markdown); + const htmlContent = convertMarkdownToHTML(node.markdown) // remove the markdown attribute after processing - const { markdown, ...remainingAttributes } = node.attributes || {}; + const { markdown, ...remainingAttributes } = node.attributes || {} return { ...node, content: htmlContent, attributes: remainingAttributes, - }; + } } - return node; -}; + return node +} export const markdownPlugin: HTMLodyPlugin = { processNode: markdownPluginHandler, -}; +} diff --git a/htmlody/htmlody-types.ts b/htmlody/htmlody-types.ts index 9e8db27..4b3b973 100644 --- a/htmlody/htmlody-types.ts +++ b/htmlody/htmlody-types.ts @@ -1,32 +1,32 @@ -import { HtmlTags } from "./constants"; -import type { CSSMapKeys } from "./css-engine"; +import { HtmlTags } from './constants' +import type { CSSMapKeys } from './css-engine' -export type Attributes = Record; +export type Attributes = Record export type ClassRecord = Partial<{ - [key in CSSMapKeys]: boolean; -}>; + [key in CSSMapKeys]: boolean +}> -const breakpoints = ["sm", "md", "lg", "xl"] as const; -export type Breakpoint = (typeof breakpoints)[number]; +const breakpoints = ['sm', 'md', 'lg', 'xl'] as const +export type Breakpoint = (typeof breakpoints)[number] export type ResponsiveClassRecord = { - "*"?: ClassRecord; - sm?: ClassRecord; - md?: ClassRecord; - lg?: ClassRecord; - xl?: ClassRecord; -}; + '*'?: ClassRecord + sm?: ClassRecord + md?: ClassRecord + lg?: ClassRecord + xl?: ClassRecord +} -export type ExtensionRec = Record; +export type ExtensionRec = Record export type JsonTagElNode = { - content?: string; - child?: JsonHtmlNodeTree>; - attributes?: Attributes; - tag: HtmlTags; -} & Omit; + content?: string + child?: JsonHtmlNodeTree> + attributes?: Attributes + tag: HtmlTags +} & Omit export type JsonHtmlNodeTree = { - [id: string]: JsonTagElNode; -}; + [id: string]: JsonTagElNode +} diff --git a/htmlody/htmlody-utils.test.ts b/htmlody/htmlody-utils.test.ts index b0bf9e7..b768a4e 100644 --- a/htmlody/htmlody-utils.test.ts +++ b/htmlody/htmlody-utils.test.ts @@ -1,125 +1,125 @@ -import { JsonTagElNode } from "bnkit/htmlody"; -import { describe, expect, it } from "bun:test"; -import { Attributes } from "./htmlody-types"; -import { collectClassNames, formatAttributes, nodeFactory, retrieveElement } from "./htmlody-utils"; +import { JsonTagElNode } from 'bnkit/htmlody' +import { describe, expect, it } from 'bun:test' +import { Attributes } from './htmlody-types' +import { collectClassNames, formatAttributes, nodeFactory, retrieveElement } from './htmlody-utils' -describe("formatAttributes", () => { - it("should handle empty attributes", () => { - const attributes: Attributes = {}; +describe('formatAttributes', () => { + it('should handle empty attributes', () => { + const attributes: Attributes = {} // @ts-expect-error - const formatted = formatAttributes(attributes, {}); - expect(formatted).toBe(""); - }); -}); + const formatted = formatAttributes(attributes, {}) + expect(formatted).toBe('') + }) +}) -describe("formatAttributes", () => { - it("should format attributes into string", () => { +describe('formatAttributes', () => { + it('should format attributes into string', () => { const attributes: Attributes = { id: `id-${Math.floor(Math.random() * 1000)}`, - class: "sample-class", - }; + class: 'sample-class', + } // @ts-expect-error - const formatted = formatAttributes(attributes, {}); - expect(formatted).toContain(attributes.id); - expect(formatted).toContain(attributes.class); - }); -}); + const formatted = formatAttributes(attributes, {}) + expect(formatted).toContain(attributes.id) + expect(formatted).toContain(attributes.class) + }) +}) -describe("retrieveElement", () => { - it("should retrieve an element from a JsonHtmlNodeMap", () => { +describe('retrieveElement', () => { + it('should retrieve an element from a JsonHtmlNodeMap', () => { const JsonHtmlNodeMap = { div: { - tag: "div", + tag: 'div', attributes: { - class: "sample-class", + class: 'sample-class', }, - content: "Sample Content", + content: 'Sample Content', }, - }; - const element = retrieveElement(JsonHtmlNodeMap, "div"); + } + const element = retrieveElement(JsonHtmlNodeMap, 'div') expect(element).toEqual({ - tag: "div", + tag: 'div', attributes: { - class: "sample-class", + class: 'sample-class', }, - content: "Sample Content", - }); - }); + content: 'Sample Content', + }) + }) - it("should return undefined if the element is not present in the JsonHtmlNodeMap", () => { + it('should return undefined if the element is not present in the JsonHtmlNodeMap', () => { const JsonHtmlNodeMap = { div: { - tag: "div", + tag: 'div', attributes: { - class: "sample-class", + class: 'sample-class', }, - content: "Sample Content", + content: 'Sample Content', }, - }; + } // @ts-expect-error - const element = retrieveElement(JsonHtmlNodeMap, "span"); - expect(element).toBeUndefined(); - }); -}); + const element = retrieveElement(JsonHtmlNodeMap, 'span') + expect(element).toBeUndefined() + }) +}) -describe("nodeFactory", () => { - it("should create a new node with the given configuration", () => { +describe('nodeFactory', () => { + it('should create a new node with the given configuration', () => { const nodeConfig = { - tag: "div", + tag: 'div', attributes: { - class: "sample-class", - id: "sample-id", + class: 'sample-class', + id: 'sample-id', }, - content: "Sample Content", - }; - const factory = nodeFactory(nodeConfig); - const newNode = factory.create(); + content: 'Sample Content', + } + const factory = nodeFactory(nodeConfig) + const newNode = factory.create() expect(newNode).toEqual({ - tag: "div", + tag: 'div', attributes: { - class: "sample-class", - id: "sample-id", + class: 'sample-class', + id: 'sample-id', }, - content: "Sample Content", - }); - }); -}); + content: 'Sample Content', + }) + }) +}) -describe("collectClassNames", () => { - it("should add class names to uniqueClassNames set", () => { +describe('collectClassNames', () => { + it('should add class names to uniqueClassNames set', () => { const node = { - tag: "div", + tag: 'div', attributes: { - class: "sample-class-1 sample-class-2", + class: 'sample-class-1 sample-class-2', }, - content: "Sample Content", - }; - const uniqueClassNames = new Set(); - collectClassNames(node, uniqueClassNames); - expect(uniqueClassNames).toEqual(new Set(["sample-class-1", "sample-class-2"])); - }); + content: 'Sample Content', + } + const uniqueClassNames = new Set() + collectClassNames(node, uniqueClassNames) + expect(uniqueClassNames).toEqual(new Set(['sample-class-1', 'sample-class-2'])) + }) - it("should not add class names to uniqueClassNames set if class attribute is not present", () => { + it('should not add class names to uniqueClassNames set if class attribute is not present', () => { const node = { - tag: "div", - content: "Sample Content", - }; - const uniqueClassNames = new Set(); - collectClassNames(node, uniqueClassNames); - expect(uniqueClassNames).toEqual(new Set()); - }); + tag: 'div', + content: 'Sample Content', + } + const uniqueClassNames = new Set() + collectClassNames(node, uniqueClassNames) + expect(uniqueClassNames).toEqual(new Set()) + }) - it("should not add class names to uniqueClassNames set if class attribute is not a string", () => { + it('should not add class names to uniqueClassNames set if class attribute is not a string', () => { const node: JsonTagElNode = { - tag: "div", + tag: 'div', attributes: { // @ts-expect-error class: 123, }, - content: "Sample Content", - }; - const uniqueClassNames = new Set(); - collectClassNames(node, uniqueClassNames); - expect(uniqueClassNames).toEqual(new Set()); - }); -}); + content: 'Sample Content', + } + const uniqueClassNames = new Set() + collectClassNames(node, uniqueClassNames) + expect(uniqueClassNames).toEqual(new Set()) + }) +}) diff --git a/htmlody/htmlody-utils.ts b/htmlody/htmlody-utils.ts index 5d1d247..033d143 100644 --- a/htmlody/htmlody-utils.ts +++ b/htmlody/htmlody-utils.ts @@ -1,13 +1,13 @@ -import { HtmlTags, htmlTagsSet } from "./constants"; -import { CRNode } from "./htmlody-plugins"; -import { Attributes, JsonHtmlNodeTree, JsonTagElNode } from "./htmlody-types"; +import { HtmlTags, htmlTagsSet } from './constants' +import { CRNode } from './htmlody-plugins' +import { Attributes, JsonHtmlNodeTree, JsonTagElNode } from './htmlody-types' export const retrieveElement = ( JsonHtmlNodeMap: Structure, element: keyof Structure, ) => { - return JsonHtmlNodeMap[element]; -}; + return JsonHtmlNodeMap[element] +} export const nodeFactory = < Extension extends Record, @@ -16,47 +16,47 @@ export const nodeFactory = < >( config: NodeConfigT, ) => { - const createNode = (options?: Omit): NodeT => { + const createNode = (options?: Omit): NodeT => { return { ...config, ...options, - }; - }; + } + } return { create: createNode, - }; -}; + } +} export function formatAttributes(attributes: Attributes): string { return Object.entries(attributes) .map(([key, value]) => `${key}="${value}"`) - .join(" "); + .join(' ') } export function isValidHtmlTag(tagName: HtmlTags): boolean { - return htmlTagsSet.has(tagName); + return htmlTagsSet.has(tagName) } export function isValidAttributesString(attributesStr: string): boolean { // Regex pattern for valid attribute format: key="value" - const attributePattern = /^(\s?[a-zA-Z-]+="[^"]*"\s?)+$/; - return attributePattern.test(attributesStr); + const attributePattern = /^(\s?[a-zA-Z-]+="[^"]*"\s?)+$/ + return attributePattern.test(attributesStr) } export function collectClassNames(node: JsonTagElNode, uniqueClassNames: Set) { - if (node.attributes && typeof node.attributes.class === "string") { - const classList = node.attributes.class.split(" "); - classList.forEach((cls) => uniqueClassNames.add(cls)); + if (node.attributes && typeof node.attributes.class === 'string') { + const classList = node.attributes.class.split(' ') + classList.forEach((cls) => uniqueClassNames.add(cls)) } } export const children = (children: JsonTagElNode[]) => { - const returnChildren: JsonHtmlNodeTree = {}; + const returnChildren: JsonHtmlNodeTree = {} for (let i = 0; i < children.length; i++) { - returnChildren[i] = children[i]; + returnChildren[i] = children[i] } - return returnChildren; -}; + return returnChildren +} diff --git a/htmlody/index.ts b/htmlody/index.ts index 41eabcb..f3c0d12 100644 --- a/htmlody/index.ts +++ b/htmlody/index.ts @@ -1,20 +1,9 @@ -export type { - Attributes, - ClassRecord, - ExtensionRec, - JsonHtmlNodeTree, - JsonTagElNode, -} from "./htmlody-types"; -export { children } from "./htmlody-utils"; +export type { Attributes, ClassRecord, ExtensionRec, JsonHtmlNodeTree, JsonTagElNode } from './htmlody-types' +export { children } from './htmlody-utils' -export { htmlodyBuilder, jsonToHtml } from "./json-to-html-engine"; +export { htmlodyBuilder, jsonToHtml } from './json-to-html-engine' -export { classRecordPlugin, markdownPlugin } from "./htmlody-plugins"; -export type { - CRNode, - ClassRecordAttributes, - HTMLodyPlugin, - MDNode, -} from "./htmlody-plugins"; +export { classRecordPlugin, markdownPlugin } from './htmlody-plugins' +export type { CRNode, ClassRecordAttributes, HTMLodyPlugin, MDNode } from './htmlody-plugins' -export { cc, uClass } from "./css-engine"; +export { cc, uClass } from './css-engine' diff --git a/htmlody/json-to-html-engine.test.ts b/htmlody/json-to-html-engine.test.ts index 8290518..e35309a 100644 --- a/htmlody/json-to-html-engine.test.ts +++ b/htmlody/json-to-html-engine.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from "bun:test"; -import { HTMLodyPlugin, classRecordPlugin, markdownPlugin } from "./htmlody-plugins"; -import { Attributes, JsonHtmlNodeTree } from "./htmlody-types"; -import { formatAttributes, isValidAttributesString } from "./htmlody-utils"; +import { describe, expect, it } from 'bun:test' +import { HTMLodyPlugin, classRecordPlugin, markdownPlugin } from './htmlody-plugins' +import { Attributes, JsonHtmlNodeTree } from './htmlody-types' +import { formatAttributes, isValidAttributesString } from './htmlody-utils' import { getHtmlTags, getValidatedAttributesStr, @@ -12,74 +12,74 @@ import { renderNodeToHtml, renderNodeWithPlugins, validateTagName, -} from "./json-to-html-engine"; +} from './json-to-html-engine' export function randomAttributes(): Attributes { return { id: `id-${Math.floor(Math.random() * 1000)}`, class: `class-${Math.floor(Math.random() * 1000)}`, - }; + } } function expectHtmlToMatch(html: string, expected: string) { - expect(html.replace(/\s+/g, "")).toBe(expected.replace(/\s+/g, "")); + expect(html.replace(/\s+/g, '')).toBe(expected.replace(/\s+/g, '')) } -describe("renderHtmlTag", () => { - it("should render HTML tag with attributes and content", () => { - const tagName = "div"; - const attributesStr = 'class="sample"'; - const content = "Hello World"; - const childrenHtml = "Child"; +describe('renderHtmlTag', () => { + it('should render HTML tag with attributes and content', () => { + const tagName = 'div' + const attributesStr = 'class="sample"' + const content = 'Hello World' + const childrenHtml = 'Child' const rendered = renderHtmlTag({ tagName, attributesStr, content, childrenHtml, - }); - expect(rendered).toContain(tagName); - expect(rendered).toContain(attributesStr); - expect(rendered).toContain(content); - expect(rendered).toContain(childrenHtml); - }); - describe("renderHtmlTag", () => { - it("should throw error for invalid tags", () => { - let errorOccurred = false; + }) + expect(rendered).toContain(tagName) + expect(rendered).toContain(attributesStr) + expect(rendered).toContain(content) + expect(rendered).toContain(childrenHtml) + }) + describe('renderHtmlTag', () => { + it('should throw error for invalid tags', () => { + let errorOccurred = false try { // @ts-expect-error - renderHtmlTag({ validate: true, tagName: "invalidTag" }); + renderHtmlTag({ validate: true, tagName: 'invalidTag' }) } catch (e) { - errorOccurred = true; + errorOccurred = true // @ts-expect-error - expect(e.message).toBe("Invalid tag name provided: invalidTag"); // If you want to check the error message + expect(e.message).toBe('Invalid tag name provided: invalidTag') // If you want to check the error message } - expect(errorOccurred).toBe(true); - }); - }); - it("should handle missing content and children", () => { + expect(errorOccurred).toBe(true) + }) + }) + it('should handle missing content and children', () => { const nodeMap: JsonHtmlNodeTree = { div_id1: { - tag: "div", + tag: 'div', attributes: randomAttributes(), }, - }; - const rendered = jsonToHtml(nodeMap, []); - expect(rendered).not.toContain("undefined"); - }); -}); - -describe("jsonToHtml", () => { - it("should apply plugins to nodes", () => { + } + const rendered = jsonToHtml(nodeMap, []) + expect(rendered).not.toContain('undefined') + }) +}) + +describe('jsonToHtml', () => { + it('should apply plugins to nodes', () => { const nodeMap = { div: { - tag: "div", + tag: 'div', cr: { - "sample-class": true, + 'sample-class': true, }, - content: "Sample Content", + content: 'Sample Content', }, - }; + } const plugins = [ { // @ts-expect-error @@ -89,27 +89,27 @@ describe("jsonToHtml", () => { ...node.attributes, class: Object.keys(node.cr) .filter((key) => node.cr[key]) - .join(" "), - }; + .join(' '), + } } - return node; + return node }, }, - ]; - const html = jsonToHtml(nodeMap, plugins); - expect(html).toBe('
Sample Content
'); - }); + ] + const html = jsonToHtml(nodeMap, plugins) + expect(html).toBe('
Sample Content
') + }) - it("should throw an error if tag name is not provided", () => { + it('should throw an error if tag name is not provided', () => { const nodeMap = { div: { attributes: { - class: "sample-class", + class: 'sample-class', }, - content: "Sample Content", + content: 'Sample Content', }, - }; // @ts-expect-error - const plugins = []; + } // @ts-expect-error + const plugins = [] expect(() => // @ts-expect-error @@ -118,251 +118,248 @@ describe("jsonToHtml", () => { }), ).toThrow( 'Tag name not provided for node. \n \n Content: Sample Content\n\n {\n "attributes": {\n "class": "sample-class"\n },\n "content": "Sample Content"\n}\n ', - ); - }); + ) + }) - it("should render entire HTML structure from node map", () => { + it('should render entire HTML structure from node map', () => { const nodeMap: JsonHtmlNodeTree = { div_id1: { - tag: "div", - content: "Sample Content", + tag: 'div', + content: 'Sample Content', attributes: { - id: "sample-id", - class: "sample-class", + id: 'sample-id', + class: 'sample-class', }, child: { span_id1: { - tag: "span", - content: "Child Content", + tag: 'span', + content: 'Child Content', }, }, }, - }; - - const rendered = jsonToHtml(nodeMap, []); - expect(rendered).toContain(nodeMap.div_id1.content); - expect(rendered).toContain(nodeMap.div_id1.attributes!.id); - expect(rendered).toContain(nodeMap.div_id1.attributes!.class); - expect(rendered).toContain(nodeMap.div_id1.child!.span_id1.content); - }); -}); - -describe("renderChildren", () => { - it("should render children recursively", () => { + } + + const rendered = jsonToHtml(nodeMap, []) + expect(rendered).toContain(nodeMap.div_id1.content) + expect(rendered).toContain(nodeMap.div_id1.attributes!.id) + expect(rendered).toContain(nodeMap.div_id1.attributes!.class) + expect(rendered).toContain(nodeMap.div_id1.child!.span_id1.content) + }) +}) + +describe('renderChildren', () => { + it('should render children recursively', () => { const children: JsonHtmlNodeTree = { div_id: { - tag: "div", + tag: 'div', content: `content-${Math.floor(Math.random() * 1000)}`, }, - }; - const rendered = renderChildrenNodes(children, []); - expect(rendered).toContain(children.div_id.content); - }); -}); + } + const rendered = renderChildrenNodes(children, []) + expect(rendered).toContain(children.div_id.content) + }) +}) -describe("getValidatedTagName", () => { +describe('getValidatedTagName', () => { it("should return the tag name if it's valid", () => { - const tagName = "div"; - const result = validateTagName(tagName); - expect(result).toBe(tagName); - }); + const tagName = 'div' + const result = validateTagName(tagName) + expect(result).toBe(tagName) + }) - it("should throw an error for invalid tags", () => { - expect(() => validateTagName("invalidTag")).toThrow("Invalid tag name provided: invalidTag"); - }); -}); + it('should throw an error for invalid tags', () => { + expect(() => validateTagName('invalidTag')).toThrow('Invalid tag name provided: invalidTag') + }) +}) -describe("getValidatedAttributesStr", () => { +describe('getValidatedAttributesStr', () => { it("should return the attributes string if it's valid", () => { - const attributesStr = 'id="test"'; - const result = getValidatedAttributesStr(attributesStr); - expect(result).toBe(attributesStr); - }); - - it("should throw an error for invalid attributes string", () => { - expect(() => getValidatedAttributesStr("invalid=attr")).toThrow("Invalid attributes string provided: invalid=attr"); - }); -}); - -describe("getHtmlTags", () => { - it("should return start and close tags for non-self closing tags", () => { - const { startTag, closeTag } = getHtmlTags("div", 'class="test"'); - expect(startTag).toBe('
'); - expect(closeTag).toBe("
"); - }); - - it("should return only start tag for self-closing tags", () => { - const { startTag, closeTag } = getHtmlTags("img", 'src="test.jpg"'); - expect(startTag).toBe(''); - expect(closeTag).toBe(""); - }); -}); - -describe("formatAttributes", () => { - it("should format an attributes object into a string", () => { + const attributesStr = 'id="test"' + const result = getValidatedAttributesStr(attributesStr) + expect(result).toBe(attributesStr) + }) + + it('should throw an error for invalid attributes string', () => { + expect(() => getValidatedAttributesStr('invalid=attr')).toThrow('Invalid attributes string provided: invalid=attr') + }) +}) + +describe('getHtmlTags', () => { + it('should return start and close tags for non-self closing tags', () => { + const { startTag, closeTag } = getHtmlTags('div', 'class="test"') + expect(startTag).toBe('
') + expect(closeTag).toBe('
') + }) + + it('should return only start tag for self-closing tags', () => { + const { startTag, closeTag } = getHtmlTags('img', 'src="test.jpg"') + expect(startTag).toBe('') + expect(closeTag).toBe('') + }) +}) + +describe('formatAttributes', () => { + it('should format an attributes object into a string', () => { const attributes = { - id: "test", - class: "sample", - }; - const result = formatAttributes(attributes); - expect(result).toBe('id="test" class="sample"'); - }); -}); - -describe("isValidAttributesString", () => { - it("should return true for valid attributes string", () => { - const isValid = isValidAttributesString('id="test" class="sample"'); - expect(isValid).toBe(true); - }); - - it("should return false for invalid attributes string", () => { - const isValid = isValidAttributesString("id=test class=sample"); - expect(isValid).toBe(false); - }); -}); - -describe("renderNodeToHtml", () => { - it("should throw an error if tag name is not provided", () => { + id: 'test', + class: 'sample', + } + const result = formatAttributes(attributes) + expect(result).toBe('id="test" class="sample"') + }) +}) + +describe('isValidAttributesString', () => { + it('should return true for valid attributes string', () => { + const isValid = isValidAttributesString('id="test" class="sample"') + expect(isValid).toBe(true) + }) + + it('should return false for invalid attributes string', () => { + const isValid = isValidAttributesString('id=test class=sample') + expect(isValid).toBe(false) + }) +}) + +describe('renderNodeToHtml', () => { + it('should throw an error if tag name is not provided', () => { const node = { attributes: { - class: "sample-class", + class: 'sample-class', }, - content: "Sample Content", - }; + content: 'Sample Content', + } // @ts-expect-error - expect(() => renderNodeToHtml(node, [])).toThrow("Tag name not provided for node."); - }); + expect(() => renderNodeToHtml(node, [])).toThrow('Tag name not provided for node.') + }) - it("should handle nested children in the node", () => { + it('should handle nested children in the node', () => { const node = { - tag: "div", + tag: 'div', attributes: { - id: "sample-id", - class: "sample-class", + id: 'sample-id', + class: 'sample-class', }, children: { span_id1: { - tag: "span", - content: "Child Content", + tag: 'span', + content: 'Child Content', }, }, - }; + } - const rendered = renderNodeToHtml(node, []); - expectHtmlToMatch(rendered, '
Child Content
'); - }); -}); + const rendered = renderNodeToHtml(node, []) + expectHtmlToMatch(rendered, '
Child Content
') + }) +}) -describe("createNodeFactory", () => { - const plugins = [classRecordPlugin, markdownPlugin] satisfies HTMLodyPlugin[]; +describe('createNodeFactory', () => { + const plugins = [classRecordPlugin, markdownPlugin] satisfies HTMLodyPlugin[] - const { nodeFactory, renderNodeTreeToHtml, renderSingleNode, renderChildren } = htmlodyBuilder({ plugins }); + const { nodeFactory, renderNodeTreeToHtml, renderSingleNode, renderChildren } = htmlodyBuilder({ plugins }) - describe("createNode", () => { - it("should create a default node with a div tag", () => { - const { div } = nodeFactory(); - const node = div(); + describe('createNode', () => { + it('should create a default node with a div tag', () => { + const { div } = nodeFactory() + const node = div() - node.content = "Sample Content"; + node.content = 'Sample Content' - expect(node.tag).toBe("div"); - }); - }); + expect(node.tag).toBe('div') + }) + }) //renderChildren - describe("renderChildren", () => { - it("should render children recursively", () => { + describe('renderChildren', () => { + it('should render children recursively', () => { const children: JsonHtmlNodeTree = { div_id: { - tag: "div", + tag: 'div', content: `content-${Math.floor(Math.random() * 1000)}`, }, - }; - const rendered = renderChildren(children); - expect(rendered).toContain(children.div_id.content); - }); - }); - - describe("renderHtml", () => { - it("should render HTML from a node map", () => { + } + const rendered = renderChildren(children) + expect(rendered).toContain(children.div_id.content) + }) + }) + + describe('renderHtml', () => { + it('should render HTML from a node map', () => { const nodeMap = { div_id1: { - tag: "div", - content: "Sample Content", + tag: 'div', + content: 'Sample Content', attributes: { - id: "sample-id", - class: "sample-class", + id: 'sample-id', + class: 'sample-class', }, children: { span_id1: { - tag: "span", - content: "Child Content", + tag: 'span', + content: 'Child Content', }, }, }, - }; + } - const html = renderNodeTreeToHtml(nodeMap); - expectHtmlToMatch( - html, - '
Sample ContentChild Content
', - ); - }); - }); + const html = renderNodeTreeToHtml(nodeMap) + expectHtmlToMatch(html, '
Sample ContentChild Content
') + }) + }) // check that markdown plugin works - it("should convert markdown to HTML", () => { - const { div } = nodeFactory(); + it('should convert markdown to HTML', () => { + const { div } = nodeFactory() const markdownDiv = div({ - markdown: "# Hello World!", - }); - const html = renderSingleNode(markdownDiv); - expectHtmlToMatch(html, "

Hello World!

"); - }); + markdown: '# Hello World!', + }) + const html = renderSingleNode(markdownDiv) + expectHtmlToMatch(html, '

Hello World!

') + }) - it("should apply plugins to the node", () => { - const { div } = nodeFactory(); + it('should apply plugins to the node', () => { + const { div } = nodeFactory() const node = div({ cr: { - "*": { - "bg-blue-200": true, + '*': { + 'bg-blue-200': true, }, }, - content: "Sample Content", - }); + content: 'Sample Content', + }) - const html = renderSingleNode(node); - expectHtmlToMatch(html, '
Sample Content
'); - }); + const html = renderSingleNode(node) + expectHtmlToMatch(html, '
Sample Content
') + }) - it("should render a single node to HTML", () => { - const div = nodeFactory().div; + it('should render a single node to HTML', () => { + const div = nodeFactory().div const node = div({ - content: "Sample Content", + content: 'Sample Content', attributes: { - id: "sample-id", - class: "sample-class", + id: 'sample-id', + class: 'sample-class', }, - }); - const html = renderNodeToHtml(node, []); - expectHtmlToMatch(html, '
Sample Content
'); - }); -}); - -describe("renderNodeWithPlugins", () => { - it("should throw an error if tag name is not provided", () => { + }) + const html = renderNodeToHtml(node, []) + expectHtmlToMatch(html, '
Sample Content
') + }) +}) + +describe('renderNodeWithPlugins', () => { + it('should throw an error if tag name is not provided', () => { const node = { attributes: { - id: "sample-id", + id: 'sample-id', }, - content: "Sample Content", - }; - const plugins = []; + content: 'Sample Content', + } + const plugins = [] expect(() => renderNodeWithPlugins(node, plugins)).toThrow( 'Tag name not provided for node. \n ID: id="sample-id"\n Content: Sample Content\n\n {\n "attributes": {\n "id": "sample-id"\n },\n "content": "Sample Content"\n}\n ', - ); - }); -}); + ) + }) +}) diff --git a/htmlody/json-to-html-engine.ts b/htmlody/json-to-html-engine.ts index 26b7464..0a497bf 100644 --- a/htmlody/json-to-html-engine.ts +++ b/htmlody/json-to-html-engine.ts @@ -1,39 +1,39 @@ -import { htmlRes, middlewareFactory } from "../server"; -import { HtmlTags, SELF_CLOSING_TAGS, htmlTags } from "./constants"; -import { generateCSS, generateColorVariables } from "./css-engine"; -import { HTMLodyPlugin } from "./htmlody-plugins"; -import { ExtensionRec, JsonHtmlNodeTree, JsonTagElNode } from "./htmlody-types"; -import { formatAttributes, isValidAttributesString, isValidHtmlTag } from "./htmlody-utils"; +import { htmlRes, middlewareFactory } from '../server' +import { HtmlTags, SELF_CLOSING_TAGS, htmlTags } from './constants' +import { generateCSS, generateColorVariables } from './css-engine' +import { HTMLodyPlugin } from './htmlody-plugins' +import { ExtensionRec, JsonHtmlNodeTree, JsonTagElNode } from './htmlody-types' +import { formatAttributes, isValidAttributesString, isValidHtmlTag } from './htmlody-utils' export function validateTagName(tagName: HtmlTags): string { if (!isValidHtmlTag(tagName)) { - throw new Error(`Invalid tag name provided: ${tagName}`); + throw new Error(`Invalid tag name provided: ${tagName}`) } - return tagName; + return tagName } export function getValidatedAttributesStr(attributesStr: string): string { - if (attributesStr !== "" && !isValidAttributesString(attributesStr)) { - throw new Error(`Invalid attributes string provided: ${attributesStr}`); + if (attributesStr !== '' && !isValidAttributesString(attributesStr)) { + throw new Error(`Invalid attributes string provided: ${attributesStr}`) } - return attributesStr; + return attributesStr } export function getHtmlTags(tagName: string, attributesStr: string): { startTag: string; closeTag: string } { - const space = attributesStr ? " " : ""; + const space = attributesStr ? ' ' : '' // Check if the tag is a self-closing tag using Set lookup if (SELF_CLOSING_TAGS.has(tagName)) { return { startTag: `<${tagName}${space}${attributesStr} />`, - closeTag: "", - }; + closeTag: '', + } } return { startTag: `<${tagName}${space}${attributesStr}>`, closeTag: ``, - }; + } } export function renderHtmlTag({ @@ -43,24 +43,24 @@ export function renderHtmlTag({ tagName, validate, }: { - tagName: HtmlTags; - attributesStr: string; - content: string; - childrenHtml: string; - validate?: boolean; + tagName: HtmlTags + attributesStr: string + content: string + childrenHtml: string + validate?: boolean }): string { if (validate) { - validateTagName(tagName); + validateTagName(tagName) } - const validatedAttributesStr = getValidatedAttributesStr(attributesStr); + const validatedAttributesStr = getValidatedAttributesStr(attributesStr) const { startTag, closeTag } = getHtmlTags( // validatedTagName, tagName, validatedAttributesStr, - ); + ) - return `${startTag}${content}${childrenHtml}${closeTag}`; + return `${startTag}${content}${childrenHtml}${closeTag}` } export function renderChildrenNodes[]>( @@ -69,7 +69,7 @@ export function renderChildrenNodes[]>( ): string { return Object.entries(children) .map(([childTagName, childNode]) => jsonToHtml({ [childTagName]: childNode }, plugins)) - .join(""); + .join('') } function processNodeWithPlugins< @@ -77,44 +77,44 @@ function processNodeWithPlugins< PluginProps extends ExtensionRec, Node extends JsonTagElNode = JsonTagElNode, >(node: Node, plugins: Plugins): Node { - let processedNode = { ...node }; + let processedNode = { ...node } for (const plugin of plugins) { - processedNode = plugin.processNode(processedNode); + processedNode = plugin.processNode(processedNode) } - return processedNode; + return processedNode } export type JSONToHTMLOptions = { - validateHtmlTags?: boolean; -}; + validateHtmlTags?: boolean +} export function renderNodeToHtml< Plugins extends HTMLodyPlugin[], PluginProps extends ExtensionRec, Node extends JsonTagElNode = JsonTagElNode, >(node: Node, plugins: Plugins, options?: JSONToHTMLOptions): string { - node = processNodeWithPlugins(node, plugins); + node = processNodeWithPlugins(node, plugins) - const idAttribute = node.attributes?.id ? `id="${node.attributes.id}"` : ""; + const idAttribute = node.attributes?.id ? `id="${node.attributes.id}"` : '' - const tagName = node.tag; + const tagName = node.tag - const content = node?.content || ""; + const content = node?.content || '' if (!tagName) { throw new Error( `Tag name not provided for node. - ${idAttribute ? `ID: ${idAttribute}` : ""} - ${content ? `Content: ${content}` : ""} + ${idAttribute ? `ID: ${idAttribute}` : ''} + ${content ? `Content: ${content}` : ''} ${JSON.stringify(node, null, 2)} `, - ); + ) } - const attributesStr = formatAttributes(node.attributes || {}); - const childrenHtml = node.child ? renderChildrenNodes(node.child, plugins) : ""; + const attributesStr = formatAttributes(node.attributes || {}) + const childrenHtml = node.child ? renderChildrenNodes(node.child, plugins) : '' - return renderHtmlTag({ tagName, attributesStr, content, childrenHtml }); + return renderHtmlTag({ tagName, attributesStr, content, childrenHtml }) } export function jsonToHtml< @@ -125,7 +125,7 @@ export function jsonToHtml< >(nodeMap: NodeMap, plugins: Plugins, options?: JSONToHTMLOptions): string { return Object.keys(nodeMap) .map((id) => renderNodeToHtml(nodeMap[id], plugins, options)) - .join(""); + .join('') } export function renderNodeWithPlugins< @@ -133,24 +133,24 @@ export function renderNodeWithPlugins< PluginProps extends ExtensionRec, Node extends JsonTagElNode = JsonTagElNode, >(node: Node, plugins: Plugins, options?: JSONToHTMLOptions): string { - node = processNodeWithPlugins(node, plugins); + node = processNodeWithPlugins(node, plugins) - const tagName = node.tag; - const content = node.content || ""; + const tagName = node.tag + const content = node.content || '' if (!tagName) { - const idAttribute = node.attributes?.id ? `id="${node.attributes.id}"` : ""; + const idAttribute = node.attributes?.id ? `id="${node.attributes.id}"` : '' throw new Error( `Tag name not provided for node. - ${idAttribute ? `ID: ${idAttribute}` : ""} - ${content ? `Content: ${content}` : ""} + ${idAttribute ? `ID: ${idAttribute}` : ''} + ${content ? `Content: ${content}` : ''} ${JSON.stringify(node, null, 2)} `, - ); + ) } - const attributesStr = formatAttributes(node.attributes || {}); - const childrenHtml = node.child ? renderChildrenNodes(node.child, plugins) : ""; + const attributesStr = formatAttributes(node.attributes || {}) + const childrenHtml = node.child ? renderChildrenNodes(node.child, plugins) : '' return renderHtmlTag({ tagName, @@ -158,104 +158,94 @@ export function renderNodeWithPlugins< content, childrenHtml, validate: options?.validateHtmlTags, - }); + }) } const generateTitleNode = (title: string): JsonTagElNode => { return { - tag: "title", + tag: 'title', content: title, - }; -}; + } +} -const generateMetaTagNode = (meta: { - name: string; - content: string; -}): JsonTagElNode => { +const generateMetaTagNode = (meta: { name: string; content: string }): JsonTagElNode => { return { - tag: "meta", + tag: 'meta', attributes: { name: meta.name, content: meta.content }, - }; -}; + } +} -const generateLinkTagNode = (link: { - rel: string; - href: string; -}): JsonTagElNode => { +const generateLinkTagNode = (link: { rel: string; href: string }): JsonTagElNode => { return { - tag: "link", + tag: 'link', attributes: { rel: link.rel, href: link.href }, - }; -}; + } +} const generateStyleTagNode = (content: string): JsonTagElNode => { return { - tag: "style", + tag: 'style', content, - }; -}; - -const generateScriptTagNode = (script: { - src?: string; - type?: string; - content: string; -}): JsonTagElNode => { + } +} + +const generateScriptTagNode = (script: { src?: string; type?: string; content: string }): JsonTagElNode => { const node: JsonTagElNode = { - tag: "script", + tag: 'script', attributes: {}, content: script.content, - }; + } if (script.src && node.attributes) { - node.attributes.src = script.src; + node.attributes.src = script.src } if (script.type && node.attributes) { - node.attributes.type = script.type; + node.attributes.type = script.type } - return node; -}; + return node +} -export type NodePluginsMapper[]> = ReturnType; +export type NodePluginsMapper[]> = ReturnType type HeadConfig = { - title: string; - metaTags?: { name: string; content: string }[]; - linkTags?: { rel: string; href: string }[]; - styleTags?: { content: string }[]; - scriptTags?: { src?: string; type: string; content: string }[]; -}; + title: string + metaTags?: { name: string; content: string }[] + linkTags?: { rel: string; href: string }[] + styleTags?: { content: string }[] + scriptTags?: { src?: string; type: string; content: string }[] +} type HTMLodyOptions = { - middleware?: ReturnType; -}; + middleware?: ReturnType +} export const htmlodyNodeFactory = < Plugins extends HTMLodyPlugin[], NodeWithPlugins extends NodePluginsMapper, - ReturnType extends Record) => JsonTagElNode> = Record< + ReturnType extends Record) => JsonTagElNode> = Record< HtmlTags, - (options?: Omit) => JsonTagElNode + (options?: Omit) => JsonTagElNode >, >(): ReturnType => { - const create = (tag: HtmlTags, options?: Omit) => { + const create = (tag: HtmlTags, options?: Omit) => { return { tag, - content: "", + content: '', attributes: {}, ...options, - } as JsonTagElNode; - }; + } as JsonTagElNode + } - const buildFns = {} as ReturnType; + const buildFns = {} as ReturnType for (const tag of htmlTags) { - buildFns[tag] = (options?: Omit) => create(tag, options); + buildFns[tag] = (options?: Omit) => create(tag, options) } - return buildFns; -}; + return buildFns +} export const htmlodyBuilder = < Plugins extends HTMLodyPlugin[], @@ -265,118 +255,118 @@ export const htmlodyBuilder = < plugins, options: builderOptions, }: { - plugins: Plugins; + plugins: Plugins options?: { allpages: { - headConfig?: HeadConfig; - }; - }; + headConfig?: HeadConfig + } + } }) => { - const effectivePlugins = plugins; + const effectivePlugins = plugins const nodeFactory = () => { - return htmlodyNodeFactory(); - }; + return htmlodyNodeFactory() + } const inferTreeFn = < Node extends JsonTagElNode = JsonTagElNode, >(): JsonHtmlNodeTree => { - return undefined as unknown as JsonHtmlNodeTree; - }; + return undefined as unknown as JsonHtmlNodeTree + } - const inferTree = inferTreeFn(); + const inferTree = inferTreeFn() const renderNodeTreeToHtml = (nodeMap: JsonHtmlNodeTree, pluginsOverride?: Plugins): string => { - const activePlugins = pluginsOverride || effectivePlugins; + const activePlugins = pluginsOverride || effectivePlugins return Object.keys(nodeMap) .map((id) => renderNodeWithPlugins(nodeMap[id], activePlugins)) - .join(""); - }; + .join('') + } const renderSingleNode = = JsonTagElNode>( node: Node, pluginsOverride?: Plugins, ): string => { - const activePlugins = pluginsOverride || effectivePlugins; - return renderNodeWithPlugins(node, activePlugins); - }; + const activePlugins = pluginsOverride || effectivePlugins + return renderNodeWithPlugins(node, activePlugins) + } const renderChildren = (children: JsonHtmlNodeTree, pluginsOverride?: Plugins): string => { - const activePlugins = pluginsOverride || effectivePlugins; + const activePlugins = pluginsOverride || effectivePlugins return Object.entries(children) .map(([childTagName, childNode]) => renderNodeWithPlugins(childNode, activePlugins)) - .join(""); - }; + .join('') + } const buildHtmlDoc = >( bodyConfig: JSONNodeTree, options?: { - headConfig?: HeadConfig; + headConfig?: HeadConfig }, ) => { - const headNodes: JsonHtmlNodeTree = {}; + const headNodes: JsonHtmlNodeTree = {} if (options?.headConfig?.title) { - headNodes["title"] = generateTitleNode(options.headConfig.title); + headNodes['title'] = generateTitleNode(options.headConfig.title) } if (builderOptions?.allpages?.headConfig?.title) { - headNodes["title"] = generateTitleNode(builderOptions?.allpages?.headConfig.title); + headNodes['title'] = generateTitleNode(builderOptions?.allpages?.headConfig.title) } if (options?.headConfig?.metaTags) { options.headConfig.metaTags.forEach((meta, index) => { - headNodes[`meta${index}`] = generateMetaTagNode(meta); - }); + headNodes[`meta${index}`] = generateMetaTagNode(meta) + }) } if (builderOptions?.allpages?.headConfig?.metaTags) { builderOptions?.allpages?.headConfig.metaTags.forEach((meta, index) => { - headNodes[`meta${index}`] = generateMetaTagNode(meta); - }); + headNodes[`meta${index}`] = generateMetaTagNode(meta) + }) } if (options?.headConfig?.linkTags) { options.headConfig.linkTags.forEach((link, index) => { - headNodes[`link${index}`] = generateLinkTagNode(link); - }); + headNodes[`link${index}`] = generateLinkTagNode(link) + }) } if (builderOptions?.allpages?.headConfig?.linkTags) { builderOptions?.allpages?.headConfig.linkTags.forEach((link, index) => { - headNodes[`link${index}`] = generateLinkTagNode(link); - }); + headNodes[`link${index}`] = generateLinkTagNode(link) + }) } if (options?.headConfig?.styleTags) { options.headConfig.styleTags.forEach((style, index) => { - headNodes[`style${index}`] = generateStyleTagNode(style.content); - }); + headNodes[`style${index}`] = generateStyleTagNode(style.content) + }) } if (builderOptions?.allpages?.headConfig?.styleTags) { builderOptions?.allpages?.headConfig.styleTags.forEach((style, index) => { - headNodes[`style${index}`] = generateStyleTagNode(style.content); - }); + headNodes[`style${index}`] = generateStyleTagNode(style.content) + }) } if (options?.headConfig?.scriptTags) { options.headConfig.scriptTags.forEach((script, index) => { - headNodes[`script${index}`] = generateScriptTagNode(script); - }); + headNodes[`script${index}`] = generateScriptTagNode(script) + }) } if (builderOptions?.allpages?.headConfig?.scriptTags) { builderOptions?.allpages?.headConfig.scriptTags.forEach((script, index) => { - headNodes[`script${index}`] = generateScriptTagNode(script); - }); + headNodes[`script${index}`] = generateScriptTagNode(script) + }) } - const headHtml = renderNodeTreeToHtml(headNodes); - const bodyHtml = renderNodeTreeToHtml(bodyConfig); + const headHtml = renderNodeTreeToHtml(headNodes) + const bodyHtml = renderNodeTreeToHtml(bodyConfig) // todo this needs to be externalized since this is specific to the class records plugin - const css = generateCSS(bodyConfig); - const colorVariables = generateColorVariables(); + const css = generateCSS(bodyConfig) + const colorVariables = generateColorVariables() return ` @@ -390,22 +380,22 @@ export const htmlodyBuilder = < ${bodyHtml} - `; - }; + ` + } const response = ( bodyConfig: JsonHtmlNodeTree, options?: { - headConfig?: HeadConfig; + headConfig?: HeadConfig }, ) => { - const html = buildHtmlDoc(bodyConfig, options); + const html = buildHtmlDoc(bodyConfig, options) return new Response(html, { headers: { - "Content-Type": "text/html; charset=utf-8", + 'Content-Type': 'text/html; charset=utf-8', }, - }); - }; + }) + } const warp = ({ id, src }: { id: string; src: string }) => { const turboFrameNode = ({ @@ -413,24 +403,24 @@ export const htmlodyBuilder = < src, children, }: { - id: string; - src?: string; - children: JsonHtmlNodeTree; + id: string + src?: string + children: JsonHtmlNodeTree }) => { const turboFrame: JsonTagElNode = { - tag: "turbo-frame", + tag: 'turbo-frame', attributes: { id, }, child: children, - }; + } if (src && turboFrame?.attributes) { - turboFrame.attributes.src = src; + turboFrame.attributes.src = src } - return turboFrame; - }; + return turboFrame + } return { pushNode: (content: JsonTagElNode) => { @@ -439,9 +429,9 @@ export const htmlodyBuilder = < children: { CONTENT: content, }, - }); + }) - return htmlRes(renderSingleNode(tfNode)); + return htmlRes(renderSingleNode(tfNode)) }, docNodeMount: (content: JsonTagElNode) => { return turboFrameNode({ @@ -450,10 +440,10 @@ export const htmlodyBuilder = < children: { CONTENT: content, }, - }); + }) }, - }; - }; + } + } return { nodeFactory, @@ -464,5 +454,5 @@ export const htmlodyBuilder = < response, inferTree, warp, - }; -}; + } +} diff --git a/htmlody/page-confg.ts b/htmlody/page-confg.ts index 5cae688..9c891d8 100644 --- a/htmlody/page-confg.ts +++ b/htmlody/page-confg.ts @@ -1,51 +1,51 @@ -import { JsonHtmlNodeTree, JsonTagElNode } from "."; -import { CRNode, MDNode } from "./htmlody-plugins"; +import { JsonHtmlNodeTree, JsonTagElNode } from '.' +import { CRNode, MDNode } from './htmlody-plugins' // import { pageFactory } from "./html-generator"; -type AppNode = CRNode & MDNode; +type AppNode = CRNode & MDNode export const htmxButton: JsonTagElNode = { - content: "Click Me", + content: 'Click Me', attributes: { - "hx-post": "/clicked", - "hx-trigger": "click", - "hx-target": "#clicked", - "hx-swap": "outerHTML", + 'hx-post': '/clicked', + 'hx-trigger': 'click', + 'hx-target': '#clicked', + 'hx-swap': 'outerHTML', }, - tag: "button", -}; + tag: 'button', +} export const htmlBody: JsonHtmlNodeTree = { h1: { - content: "Hello World", + content: 'Hello World', attributes: { - class: "bg-blue-500", - id: "title-id", + class: 'bg-blue-500', + id: 'title-id', }, - tag: "h1", + tag: 'h1', }, - p: { content: "This is a description", tag: "p" }, + p: { content: 'This is a description', tag: 'p' }, a: { - content: "Click Me", + content: 'Click Me', attributes: { - href: "https://www.example.com", + href: 'https://www.example.com', }, - tag: "a", + tag: 'a', }, div_1: { - tag: "div", + tag: 'div', attributes: { - class: "bg-red-500", + class: 'bg-red-500', }, child: { button_1: htmxButton, div_1: { - tag: "div", - content: "Hello World", + tag: 'div', + content: 'Hello World', }, }, }, -}; +} diff --git a/index.ts b/index.ts index f18eb7e..3117043 100644 --- a/index.ts +++ b/index.ts @@ -1,22 +1,22 @@ -import * as auth from "./auth"; -import * as cli from "./cli"; -import * as cookies from "./cookies"; -import * as dataGen from "./data-gen"; -import * as deploy from "./deploy"; -import * as fetcher from "./fetcher"; -import * as filesFolders from "./files-folders"; -import * as htmlody from "./htmlody"; -import * as jwt from "./jwt"; -import * as logger from "./logger"; -import * as npm from "./npm-release"; -import * as server from "./server"; -import * as sqlite from "./sqlite"; -import * as state from "./state"; -import * as uuid from "./uuid"; -import * as validation from "./validation"; +import * as auth from './auth' +import * as cli from './cli' +import * as cookies from './cookies' +import * as dataGen from './data-gen' +import * as deploy from './deploy' +import * as fetcher from './fetcher' +import * as filesFolders from './files-folders' +import * as htmlody from './htmlody' +import * as jwt from './jwt' +import * as logger from './logger' +import * as npm from './npm-release' +import * as server from './server' +import * as sqlite from './sqlite' +import * as state from './state' +import * as uuid from './uuid' +import * as validation from './validation' // utility exports -import * as utils from "./utils/classy"; +import * as utils from './utils/classy' export { auth, @@ -36,4 +36,4 @@ export { utils, uuid, validation, -}; +} diff --git a/jwt/index.ts b/jwt/index.ts index 88586fd..3e02d28 100644 --- a/jwt/index.ts +++ b/jwt/index.ts @@ -1,3 +1,3 @@ -export { jwtBackend as jwtBack } from "./jwt-be"; -export { jwtClient as jwtFront } from "./jwt-client"; -export { createJwtFileHandlers } from "./jwt-token-file-handlers"; +export { jwtBackend as jwtBack } from './jwt-be' +export { jwtClient as jwtFront } from './jwt-client' +export { createJwtFileHandlers } from './jwt-token-file-handlers' diff --git a/jwt/jwt-be.test.ts b/jwt/jwt-be.test.ts index 858f202..0c6ac58 100644 --- a/jwt/jwt-be.test.ts +++ b/jwt/jwt-be.test.ts @@ -1,174 +1,174 @@ -import { afterAll, afterEach, describe, expect, it, jest } from "bun:test"; -import { jwtBackend } from "./jwt-be"; -import { base64UrlDecode, base64UrlEncode, sign } from "./jwt-server-utils"; -import { createJwtFileHandlers } from "./jwt-token-file-handlers"; +import { afterAll, afterEach, describe, expect, it, jest } from 'bun:test' +import { jwtBackend } from './jwt-be' +import { base64UrlDecode, base64UrlEncode, sign } from './jwt-server-utils' +import { createJwtFileHandlers } from './jwt-token-file-handlers' -describe("JWT Server Side Factory", () => { - const factorySecret = "test-secret"; // For testing purposes only +describe('JWT Server Side Factory', () => { + const factorySecret = 'test-secret' // For testing purposes only const jwtFactory = jwtBackend({ - handlers: createJwtFileHandlers("./jwt-tokens.json"), + handlers: createJwtFileHandlers('./jwt-tokens.json'), factorySignSecret: factorySecret, - }); - const testPayload = { userId: 12345, roles: ["user"] }; + }) + const testPayload = { userId: 12345, roles: ['user'] } afterAll(() => { Bun.spawnSync({ - cmd: ["rm", "jwt-tokens.json"], - }); - }); + cmd: ['rm', 'jwt-tokens.json'], + }) + }) - describe("JWT Creation & Verification", () => { - it("should create and verify JWT correctly", async () => { + describe('JWT Creation & Verification', () => { + it('should create and verify JWT correctly', async () => { const jwt = jwtFactory.createJwt({ payload: testPayload, - }); - expect(jwt).not.toBe(null); - - const { header, payload } = await jwtFactory.verifyJwt(jwt, factorySecret); - expect(header).toEqual({ alg: "HS256", typ: "JWT" }); - expect(payload.userId).toBe(testPayload.userId); - expect(payload.roles[0]).toBe(testPayload.roles[0]); - }); - }); - - describe("JWT Encryption & Decryption", () => { - it("should throw error for tampered JWT", async () => { + }) + expect(jwt).not.toBe(null) + + const { header, payload } = await jwtFactory.verifyJwt(jwt, factorySecret) + expect(header).toEqual({ alg: 'HS256', typ: 'JWT' }) + expect(payload.userId).toBe(testPayload.userId) + expect(payload.roles[0]).toBe(testPayload.roles[0]) + }) + }) + + describe('JWT Encryption & Decryption', () => { + it('should throw error for tampered JWT', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - const tamperedJwt = jwt.substring(0, 10) + "XX" + jwt.substring(12); // Tampering the JWT - expect(async () => await jwtFactory.verifyJwt(tamperedJwt, factorySecret)).toThrow(Error); - }); - }); - - describe("Utility Functions", () => { - it("should correctly encode and then decode base64Url", () => { - const testStr = "Hello, World!"; - const encoded = base64UrlEncode(testStr); - const decoded = base64UrlDecode(encoded); - expect(decoded).toBe(testStr); - }); - - it("should sign and verify data correctly", () => { - const data = "some random data"; - const signature = sign(data, factorySecret); - const verification = sign(data, factorySecret); - expect(signature).toBe(verification); - }); - - it("should throw error for wrong signature", () => { - const data = "some random data"; - const signature = sign(data, factorySecret + "tamper"); - const verification = sign(data, factorySecret); - expect(signature).not.toBe(verification); - }); - }); - - describe("Edge Cases", () => { - it("should invalidate and then check and throw error for a token", async () => { + }) + const tamperedJwt = jwt.substring(0, 10) + 'XX' + jwt.substring(12) // Tampering the JWT + expect(async () => await jwtFactory.verifyJwt(tamperedJwt, factorySecret)).toThrow(Error) + }) + }) + + describe('Utility Functions', () => { + it('should correctly encode and then decode base64Url', () => { + const testStr = 'Hello, World!' + const encoded = base64UrlEncode(testStr) + const decoded = base64UrlDecode(encoded) + expect(decoded).toBe(testStr) + }) + + it('should sign and verify data correctly', () => { + const data = 'some random data' + const signature = sign(data, factorySecret) + const verification = sign(data, factorySecret) + expect(signature).toBe(verification) + }) + + it('should throw error for wrong signature', () => { + const data = 'some random data' + const signature = sign(data, factorySecret + 'tamper') + const verification = sign(data, factorySecret) + expect(signature).not.toBe(verification) + }) + }) + + describe('Edge Cases', () => { + it('should invalidate and then check and throw error for a token', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - await jwtFactory.invalidateToken(jwt); + }) + await jwtFactory.invalidateToken(jwt) - expect(async () => await jwtFactory.verifyJwt(jwt, factorySecret)).toThrow(Error); - }); - }); + expect(async () => await jwtFactory.verifyJwt(jwt, factorySecret)).toThrow(Error) + }) + }) - describe("Token Blacklisting", () => { - it("should not allow invalidated token to be used", async () => { + describe('Token Blacklisting', () => { + it('should not allow invalidated token to be used', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - await jwtFactory.invalidateToken(jwt); - expect(async () => await jwtFactory.verifyJwt(jwt, factorySecret)).toThrow(Error); - }); - }); - - describe("Token Structure", () => { - it("should have a valid JWT structure", async () => { + }) + await jwtFactory.invalidateToken(jwt) + expect(async () => await jwtFactory.verifyJwt(jwt, factorySecret)).toThrow(Error) + }) + }) + + describe('Token Structure', () => { + it('should have a valid JWT structure', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - expect(jwt.split(".").length).toBe(3); - }); + }) + expect(jwt.split('.').length).toBe(3) + }) - it("should throw an error for tampered encrypted tokens", async () => { + it('should throw an error for tampered encrypted tokens', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - const tamperedJwt = jwt.substring(0, 10) + "XX" + jwt.substring(12); - expect(async () => await jwtFactory.verifyJwt(tamperedJwt, factorySecret)).toThrow(); - }); - }); - - describe("Refresh Tokens", () => { - it("should invalidate token and check it", async () => { - const refreshToken = await jwtFactory.generateRefreshToken(); - await jwtFactory.invalidateToken(refreshToken); - expect(await jwtFactory.validateRefreshToken(refreshToken)).toBeFalsy(); - }); - }); - - describe("Signature", () => { - it("should throw error for tampered JWT signature", async () => { + }) + const tamperedJwt = jwt.substring(0, 10) + 'XX' + jwt.substring(12) + expect(async () => await jwtFactory.verifyJwt(tamperedJwt, factorySecret)).toThrow() + }) + }) + + describe('Refresh Tokens', () => { + it('should invalidate token and check it', async () => { + const refreshToken = await jwtFactory.generateRefreshToken() + await jwtFactory.invalidateToken(refreshToken) + expect(await jwtFactory.validateRefreshToken(refreshToken)).toBeFalsy() + }) + }) + + describe('Signature', () => { + it('should throw error for tampered JWT signature', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - const parts = jwt.split("."); - const tamperedJwt = `${parts[0]}.${parts[1]}.tamperedSignature`; - expect(async () => await jwtFactory.verifyJwt(tamperedJwt, factorySecret)).toThrow("Invalid signature"); - }); - }); - - describe("JWT Expiration", () => { + }) + const parts = jwt.split('.') + const tamperedJwt = `${parts[0]}.${parts[1]}.tamperedSignature` + expect(async () => await jwtFactory.verifyJwt(tamperedJwt, factorySecret)).toThrow('Invalid signature') + }) + }) + + describe('JWT Expiration', () => { // Mock Date.now() to simulate the passage of time - const _DateNow = Date.now; + const _DateNow = Date.now function mockDateNow(mockedTime: number) { - Date.now = jest.fn(() => mockedTime); + Date.now = jest.fn(() => mockedTime) } afterEach(() => { - Date.now = _DateNow; - }); + Date.now = _DateNow + }) - it("should verify JWT correctly before expiration", () => { + it('should verify JWT correctly before expiration', () => { // Create a JWT that expires in 2 seconds const jwt = jwtFactory.createJwt({ expiresIn: 2, payload: testPayload, - }); - expect(() => jwtFactory.verifyJwt(jwt, factorySecret)).not.toThrow(); - }); + }) + expect(() => jwtFactory.verifyJwt(jwt, factorySecret)).not.toThrow() + }) - it("should throw error for expired JWT", async () => { + it('should throw error for expired JWT', async () => { // Create a JWT that expires in 2 seconds const jwt = jwtFactory.createJwt({ expiresIn: 2, payload: testPayload, - }); + }) // Simulate the passage of 3 seconds - mockDateNow(_DateNow() + 3000); + mockDateNow(_DateNow() + 3000) // Now, the token should be expired - expect(async () => await jwtFactory.verifyJwt(jwt, factorySecret)).toThrow("Token expired"); - }); - }); -}); + expect(async () => await jwtFactory.verifyJwt(jwt, factorySecret)).toThrow('Token expired') + }) + }) +}) -describe("new tokens not on invalid list", () => { - const factorySecret = "test-secret"; // For testing purposes only +describe('new tokens not on invalid list', () => { + const factorySecret = 'test-secret' // For testing purposes only const jwtFactory = jwtBackend({ - handlers: createJwtFileHandlers("./jwt-tokens.json"), + handlers: createJwtFileHandlers('./jwt-tokens.json'), factorySignSecret: factorySecret, - }); - const testPayload = { userId: 12345, roles: ["user"] }; + }) + const testPayload = { userId: 12345, roles: ['user'] } - it("newly created token should not be on the blacklist", async () => { + it('newly created token should not be on the blacklist', async () => { const jwt = await jwtFactory.createJwt({ payload: testPayload, - }); - expect(async () => jwtFactory.verifyJwt(jwt, factorySecret)).not.toThrow(); - }); -}); + }) + expect(async () => jwtFactory.verifyJwt(jwt, factorySecret)).not.toThrow() + }) +}) diff --git a/jwt/jwt-be.ts b/jwt/jwt-be.ts index bff699b..b48d885 100644 --- a/jwt/jwt-be.ts +++ b/jwt/jwt-be.ts @@ -1,134 +1,132 @@ -import crypto from "crypto"; +import crypto from 'crypto' import { - createJwtHeader, - decodeJwt, - decrypt, - encodeJwt, - encrypt, - isTokenExpired, - payloadValidator, -} from "./jwt-server-utils"; -import { JwtHeader, JwtPayload, RefreshToken } from "./jwt-types"; + createJwtHeader, + decodeJwt, + decrypt, + encodeJwt, + encrypt, + isTokenExpired, + payloadValidator, +} from './jwt-server-utils' +import { JwtHeader, JwtPayload, RefreshToken } from './jwt-types' export interface JwtHandlers { - getInvalidTokens: () => Promise; - addInvalidToken: (token: string) => Promise; - getRefreshTokens: () => Promise; - saveRefreshToken: (token: RefreshToken) => Promise; - removeRefreshToken: (token: string) => Promise; + getInvalidTokens: () => Promise + addInvalidToken: (token: string) => Promise + getRefreshTokens: () => Promise + saveRefreshToken: (token: RefreshToken) => Promise + removeRefreshToken: (token: string) => Promise } // backend jwt handling export const jwtBackend = < - Payload extends object, - FactoryJwtPayload extends JwtPayload = JwtPayload, + Payload extends object, + FactoryJwtPayload extends JwtPayload = JwtPayload, >({ - factorySignSecret, - handlers, - encryption, + factorySignSecret, + handlers, + encryption, }: { - factorySignSecret: string; - handlers: JwtHandlers; - encryption?: { - encryptionSecret: string; - }; + factorySignSecret: string + handlers: JwtHandlers + encryption?: { + encryptionSecret: string + } }) => { - async function generateRefreshToken(): Promise { - const refreshToken = crypto.randomBytes(40).toString("hex"); - const expiresIn = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7; // Token valid for one week - const tokenData = { token: refreshToken, exp: expiresIn }; - await handlers.saveRefreshToken(tokenData); - return refreshToken; - } - - async function validateRefreshToken(token: string): Promise { - if (await isValidToken(token)) { - return false; - } - - const refreshTokens = await handlers.getRefreshTokens(); - const refreshToken = refreshTokens.find((t) => t.token === token); - if (!refreshToken) { - return false; - } - if (refreshToken.exp < Math.floor(Date.now() / 1000)) { - await handlers.removeRefreshToken(token); - return false; - } - return true; - } - - async function invalidateToken(token: string): Promise { - await handlers.addInvalidToken(token); - } - - async function isValidToken(token: string): Promise { - const invalidTokens = await handlers.getInvalidTokens(); - - return invalidTokens.includes(token); - } - - function createJwt({ - payload, - signSecret = factorySignSecret, - expiresIn = 60 * 60 * 24 * 7, // one week - encryptionSecret = encryption?.encryptionSecret, - }: { - payload: CreatePayload; - signSecret?: string; - expiresIn?: number; - // encryption must be enabled on the factory in order for this to work - encryptionSecret?: string; - }): string { - const finalPayload: JwtPayload = { - ...payload, - }; - try { - payloadValidator(payload); - } catch (e) { - throw new Error("Invalid Payload"); - } - - const header = createJwtHeader(); - finalPayload.exp = Math.floor(Date.now() / 1000) + expiresIn; - - // JWT is already sign - const jwt = encodeJwt(header, finalPayload, signSecret); - return encryption && encryptionSecret - ? encrypt(jwt, encryptionSecret) - : jwt; - } - - async function verifyJwt( - token: string, - signSecret: string = factorySignSecret, - // encryption must be enabled on the factory in order for this to work - encryptionSecret: string | undefined = encryption?.encryptionSecret, - ): Promise<{ header: JwtHeader; payload: FactoryJwtPayload }> { - let decryptedToken: string = token; - - if (encryption && encryptionSecret) { - decryptedToken = decrypt(token, encryptionSecret); - } - - if (await isValidToken(token)) { - throw new Error("This token is blacklisted"); - } - - const { header, payload } = decodeJwt(decryptedToken, signSecret); - - if (isTokenExpired(payload)) { - throw new Error("Token expired"); - } - - return { header, payload }; - } - - return { - createJwt, - verifyJwt, - generateRefreshToken, - validateRefreshToken, - invalidateToken, - }; -}; + async function generateRefreshToken(): Promise { + const refreshToken = crypto.randomBytes(40).toString('hex') + const expiresIn = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7 // Token valid for one week + const tokenData = { token: refreshToken, exp: expiresIn } + await handlers.saveRefreshToken(tokenData) + return refreshToken + } + + async function validateRefreshToken(token: string): Promise { + if (await isValidToken(token)) { + return false + } + + const refreshTokens = await handlers.getRefreshTokens() + const refreshToken = refreshTokens.find((t) => t.token === token) + if (!refreshToken) { + return false + } + if (refreshToken.exp < Math.floor(Date.now() / 1000)) { + await handlers.removeRefreshToken(token) + return false + } + return true + } + + async function invalidateToken(token: string): Promise { + await handlers.addInvalidToken(token) + } + + async function isValidToken(token: string): Promise { + const invalidTokens = await handlers.getInvalidTokens() + + return invalidTokens.includes(token) + } + + function createJwt({ + payload, + signSecret = factorySignSecret, + expiresIn = 60 * 60 * 24 * 7, // one week + encryptionSecret = encryption?.encryptionSecret, + }: { + payload: CreatePayload + signSecret?: string + expiresIn?: number + // encryption must be enabled on the factory in order for this to work + encryptionSecret?: string + }): string { + const finalPayload: JwtPayload = { + ...payload, + } + try { + payloadValidator(payload) + } catch (e) { + throw new Error('Invalid Payload') + } + + const header = createJwtHeader() + finalPayload.exp = Math.floor(Date.now() / 1000) + expiresIn + + // JWT is already sign + const jwt = encodeJwt(header, finalPayload, signSecret) + return encryption && encryptionSecret ? encrypt(jwt, encryptionSecret) : jwt + } + + async function verifyJwt( + token: string, + signSecret: string = factorySignSecret, + // encryption must be enabled on the factory in order for this to work + encryptionSecret: string | undefined = encryption?.encryptionSecret, + ): Promise<{ header: JwtHeader; payload: FactoryJwtPayload }> { + let decryptedToken: string = token + + if (encryption && encryptionSecret) { + decryptedToken = decrypt(token, encryptionSecret) + } + + if (await isValidToken(token)) { + throw new Error('This token is blacklisted') + } + + const { header, payload } = decodeJwt(decryptedToken, signSecret) + + if (isTokenExpired(payload)) { + throw new Error('Token expired') + } + + return { header, payload } + } + + return { + createJwt, + verifyJwt, + generateRefreshToken, + validateRefreshToken, + invalidateToken, + } +} diff --git a/jwt/jwt-client.test.ts b/jwt/jwt-client.test.ts index 8900d45..e02fea1 100644 --- a/jwt/jwt-client.test.ts +++ b/jwt/jwt-client.test.ts @@ -1,68 +1,68 @@ -import { describe, expect, test } from "bun:test"; -import { jwtClient } from "./jwt-client"; +import { describe, expect, test } from 'bun:test' +import { jwtClient } from './jwt-client' const base64url = (source: Buffer) => { // Encode in classical base64 - let encodedSource = source.toString("base64"); + let encodedSource = source.toString('base64') // Remove padding equal characters - encodedSource = encodedSource.replace(/=+$/, ""); + encodedSource = encodedSource.replace(/=+$/, '') // Replace characters according to base64url specifications - encodedSource = encodedSource.replace(/\+/g, "-"); - encodedSource = encodedSource.replace(/\//g, "_"); + encodedSource = encodedSource.replace(/\+/g, '-') + encodedSource = encodedSource.replace(/\//g, '_') - return encodedSource; -}; + return encodedSource +} -describe("jwtClientSideFactory", () => { - const jwtService = jwtClient(); +describe('jwtClientSideFactory', () => { + const jwtService = jwtClient() - test("decodeJwt decodes a JWT", () => { + test('decodeJwt decodes a JWT', () => { const token = base64url(Buffer.from('{"alg":"HS256","typ":"JWT"}')) + - "." + - base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1609459200}')); - const decodedToken = jwtService.decodeJwt(token); - expect(decodedToken.header).toEqual({ alg: "HS256", typ: "JWT" }); + '.' + + base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1609459200}')) + const decodedToken = jwtService.decodeJwt(token) + expect(decodedToken.header).toEqual({ alg: 'HS256', typ: 'JWT' }) expect(decodedToken.payload).toEqual({ - sub: "1234567890", - name: "John Doe", - roles: ["admin"], + sub: '1234567890', + name: 'John Doe', + roles: ['admin'], exp: 1609459200, - }); - }); + }) + }) - test("isJwtExpired checks if a JWT is expired", () => { + test('isJwtExpired checks if a JWT is expired', () => { const expiredToken = base64url(Buffer.from('{"alg":"HS256","typ":"JWT"}')) + - "." + - base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1609459200}')); + '.' + + base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1609459200}')) const unexpiredToken = base64url(Buffer.from('{"alg":"HS256","typ":"JWT"}')) + - "." + - base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1909459200}')); - expect(jwtService.isJwtExpired(expiredToken)).toBeTruthy(); - expect(jwtService.isJwtExpired(unexpiredToken)).toBeFalsy(); - }); + '.' + + base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1909459200}')) + expect(jwtService.isJwtExpired(expiredToken)).toBeTruthy() + expect(jwtService.isJwtExpired(unexpiredToken)).toBeFalsy() + }) - test("hasRole checks if a JWT contains a role", () => { + test('hasRole checks if a JWT contains a role', () => { const token = base64url(Buffer.from('{"alg":"HS256","typ":"JWT"}')) + - "." + - base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1609459200}')); - expect(jwtService.hasRole(token, "admin")).toBeTruthy(); - expect(jwtService.hasRole(token, "user")).toBeFalsy(); - }); + '.' + + base64url(Buffer.from('{"sub":"1234567890","name":"John Doe","roles":["admin"],"exp":1609459200}')) + expect(jwtService.hasRole(token, 'admin')).toBeTruthy() + expect(jwtService.hasRole(token, 'user')).toBeFalsy() + }) - test("setRefreshToken and getRefreshToken manage the refresh token", () => { - jwtService.setRefreshToken("refreshToken"); - expect(jwtService.getRefreshToken()).toBe("refreshToken"); - }); + test('setRefreshToken and getRefreshToken manage the refresh token', () => { + jwtService.setRefreshToken('refreshToken') + expect(jwtService.getRefreshToken()).toBe('refreshToken') + }) - test("clearRefreshToken clears the refresh token", () => { - jwtService.setRefreshToken("refreshToken"); - jwtService.clearRefreshToken(); - expect(jwtService.getRefreshToken()).toBeNull(); - }); -}); + test('clearRefreshToken clears the refresh token', () => { + jwtService.setRefreshToken('refreshToken') + jwtService.clearRefreshToken() + expect(jwtService.getRefreshToken()).toBeNull() + }) +}) diff --git a/jwt/jwt-client.ts b/jwt/jwt-client.ts index 41024f4..a686443 100644 --- a/jwt/jwt-client.ts +++ b/jwt/jwt-client.ts @@ -1,40 +1,40 @@ // frontend export const jwtClient = () => { function decodeJwt(token: string) { - const [headerEncoded, payloadEncoded] = token.split("."); - const headerJson = Buffer.from(headerEncoded, "base64").toString(); - const payloadJson = Buffer.from(payloadEncoded, "base64").toString(); + const [headerEncoded, payloadEncoded] = token.split('.') + const headerJson = Buffer.from(headerEncoded, 'base64').toString() + const payloadJson = Buffer.from(payloadEncoded, 'base64').toString() return { header: JSON.parse(headerJson), payload: JSON.parse(payloadJson), - }; + } } function isJwtExpired(token: string) { - const { payload } = decodeJwt(token); - const currentTime = Math.floor(Date.now() / 1000); + const { payload } = decodeJwt(token) + const currentTime = Math.floor(Date.now() / 1000) - return payload.exp && payload.exp < currentTime; + return payload.exp && payload.exp < currentTime } function hasRole(token: string, role: string) { - const { payload } = decodeJwt(token); - return payload.roles && payload.roles.includes(role); + const { payload } = decodeJwt(token) + return payload.roles && payload.roles.includes(role) } - let refreshToken: string | null = null; + let refreshToken: string | null = null function setRefreshToken(token: string) { - refreshToken = token; + refreshToken = token } function getRefreshToken() { - return refreshToken; + return refreshToken } function clearRefreshToken() { - refreshToken = null; + refreshToken = null } return { @@ -44,5 +44,5 @@ export const jwtClient = () => { setRefreshToken, getRefreshToken, clearRefreshToken, - }; -}; + } +} diff --git a/jwt/jwt-server-utils.test.ts b/jwt/jwt-server-utils.test.ts index ea0d0b9..1b6e883 100644 --- a/jwt/jwt-server-utils.test.ts +++ b/jwt/jwt-server-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "bun:test"; +import { describe, expect, it } from 'bun:test' import { base64UrlDecode, base64UrlEncode, @@ -9,117 +9,117 @@ import { isTokenExpired, payloadValidator, sign, -} from "./jwt-server-utils"; -import { JwtHeader, JwtPayload } from "./jwt-types"; +} from './jwt-server-utils' +import { JwtHeader, JwtPayload } from './jwt-types' -const secret = "test-secret"; // For testing purposes only -const header: JwtHeader = { alg: "HS256", typ: "JWT" }; +const secret = 'test-secret' // For testing purposes only +const header: JwtHeader = { alg: 'HS256', typ: 'JWT' } const payload: JwtPayload = { userId: 12345, - roles: ["user"], + roles: ['user'], exp: Math.floor(Date.now() / 1000) + 3600, -}; - -describe("JWT Utility Functions", () => { - describe("Encryption & Decryption", () => { - it("should encrypt and then correctly decrypt data", () => { - const data = "testData"; - const encryptedData = encrypt(data, secret); - expect(encryptedData).not.toBe(data); // Ensure data is encrypted - - const decryptedData = decrypt(encryptedData, secret); - expect(decryptedData).toBe(data); - }); - - it("should throw error for tampered encrypted data", () => { - const data = "testData"; - const encryptedData = encrypt(data, secret); - const tamperedData = encryptedData.substring(0, encryptedData.length - 10) + "TAMPER"; - expect(() => decrypt(tamperedData, secret)).toThrow("Unable to decrypt token"); - }); - }); - - describe("Base64Url Encoding & Decoding", () => { - it("should correctly encode and then decode base64Url", () => { - const testStr = "Hello, World!"; - const encoded = base64UrlEncode(testStr); - const decoded = base64UrlDecode(encoded); - expect(decoded).toBe(testStr); - }); - - it("should handle special characters correctly", () => { - const testStr = "Hello+World/="; - const encoded = base64UrlEncode(testStr); - const decoded = base64UrlDecode(encoded); - expect(decoded).toBe(testStr); - }); - }); - - describe("JWT Encoding & Decoding", () => { - it("should correctly encode and then decode JWT", () => { - const jwt = encodeJwt(header, payload, secret); - const { header: decodedHeader, payload: decodedPayload } = decodeJwt(jwt, secret); - - expect(decodedHeader).toEqual(header); - expect(decodedPayload.userId).toBe(payload.userId); - expect(decodedPayload.roles[0]).toBe(payload.roles[0]); - }); - - it("should throw error for tampered JWT signature", () => { - const jwt = encodeJwt(header, payload, secret); - const parts = jwt.split("."); - const tamperedJwt = `${parts[0]}.${parts[1]}.tamperedSignature`; - expect(() => decodeJwt(tamperedJwt, secret)).toThrow("Invalid signature"); - }); - }); - - describe("Payload Validation", () => { - it("should validate non-empty payload", () => { - const validPayload = { userId: 12345, roles: ["user"] }; - expect(() => payloadValidator(validPayload)).not.toThrow(Error); - }); - - it("should throw error for empty payload", () => { - expect(() => payloadValidator({})).toThrow("Payload cannot be empty"); - }); - }); - - describe("Data Signing", () => { - it("should sign and verify data correctly", () => { - const data = "some random data"; - const signature1 = sign(data, secret); - const signature2 = sign(data, secret); - expect(signature1).toBe(signature2); - }); - - it("should produce different signatures for different secrets", () => { - const data = "some random data"; - const signature1 = sign(data, secret); - const signature2 = sign(data, secret + "tamper"); - expect(signature1).not.toBe(signature2); - }); - }); - - describe("Token Expiry Check", () => { - it("should correctly identify an unexpired token", () => { +} + +describe('JWT Utility Functions', () => { + describe('Encryption & Decryption', () => { + it('should encrypt and then correctly decrypt data', () => { + const data = 'testData' + const encryptedData = encrypt(data, secret) + expect(encryptedData).not.toBe(data) // Ensure data is encrypted + + const decryptedData = decrypt(encryptedData, secret) + expect(decryptedData).toBe(data) + }) + + it('should throw error for tampered encrypted data', () => { + const data = 'testData' + const encryptedData = encrypt(data, secret) + const tamperedData = encryptedData.substring(0, encryptedData.length - 10) + 'TAMPER' + expect(() => decrypt(tamperedData, secret)).toThrow('Unable to decrypt token') + }) + }) + + describe('Base64Url Encoding & Decoding', () => { + it('should correctly encode and then decode base64Url', () => { + const testStr = 'Hello, World!' + const encoded = base64UrlEncode(testStr) + const decoded = base64UrlDecode(encoded) + expect(decoded).toBe(testStr) + }) + + it('should handle special characters correctly', () => { + const testStr = 'Hello+World/=' + const encoded = base64UrlEncode(testStr) + const decoded = base64UrlDecode(encoded) + expect(decoded).toBe(testStr) + }) + }) + + describe('JWT Encoding & Decoding', () => { + it('should correctly encode and then decode JWT', () => { + const jwt = encodeJwt(header, payload, secret) + const { header: decodedHeader, payload: decodedPayload } = decodeJwt(jwt, secret) + + expect(decodedHeader).toEqual(header) + expect(decodedPayload.userId).toBe(payload.userId) + expect(decodedPayload.roles[0]).toBe(payload.roles[0]) + }) + + it('should throw error for tampered JWT signature', () => { + const jwt = encodeJwt(header, payload, secret) + const parts = jwt.split('.') + const tamperedJwt = `${parts[0]}.${parts[1]}.tamperedSignature` + expect(() => decodeJwt(tamperedJwt, secret)).toThrow('Invalid signature') + }) + }) + + describe('Payload Validation', () => { + it('should validate non-empty payload', () => { + const validPayload = { userId: 12345, roles: ['user'] } + expect(() => payloadValidator(validPayload)).not.toThrow(Error) + }) + + it('should throw error for empty payload', () => { + expect(() => payloadValidator({})).toThrow('Payload cannot be empty') + }) + }) + + describe('Data Signing', () => { + it('should sign and verify data correctly', () => { + const data = 'some random data' + const signature1 = sign(data, secret) + const signature2 = sign(data, secret) + expect(signature1).toBe(signature2) + }) + + it('should produce different signatures for different secrets', () => { + const data = 'some random data' + const signature1 = sign(data, secret) + const signature2 = sign(data, secret + 'tamper') + expect(signature1).not.toBe(signature2) + }) + }) + + describe('Token Expiry Check', () => { + it('should correctly identify an unexpired token', () => { const unexpiredPayload: JwtPayload = { ...payload, exp: Math.floor(Date.now() / 1000) + 3600, - }; - expect(isTokenExpired(unexpiredPayload)).toBe(false); - }); + } + expect(isTokenExpired(unexpiredPayload)).toBe(false) + }) - it("should correctly identify an expired token", () => { + it('should correctly identify an expired token', () => { const expiredPayload: JwtPayload = { ...payload, exp: Math.floor(Date.now() / 1000) - 3600, - }; - expect(isTokenExpired(expiredPayload)).toBe(true); - }); - - it("should return false for tokens without expiration", () => { - const noExpiryPayload: JwtPayload = { userId: 12345, roles: ["user"] }; - expect(isTokenExpired(noExpiryPayload)).toBe(false); - }); - }); -}); + } + expect(isTokenExpired(expiredPayload)).toBe(true) + }) + + it('should return false for tokens without expiration', () => { + const noExpiryPayload: JwtPayload = { userId: 12345, roles: ['user'] } + expect(isTokenExpired(noExpiryPayload)).toBe(false) + }) + }) +}) diff --git a/jwt/jwt-server-utils.ts b/jwt/jwt-server-utils.ts index 894751e..f54dc84 100644 --- a/jwt/jwt-server-utils.ts +++ b/jwt/jwt-server-utils.ts @@ -1,95 +1,89 @@ -import crypto from "crypto"; -import { JwtHeader, JwtPayload } from "./jwt-types"; +import crypto from 'crypto' +import { JwtHeader, JwtPayload } from './jwt-types' export function encrypt(data: string, secret: string): string { - const algorithm = "aes-256-gcm"; - const key = crypto.createHash("sha256").update(secret).digest(); // Ensure 256-bit key - const iv = crypto.randomBytes(12); // 96-bit IV for AES-GCM - const cipher = crypto.createCipheriv(algorithm, key, iv); - let encrypted = cipher.update(data, "utf8", "hex"); - encrypted += cipher.final("hex"); - const authTag = cipher.getAuthTag(); - return `${iv.toString("hex")}.${encrypted}.${authTag.toString("hex")}`; + const algorithm = 'aes-256-gcm' + const key = crypto.createHash('sha256').update(secret).digest() // Ensure 256-bit key + const iv = crypto.randomBytes(12) // 96-bit IV for AES-GCM + const cipher = crypto.createCipheriv(algorithm, key, iv) + let encrypted = cipher.update(data, 'utf8', 'hex') + encrypted += cipher.final('hex') + const authTag = cipher.getAuthTag() + return `${iv.toString('hex')}.${encrypted}.${authTag.toString('hex')}` } export function decrypt(encryptedData: string, secret: string): string { try { - const algorithm = "aes-256-gcm"; - const key = crypto.createHash("sha256").update(secret).digest(); // Ensure 256-bit key - const [ivHex, encrypted, authTagHex] = encryptedData.split("."); - const iv = Buffer.from(ivHex, "hex"); - const decipher = crypto.createDecipheriv(algorithm, key, iv); - decipher.setAuthTag(Buffer.from(authTagHex, "hex")); - let decrypted = decipher.update(encrypted, "hex", "utf8"); - decrypted += decipher.final("utf8"); - return decrypted; + const algorithm = 'aes-256-gcm' + const key = crypto.createHash('sha256').update(secret).digest() // Ensure 256-bit key + const [ivHex, encrypted, authTagHex] = encryptedData.split('.') + const iv = Buffer.from(ivHex, 'hex') + const decipher = crypto.createDecipheriv(algorithm, key, iv) + decipher.setAuthTag(Buffer.from(authTagHex, 'hex')) + let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decrypted += decipher.final('utf8') + return decrypted } catch (e) { - console.error(e); - throw new Error("Unable to decrypt token"); + console.error(e) + throw new Error('Unable to decrypt token') } } -export function payloadValidator( - payload: JwtPayload -): boolean { +export function payloadValidator(payload: JwtPayload): boolean { if (!payload || Object.keys(payload).length === 0) { - throw new Error("Payload cannot be empty"); + throw new Error('Payload cannot be empty') } - return true; + return true } export function sign(data: string, secret: string): string { // TODO: maybe use hash factory here? - const hmac = crypto.createHmac("sha256", secret); - hmac.update(data); - return hmac.digest("base64"); + const hmac = crypto.createHmac('sha256', secret) + hmac.update(data) + return hmac.digest('base64') } export function createJwtHeader(): JwtHeader { - return { alg: "HS256", typ: "JWT" }; + return { alg: 'HS256', typ: 'JWT' } } export function base64UrlEncode(str: string): string { - const base64 = Buffer.from(str).toString("base64"); - return base64.replace("+", "-").replace("/", "_").replace(/=+$/, ""); + const base64 = Buffer.from(str).toString('base64') + return base64.replace('+', '-').replace('/', '_').replace(/=+$/, '') } export function base64UrlDecode(str: string): string { - str = str.replace("-", "+").replace("_", "/"); + str = str.replace('-', '+').replace('_', '/') while (str.length % 4) { - str += "="; + str += '=' } - return Buffer.from(str, "base64").toString(); + return Buffer.from(str, 'base64').toString() } -export function encodeJwt( - header: JwtHeader, - payload: JwtPayload, - secret: string -): string { - const headerEncoded = base64UrlEncode(JSON.stringify(header)); - const payloadEncoded = base64UrlEncode(JSON.stringify(payload)); - const signature = sign(`${headerEncoded}.${payloadEncoded}`, secret); - return `${headerEncoded}.${payloadEncoded}.${base64UrlEncode(signature)}`; +export function encodeJwt(header: JwtHeader, payload: JwtPayload, secret: string): string { + const headerEncoded = base64UrlEncode(JSON.stringify(header)) + const payloadEncoded = base64UrlEncode(JSON.stringify(payload)) + const signature = sign(`${headerEncoded}.${payloadEncoded}`, secret) + return `${headerEncoded}.${payloadEncoded}.${base64UrlEncode(signature)}` } export function decodeJwt( jwt: string, - secret: string + secret: string, ): { header: JwtHeader; payload: JwtPayload } { - const [headerEncoded, payloadEncoded, signatureEncoded] = jwt.split("."); - const signatureToVerify = sign(`${headerEncoded}.${payloadEncoded}`, secret); + const [headerEncoded, payloadEncoded, signatureEncoded] = jwt.split('.') + const signatureToVerify = sign(`${headerEncoded}.${payloadEncoded}`, secret) if (base64UrlEncode(signatureToVerify) !== signatureEncoded) { - throw new Error("Invalid signature"); + throw new Error('Invalid signature') } return { header: JSON.parse(base64UrlDecode(headerEncoded)), payload: JSON.parse(base64UrlDecode(payloadEncoded)), - }; + } } export function isTokenExpired(payload: JwtPayload): boolean { - return !!(payload.exp && payload.exp < Math.floor(Date.now() / 1000)); + return !!(payload.exp && payload.exp < Math.floor(Date.now() / 1000)) } diff --git a/jwt/jwt-token-file-handlers.ts b/jwt/jwt-token-file-handlers.ts index fb9cce3..653c9af 100644 --- a/jwt/jwt-token-file-handlers.ts +++ b/jwt/jwt-token-file-handlers.ts @@ -1,54 +1,54 @@ -import { JwtHandlers } from "./jwt-be"; -import { RefreshToken } from "./jwt-types"; +import { JwtHandlers } from './jwt-be' +import { RefreshToken } from './jwt-types' export interface StoredRefreshToken extends RefreshToken { - token: string; - exp: number; + token: string + exp: number } export interface JwtStorage { - invalidTokens: string[]; - refreshTokens: StoredRefreshToken[]; + invalidTokens: string[] + refreshTokens: StoredRefreshToken[] } async function getJwtStorage(filePath: string): Promise { - const storageFile = Bun.file(filePath); - if (storageFile.size === 0) return { invalidTokens: [], refreshTokens: [] }; - const content = await storageFile.text(); - return JSON.parse(content); + const storageFile = Bun.file(filePath) + if (storageFile.size === 0) return { invalidTokens: [], refreshTokens: [] } + const content = await storageFile.text() + return JSON.parse(content) } async function saveJwtStorage(data: JwtStorage, filePath: string): Promise { - const storageFile = Bun.file(filePath); - await Bun.write(storageFile, JSON.stringify(data)); + const storageFile = Bun.file(filePath) + await Bun.write(storageFile, JSON.stringify(data)) } async function getRefreshTokens(filePath: string): Promise { - const storage = await getJwtStorage(filePath); - return storage.refreshTokens; + const storage = await getJwtStorage(filePath) + return storage.refreshTokens } async function saveRefreshToken(token: StoredRefreshToken, filePath: string): Promise { - const storage = await getJwtStorage(filePath); - storage.refreshTokens.push(token); - await saveJwtStorage(storage, filePath); + const storage = await getJwtStorage(filePath) + storage.refreshTokens.push(token) + await saveJwtStorage(storage, filePath) } async function removeRefreshToken(token: string, filePath: string): Promise { - const storage = await getJwtStorage(filePath); - storage.refreshTokens = storage.refreshTokens.filter((t) => t.token !== token); - await saveJwtStorage(storage, filePath); + const storage = await getJwtStorage(filePath) + storage.refreshTokens = storage.refreshTokens.filter((t) => t.token !== token) + await saveJwtStorage(storage, filePath) } async function getInvalidTokens(filePath: string): Promise { - const storage = await getJwtStorage(filePath); - return storage.invalidTokens; + const storage = await getJwtStorage(filePath) + return storage.invalidTokens } async function addInvalidToken(token: string, filePath: string): Promise { - const storage = await getJwtStorage(filePath); - storage.invalidTokens.push(token); - await saveJwtStorage(storage, filePath); + const storage = await getJwtStorage(filePath) + storage.invalidTokens.push(token) + await saveJwtStorage(storage, filePath) } export function createJwtFileHandlers(defaultFilePath: string): JwtHandlers { @@ -58,5 +58,5 @@ export function createJwtFileHandlers(defaultFilePath: string): JwtHandlers { getRefreshTokens: () => getRefreshTokens(defaultFilePath), removeRefreshToken: (token: string) => removeRefreshToken(token, defaultFilePath), saveRefreshToken: (token: StoredRefreshToken) => saveRefreshToken(token, defaultFilePath), - }; + } } diff --git a/jwt/jwt-types.ts b/jwt/jwt-types.ts index 97f62a8..b09c12e 100644 --- a/jwt/jwt-types.ts +++ b/jwt/jwt-types.ts @@ -1,14 +1,14 @@ export interface JwtHeader { - alg: string; - typ: string; + alg: string + typ: string } export type JwtPayload = { - exp?: number; - roles?: string[]; -} & T; + exp?: number + roles?: string[] +} & T export interface RefreshToken { - token: string; - exp: number; + token: string + exp: number } diff --git a/logger/client-logger.ts b/logger/client-logger.ts index 4309a3e..dfa0685 100644 --- a/logger/client-logger.ts +++ b/logger/client-logger.ts @@ -1,4 +1,4 @@ -import { createLoggerFactory } from "./create-logger-factory"; +import { createLoggerFactory } from './create-logger-factory' // Client-side logger export const clientLogger = createLoggerFactory((level, message, code, data) => { @@ -7,9 +7,9 @@ export const clientLogger = createLoggerFactory((level, message, code, data) => // Here, you can replace `sendErrorToServer` with the actual function that sends the error to the server const sendErrorToServer = async (level: string, message: string, code: string | undefined, data: any) => { // TODO Implementation for sending the error to the server - }; - sendErrorToServer(level, message, code, JSON.stringify(data)); + } + sendErrorToServer(level, message, code, JSON.stringify(data)) } catch (error) { - console.error("Error sending log to server:", error); + console.error('Error sending log to server:', error) } -}); +}) diff --git a/logger/create-logger-factory.ts b/logger/create-logger-factory.ts index 61a8006..fde8839 100644 --- a/logger/create-logger-factory.ts +++ b/logger/create-logger-factory.ts @@ -1,50 +1,50 @@ -export type LogLevel = "info" | "warn" | "error"; +export type LogLevel = 'info' | 'warn' | 'error' -const logLevels: Record<"INFO" | "WARN" | "ERROR", LogLevel> = { - INFO: "info", - WARN: "warn", - ERROR: "error", -}; +const logLevels: Record<'INFO' | 'WARN' | 'ERROR', LogLevel> = { + INFO: 'info', + WARN: 'warn', + ERROR: 'error', +} -export type ErrorCodes = keyof typeof errorCodeMap; +export type ErrorCodes = keyof typeof errorCodeMap export const errorCodeMap = { - API_ERROR: "API_ERROR", - AUTHENTICATION_ERROR: "AUTHENTICATION_ERROR", - AUTHORIZATION_ERROR: "AUTHORIZATION_ERROR", - DATABASE_ERROR: "DATABASE_ERROR", - FORBIDDEN_ERROR: "FORBIDDEN_ERROR", - STOPPING_SERVER_ERROR: "STOPPING_SERVER_ERROR", -}; + API_ERROR: 'API_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + DATABASE_ERROR: 'DATABASE_ERROR', + FORBIDDEN_ERROR: 'FORBIDDEN_ERROR', + STOPPING_SERVER_ERROR: 'STOPPING_SERVER_ERROR', +} -export type LoggerFunction = (level: LogLevel, message: string, data: any, code?: ErrorCodes | undefined) => void; +export type LoggerFunction = (level: LogLevel, message: string, data: any, code?: ErrorCodes | undefined) => void export function createLoggerFactory(loggerFunction: LoggerFunction) { const processLog = (level: LogLevel) => (message: string, data: any, code?: ErrorCodes) => { - const loggableData = data ? JSON.stringify(data) : ""; + const loggableData = data ? JSON.stringify(data) : '' - const logPayload: string[] = []; + const logPayload: string[] = [] - logPayload.push(`[${level.toUpperCase()}] DT: ${new Date().toISOString()}`); - logPayload.push(`MESSAGE: ${message}`); + logPayload.push(`[${level.toUpperCase()}] DT: ${new Date().toISOString()}`) + logPayload.push(`MESSAGE: ${message}`) if (loggableData) { - logPayload.push(`DATA: ${loggableData}`); + logPayload.push(`DATA: ${loggableData}`) } if (code) { - logPayload.push(`CODE: ${code}`); + logPayload.push(`CODE: ${code}`) } // Use the passed loggerFunction to handle logging - loggerFunction(level, message, code, data); - }; + loggerFunction(level, message, code, data) + } return { info: processLog(logLevels.INFO), warn: processLog(logLevels.WARN), error: processLog(logLevels.ERROR), - }; + } } // Usage: diff --git a/logger/default-logger.ts b/logger/default-logger.ts index 813aebb..b8d4aa0 100644 --- a/logger/default-logger.ts +++ b/logger/default-logger.ts @@ -1,29 +1,29 @@ -import { createLoggerFactory } from "./create-logger-factory"; +import { createLoggerFactory } from './create-logger-factory' export const defaultLogger = createLoggerFactory((level, message, code, data) => { - if (level === "error") { + if (level === 'error') { console.error({ level, message, code, data, - }); + }) } - if (level === "warn") { + if (level === 'warn') { console.warn({ level, message, code, data, - }); + }) } - if (level === "info") { + if (level === 'info') { console.info({ level, message, code, data, - }); + }) } -}); +}) diff --git a/logger/index.ts b/logger/index.ts index af9b39b..3a5b252 100644 --- a/logger/index.ts +++ b/logger/index.ts @@ -1,4 +1,4 @@ -export { clientLogger } from "./client-logger"; -export { createLoggerFactory } from "./create-logger-factory"; -export { defaultLogger } from "./default-logger"; -export { serverLogger } from "./server-logger.server"; +export { clientLogger } from './client-logger' +export { createLoggerFactory } from './create-logger-factory' +export { defaultLogger } from './default-logger' +export { serverLogger } from './server-logger.server' diff --git a/logger/server-logger.server.ts b/logger/server-logger.server.ts index 14bb07b..cd1cb5e 100644 --- a/logger/server-logger.server.ts +++ b/logger/server-logger.server.ts @@ -1,12 +1,12 @@ -import { createLoggerFactory, type LogLevel } from "./create-logger-factory"; +import { createLoggerFactory, type LogLevel } from './create-logger-factory' export async function logToDB(level: LogLevel, message: string, code: string | undefined) { try { // log to db here } catch (error) { - console.error("Logging Error:", error); + console.error('Logging Error:', error) } } // Server-side logger -export const serverLogger = createLoggerFactory(logToDB); +export const serverLogger = createLoggerFactory(logToDB) diff --git a/npm-release/index.ts b/npm-release/index.ts index e52f635..be94807 100644 --- a/npm-release/index.ts +++ b/npm-release/index.ts @@ -5,5 +5,5 @@ export { setupNpmAuth, updatePackageVersion, updateVersion, -} from "./npm-release"; -export type { NpmReleaseFactoryOptions } from "./npm-release"; +} from './npm-release' +export type { NpmReleaseFactoryOptions } from './npm-release' diff --git a/npm-release/npm-release.test.ts b/npm-release/npm-release.test.ts index bd96ca9..e54df93 100644 --- a/npm-release/npm-release.test.ts +++ b/npm-release/npm-release.test.ts @@ -1,90 +1,90 @@ -import { describe, expect, jest, test } from "bun:test"; -import { isTestFile } from "../test-utils"; -import { getCurrentVersion, npmPublish, updateVersion } from "./npm-release"; -describe("getCurrentVersion", () => { - test("returns the version from the package.json", async () => { - const packagePath = isTestFile(import.meta) ? import.meta.dir + "/mock-package.json" : "./package.json"; +import { describe, expect, jest, test } from 'bun:test' +import { isTestFile } from '../test-utils' +import { getCurrentVersion, npmPublish, updateVersion } from './npm-release' +describe('getCurrentVersion', () => { + test('returns the version from the package.json', async () => { + const packagePath = isTestFile(import.meta) ? import.meta.dir + '/mock-package.json' : './package.json' - const version = await getCurrentVersion(packagePath); - const hasThreeParts = version.split(".").length === 3; + const version = await getCurrentVersion(packagePath) + const hasThreeParts = version.split('.').length === 3 - expect(hasThreeParts).toBe(true); - }); -}); + expect(hasThreeParts).toBe(true) + }) +}) -describe("updateVersion", () => { - test("increments patch version by default", () => { +describe('updateVersion', () => { + test('increments patch version by default', () => { const version = updateVersion({ - currentVersion: "1.0.0", - }); - expect(version).toEqual("1.0.1"); - }); + currentVersion: '1.0.0', + }) + expect(version).toEqual('1.0.1') + }) - test("increments minor version", () => { + test('increments minor version', () => { const version = updateVersion({ - currentVersion: "1.0.0", - increment: "minor", - }); - expect(version).toEqual("1.1.0"); - }); - test("increments major version", () => { + currentVersion: '1.0.0', + increment: 'minor', + }) + expect(version).toEqual('1.1.0') + }) + test('increments major version', () => { const version = updateVersion({ - currentVersion: "1.0.0", - increment: "major", - }); - expect(version).toEqual("2.0.0"); - }); + currentVersion: '1.0.0', + increment: 'major', + }) + expect(version).toEqual('2.0.0') + }) - test("increments major with alpha", () => { + test('increments major with alpha', () => { const version = updateVersion({ - currentVersion: "1.0.0", - increment: "major", + currentVersion: '1.0.0', + increment: 'major', isAlpha: true, - }); + }) - expect(version).toContain("alpha"); - expect(version).toStartWith("2.0.0"); - }); + expect(version).toContain('alpha') + expect(version).toStartWith('2.0.0') + }) - test("increments alpha version with hash", () => { + test('increments alpha version with hash', () => { const version = updateVersion({ - currentVersion: "1.0.0", - increment: "patch", + currentVersion: '1.0.0', + increment: 'patch', isAlpha: true, - }); - expect(version).toMatch(/1\.0\.1-alpha\.[a-z0-9]{8}/); - }); + }) + expect(version).toMatch(/1\.0\.1-alpha\.[a-z0-9]{8}/) + }) - test("increments alpha version with hash", () => { + test('increments alpha version with hash', () => { const version = updateVersion({ - currentVersion: "1.0.0", - increment: "patch", + currentVersion: '1.0.0', + increment: 'patch', isBeta: true, - }); - expect(version).toMatch(/1\.0\.1-beta\.[a-z0-9]{8}/); - }); -}); + }) + expect(version).toMatch(/1\.0\.1-beta\.[a-z0-9]{8}/) + }) +}) -describe("npmPublish", () => { - test("retries on version conflict", async () => { +describe('npmPublish', () => { + test('retries on version conflict', async () => { // Mock failed publish due to version conflict Bun.spawnSync = jest .fn() .mockReturnValueOnce({ - stdout: "", - stderr: "Cannot publish over previously published version", + stdout: '', + stderr: 'Cannot publish over previously published version', }) .mockReturnValueOnce({ - stdout: "Published", - stderr: "", - }); + stdout: 'Published', + stderr: '', + }) await npmPublish({ - packagePath: "./package.json", + packagePath: './package.json', isAlpha: false, maxRetries: 2, - }); + }) - expect(Bun.spawnSync).toHaveBeenCalledTimes(2); - }); -}); + expect(Bun.spawnSync).toHaveBeenCalledTimes(2) + }) +}) diff --git a/npm-release/npm-release.ts b/npm-release/npm-release.ts index 89aea15..563c45e 100644 --- a/npm-release/npm-release.ts +++ b/npm-release/npm-release.ts @@ -1,78 +1,78 @@ -import path from "path"; -import { exit } from "process"; -import { ulog } from "../utils/ulog"; +import path from 'path' +import { exit } from 'process' +import { ulog } from '../utils/ulog' export type NpmReleaseFactoryOptions = { - maxRetries?: number; - npmToken?: string; -}; + maxRetries?: number + npmToken?: string +} export const getCurrentVersion = async (packagePath: string): Promise => { - const packageData = await Bun.file(packagePath).text(); - const parsedData = JSON.parse(packageData); - return parsedData.version; -}; + const packageData = await Bun.file(packagePath).text() + const parsedData = JSON.parse(packageData) + return parsedData.version +} export const setupNpmAuth = (npmToken: string) => { try { if (!npmToken) { - console.error("NPM_TOKEN is not set in environment variables."); - throw new Error("NPM_TOKEN is not set in environment variables."); + console.error('NPM_TOKEN is not set in environment variables.') + throw new Error('NPM_TOKEN is not set in environment variables.') } - const npmrcContent = `//registry.npmjs.org/:_authToken=${npmToken}`; - const npmrcPath = path.resolve(process.cwd(), ".npmrc"); - Bun.write(npmrcPath, npmrcContent); + const npmrcContent = `//registry.npmjs.org/:_authToken=${npmToken}` + const npmrcPath = path.resolve(process.cwd(), '.npmrc') + Bun.write(npmrcPath, npmrcContent) } catch (error) { - console.error("Failed to set up npm authentication:", error); - throw error; + console.error('Failed to set up npm authentication:', error) + throw error } -}; +} -export type SemanticVersions = "major" | "minor" | "patch"; +export type SemanticVersions = 'major' | 'minor' | 'patch' export interface VersionConfig { - currentVersion: string; - increment?: SemanticVersions; - isAlpha?: boolean; - isBeta?: boolean; - hash?: string; + currentVersion: string + increment?: SemanticVersions + isAlpha?: boolean + isBeta?: boolean + hash?: string } export const updateVersion = (config: VersionConfig): string => { - const { currentVersion, increment = "patch", isAlpha, isBeta } = config; + const { currentVersion, increment = 'patch', isAlpha, isBeta } = config - const hash = config.hash ? config.hash : Math.random().toString(36).substr(2, 8); + const hash = config.hash ? config.hash : Math.random().toString(36).substr(2, 8) - const [major, minor, patch] = currentVersion.split(".").map(Number); - let newMajor = major; - let newMinor = minor; - let newPatch = patch; + const [major, minor, patch] = currentVersion.split('.').map(Number) + let newMajor = major + let newMinor = minor + let newPatch = patch switch (increment) { - case "major": - newMajor += 1; - newMinor = 0; - newPatch = 0; - break; - case "minor": - newMinor += 1; - newPatch = 0; - break; - case "patch": - newPatch += 1; - break; + case 'major': + newMajor += 1 + newMinor = 0 + newPatch = 0 + break + case 'minor': + newMinor += 1 + newPatch = 0 + break + case 'patch': + newPatch += 1 + break } if (isAlpha) { - const alphaHash = hash; - return `${newMajor}.${newMinor}.${newPatch}-alpha.${alphaHash}`; + const alphaHash = hash + return `${newMajor}.${newMinor}.${newPatch}-alpha.${alphaHash}` } else if (isBeta) { - const betaHash = hash; - return `${newMajor}.${newMinor}.${newPatch}-beta.${betaHash}`; + const betaHash = hash + return `${newMajor}.${newMinor}.${newPatch}-beta.${betaHash}` } else { - return `${newMajor}.${newMinor}.${newPatch}`; + return `${newMajor}.${newMinor}.${newPatch}` } -}; +} export const updatePackageVersion = async ({ isAlpha, @@ -80,91 +80,87 @@ export const updatePackageVersion = async ({ newVersion, isBeta, }: { - packagePath: string; - isAlpha?: boolean; - isBeta?: boolean; - newVersion?: string; + packagePath: string + isAlpha?: boolean + isBeta?: boolean + newVersion?: string }) => { - ulog(`Updating ${packagePath}`); - const packageData = await Bun.file(packagePath).text(); - const parsedData = JSON.parse(packageData); - parsedData.version = parsedData.version ? updateVersion({ currentVersion: parsedData.version, isAlpha }) : newVersion; + ulog(`Updating ${packagePath}`) + const packageData = await Bun.file(packagePath).text() + const parsedData = JSON.parse(packageData) + parsedData.version = parsedData.version ? updateVersion({ currentVersion: parsedData.version, isAlpha }) : newVersion // Use the provided new version if available - await Bun.write(packagePath, JSON.stringify(parsedData, null, 2)); - return parsedData.version; -}; + await Bun.write(packagePath, JSON.stringify(parsedData, null, 2)) + return parsedData.version +} type NpmPublishParams = { - packagePath: string; - isAlpha?: boolean; - isBeta?: boolean; - maxRetries?: number; -}; + packagePath: string + isAlpha?: boolean + isBeta?: boolean + maxRetries?: number +} export const npmPublish = async ({ isAlpha, isBeta, packagePath, maxRetries = 10 }: NpmPublishParams) => { - const dir = path.dirname(packagePath); + const dir = path.dirname(packagePath) - let success = false; + let success = false for (let i = 0; i < maxRetries && !success; i++) { - ulog(`Publishing from directory: ${dir}, attempt ${i + 1} of ${maxRetries}`); + ulog(`Publishing from directory: ${dir}, attempt ${i + 1} of ${maxRetries}`) - const npmWhoIs = Bun.spawnSync(["npm", "whoami"]); - ulog({ npmWhoIs: npmWhoIs.stdout.toString() }); + const npmWhoIs = Bun.spawnSync(['npm', 'whoami']) + ulog({ npmWhoIs: npmWhoIs.stdout.toString() }) - const publishScript = ["npm", "publish"]; + const publishScript = ['npm', 'publish'] if (isAlpha) { - publishScript.push("--tag", "alpha", "--access", "public"); + publishScript.push('--tag', 'alpha', '--access', 'public') } else { - publishScript.push("--access", "public"); + publishScript.push('--access', 'public') } const proc = Bun.spawnSync(publishScript, { cwd: dir, - }); + }) - const response = proc.stdout.toString(); + const response = proc.stdout.toString() - const errorResponse = proc.stderr.toString(); + const errorResponse = proc.stderr.toString() // If there's a specific error indicating a version conflict - if (errorResponse.includes("cannot publish over the previously published versions")) { - ulog(`Version conflict for ${dir}, trying next version...`); + if (errorResponse.includes('cannot publish over the previously published versions')) { + ulog(`Version conflict for ${dir}, trying next version...`) - const currentVersion = await getCurrentVersion(packagePath); - const newVersion = updateVersion({ currentVersion }); - ulog(`Updating version from ${currentVersion} to ${newVersion}`); - await updatePackageVersion({ packagePath, isAlpha, isBeta, newVersion }); + const currentVersion = await getCurrentVersion(packagePath) + const newVersion = updateVersion({ currentVersion }) + ulog(`Updating version from ${currentVersion} to ${newVersion}`) + await updatePackageVersion({ packagePath, isAlpha, isBeta, newVersion }) // Don't set success to true; let the loop continue to retry with the new version } else { // If there's no specific version conflict error, assume success - success = true; + success = true } } if (!success) { - console.error(`Failed to publish after ${maxRetries} attempts.`); - exit(1); + console.error(`Failed to publish after ${maxRetries} attempts.`) + exit(1) } -}; +} // TODO create the factory based on a package json file path export function npmReleaseFactory(options: NpmReleaseFactoryOptions) { - const { maxRetries = 10, npmToken } = options; + const { maxRetries = 10, npmToken } = options return { getCurrentVersion: () => getCurrentVersion, // this returns a function, not the result of the function. Is this intentional? - setupNpmAuth: () => setupNpmAuth(npmToken || ""), + setupNpmAuth: () => setupNpmAuth(npmToken || ''), npmPublish: ({ isAlpha, packagePath }: NpmPublishParams) => npmPublish({ isAlpha, packagePath, maxRetries }), updateVersion: (currentVersion: string, isAlpha: boolean) => updateVersion({ currentVersion, isAlpha }), - updatePackageVersion: (params: { - packagePath: string; - isAlpha?: boolean; - newVersion?: string; - isBeta?: boolean; - }) => updatePackageVersion(params), - }; + updatePackageVersion: (params: { packagePath: string; isAlpha?: boolean; newVersion?: string; isBeta?: boolean }) => + updatePackageVersion(params), + } } diff --git a/package.json b/package.json index 6d02ccc..6ef9bf6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "0.5.14", "main": "index.ts", "devDependencies": { - "@biomejs/biome": "1.4.0", "bun": "^1.0.14", "bun-types": "^1.0.14", "tsd": "^0.29.0" @@ -18,10 +17,12 @@ "tsc": "tsc --project tsconfig.json", "tsc:noEmit": "tsc --noEmit --project tsconfig-noEmit.json", "validate": "bun run tsc:noEmite && bun run test", - "format": "biome format . --write", - "lint": "biome lint ." + "format": "prettier --write ." }, "bin": { "bnkit": "sh ./scripts/cli.ts" + }, + "dependencies": { + "prettier": "^3.2.4" } -} \ No newline at end of file +} diff --git a/plugins/react-server/README.md b/plugins/react-server/README.md index 5de5630..9b43bd4 100644 --- a/plugins/react-server/README.md +++ b/plugins/react-server/README.md @@ -1,4 +1,5 @@ # react-server + ![npm bundle size](https://img.shields.io/bundlephobia/min/%40bnk%2Freact-server) To install dependencies: diff --git a/plugins/react-server/config.ts b/plugins/react-server/config.ts index 4869574..f7f5d05 100644 --- a/plugins/react-server/config.ts +++ b/plugins/react-server/config.ts @@ -1,9 +1,9 @@ -import * as path from "path"; +import * as path from 'path' -export type ENV_MODES = "DEV" | "PROD"; +export type ENV_MODES = 'DEV' | 'PROD' -export const PROJECT_ROOT = import.meta.dir; -export const BUILD_DIR = path.resolve(PROJECT_ROOT, "build"); -export const PUBLIC_DIR = path.resolve(PROJECT_ROOT, "public"); -export const MODE: ENV_MODES = (process.env.MODE as ENV_MODES) || "DEV"; -export const JS_ENTRY_FILE = "index.js"; +export const PROJECT_ROOT = import.meta.dir +export const BUILD_DIR = path.resolve(PROJECT_ROOT, 'build') +export const PUBLIC_DIR = path.resolve(PROJECT_ROOT, 'public') +export const MODE: ENV_MODES = (process.env.MODE as ENV_MODES) || 'DEV' +export const JS_ENTRY_FILE = 'index.js' diff --git a/plugins/react-server/fullstack-state.tsx b/plugins/react-server/fullstack-state.tsx index 96f65bb..3c6ba32 100644 --- a/plugins/react-server/fullstack-state.tsx +++ b/plugins/react-server/fullstack-state.tsx @@ -1,131 +1,127 @@ -import React from "react"; +import React from 'react' // this file is just an example -import { createContext, useEffect, useState } from "react"; +import { createContext, useEffect, useState } from 'react' export type ContextStateT = { - state: StateT; - updateKey: (key: Key, value: StateT[Key]) => void; -}; + state: StateT + updateKey: (key: Key, value: StateT[Key]) => void +} -export type ReactContextT = React.Context>; +export type ReactContextT = React.Context> const isServer = () => { - return typeof window === "undefined"; -}; + return typeof window === 'undefined' +} export const appState = { count: 0, -}; +} -export type AppStateT = typeof appState; +export type AppStateT = typeof appState const initState = async () => { - const isServerSide = isServer(); + const isServerSide = isServer() if (isServerSide) { return { ...appState, - }; + } } - const res = await fetch("/state", { - method: "get", - }); + const res = await fetch('/state', { + method: 'get', + }) - const json = await res.json(); + const json = await res.json() - return json as StateT; -}; + return json as StateT +} const AppContext = createContext<{ - state: typeof appState; + state: typeof appState updateKey: ( key: Key, value: (typeof appState)[Key], - ) => void; + ) => void }>({ state: { count: 0 }, updateKey: () => {}, -}); +}) const clientToServerStateKeyUpdate = async ( key: Key, value: StateT[Key], ) => { - const res = await fetch("/state", { - method: "post", + const res = await fetch('/state', { + method: 'post', body: JSON.stringify({ - type: "partial", + type: 'partial', state: { [key]: value, }, }), - }); - const json = await res.json(); - return json; -}; + }) + const json = await res.json() + return json +} export const StateProvider = ({ children, defaultState, }: { - children: React.ReactNode; - defaultState?: StateT; + children: React.ReactNode + defaultState?: StateT }) => { - const [state, setState] = useState(() => (defaultState || {}) as StateT); + const [state, setState] = useState(() => (defaultState || {}) as StateT) useEffect(() => { initState().then((state) => { setState((prev) => ({ ...prev, ...state, - })); - }); - }, []); + })) + }) + }, []) - const isServerSide = isServer(); + const isServerSide = isServer() const updateKey = async (key: Key, value: StateT[Key]) => { - if (isServerSide) return null; + if (isServerSide) return null setState((prev) => ({ ...prev, [key]: value, - })); - clientToServerStateKeyUpdate(key, value); - }; + })) + clientToServerStateKeyUpdate(key, value) + } - return {children}; -}; + return {children} +} const useClientAppState = () => { - const { state, updateKey } = React.useContext(AppContext); + const { state, updateKey } = React.useContext(AppContext) return { state, updateKey, - }; -}; + } +} const Counter = () => { - const { state, updateKey } = useClientAppState(); + const { state, updateKey } = useClientAppState() return (
Count: {state.count}
- +
- ); -}; + ) +} -export const AppEntry = ({ - defaultState, -}: { - defaultState: StateT; -}) => { +export const AppEntry = ({ defaultState }: { defaultState: StateT }) => { return ( - ); -}; + ) +} diff --git a/plugins/react-server/html-document.tsx b/plugins/react-server/html-document.tsx index e6b545e..0bb53d6 100644 --- a/plugins/react-server/html-document.tsx +++ b/plugins/react-server/html-document.tsx @@ -1,12 +1,6 @@ -import React from "react"; +import React from 'react' -export const HtmlDocument = ({ - children, - entryFilePath, -}: { - children?: React.ReactNode; - entryFilePath: string; -}) => { +export const HtmlDocument = ({ children, entryFilePath }: { children?: React.ReactNode; entryFilePath: string }) => { return ( @@ -29,5 +23,5 @@ export const HtmlDocument = ({
{children}
- ); -}; + ) +} diff --git a/plugins/react-server/hydrate-client.tsx b/plugins/react-server/hydrate-client.tsx index d276b62..a482d86 100644 --- a/plugins/react-server/hydrate-client.tsx +++ b/plugins/react-server/hydrate-client.tsx @@ -1,16 +1,16 @@ -import React from "react"; -import { hydrateRoot } from "react-dom/client"; +import React from 'react' +import { hydrateRoot } from 'react-dom/client' export const hydrateClient = ({ AppEntry }: { AppEntry: React.ReactNode }) => { - if (typeof window !== "undefined") { - const root = typeof document !== "undefined" && document.getElementById("root"); + if (typeof window !== 'undefined') { + const root = typeof document !== 'undefined' && document.getElementById('root') if (!root) { - console.error("Root node not found"); - throw new Error("Root node not found"); + console.error('Root node not found') + throw new Error('Root node not found') } - console.log("Hydrate!"); - hydrateRoot(root, {AppEntry}); + console.log('Hydrate!') + hydrateRoot(root, {AppEntry}) } -}; +} diff --git a/plugins/react-server/index.ts b/plugins/react-server/index.ts index a3d13d4..b7a9e2e 100644 --- a/plugins/react-server/index.ts +++ b/plugins/react-server/index.ts @@ -1,4 +1,4 @@ -export { HtmlDocument } from "./html-document"; -export { hydrateClient } from "./hydrate-client"; -export { createReactStreamHandler } from "./react-dom-stream-handler"; -export { reactServer } from "./react-server"; +export { HtmlDocument } from './html-document' +export { hydrateClient } from './hydrate-client' +export { createReactStreamHandler } from './react-dom-stream-handler' +export { reactServer } from './react-server' diff --git a/plugins/react-server/package.json b/plugins/react-server/package.json index 22cceca..55b40cf 100644 --- a/plugins/react-server/package.json +++ b/plugins/react-server/package.json @@ -23,4 +23,4 @@ "react-dom": "^18.0.0", "bnkit": "^0.5.12" } -} \ No newline at end of file +} diff --git a/plugins/react-server/react-dom-stream-handler.tsx b/plugins/react-server/react-dom-stream-handler.tsx index 889983d..6f2338e 100644 --- a/plugins/react-server/react-dom-stream-handler.tsx +++ b/plugins/react-server/react-dom-stream-handler.tsx @@ -1,24 +1,24 @@ -import { RouteHandler } from "bnkit/server"; -import { renderToReadableStream } from "react-dom/server"; +import { RouteHandler } from 'bnkit/server' +import { renderToReadableStream } from 'react-dom/server' export const createReactStreamHandler = async ({ renderNode, - entryPath + entryPath, }: { - renderNode: React.ReactNode; - entryPath: string; + renderNode: React.ReactNode + entryPath: string }) => { const reactDomStreamHandler: RouteHandler = async (req) => { const stream = await renderToReadableStream(renderNode, { bootstrapScripts: [entryPath], - }); + }) return new Response(stream, { headers: { - "Content-Type": "text/html", + 'Content-Type': 'text/html', }, - }); - }; + }) + } - return reactDomStreamHandler; -}; \ No newline at end of file + return reactDomStreamHandler +} diff --git a/plugins/react-server/react-server.tsx b/plugins/react-server/react-server.tsx index 869045f..66b8069 100644 --- a/plugins/react-server/react-server.tsx +++ b/plugins/react-server/react-server.tsx @@ -1,59 +1,54 @@ -import { MiddlewareConfigMap, Routes, serverFactory } from "bnkit/server"; -import { createReactStreamHandler } from "./react-dom-stream-handler"; +import { MiddlewareConfigMap, Routes, serverFactory } from 'bnkit/server' +import { createReactStreamHandler } from './react-dom-stream-handler' export const createReactServerRoutes = async ({ Component, - buildPath = "/build/", + buildPath = '/build/', }: { - Component: React.ReactNode; - buildPath?: string; + Component: React.ReactNode + buildPath?: string }) => { // change ./ to just / for buildEntry - const routes - = { - "/": { + const routes = { + '/': { get: await createReactStreamHandler({ // idea pass middleware to renderNode and access data on client renderNode: Component, entryPath: buildPath, }), }, - "^/build/.+": { + '^/build/.+': { get: () => { return new Response(Bun.file(buildPath).stream(), { headers: { - "Content-Type": "application/javascript", + 'Content-Type': 'application/javascript', }, - }); + }) }, }, - }; + } - return routes; -}; + return routes +} export const reactServer = async ({ Entry, port = 3000, - buildPath + buildPath, }: { - Entry: React.ReactNode; - port?: number; - buildPath?: string; + Entry: React.ReactNode + port?: number + buildPath?: string }) => { - const { start } = serverFactory({ serve: Bun.serve, // serve, routes: await createReactServerRoutes({ Component: Entry, - buildPath - + buildPath, }), + }) - }); - - start(port); -}; - + start(port) +} diff --git a/plugins/react-server/render-app.tsx b/plugins/react-server/render-app.tsx index 22d6650..ff03ab2 100644 --- a/plugins/react-server/render-app.tsx +++ b/plugins/react-server/render-app.tsx @@ -1,31 +1,27 @@ -import { HtmlDocument } from "./html-document"; -import { AppEntry } from "./fullstack-state"; -import React from "react"; +import { HtmlDocument } from './html-document' +import { AppEntry } from './fullstack-state' +import React from 'react' const isServer = () => { - return typeof window === "undefined"; -}; + return typeof window === 'undefined' +} const useIsServer = () => { - const [server, setServer] = React.useState(isServer()); + const [server, setServer] = React.useState(isServer()) React.useEffect(() => { - setServer(isServer()); - }, []); + setServer(isServer()) + }, []) - return server; -}; + return server +} -export const RenderApp = ({ - retrieveStateFn, -}: { - retrieveStateFn?: (...args: any[]) => State; -}) => { - const isServer = useIsServer(); +export const RenderApp = ({ retrieveStateFn }: { retrieveStateFn?: (...args: any[]) => State }) => { + const isServer = useIsServer() return ( - + {isServer} - ); -}; + ) +} diff --git a/plugins/react/README.md b/plugins/react/README.md index 751f937..0a98734 100644 --- a/plugins/react/README.md +++ b/plugins/react/README.md @@ -1,4 +1,5 @@ # React Bun Nookit Plugin + ![npm bundle size](https://img.shields.io/bundlephobia/min/%40bnk%2Freact) ## Bun Nookit Plugins diff --git a/plugins/react/api-hook.tsx b/plugins/react/api-hook.tsx index 2266e7c..c3ee6ae 100644 --- a/plugins/react/api-hook.tsx +++ b/plugins/react/api-hook.tsx @@ -1,26 +1,26 @@ -import { createFetchFactory } from "bnkit/fetcher"; -import React, { ReactNode, createContext, useContext, useMemo, useState } from "react"; +import { createFetchFactory } from 'bnkit/fetcher' +import React, { ReactNode, createContext, useContext, useMemo, useState } from 'react' // TODO: This hook needs some more work -type FetchFactoryReturn = ReturnType>; +type FetchFactoryReturn = ReturnType> const FetchContext = createContext( createFetchFactory({ - baseUrl: "", + baseUrl: '', config: { - "/test": { - endpoint: "/test", - method: "get", + '/test': { + endpoint: '/test', + method: 'get', }, }, }), -); +) export const FetchProvider = ({ children, factoryConfig, }: { - children: ReactNode; - factoryConfig: FetchConfig; + children: ReactNode + factoryConfig: FetchConfig }) => { const fetchFactory = useMemo( () => @@ -28,89 +28,89 @@ export const FetchProvider = ({ config: factoryConfig, }), [], - ); + ) - return {children}; -}; + return {children} +} export function useFetchFactory() { - const fetchFactory = useContext(FetchContext); + const fetchFactory = useContext(FetchContext) if (!fetchFactory) { - throw new Error("useFetchFactory must be used within a FetchProvider"); + throw new Error('useFetchFactory must be used within a FetchProvider') } - return fetchFactory; + return fetchFactory } type FetchState = - | { stage: "idle"; data: null; retries: 0 } - | { stage: "fetching"; data: null; retries: number } - | { stage: "resolved"; data: DataT; retries: number } - | { stage: "rejected"; data: null; retries: number }; + | { stage: 'idle'; data: null; retries: 0 } + | { stage: 'fetching'; data: null; retries: number } + | { stage: 'resolved'; data: DataT; retries: number } + | { stage: 'rejected'; data: null; retries: number } export function createApiHook({ configMap, baseUrl, }: { - configMap: ConfigMap; - baseUrl: string; + configMap: ConfigMap + baseUrl: string }) { return function useCustomApi(endpointKey: keyof ConfigMap) { - const { get, post, ...rest } = useFetchFactory(); - const endpointConfig = configMap[endpointKey]; + const { get, post, ...rest } = useFetchFactory() + const endpointConfig = configMap[endpointKey] if (!endpointConfig) { - throw new Error(`No configuration found for endpoint: ${configMap[typeof endpointKey].endpoint}`); + throw new Error(`No configuration found for endpoint: ${configMap[typeof endpointKey].endpoint}`) } - const { response } = endpointConfig; - type DataT = typeof response.data; + const { response } = endpointConfig + type DataT = typeof response.data const [state, setState] = useState>({ - stage: "idle", + stage: 'idle', data: null, retries: 0, - }); + }) const fetchWithStateMachine = (config: ConfigMap[typeof endpointKey]) => { switch (state.stage) { - case "idle": + case 'idle': setState((prev) => ({ ...prev, - stage: "fetching", + stage: 'fetching', data: null, retries: prev.retries, - })); - break; - case "fetching": + })) + break + case 'fetching': // TODO ensure the request config in the config map matches what the get request expects get(config) .then((data) => { - setState({ stage: "resolved", data, retries: state.retries }); + setState({ stage: 'resolved', data, retries: state.retries }) }) .catch(() => { setState((prev) => ({ - stage: "rejected", + stage: 'rejected', data: null, retries: prev.retries + 1, - })); - }); - break; - case "rejected": + })) + }) + break + case 'rejected': if (state.retries <= 3) { setState((prev) => ({ ...prev, - stage: "fetching", + stage: 'fetching', data: null, retries: prev.retries, - })); + })) } - break; + break default: - break; + break } - }; + } // return { // get: (fetchConfig: APIConfig) => @@ -118,7 +118,7 @@ export function createApiHook({ // data: state.data, // // Include other properties/methods as needed // }; - }; + } } // Now you can use this to create custom hooks tailored for specific APIs diff --git a/plugins/react/app/App.tsx b/plugins/react/app/App.tsx index a854d60..c55ba0b 100644 --- a/plugins/react/app/App.tsx +++ b/plugins/react/app/App.tsx @@ -1,19 +1,19 @@ -import { useFetcher } from ".."; -import "./App.css"; +import { useFetcher } from '..' +import './App.css' interface DataType { - userId: number; - id: number; - title: string; - completed: boolean; + userId: number + id: number + title: string + completed: boolean } function App() { const { get, status, useData } = useFetcher({ options: { - baseUrl: "https://jsonplaceholder.typicode.com/", + baseUrl: 'https://jsonplaceholder.typicode.com/', }, - }); + }) return (
@@ -23,13 +23,13 @@ function App() {
- ); + ) } -export default App; +export default App diff --git a/plugins/react/app/main.tsx b/plugins/react/app/main.tsx index 2f06727..faf5107 100644 --- a/plugins/react/app/main.tsx +++ b/plugins/react/app/main.tsx @@ -1,10 +1,10 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.js"; -import "./index.css"; +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.js' +import './index.css' -ReactDOM.createRoot(document.getElementById("root")).render( +ReactDOM.createRoot(document.getElementById('root')).render( , -); +) diff --git a/plugins/react/index.ts b/plugins/react/index.ts index fcb8db4..eb35b22 100644 --- a/plugins/react/index.ts +++ b/plugins/react/index.ts @@ -1,3 +1,3 @@ -export { useClipboard } from "./use-clipboard"; -export { useCookie } from "./use-cookie"; -export { useLocalStorage } from "./use-local-storage"; +export { useClipboard } from './use-clipboard' +export { useCookie } from './use-cookie' +export { useLocalStorage } from './use-local-storage' diff --git a/plugins/react/package.json b/plugins/react/package.json index 1baabea..cae18e8 100644 --- a/plugins/react/package.json +++ b/plugins/react/package.json @@ -28,4 +28,4 @@ "react-dom": "^18.0.0", "bnkit": "latest" } -} \ No newline at end of file +} diff --git a/plugins/react/tsconfig.json b/plugins/react/tsconfig.json index e2e618d..a591d59 100644 --- a/plugins/react/tsconfig.json +++ b/plugins/react/tsconfig.json @@ -17,9 +17,5 @@ ], "baseUrl": "." }, - "include": [ - "apps/**/*.ts", - "_examples/**/*.ts", - "./release.ts" - ] + "include": ["apps/**/*.ts", "_examples/**/*.ts", "./release.ts"] } diff --git a/plugins/react/use-clipboard.ts b/plugins/react/use-clipboard.ts index ebc2550..81376fc 100644 --- a/plugins/react/use-clipboard.ts +++ b/plugins/react/use-clipboard.ts @@ -1,50 +1,50 @@ -import { useCallback, useState } from "react"; +import { useCallback, useState } from 'react' export type UseClipboardRes = { - clipboardData: string | null; - setClipboard: (data: string) => Promise; - getClipboard: () => Promise; -}; + clipboardData: string | null + setClipboard: (data: string) => Promise + getClipboard: () => Promise +} export function useClipboard(externalValue?: string, updater?: (value: string) => void): UseClipboardRes { - const [internalClipboardData, setInternalClipboardData] = useState(externalValue || null); + const [internalClipboardData, setInternalClipboardData] = useState(externalValue || null) - const clipboardData = externalValue !== undefined ? externalValue : internalClipboardData; + const clipboardData = externalValue !== undefined ? externalValue : internalClipboardData // Write to clipboard const setClipboard = useCallback( async (data: string) => { try { - await navigator.clipboard.writeText(data); + await navigator.clipboard.writeText(data) if (updater) { - updater(data); + updater(data) } else { - setInternalClipboardData(data); + setInternalClipboardData(data) } } catch (error) { - console.error("Failed to write to clipboard", error); + console.error('Failed to write to clipboard', error) } }, [updater], - ); + ) // Read from clipboard const getClipboard = useCallback(async () => { try { - const data = await navigator.clipboard.readText(); + const data = await navigator.clipboard.readText() if (updater) { - updater(data); + updater(data) } else { - setInternalClipboardData(data); + setInternalClipboardData(data) } } catch (error) { - console.error("Failed to read from clipboard", error); + console.error('Failed to read from clipboard', error) } - }, [updater]); + }, [updater]) return { clipboardData, setClipboard, getClipboard, - }; + } } diff --git a/plugins/react/use-cookie.ts b/plugins/react/use-cookie.ts index 86b6eea..c14a43b 100644 --- a/plugins/react/use-cookie.ts +++ b/plugins/react/use-cookie.ts @@ -1,62 +1,62 @@ -import { CookieOptions } from "bnkit/cookies/cookie-types"; -import { clientCookieFactory } from "bnkit/cookies/create-client-side-cookie-factory"; -import { useEffect, useState } from "react"; +import { CookieOptions } from 'bnkit/cookies/cookie-types' +import { clientCookieFactory } from 'bnkit/cookies/create-client-side-cookie-factory' +import { useEffect, useState } from 'react' export function useCookie(cookieKey: string, options?: CookieOptions) { - const cookie = clientCookieFactory(cookieKey); + const cookie = clientCookieFactory(cookieKey) const [cookieData, setCookieData] = useState<{ value: T | null }>(() => { return { value: cookie.getParsedCookie(), - }; - }); + } + }) const getCookie = () => { - return cookie.getRawCookie(); - }; + return cookie.getRawCookie() + } useEffect(() => { setCookieData({ value: cookie.getParsedCookie(), - }); - }, []); + }) + }, []) const refreshCookie = () => { setCookieData({ value: cookie.getParsedCookie(), - }); - }; + }) + } const updateCookie = ( value: T, updateOptions: CookieOptions & { - cookieKey?: string; // optionally override cookie key + cookieKey?: string // optionally override cookie key } = options || {}, ) => { - const stringifiedValue = stringifyCookieData(value); + const stringifiedValue = stringifyCookieData(value) cookie.setCookie(stringifiedValue, { ...options, ...updateOptions, - }); - setCookieData({ value: value }); - }; + }) + setCookieData({ value: value }) + } const removeCookie = () => { - cookie.deleteCookie(); - setCookieData({ value: null }); - }; + cookie.deleteCookie() + setCookieData({ value: null }) + } const stringifyCookieData = (data: T): string => { - if (typeof data === "string") { - return data; + if (typeof data === 'string') { + return data } else { - return JSON.stringify(data); + return JSON.stringify(data) } - }; + } const checkCookie = () => { - return cookie.checkCookie(); - }; + return cookie.checkCookie() + } return { cookie: cookieData.value, @@ -65,7 +65,7 @@ export function useCookie(cookieKey: string, options?: CookieOptions checkCookie, getCookie, refreshCookie, - }; + } } -export default useCookie; +export default useCookie diff --git a/plugins/react/use-local-storage.ts b/plugins/react/use-local-storage.ts index 8028faa..33311c8 100644 --- a/plugins/react/use-local-storage.ts +++ b/plugins/react/use-local-storage.ts @@ -1,111 +1,111 @@ -import { Dispatch, useEffect, useState } from "react"; +import { Dispatch, useEffect, useState } from 'react' export type LocalStoreConfig = { - key: string; // LocalStorage key - initialState: DataT; // Initial state if there's nothing in LocalStorage -}; + key: string // LocalStorage key + initialState: DataT // Initial state if there's nothing in LocalStorage +} -export type LocalStoreReturnT = [DataT, React.Dispatch>]; +export type LocalStoreReturnT = [DataT, React.Dispatch>] export type GetLSKeyOptions = { - fallbackToInitialValOnError?: boolean; -}; + fallbackToInitialValOnError?: boolean +} -export type GetLSKeyFn = (options: GetLSKeyOptions, onData?: (data: DataT) => void) => DataT | null; +export type GetLSKeyFn = (options: GetLSKeyOptions, onData?: (data: DataT) => void) => DataT | null -export type SetLSKeyFn = (val: DataT | ((prevState: DataT) => DataT)) => void; +export type SetLSKeyFn = (val: DataT | ((prevState: DataT) => DataT)) => void -export type SyncLSKeyFn = (fallbackToInitialVal: boolean) => ReturnType>; +export type SyncLSKeyFn = (fallbackToInitialVal: boolean) => ReturnType> export type UseLocalStorageReturn = { - get: GetLSKeyFn; - set: SetLSKeyFn; - state: DataT; - setState: Dispatch>; - sync: SyncLSKeyFn; - startSyncInterval: (syncConfig: SyncIntervalConfig) => void; - stopSyncInterval: (syncConfig: SyncIntervalConfig) => void; -}; + get: GetLSKeyFn + set: SetLSKeyFn + state: DataT + setState: Dispatch> + sync: SyncLSKeyFn + startSyncInterval: (syncConfig: SyncIntervalConfig) => void + stopSyncInterval: (syncConfig: SyncIntervalConfig) => void +} export type SyncIntervalConfig = { - interval: number; // Time interval in milliseconds -}; + interval: number // Time interval in milliseconds +} export function useLocalStorage(config: LocalStoreConfig): UseLocalStorageReturn { // Initial state from local storage or fallback to initialState const getLSKey: GetLSKeyFn = (options, onData) => { - const { fallbackToInitialValOnError: fallbackToInitialValOnErrror = true } = options; + const { fallbackToInitialValOnError: fallbackToInitialValOnErrror = true } = options - const storedData = localStorage.getItem(config.key); + const storedData = localStorage.getItem(config.key) try { - const parsedData = JSON.parse(storedData || "") as DataT; + const parsedData = JSON.parse(storedData || '') as DataT if (onData) { - onData(parsedData); + onData(parsedData) } - return parsedData; + return parsedData } catch (e) { console.error( - "Failed to properly get Local Storage key/value, returning initial state", + 'Failed to properly get Local Storage key/value, returning initial state', config.key, storedData, e, - ); + ) if (fallbackToInitialValOnErrror) { if (onData) { - onData(config.initialState); + onData(config.initialState) } - return config.initialState; + return config.initialState } - return null; + return null } - }; + } const getInitState = () => { - let data: DataT | null = null; + let data: DataT | null = null getLSKey( { fallbackToInitialValOnError: false, }, (dataCb) => { - data = dataCb; + data = dataCb }, - ); + ) - return data ?? config.initialState; - }; + return data ?? config.initialState + } - const [lsKeyState, setLsKeyState] = useState(getInitState()); - const [syncIntervalId, setSyncIntervalId] = useState(null); + const [lsKeyState, setLsKeyState] = useState(getInitState()) + const [syncIntervalId, setSyncIntervalId] = useState(null) const startSyncInterval = (syncConfig: SyncIntervalConfig) => { // Clear any existing interval if (syncIntervalId) { - clearInterval(syncIntervalId); + clearInterval(syncIntervalId) } - if (typeof window !== "undefined") { + if (typeof window !== 'undefined') { // Set up the new interval const id = window?.setInterval(() => { - syncLSKeyState(true); // Assuming you want to fallback to initial value during auto-sync - }, syncConfig.interval); + syncLSKeyState(true) // Assuming you want to fallback to initial value during auto-sync + }, syncConfig.interval) - setSyncIntervalId(id); + setSyncIntervalId(id) } - }; + } const stopSyncInterval = () => { if (syncIntervalId) { - clearInterval(syncIntervalId); + clearInterval(syncIntervalId) } - }; + } useEffect(() => { - return () => stopSyncInterval(); - }, [syncIntervalId]); + return () => stopSyncInterval() + }, [syncIntervalId]) // syncs the state to local storage const syncLSKeyState = (fallbackToInitialVal: boolean = false): DataT | null => { @@ -114,27 +114,27 @@ export function useLocalStorage(config: LocalStoreConfig): UseLoca fallbackToInitialValOnError: fallbackToInitialVal, }, (data) => { - setLsKeyState(data); + setLsKeyState(data) }, - ); - }; + ) + } const setLSKey: SetLSKeyFn = (value) => { - let stringifiedVal = ""; + let stringifiedVal = '' try { - stringifiedVal = JSON.stringify(value); + stringifiedVal = JSON.stringify(value) } catch (e) { - console.error("Failed to properly set Local Storage key/value", config.key, value, e); + console.error('Failed to properly set Local Storage key/value', config.key, value, e) } try { - localStorage.setItem(config.key, stringifiedVal); - setLsKeyState(value); + localStorage.setItem(config.key, stringifiedVal) + setLsKeyState(value) } catch (e) { - console.error("Failed to properly set Local Storage key/value", config.key, value, e); + console.error('Failed to properly set Local Storage key/value', config.key, value, e) } - }; + } // Whenever key changes, update local storage useEffect(() => { @@ -143,10 +143,10 @@ export function useLocalStorage(config: LocalStoreConfig): UseLoca fallbackToInitialValOnError: true, }, (data) => { - setLsKeyState(data); + setLsKeyState(data) }, - ); - }, [config.key]); + ) + }, [config.key]) return { set: setLSKey, @@ -157,5 +157,5 @@ export function useLocalStorage(config: LocalStoreConfig): UseLoca sync: syncLSKeyState, startSyncInterval, stopSyncInterval, - }; + } } diff --git a/plugins/react/use-server-state.ts b/plugins/react/use-server-state.ts index 3a5bb4e..1babdab 100644 --- a/plugins/react/use-server-state.ts +++ b/plugins/react/use-server-state.ts @@ -1,135 +1,135 @@ -import { useEffect, useRef, useState } from "react"; -import { Dispatchers } from "../../types"; -import { createStateDispatchers } from "state/create-state-dispatchers"; -const MAX_RETRIES = 5; +import { useEffect, useRef, useState } from 'react' +import { Dispatchers } from '../../types' +import { createStateDispatchers } from 'state/create-state-dispatchers' +const MAX_RETRIES = 5 const getAppStateFromLocalStorage = (defaultState: State): State => { - const appStateString = localStorage.getItem("appState"); + const appStateString = localStorage.getItem('appState') try { - const storedState = appStateString ? JSON.parse(appStateString) : {}; + const storedState = appStateString ? JSON.parse(appStateString) : {} // Merge the default state with the stored state. // This will ensure that missing keys in storedState will be taken from defaultState. - return { ...defaultState, ...storedState }; + return { ...defaultState, ...storedState } } catch (error) { - return defaultState; + return defaultState } -}; +} -type OptimisticMap = Partial>; +type OptimisticMap = Partial> export type DefaultOptions = { - optimistic?: boolean; -}; + optimistic?: boolean +} export function useServerState({ defaultState, url, optimisticMap, }: { - url: string; - defaultState: State; - optimisticMap?: OptimisticMap; + url: string + defaultState: State + optimisticMap?: OptimisticMap }): { - state: State; - control: Dispatchers; - dispatch: (key: keyof State, value: State[keyof State], opts?: Options) => void; + state: State + control: Dispatchers + dispatch: (key: keyof State, value: State[keyof State], opts?: Options) => void } { - const [state, setState] = useState(() => getAppStateFromLocalStorage(defaultState)); - const wsRef = useRef(null); - const initialized = useRef(false); - const prevStateRef = useRef(null); - const retryCount = useRef(0); + const [state, setState] = useState(() => getAppStateFromLocalStorage(defaultState)) + const wsRef = useRef(null) + const initialized = useRef(false) + const prevStateRef = useRef(null) + const retryCount = useRef(0) useEffect(() => { const connectToServer = () => { - const websocket = new WebSocket(url); - wsRef.current = websocket; + const websocket = new WebSocket(url) + wsRef.current = websocket websocket.onopen = () => { - retryCount.current = 0; - }; + retryCount.current = 0 + } websocket.onmessage = (event) => { - if (typeof event.data !== "string") return; - const receivedData = JSON.parse(event.data); + if (typeof event.data !== 'string') return + const receivedData = JSON.parse(event.data) - if (receivedData.status === "failure" && prevStateRef.current) { - setState(prevStateRef.current); - } else if (receivedData.key && "value" in receivedData) { + if (receivedData.status === 'failure' && prevStateRef.current) { + setState(prevStateRef.current) + } else if (receivedData.key && 'value' in receivedData) { setState((prevState) => ({ ...prevState, [receivedData.key]: receivedData.value, - })); + })) } - }; + } websocket.onclose = (event) => { // Check if the WebSocket was closed unexpectedly and we haven't exceeded max retries if (event.code !== 1000 && retryCount.current < MAX_RETRIES) { - retryCount.current += 1; - const delay = Math.min(1000 * retryCount.current, 30000); - setTimeout(connectToServer, delay); + retryCount.current += 1 + const delay = Math.min(1000 * retryCount.current, 30000) + setTimeout(connectToServer, delay) } else { - retryCount.current = 0; // Reset retry count if we've reached max retries or if closure was normal + retryCount.current = 0 // Reset retry count if we've reached max retries or if closure was normal } - }; + } return () => { - websocket?.close?.(); - }; - }; + websocket?.close?.() + } + } - connectToServer(); + connectToServer() // Clean up function return () => { - wsRef.current?.close(); - }; - }, [url]); + wsRef.current?.close() + } + }, [url]) useEffect(() => { - const storedState = getAppStateFromLocalStorage(defaultState); + const storedState = getAppStateFromLocalStorage(defaultState) if (!initialized.current) { - setState(storedState); - initialized.current = true; - return; + setState(storedState) + initialized.current = true + return } - }, []); + }, []) useEffect(() => { if (initialized.current) { - localStorage.setItem("appState", JSON.stringify(state)); + localStorage.setItem('appState', JSON.stringify(state)) } - }, [state]); + }, [state]) const sendToServer = (key: keyof State, value: State[keyof State]) => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ key, value })); + wsRef.current.send(JSON.stringify({ key, value })) } - }; + } const dispatch = (key: keyof State, value: State[keyof State], opts?: Options) => { - const isOptimistic = opts?.optimistic ?? optimisticMap?.[key] !== false; + const isOptimistic = opts?.optimistic ?? optimisticMap?.[key] !== false if (isOptimistic) { - prevStateRef.current = state; - setState((prev) => ({ ...prev, [key]: value })); + prevStateRef.current = state + setState((prev) => ({ ...prev, [key]: value })) } - sendToServer(key, value); - }; + sendToServer(key, value) + } const control = createStateDispatchers({ defaultState, state, updateFunction: dispatch, - }); + }) return { state, control, dispatch, - }; + } } diff --git a/plugins/react/vite.config.js b/plugins/react/vite.config.js index 9cc50ea..5a33944 100644 --- a/plugins/react/vite.config.js +++ b/plugins/react/vite.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}); +}) diff --git a/plugins/react/webrtc/index.ts b/plugins/react/webrtc/index.ts index 52dbf8d..68f10f0 100644 --- a/plugins/react/webrtc/index.ts +++ b/plugins/react/webrtc/index.ts @@ -1 +1 @@ -export { createWebRTCFactory } from "./webrtc-factory"; +export { createWebRTCFactory } from './webrtc-factory' diff --git a/plugins/react/webrtc/webrtc-factory.ts b/plugins/react/webrtc/webrtc-factory.ts index f32574d..3b3f850 100644 --- a/plugins/react/webrtc/webrtc-factory.ts +++ b/plugins/react/webrtc/webrtc-factory.ts @@ -1,22 +1,18 @@ export type WebRTCConfig = { - iceServers?: RTCIceServer[]; -}; + iceServers?: RTCIceServer[] +} -export type WebRTCFactoryMethods = keyof ReturnType; +export type WebRTCFactoryMethods = keyof ReturnType /** * Creates a WebRTC factory object with methods to create peer connections, offers, answers, and set remote descriptions. * @param defaultConfig - Optional default configuration for the WebRTC factory. * @returns An object with methods to create peer connections, offers, answers, and set remote descriptions. */ -export function createWebRTCFactory({ - defaultConfig, -}: { - defaultConfig?: WebRTCConfig; -}) { +export function createWebRTCFactory({ defaultConfig }: { defaultConfig?: WebRTCConfig }) { const configuration: RTCConfiguration = { iceServers: defaultConfig?.iceServers || [], - }; + } /** * Creates a new RTCPeerConnection object with the given custom configuration. @@ -27,8 +23,8 @@ export function createWebRTCFactory({ const peerConnection = new RTCPeerConnection({ ...configuration, ...customConfig, - }); - return peerConnection; + }) + return peerConnection } /** @@ -38,8 +34,8 @@ export function createWebRTCFactory({ */ function createOffer(peerConnection: RTCPeerConnection): Promise { return peerConnection.createOffer().then((offer) => { - return peerConnection.setLocalDescription(offer).then(() => offer); - }); + return peerConnection.setLocalDescription(offer).then(() => offer) + }) } /** @@ -49,8 +45,8 @@ export function createWebRTCFactory({ */ function createAnswer(peerConnection: RTCPeerConnection): Promise { return peerConnection.createAnswer().then((answer) => { - return peerConnection.setLocalDescription(answer).then(() => answer); - }); + return peerConnection.setLocalDescription(answer).then(() => answer) + }) } /** @@ -63,7 +59,7 @@ export function createWebRTCFactory({ peerConnection: RTCPeerConnection, description: RTCSessionDescriptionInit, ): Promise { - return peerConnection.setRemoteDescription(description); + return peerConnection.setRemoteDescription(description) } return { @@ -71,5 +67,5 @@ export function createWebRTCFactory({ createOffer, createAnswer, setRemoteDescription, - }; + } } diff --git a/release.ts b/release.ts index e9a3b35..f5f7ce2 100644 --- a/release.ts +++ b/release.ts @@ -1,45 +1,45 @@ -import Bun from "bun"; -import path from "path"; -import { exit } from "process"; +import Bun from 'bun' +import path from 'path' +import { exit } from 'process' // import * as u from "./"; -import { deploy, npm } from "index"; -import { ulog } from "./utils/ulog"; +import { deploy, npm } from 'index' +import { ulog } from './utils/ulog' // run bun test -const testProc = Bun.spawnSync(["bun", "test", "--coverage"], {}); +const testProc = Bun.spawnSync(['bun', 'test', '--coverage'], {}) -const output = await deploy.logStdOutput(testProc); +const output = await deploy.logStdOutput(testProc) if (!output) { - ulog("No output"); - exit(1); + ulog('No output') + exit(1) } -const NPM_TOKEN = Bun.env.NPM_TOKEN || ""; -const MAX_RETRIES = Number(Bun.env.MAX_PUBLISH_RETRY) || 10; // Define a max number of retries to prevent infinite loops +const NPM_TOKEN = Bun.env.NPM_TOKEN || '' +const MAX_RETRIES = Number(Bun.env.MAX_PUBLISH_RETRY) || 10 // Define a max number of retries to prevent infinite loops -const e = Bun.env; +const e = Bun.env const { commitAndPush, setupGitConfig, actionsEnv } = deploy.createGitHubActionsFactory({ - sshRepoUrl: "git@github.com:brandon-schabel/bun-nook-kit.git", -}); + sshRepoUrl: 'git@github.com:brandon-schabel/bun-nook-kit.git', +}) -const isBeta = actionsEnv.branch === "main"; -const isRelease = actionsEnv.branch === "release"; -const isAlpha = actionsEnv.eventName === "pull_request"; -const isLocalRun = Bun.env.LOCAL_RUN === "true"; +const isBeta = actionsEnv.branch === 'main' +const isRelease = actionsEnv.branch === 'release' +const isAlpha = actionsEnv.eventName === 'pull_request' +const isLocalRun = Bun.env.LOCAL_RUN === 'true' const { npmPublish, setupNpmAuth, updatePackageVersion } = npm.npmReleaseFactory({ maxRetries: MAX_RETRIES, npmToken: NPM_TOKEN, -}); +}) -const corePackagePath = path.resolve(process.cwd(), "package.json"); +const corePackagePath = path.resolve(process.cwd(), 'package.json') // todo resolve all plugin paths -const pluginReactPath = path.resolve(process.cwd(), "plugins", "react", "package.json"); +const pluginReactPath = path.resolve(process.cwd(), 'plugins', 'react', 'package.json') -const pluginReactServerPath = path.resolve(process.cwd(), "plugins", "react-server", "package.json"); +const pluginReactServerPath = path.resolve(process.cwd(), 'plugins', 'react-server', 'package.json') ulog({ actor: e?.GITHUB_ACTOR, @@ -54,34 +54,34 @@ ulog({ isAlpha, corePackagePath, pluginReactPath, -}); +}) /* Script */ if (!isLocalRun) { - setupNpmAuth(); - await setupGitConfig(); + setupNpmAuth() + await setupGitConfig() } -ulog(`Updating versions to ${isAlpha ? "alpha" : "Release"}`); +ulog(`Updating versions to ${isAlpha ? 'alpha' : 'Release'}`) const newVersion = await updatePackageVersion({ packagePath: corePackagePath, - isAlpha: actionsEnv.eventName === "pull_request", - isBeta: actionsEnv.eventName === "push", -}); + isAlpha: actionsEnv.eventName === 'pull_request', + isBeta: actionsEnv.eventName === 'push', +}) -await npmPublish({ packagePath: corePackagePath, isAlpha, isBeta }); +await npmPublish({ packagePath: corePackagePath, isAlpha, isBeta }) -await updatePackageVersion({ packagePath: pluginReactPath, isAlpha, isBeta }); -await npmPublish({ packagePath: pluginReactPath, isAlpha, isBeta }); +await updatePackageVersion({ packagePath: pluginReactPath, isAlpha, isBeta }) +await npmPublish({ packagePath: pluginReactPath, isAlpha, isBeta }) await updatePackageVersion({ packagePath: pluginReactServerPath, isAlpha, isBeta, -}); +}) -await npmPublish({ packagePath: pluginReactServerPath, isAlpha, isBeta }); +await npmPublish({ packagePath: pluginReactServerPath, isAlpha, isBeta }) if (!isLocalRun && !isAlpha) { - await commitAndPush(`Pushing version: ${newVersion}`); + await commitAndPush(`Pushing version: ${newVersion}`) } diff --git a/scripts/cli.ts b/scripts/cli.ts index 70667c0..fa335ac 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -1,2 +1,2 @@ #!/usr/bin/env bun -Bun.spawnSync("sh", ["./cli.sh"]); +Bun.spawnSync('sh', ['./cli.sh']) diff --git a/scripts/fly-deploy/fly-readme.md b/scripts/fly-deploy/fly-readme.md index 3e28952..a474b21 100644 --- a/scripts/fly-deploy/fly-readme.md +++ b/scripts/fly-deploy/fly-readme.md @@ -8,8 +8,10 @@ brew install flyctl ``` ## Important + ### Update `fly.toml` -Change the "app" value to a name of your choosing, this is important as this will throw an error if you try to deploy under the default name + +Change the "app" value to a name of your choosing, this is important as this will throw an error if you try to deploy under the default name [Fly Docs]("https://fly.io/docs/hands-on/install-flyctl/") @@ -24,7 +26,7 @@ Or: [Fly Signup Link]("https://fly.io/app/sign-up") ## Sign In With Fly ```bash -fly auth +fly auth ``` Go to your [Fly dashboard](https://fly.io/dashboard/personal) diff --git a/scripts/starter/README.md b/scripts/starter/README.md index ed2a687..6181637 100644 --- a/scripts/starter/README.md +++ b/scripts/starter/README.md @@ -6,10 +6,9 @@ bash <(curl -fsSL https://raw.githubusercontent.com/brandon-schabel/bun-nook-kit/main/scripts/quickstart.sh) ``` -you can pass in a `-p` flag followed by a desired folder name to pass the custom name directly to the command. - -The above commands/script will create a template from the file and run a setup script to install bun if it's not already installed, install dependencies(bun/bun-types packages). Then it will start the server and open the browser. +you can pass in a `-p` flag followed by a desired folder name to pass the custom name directly to the command. +The above commands/script will create a template from the file and run a setup script to install bun if it's not already installed, install dependencies(bun/bun-types packages). Then it will start the server and open the browser. ## Manual setup @@ -32,30 +31,30 @@ bun dev Dev server default port 3000, link: [`http://localhost:3000`](http://localhost:3000) - Visit `http://localhost:3000` in your browser and you should see Hello world and `http://localhost:3000/json` for the json # [Bun Nookit Docs](https://nookit.dev/readme) -## [BNK Server Docs](https://nookit.dev/readmes/server) +## [BNK Server Docs](https://nookit.dev/readmes/server) ## Handle Form Data + ```typescript const routes = { -// ... other routes -"/poll": { + // ... other routes + '/poll': { post: async (request) => { - const form = await request.formData(); + const form = await request.formData() - const firstName = form.get("firstName"); - const lastName = form.get("lastName"); + const firstName = form.get('firstName') + const lastName = form.get('lastName') /// do something with the data - return new Response(`Thank you ${firstName} for taking the poll!`); + return new Response(`Thank you ${firstName} for taking the poll!`) }, - } + }, } ``` diff --git a/scripts/starter/index.ts b/scripts/starter/index.ts index 111722c..10cedcd 100644 --- a/scripts/starter/index.ts +++ b/scripts/starter/index.ts @@ -1,25 +1,25 @@ -import { jsonRes, serverFactory } from "bnkit/server"; -import { RoutesWithMiddleware, middleware } from "./middlewares"; +import { jsonRes, serverFactory } from 'bnkit/server' +import { RoutesWithMiddleware, middleware } from './middlewares' const routes = { - "/": { + '/': { get: (_, { time }) => { - return new Response(`Hello World! ${time?.timestamp.toISOString()}`); + return new Response(`Hello World! ${time?.timestamp.toISOString()}`) }, }, - "/json": { + '/json': { get: (_, { time }) => jsonRes({ - message: "Hello JSON Response!", + message: 'Hello JSON Response!', ...time, }), }, -} satisfies RoutesWithMiddleware; +} satisfies RoutesWithMiddleware // Create Server Factory with middleware const { start } = serverFactory({ routes, middleware, -}); +}) -start(); +start() diff --git a/scripts/starter/middlewares.ts b/scripts/starter/middlewares.ts index 0e8c94c..0371dc2 100644 --- a/scripts/starter/middlewares.ts +++ b/scripts/starter/middlewares.ts @@ -1,13 +1,13 @@ -import { Routes, middlewareFactory, type Middleware, type MiddlewareConfigMap } from "bnkit/server"; +import { Routes, middlewareFactory, type Middleware, type MiddlewareConfigMap } from 'bnkit/server' export type RoutesWithMiddleware = Routes<{ - middleware: typeof middlewareConfig; -}>; + middleware: typeof middlewareConfig +}> type CustomMiddleware = Middleware<{ - timestamp: Date; - method: string; -}>; + timestamp: Date + method: string +}> // creating a middleware, you could easily connect your own own auth system here const customMiddlware: CustomMiddleware = (req) => { @@ -15,12 +15,12 @@ const customMiddlware: CustomMiddleware = (req) => { return { timestamp: new Date(), method: req.method, - }; -}; + } +} export const middlewareConfig = { // register a middleware under a key time: customMiddlware, -} satisfies MiddlewareConfigMap; +} satisfies MiddlewareConfigMap -export const middleware = middlewareFactory(middlewareConfig); +export const middleware = middlewareFactory(middlewareConfig) diff --git a/server/cors-types.ts b/server/cors-types.ts index f0b82d8..97871b7 100644 --- a/server/cors-types.ts +++ b/server/cors-types.ts @@ -1,9 +1,9 @@ -import { CommonHttpHeaders, RouteMethods } from "../utils/http-types"; +import { CommonHttpHeaders, RouteMethods } from '../utils/http-types' export type CORSOptions = { - origins?: string[]; - methods?: RouteMethods[]; - headers?: CommonHttpHeaders[] | string[]; - credentials?: boolean; -}; -export type ClientCORSCredentialOpts = "omit" | "same-origin" | "include"; + origins?: string[] + methods?: RouteMethods[] + headers?: CommonHttpHeaders[] | string[] + credentials?: boolean +} +export type ClientCORSCredentialOpts = 'omit' | 'same-origin' | 'include' diff --git a/server/create-cors-middleware.test.ts b/server/create-cors-middleware.test.ts index 0af0ea6..6cefc31 100644 --- a/server/create-cors-middleware.test.ts +++ b/server/create-cors-middleware.test.ts @@ -1,37 +1,37 @@ -import { describe, expect, test } from "bun:test"; -import { CORSOptions, HTTPMethod } from "utils/http-types"; -import { configCorsMW } from "./create-cors-middleware"; +import { describe, expect, test } from 'bun:test' +import { CORSOptions, HTTPMethod } from 'utils/http-types' +import { configCorsMW } from './create-cors-middleware' -const tstOrigin = "http://example.com"; -const tstMethods: HTTPMethod[] = ["GET", "POST", "PUT", "DELETE"]; -const tstHeaders = ["Content-Type"]; +const tstOrigin = 'http://example.com' +const tstMethods: HTTPMethod[] = ['GET', 'POST', 'PUT', 'DELETE'] +const tstHeaders = ['Content-Type'] const defaultOptions: CORSOptions = { allowedOrigins: [tstOrigin], allowedMethods: tstMethods, allowedHeaders: tstHeaders, -}; +} const tstReq = ( origin: string = tstOrigin, options?: { - headers?: Record; - Origin?: string; - noOrigin?: boolean; + headers?: Record + Origin?: string + noOrigin?: boolean }, ) => { - const req = (rMethod: HTTPMethod = "GET") => { + const req = (rMethod: HTTPMethod = 'GET') => { const headers = new Headers({ ...options?.headers, - }); + }) if (options?.noOrigin) { - headers.delete("Origin"); + headers.delete('Origin') return new Request(origin, { method: rMethod, headers, - }); + }) } return new Request(origin, { @@ -40,229 +40,229 @@ const tstReq = ( ...options?.headers, Origin: options?.Origin ? options.Origin : origin, }), - }); - }; + }) + } return { - get: req("GET"), - post: req("POST"), - put: req("PUT"), - delete: req("DELETE"), - options: req("OPTIONS"), - }; -}; - -describe("createCorsMiddleware function", () => { - test("default values", async () => { - const requester = tstReq(tstOrigin).get; - const headers = requester.headers; - - headers.set("Access-Control-Request-Method", "POST"); + get: req('GET'), + post: req('POST'), + put: req('PUT'), + delete: req('DELETE'), + options: req('OPTIONS'), + } +} + +describe('createCorsMiddleware function', () => { + test('default values', async () => { + const requester = tstReq(tstOrigin).get + const headers = requester.headers + + headers.set('Access-Control-Request-Method', 'POST') const mwConfig = await configCorsMW({ allowedOrigins: [tstOrigin], allowedMethods: tstMethods, - allowedHeaders: ["Content-Type"], - }); + allowedHeaders: ['Content-Type'], + }) - const response = mwConfig(requester); + const response = mwConfig(requester) - expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, PUT, DELETE"); - expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type"); - }); + expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, PUT, DELETE') + expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type') + }) - test("missing Origin header", async () => { + test('missing Origin header', async () => { const requester = tstReq(tstOrigin, { noOrigin: true, - }).get; + }).get const middlewareHandler = configCorsMW({ ...defaultOptions, - }); + }) - const response = await middlewareHandler(requester); + const response = await middlewareHandler(requester) - expect(response.status).toBe(400); - }); + expect(response.status).toBe(400) + }) - test("options request", async () => { + test('options request', async () => { const requester = tstReq(tstOrigin, { headers: { - "Access-Control-Allow-Methods": "POST", + 'Access-Control-Allow-Methods': 'POST', }, - }).options; + }).options - const response = await configCorsMW(defaultOptions)(requester); + const response = await configCorsMW(defaultOptions)(requester) - expect(response.status).toBe(204); - expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, PUT, DELETE"); - }); + expect(response.status).toBe(204) + expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, PUT, DELETE') + }) - test("Allow all origins option", async () => { + test('Allow all origins option', async () => { const requester = tstReq(tstOrigin, { headers: { Origin: tstOrigin }, - }).get; + }).get const mwConfig = await configCorsMW({ - allowedOrigins: ["*"], - allowedMethods: ["GET", "PATCH"], - }); + allowedOrigins: ['*'], + allowedMethods: ['GET', 'PATCH'], + }) - const response = mwConfig(requester); + const response = mwConfig(requester) - expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); - }); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + }) - test("unallowed method with options request", async () => { + test('unallowed method with options request', async () => { const request = tstReq(tstOrigin, { headers: { Origin: tstOrigin, - "Access-Control-Request-Method": "PATCH", + 'Access-Control-Request-Method': 'PATCH', }, - }).options; + }).options const mwConfig = await configCorsMW({ allowedOrigins: [tstOrigin], - allowedMethods: ["GET", "POST"], - }); + allowedMethods: ['GET', 'POST'], + }) - const response = mwConfig(request); + const response = mwConfig(request) - expect(response.status).toBe(405); - }); + expect(response.status).toBe(405) + }) - test("non-options request", async () => { + test('non-options request', async () => { const requester = new Request(tstOrigin, { - method: "GET", + method: 'GET', headers: new Headers({ Origin: tstOrigin, }), - }); + }) const mwConfig = await configCorsMW({ - allowedMethods: ["GET"], + allowedMethods: ['GET'], allowedOrigins: [tstOrigin], - }); + }) - const response = mwConfig(requester); + const response = mwConfig(requester) - expect(response?.headers.get("Access-Control-Allow-Origin")).toBe(tstOrigin); - }); + expect(response?.headers.get('Access-Control-Allow-Origin')).toBe(tstOrigin) + }) - test("should set Access-Control-Allow-Origin header to request origin", async () => { + test('should set Access-Control-Allow-Origin header to request origin', async () => { const request = tstReq(tstOrigin, { headers: { Origin: tstOrigin }, - }).get; + }).get const mwConfig = await configCorsMW({ allowedOrigins: [tstOrigin], - }); + }) - const response = mwConfig(request); + const response = mwConfig(request) - expect(response.headers.get("Access-Control-Allow-Origin")).toBe(tstOrigin); - }); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe(tstOrigin) + }) - test("should set Access-Control-Allow-Origin header to * if allowedOrigins includes *", async () => { + test('should set Access-Control-Allow-Origin header to * if allowedOrigins includes *', async () => { const request = tstReq(tstOrigin, { headers: { Origin: tstOrigin }, - }).get; + }).get - const mwConfig = await configCorsMW({ allowedOrigins: ["*"] }); + const mwConfig = await configCorsMW({ allowedOrigins: ['*'] }) - const response = mwConfig(request); + const response = mwConfig(request) - expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); - }); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') + }) - test("should set Access-Control-Allow-Methods header to allowedMethods", async () => { - const request = new Request("http://example.com", { - headers: { Origin: "http://example.com" }, - method: "GET", - }); + test('should set Access-Control-Allow-Methods header to allowedMethods', async () => { + const request = new Request('http://example.com', { + headers: { Origin: 'http://example.com' }, + method: 'GET', + }) const mwConfig = await configCorsMW({ - allowedMethods: ["GET", "POST"], + allowedMethods: ['GET', 'POST'], allowedOrigins: [tstOrigin], - }); + }) - const response = mwConfig(request); - expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST"); - }); + const response = mwConfig(request) + expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST') + }) - test("should set Access-Control-Allow-Headers header to allowedHeaders", async () => { + test('should set Access-Control-Allow-Headers header to allowedHeaders', async () => { const request = tstReq(tstOrigin, { - headers: { Origin: tstOrigin, "Content-Type": "application/json" }, - }).get; + headers: { Origin: tstOrigin, 'Content-Type': 'application/json' }, + }).get const mwConfig = configCorsMW({ - allowedHeaders: ["Content-Type"], + allowedHeaders: ['Content-Type'], allowedOrigins: [tstOrigin], - }); + }) - const response = await mwConfig(request); + const response = await mwConfig(request) console.log({ mwConfig, request, response, - }); + }) - expect(response.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type"); - }); + expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Content-Type') + }) - test("should return 400 Bad Request if request does not have Origin header", async () => { + test('should return 400 Bad Request if request does not have Origin header', async () => { const request = tstReq(tstOrigin, { noOrigin: true, - }).get; - const mwConfig = await configCorsMW({}); - const response = mwConfig(request); + }).get + const mwConfig = await configCorsMW({}) + const response = mwConfig(request) - expect(response.status).toBe(400); - }); + expect(response.status).toBe(400) + }) - test("should return 403 Forbidden if request origin is not allowed", async () => { - const request = new Request("http://example.org", { - headers: { Origin: "http://example.org" }, - }); + test('should return 403 Forbidden if request origin is not allowed', async () => { + const request = new Request('http://example.org', { + headers: { Origin: 'http://example.org' }, + }) const mwConfig = await configCorsMW({ - allowedOrigins: ["http://example.com"], - }); + allowedOrigins: ['http://example.com'], + }) - const response = mwConfig(request); + const response = mwConfig(request) - expect(response.status).toBe(403); - }); + expect(response.status).toBe(403) + }) - test("should return 405 Method Not Allowed if request method is not allowed", async () => { + test('should return 405 Method Not Allowed if request method is not allowed', async () => { const request = tstReq(tstOrigin, { headers: { - "Access-Control-Allow-Methods": "POST", + 'Access-Control-Allow-Methods': 'POST', }, - }).post; + }).post const response = await configCorsMW( { - allowedMethods: ["GET"], + allowedMethods: ['GET'], allowedOrigins: [tstOrigin], }, true, - )(request); + )(request) - expect(response.status).toBe(405); - }); + expect(response.status).toBe(405) + }) - test("should return 204 No Content if request method is options and allowed", async () => { + test('should return 204 No Content if request method is options and allowed', async () => { const request = tstReq(tstOrigin, { headers: { - "Access-Control-Allow-Methods": "GET", + 'Access-Control-Allow-Methods': 'GET', }, - }).options; + }).options const mwConfig = await configCorsMW({ - allowedMethods: ["GET"], + allowedMethods: ['GET'], allowedOrigins: [tstOrigin], - }); + }) - const response = mwConfig(request); + const response = mwConfig(request) - expect(response.status).toBe(204); - }); -}); + expect(response.status).toBe(204) + }) +}) diff --git a/server/create-cors-middleware.ts b/server/create-cors-middleware.ts index 7f1b4fe..f98cfcf 100644 --- a/server/create-cors-middleware.ts +++ b/server/create-cors-middleware.ts @@ -1,106 +1,106 @@ -import { CORSOptions } from "../utils/http-types"; -import { Middleware } from "./middleware-types"; +import { CORSOptions } from '../utils/http-types' +import { Middleware } from './middleware-types' const setAllowOrigin = (headers: Headers, originToSet: string) => - headers.set("Access-Control-Allow-Origin", originToSet || ""); + headers.set('Access-Control-Allow-Origin', originToSet || '') const setAllowMethods = (headers: Headers, methods: string[]) => - headers.set("Access-Control-Allow-Methods", methods.join(", ")); + headers.set('Access-Control-Allow-Methods', methods.join(', ')) const addAllowHeader = (headers: Headers, options?: CORSOptions) => { if (options?.allowedHeaders?.join) { - headers.set("Access-Control-Allow-Headers", options.allowedHeaders.join(", ")); + headers.set('Access-Control-Allow-Headers', options.allowedHeaders.join(', ')) } -}; +} const setAllowCredentials = (headers: Headers, options?: CORSOptions) => - options?.credentials && headers.set("Access-Control-Allow-Credentials", "true"); + options?.credentials && headers.set('Access-Control-Allow-Credentials', 'true') export const configCorsMW = (options?: CORSOptions, debug: boolean = false): Middleware => { - const allowedMethods: string[] = options?.allowedMethods || []; + const allowedMethods: string[] = options?.allowedMethods || [] const log = (input: any) => { if (debug) { - console.log(input); + console.log(input) } - }; + } const sendErrorResponse = (status: number, statusText: string, detail: string, headers?: Headers) => { - const errorResponse = { statusText, detail }; + const errorResponse = { statusText, detail } if (debug) { - console.error(errorResponse); + console.error(errorResponse) } - const finalHeaders = headers; + const finalHeaders = headers - finalHeaders?.set("Content-Type", "application/json"); + finalHeaders?.set('Content-Type', 'application/json') return new Response(JSON.stringify(errorResponse), { status, headers: finalHeaders, - }); - }; + }) + } const requestHandler = (request: Request) => { - const reqMethod = request.method; - const reqOrigin = request.headers.get("Origin"); - const allowedOrigins = options?.allowedOrigins || []; - const originAllowed = allowedOrigins.includes(reqOrigin || ""); + const reqMethod = request.method + const reqOrigin = request.headers.get('Origin') + const allowedOrigins = options?.allowedOrigins || [] + const originAllowed = allowedOrigins.includes(reqOrigin || '') if (debug && !originAllowed) { log({ allowedOrigins, reqOrigin, originAllowed, - }); + }) } // if (originToSet) { - const headers = new Headers(); - const originToSet = allowedOrigins.includes("*") ? "*" : reqOrigin; + const headers = new Headers() + const originToSet = allowedOrigins.includes('*') ? '*' : reqOrigin if (!reqOrigin) { - return sendErrorResponse(400, "Bad Request", "Origin header missing"); + return sendErrorResponse(400, 'Bad Request', 'Origin header missing') } // Set Access-Control-Allow-Origin header - setAllowOrigin(headers, originToSet || ""); + setAllowOrigin(headers, originToSet || '') // Set Access-Control-Allow-Methods header - setAllowMethods(headers, allowedMethods); + setAllowMethods(headers, allowedMethods) // Set Access-Control-Allow-Headers header - addAllowHeader(headers, options); + addAllowHeader(headers, options) // Set Access-Control-Allow-Credentials header - setAllowCredentials(headers, options); + setAllowCredentials(headers, options) // check if request method is options and allowed - if (reqMethod === "OPTIONS") { - const optionRequestMethod = request.headers.get("Access-Control-Allow-Methods"); + if (reqMethod === 'OPTIONS') { + const optionRequestMethod = request.headers.get('Access-Control-Allow-Methods') - if (!allowedMethods.includes(optionRequestMethod || "")) { - return sendErrorResponse(405, "Method Not Allowed", `Method ${optionRequestMethod} is not allowed`, headers); + if (!allowedMethods.includes(optionRequestMethod || '')) { + return sendErrorResponse(405, 'Method Not Allowed', `Method ${optionRequestMethod} is not allowed`, headers) } if (!headers) { - return sendErrorResponse(500, "Internal Server Error", "Missing headers for options return", headers); + return sendErrorResponse(500, 'Internal Server Error', 'Missing headers for options return', headers) } // Set Access-Control-Max-Age for caching preflight request // headers.set("Access-Control-Max-Age", "600"); // 10 minutes - return new Response(null, { status: 204, headers }); + return new Response(null, { status: 204, headers }) } - if (!allowedOrigins.includes(reqOrigin || "")) { - setAllowMethods(headers, allowedMethods); - return sendErrorResponse(403, "Forbidden", `Origin ${reqOrigin} not allowed`, headers); + if (!allowedOrigins.includes(reqOrigin || '')) { + setAllowMethods(headers, allowedMethods) + return sendErrorResponse(403, 'Forbidden', `Origin ${reqOrigin} not allowed`, headers) } if (!allowedMethods.includes(request.method)) { - return sendErrorResponse(405, "Method Not Allowed", `Method ${reqMethod} not allowed`, headers); + return sendErrorResponse(405, 'Method Not Allowed', `Method ${reqMethod} not allowed`, headers) } - return new Response(null, { status: 200, headers }); - }; + return new Response(null, { status: 200, headers }) + } - return requestHandler; -}; + return requestHandler +} diff --git a/server/incoming-request-handler.ts b/server/incoming-request-handler.ts index 1ea057f..5c0cc51 100644 --- a/server/incoming-request-handler.ts +++ b/server/incoming-request-handler.ts @@ -1,13 +1,13 @@ -import { InferMiddlewareDataMap, MiddlewareConfigMap } from "."; -import { middlewareFactory } from "./middleware-manager"; -import { RouteHandler, Routes } from "./routes"; +import { InferMiddlewareDataMap, MiddlewareConfigMap } from '.' +import { middlewareFactory } from './middleware-manager' +import { RouteHandler, Routes } from './routes' function isValidRegex(str: string): boolean { - if (str === "/") return false; + if (str === '/') return false try { - new RegExp(str); - return true; + new RegExp(str) + return true } catch (e) { - return false; + return false } } @@ -21,46 +21,46 @@ export const serverRequestHandler = < middlewareRet, optionsHandler, }: { - req: Request; - routes: Routes; - middlewareRet?: MiddlewareFactory; - optionsHandler?: RouteHandler; + req: Request + routes: Routes + middlewareRet?: MiddlewareFactory + optionsHandler?: RouteHandler }): Promise => { - const url = new URL(req.url); - let matchedHandler: RouteHandler | null | undefined = null; + const url = new URL(req.url) + let matchedHandler: RouteHandler | null | undefined = null - const pathRoutes = routes[url.pathname]; + const pathRoutes = routes[url.pathname] - matchedHandler = pathRoutes ? pathRoutes[req.method.toLowerCase() as keyof typeof pathRoutes] : null; + matchedHandler = pathRoutes ? pathRoutes[req.method.toLowerCase() as keyof typeof pathRoutes] : null // try regex match after direct string match if (!matchedHandler) { for (const pattern in routes) { if (isValidRegex(pattern)) { - const regex = new RegExp(pattern, "i"); + const regex = new RegExp(pattern, 'i') if (regex.test(url.pathname)) { - matchedHandler = routes[pattern][req.method.toLowerCase() as keyof (typeof routes)[typeof pattern]]; - break; + matchedHandler = routes[pattern][req.method.toLowerCase() as keyof (typeof routes)[typeof pattern]] + break } } } } - if (!matchedHandler && !optionsHandler) return Promise.resolve(new Response("Not Found", { status: 404 })); - const executeMiddlewares = middlewareRet?.executeMiddlewares; + if (!matchedHandler && !optionsHandler) return Promise.resolve(new Response('Not Found', { status: 404 })) + const executeMiddlewares = middlewareRet?.executeMiddlewares // Ensure that middleware execution is properly handled when it's not provided - const middlewareResponses = executeMiddlewares ? executeMiddlewares(req) : Promise.resolve({} as MiddlewareDataMap); + const middlewareResponses = executeMiddlewares ? executeMiddlewares(req) : Promise.resolve({} as MiddlewareDataMap) return middlewareResponses .then((resolvedMwResponses) => { - if (req.method === "options" && !matchedHandler && optionsHandler) { - return optionsHandler(req, resolvedMwResponses as MiddlewareDataMap); + if (req.method === 'options' && !matchedHandler && optionsHandler) { + return optionsHandler(req, resolvedMwResponses as MiddlewareDataMap) } return matchedHandler ? matchedHandler(req, resolvedMwResponses as MiddlewareDataMap) - : new Response("Method Not Allowed", { status: 405 }); + : new Response('Method Not Allowed', { status: 405 }) }) - .catch((err) => new Response(err.message, { status: 500 })); -}; + .catch((err) => new Response(err.message, { status: 500 })) +} diff --git a/server/index.ts b/server/index.ts index b79e1e9..68bca5d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,17 +1,9 @@ -export { middlewareFactory } from "./middleware-manager"; -export type { InferMiddlewareFromFactory } from "./middleware-manager"; -export { serverFactory } from "./server-factory"; +export { middlewareFactory } from './middleware-manager' +export type { InferMiddlewareFromFactory } from './middleware-manager' +export { serverFactory } from './server-factory' -export type { - InferMiddlewareDataMap, - Middleware, - MiddlewareConfigMap, -} from "./middleware-types"; +export type { InferMiddlewareDataMap, Middleware, MiddlewareConfigMap } from './middleware-types' -export type { - RouteHandler, - RouteOptionsMiddlewareManger, - Routes, -} from "./routes"; +export type { RouteHandler, RouteOptionsMiddlewareManger, Routes } from './routes' -export { htmlRes, jsonRes, redirectRes } from "./server-utils"; +export { htmlRes, jsonRes, redirectRes } from './server-utils' diff --git a/server/middleware-manager.ts b/server/middleware-manager.ts index 5c9d88e..c099519 100644 --- a/server/middleware-manager.ts +++ b/server/middleware-manager.ts @@ -1,45 +1,45 @@ -import { InferMiddlewareDataMap, MiddlewareConfigMap } from "./middleware-types"; +import { InferMiddlewareDataMap, MiddlewareConfigMap } from './middleware-types' export type InferMiddlewareFromFactory = ReturnType< - ReturnType["inferTypes"] ->; + ReturnType['inferTypes'] +> export const middlewareFactory = (middlewareOptions: T) => { const middlewares: MiddlewareConfigMap = { ...middlewareOptions, - }; + } const executeMiddlewares = async (req: Request) => { - const results: InferMiddlewareDataMap = {} as InferMiddlewareDataMap; + const results: InferMiddlewareDataMap = {} as InferMiddlewareDataMap // An array to store promises which will resolve with [key, value] pairs - const promises: Promise<[string, any]>[] = []; + const promises: Promise<[string, any]>[] = [] for (const [id, mw] of Object.entries(middlewares)) { - const result = mw(req); + const result = mw(req) if (result instanceof Promise) { // Push a promise which will resolve with [id, resolvedValue] - promises.push(result.then((resolvedValue) => [id, resolvedValue])); + promises.push(result.then((resolvedValue) => [id, resolvedValue])) } else { - results[id as keyof T] = result; + results[id as keyof T] = result } } // Wait for all promises to resolve - const resolvedPairs = await Promise.all(promises); + const resolvedPairs = await Promise.all(promises) // Map the resolved [key, value] pairs to the results object for (const [key, value] of resolvedPairs) { - results[key as keyof T] = value; + results[key as keyof T] = value } - return results; - }; + return results + } const inferTypes = () => { - return middlewares as InferMiddlewareDataMap; - }; + return middlewares as InferMiddlewareDataMap + } - return { executeMiddlewares, inferTypes }; -}; + return { executeMiddlewares, inferTypes } +} diff --git a/server/middleware-types.test.ts b/server/middleware-types.test.ts index d61e239..b7fe40a 100644 --- a/server/middleware-types.test.ts +++ b/server/middleware-types.test.ts @@ -1,82 +1,82 @@ -import { InferMiddlewareDataMap } from "."; -import { TypeCheck, typeCheck } from "../test-utils/type-testers"; +import { InferMiddlewareDataMap } from '.' +import { TypeCheck, typeCheck } from '../test-utils/type-testers' const middlewareMap = { syncMiddleware: (req: Request) => ({ sync: true }), asyncMiddleware: async (req: Request) => ({ async: true }), -}; +} // example inference -type Inferred = InferMiddlewareDataMap; +type Inferred = InferMiddlewareDataMap // check that inffered is correct const inferred: Inferred = { syncMiddleware: { sync: true }, asyncMiddleware: { async: true }, -}; +} // check satisfies const satisfiesMap = { syncMiddleware: { sync: true }, asyncMiddleware: { async: true }, -} satisfies Inferred; +} satisfies Inferred // Compile-time checks -type TestSync = TypeCheck; -type TestAsync = TypeCheck; +type TestSync = TypeCheck +type TestAsync = TypeCheck -typeCheck(); -typeCheck(); +typeCheck() +typeCheck() const middlewareMapDifferentReturns = { - stringMiddleware: (req: Request) => "stringValue", + stringMiddleware: (req: Request) => 'stringValue', numberMiddleware: (req: Request) => 42, booleanMiddleware: (req: Request) => true, -}; +} -type InferredDifferentReturns = InferMiddlewareDataMap; +type InferredDifferentReturns = InferMiddlewareDataMap -type TestString = TypeCheck; -type TestNumber = TypeCheck; -type TestBoolean = TypeCheck; +type TestString = TypeCheck +type TestNumber = TypeCheck +type TestBoolean = TypeCheck -typeCheck(); -typeCheck(); -typeCheck(); +typeCheck() +typeCheck() +typeCheck() const middlewareMapNestedPromise = { nestedPromiseMiddleware: async (req: Request) => Promise.resolve(42), -}; +} -type InferredNestedPromise = InferMiddlewareDataMap; +type InferredNestedPromise = InferMiddlewareDataMap -type TestNestedPromise = TypeCheck; +type TestNestedPromise = TypeCheck -typeCheck(); +typeCheck() class ComplexClass { - prop: string; + prop: string constructor(prop: string) { - this.prop = prop; + this.prop = prop } } const middlewareMapComplexReturns = { - complexReturnMiddleware: (req: Request) => new ComplexClass("value"), -}; + complexReturnMiddleware: (req: Request) => new ComplexClass('value'), +} -type InferredComplexReturns = InferMiddlewareDataMap; +type InferredComplexReturns = InferMiddlewareDataMap -type TestComplexReturns = TypeCheck; +type TestComplexReturns = TypeCheck -typeCheck(); +typeCheck() const middlewareMapFunctionReturn = { - functionReturnMiddleware: (req: Request) => () => "hello", -}; + functionReturnMiddleware: (req: Request) => () => 'hello', +} -type InferredFunctionReturn = InferMiddlewareDataMap; +type InferredFunctionReturn = InferMiddlewareDataMap -type TestFunctionReturn = TypeCheck string>; +type TestFunctionReturn = TypeCheck string> -typeCheck(); +typeCheck() diff --git a/server/middleware-types.ts b/server/middleware-types.ts index 3320ae1..b28b0da 100644 --- a/server/middleware-types.ts +++ b/server/middleware-types.ts @@ -1,10 +1,10 @@ -export type Middleware = (request: Request) => Res; +export type Middleware = (request: Request) => Res export type MiddlewareConfigMap = { - [id: string]: Middleware; -}; -export type UnwrapPromise = T extends Promise ? U : T; + [id: string]: Middleware +} +export type UnwrapPromise = T extends Promise ? U : T export type InferMiddlewareDataMap = { - [K in keyof T]: UnwrapPromise>; -}; + [K in keyof T]: UnwrapPromise> +} diff --git a/server/route-types.ts b/server/route-types.ts new file mode 100644 index 0000000..58d8642 --- /dev/null +++ b/server/route-types.ts @@ -0,0 +1,37 @@ +import { RouteMethods } from '../utils/http-types' +import { middlewareFactory } from './middleware-manager' + +import { HTMLodyPlugin, htmlodyBuilder } from '../htmlody' +import type { InferMiddlewareDataMap, MiddlewareConfigMap } from './middleware-types' + +export type RouteHandler = (request: Request, middlewareData: M) => Response | Promise + +type RouteTypeOptsT = { + middleware?: MiddlewareConfigMap + htmlody?: { + plugins?: HTMLodyPlugin[] + builder: typeof htmlodyBuilder + } + + htmlodyBuilder?: typeof htmlodyBuilder +} + +export type Routes< + RouteType extends RouteTypeOptsT = RouteTypeOptsT, + InferredMiddlewareData = RouteType['middleware'] extends MiddlewareConfigMap + ? InferMiddlewareDataMap + : never, +> = { + [path: string]: Partial<{ + [K in RouteMethods]: RouteHandler + }> +} + +export type RouteOptionsMiddlewareManger< + MiddlewareFactory extends ReturnType, + MiddlewareDataMap = InferMiddlewareDataMap, +> = { + [path: string]: Partial<{ + [K in RouteMethods]: RouteHandler + }> +} diff --git a/server/routes.ts b/server/routes.ts index d6a8116..58d8642 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,37 +1,37 @@ -import { RouteMethods } from "../utils/http-types"; -import { middlewareFactory } from "./middleware-manager"; +import { RouteMethods } from '../utils/http-types' +import { middlewareFactory } from './middleware-manager' -import { HTMLodyPlugin, htmlodyBuilder } from "../htmlody"; -import type { InferMiddlewareDataMap, MiddlewareConfigMap } from "./middleware-types"; +import { HTMLodyPlugin, htmlodyBuilder } from '../htmlody' +import type { InferMiddlewareDataMap, MiddlewareConfigMap } from './middleware-types' -export type RouteHandler = (request: Request, middlewareData: M) => Response | Promise; +export type RouteHandler = (request: Request, middlewareData: M) => Response | Promise type RouteTypeOptsT = { - middleware?: MiddlewareConfigMap; + middleware?: MiddlewareConfigMap htmlody?: { - plugins?: HTMLodyPlugin[]; - builder: typeof htmlodyBuilder; - }; + plugins?: HTMLodyPlugin[] + builder: typeof htmlodyBuilder + } - htmlodyBuilder?: typeof htmlodyBuilder; -}; + htmlodyBuilder?: typeof htmlodyBuilder +} export type Routes< RouteType extends RouteTypeOptsT = RouteTypeOptsT, - InferredMiddlewareData = RouteType["middleware"] extends MiddlewareConfigMap - ? InferMiddlewareDataMap + InferredMiddlewareData = RouteType['middleware'] extends MiddlewareConfigMap + ? InferMiddlewareDataMap : never, > = { [path: string]: Partial<{ - [K in RouteMethods]: RouteHandler; - }>; -}; + [K in RouteMethods]: RouteHandler + }> +} export type RouteOptionsMiddlewareManger< MiddlewareFactory extends ReturnType, MiddlewareDataMap = InferMiddlewareDataMap, > = { [path: string]: Partial<{ - [K in RouteMethods]: RouteHandler; - }>; -}; + [K in RouteMethods]: RouteHandler + }> +} diff --git a/server/server-factory.ts b/server/server-factory.ts index e70016f..f4cd159 100644 --- a/server/server-factory.ts +++ b/server/server-factory.ts @@ -1,8 +1,8 @@ -import { WebSocketHandler } from "bun"; -import { serverRequestHandler } from "./incoming-request-handler"; -import { middlewareFactory } from "./middleware-manager"; -import { InferMiddlewareDataMap, MiddlewareConfigMap } from "./middleware-types"; -import { RouteHandler, Routes } from "./routes"; +import { WebSocketHandler } from 'bun' +import { serverRequestHandler } from './incoming-request-handler' +import { middlewareFactory } from './middleware-manager' +import { InferMiddlewareDataMap, MiddlewareConfigMap } from './middleware-types' +import { RouteHandler, Routes } from './routes' export const serverFactory = < MiddlewareFactory extends ReturnType, @@ -14,19 +14,18 @@ export const serverFactory = < fetchHandler = serverRequestHandler, optionsHandler, serve = Bun.serve, - websocket + websocket, }: { - middleware?: MiddlewareFactory; - routes: Routes; - fetchHandler?: typeof serverRequestHandler; - optionsHandler?: RouteHandler; - serve?: typeof Bun.serve; - websocket: WebSocketHandler + middleware?: MiddlewareFactory + routes: Routes + fetchHandler?: typeof serverRequestHandler + optionsHandler?: RouteHandler + serve?: typeof Bun.serve + websocket?: WebSocketHandler }) => { const start = (port = 3000) => { - if (Bun?.env.NODE_ENV === "development") { - console; - console.log("Starting server on port: ", port); + if (Bun?.env.NODE_ENV === 'development') { + console.log('Starting server on port: ', port) } return serve({ port, @@ -37,12 +36,11 @@ export const serverFactory = < middlewareRet: middleware, optionsHandler, }), - websocket - }); - - }; + websocket, + }) + } return { start, - }; -}; \ No newline at end of file + } +} diff --git a/server/server-utils.ts b/server/server-utils.ts index 23eef74..e620c02 100644 --- a/server/server-utils.ts +++ b/server/server-utils.ts @@ -1,24 +1,24 @@ export function parseQueryParams(request: Request): ParamsType { - const url = new URL(request.url); - const params: ParamsType = {} as ParamsType; + const url = new URL(request.url) + const params: ParamsType = {} as ParamsType url.searchParams.forEach((value, key) => { // @ts-ignore - params[key] = value as any; - }); + params[key] = value as any + }) - return params; + return params } export function parseRequestHeaders(request: Request): HeadersType { - return request.headers.toJSON() as unknown as HeadersType; + return request.headers.toJSON() as unknown as HeadersType } export type JSONResType = ( body: JSONBodyGeneric, options?: ResponseInit, response?: Response, -) => Response; +) => Response // json res creates it's own response object, but if one is passed in, it will copy headers export const jsonRes: JSONResType = (body, options = {}, response) => { @@ -27,48 +27,48 @@ export const jsonRes: JSONResType = (body, options = {}, response) => { const combinedHeaders: HeadersInit = { ...options?.headers, ...new Headers(response?.headers), - }; + } return new Response(JSON.stringify(body), { ...options, headers: { ...combinedHeaders, // jsonRes should allows have json content type - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, - }); -}; + }) +} export function htmlRes(body: string, options?: ResponseInit): Response { return new Response(body, { ...options, headers: { - "Content-Type": "text/html", + 'Content-Type': 'text/html', ...options?.headers, }, - }); + }) } type RedirectOptions = { - status?: number; - statusText?: string; - headers?: Record; - body?: string | null; - cookies?: Record; -}; + status?: number + statusText?: string + headers?: Record + body?: string | null + cookies?: Record +} export const redirectRes = (url: string, options: RedirectOptions = {}): Response => { const defaultHeaders: Record = { Location: url, - }; + } // Merge custom headers with default headers - const headers = { ...defaultHeaders, ...options.headers }; + const headers = { ...defaultHeaders, ...options.headers } // Set cookies if provided if (options.cookies) { for (const [name, value] of Object.entries(options.cookies)) { - headers["Set-Cookie"] = `${name}=${value}`; + headers['Set-Cookie'] = `${name}=${value}` } } @@ -76,5 +76,5 @@ export const redirectRes = (url: string, options: RedirectOptions = {}): Respons status: options.status || 302, statusText: options.statusText, headers, - }); -}; + }) +} diff --git a/sqlite/index.ts b/sqlite/index.ts index 4e0597c..61e89fc 100644 --- a/sqlite/index.ts +++ b/sqlite/index.ts @@ -1,8 +1,3 @@ -export { sqliteFactory as createSqliteFactory } from "./sqlite-factory"; -export { - createItem, - deleteItemById, - readItems, - updateItem, -} from "./sqlite-utils/crud-fn-utils"; -export { createTableQuery } from "./sqlite-utils/table-query-gen"; +export { sqliteFactory as createSqliteFactory } from './sqlite-factory' +export { createItem, deleteItemById, readItems, updateItem } from './sqlite-utils/crud-fn-utils' +export { createTableQuery } from './sqlite-utils/table-query-gen' diff --git a/sqlite/sqlite-factory.test.ts b/sqlite/sqlite-factory.test.ts index 80c95c8..7162fef 100644 --- a/sqlite/sqlite-factory.test.ts +++ b/sqlite/sqlite-factory.test.ts @@ -1,90 +1,90 @@ -import Database from "bun:sqlite"; -import { beforeEach, describe, expect, test } from "bun:test"; -import { SchemaMap, sqliteFactory } from "./sqlite-factory"; +import Database from 'bun:sqlite' +import { beforeEach, describe, expect, test } from 'bun:test' +import { SchemaMap, sqliteFactory } from './sqlite-factory' -let db = new Database(":memory:"); +let db = new Database(':memory:') const noteSchema = { - id: { type: "TEXT" }, - text: { type: "TEXT" }, -} satisfies SchemaMap; + id: { type: 'TEXT' }, + text: { type: 'TEXT' }, +} satisfies SchemaMap -describe("createSqliteFactory", () => { +describe('createSqliteFactory', () => { beforeEach(() => { - db = new Database(":memory:"); - }); - test("It should create a db factory", () => { - const { dbTableFactory } = sqliteFactory({ db }); + db = new Database(':memory:') + }) + test('It should create a db factory', () => { + const { dbTableFactory } = sqliteFactory({ db }) - expect(dbTableFactory).toBeDefined(); - }); + expect(dbTableFactory).toBeDefined() + }) - test("should create and read a note in sqlite", async () => { - const { dbTableFactory } = sqliteFactory({ db }); + test('should create and read a note in sqlite', async () => { + const { dbTableFactory } = sqliteFactory({ db }) const notesTable = dbTableFactory({ schema: noteSchema, - tableName: "notes", + tableName: 'notes', debug: false, - }); + }) notesTable.create({ - id: "1", - text: "some text", - }); + id: '1', + text: 'some text', + }) - const notes = notesTable.getAll(); + const notes = notesTable.getAll() - expect(notes).toEqual([{ id: "1", text: "some text" }]); - }); - test("should create and read a note in sqlite and update it", () => { - const { dbTableFactory } = sqliteFactory({ db, debug: true }); + expect(notes).toEqual([{ id: '1', text: 'some text' }]) + }) + test('should create and read a note in sqlite and update it', () => { + const { dbTableFactory } = sqliteFactory({ db, debug: true }) const notesTable = dbTableFactory({ schema: noteSchema, - tableName: "notes", + tableName: 'notes', debug: false, - }); + }) notesTable.create({ - id: "1", - text: "some text", - }); + id: '1', + text: 'some text', + }) - const notes = notesTable.getAll(); + const notes = notesTable.getAll() - expect(notes).toEqual([{ id: "1", text: "some text" }]); + expect(notes).toEqual([{ id: '1', text: 'some text' }]) - notesTable.update("1", { - text: "some text updated", - }); + notesTable.update('1', { + text: 'some text updated', + }) - const updatedNotes = notesTable.getAll(); + const updatedNotes = notesTable.getAll() - expect(updatedNotes).toEqual([{ id: "1", text: "some text updated" }]); - }); - test("should create and read a note in sqlite and delete it", () => { - const { dbTableFactory } = sqliteFactory({ db, debug: true }); + expect(updatedNotes).toEqual([{ id: '1', text: 'some text updated' }]) + }) + test('should create and read a note in sqlite and delete it', () => { + const { dbTableFactory } = sqliteFactory({ db, debug: true }) const notesTable = dbTableFactory({ schema: noteSchema, - tableName: "notes", + tableName: 'notes', debug: false, - }); + }) notesTable.create({ - id: "1", - text: "some text", - }); + id: '1', + text: 'some text', + }) - const notes = notesTable.getAll(); + const notes = notesTable.getAll() - expect(notes).toEqual([{ id: "1", text: "some text" }]); + expect(notes).toEqual([{ id: '1', text: 'some text' }]) - notesTable.delById("1"); + notesTable.delById('1') - const updatedNotes = notesTable.getAll(); + const updatedNotes = notesTable.getAll() - expect(updatedNotes).toEqual([]); - }); -}); + expect(updatedNotes).toEqual([]) + }) +}) diff --git a/sqlite/sqlite-factory.ts b/sqlite/sqlite-factory.ts index ade72f2..2c45de1 100644 --- a/sqlite/sqlite-factory.ts +++ b/sqlite/sqlite-factory.ts @@ -1,87 +1,79 @@ -import { Database } from "bun:sqlite"; +import { Database } from 'bun:sqlite' -import { - SqliteTableFactoryParams, - sqliteTableFactory, -} from "./sqlite-table-factory"; +import { SqliteTableFactoryParams, sqliteTableFactory } from './sqlite-table-factory' type SqliteFactoryParams = { - db: Database; - debug?: boolean; - enableForeignKeys?: boolean; -}; + db: Database + debug?: boolean + enableForeignKeys?: boolean +} -type DBTableFactoryParams = Omit< - SqliteTableFactoryParams, - "db" -> & { - debug?: boolean; -}; +type DBTableFactoryParams = Omit, 'db'> & { + debug?: boolean +} // Mapping of SQLite types to TypeScript types. export type SQLiteSchemaToTSMap = { - TEXT: string; - NUMERIC: number | string; - INTEGER: number; - REAL: number; - BLOB: any; - DATE: Date; -}; + TEXT: string + NUMERIC: number | string + INTEGER: number + REAL: number + BLOB: any + DATE: Date +} -export type SQLiteData = keyof SQLiteSchemaToTSMap; +export type SQLiteData = keyof SQLiteSchemaToTSMap export type FieldDef = { - type: SQLiteData; - primaryKey?: boolean; - unique?: boolean; - foreignKey?: string; - required?: boolean; - defaultValue?: string | number; -}; + type: SQLiteData + primaryKey?: boolean + unique?: boolean + foreignKey?: string + required?: boolean + defaultValue?: string | number +} // Mapped type that takes a schema with SQLite types and returns a schema with TypeScript types. export type SQLInfer = { - [K in keyof T]: T[K] extends FieldDef - ? SQLiteSchemaToTSMap[T[K]["type"]] - : never; -}; + [K in keyof T]: T[K] extends FieldDef ? SQLiteSchemaToTSMap[T[K]['type']] : never +} -export type SchemaMap = Partial>; +export type SchemaMap = Partial> export const getType = (schema: T): SQLInfer => { - return undefined as any as SQLInfer; -}; + return undefined as any as SQLInfer +} export function sqliteFactory({ - db, - debug = false, - // because foreign keys in sqlite are disabled by default - // https://renenyffenegger.ch/notes/development/databases/SQLite/sql/pragma/foreign_keys#:~:text=pragma%20foreign_keys%20%3D%20on%20enforces%20foreign,does%20not%20enforce%20foreign%20keys.&text=Explicitly%20turning%20off%20the%20validation,dump%20'ed%20database. - // turning off foreign keys may be using when importing a .dump'ed database - enableForeignKeys = false, + db, + debug = false, + // because foreign keys in sqlite are disabled by default + // https://renenyffenegger.ch/notes/development/databases/SQLite/sql/pragma/foreign_keys#:~:text=pragma%20foreign_keys%20%3D%20on%20enforces%20foreign,does%20not%20enforce%20foreign%20keys.&text=Explicitly%20turning%20off%20the%20validation,dump%20'ed%20database. + // turning off foreign keys may be using when importing a .dump'ed database + enableForeignKeys = false, }: SqliteFactoryParams) { - if (enableForeignKeys) { - // Enable foreign key constraints - db.query("PRAGMA foreign_keys = ON;").run(); - } + if (enableForeignKeys) { + // Enable foreign key constraints + db.query('PRAGMA foreign_keys = ON;').run() + } - function dbTableFactory({ - debug: debugTable = debug || false, - schema, - tableName, - }: DBTableFactoryParams) { - return sqliteTableFactory( - { - db, - schema, - tableName, - }, - { - debug: debugTable, - enableForeignKeys: debug, - }, - ); - } + function dbTableFactory({ + debug: debugTable = debug || false, + schema, + tableName, + }: DBTableFactoryParams) { + return sqliteTableFactory( + { + db, + schema, + tableName, + }, + { + debug: debugTable, + enableForeignKeys: debug, + }, + ) + } - return { dbTableFactory }; + return { dbTableFactory } } diff --git a/sqlite/sqlite-table-factory.test.ts b/sqlite/sqlite-table-factory.test.ts index 8be6a45..89d5381 100644 --- a/sqlite/sqlite-table-factory.test.ts +++ b/sqlite/sqlite-table-factory.test.ts @@ -1,64 +1,64 @@ -import { Database } from "bun:sqlite"; -import { afterEach, describe, expect, test } from "bun:test"; -import { SchemaMap } from "./sqlite-factory"; -import { sqliteTableFactory } from "./sqlite-table-factory"; +import { Database } from 'bun:sqlite' +import { afterEach, describe, expect, test } from 'bun:test' +import { SchemaMap } from './sqlite-factory' +import { sqliteTableFactory } from './sqlite-table-factory' -const mockDb = new Database(":memory:"); +const mockDb = new Database(':memory:') const testSchema = { - id: { type: "TEXT" }, - name: { type: "TEXT" }, - age: { type: "INTEGER" }, -} satisfies SchemaMap; + id: { type: 'TEXT' }, + name: { type: 'TEXT' }, + age: { type: 'INTEGER' }, +} satisfies SchemaMap const factoryOptions = { debug: true, -}; +} -describe("sqliteTableFactory", () => { +describe('sqliteTableFactory', () => { const factory = sqliteTableFactory( { db: mockDb, schema: testSchema, - tableName: "test", + tableName: 'test', }, factoryOptions, - ); + ) afterEach(() => { // Clean up the database after each test (for isolation purposes) - mockDb.query("delete FROM test;").run(); - }); + mockDb.query('delete FROM test;').run() + }) - test("should insert an item into the database using the factory", () => { - const item = { id: "1", name: "John", age: 30 }; - factory.create(item); - const items = factory.getAll(); - expect(items.length).toBe(1); - expect(items[0]).toEqual(item); - }); + test('should insert an item into the database using the factory', () => { + const item = { id: '1', name: 'John', age: 30 } + factory.create(item) + const items = factory.getAll() + expect(items.length).toBe(1) + expect(items[0]).toEqual(item) + }) - test("should read items from the database using the factory", () => { - const item = { id: "1", name: "Jane", age: 25 }; - mockDb.query("INSERT INTO test (id, name, age) VALUES (?, ?, ?)").run(item.id, item.name, item.age); - const items = factory.getAll(); - expect(items.length).toBe(1); - expect(items[0]).toEqual(item); - }); + test('should read items from the database using the factory', () => { + const item = { id: '1', name: 'Jane', age: 25 } + mockDb.query('INSERT INTO test (id, name, age) VALUES (?, ?, ?)').run(item.id, item.name, item.age) + const items = factory.getAll() + expect(items.length).toBe(1) + expect(items[0]).toEqual(item) + }) - test("should update an item in the database using the factory", () => { - const item = { id: "1", name: "Doe", age: 35 }; - mockDb.query("INSERT INTO test (id, name, age) VALUES (?, ?, ?)").run(item.id, item.name, item.age); - const updatedName = "John Doe"; - factory.update(item.id, { name: updatedName }); - const items = factory.getAll(); - expect(items.length).toBe(1); - expect(items[0]).toEqual({ ...item, name: updatedName }); - }); - test("should delete an item from the database using the factory", () => { - const item = { id: "1", name: "Alice", age: 40 }; - mockDb.query("INSERT INTO test (id, name, age) VALUES (?, ?, ?)").run(item.id, item.name, item.age); - factory.delById(item.id); - const items = factory.getAll(); - expect(items.length).toBe(0); - }); -}); + test('should update an item in the database using the factory', () => { + const item = { id: '1', name: 'Doe', age: 35 } + mockDb.query('INSERT INTO test (id, name, age) VALUES (?, ?, ?)').run(item.id, item.name, item.age) + const updatedName = 'John Doe' + factory.update(item.id, { name: updatedName }) + const items = factory.getAll() + expect(items.length).toBe(1) + expect(items[0]).toEqual({ ...item, name: updatedName }) + }) + test('should delete an item from the database using the factory', () => { + const item = { id: '1', name: 'Alice', age: 40 } + mockDb.query('INSERT INTO test (id, name, age) VALUES (?, ?, ?)').run(item.id, item.name, item.age) + factory.delById(item.id) + const items = factory.getAll() + expect(items.length).toBe(0) + }) +}) diff --git a/sqlite/sqlite-table-factory.ts b/sqlite/sqlite-table-factory.ts index 03fd036..47339d3 100644 --- a/sqlite/sqlite-table-factory.ts +++ b/sqlite/sqlite-table-factory.ts @@ -1,5 +1,5 @@ -import Database from "bun:sqlite"; -import { SQLInfer, SchemaMap } from "./sqlite-factory"; +import Database from 'bun:sqlite' +import { SQLInfer, SchemaMap } from './sqlite-factory' import { createItem, deleteItemById, @@ -8,50 +8,50 @@ import { readItems, readItemsWhere, updateItem, -} from "./sqlite-utils/crud-fn-utils"; -import { createTableQuery } from "./sqlite-utils/table-query-gen"; +} from './sqlite-utils/crud-fn-utils' +import { createTableQuery } from './sqlite-utils/table-query-gen' -export type ForeignKeysT = { column: keyof Schema; references: string }[] | null; +export type ForeignKeysT = { column: keyof Schema; references: string }[] | null export type SqliteTableFactoryParams = { - db: Database; - tableName: string; - schema: Schema; -}; + db: Database + tableName: string + schema: Schema +} export type SqliteTableOptions = { - debug?: boolean; - enableForeignKeys?: boolean; - foreignKeys?: ForeignKeysT; -}; + debug?: boolean + enableForeignKeys?: boolean + foreignKeys?: ForeignKeysT +} // Logger utility function logger(debug: boolean) { return (...args: any[]) => { if (debug) { - console.info(...args); + console.info(...args) } - }; + } } export function sqliteTableFactory< Schema extends SchemaMap, TranslatedSchema extends SQLInfer = SQLInfer, >(params: SqliteTableFactoryParams, options: SqliteTableOptions = {}) { - const { db, schema, tableName } = params; - const { debug = false } = options; + const { db, schema, tableName } = params + const { debug = false } = options - db.query(createTableQuery({ tableName, schema, debug })).run(); + db.query(createTableQuery({ tableName, schema, debug })).run() return { readWhere(where: Partial) { - return readItemsWhere({ db, tableName, debug, where }); + return readItemsWhere({ db, tableName, debug, where }) }, create( item: TranslatedSchema, createOptions?: { - returnInsertedItem?: ReturnInserted; - keyForInsertLookup?: keyof SQLInfer extends string ? keyof SQLInfer : never; + returnInsertedItem?: ReturnInserted + keyForInsertLookup?: keyof SQLInfer extends string ? keyof SQLInfer : never }, ) { return createItem({ @@ -61,13 +61,13 @@ export function sqliteTableFactory< item, returnInsertedItem: createOptions?.returnInsertedItem, keyForInsertLookup: createOptions?.keyForInsertLookup, - }); + }) }, getAll(): TranslatedSchema[] { - return readItems({ db, tableName, debug }) as TranslatedSchema[]; + return readItems({ db, tableName, debug }) as TranslatedSchema[] }, getById(id: string | number) { - return readItemById({ db, tableName, debug, id }); + return readItemById({ db, tableName, debug, id }) }, getByKey(key: string, value: string | number) { return readFirstItemByKey({ @@ -76,16 +76,16 @@ export function sqliteTableFactory< debug, key, value, - }) as unknown as TranslatedSchema; + }) as unknown as TranslatedSchema }, - update(id: string | number, item: Partial>) { - return updateItem({ db, tableName, debug, id, item }); + update(id: string | number, item: Partial>) { + return updateItem({ db, tableName, debug, id, item }) }, delById(id: number | string) { - return deleteItemById({ db, tableName, debug, id }); + return deleteItemById({ db, tableName, debug, id }) }, infer(): TranslatedSchema { - return undefined as any as TranslatedSchema; + return undefined as any as TranslatedSchema }, - }; + } } diff --git a/sqlite/sqlite-utils/crud-fn-utils.test.ts b/sqlite/sqlite-utils/crud-fn-utils.test.ts index 7f8e252..e90b41a 100644 --- a/sqlite/sqlite-utils/crud-fn-utils.test.ts +++ b/sqlite/sqlite-utils/crud-fn-utils.test.ts @@ -1,148 +1,148 @@ -import Database from "bun:sqlite"; -import { beforeEach, describe, expect, it, test } from "bun:test"; -import { SchemaMap } from "../sqlite-factory"; -import { createItem, createWhereClause, deleteItemById, readItems, updateItem } from "./crud-fn-utils"; +import Database from 'bun:sqlite' +import { beforeEach, describe, expect, it, test } from 'bun:test' +import { SchemaMap } from '../sqlite-factory' +import { createItem, createWhereClause, deleteItemById, readItems, updateItem } from './crud-fn-utils' const testSchema = { - id: { type: "TEXT" }, - name: { type: "TEXT" }, - age: { type: "INTEGER" }, -} satisfies SchemaMap; + id: { type: 'TEXT' }, + name: { type: 'TEXT' }, + age: { type: 'INTEGER' }, +} satisfies SchemaMap -let db = new Database(":memory:"); +let db = new Database(':memory:') -describe("Database utility functions", () => { +describe('Database utility functions', () => { beforeEach(() => { - db = new Database(":memory:"); - db.query("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").run(); - }); + db = new Database(':memory:') + db.query('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)').run() + }) - test("should create an item in the database", () => { + test('should create an item in the database', () => { createItem({ db, - tableName: "test", + tableName: 'test', debug: false, - item: { id: "1", name: "John", age: 25 }, + item: { id: '1', name: 'John', age: 25 }, returnInsertedItem: false, - }); - const items = readItems({ db, tableName: "test" }); - expect(items).toEqual([{ id: 1, name: "John", age: 25 }]); - }); - - test("should read items from the database", () => { - db.query("INSERT INTO test (name, age) VALUES (?, ?)").run("Jane", 30); - const items = readItems({ db, tableName: "test" }); - expect(items).toEqual([{ id: 1, name: "Jane", age: 30 }]); - }); - - test("should update an item in the database", () => { - db.query("INSERT INTO test (name, age) VALUES (?, ?)").run("Doe", 35); + }) + const items = readItems({ db, tableName: 'test' }) + expect(items).toEqual([{ id: 1, name: 'John', age: 25 }]) + }) + + test('should read items from the database', () => { + db.query('INSERT INTO test (name, age) VALUES (?, ?)').run('Jane', 30) + const items = readItems({ db, tableName: 'test' }) + expect(items).toEqual([{ id: 1, name: 'Jane', age: 30 }]) + }) + + test('should update an item in the database', () => { + db.query('INSERT INTO test (name, age) VALUES (?, ?)').run('Doe', 35) updateItem({ db, - tableName: "test", + tableName: 'test', debug: false, id: 1, - item: { name: "John Doe" }, - }); - const items = readItems({ db, tableName: "test" }); - expect(items).toEqual([{ id: 1, name: "John Doe", age: 35 }]); - }); - test("should delete an item from the database by ID", () => { - db.query("INSERT INTO test (name, age) VALUES (?, ?)").run("Alice", 40); - deleteItemById({ db, id: 1, tableName: "test" }); - const items = readItems({ db, tableName: "test" }); - expect(items).toEqual([]); - }); -}); - -describe("createWhereClause", () => { - it("should create a WHERE clause and parameters for a SQL query", () => { - const where = { id: 1, name: "John" }; - const result = createWhereClause(where); + item: { name: 'John Doe' }, + }) + const items = readItems({ db, tableName: 'test' }) + expect(items).toEqual([{ id: 1, name: 'John Doe', age: 35 }]) + }) + test('should delete an item from the database by ID', () => { + db.query('INSERT INTO test (name, age) VALUES (?, ?)').run('Alice', 40) + deleteItemById({ db, id: 1, tableName: 'test' }) + const items = readItems({ db, tableName: 'test' }) + expect(items).toEqual([]) + }) +}) + +describe('createWhereClause', () => { + it('should create a WHERE clause and parameters for a SQL query', () => { + const where = { id: 1, name: 'John' } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "id = ? AND name = ?", - parameters: [1, "John"], - }); - }); + whereClause: 'id = ? AND name = ?', + parameters: [1, 'John'], + }) + }) - it("should handle an empty object", () => { - const where = {}; - const result = createWhereClause(where); + it('should handle an empty object', () => { + const where = {} + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "", + whereClause: '', parameters: [], - }); - }); + }) + }) - it("should handle null and undefined values", () => { - const where = { id: null, name: undefined }; - const result = createWhereClause(where); + it('should handle null and undefined values', () => { + const where = { id: null, name: undefined } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "id = ? AND name = ?", + whereClause: 'id = ? AND name = ?', parameters: [null, undefined], - }); - }); + }) + }) - it("should handle more than two properties", () => { - const where = { id: 1, name: "John", age: 30 }; - const result = createWhereClause(where); + it('should handle more than two properties', () => { + const where = { id: 1, name: 'John', age: 30 } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "id = ? AND name = ? AND age = ?", - parameters: [1, "John", 30], - }); - }); + whereClause: 'id = ? AND name = ? AND age = ?', + parameters: [1, 'John', 30], + }) + }) - it("should handle boolean values", () => { - const where = { isActive: true }; - const result = createWhereClause(where); + it('should handle boolean values', () => { + const where = { isActive: true } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "isActive = ?", + whereClause: 'isActive = ?', parameters: [true], - }); - }); + }) + }) - it("should handle numeric string values", () => { - const where = { id: "1" }; - const result = createWhereClause(where); + it('should handle numeric string values', () => { + const where = { id: '1' } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "id = ?", - parameters: ["1"], - }); - }); + whereClause: 'id = ?', + parameters: ['1'], + }) + }) - it("should handle more than two properties", () => { - const where = { id: 1, name: "John", age: 30 }; - const result = createWhereClause(where); + it('should handle more than two properties', () => { + const where = { id: 1, name: 'John', age: 30 } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "id = ? AND name = ? AND age = ?", - parameters: [1, "John", 30], - }); - }); + whereClause: 'id = ? AND name = ? AND age = ?', + parameters: [1, 'John', 30], + }) + }) - it("should handle boolean values", () => { - const where = { isActive: true }; - const result = createWhereClause(where); + it('should handle boolean values', () => { + const where = { isActive: true } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "isActive = ?", + whereClause: 'isActive = ?', parameters: [true], - }); - }); + }) + }) - it("should handle numeric string values", () => { - const where = { id: "1" }; - const result = createWhereClause(where); + it('should handle numeric string values', () => { + const where = { id: '1' } + const result = createWhereClause(where) expect(result).toEqual({ - whereClause: "id = ?", - parameters: ["1"], - }); - }); -}); + whereClause: 'id = ?', + parameters: ['1'], + }) + }) +}) diff --git a/sqlite/sqlite-utils/crud-fn-utils.ts b/sqlite/sqlite-utils/crud-fn-utils.ts index 68e01c4..14399eb 100644 --- a/sqlite/sqlite-utils/crud-fn-utils.ts +++ b/sqlite/sqlite-utils/crud-fn-utils.ts @@ -1,231 +1,200 @@ -import Database from "bun:sqlite"; -import { SQLInfer, SchemaMap } from "../sqlite-factory"; -import { - deleteQueryString, - insertQueryString, - selectAllTableQueryString, - updateQueryString, -} from "./crud-string-utils"; +import Database from 'bun:sqlite' +import { SQLInfer, SchemaMap } from '../sqlite-factory' +import { deleteQueryString, insertQueryString, selectAllTableQueryString, updateQueryString } from './crud-string-utils' type BaseDBParams = { - db: Database; - tableName: string; - debug?: boolean; -}; + db: Database + tableName: string + debug?: boolean +} -type ParamsWithId = BaseDBParams & { id: string | number }; +type ParamsWithId = BaseDBParams & { id: string | number } -type DBItem = Partial>; +type DBItem = Partial> export function createItem({ - db, - debug, - item, - returnInsertedItem, - tableName, - keyForInsertLookup, -}: Omit & { - item: Partial>; - returnInsertedItem?: ReturnItem; - keyForInsertLookup?: keyof SQLInfer extends string - ? keyof SQLInfer - : never; + db, + debug, + item, + returnInsertedItem, + tableName, + keyForInsertLookup, +}: Omit & { + item: Partial> + returnInsertedItem?: ReturnItem + keyForInsertLookup?: keyof SQLInfer extends string ? keyof SQLInfer : never }): ReturnItem extends true ? SQLInfer : null { - const query = insertQueryString(tableName, item); - const valuesArray: any[] = []; - - for (const [key, value] of Object.entries(item)) { - const valueToInsert = value instanceof Date ? value.toISOString() : value; - - if (value !== undefined) { - valuesArray.push(valueToInsert); - } - } - - if (debug) console.table({ query, valuesArray }); - - try { - // Perform the insert operation - db.query(query).run(...valuesArray); - } catch (e) { - if (debug) { - throw { - info: "CreateItem: Error during database insert operation", - message: e.message, - query, - valuesArray, - }; - } - - throw e; - } - - const lookupKey = keyForInsertLookup ? keyForInsertLookup : "id"; - const lookupValue = item[lookupKey]; - - if (lookupValue && lookupKey && returnInsertedItem) { - const selectQuery = `SELECT * FROM ${tableName} WHERE ${lookupKey} = ?;`; - try { - const insertedItem = db - .prepare(selectQuery) - .get(lookupValue) as SQLInfer; - - if (debug) console.table({ selectQuery, lookupValue, insertedItem }); - - return insertedItem as ReturnItem extends true ? SQLInfer : null; - } catch (e) { - if (debug) { - throw { - info: "CreateItem: Error during database select operation", - message: e.message, - selectQuery, - lookupValue, - }; - } - throw e; - } - } - - if ((!lookupValue || !lookupKey) && returnInsertedItem) { - const errorMsg = `returnInsertedItem is true but no lookupKey or lookupValue was provided \n + const query = insertQueryString(tableName, item) + const valuesArray: any[] = [] + + for (const [key, value] of Object.entries(item)) { + const valueToInsert = value instanceof Date ? value.toISOString() : value + + if (value !== undefined) { + valuesArray.push(valueToInsert) + } + } + + if (debug) console.table({ query, valuesArray }) + + try { + // Perform the insert operation + db.query(query).run(...valuesArray) + } catch (e) { + if (debug) { + throw { + info: 'CreateItem: Error during database insert operation', + message: e.message, + query, + valuesArray, + } + } + + throw e + } + + const lookupKey = keyForInsertLookup ? keyForInsertLookup : 'id' + const lookupValue = item[lookupKey] + + if (lookupValue && lookupKey && returnInsertedItem) { + const selectQuery = `SELECT * FROM ${tableName} WHERE ${lookupKey} = ?;` + try { + const insertedItem = db.prepare(selectQuery).get(lookupValue) as SQLInfer + + if (debug) console.table({ selectQuery, lookupValue, insertedItem }) + + return insertedItem as ReturnItem extends true ? SQLInfer : null + } catch (e) { + if (debug) { + throw { + info: 'CreateItem: Error during database select operation', + message: e.message, + selectQuery, + lookupValue, + } + } + throw e + } + } + + if ((!lookupValue || !lookupKey) && returnInsertedItem) { + const errorMsg = `returnInsertedItem is true but no lookupKey or lookupValue was provided \n lookupKey: ${lookupKey} \n - lookupValue: ${lookupValue} \n`; - console.error(errorMsg); - throw new Error(errorMsg); - } + lookupValue: ${lookupValue} \n` + console.error(errorMsg) + throw new Error(errorMsg) + } - return null as ReturnItem extends true ? SQLInfer : null; + return null as ReturnItem extends true ? SQLInfer : null } export function readFirstItemByKey({ - db, - debug, - key, - tableName, - value, + db, + debug, + key, + tableName, + value, }: BaseDBParams & { - key: keyof SQLInfer; - value: string | number; + key: keyof SQLInfer + value: string | number }): SQLInfer { - const queryString = selectItemByKeyQueryString(tableName, String(key)); - if (debug) console.info(`readFirstItemByKey: ${queryString}`); - const query = db.prepare(queryString).get(value) as SQLInfer; - return query; + const queryString = selectItemByKeyQueryString(tableName, String(key)) + if (debug) console.info(`readFirstItemByKey: ${queryString}`) + const query = db.prepare(queryString).get(value) as SQLInfer + return query } // Modify the readItems function to include an optional id parameter. -export function readItemById({ - db, - debug, - id, - tableName, -}: ParamsWithId): SQLInfer { - const query = selectItemByKeyQueryString(tableName, "id"); - if (debug) console.info(`readItemById: ${query}`); - - const data = db.prepare(query).get(id) as SQLInfer; - - return data; +export function readItemById({ db, debug, id, tableName }: ParamsWithId): SQLInfer { + const query = selectItemByKeyQueryString(tableName, 'id') + if (debug) console.info(`readItemById: ${query}`) + + const data = db.prepare(query).get(id) as SQLInfer + + return data } // This type represents the shape of the 'where' parameter -type Where = Partial; +type Where = Partial // This interface will be used to type the return value of createWhereClause interface WhereClauseResult { - whereClause: string; - parameters: { [key: string]: any }; + whereClause: string + parameters: { [key: string]: any } } // Function to create a WHERE clause and parameters for a SQL query -export function createWhereClause>( - where: Where, - debug = false, -): WhereClauseResult { - const keys = Object.keys(where) as Array; - const whereClause = keys.map((key) => `${String(key)} = ?`).join(" AND "); - const parameters = keys.map((key) => where[key]); - - if (debug) { - // create object of keys/values to be used as parameters in the query - // e.g. { $name: 'John', $age: 25 } - const debugEntries = Object.fromEntries( - keys.map((key) => [`$${key as string}`, where[key]]), - ); - - console.table(debugEntries); - } - - return { whereClause, parameters }; +export function createWhereClause>(where: Where, debug = false): WhereClauseResult { + const keys = Object.keys(where) as Array + const whereClause = keys.map((key) => `${String(key)} = ?`).join(' AND ') + const parameters = keys.map((key) => where[key]) + + if (debug) { + // create object of keys/values to be used as parameters in the query + // e.g. { $name: 'John', $age: 25 } + const debugEntries = Object.fromEntries(keys.map((key) => [`$${key as string}`, where[key]])) + + console.table(debugEntries) + } + + return { whereClause, parameters } } export function readItemsWhere({ - db, - tableName, - debug, - where, + db, + tableName, + debug, + where, }: BaseDBParams & { - where: Where>; + where: Where> }): SQLInfer[] { - const { whereClause, parameters } = createWhereClause>( - where, - debug, - ); + const { whereClause, parameters } = createWhereClause>(where, debug) - // The query string now uses '?' placeholders for parameters - const queryString = `SELECT * FROM ${tableName} WHERE ${whereClause};`; - if (debug) console.info(`readItemsWhere ${queryString}`); + // The query string now uses '?' placeholders for parameters + const queryString = `SELECT * FROM ${tableName} WHERE ${whereClause};` + if (debug) console.info(`readItemsWhere ${queryString}`) - // Prepare the statement with the queryString - const statement = db.prepare(queryString); + // Prepare the statement with the queryString + const statement = db.prepare(queryString) - // Assuming the .all() method on the prepared statement executes the query - // and retrieves all the results after binding the parameters - const data = statement.all(parameters) as SQLInfer[]; + // Assuming the .all() method on the prepared statement executes the query + // and retrieves all the results after binding the parameters + const data = statement.all(parameters) as SQLInfer[] - return data; // Return the query results + return data // Return the query results } // In your crud-string-utils file, add a function to create a SQL query string to select by ID. -export function selectItemByKeyQueryString( - tableName: string, - key: string, -): string { - return `SELECT * FROM ${tableName} WHERE ${key} = ?`; +export function selectItemByKeyQueryString(tableName: string, key: string): string { + return `SELECT * FROM ${tableName} WHERE ${key} = ?` } -export function readItems({ - db, - debug, - tableName, -}: BaseDBParams): SQLInfer[] { - const query = selectAllTableQueryString(tableName); - if (debug) console.info(query); - const data = db.query(query).all() as SQLInfer[]; - return data; +export function readItems({ db, debug, tableName }: BaseDBParams): SQLInfer[] { + const query = selectAllTableQueryString(tableName) + if (debug) console.info(query) + const data = db.query(query).all() as SQLInfer[] + return data } export function updateItem({ - db, - debug, - id, - item, - tableName, + db, + debug, + id, + item, + tableName, }: ParamsWithId & { - item: Partial, "id">>; + item: Partial, 'id'>> }) { - const query = updateQueryString(tableName, item); + const query = updateQueryString(tableName, item) - if (debug) console.info(query); + if (debug) console.info(query) - const params = Object.fromEntries( - Object.entries(item).map(([key, value]) => [`$${key}`, value]), - ); - db.query(query).run({ ...params, $id: id }); + const params = Object.fromEntries(Object.entries(item).map(([key, value]) => [`$${key}`, value])) + db.query(query).run({ ...params, $id: id }) } export function deleteItemById({ db, debug, id, tableName }: ParamsWithId) { - const query = deleteQueryString(tableName); - if (debug) console.info("deleteQueryString: ", query); - db.query(query).run({ $id: id }); + const query = deleteQueryString(tableName) + if (debug) console.info('deleteQueryString: ', query) + db.query(query).run({ $id: id }) } diff --git a/sqlite/sqlite-utils/crud-string-utils.test.ts b/sqlite/sqlite-utils/crud-string-utils.test.ts index e6adb6b..e74d6f9 100644 --- a/sqlite/sqlite-utils/crud-string-utils.test.ts +++ b/sqlite/sqlite-utils/crud-string-utils.test.ts @@ -1,41 +1,36 @@ -import { expect, test } from "bun:test"; -import { - deleteQueryString, - insertQueryString, - selectAllTableQueryString, - updateQueryString, -} from "./crud-string-utils"; +import { expect, test } from 'bun:test' +import { deleteQueryString, insertQueryString, selectAllTableQueryString, updateQueryString } from './crud-string-utils' // Test for insertQueryString -test("insertQueryString", () => { - const tableName = "users"; - const item = { name: "Alice", age: 30 }; - const query = insertQueryString(tableName, item); - const expectedQuery = `INSERT INTO users (name, age) VALUES (?, ?)`; - expect(query).toBe(expectedQuery); -}); +test('insertQueryString', () => { + const tableName = 'users' + const item = { name: 'Alice', age: 30 } + const query = insertQueryString(tableName, item) + const expectedQuery = `INSERT INTO users (name, age) VALUES (?, ?)` + expect(query).toBe(expectedQuery) +}) // Test for selectAllTableQueryString -test("selectAllTableQueryString", () => { - const tableName = "users"; - const query = selectAllTableQueryString(tableName); - const expectedQuery = `SELECT * FROM users;`; - expect(query).toBe(expectedQuery); -}); +test('selectAllTableQueryString', () => { + const tableName = 'users' + const query = selectAllTableQueryString(tableName) + const expectedQuery = `SELECT * FROM users;` + expect(query).toBe(expectedQuery) +}) // Test for deleteQueryString -test("deleteQueryString", () => { - const tableName = "users"; - const query = deleteQueryString(tableName); - const expectedQuery = `delete FROM users WHERE id = $id;`; - expect(query).toBe(expectedQuery); -}); +test('deleteQueryString', () => { + const tableName = 'users' + const query = deleteQueryString(tableName) + const expectedQuery = `delete FROM users WHERE id = $id;` + expect(query).toBe(expectedQuery) +}) // Test for updateQueryString -test("updateQueryString", () => { - const tableName = "users"; - const item = { name: "John", age: 25 }; // example item - const query = updateQueryString(tableName, item); - const expectedQuery = `UPDATE users SET name = $name, age = $age WHERE id = $id;`; - expect(query).toBe(expectedQuery); -}); +test('updateQueryString', () => { + const tableName = 'users' + const item = { name: 'John', age: 25 } // example item + const query = updateQueryString(tableName, item) + const expectedQuery = `UPDATE users SET name = $name, age = $age WHERE id = $id;` + expect(query).toBe(expectedQuery) +}) diff --git a/sqlite/sqlite-utils/crud-string-utils.ts b/sqlite/sqlite-utils/crud-string-utils.ts index 8c3d47e..24d5431 100644 --- a/sqlite/sqlite-utils/crud-string-utils.ts +++ b/sqlite/sqlite-utils/crud-string-utils.ts @@ -1,52 +1,52 @@ export function insertQueryString>(tableName: string, item: Item): string { // Define a whitelist for table names if they are dynamic or ensure tableName is sanitized. - const safeTableName = escapeIdentifier(tableName); + const safeTableName = escapeIdentifier(tableName) - const definedKeys = Object.keys(item).filter((key) => item[key] !== undefined); + const definedKeys = Object.keys(item).filter((key) => item[key] !== undefined) // Map the defined keys to column names and placeholders - const columns = definedKeys.map((column) => escapeIdentifier(column)).join(", "); - const placeholders = definedKeys.map(() => "?").join(", "); + const columns = definedKeys.map((column) => escapeIdentifier(column)).join(', ') + const placeholders = definedKeys.map(() => '?').join(', ') // Handle the case where the item might be empty. if (columns.length === 0 || placeholders.length === 0) { - throw new Error("No data provided for insert."); + throw new Error('No data provided for insert.') } - return `INSERT INTO ${safeTableName} (${columns}) VALUES (${placeholders})`; + return `INSERT INTO ${safeTableName} (${columns}) VALUES (${placeholders})` } function escapeIdentifier(identifier: string): string { // This is a simplistic approach and might not cover all edge cases. if (!identifier.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { - throw new Error("Invalid identifier"); + throw new Error('Invalid identifier') } - return identifier; // Assuming the identifier is safe, otherwise escape it properly. + return identifier // Assuming the identifier is safe, otherwise escape it properly. } export function selectAllTableQueryString(tableName: string): string { // Validate or escape the tableName to prevent SQL injection - const safeTableName = escapeIdentifier(tableName); - return `SELECT * FROM ${safeTableName};`; + const safeTableName = escapeIdentifier(tableName) + return `SELECT * FROM ${safeTableName};` } export function deleteQueryString(tableName: string): string { // Validate or escape the tableName to prevent SQL injection - const safeTableName = escapeIdentifier(tableName); - return `delete FROM ${safeTableName} WHERE id = $id;`; + const safeTableName = escapeIdentifier(tableName) + return `delete FROM ${safeTableName} WHERE id = $id;` } export function updateQueryString(tableName: string, item: Record): string { // Validate or escape the tableName to prevent SQL injection - const safeTableName = escapeIdentifier(tableName); + const safeTableName = escapeIdentifier(tableName) const updateFields = Object.keys(item) .map((key) => `${escapeIdentifier(key)} = $${key}`) - .join(", "); + .join(', ') // Check if the updateFields string is empty and throw an error if it is if (updateFields.length === 0) { - throw new Error("No fields to update were provided."); + throw new Error('No fields to update were provided.') } - return `UPDATE ${safeTableName} SET ${updateFields} WHERE id = $id;`; + return `UPDATE ${safeTableName} SET ${updateFields} WHERE id = $id;` } diff --git a/sqlite/sqlite-utils/format-schema.test.ts b/sqlite/sqlite-utils/format-schema.test.ts index a3450ca..e7e2413 100644 --- a/sqlite/sqlite-utils/format-schema.test.ts +++ b/sqlite/sqlite-utils/format-schema.test.ts @@ -1,16 +1,16 @@ -import { expect, test } from "bun:test"; -import { SchemaMap } from "../sqlite-factory"; -import { formatSchema } from "./format-schema"; +import { expect, test } from 'bun:test' +import { SchemaMap } from '../sqlite-factory' +import { formatSchema } from './format-schema' -test("formatSchema formats schema correctly", () => { +test('formatSchema formats schema correctly', () => { const schema = { - id: { type: "INTEGER" }, - name: { type: "TEXT" }, - } satisfies SchemaMap; + id: { type: 'INTEGER' }, + name: { type: 'TEXT' }, + } satisfies SchemaMap // TODO need to fix schema type - const result = formatSchema(schema); + const result = formatSchema(schema) - expect(result[0]).toBe("id INTEGER"); - expect(result[1]).toBe("name TEXT"); -}); + expect(result[0]).toBe('id INTEGER') + expect(result[1]).toBe('name TEXT') +}) diff --git a/sqlite/sqlite-utils/format-schema.ts b/sqlite/sqlite-utils/format-schema.ts index cd0ebbd..f869485 100644 --- a/sqlite/sqlite-utils/format-schema.ts +++ b/sqlite/sqlite-utils/format-schema.ts @@ -1,5 +1,5 @@ -import { SchemaMap } from "../sqlite-factory"; +import { SchemaMap } from '../sqlite-factory' export function formatSchema(schema: Schema): string[] { - return Object.entries(schema).map(([key, fieldDefinition]) => `${key} ${fieldDefinition?.type.toUpperCase()}`); + return Object.entries(schema).map(([key, fieldDefinition]) => `${key} ${fieldDefinition?.type.toUpperCase()}`) } diff --git a/sqlite/sqlite-utils/table-query-gen.test.ts b/sqlite/sqlite-utils/table-query-gen.test.ts index eab5f7c..bcec05f 100644 --- a/sqlite/sqlite-utils/table-query-gen.test.ts +++ b/sqlite/sqlite-utils/table-query-gen.test.ts @@ -1,115 +1,115 @@ -import { describe, expect, it, test } from "bun:test"; -import { FieldDef, SchemaMap } from "../sqlite-factory"; +import { describe, expect, it, test } from 'bun:test' +import { FieldDef, SchemaMap } from '../sqlite-factory' import { assembleCreateTableQuery, createColumnDefinition, createTableLevelConstraint, createTableQuery, -} from "./table-query-gen"; +} from './table-query-gen' -test("createTableQuery constructs SQL query correctly with foreign keys", () => { +test('createTableQuery constructs SQL query correctly with foreign keys', () => { const schema = { - id: { type: "INTEGER" }, - name: { type: "TEXT" }, - fkTest: { type: "TEXT", foreignKey: "other_table(id)" }, - } satisfies SchemaMap; + id: { type: 'INTEGER' }, + name: { type: 'TEXT' }, + fkTest: { type: 'TEXT', foreignKey: 'other_table(id)' }, + } satisfies SchemaMap const result = createTableQuery({ schema, - tableName: "test_table", - }); + tableName: 'test_table', + }) expect(result).toBe( - "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT, `fkTest` TEXT, FOREIGN KEY (`fkTest`) REFERENCES other_table(id));", - ); -}); + 'CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT, `fkTest` TEXT, FOREIGN KEY (`fkTest`) REFERENCES other_table(id));', + ) +}) -test("createTableQuery constructs SQL query correctly without foreign keys", () => { +test('createTableQuery constructs SQL query correctly without foreign keys', () => { const schema = { - id: { type: "INTEGER" }, - name: { type: "TEXT" }, - } satisfies SchemaMap; + id: { type: 'INTEGER' }, + name: { type: 'TEXT' }, + } satisfies SchemaMap const result = createTableQuery({ schema, - tableName: "test_table", - }); + tableName: 'test_table', + }) - expect(result).toBe("CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT);"); -}); + expect(result).toBe('CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT);') +}) -describe("createColumnDefinition", () => { - it("should generate a column definition for a simple TEXT field", () => { - const definition: FieldDef = { type: "TEXT" }; - const result = createColumnDefinition("name", definition); - expect(result).toBe("`name` TEXT"); - }); +describe('createColumnDefinition', () => { + it('should generate a column definition for a simple TEXT field', () => { + const definition: FieldDef = { type: 'TEXT' } + const result = createColumnDefinition('name', definition) + expect(result).toBe('`name` TEXT') + }) - it("should generate a column definition for an INTEGER field with a PRIMARY KEY", () => { - const definition: FieldDef = { type: "INTEGER", primaryKey: true }; - const result = createColumnDefinition("id", definition); - expect(result).toBe("`id` INTEGER PRIMARY KEY"); - }); + it('should generate a column definition for an INTEGER field with a PRIMARY KEY', () => { + const definition: FieldDef = { type: 'INTEGER', primaryKey: true } + const result = createColumnDefinition('id', definition) + expect(result).toBe('`id` INTEGER PRIMARY KEY') + }) // Add tests for unique, notNull, and defaultValue... - it("should throw an error if the definition is not provided", () => { + it('should throw an error if the definition is not provided', () => { expect(() => { - createColumnDefinition("age", undefined as unknown as FieldDef); - }).toThrow(); - }); - - it("should generate a column definition with a UNIQUE constraint", () => { - const definition: FieldDef = { type: "TEXT", unique: true }; - const result = createColumnDefinition("username", definition); - expect(result).toBe("`username` TEXT UNIQUE"); - }); - - it("should generate a column definition with a NOT NULL constraint", () => { - const definition: FieldDef = { type: "INTEGER", required: true }; - const result = createColumnDefinition("age", definition); - expect(result).toBe("`age` INTEGER NOT NULL"); - }); - - it("should generate a column definition with a DEFAULT value", () => { - const definition: FieldDef = { type: "TEXT", defaultValue: "N/A" }; - const result = createColumnDefinition("status", definition); - expect(result).toBe("`status` TEXT DEFAULT N/A"); - }); - - it("should correctly quote a DEFAULT string value", () => { + createColumnDefinition('age', undefined as unknown as FieldDef) + }).toThrow() + }) + + it('should generate a column definition with a UNIQUE constraint', () => { + const definition: FieldDef = { type: 'TEXT', unique: true } + const result = createColumnDefinition('username', definition) + expect(result).toBe('`username` TEXT UNIQUE') + }) + + it('should generate a column definition with a NOT NULL constraint', () => { + const definition: FieldDef = { type: 'INTEGER', required: true } + const result = createColumnDefinition('age', definition) + expect(result).toBe('`age` INTEGER NOT NULL') + }) + + it('should generate a column definition with a DEFAULT value', () => { + const definition: FieldDef = { type: 'TEXT', defaultValue: 'N/A' } + const result = createColumnDefinition('status', definition) + expect(result).toBe('`status` TEXT DEFAULT N/A') + }) + + it('should correctly quote a DEFAULT string value', () => { const definition: FieldDef = { - type: "TEXT", + type: 'TEXT', defaultValue: "'active'", - }; - const result = createColumnDefinition("state", definition); - expect(result).toBe("`state` TEXT DEFAULT 'active'"); - }); + } + const result = createColumnDefinition('state', definition) + expect(result).toBe("`state` TEXT DEFAULT 'active'") + }) - it("should generate a column definition with multiple constraints", () => { + it('should generate a column definition with multiple constraints', () => { const definition: FieldDef = { - type: "INTEGER", + type: 'INTEGER', required: true, unique: true, defaultValue: 0, - }; - const result = createColumnDefinition("count", definition); - expect(result).toContain("`count` INTEGER"); - expect(result).toContain("NOT NULL"); - expect(result).toContain("DEFAULT 0"); - }); - - it("should not include DEFAULT when defaultValue is not provided", () => { - const definition: FieldDef = { type: "REAL" }; - const result = createColumnDefinition("price", definition); - expect(result).toBe("`price` REAL"); - }); - - it("should handle numeric DEFAULT values correctly", () => { - const definition: FieldDef = { type: "INTEGER", defaultValue: 10 }; - const result = createColumnDefinition("quantity", definition); - expect(result).toBe("`quantity` INTEGER DEFAULT 10"); - }); + } + const result = createColumnDefinition('count', definition) + expect(result).toContain('`count` INTEGER') + expect(result).toContain('NOT NULL') + expect(result).toContain('DEFAULT 0') + }) + + it('should not include DEFAULT when defaultValue is not provided', () => { + const definition: FieldDef = { type: 'REAL' } + const result = createColumnDefinition('price', definition) + expect(result).toBe('`price` REAL') + }) + + it('should handle numeric DEFAULT values correctly', () => { + const definition: FieldDef = { type: 'INTEGER', defaultValue: 10 } + const result = createColumnDefinition('quantity', definition) + expect(result).toBe('`quantity` INTEGER DEFAULT 10') + }) // Test for an edge case where type is unknown // it("should throw an error if an invalid type is provided", () => { @@ -120,170 +120,170 @@ describe("createColumnDefinition", () => { // }); // Test for proper escaping of field names that are SQL keywords - it("should escape field names that are SQL keywords", () => { - const definition: FieldDef = { type: "TEXT" }; - const result = createColumnDefinition("group", definition); - expect(result).toBe("`group` TEXT"); - }); -}); - -describe("createTableLevelConstraint", () => { - it("should generate a foreign key constraint", () => { + it('should escape field names that are SQL keywords', () => { + const definition: FieldDef = { type: 'TEXT' } + const result = createColumnDefinition('group', definition) + expect(result).toBe('`group` TEXT') + }) +}) + +describe('createTableLevelConstraint', () => { + it('should generate a foreign key constraint', () => { const definition: FieldDef = { - type: "INTEGER", - foreignKey: "other_table(id)", - }; - const result = createTableLevelConstraint("fkTest", definition); - expect(result).toBe("FOREIGN KEY (`fkTest`) REFERENCES other_table(id)"); - }); - - it("should return null if no foreign key is defined", () => { - const definition: FieldDef = { type: "INTEGER" }; - const result = createTableLevelConstraint("fkTest", definition); - expect(result).toBeNull(); - }); - - it("should generate a foreign key constraint with a custom reference field", () => { + type: 'INTEGER', + foreignKey: 'other_table(id)', + } + const result = createTableLevelConstraint('fkTest', definition) + expect(result).toBe('FOREIGN KEY (`fkTest`) REFERENCES other_table(id)') + }) + + it('should return null if no foreign key is defined', () => { + const definition: FieldDef = { type: 'INTEGER' } + const result = createTableLevelConstraint('fkTest', definition) + expect(result).toBeNull() + }) + + it('should generate a foreign key constraint with a custom reference field', () => { const definition: FieldDef = { - type: "INTEGER", - foreignKey: "other_table(custom_id)", - }; - const result = createTableLevelConstraint("fkCustomId", definition); - expect(result).toBe("FOREIGN KEY (`fkCustomId`) REFERENCES other_table(custom_id)"); - }); - - it("should properly trim the foreign key definition", () => { + type: 'INTEGER', + foreignKey: 'other_table(custom_id)', + } + const result = createTableLevelConstraint('fkCustomId', definition) + expect(result).toBe('FOREIGN KEY (`fkCustomId`) REFERENCES other_table(custom_id)') + }) + + it('should properly trim the foreign key definition', () => { const definition: FieldDef = { - type: "INTEGER", - foreignKey: " other_table (custom_id) ", - }; - const result = createTableLevelConstraint("fkCustomId", definition); - expect(result).toBe("FOREIGN KEY (`fkCustomId`) REFERENCES other_table(custom_id)"); - }); - - it("should handle foreign keys that include spaces or special characters", () => { + type: 'INTEGER', + foreignKey: ' other_table (custom_id) ', + } + const result = createTableLevelConstraint('fkCustomId', definition) + expect(result).toBe('FOREIGN KEY (`fkCustomId`) REFERENCES other_table(custom_id)') + }) + + it('should handle foreign keys that include spaces or special characters', () => { const definition: FieldDef = { - type: "TEXT", - foreignKey: "`other table`(`special id`)", - }; - const result = createTableLevelConstraint("fkSpecial", definition); - expect(result).toBe("FOREIGN KEY (`fkSpecial`) REFERENCES `other table`(`special id`)"); - }); - - it("should return null for a malformed foreign key definition", () => { + type: 'TEXT', + foreignKey: '`other table`(`special id`)', + } + const result = createTableLevelConstraint('fkSpecial', definition) + expect(result).toBe('FOREIGN KEY (`fkSpecial`) REFERENCES `other table`(`special id`)') + }) + + it('should return null for a malformed foreign key definition', () => { const definition: FieldDef = { - type: "INTEGER", - foreignKey: "malformed", - }; - expect(() => createTableLevelConstraint("fkMalformed", definition)).toThrow(); - }); + type: 'INTEGER', + foreignKey: 'malformed', + } + expect(() => createTableLevelConstraint('fkMalformed', definition)).toThrow() + }) // Test for a case where the foreign key reference does not include a field - it("should return null if foreign key reference is incomplete", () => { + it('should return null if foreign key reference is incomplete', () => { const definition: FieldDef = { - type: "INTEGER", - foreignKey: "other_table", - }; - expect(() => createTableLevelConstraint("fkIncomplete", definition)).toThrow(); - }); + type: 'INTEGER', + foreignKey: 'other_table', + } + expect(() => createTableLevelConstraint('fkIncomplete', definition)).toThrow() + }) // Test for proper escaping of table and column names in foreign key definitions - it("should escape table and column names in foreign key definitions", () => { + it('should escape table and column names in foreign key definitions', () => { const definition: FieldDef = { - type: "INTEGER", - foreignKey: "`other-table`(`id`)", - }; - const result = createTableLevelConstraint("fkEscaped", definition); - expect(result).toBe("FOREIGN KEY (`fkEscaped`) REFERENCES `other-table`(`id`)"); - }); -}); - -describe("assembleCreateTableQuery", () => { - it("should assemble a create table query with a single column", () => { - const columns = ["`id` INTEGER PRIMARY KEY"]; - const result = assembleCreateTableQuery("test_table", columns, []); - expect(result).toBe("CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER PRIMARY KEY);"); - }); - - it("should assemble a create table query with foreign key constraints", () => { - const columns = ["`id` INTEGER", "`name` TEXT"]; - const constraints = ["FOREIGN KEY (`id`) REFERENCES other_table(id)"]; - const result = assembleCreateTableQuery("test_table", columns, constraints); + type: 'INTEGER', + foreignKey: '`other-table`(`id`)', + } + const result = createTableLevelConstraint('fkEscaped', definition) + expect(result).toBe('FOREIGN KEY (`fkEscaped`) REFERENCES `other-table`(`id`)') + }) +}) + +describe('assembleCreateTableQuery', () => { + it('should assemble a create table query with a single column', () => { + const columns = ['`id` INTEGER PRIMARY KEY'] + const result = assembleCreateTableQuery('test_table', columns, []) + expect(result).toBe('CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER PRIMARY KEY);') + }) + + it('should assemble a create table query with foreign key constraints', () => { + const columns = ['`id` INTEGER', '`name` TEXT'] + const constraints = ['FOREIGN KEY (`id`) REFERENCES other_table(id)'] + const result = assembleCreateTableQuery('test_table', columns, constraints) expect(result).toBe( - "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT, FOREIGN KEY (`id`) REFERENCES other_table(id));", - ); - }); + 'CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT, FOREIGN KEY (`id`) REFERENCES other_table(id));', + ) + }) - it("should handle tables with multiple foreign key constraints", () => { - const columns = ["`id` INTEGER", "`parent_id` INTEGER", "`owner_id` INTEGER"]; + it('should handle tables with multiple foreign key constraints', () => { + const columns = ['`id` INTEGER', '`parent_id` INTEGER', '`owner_id` INTEGER'] const constraints = [ - "FOREIGN KEY (`parent_id`) REFERENCES parents(id)", - "FOREIGN KEY (`owner_id`) REFERENCES owners(id)", - ]; - const result = assembleCreateTableQuery("test_table", columns, constraints); + 'FOREIGN KEY (`parent_id`) REFERENCES parents(id)', + 'FOREIGN KEY (`owner_id`) REFERENCES owners(id)', + ] + const result = assembleCreateTableQuery('test_table', columns, constraints) expect(result).toBe( - "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `parent_id` INTEGER, `owner_id` INTEGER, FOREIGN KEY (`parent_id`) REFERENCES parents(id), FOREIGN KEY (`owner_id`) REFERENCES owners(id));", - ); - }); - - it("should include unique constraints at the table level", () => { - const columns = ["`id` INTEGER", "`email` TEXT"]; - const constraints = ["UNIQUE (`email`)"]; - const result = assembleCreateTableQuery("users", columns, constraints); - expect(result).toBe("CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER, `email` TEXT, UNIQUE (`email`));"); - }); - - it("should return a query without any constraints if none are provided", () => { - const columns = ["`id` INTEGER", "`name` TEXT"]; - const result = assembleCreateTableQuery("simple_table", columns, []); - expect(result).toBe("CREATE TABLE IF NOT EXISTS `simple_table` (`id` INTEGER, `name` TEXT);"); - }); - - it("should escape table names that are SQL keywords", () => { - const columns = ["`id` INTEGER"]; - const result = assembleCreateTableQuery("group", columns, []); - expect(result).toBe("CREATE TABLE IF NOT EXISTS `group` (`id` INTEGER);"); - }); - - it("should throw an error if columns are empty", () => { + 'CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `parent_id` INTEGER, `owner_id` INTEGER, FOREIGN KEY (`parent_id`) REFERENCES parents(id), FOREIGN KEY (`owner_id`) REFERENCES owners(id));', + ) + }) + + it('should include unique constraints at the table level', () => { + const columns = ['`id` INTEGER', '`email` TEXT'] + const constraints = ['UNIQUE (`email`)'] + const result = assembleCreateTableQuery('users', columns, constraints) + expect(result).toBe('CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER, `email` TEXT, UNIQUE (`email`));') + }) + + it('should return a query without any constraints if none are provided', () => { + const columns = ['`id` INTEGER', '`name` TEXT'] + const result = assembleCreateTableQuery('simple_table', columns, []) + expect(result).toBe('CREATE TABLE IF NOT EXISTS `simple_table` (`id` INTEGER, `name` TEXT);') + }) + + it('should escape table names that are SQL keywords', () => { + const columns = ['`id` INTEGER'] + const result = assembleCreateTableQuery('group', columns, []) + expect(result).toBe('CREATE TABLE IF NOT EXISTS `group` (`id` INTEGER);') + }) + + it('should throw an error if columns are empty', () => { expect(() => { - assembleCreateTableQuery("empty_table", [], []); - }).toThrow(); - }); + assembleCreateTableQuery('empty_table', [], []) + }).toThrow() + }) - it("should properly format a table with default values", () => { - const columns = ["`id` INTEGER", "`name` TEXT DEFAULT 'Unknown'"]; - const result = assembleCreateTableQuery("default_values_table", columns, []); + it('should properly format a table with default values', () => { + const columns = ['`id` INTEGER', "`name` TEXT DEFAULT 'Unknown'"] + const result = assembleCreateTableQuery('default_values_table', columns, []) expect(result).toBe( "CREATE TABLE IF NOT EXISTS `default_values_table` (`id` INTEGER, `name` TEXT DEFAULT 'Unknown');", - ); - }); - - it("should assemble a create table query with check constraints", () => { - const columns = ["`age` INTEGER"]; - const constraints = ["CHECK (`age` >= 18)"]; - const result = assembleCreateTableQuery("check_constraints_table", columns, constraints); - expect(result).toBe("CREATE TABLE IF NOT EXISTS `check_constraints_table` (`age` INTEGER, CHECK (`age` >= 18));"); - }); - - it("should handle a table with composite primary keys", () => { - const columns = ["`id` INTEGER", "`revision` INTEGER"]; - const constraints = ["PRIMARY KEY (`id`, `revision`)"]; - const result = assembleCreateTableQuery("composite_keys_table", columns, constraints); + ) + }) + + it('should assemble a create table query with check constraints', () => { + const columns = ['`age` INTEGER'] + const constraints = ['CHECK (`age` >= 18)'] + const result = assembleCreateTableQuery('check_constraints_table', columns, constraints) + expect(result).toBe('CREATE TABLE IF NOT EXISTS `check_constraints_table` (`age` INTEGER, CHECK (`age` >= 18));') + }) + + it('should handle a table with composite primary keys', () => { + const columns = ['`id` INTEGER', '`revision` INTEGER'] + const constraints = ['PRIMARY KEY (`id`, `revision`)'] + const result = assembleCreateTableQuery('composite_keys_table', columns, constraints) expect(result).toBe( - "CREATE TABLE IF NOT EXISTS `composite_keys_table` (`id` INTEGER, `revision` INTEGER, PRIMARY KEY (`id`, `revision`));", - ); - }); + 'CREATE TABLE IF NOT EXISTS `composite_keys_table` (`id` INTEGER, `revision` INTEGER, PRIMARY KEY (`id`, `revision`));', + ) + }) // Test to ensure that backticks are used consistently - it("should use backticks for all identifiers", () => { - const columns = ["`id` INTEGER", "`name` TEXT"]; - const constraints = ["FOREIGN KEY (`id`) REFERENCES `other_table`(`id`)"]; - const result = assembleCreateTableQuery("backtick_test", columns, constraints); + it('should use backticks for all identifiers', () => { + const columns = ['`id` INTEGER', '`name` TEXT'] + const constraints = ['FOREIGN KEY (`id`) REFERENCES `other_table`(`id`)'] + const result = assembleCreateTableQuery('backtick_test', columns, constraints) expect(result).toBe( - "CREATE TABLE IF NOT EXISTS `backtick_test` (`id` INTEGER, `name` TEXT, FOREIGN KEY (`id`) REFERENCES `other_table`(`id`));", - ); - }); + 'CREATE TABLE IF NOT EXISTS `backtick_test` (`id` INTEGER, `name` TEXT, FOREIGN KEY (`id`) REFERENCES `other_table`(`id`));', + ) + }) // Add more tests for complex tables, edge cases, etc... -}); +}) diff --git a/sqlite/sqlite-utils/table-query-gen.ts b/sqlite/sqlite-utils/table-query-gen.ts index b21a118..ba7597a 100644 --- a/sqlite/sqlite-utils/table-query-gen.ts +++ b/sqlite/sqlite-utils/table-query-gen.ts @@ -1,39 +1,39 @@ -import { FieldDef, SchemaMap } from "../sqlite-factory"; +import { FieldDef, SchemaMap } from '../sqlite-factory' export function assembleCreateTableQuery( tableName: string, columns: string[], tableLevelConstraints: string[], ): string { - if (columns.length === 0) throw new Error(`No columns for table ${tableName}`); - const tableDefinition = [...columns, ...tableLevelConstraints].filter(Boolean).join(", "); - return `CREATE TABLE IF NOT EXISTS \`${tableName}\` (${tableDefinition});`; + if (columns.length === 0) throw new Error(`No columns for table ${tableName}`) + const tableDefinition = [...columns, ...tableLevelConstraints].filter(Boolean).join(', ') + return `CREATE TABLE IF NOT EXISTS \`${tableName}\` (${tableDefinition});` } export function createTableLevelConstraint(fieldName: string, definition: FieldDef): string | null { if (definition.foreignKey) { - const [referencedTable, referencedField]: string[] = definition.foreignKey.split("("); + const [referencedTable, referencedField]: string[] = definition.foreignKey.split('(') - if (!referencedField) throw new Error(`No referenced field for foreign key ${definition.foreignKey}`); - if (!referencedTable) throw new Error(`No referenced table for foreign key ${definition.foreignKey}`); - return `FOREIGN KEY (\`${fieldName}\`) REFERENCES ${referencedTable.trim()}(${referencedField}`.trim(); + if (!referencedField) throw new Error(`No referenced field for foreign key ${definition.foreignKey}`) + if (!referencedTable) throw new Error(`No referenced table for foreign key ${definition.foreignKey}`) + return `FOREIGN KEY (\`${fieldName}\`) REFERENCES ${referencedTable.trim()}(${referencedField}`.trim() } - return null; + return null } export function createColumnDefinition(fieldName: string, definition: FieldDef): string { - if (!definition) throw new Error(`No definition for field ${fieldName}`); - const type = definition.type; - const constraints = []; + if (!definition) throw new Error(`No definition for field ${fieldName}`) + const type = definition.type + const constraints = [] - if (definition.primaryKey) constraints.push("PRIMARY KEY"); - if (definition.unique) constraints.push("UNIQUE"); - if (definition.required) constraints.push("NOT NULL"); + if (definition.primaryKey) constraints.push('PRIMARY KEY') + if (definition.unique) constraints.push('UNIQUE') + if (definition.required) constraints.push('NOT NULL') if (definition.defaultValue !== undefined) { - constraints.push(`DEFAULT ${definition.defaultValue}`); + constraints.push(`DEFAULT ${definition.defaultValue}`) } - return `\`${fieldName}\` ${type} ${constraints.join(" ")}`.trim(); + return `\`${fieldName}\` ${type} ${constraints.join(' ')}`.trim() } export function createTableQuery({ @@ -41,28 +41,28 @@ export function createTableQuery({ tableName, debug = false, }: { - tableName: string; - schema: Schema; - debug?: boolean; + tableName: string + schema: Schema + debug?: boolean }): string { if (debug) { - console.info({ schema, tableName }); + console.info({ schema, tableName }) } - if (!schema) throw new Error(`No schema provided for table ${tableName}`); + if (!schema) throw new Error(`No schema provided for table ${tableName}`) const columns = Object.keys(schema).map((fieldName) => { - return createColumnDefinition(fieldName, schema[fieldName] as FieldDef); - }); + return createColumnDefinition(fieldName, schema[fieldName] as FieldDef) + }) const tableLevelConstraints = Object.keys(schema) - .map((fieldName) => createTableLevelConstraint(fieldName, schema[fieldName] as FieldDef) || "") - .filter(Boolean); + .map((fieldName) => createTableLevelConstraint(fieldName, schema[fieldName] as FieldDef) || '') + .filter(Boolean) - const query = assembleCreateTableQuery(tableName, columns, tableLevelConstraints); + const query = assembleCreateTableQuery(tableName, columns, tableLevelConstraints) if (debug) { - console.info({ query, schema, tableName }); + console.info({ query, schema, tableName }) } - return query; + return query } diff --git a/state/create-state-dispatchers.test.ts b/state/create-state-dispatchers.test.ts index 43584d0..4006062 100644 --- a/state/create-state-dispatchers.test.ts +++ b/state/create-state-dispatchers.test.ts @@ -1,95 +1,95 @@ -import { beforeEach, describe, expect, it, jest } from "bun:test"; -import { createStateDispatchers } from "./create-state-dispatchers"; +import { beforeEach, describe, expect, it, jest } from 'bun:test' +import { createStateDispatchers } from './create-state-dispatchers' -describe("createStateDispatchers", () => { - let state: any; - let dispatchers: any; - let updateFunction: jest.Mock = jest.fn(); +describe('createStateDispatchers', () => { + let state: any + let dispatchers: any + let updateFunction: jest.Mock = jest.fn() const updateAndGetLastCall = () => { - const calls = updateFunction.mock.calls; - return calls[calls.length - 1]; - }; + const calls = updateFunction.mock.calls + return calls[calls.length - 1] + } beforeEach(async () => { state = { - name: "John Doe", + name: 'John Doe', age: 25, - emails: ["john@example.com", "doe@example.com"], + emails: ['john@example.com', 'doe@example.com'], active: true, address: { - street: "Main street", - city: "New York", + street: 'Main street', + city: 'New York', }, - }; + } - updateFunction = jest.fn(); + updateFunction = jest.fn() dispatchers = createStateDispatchers({ state, defaultState: state, updateFunction, - }); - }); + }) + }) - it("should set string value correctly", () => { - dispatchers.name.set("Jane Doe"); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("name"); - expect(lastValue).toBe("Jane Doe"); - }); + it('should set string value correctly', () => { + dispatchers.name.set('Jane Doe') + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('name') + expect(lastValue).toBe('Jane Doe') + }) - it("should handle numbers correctly", () => { - (dispatchers.age as { increment: Function }).increment(5); - let [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("age"); - expect(lastValue).toBe(30); + it('should handle numbers correctly', () => { + ;(dispatchers.age as { increment: Function }).increment(5) + let [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('age') + expect(lastValue).toBe(30) - (dispatchers.age as { decrement: Function }).decrement(10); - [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("age"); - expect(lastValue).toBe(15); - }); + ;(dispatchers.age as { decrement: Function }).decrement(10) + ;[lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('age') + expect(lastValue).toBe(15) + }) - it("should set boolean value correctly", () => { - dispatchers.active.set(false); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("active"); - expect(lastValue).toBe(false); - }); + it('should set boolean value correctly', () => { + dispatchers.active.set(false) + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('active') + expect(lastValue).toBe(false) + }) - it("should update object values correctly", () => { - (dispatchers.address as { update: Function }).update({ - city: "Los Angeles", - }); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("address"); - expect(lastValue).toEqual({ street: "Main street", city: "Los Angeles" }); - }); - it("should handle array push", () => { - (dispatchers.emails as { push: Function }).push("another@example.com"); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("emails"); - expect(lastValue).toEqual(["john@example.com", "doe@example.com", "another@example.com"]); - }); + it('should update object values correctly', () => { + ;(dispatchers.address as { update: Function }).update({ + city: 'Los Angeles', + }) + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('address') + expect(lastValue).toEqual({ street: 'Main street', city: 'Los Angeles' }) + }) + it('should handle array push', () => { + ;(dispatchers.emails as { push: Function }).push('another@example.com') + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('emails') + expect(lastValue).toEqual(['john@example.com', 'doe@example.com', 'another@example.com']) + }) - it("should handle array pop correctly", () => { - (dispatchers.emails as { pop: Function }).pop(); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("emails"); - expect(lastValue).toEqual(["john@example.com"]); - }); + it('should handle array pop correctly', () => { + ;(dispatchers.emails as { pop: Function }).pop() + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('emails') + expect(lastValue).toEqual(['john@example.com']) + }) - it("should handle array insert correctly", () => { - (dispatchers.emails as { insert: Function }).insert(1, "inserted@example.com"); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("emails"); - expect(lastValue).toEqual(["john@example.com", "inserted@example.com", "doe@example.com"]); - }); + it('should handle array insert correctly', () => { + ;(dispatchers.emails as { insert: Function }).insert(1, 'inserted@example.com') + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('emails') + expect(lastValue).toEqual(['john@example.com', 'inserted@example.com', 'doe@example.com']) + }) - it("should handle array replace correctly", () => { - (dispatchers.emails as { replace: Function }).replace(1, "replaced@example.com"); - const [lastKey, lastValue] = updateAndGetLastCall(); - expect(lastKey).toBe("emails"); - expect(lastValue).toEqual(["john@example.com", "replaced@example.com"]); - }); -}); + it('should handle array replace correctly', () => { + ;(dispatchers.emails as { replace: Function }).replace(1, 'replaced@example.com') + const [lastKey, lastValue] = updateAndGetLastCall() + expect(lastKey).toBe('emails') + expect(lastValue).toEqual(['john@example.com', 'replaced@example.com']) + }) +}) diff --git a/state/create-state-dispatchers.ts b/state/create-state-dispatchers.ts index 41aa669..df49358 100644 --- a/state/create-state-dispatchers.ts +++ b/state/create-state-dispatchers.ts @@ -1,5 +1,5 @@ -import { Dispatchers } from "../types"; -import { isArray, isBool, isNum, isObj } from "../utils/value-checkers"; +import { Dispatchers } from '../types' +import { isArray, isBool, isNum, isObj } from '../utils/value-checkers' export function createArrayDispatchers( key: Key, @@ -8,40 +8,40 @@ export function createArrayDispatchers( ) { return { set: (value: T[], opts?: Options) => { - updateFunction(key, value, opts); + updateFunction(key, value, opts) }, push: (value: T, opts?: Options) => { - const existingState = state || []; - const newArr = [...existingState, value]; - updateFunction(key, newArr, opts); + const existingState = state || [] + const newArr = [...existingState, value] + updateFunction(key, newArr, opts) }, pop: (opts?: Options) => { - const newArr = state.slice(0, -1); - updateFunction(key, newArr, opts); + const newArr = state.slice(0, -1) + updateFunction(key, newArr, opts) }, insert: (index: number, value: T, overwrite = false, opts?: Options) => { - if (!state) state = []; + if (!state) state = [] - const newArr = [...state]; + const newArr = [...state] if (overwrite) { - newArr[index] = value; + newArr[index] = value } else { - newArr.splice(index, 0, value); + newArr.splice(index, 0, value) } - updateFunction(key, newArr, opts); + updateFunction(key, newArr, opts) }, replace: (index: number, value: T, opts?: Options) => { - if (!state) state = []; + if (!state) state = [] - const updatedState = [...state]; + const updatedState = [...state] - updatedState[index] = value; + updatedState[index] = value - updateFunction(key, updatedState, opts); + updateFunction(key, updatedState, opts) }, - }; + } } export function createBooleanDispatchers( @@ -51,12 +51,12 @@ export function createBooleanDispatchers( ) { return { set: (value: boolean, opts?: Options) => { - updateFunction(key, value, opts); + updateFunction(key, value, opts) }, toggle: (opts?: Options) => { - updateFunction(key, !state, opts); + updateFunction(key, !state, opts) }, - }; + } } export function createObjectDispatchers( @@ -66,13 +66,13 @@ export function createObjectDispatchers( ) { return { set: (value: T, opts?: Options) => { - updateFunction(key, value, opts); + updateFunction(key, value, opts) }, update: (value: Partial, opts?: Options) => { - const newValue = { ...state, ...value }; - updateFunction(key, newValue, opts); + const newValue = { ...state, ...value } + updateFunction(key, newValue, opts) }, - }; + } } export function createNumberDispatchers( @@ -82,15 +82,15 @@ export function createNumberDispatchers( ) { return { set: (value: number, opts?: Options) => { - updateFunction(key, value, opts); + updateFunction(key, value, opts) }, increment: (amount: number = 1, opts?: Options) => { - updateFunction(key, state + amount, opts); + updateFunction(key, state + amount, opts) }, decrement: (amount: number = 1, opts?: Options) => { - updateFunction(key, state - amount, opts); + updateFunction(key, state - amount, opts) }, - }; + } } export function createDefaultDispatchers( @@ -99,33 +99,33 @@ export function createDefaultDispatchers( ) { return { set: (value: T, opts?: Options) => { - updateFunction(key, value, opts); + updateFunction(key, value, opts) }, - }; + } } export function mergeWithDefault( defaultState: Readonly, // mark it as readonly state: Readonly, // mark it as readonly ): State { - const mergedState: Partial = {}; - const missingKeys: (keyof State)[] = []; + const mergedState: Partial = {} + const missingKeys: (keyof State)[] = [] for (const key in defaultState) { if (key in state) { - mergedState[key] = state[key]; + mergedState[key] = state[key] } else { - mergedState[key] = defaultState[key]; - missingKeys.push(key); + mergedState[key] = defaultState[key] + missingKeys.push(key) } } // Log missing keys if (missingKeys.length > 0) { - console.info("Missing keys from state:", missingKeys.join(", ")); + console.info('Missing keys from state:', missingKeys.join(', ')) } - return mergedState as State; + return mergedState as State } export function createStateDispatchers({ @@ -133,31 +133,28 @@ export function createStateDispatchers void; + state: State + defaultState: State + updateFunction: (key: keyof State, value: any, opts?: UpdateFnOpts) => void }): Dispatchers { - const mergedState = mergeWithDefault(defaultState, state); - - return (Object.keys(mergedState) as (keyof State)[]).reduce( - (acc, key) => { - const k = key as keyof State; - const currentValue = mergedState[k]; - - if (isArray(currentValue)) { - acc[k] = createArrayDispatchers(k, currentValue, updateFunction) as any; - } else if (isObj(currentValue)) { - acc[k] = createObjectDispatchers(k, currentValue, updateFunction) as any; - } else if (isNum(currentValue)) { - acc[k] = createNumberDispatchers(k, currentValue, updateFunction) as any; - } else if (isBool(currentValue)) { - acc[k] = createBooleanDispatchers(k, currentValue, updateFunction) as any; - } else { - acc[k] = createDefaultDispatchers(k, updateFunction) as any; - } + const mergedState = mergeWithDefault(defaultState, state) + + return (Object.keys(mergedState) as (keyof State)[]).reduce((acc, key) => { + const k = key as keyof State + const currentValue = mergedState[k] + + if (isArray(currentValue)) { + acc[k] = createArrayDispatchers(k, currentValue, updateFunction) as any + } else if (isObj(currentValue)) { + acc[k] = createObjectDispatchers(k, currentValue, updateFunction) as any + } else if (isNum(currentValue)) { + acc[k] = createNumberDispatchers(k, currentValue, updateFunction) as any + } else if (isBool(currentValue)) { + acc[k] = createBooleanDispatchers(k, currentValue, updateFunction) as any + } else { + acc[k] = createDefaultDispatchers(k, updateFunction) as any + } - return acc; - }, - {} as Dispatchers, - ); + return acc + }, {} as Dispatchers) } diff --git a/state/index.ts b/state/index.ts index b7e2cf1..dd5ce5f 100644 --- a/state/index.ts +++ b/state/index.ts @@ -1,4 +1,4 @@ -export { createStateDispatchers } from "./create-state-dispatchers"; -export { createStateManager } from "./state-manager"; -export type { AllowedStateKeys } from "./state-manager"; -export { createWSStateHandler } from "./ws-state-manager"; +export { createStateDispatchers } from './create-state-dispatchers' +export { createStateManager } from './state-manager' +export type { AllowedStateKeys } from './state-manager' +export { createWSStateHandler } from './ws-state-manager' diff --git a/state/state-manager.test.ts b/state/state-manager.test.ts index 1be6e29..460fb6d 100644 --- a/state/state-manager.test.ts +++ b/state/state-manager.test.ts @@ -1,60 +1,60 @@ -import { beforeEach, describe, expect, it, jest } from "bun:test"; -import { createStateManager } from "./state-manager"; // Adjust path +import { beforeEach, describe, expect, it, jest } from 'bun:test' +import { createStateManager } from './state-manager' // Adjust path -describe("createStateManager", () => { +describe('createStateManager', () => { type TestState = { - count: number; - flag: boolean; - items: string[]; - }; + count: number + flag: boolean + items: string[] + } - let stateManager: ReturnType>; + let stateManager: ReturnType> beforeEach(() => { stateManager = createStateManager({ count: 0, flag: false, items: [], - }); - }); + }) + }) - it("should initialize state correctly", () => { - expect(stateManager.state.count).toBe(0); - expect(stateManager.state.flag).toBe(false); - expect(stateManager.state.items).toEqual([]); - }); + it('should initialize state correctly', () => { + expect(stateManager.state.count).toBe(0) + expect(stateManager.state.flag).toBe(false) + expect(stateManager.state.items).toEqual([]) + }) - it("should update state and dispatch changes", () => { - stateManager.updateStateAndDispatch("count", 5); - expect(stateManager.state.count).toBe(5); - }); + it('should update state and dispatch changes', () => { + stateManager.updateStateAndDispatch('count', 5) + expect(stateManager.state.count).toBe(5) + }) - it("should listen to state changes with onStateChange", () => { - const mockCallback = jest.fn(); - stateManager.onStateChange("count", mockCallback); + it('should listen to state changes with onStateChange', () => { + const mockCallback = jest.fn() + stateManager.onStateChange('count', mockCallback) - stateManager.updateStateAndDispatch("count", 10); - expect(mockCallback).toHaveBeenCalled(); - }); + stateManager.updateStateAndDispatch('count', 10) + expect(mockCallback).toHaveBeenCalled() + }) - it("should handle conditional state changes with whenValueIs", (done) => { - stateManager.whenValueIs("count", 15).then(() => { - done(); // Complete the test when this condition is met - }); + it('should handle conditional state changes with whenValueIs', (done) => { + stateManager.whenValueIs('count', 15).then(() => { + done() // Complete the test when this condition is met + }) - stateManager.updateStateAndDispatch("count", 15); - }); + stateManager.updateStateAndDispatch('count', 15) + }) - it("should handle subscriptions", () => { - const mockSubscription = jest.fn(); - const unsubscribe = stateManager.subscribe(mockSubscription); + it('should handle subscriptions', () => { + const mockSubscription = jest.fn() + const unsubscribe = stateManager.subscribe(mockSubscription) - stateManager.updateStateAndDispatch("count", 20); - expect(mockSubscription).toHaveBeenCalled(); + stateManager.updateStateAndDispatch('count', 20) + expect(mockSubscription).toHaveBeenCalled() - unsubscribe(); + unsubscribe() - stateManager.updateStateAndDispatch("count", 25); - expect(mockSubscription).toHaveBeenCalledTimes(1); // Shouldn't be called again after unsubscribe - }); -}); + stateManager.updateStateAndDispatch('count', 25) + expect(mockSubscription).toHaveBeenCalledTimes(1) // Shouldn't be called again after unsubscribe + }) +}) diff --git a/state/state-manager.ts b/state/state-manager.ts index 9399b72..8a8983e 100644 --- a/state/state-manager.ts +++ b/state/state-manager.ts @@ -1,33 +1,33 @@ -import { FilteredKeys } from "../type-utils"; -import { createStateDispatchers } from "./create-state-dispatchers"; +import { FilteredKeys } from '../type-utils' +import { createStateDispatchers } from './create-state-dispatchers' -export type AllowedStateKeys = boolean | string | number; +export type AllowedStateKeys = boolean | string | number export const createStateManager = (initialState: State) => { - let currentState: State = initialState; + let currentState: State = initialState const stateChangeCallbacks: { - [Key in keyof State]?: Array<(newValue: State[Key]) => void>; - } = {}; + [Key in keyof State]?: Array<(newValue: State[Key]) => void> + } = {} - const listeners: Array<() => void> = []; + const listeners: Array<() => void> = [] function broadcast() { - listeners.forEach((fn) => fn()); + listeners.forEach((fn) => fn()) } function subscribe(listener: () => void) { - listeners.push(listener); + listeners.push(listener) return () => { - const index = listeners.indexOf(listener); + const index = listeners.indexOf(listener) if (index !== -1) { - listeners.splice(index, 1); + listeners.splice(index, 1) } - }; + } } function onStateChange(key: Key, callback: (newValue: State[Key]) => void) { - (stateChangeCallbacks[key] ??= []).push(callback); + ;(stateChangeCallbacks[key] ??= []).push(callback) } function updateStateAndDispatch( @@ -35,14 +35,14 @@ export const createStateManager = (initialState: State) => updater: ((currentState: State[keyof State]) => State[keyof State]) | State[keyof State], ) { const newValue = - typeof updater === "function" + typeof updater === 'function' ? (updater as (currentState: State[keyof State]) => State[keyof State])(currentState[key]) - : updater; - currentState[key] = newValue; + : updater + currentState[key] = newValue - stateChangeCallbacks[key]?.forEach((callback) => callback(newValue)); + stateChangeCallbacks[key]?.forEach((callback) => callback(newValue)) - broadcast(); + broadcast() } const whenValueIs = < @@ -52,27 +52,27 @@ export const createStateManager = (initialState: State) => key: Key, expectedValue: ExpectedVal, ) => { - const value = currentState[key]; + const value = currentState[key] return { then: (callback: () => void) => { - if (value === expectedValue) callback(); + if (value === expectedValue) callback() else { onStateChange(key, (newValue) => { if (newValue === expectedValue) { - callback(); - stateChangeCallbacks[key] = stateChangeCallbacks[key]?.filter((c) => c !== callback); + callback() + stateChangeCallbacks[key] = stateChangeCallbacks[key]?.filter((c) => c !== callback) } - }); + }) } }, - }; - }; + } + } const dispatchers = createStateDispatchers({ defaultState: initialState, state: currentState, updateFunction: updateStateAndDispatch, - }); + }) return { state: currentState, @@ -81,5 +81,5 @@ export const createStateManager = (initialState: State) => updateStateAndDispatch, whenValueIs, subscribe, - }; -}; + } +} diff --git a/state/ws-state-manager.ts b/state/ws-state-manager.ts index 6ba095a..ce8c21b 100644 --- a/state/ws-state-manager.ts +++ b/state/ws-state-manager.ts @@ -1,28 +1,28 @@ -import { ServerWebSocket, WebSocketHandler } from "bun"; -import { createStateManager } from "./state-manager"; +import { ServerWebSocket, WebSocketHandler } from 'bun' +import { createStateManager } from './state-manager' export const createWSStateHandler = ( stateMachine: ReturnType>, ) => { - const connectedClients = new Set(); + const connectedClients = new Set() const websocketHandler: WebSocketHandler = { open: (ws) => { - connectedClients.add(ws); + connectedClients.add(ws) }, close: (ws) => { - connectedClients.delete(ws); + connectedClients.delete(ws) }, message: (ws, msg) => { - if (typeof msg !== "string") return; + if (typeof msg !== 'string') return - const data: { key: keyof State; value: State[keyof State] } = JSON.parse(msg); + const data: { key: keyof State; value: State[keyof State] } = JSON.parse(msg) if (data.key in stateMachine.state) { - stateMachine.updateStateAndDispatch(data.key, data.value); + stateMachine.updateStateAndDispatch(data.key, data.value) } }, - }; + } // Watch state changes and broadcast to connected clients for (const key in stateMachine.state) { @@ -30,26 +30,26 @@ export const createWSStateHandler = ( const updatedStateData = { key, value: newValue, - }; + } for (const client of connectedClients) { - client.send(JSON.stringify(updatedStateData)); + client.send(JSON.stringify(updatedStateData)) } - }); + }) } stateMachine.subscribe(() => { for (const key in stateMachine.state) { - const newValue = stateMachine.state[key]; + const newValue = stateMachine.state[key] const updatedStateData = { key, value: newValue, - }; + } for (const client of connectedClients) { - client.send(JSON.stringify(updatedStateData)); + client.send(JSON.stringify(updatedStateData)) } } - }); + }) - return websocketHandler; -}; + return websocketHandler +} diff --git a/test-utils/index.ts b/test-utils/index.ts index 3d29ee4..1aace80 100644 --- a/test-utils/index.ts +++ b/test-utils/index.ts @@ -1 +1 @@ -export { isTestFile } from "./test-utils"; +export { isTestFile } from './test-utils' diff --git a/test-utils/test-utils.test.ts b/test-utils/test-utils.test.ts index b18a239..e7270e0 100644 --- a/test-utils/test-utils.test.ts +++ b/test-utils/test-utils.test.ts @@ -1,21 +1,21 @@ -import { describe, expect, test } from "bun:test"; -import { isTestFile } from "./test-utils"; +import { describe, expect, test } from 'bun:test' +import { isTestFile } from './test-utils' -describe("isTestFile", () => { - test("returns true when file name ends with test", () => { +describe('isTestFile', () => { + test('returns true when file name ends with test', () => { const meta = { - file: "utils.test.ts", - dir: "utils", - }; - const result = isTestFile(meta as ImportMeta); - expect(result).toBe(true); - }); - test("returns false when file name does not end with test", () => { + file: 'utils.test.ts', + dir: 'utils', + } + const result = isTestFile(meta as ImportMeta) + expect(result).toBe(true) + }) + test('returns false when file name does not end with test', () => { const meta = { - file: "utils.ts", - dir: "utils", - }; - const result = isTestFile(meta as ImportMeta); - expect(result).toBe(false); - }); -}); + file: 'utils.ts', + dir: 'utils', + } + const result = isTestFile(meta as ImportMeta) + expect(result).toBe(false) + }) +}) diff --git a/test-utils/test-utils.ts b/test-utils/test-utils.ts index c6dc752..d3e8737 100644 --- a/test-utils/test-utils.ts +++ b/test-utils/test-utils.ts @@ -1,6 +1,6 @@ -export const isTestFile = (meta: ImportMeta, testFileMatch: string = "test") => { - const splitFile = meta.file.split("."); - const fileExtensions = splitFile.slice(1).join("."); +export const isTestFile = (meta: ImportMeta, testFileMatch: string = 'test') => { + const splitFile = meta.file.split('.') + const fileExtensions = splitFile.slice(1).join('.') - return fileExtensions?.includes(testFileMatch) || false; -}; + return fileExtensions?.includes(testFileMatch) || false +} diff --git a/test-utils/type-testers.ts b/test-utils/type-testers.ts index 3eab540..d2ce290 100644 --- a/test-utils/type-testers.ts +++ b/test-utils/type-testers.ts @@ -1,3 +1,3 @@ // Type Check Helper -export type TypeCheck = T extends U ? true : never; +export type TypeCheck = T extends U ? true : never export function typeCheck() {} diff --git a/tsconfig.json b/tsconfig.json index ba83e41..f0fe2a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,9 +12,7 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "allowJs": false, - "types": [ - "bun-types", - ], + "types": ["bun-types"], "baseUrl": ".", "paths": { "cli": ["cli"], diff --git a/type-utils.ts b/type-utils.ts index 0a0c7ee..33bf170 100644 --- a/type-utils.ts +++ b/type-utils.ts @@ -1,22 +1,22 @@ export type FilteredKeys = { - [K in keyof T]: T[K] extends U ? K : never; -}[keyof T]; + [K in keyof T]: T[K] extends U ? K : never +}[keyof T] -export type ArrayTypesExtract, K extends keyof T[number]> = T[number][K]; +export type ArrayTypesExtract, K extends keyof T[number]> = T[number][K] -export type IfFunction = T extends (...args: any[]) => any ? ReturnType : T; +export type IfFunction = T extends (...args: any[]) => any ? ReturnType : T // creates a union type of all the types in the array -export type UnboxArray> = T extends Array ? U : never; +export type UnboxArray> = T extends Array ? U : never // type that takes an object type, an input type and an output type, // and returns a type that has the same keys as the object type, // however if you have string values and you want to transform the values to numbers // you can easily do that with this type util, otherwise every other key is left untouched export type TransformValues = { - [K in keyof T]: T[K] extends FindT ? ReplaceT : T[K]; -}; + [K in keyof T]: T[K] extends FindT ? ReplaceT : T[K] +} export type PartialRecord = { - [P in K]?: T; -}; + [P in K]?: T +} diff --git a/types.ts b/types.ts index fed4c56..9744b50 100644 --- a/types.ts +++ b/types.ts @@ -1,41 +1,41 @@ export type TypeMapping = { - string: string; - number: number; - boolean: boolean; - date: Date; -}; + string: string + number: number + boolean: boolean + date: Date +} -export type TypeMappingKeys = keyof TypeMapping; +export type TypeMappingKeys = keyof TypeMapping export type SchemaTInference> = { - [K in keyof T]: TypeMapping[T[K]]; -}; + [K in keyof T]: TypeMapping[T[K]] +} export type ValidationResult = { - error?: string; - data?: Schema[]; -}; + error?: string + data?: Schema[] +} -export type SchemaT = Record; +export type SchemaT = Record export type SetDispatch = { - set: (value: Key, options?: Options) => void; -}; + set: (value: Key, options?: Options) => void +} export type ArrayDispatch = { - push: (value: T, options?: Options) => void; - pop: (options?: Options) => void; - insert: (index: number, value: T, options?: Options) => void; -}; + push: (value: T, options?: Options) => void + pop: (options?: Options) => void + insert: (index: number, value: T, options?: Options) => void +} export type ObjectDispatch = { - update: (value: Partial, options?: Options) => void; -}; + update: (value: Partial, options?: Options) => void +} export type NumberDispatch = { - increment: (amount?: number, options?: Options) => void; - decrement: (amount?: number, options?: Options) => void; -}; + increment: (amount?: number, options?: Options) => void + decrement: (amount?: number, options?: Options) => void +} export type Dispatchers = { [Key in keyof State]: State[Key] extends (infer T)[] @@ -44,5 +44,5 @@ export type Dispatchers = { ? SetDispatch & ObjectDispatch : State[Key] extends number ? SetDispatch & NumberDispatch - : SetDispatch; -}; + : SetDispatch +} diff --git a/utils/base-error.ts b/utils/base-error.ts index 38a85e3..500caf4 100644 --- a/utils/base-error.ts +++ b/utils/base-error.ts @@ -1,15 +1,15 @@ export class BaseError extends Error { - public readonly name: string; - public readonly data: T; + public readonly name: string + public readonly data: T constructor(message: string, data: T) { - super(message); + super(message) if (Error.captureStackTrace) { - Error.captureStackTrace(this, BaseError); + Error.captureStackTrace(this, BaseError) } - this.name = "BaseError"; - this.data = data; + this.name = 'BaseError' + this.data = data } } diff --git a/utils/classy.test.ts b/utils/classy.test.ts index 82f3445..3ef7a2b 100644 --- a/utils/classy.test.ts +++ b/utils/classy.test.ts @@ -1,30 +1,30 @@ -import { expect, test } from "bun:test"; -import classy from "./classy"; -test("test single string argument", () => { - expect(classy("hello")).toBe("hello"); -}); +import { expect, test } from 'bun:test' +import classy from './classy' +test('test single string argument', () => { + expect(classy('hello')).toBe('hello') +}) -test("test multiple string arguments", () => { - expect(classy("hello", "world")).toBe("hello world"); -}); +test('test multiple string arguments', () => { + expect(classy('hello', 'world')).toBe('hello world') +}) -test("test single number argument", () => { - expect(classy(123)).toBe("123"); -}); +test('test single number argument', () => { + expect(classy(123)).toBe('123') +}) // Tests that passing a single boolean argument returns an empty string -test("test single boolean argument", () => { - expect(classy(true)).toBe(""); -}); +test('test single boolean argument', () => { + expect(classy(true)).toBe('') +}) -test("test single null argument", () => { - expect(classy(null)).toBe(""); -}); +test('test single null argument', () => { + expect(classy(null)).toBe('') +}) -test("test single undefined argument", () => { - expect(classy(undefined)).toBe(""); -}); +test('test single undefined argument', () => { + expect(classy(undefined)).toBe('') +}) -test("test object classes", () => { - expect(classy({ hello: true, world: false })).toBe("hello"); -}); +test('test object classes', () => { + expect(classy({ hello: true, world: false })).toBe('hello') +}) diff --git a/utils/classy.ts b/utils/classy.ts index bee1eb4..6184150 100644 --- a/utils/classy.ts +++ b/utils/classy.ts @@ -1,28 +1,28 @@ -type ClassName = string | number | boolean | null | undefined | ClassNameMap; +type ClassName = string | number | boolean | null | undefined | ClassNameMap interface ClassNameMap { - [key: string]: boolean | null | undefined; + [key: string]: boolean | null | undefined } export function classy(...args: ClassName[]): string { - const classes: string[] = []; + const classes: string[] = [] args.forEach((arg) => { - if (arg == null) return; + if (arg == null) return - if (typeof arg === "string" || typeof arg === "number") { - classes.push(arg.toString()); + if (typeof arg === 'string' || typeof arg === 'number') { + classes.push(arg.toString()) } else if (arg instanceof Array) { - classes.push(classy(...arg)); - } else if (typeof arg === "object") { + classes.push(classy(...arg)) + } else if (typeof arg === 'object') { Object.keys(arg).forEach((key) => { if (arg[key]) { - classes.push(key); + classes.push(key) } - }); + }) } - }); + }) - return classes.join(" "); + return classes.join(' ') } -export default classy; +export default classy diff --git a/utils/http-types.ts b/utils/http-types.ts index bc2512e..64abb6a 100644 --- a/utils/http-types.ts +++ b/utils/http-types.ts @@ -1,53 +1,53 @@ -import { PartialRecord } from "../type-utils"; +import { PartialRecord } from '../type-utils' // for improved readbility when creating server routes lower -export type RouteMethods = "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; +export type RouteMethods = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' -export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD"; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD' export type CommonHttpHeaders = - | "Accept" - | "Authorization" - | "Cache-Control" - | "Content-Type" - | "Date" - | "Host" - | "Origin" - | "Referer" - | "User-Agent" - | "X-Requested-With" - | "X-Forwarded-For" - | "X-HTTP-Method-Override"; - -export type UToolHTTPHeaders = PartialRecord; - -export type RouteHandler = (request: Request) => Response | Promise; + | 'Accept' + | 'Authorization' + | 'Cache-Control' + | 'Content-Type' + | 'Date' + | 'Host' + | 'Origin' + | 'Referer' + | 'User-Agent' + | 'X-Requested-With' + | 'X-Forwarded-For' + | 'X-HTTP-Method-Override' + +export type UToolHTTPHeaders = PartialRecord + +export type RouteHandler = (request: Request) => Response | Promise export type MiddlwareParams = { - request: Request; - next?: Middleware; - context?: CtxT; - response: Response; -}; + request: Request + next?: Middleware + context?: CtxT + response: Response +} export type Middleware = ({ request, next, context, response, -}: MiddlwareParams) => Response; +}: MiddlwareParams) => Response -export type RouteMap = Record; +export type RouteMap = Record -export type ErrorHandler = (error: any, request: Request) => Response; +export type ErrorHandler = (error: any, request: Request) => Response export type RouteReqDataOpts = { - body?: Request["body"]; - params?: object; - headers?: object; -}; + body?: Request['body'] + params?: object + headers?: object +} -export type ClientCORSCredentialOpts = "omit" | "same-origin" | "include"; +export type ClientCORSCredentialOpts = 'omit' | 'same-origin' | 'include' /** * Options for configuring Cross-Origin Resource Sharing (CORS) behavior. @@ -57,20 +57,20 @@ export type CORSOptions = { * An array of allowed origins. Requests from origins not in this array will be rejected. * ["*"] will allow all origins. */ - allowedOrigins?: string[]; + allowedOrigins?: string[] /** * An array of allowed HTTP methods. Requests using methods not in this array will be rejected. */ - allowedMethods?: HTTPMethod[]; + allowedMethods?: HTTPMethod[] /** * An array of allowed HTTP headers. Requests with headers not in this array will be rejected. */ - allowedHeaders?: CommonHttpHeaders[] | string[]; + allowedHeaders?: CommonHttpHeaders[] | string[] /** * Whether or not to allow credentials to be sent with the request. */ - credentials?: boolean; -}; + credentials?: boolean +} /** * Type for a generic route handler that provides typed access to the request body, query params, and headers. @@ -79,20 +79,18 @@ export type CORSOptions = { * as well as typed parsing methods for the request body, query params, and headers. */ export type CreateRouteGeneric = { - request: Request; - parseBodyJson: () => Promise; - parseQueryParams: () => ParamsType; - parseHeaders: () => HeadersT; - response: Response; -}; + request: Request + parseBodyJson: () => Promise + parseQueryParams: () => ParamsType + parseHeaders: () => HeadersT + response: Response +} -export type ReqHandler = ( - args: CreateRouteGeneric, -) => Response | Promise; +export type ReqHandler = (args: CreateRouteGeneric) => Response | Promise -export type OnRequestT = (handler: ReqHandler | Promise>) => void; +export type OnRequestT = (handler: ReqHandler | Promise>) => void export interface RouteOptions { - errorMessage?: string; - onError?: (error: Error, request: Request) => Response; + errorMessage?: string + onError?: (error: Error, request: Request) => Response } diff --git a/utils/normalize-bytes.test.ts b/utils/normalize-bytes.test.ts index ec22dc7..bc393f8 100644 --- a/utils/normalize-bytes.test.ts +++ b/utils/normalize-bytes.test.ts @@ -1,52 +1,52 @@ -import { describe, expect, test } from "bun:test"; -import { normalizeBytes } from "./normalize-bytes"; - -describe("normalizeBytes function", () => { - test("should return 0 Bytes for input 0", () => { - const result = normalizeBytes(0); - expect(result.value).toBe(0); - expect(result.unit).toBe("Bytes"); - }); - - test("should correctly normalize Bytes", () => { - const result = normalizeBytes(500); - expect(result.value).toBe(500); - expect(result.unit).toBe("Bytes"); - }); - - test("should correctly normalize KB", () => { - const result = normalizeBytes(1500); - expect(result.value).toBe(1.46); // 1500/1024 = 1.46484375, which becomes 1.46 after rounding to 2 decimals - expect(result.unit).toBe("KB"); - }); - - test("should correctly normalize MB", () => { - const result = normalizeBytes(10485760); // 10*1024*1024 - expect(result.value).toBe(10); - expect(result.unit).toBe("MB"); - }); - - test("should correctly normalize GB", () => { - const result = normalizeBytes(10737418240); // 10*1024*1024*1024 - expect(result.value).toBe(10); - expect(result.unit).toBe("GB"); - }); - - test("should correctly normalize with more than 2 decimals", () => { - const result = normalizeBytes(1500, 4); - expect(result.value).toBe(1.4648); // Rounded to 4 decimals - expect(result.unit).toBe("KB"); - }); - - test("should correctly normalize with no decimals", () => { - const result = normalizeBytes(1500, 0); - expect(result.value).toBe(1); // Rounded to 0 decimals - expect(result.unit).toBe("KB"); - }); - - test("should handle negative decimals (edge case)", () => { - const result = normalizeBytes(1500, -2); - expect(result.value).toBe(1.46); // Should revert to default behavior of 2 decimals - expect(result.unit).toBe("KB"); - }); -}); +import { describe, expect, test } from 'bun:test' +import { normalizeBytes } from './normalize-bytes' + +describe('normalizeBytes function', () => { + test('should return 0 Bytes for input 0', () => { + const result = normalizeBytes(0) + expect(result.value).toBe(0) + expect(result.unit).toBe('Bytes') + }) + + test('should correctly normalize Bytes', () => { + const result = normalizeBytes(500) + expect(result.value).toBe(500) + expect(result.unit).toBe('Bytes') + }) + + test('should correctly normalize KB', () => { + const result = normalizeBytes(1500) + expect(result.value).toBe(1.46) // 1500/1024 = 1.46484375, which becomes 1.46 after rounding to 2 decimals + expect(result.unit).toBe('KB') + }) + + test('should correctly normalize MB', () => { + const result = normalizeBytes(10485760) // 10*1024*1024 + expect(result.value).toBe(10) + expect(result.unit).toBe('MB') + }) + + test('should correctly normalize GB', () => { + const result = normalizeBytes(10737418240) // 10*1024*1024*1024 + expect(result.value).toBe(10) + expect(result.unit).toBe('GB') + }) + + test('should correctly normalize with more than 2 decimals', () => { + const result = normalizeBytes(1500, 4) + expect(result.value).toBe(1.4648) // Rounded to 4 decimals + expect(result.unit).toBe('KB') + }) + + test('should correctly normalize with no decimals', () => { + const result = normalizeBytes(1500, 0) + expect(result.value).toBe(1) // Rounded to 0 decimals + expect(result.unit).toBe('KB') + }) + + test('should handle negative decimals (edge case)', () => { + const result = normalizeBytes(1500, -2) + expect(result.value).toBe(1.46) // Should revert to default behavior of 2 decimals + expect(result.unit).toBe('KB') + }) +}) diff --git a/utils/normalize-bytes.ts b/utils/normalize-bytes.ts index 645cdd4..9ac74fa 100644 --- a/utils/normalize-bytes.ts +++ b/utils/normalize-bytes.ts @@ -2,18 +2,18 @@ export const normalizeBytes = (bytes: number, decimals = 2) => { if (bytes === 0) return { value: 0, - unit: "Bytes", - }; + unit: 'Bytes', + } - const k = 1024; - const dm = Math.abs(decimals); + const k = 1024 + const dm = Math.abs(decimals) - const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"]; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)); + const i = Math.floor(Math.log(bytes) / Math.log(k)) return { value: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)), unit: sizes[i], - }; -}; + } +} diff --git a/utils/text-utils.test.ts b/utils/text-utils.test.ts index cc7a2d3..514a83d 100644 --- a/utils/text-utils.test.ts +++ b/utils/text-utils.test.ts @@ -1,87 +1,87 @@ -import { describe, expect, test } from "bun:test"; -import { convertMarkdownToHTML, parsers, replaceMarkdown } from "./text-utils"; -describe("HTML and Markdown utilities", () => { +import { describe, expect, test } from 'bun:test' +import { convertMarkdownToHTML, parsers, replaceMarkdown } from './text-utils' +describe('HTML and Markdown utilities', () => { // Testing Markdown related functions - describe("Markdown conversion", () => { + describe('Markdown conversion', () => { // replaceMarkdown function - test("replaceMarkdown replaces patterns correctly", () => { - const markdown = "**Hello**"; - const regex = /\*\*(.+?)\*\*/g; - const replacement = "$1"; - const expectedHTML = "Hello"; - expect(replaceMarkdown(markdown, regex, replacement)).toEqual(expectedHTML); - }); + test('replaceMarkdown replaces patterns correctly', () => { + const markdown = '**Hello**' + const regex = /\*\*(.+?)\*\*/g + const replacement = '$1' + const expectedHTML = 'Hello' + expect(replaceMarkdown(markdown, regex, replacement)).toEqual(expectedHTML) + }) // parsers object - test("convertMarkdownToHTML converts markdown to HTML", () => { - const markdown = "# Hello\n**World**\n*This* is a [link](https://example.com)"; + test('convertMarkdownToHTML converts markdown to HTML', () => { + const markdown = '# Hello\n**World**\n*This* is a [link](https://example.com)' const expectedHTML = - '

Hello

\nWorld\nThis is a link'; - expect(convertMarkdownToHTML(markdown)).toEqual(expectedHTML); - }); + '

Hello

\nWorld\nThis is a link' + expect(convertMarkdownToHTML(markdown)).toEqual(expectedHTML) + }) // convertMarkdownToHTML function - test("convertMarkdownToHTML converts markdown to HTML", () => { - const markdown = "# Hello\n**World**\n*This* is a [link](https://example.com)"; + test('convertMarkdownToHTML converts markdown to HTML', () => { + const markdown = '# Hello\n**World**\n*This* is a [link](https://example.com)' const expectedHTML = - '

Hello

\nWorld\nThis is a link'; - expect(convertMarkdownToHTML(markdown)).toEqual(expectedHTML); - }); - }); -}); + '

Hello

\nWorld\nThis is a link' + expect(convertMarkdownToHTML(markdown)).toEqual(expectedHTML) + }) + }) +}) -describe("Markdown Parsers", () => { - test("headers parser", () => { - const input = "# Heading 1\n## Heading 2\n### Heading 3"; - const output = parsers.headers(input); - expect(output).toBe("

Heading 1

\n

Heading 2

\n

Heading 3

"); - }); +describe('Markdown Parsers', () => { + test('headers parser', () => { + const input = '# Heading 1\n## Heading 2\n### Heading 3' + const output = parsers.headers(input) + expect(output).toBe('

Heading 1

\n

Heading 2

\n

Heading 3

') + }) - test("bold parser", () => { - const input = "**bold text**"; - const output = parsers.bold(input); - expect(output).toBe("bold text"); - }); + test('bold parser', () => { + const input = '**bold text**' + const output = parsers.bold(input) + expect(output).toBe('bold text') + }) - test("italic parser", () => { - const input = "*italic text*"; - const output = parsers.italic(input); - expect(output).toBe("italic text"); - }); + test('italic parser', () => { + const input = '*italic text*' + const output = parsers.italic(input) + expect(output).toBe('italic text') + }) - test("links parser", () => { - const input = "[an example](http://example.com/)"; - const output = parsers.links(input); - expect(output).toBe('an example'); - }); + test('links parser', () => { + const input = '[an example](http://example.com/)' + const output = parsers.links(input) + expect(output).toBe('an example') + }) - test("unorderedLists parser", () => { - const input = "- item 1\n- item 2"; - const output = parsers.unorderedLists(input); - expect(output).toBe("
  • item 1
  • \n
  • item 2
"); - }); + test('unorderedLists parser', () => { + const input = '- item 1\n- item 2' + const output = parsers.unorderedLists(input) + expect(output).toBe('
  • item 1
  • \n
  • item 2
') + }) - test("orderedLists parser", () => { - const input = "1. item 1\n2. item 2"; - const output = parsers.orderedLists(input); - expect(output).toBe("
  1. item 1
  2. \n
  3. item 2
"); - }); + test('orderedLists parser', () => { + const input = '1. item 1\n2. item 2' + const output = parsers.orderedLists(input) + expect(output).toBe('
  1. item 1
  2. \n
  3. item 2
') + }) - test("blockquotes parser", () => { - const input = "> blockquote"; - const output = parsers.blockquotes(input); - expect(output).toBe("
blockquote
"); - }); + test('blockquotes parser', () => { + const input = '> blockquote' + const output = parsers.blockquotes(input) + expect(output).toBe('
blockquote
') + }) - test("codeBlocks parser", () => { - const input = "```\nconst a = 10;\n```"; - const output = parsers.codeBlocks(input); - expect(output).toBe("
\nconst a = 10;\n
"); - }); + test('codeBlocks parser', () => { + const input = '```\nconst a = 10;\n```' + const output = parsers.codeBlocks(input) + expect(output).toBe('
\nconst a = 10;\n
') + }) - test("inlineCode parser", () => { - const input = "`const a = 10;`"; - const output = parsers.inlineCode(input); - expect(output).toBe("const a = 10;"); - }); -}); + test('inlineCode parser', () => { + const input = '`const a = 10;`' + const output = parsers.inlineCode(input) + expect(output).toBe('const a = 10;') + }) +}) diff --git a/utils/text-utils.ts b/utils/text-utils.ts index ecddde8..dfb3b33 100644 --- a/utils/text-utils.ts +++ b/utils/text-utils.ts @@ -1,71 +1,71 @@ export function replaceMarkdown(text: string, regex: RegExp, replacement: string): string { - return text.replace(regex, replacement); + return text.replace(regex, replacement) } export const parsers = { headers(text: string): string { for (let i = 6; i > 0; i--) { - const regex = new RegExp(`^(#{${i}}) (.*)`, "gm"); - text = replaceMarkdown(text, regex, `$2`); + const regex = new RegExp(`^(#{${i}}) (.*)`, 'gm') + text = replaceMarkdown(text, regex, `$2`) } - return text; + return text }, bold(text: string): string { - return replaceMarkdown(text, /\*\*(.+?)\*\*/g, "$1"); + return replaceMarkdown(text, /\*\*(.+?)\*\*/g, '$1') }, italic(text: string): string { - return replaceMarkdown(text, /\*(.+?)\*/g, "$1"); + return replaceMarkdown(text, /\*(.+?)\*/g, '$1') }, links(text: string): string { - return replaceMarkdown(text, /\[(.+?)\]\((.+?)\)/g, '$1'); + return replaceMarkdown(text, /\[(.+?)\]\((.+?)\)/g, '$1') }, unorderedLists(text: string): string { return text.replace(/(- .*(\n|$))+/g, (match) => { - const items = match.split("\n").filter(Boolean); - const liItems = items.map((item) => item.replace(/- (.*)/, "
  • $1
  • ")).join("\n"); - return `
      ${liItems}
    `; - }); + const items = match.split('\n').filter(Boolean) + const liItems = items.map((item) => item.replace(/- (.*)/, '
  • $1
  • ')).join('\n') + return `
      ${liItems}
    ` + }) }, orderedLists(text: string): string { return text.replace(/(\d+\. .*(\n|$))+/g, (match) => { - const items = match.split("\n").filter(Boolean); - const liItems = items.map((item) => item.replace(/\d+\. (.*)/, "
  • $1
  • ")).join("\n"); - return `
      ${liItems}
    `; - }); + const items = match.split('\n').filter(Boolean) + const liItems = items.map((item) => item.replace(/\d+\. (.*)/, '
  • $1
  • ')).join('\n') + return `
      ${liItems}
    ` + }) }, blockquotes(text: string): string { - return replaceMarkdown(text, /^>\s(.+)/gm, "
    $1
    "); + return replaceMarkdown(text, /^>\s(.+)/gm, '
    $1
    ') }, codeBlocks(text: string): string { - return replaceMarkdown(text, /```(.+?)```/gs, "
    $1
    "); + return replaceMarkdown(text, /```(.+?)```/gs, '
    $1
    ') }, inlineCode(text: string): string { - return replaceMarkdown(text, /`(.+?)`/g, "$1"); + return replaceMarkdown(text, /`(.+?)`/g, '$1') }, paragraphs(text: string): string { // Wrap lines not starting with HTML tags in

    tags - return text.replace(/^(?!<.*>)(.+)$/gm, "

    $1

    "); + return text.replace(/^(?!<.*>)(.+)$/gm, '

    $1

    ') }, -}; +} export function convertMarkdownToHTML(markdownText: string): string { - let html = markdownText; + let html = markdownText const parserOrder: Array = [ - "headers", - "bold", - "italic", - "links", - "unorderedLists", - "orderedLists", - "blockquotes", - "codeBlocks", - "inlineCode", - "paragraphs", - ]; + 'headers', + 'bold', + 'italic', + 'links', + 'unorderedLists', + 'orderedLists', + 'blockquotes', + 'codeBlocks', + 'inlineCode', + 'paragraphs', + ] for (const parserName of parserOrder) { - html = parsers[parserName](html); + html = parsers[parserName](html) } - return html; + return html } diff --git a/utils/ulog.ts b/utils/ulog.ts index 538fd58..f2fc83d 100644 --- a/utils/ulog.ts +++ b/utils/ulog.ts @@ -1,7 +1,7 @@ // utility log -import Bun from "bun"; +import Bun from 'bun' export const ulog = (...args: unknown[]) => { - const currentTime = Bun.nanoseconds() / 1e9; // Convert to seconds - console.log(`[${currentTime.toFixed(4)}s]`, args); -}; + const currentTime = Bun.nanoseconds() / 1e9 // Convert to seconds + console.log(`[${currentTime.toFixed(4)}s]`, args) +} diff --git a/utils/value-checkers.test.ts b/utils/value-checkers.test.ts index 54b8131..fb4fb1e 100644 --- a/utils/value-checkers.test.ts +++ b/utils/value-checkers.test.ts @@ -1,30 +1,30 @@ -import { describe, expect, test } from "bun:test"; -import { isArray, isBool, isNum, isObj, isStr } from "./value-checkers"; +import { describe, expect, test } from 'bun:test' +import { isArray, isBool, isNum, isObj, isStr } from './value-checkers' -describe("value-checkers tests", () => { - test("isArray", () => { - expect(isArray([1, 2, 3])).toBe(true); - expect(isArray("not an array")).toBe(false); - }); +describe('value-checkers tests', () => { + test('isArray', () => { + expect(isArray([1, 2, 3])).toBe(true) + expect(isArray('not an array')).toBe(false) + }) - test("isObj", () => { - expect(isObj({ hello: "world" })).toBe(true); - expect(isObj(123)).toBe(false); - }); + test('isObj', () => { + expect(isObj({ hello: 'world' })).toBe(true) + expect(isObj(123)).toBe(false) + }) - test("isNum", () => { - expect(isNum(123)).toBe(true); - expect(isNum("not a number")).toBe(false); - }); + test('isNum', () => { + expect(isNum(123)).toBe(true) + expect(isNum('not a number')).toBe(false) + }) - test("isBool", () => { - expect(isBool(true)).toBe(true); - expect(isBool(false)).toBe(true); - expect(isBool("not a bool")).toBe(false); - }); + test('isBool', () => { + expect(isBool(true)).toBe(true) + expect(isBool(false)).toBe(true) + expect(isBool('not a bool')).toBe(false) + }) - test("isStr", () => { - expect(isStr("a string")).toBe(true); - expect(isStr(123)).toBe(false); - }); -}); + test('isStr', () => { + expect(isStr('a string')).toBe(true) + expect(isStr(123)).toBe(false) + }) +}) diff --git a/utils/value-checkers.ts b/utils/value-checkers.ts index 8a1ced1..f2e6e51 100644 --- a/utils/value-checkers.ts +++ b/utils/value-checkers.ts @@ -1,19 +1,19 @@ export function isArray(input: T | any[]): input is any[] { - return Array.isArray(input); + return Array.isArray(input) } export function isObj(input: any): input is object { - return typeof input === "object" && input !== null && !Array.isArray(input); + return typeof input === 'object' && input !== null && !Array.isArray(input) } export function isNum(input: any): input is number { - return typeof input === "number"; + return typeof input === 'number' } export function isBool(input: any): input is boolean { - return typeof input === "boolean"; + return typeof input === 'boolean' } export function isStr(input: any): input is string { - return typeof input === "string"; + return typeof input === 'string' } diff --git a/uuid/generate-uuid.test.ts b/uuid/generate-uuid.test.ts index 76f01b7..0d6cccc 100644 --- a/uuid/generate-uuid.test.ts +++ b/uuid/generate-uuid.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from 'bun:test' import { extractTimestampFromUuidV6, extractTimestampFromUuidV7, @@ -18,197 +18,197 @@ import { getTimestampForV7, isValidUuid, uuidV7ToDate, -} from "./generate-uuid"; +} from './generate-uuid' -const MOCK_TIMESTAMP = 1627524478387; +const MOCK_TIMESTAMP = 1627524478387 // note dashes are seperators and not part of the uuid so the length is 36 including the dashes -describe("UUID Generation Functions", () => { - const MOCK_RANDOM = 0.123456789; +describe('UUID Generation Functions', () => { + const MOCK_RANDOM = 0.123456789 - test("getTimestampForV6 returns a predictable bigint", () => { + test('getTimestampForV6 returns a predictable bigint', () => { // Date.now = jest.fn(() => MOCK_TIMESTAMP); - const expectedTimestamp = BigInt(MOCK_TIMESTAMP) * BigInt(10000) + BigInt(0x01b21dd213814000); + const expectedTimestamp = BigInt(MOCK_TIMESTAMP) * BigInt(10000) + BigInt(0x01b21dd213814000) - const v6Timestamp = getTimestampForV6(MOCK_TIMESTAMP); + const v6Timestamp = getTimestampForV6(MOCK_TIMESTAMP) - expect(v6Timestamp).toBe(expectedTimestamp); - }); + expect(v6Timestamp).toBe(expectedTimestamp) + }) - test("getRandomClockSeq returns a predictable bigint", () => { - expect(getRandomClockSeq(1)).toBe(BigInt(0x3fff)); // If you expect the max value - }); + test('getRandomClockSeq returns a predictable bigint', () => { + expect(getRandomClockSeq(1)).toBe(BigInt(0x3fff)) // If you expect the max value + }) - test("getTimestampForV6 returns a bigint", () => { - expect(typeof getTimestampForV6(MOCK_TIMESTAMP)).toBe("bigint"); - }); + test('getTimestampForV6 returns a bigint', () => { + expect(typeof getTimestampForV6(MOCK_TIMESTAMP)).toBe('bigint') + }) - test("getTimestampForV7 returns a bigint", () => { - expect(typeof getTimestampForV7(MOCK_TIMESTAMP)).toBe("bigint"); - }); + test('getTimestampForV7 returns a bigint', () => { + expect(typeof getTimestampForV7(MOCK_TIMESTAMP)).toBe('bigint') + }) - test("getRandomClockSeq returns a bigint", () => { - expect(typeof getRandomClockSeq(MOCK_RANDOM)).toBe("bigint"); - }); + test('getRandomClockSeq returns a bigint', () => { + expect(typeof getRandomClockSeq(MOCK_RANDOM)).toBe('bigint') + }) - test("getRandomNode returns a bigint", () => { - expect(typeof getRandomNode()).toBe("bigint"); - }); + test('getRandomNode returns a bigint', () => { + expect(typeof getRandomNode()).toBe('bigint') + }) - test("getRandomValues returns a bigint", () => { - expect(typeof getRandomValues()).toBe("bigint"); - }); + test('getRandomValues returns a bigint', () => { + expect(typeof getRandomValues()).toBe('bigint') + }) // failing - test("formatTimeLow truncates correctly", () => { - expect(formatTimeLow(BigInt("0x123456789abcdef0"))).toBe("9abcdef0"); - }); + test('formatTimeLow truncates correctly', () => { + expect(formatTimeLow(BigInt('0x123456789abcdef0'))).toBe('9abcdef0') + }) - test("formatTimeLow pads correctly", () => { - expect(formatTimeLow(BigInt("0x1f"))).toBe("0000001f"); - }); + test('formatTimeLow pads correctly', () => { + expect(formatTimeLow(BigInt('0x1f'))).toBe('0000001f') + }) - test("formatTimeLow returns a string of length 8", () => { - expect(formatTimeLow(BigInt(12345678901234))).toHaveLength(8); - }); + test('formatTimeLow returns a string of length 8', () => { + expect(formatTimeLow(BigInt(12345678901234))).toHaveLength(8) + }) - test("formatTimeMid returns a string of length 4", () => { - expect(formatTimeMid(BigInt(12345678901234))).toHaveLength(4); - }); + test('formatTimeMid returns a string of length 4', () => { + expect(formatTimeMid(BigInt(12345678901234))).toHaveLength(4) + }) - test("formatTimeHighAndVersion returns a string of length 4", () => { - expect(formatTimeHighAndVersion(BigInt(12345678901234), BigInt(6))).toHaveLength(4); - }); + test('formatTimeHighAndVersion returns a string of length 4', () => { + expect(formatTimeHighAndVersion(BigInt(12345678901234), BigInt(6))).toHaveLength(4) + }) - test("formatClockSeq returns a string of length 4", () => { - expect(formatClockSeq(BigInt(12345678901234))).toHaveLength(4); - }); + test('formatClockSeq returns a string of length 4', () => { + expect(formatClockSeq(BigInt(12345678901234))).toHaveLength(4) + }) - test("formatNode returns a string of length 12", () => { - expect(formatNode(BigInt(12345678901234))).toHaveLength(12); - }); + test('formatNode returns a string of length 12', () => { + expect(formatNode(BigInt(12345678901234))).toHaveLength(12) + }) - test("generateUuidV6 returns a UUID string of length 36", () => { - expect(generateUuidV6()).toHaveLength(36); - }); + test('generateUuidV6 returns a UUID string of length 36', () => { + expect(generateUuidV6()).toHaveLength(36) + }) - test("generateUuidV7 returns a UUID string of length 36", () => { - expect(generateUuidV7()).toHaveLength(36); - }); + test('generateUuidV7 returns a UUID string of length 36', () => { + expect(generateUuidV7()).toHaveLength(36) + }) - test("generateUuidV8 returns a UUID string of length 36", () => { - const customData = [BigInt(0x123456789abc), BigInt(0x123), BigInt(0x3fffffffffffffff)]; - expect(generateUuidV8(customData)).toHaveLength(36); - }); + test('generateUuidV8 returns a UUID string of length 36', () => { + const customData = [BigInt(0x123456789abc), BigInt(0x123), BigInt(0x3fffffffffffffff)] + expect(generateUuidV8(customData)).toHaveLength(36) + }) - test("generateUuid returns a UUID string of length 36 for 6, 7, and 8", () => { - expect(generateUuid(6)).toHaveLength(36); - expect(generateUuid(7)).toHaveLength(36); - expect(generateUuid(8)).toHaveLength(36); - }); + test('generateUuid returns a UUID string of length 36 for 6, 7, and 8', () => { + expect(generateUuid(6)).toHaveLength(36) + expect(generateUuid(7)).toHaveLength(36) + expect(generateUuid(8)).toHaveLength(36) + }) - test("generateUuid returns a UUID string of length 36 with custom data", () => { - const customData: bigint[] = [BigInt(0x123456789abc), BigInt(0x123), BigInt(0x3fffffffff)]; + test('generateUuid returns a UUID string of length 36 with custom data', () => { + const customData: bigint[] = [BigInt(0x123456789abc), BigInt(0x123), BigInt(0x3fffffffff)] - expect(generateUuid(8, customData)).toHaveLength(36); - }); + expect(generateUuid(8, customData)).toHaveLength(36) + }) // test("generateUuid throws an error for invalid UUIDv8 input", () => { // expect(() => // generateUuid(8, [BigInt(0x123456789abc), BigInt(0x123)]) // ).toThrow("Invalid custom data for UUIDv8 must be 3 bigints"); // }); -}); +}) -test("isValidUuid returns true for valid UUIDs", () => { - const UUID = "5a65fa79-4004-4442-ac78-78dabbc58bdc"; - expect(isValidUuid(UUID)).toBeTruthy(); -}); +test('isValidUuid returns true for valid UUIDs', () => { + const UUID = '5a65fa79-4004-4442-ac78-78dabbc58bdc' + expect(isValidUuid(UUID)).toBeTruthy() +}) -test("UUIDv6 generation and extraction", () => { - const uuidV6 = generateUuidV6(); - expect(isValidUuid(uuidV6)).toBeTruthy(); +test('UUIDv6 generation and extraction', () => { + const uuidV6 = generateUuidV6() + expect(isValidUuid(uuidV6)).toBeTruthy() - const { timestamp, version } = extractTimestampFromUuidV6(uuidV6); - expect(timestamp).toBeDefined(); - expect(version).toBe(BigInt(6)); -}); + const { timestamp, version } = extractTimestampFromUuidV6(uuidV6) + expect(timestamp).toBeDefined() + expect(version).toBe(BigInt(6)) +}) // UUIDv7 Tests -describe("UUIDv7 generation", () => { - test("UUIDv7 generation and extraction", () => { - const uuidV7 = generateUuidV7({ dateTime: new Date(MOCK_TIMESTAMP) }); - expect(isValidUuid(uuidV7)).toBeTruthy(); +describe('UUIDv7 generation', () => { + test('UUIDv7 generation and extraction', () => { + const uuidV7 = generateUuidV7({ dateTime: new Date(MOCK_TIMESTAMP) }) + expect(isValidUuid(uuidV7)).toBeTruthy() - const { timestamp, version } = extractTimestampFromUuidV7(uuidV7); - expect(timestamp).toBeDefined(); - expect(version).toBe(BigInt(7)); - }); + const { timestamp, version } = extractTimestampFromUuidV7(uuidV7) + expect(timestamp).toBeDefined() + expect(version).toBe(BigInt(7)) + }) - test("UUIDV7 returns timestamp", () => { + test('UUIDV7 returns timestamp', () => { const [uuid, timestamp] = generateUuidV7({ dateTime: new Date(MOCK_TIMESTAMP), returnTimestamp: true, - }); + }) - expect(isValidUuid(uuid)).toBeTruthy(); + expect(isValidUuid(uuid)).toBeTruthy() - expect(timestamp).toEqual(new Date(MOCK_TIMESTAMP)); - }); + expect(timestamp).toEqual(new Date(MOCK_TIMESTAMP)) + }) - test("UUIDv7 can extract timestamp", () => { - const expectedTimestampDate = new Date(MOCK_TIMESTAMP); + test('UUIDv7 can extract timestamp', () => { + const expectedTimestampDate = new Date(MOCK_TIMESTAMP) - const uuidV7 = generateUuidV7({ dateTime: new Date(MOCK_TIMESTAMP) }); - expect(isValidUuid(uuidV7)).toBeTruthy(); + const uuidV7 = generateUuidV7({ dateTime: new Date(MOCK_TIMESTAMP) }) + expect(isValidUuid(uuidV7)).toBeTruthy() - const { timestamp } = extractTimestampFromUuidV7(uuidV7); - const receivedTimestampAsDate = new Date(Number(timestamp)); + const { timestamp } = extractTimestampFromUuidV7(uuidV7) + const receivedTimestampAsDate = new Date(Number(timestamp)) - expect(receivedTimestampAsDate).toEqual(expectedTimestampDate); + expect(receivedTimestampAsDate).toEqual(expectedTimestampDate) - expect(timestamp).toBeDefined(); - }); -}); + expect(timestamp).toBeDefined() + }) +}) -describe("uuidToDate", () => { - test("uuidToDate converts a valid UUIDv7 to the correct date", () => { - const dateNow = new Date(); - const uuidV7 = generateUuidV7({ dateTime: dateNow }); - expect(isValidUuid(uuidV7)).toBe(true); +describe('uuidToDate', () => { + test('uuidToDate converts a valid UUIDv7 to the correct date', () => { + const dateNow = new Date() + const uuidV7 = generateUuidV7({ dateTime: dateNow }) + expect(isValidUuid(uuidV7)).toBe(true) - const date = uuidV7ToDate(uuidV7); + const date = uuidV7ToDate(uuidV7) - expect(date).toEqual(dateNow); - }); + expect(date).toEqual(dateNow) + }) - test("uuidV7ToDate throws an error for an invalid UUID format", () => { - const invalidUuid = "12345"; + test('uuidV7ToDate throws an error for an invalid UUID format', () => { + const invalidUuid = '12345' // Check if the function throws the correct error // expect(() => uuidToDate(invalidUuid)).toR("Invalid UUID"); try { - uuidV7ToDate(invalidUuid); + uuidV7ToDate(invalidUuid) } catch (e) { if (e instanceof Error) { - expect(e.message).toBe("Invalid UUID: "); + expect(e.message).toBe('Invalid UUID: ') } } - }); + }) - test("uuidV7ToDate throws an error for a non-v7 UUID", () => { + test('uuidV7ToDate throws an error for a non-v7 UUID', () => { // Example of a v4 UUID. You should use a real UUIDv4 generator here. - const nonV7Uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"; + const nonV7Uuid = 'f47ac10b-58cc-4372-a567-0e02b2c3d479' // Check if the function throws the correct error try { - uuidV7ToDate(nonV7Uuid); + uuidV7ToDate(nonV7Uuid) } catch (e) { if (e instanceof Error) { - expect(e.message).toBe("Invalid UUID version"); + expect(e.message).toBe('Invalid UUID version') } } - }); -}); + }) +}) diff --git a/uuid/generate-uuid.ts b/uuid/generate-uuid.ts index 0d8eec0..f211db5 100644 --- a/uuid/generate-uuid.ts +++ b/uuid/generate-uuid.ts @@ -1,77 +1,77 @@ -import { randomBytes } from "crypto"; +import { randomBytes } from 'crypto' export function getTimestampForV6(timestamp?: number): bigint { - const dateUnixEpoch = timestamp || Date.now(); - return BigInt(dateUnixEpoch) * BigInt(10000) + BigInt(0x01b21dd213814000); + const dateUnixEpoch = timestamp || Date.now() + return BigInt(dateUnixEpoch) * BigInt(10000) + BigInt(0x01b21dd213814000) } export function getTimestampForV7(timestamp?: number): bigint { - const dateUnixEpoch = timestamp || Date.now(); - return BigInt(dateUnixEpoch); + const dateUnixEpoch = timestamp || Date.now() + return BigInt(dateUnixEpoch) } export function getRandomClockSeq(randomNum?: number): bigint { - return BigInt(Math.floor((randomNum ? randomNum : Math.random()) * 0x3fff)); + return BigInt(Math.floor((randomNum ? randomNum : Math.random()) * 0x3fff)) } export function getRandomNode(): bigint { - return BigInt(Math.floor(Math.random() * 0xffffffffffff)); + return BigInt(Math.floor(Math.random() * 0xffffffffffff)) } export function getRandomValues(): bigint { - return BigInt(Math.floor(Math.random() * 0x3fffffffffffffff)); + return BigInt(Math.floor(Math.random() * 0x3fffffffffffffff)) } export function formatTimeLow(timeLow: bigint): string { - return (timeLow & BigInt("0xFFFFFFFF")).toString(16).padStart(8, "0"); + return (timeLow & BigInt('0xFFFFFFFF')).toString(16).padStart(8, '0') } export function formatTimeMid(timeMid: bigint): string { - return (timeMid & 0xffffn).toString(16).padStart(4, "0"); + return (timeMid & 0xffffn).toString(16).padStart(4, '0') } export function formatTimeHighAndVersion(timeHi: bigint, version: bigint): string { - return (((timeHi << 4n) | version) & 0xffffn).toString(16).padStart(4, "0"); + return (((timeHi << 4n) | version) & 0xffffn).toString(16).padStart(4, '0') } export function formatClockSeq(clockSeq: bigint): string { - return ((clockSeq >> 8n) & 0xffn).toString(16).padStart(2, "0") + (clockSeq & 0xffn).toString(16).padStart(2, "0"); + return ((clockSeq >> 8n) & 0xffn).toString(16).padStart(2, '0') + (clockSeq & 0xffn).toString(16).padStart(2, '0') } export function formatNode(node: bigint): string { - return node.toString(16).padStart(12, "0"); + return node.toString(16).padStart(12, '0') } export function generateUuidV6(): string { - const timestamp = getTimestampForV6(); - const timeLow = (timestamp & BigInt(0xffffffff00000000)) >> 32n; - const timeMid = (timestamp & BigInt(0x00000000ffff0000)) >> 16n; - const timeHi = timestamp & BigInt(0x0000000000000fff); - const clockSeq = getRandomClockSeq(); - const node = getRandomNode(); + const timestamp = getTimestampForV6() + const timeLow = (timestamp & BigInt(0xffffffff00000000)) >> 32n + const timeMid = (timestamp & BigInt(0x00000000ffff0000)) >> 16n + const timeHi = timestamp & BigInt(0x0000000000000fff) + const clockSeq = getRandomClockSeq() + const node = getRandomNode() return ( formatTimeLow(timeLow) + - "-" + + '-' + formatTimeMid(timeMid) + - "-" + + '-' + formatTimeHighAndVersion(timeHi, 6n) + - "-" + + '-' + formatClockSeq(clockSeq) + - "-" + + '-' + formatNode(node) - ); + ) } type Version7Params = { - dateTime?: Date; - randA?: bigint; - randB?: bigint; - returnTimestamp?: RetTS; -}; + dateTime?: Date + randA?: bigint + randB?: bigint + returnTimestamp?: RetTS +} -type UUID = string; -type Timestamp = Date; -type Version7Return = [UUID, Timestamp]; +type UUID = string +type Timestamp = Date +type Version7Return = [UUID, Timestamp] export function generateUuidV7({ dateTime, @@ -79,23 +79,23 @@ export function generateUuidV7({ randB: inputRandB, returnTimestamp, }: Version7Params = {}): RetTS extends true ? Version7Return : string { - const selectedDate = dateTime || new Date(); + const selectedDate = dateTime || new Date() - const unixTsMs = BigInt(selectedDate.getTime()) & 0xffffffffffffn; // Ensure 48 bits - const version = 0x7; + const unixTsMs = BigInt(selectedDate.getTime()) & 0xffffffffffffn // Ensure 48 bits + const version = 0x7 - const randA = (inputRandA ?? BigInt(randomBytes(2).readUInt16BE(0))) & 0xfffn; // Ensure 12 bits - const varField = BigInt(0x2); - const randB = (inputRandB ?? BigInt(randomBytes(8).readBigUInt64BE(0))) & 0x3fffffffffffffffn; // Ensure 62 bits + const randA = (inputRandA ?? BigInt(randomBytes(2).readUInt16BE(0))) & 0xfffn // Ensure 12 bits + const varField = BigInt(0x2) + const randB = (inputRandB ?? BigInt(randomBytes(8).readBigUInt64BE(0))) & 0x3fffffffffffffffn // Ensure 62 bits - const unixTsMsBits = unixTsMs << BigInt(80); - const versionBits = BigInt(version) << BigInt(76); - const randABits = randA << BigInt(64); - const varBits = varField << BigInt(62); - const randBBits = randB; + const unixTsMsBits = unixTsMs << BigInt(80) + const versionBits = BigInt(version) << BigInt(76) + const randABits = randA << BigInt(64) + const varBits = varField << BigInt(62) + const randBBits = randB - const uuidBigInt = unixTsMsBits | versionBits | randABits | varBits | randBBits; - const uuidHex = uuidBigInt.toString(16).padStart(32, "0"); + const uuidBigInt = unixTsMsBits | versionBits | randABits | varBits | randBBits + const uuidHex = uuidBigInt.toString(16).padStart(32, '0') const uuid = [ uuidHex.slice(0, 8), @@ -103,117 +103,117 @@ export function generateUuidV7({ uuidHex.slice(12, 16), uuidHex.slice(16, 20), uuidHex.slice(20), - ].join("-"); + ].join('-') if (returnTimestamp) { // Explicitly assert the type of the returned object - return [uuid, selectedDate] as RetTS extends true ? Version7Return : string; + return [uuid, selectedDate] as RetTS extends true ? Version7Return : string } // Explicitly assert the type of the returned string - return uuid as RetTS extends true ? Version7Return : string; + return uuid as RetTS extends true ? Version7Return : string } export const uuidV7DT = () => { - return generateUuidV7({ returnTimestamp: true }); -}; + return generateUuidV7({ returnTimestamp: true }) +} // expect a string of length 36 (32 hexadecimal characters + 4 dashes): export function isValidUuid(uuid: string) { - const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; - return uuidRegex.test(uuid); + const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + return uuidRegex.test(uuid) } export const getUuidV7Date = (uuid: string) => { - const extracted = extractTimestampFromUuidV7(uuid); - const date = new Date(Number(extracted.timestamp)); - return date; -}; + const extracted = extractTimestampFromUuidV7(uuid) + const date = new Date(Number(extracted.timestamp)) + return date +} export function extractTimestampFromUuidV7(uuid: string): { - timestamp: BigInt; - version: BigInt; + timestamp: BigInt + version: BigInt } { - const uuidHex = uuid.replace(/-/g, ""); // Remove hyphens - const uuidBigInt = BigInt(`0x${uuidHex}`); + const uuidHex = uuid.replace(/-/g, '') // Remove hyphens + const uuidBigInt = BigInt(`0x${uuidHex}`) // Extracting the timestamp: it is the most significant 48 bits, so we shift right by (128 - 48) = 80 bits. - const timestamp = uuidBigInt >> BigInt(80); + const timestamp = uuidBigInt >> BigInt(80) // Extracting the version: it's 4 bits after the timestamp. First, we mask off the timestamp bits by doing a bitwise AND with a mask that is 1 for the version bits and 0 elsewhere. Then, we shift right by (128 - 48 - 4) = 76 bits. - const versionMask = BigInt(`0x${"0".repeat(12)}${"F".repeat(1)}${"0".repeat(19)}`); - const version = (uuidBigInt & versionMask) >> BigInt(76); + const versionMask = BigInt(`0x${'0'.repeat(12)}${'F'.repeat(1)}${'0'.repeat(19)}`) + const version = (uuidBigInt & versionMask) >> BigInt(76) - return { timestamp, version }; + return { timestamp, version } } export function extractRandomValuesFromUuidV7(uuid: string) { - const uuidWithoutHyphens = uuid.replace(/-/g, ""); - const randA = BigInt(`0x${uuidWithoutHyphens.slice(12, 15)}`); - const varField = BigInt(`0x${uuidWithoutHyphens.slice(15, 16)}`); - const randB = BigInt(`0x${uuidWithoutHyphens.slice(16)}`); - return { randA, varField, randB }; + const uuidWithoutHyphens = uuid.replace(/-/g, '') + const randA = BigInt(`0x${uuidWithoutHyphens.slice(12, 15)}`) + const varField = BigInt(`0x${uuidWithoutHyphens.slice(15, 16)}`) + const randB = BigInt(`0x${uuidWithoutHyphens.slice(16)}`) + return { randA, varField, randB } } export function extractTimestampFromUuidV6(uuid: string) { - const uuidWithoutHyphens = uuid.replace(/-/g, ""); - const timeHigh = BigInt(`0x${uuidWithoutHyphens.slice(0, 8)}`); - const timeMid = BigInt(`0x${uuidWithoutHyphens.slice(8, 12)}`); - const timeLow = BigInt(`0x${uuidWithoutHyphens.slice(12, 16)}`) >> 4n; - const version = BigInt(`0x${uuidWithoutHyphens.slice(15, 16)}`); + const uuidWithoutHyphens = uuid.replace(/-/g, '') + const timeHigh = BigInt(`0x${uuidWithoutHyphens.slice(0, 8)}`) + const timeMid = BigInt(`0x${uuidWithoutHyphens.slice(8, 12)}`) + const timeLow = BigInt(`0x${uuidWithoutHyphens.slice(12, 16)}`) >> 4n + const version = BigInt(`0x${uuidWithoutHyphens.slice(15, 16)}`) - const timestamp = (timeHigh << 32n) | (timeMid << 16n) | timeLow; + const timestamp = (timeHigh << 32n) | (timeMid << 16n) | timeLow - return { timestamp, version }; + return { timestamp, version } } export function extractClockSeqAndNodeFromUuidV6(uuid: string) { - const clockSeq = BigInt(`0x${uuid.slice(16, 20)}`); - const node = BigInt(`0x${uuid.slice(20)}`); + const clockSeq = BigInt(`0x${uuid.slice(16, 20)}`) + const node = BigInt(`0x${uuid.slice(20)}`) - return { clockSeq, node }; + return { clockSeq, node } } export function extractCustomDataFromUuidV8(uuid: string) { // Removing hyphens and converting UUID to a BigInt - const uuidWithoutHyphens = uuid.replace(/-/g, ""); - const uuidBigInt = BigInt(`0x${uuidWithoutHyphens}`); + const uuidWithoutHyphens = uuid.replace(/-/g, '') + const uuidBigInt = BigInt(`0x${uuidWithoutHyphens}`) // Extracting custom_a (48 bits from the start) - const customA = (uuidBigInt >> 80n) & 0xffffffffffffn; + const customA = (uuidBigInt >> 80n) & 0xffffffffffffn // Extracting ver (4 bits after custom_a) - const ver = (uuidBigInt >> 76n) & 0xfn; + const ver = (uuidBigInt >> 76n) & 0xfn // Extracting custom_b (12 bits after ver) - const customB = (uuidBigInt >> 64n) & 0xfffn; + const customB = (uuidBigInt >> 64n) & 0xfffn // Extracting var (2 bits after custom_b) - const varField = (uuidBigInt >> 62n) & 0x3n; + const varField = (uuidBigInt >> 62n) & 0x3n // Extracting custom_c (remaining 62 bits) - const customC = uuidBigInt & 0x3fffffffffffffffn; + const customC = uuidBigInt & 0x3fffffffffffffffn - return { customA, ver, customB, varField, customC }; + return { customA, ver, customB, varField, customC } } export function generateUuidV8(customData: bigint[] = [0n, 0n, 0n]): string { if (customData.length !== 3) { - console.error("Invalid custom data", customData); - throw new Error(`Invalid custom data for UUIDv8 must be 3 bigints`); + console.error('Invalid custom data', customData) + throw new Error(`Invalid custom data for UUIDv8 must be 3 bigints`) } - const version = 0x8n; - const variant = 0x2n; + const version = 0x8n + const variant = 0x2n - const custom_a = customData[0] & 0xffffffffffffn; - const ver = version & 0xfn; - const custom_b = customData[1] & 0xfffn; - const varField = variant & 0x3n; - const custom_c = customData[2] & 0x3fffffffffffffffn; + const custom_a = customData[0] & 0xffffffffffffn + const ver = version & 0xfn + const custom_b = customData[1] & 0xfffn + const varField = variant & 0x3n + const custom_c = customData[2] & 0x3fffffffffffffffn - const uuidBigInt = (custom_a << 80n) | (ver << 76n) | (custom_b << 64n) | (varField << 62n) | custom_c; + const uuidBigInt = (custom_a << 80n) | (ver << 76n) | (custom_b << 64n) | (varField << 62n) | custom_c - const uuidHex = uuidBigInt.toString(16).padStart(32, "0"); + const uuidHex = uuidBigInt.toString(16).padStart(32, '0') return [ uuidHex.slice(0, 8), @@ -221,32 +221,32 @@ export function generateUuidV8(customData: bigint[] = [0n, 0n, 0n]): string { uuidHex.slice(12, 16), uuidHex.slice(16, 20), uuidHex.slice(20), - ].join("-"); + ].join('-') } export const uuidV7ToDate = (uuid: string) => { - const validUuid = isValidUuid(uuid); + const validUuid = isValidUuid(uuid) if (!validUuid) { - throw new Error("Invalid UUID: "); + throw new Error('Invalid UUID: ') } - const { timestamp, version } = extractTimestampFromUuidV7(uuid); + const { timestamp, version } = extractTimestampFromUuidV7(uuid) if (version !== 7n) { - console.error("Invalid UUID version", version); - throw new Error("Invalid UUID version"); + console.error('Invalid UUID version', version) + throw new Error('Invalid UUID version') } - const date = new Date(Number(timestamp)); - return date; -}; + const date = new Date(Number(timestamp)) + return date +} -type UuidVersion = 6 | 7 | 8; +type UuidVersion = 6 | 7 | 8 export function generateUuid(version: Ver, customData?: bigint[]): string { if (version === 6) { - return generateUuidV6(); + return generateUuidV6() } else if (version === 7) { - return generateUuidV7(); + return generateUuidV7() } else if (version === 8) { - return generateUuidV8(customData); + return generateUuidV8(customData) } else { - throw new Error("Invalid version or custom data for UUIDv8"); + throw new Error('Invalid version or custom data for UUIDv8') } } diff --git a/uuid/index.ts b/uuid/index.ts index 92908a7..5a8f0e2 100644 --- a/uuid/index.ts +++ b/uuid/index.ts @@ -1,9 +1,4 @@ -export { - generateUuid as uuid, - generateUuidV6 as v6, - generateUuidV7 as v7, - generateUuidV8 as v8, -} from "./generate-uuid"; +export { generateUuid as uuid, generateUuidV6 as v6, generateUuidV7 as v7, generateUuidV8 as v8 } from './generate-uuid' export { extractClockSeqAndNodeFromUuidV6, @@ -15,4 +10,4 @@ export { isValidUuid, uuidV7ToDate, uuidV7DT, -} from "./generate-uuid"; +} from './generate-uuid' diff --git a/validation/index.ts b/validation/index.ts index b7aaf18..1df2f97 100644 --- a/validation/index.ts +++ b/validation/index.ts @@ -1 +1 @@ -export { createValidatorFactory } from "./validation-factory"; +export { createValidatorFactory } from './validation-factory' diff --git a/validation/validation-factory.ts b/validation/validation-factory.ts index 8ad0b9a..5d3469d 100644 --- a/validation/validation-factory.ts +++ b/validation/validation-factory.ts @@ -1,89 +1,89 @@ -import { ValidationResult } from "../types"; +import { ValidationResult } from '../types' -export type ErrorT = "ValidationError" | "APIError" | "JavaScriptError"; +export type ErrorT = 'ValidationError' | 'APIError' | 'JavaScriptError' export type CustomError = { - type: ErrorT; - message: string; -}; + type: ErrorT + message: string +} export const apiErrorMap = { - APIError: "API Error", - ValidationError: "Validation Error", - JavaScriptError: "JavaScript Error", -}; + APIError: 'API Error', + ValidationError: 'Validation Error', + JavaScriptError: 'JavaScript Error', +} function mapBuiltInErrorType(error: Error): ErrorT { if (error instanceof TypeError) { - return "JavaScriptError"; + return 'JavaScriptError' } else if (error instanceof ReferenceError) { - return "JavaScriptError"; + return 'JavaScriptError' } else if (error instanceof SyntaxError) { - return "JavaScriptError"; + return 'JavaScriptError' } else { - return "JavaScriptError"; + return 'JavaScriptError' } } export const getErrorType = (error: Error | CustomError): ErrorT => { - return "type" in error ? error.type : mapBuiltInErrorType(error); -}; + return 'type' in error ? error.type : mapBuiltInErrorType(error) +} export function handleError(error: Error | CustomError, throwError = false): CustomError | undefined { const handledError: CustomError = - "type" in error + 'type' in error ? error : { type: getErrorType(error), message: error.message, - }; + } if (throwError) { - throw handledError; + throw handledError } else { - return handledError; + return handledError } } export function createValidatorFactory>(schema: Schema) { - type SchemaKeys = keyof Schema; + type SchemaKeys = keyof Schema function validateItem(item: any): Schema { - if (typeof item !== "object" || item === null) { - throw handleError({ type: "ValidationError", message: "Invalid data type" }, true); + if (typeof item !== 'object' || item === null) { + throw handleError({ type: 'ValidationError', message: 'Invalid data type' }, true) } - const validateSchema: Schema = {} as Schema; + const validateSchema: Schema = {} as Schema const isValid = Object.keys(schema as Schema).every((key) => { - const typedKey: SchemaKeys = key as SchemaKeys; - const expectedType = schema[key]; - const actualType = typeof item[key]; + const typedKey: SchemaKeys = key as SchemaKeys + const expectedType = schema[key] + const actualType = typeof item[key] if (actualType !== expectedType) { - return false; + return false } - validateSchema[typedKey] = item[key] as Schema[keyof Schema]; - return true; - }); + validateSchema[typedKey] = item[key] as Schema[keyof Schema] + return true + }) if (!isValid) { - throw handleError({ type: "ValidationError", message: "Invalid data type" }, true); + throw handleError({ type: 'ValidationError', message: 'Invalid data type' }, true) } - return validateSchema; + return validateSchema } function validateAgainstArraySchema(schema: Schema, data: unknown[]): ValidationResult { try { - const validatedData = data.map((item) => validateItem(item)); - return { data: validatedData as Schema[] }; + const validatedData = data.map((item) => validateItem(item)) + return { data: validatedData as Schema[] } } catch (error) { - const handledError = handleError(error as Error); - if (handledError?.type === "ValidationError") { - return { error: handledError.message }; + const handledError = handleError(error as Error) + if (handledError?.type === 'ValidationError') { + return { error: handledError.message } } else { - throw error; + throw error } } } @@ -91,8 +91,8 @@ export function createValidatorFactory>(s return { validateAgainstArraySchema, validateItem, - }; + } } // One export per file -export default createValidatorFactory; +export default createValidatorFactory