diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..716ae5f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,48 @@ +name: Publish to JSR + +on: + push: + tags: + - "v*" + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write # Required for JSR provenance + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Verify version matches tag + run: | + TAG_VERSION="${GITHUB_REF#refs/tags/v}" + PACKAGE_VERSION=$(deno eval "const c = JSON.parse(await Deno.readTextFile('deno.jsonc').then(t => t.replace(/\/\/.*/g, ''))); console.log(c.version)") + if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then + echo "❌ Tag version ($TAG_VERSION) does not match package version ($PACKAGE_VERSION)" + exit 1 + fi + echo "✅ Version $TAG_VERSION matches" + + - name: Clone Drizzle shared test suite + run: make clone-repo + + - name: Run tests + run: deno task test + + - name: Show lockfile changes (if any) + run: git diff deno.lock || true + + - name: Restore lockfile (tests may modify it due to Deno version differences) + run: git checkout deno.lock + + - name: Publish to JSR + run: deno publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2fb9b8a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,88 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Check formatting + run: deno fmt --check + + - name: Lint + run: deno lint + + test-deno: + name: Test Deno ${{ matrix.deno-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + deno-version: ["2.6", "2.7.2"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: ${{ matrix.deno-version }} + + - name: Cache Deno dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/deno + ~/.deno + key: ${{ runner.os }}-deno-${{ matrix.deno-version }}-${{ hashFiles('deno.jsonc', 'deno.lock') }} + restore-keys: | + ${{ runner.os }}-deno-${{ matrix.deno-version }}- + + - name: Clone Drizzle shared test suite + run: make clone-repo + + - name: Install dependencies + run: deno install + + - name: Run tests + run: deno task test + + test-node: + name: Test Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["22", "24"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Clone Drizzle shared test suite + run: make clone-repo + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test diff --git a/Makefile b/Makefile index 9b478d8..57b5949 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -.PHONY: clone-repos clean-repos test test-node22 test-node24 test-deno build lint fmt check +.PHONY: clone-repo clean-repos test test-node22 test-node24 test-deno build lint fmt check # Clone Drizzle ORM repo for shared test suite -clone-repos: +clone-repo: @mkdir -p repos @if [ ! -d repos/drizzle-orm ]; then \ git clone --depth 1 https://github.com/drizzle-team/drizzle-orm.git repos/drizzle-orm; \ diff --git a/README.md b/README.md index 97f0d8d..444efff 100644 --- a/README.md +++ b/README.md @@ -138,13 +138,21 @@ This matches the sqlite-proxy specification. For most use cases, prefer the quer - Docker and Docker Compose - Make +### Local Setup + +```bash +# Enable pre-commit hooks (runs fmt/lint/check) +git config core.hooksPath .githooks && \ +chmod +x .githooks/pre-commit +``` + ### Running Tests Tests run in Docker containers to ensure consistent environments. ```bash # Clone Drizzle's shared test suite (required) -make clone-repos +make clone-repo # Run Deno 2.7.2 tests (default) make test-deno-2.7 diff --git a/REVIEW.md b/REVIEW.md deleted file mode 100644 index 5c08f34..0000000 --- a/REVIEW.md +++ /dev/null @@ -1,189 +0,0 @@ -# Code Review: drizzle-runtime-sqlite - -**Reviewer:** GitHub Copilot -**Date:** 2026-03-03 -**Files Reviewed:** `src/callback.ts`, `src/types.ts`, `mod.ts` - ---- - -## Summary - -This is a well-structured Drizzle ORM adapter bridging Deno's `node:sqlite` with the sqlite-proxy interface. The code is clean, documented, and handles version-specific Node.js APIs gracefully. There are several areas for improvement around type safety, error handling, and code organization. - -**Overall Assessment:** Good quality with minor issues - ---- - -## Strengths - -### 1. Clean Documentation -- Comprehensive JSDoc comments with examples -- Clear explanation of version-specific behavior (Node 22.16+/24+) -- Documented limitations (e.g., join column name collision) - -### 2. Good API Design -- Flexible `drizzle()` overloads supporting multiple call patterns -- Clean `createClient()` helper for direct access scenarios -- Proper re-exports for consumer convenience - -### 3. Feature Detection -- Graceful handling of `setReturnArrays` availability -- Fallback to object-to-array conversion for older Node versions - ---- - -## Issues - -### High Priority - -#### 1. Missing Error Handling -**Location:** [callback.ts](src/callback.ts#L46-L81) - -Database operations can throw errors (constraint violations, invalid SQL, etc.). The callbacks should wrap operations in try/catch and handle errors appropriately. - -```typescript -// Current: No error handling -return executeStatement(stmt, params, method); - -// Suggested: -try { - return executeStatement(stmt, params, method); -} catch (error) { - // Consider wrapping in a custom error type or re-throwing with context - throw error; -} -``` - -#### 2. Unnecessary Async/Await -**Location:** [callback.ts](src/callback.ts#L96-L116), [callback.ts](src/callback.ts#L133-L156) - -The callbacks are marked `async` but perform no asynchronous operations—`node:sqlite` is synchronous. This adds unnecessary Promise overhead. - -```typescript -// Current: -return async (sql, params, method): Promise => { - const stmt = db.prepare(sql); - // ...synchronous code... -}; - -// Note: May be intentional to match Drizzle's AsyncRemoteCallback signature -// Consider documenting that this is sync-under-async for compatibility -``` - -### Medium Priority - -#### 3. Code Duplication in Statement Options -**Location:** [callback.ts](src/callback.ts#L99-L110) and [callback.ts](src/callback.ts#L140-L151) - -Statement option application is duplicated between `createCallback` and `createBatchCallback`. - -```typescript -// Consider extracting: -function applyStatementOptions( - stmt: StatementSync, - options: SqliteOptions -): void { - if (options.readBigInts) { - stmt.setReadBigInts(true); - } - // ...etc -} -``` - -#### 4. Loose Typing in ProxyResult -**Location:** [types.ts](src/types.ts#L11-L13) - -```typescript -// Current: -export interface ProxyResult { - rows: any[] | undefined; -} - -// Suggested: -export interface ProxyResult { - rows: unknown[] | unknown[][] | undefined; -} -``` - -#### 5. Unused Type Definitions -**Location:** [types.ts](src/types.ts#L53-L68) - -`ColumnInfo`, `SQLInputValue`, and `SQLOutputValue` are defined but never imported or used. Either remove them or utilize them for better type safety in the callback implementation. - -### Low Priority - -#### 6. Type Assertion Chain -**Location:** [callback.ts](src/callback.ts#L33) - -```typescript -const extStmt = stmt as unknown as ExtendedStatementMethods; -``` - -This pattern appears multiple times. Consider a helper function: - -```typescript -function asExtended(stmt: StatementSync): ExtendedStatementMethods { - return stmt as unknown as ExtendedStatementMethods; -} -``` - -#### 7. Known Limitation with Join Column Names -**Location:** [callback.ts](src/callback.ts#L21-L23) - -The comment correctly documents this limitation: -```typescript -// Note: This doesn't work correctly for joins with duplicate column names. -``` - -This is acceptable for the fallback path, but consider: -- Adding a runtime warning when this scenario is detected -- Documenting this limitation in README.md - -#### 8. BatchItem Interface Location -**Location:** [callback.ts](src/callback.ts#L119-L123) - -`BatchItem` is defined in callback.ts but could live in types.ts with other interfaces for consistency. - ---- - -## Test Coverage Observations - -From [sqlite.test.ts](tests/node/sqlite.test.ts): - -- Good use of Drizzle's official shared test suite -- Proper feature detection for skippping tests -- Several tests skipped due to driver-specific behavior—consider documenting why in the skip list comments - -Tests skipped without array mode: -- `partial join with alias` -- `full join with alias` -- `select from alias` -- `join view as subquery` -- `cross join` - -These should be re-enabled automatically when running on Node 24+. - ---- - -## Security Considerations - -- No SQL injection risks—using parameterized statements correctly -- No credential handling -- File path handling relies on Node.js/Deno sandboxing - ---- - -## Recommendations - -1. **Add error handling** to callbacks with meaningful error messages -2. **Extract statement options application** to a shared helper function -3. **Document the sync-as-async pattern** if intentional for Drizzle compatibility -4. **Clean up unused types** or use them to strengthen type safety -5. **Move `BatchItem`** to types.ts for consistency -6. **Add README documentation** for known limitations with join columns - ---- - -## Conclusion - -The codebase is well-organized and follows good practices for a database adapter library. The main areas for improvement are error handling robustness and minor code organization. The version-detection strategy for Node.js API differences is particularly well done. diff --git a/deno.jsonc b/deno.jsonc index 5e99ad6..4b5cc59 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,5 @@ { - "$schema": "https://jsr.io/schema/config-file.v1.json", - "name": "@hotsauce/drizzle-deno-sqlite", + "name": "@hotsauce/drizzle-runtime-sqlite", "version": "0.1.0", "exports": "./mod.ts", "imports": { diff --git a/mod.ts b/mod.ts index decaeee..8a1f73a 100644 --- a/mod.ts +++ b/mod.ts @@ -22,15 +22,19 @@ import { DatabaseSync } from "node:sqlite"; import { drizzle as proxyDrizzle } from "drizzle-orm/sqlite-proxy"; import type { - SqliteRemoteDatabase, - AsyncRemoteCallback, AsyncBatchRemoteCallback, + AsyncRemoteCallback, + SqliteRemoteDatabase, } from "drizzle-orm/sqlite-proxy"; -import { createCallback, createBatchCallback } from "./src/callback.ts"; +import { createBatchCallback, createCallback } from "./src/callback.ts"; import type { DrizzleDenoSqliteConfig } from "./src/types.ts"; -export type { BatchItem, DrizzleDenoSqliteConfig, SqliteOptions } from "./src/types.ts"; -export { createCallback, createBatchCallback } from "./src/callback.ts"; +export type { + BatchItem, + DrizzleDenoSqliteConfig, + SqliteOptions, +} from "./src/types.ts"; +export { createBatchCallback, createCallback } from "./src/callback.ts"; /** * Create a Drizzle ORM instance backed by node:sqlite. @@ -71,7 +75,10 @@ export function drizzle< export function drizzle< TSchema extends Record = Record, >( - clientOrPathOrConfig?: DatabaseSync | string | DrizzleDenoSqliteConfig, + clientOrPathOrConfig?: + | DatabaseSync + | string + | DrizzleDenoSqliteConfig, maybeConfig?: DrizzleDenoSqliteConfig, ): SqliteRemoteDatabase { let client: DatabaseSync; diff --git a/src/callback.ts b/src/callback.ts index d358525..0f2bf57 100644 --- a/src/callback.ts +++ b/src/callback.ts @@ -4,8 +4,17 @@ * Clean room implementation based on Drizzle's sqlite-proxy contract. */ -import type { DatabaseSync, StatementSync, SupportedValueType } from "node:sqlite"; -import type { BatchItem, ProxyMethod, ProxyResult, SqliteOptions } from "./types.ts"; +import type { + DatabaseSync, + StatementSync, + SupportedValueType, +} from "node:sqlite"; +import type { + BatchItem, + ProxyMethod, + ProxyResult, + SqliteOptions, +} from "./types.ts"; /** * Extended StatementSync interface for newer Node.js APIs not yet in Deno's types. @@ -38,7 +47,9 @@ function applyStatementOptions( stmt.setAllowBareNamedParameters(options.allowBareNamedParameters); } if (options.allowUnknownNamedParameters !== undefined) { - asExtended(stmt).setAllowUnknownNamedParameters(options.allowUnknownNamedParameters); + asExtended(stmt).setAllowUnknownNamedParameters( + options.allowUnknownNamedParameters, + ); } } @@ -57,7 +68,8 @@ function rowToArray( * Check if the statement supports setReturnArrays (Node 22.16+/24+) */ function hasArrayMode(stmt: StatementSync): boolean { - return typeof (stmt as unknown as ExtendedStatementMethods).setReturnArrays === "function"; + return typeof (stmt as unknown as ExtendedStatementMethods) + .setReturnArrays === "function"; } /** @@ -88,7 +100,10 @@ function executeStatement( return { rows: [] }; } case "get": { - const row = stmt.get(...typedParams) as unknown[] | Record | undefined; + const row = stmt.get(...typedParams) as + | unknown[] + | Record + | undefined; if (row === undefined) { return { rows: undefined }; } @@ -102,7 +117,10 @@ function executeStatement( } case "all": case "values": { - const rows = stmt.all(...typedParams) as unknown[][] | Record[]; + const rows = stmt.all(...typedParams) as unknown[][] | Record< + string, + unknown + >[]; if (rows.length === 0) { return { rows: [] }; } @@ -112,7 +130,11 @@ function executeStatement( } // Convert objects to arrays using column metadata const columns = extStmt.columns(); - return { rows: (rows as Record[]).map((row) => rowToArray(row, columns)) }; + return { + rows: (rows as Record[]).map((row) => + rowToArray(row, columns) + ), + }; } default: { throw new Error(`Unknown method: ${method}`); diff --git a/src/types.ts b/src/types.ts index 3753572..fe84d63 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,5 +63,3 @@ export interface DrizzleDenoSqliteConfig< */ logger?: boolean; } - - diff --git a/tests/deno/common.ts b/tests/deno/common.ts index a9bd0bb..31286ed 100644 --- a/tests/deno/common.ts +++ b/tests/deno/common.ts @@ -2,12 +2,12 @@ * Common test utilities (from Drizzle's integration-tests/tests/common.ts) */ -import { beforeEach } from 'vitest'; +import { beforeEach } from "vitest"; export function skipTests(names: string[]) { - beforeEach((ctx) => { - if (ctx.task.suite?.name === 'common' && names.includes(ctx.task.name)) { - ctx.skip(); - } - }); + beforeEach((ctx) => { + if (ctx.task.suite?.name === "common" && names.includes(ctx.task.name)) { + ctx.skip(); + } + }); } diff --git a/tests/deno/sqlite.test.ts b/tests/deno/sqlite.test.ts index 8115f62..e38aea1 100644 --- a/tests/deno/sqlite.test.ts +++ b/tests/deno/sqlite.test.ts @@ -1,6 +1,6 @@ /** * Integration tests using Drizzle's official shared test suite. - * + * * Runs the same tests as Node but using Deno's built-in node:sqlite. */ @@ -9,7 +9,11 @@ import type { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy"; import { drizzle as proxyDrizzle } from "drizzle-orm/sqlite-proxy"; import { afterAll, beforeAll, beforeEach, expect, test } from "vitest"; import { skipTests } from "./common.ts"; -import { createCallback, createBatchCallback, createClient } from "../../mod.ts"; +import { + createBatchCallback, + createCallback, + createClient, +} from "../../mod.ts"; import { tests, usersTable, @@ -22,6 +26,7 @@ let client: ReturnType; function hasArrayMode(): boolean { const testClient = createClient(":memory:"); const stmt = testClient.db.prepare("SELECT 1"); + // deno-lint-ignore no-explicit-any const has = typeof (stmt as any).setReturnArrays === "function"; testClient.db.close(); return has; @@ -29,7 +34,7 @@ function hasArrayMode(): boolean { const supportsArrayMode = hasArrayMode(); -beforeAll(async () => { +beforeAll(() => { const dbPath = Deno.env.get("SQLITE_DB_PATH") ?? ":memory:"; client = createClient(dbPath); @@ -45,7 +50,7 @@ beforeEach((ctx) => { }; }); -afterAll(async () => { +afterAll(() => { client?.db.close(); }); @@ -70,7 +75,9 @@ const requiresArrayMode = [ "cross join", ]; -skipTests(supportsArrayMode ? alwaysSkip : [...alwaysSkip, ...requiresArrayMode]); +skipTests( + supportsArrayMode ? alwaysSkip : [...alwaysSkip, ...requiresArrayMode], +); tests(); @@ -89,14 +96,23 @@ beforeEach(async () => { }); test("insert via db.get w/ query builder", async () => { - const inserted = await db.get>( - db.insert(usersTable).values({ name: "John" }).returning({ id: usersTable.id, name: usersTable.name }), + const inserted = await db.get< + Pick + >( + db.insert(usersTable).values({ name: "John" }).returning({ + id: usersTable.id, + name: usersTable.name, + }), ); expect(inserted).toEqual([1, "John"]); }); test("insert via db.run + select via db.get", async () => { - await db.run(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${"John"})`); + await db.run( + sql`insert into ${usersTable} (${new Name( + usersTable.name.name, + )}) values (${"John"})`, + ); const result = await db.get<{ id: number; name: string }>( sql`select ${usersTable.id}, ${usersTable.name} from ${usersTable}`, @@ -114,7 +130,11 @@ test("insert via db.get", async () => { }); test("insert via db.run + select via db.all", async () => { - await db.run(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${"John"})`); + await db.run( + sql`insert into ${usersTable} (${new Name( + usersTable.name.name, + )}) values (${"John"})`, + ); const result = await db.all<{ id: number; name: string }>( sql`select ${usersTable.id}, ${usersTable.name} from ${usersTable}`, diff --git a/tests/deno/utils.ts b/tests/deno/utils.ts index 18db1b6..ba462e6 100644 --- a/tests/deno/utils.ts +++ b/tests/deno/utils.ts @@ -5,14 +5,18 @@ // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars export function Expect() {} -export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true - : false; +export type Equal = (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true + : false; export function toLocalDate(date: Date) { - const localTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000); - localTime.setUTCHours(0); - return localTime; + const localTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000); + localTime.setUTCHours(0); + return localTime; } export const randomString = () => - Array.from({ length: 10 }, () => 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)]).join(''); + Array.from( + { length: 10 }, + () => "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)], + ).join(""); diff --git a/tests/node/common.ts b/tests/node/common.ts index a9bd0bb..31286ed 100644 --- a/tests/node/common.ts +++ b/tests/node/common.ts @@ -2,12 +2,12 @@ * Common test utilities (from Drizzle's integration-tests/tests/common.ts) */ -import { beforeEach } from 'vitest'; +import { beforeEach } from "vitest"; export function skipTests(names: string[]) { - beforeEach((ctx) => { - if (ctx.task.suite?.name === 'common' && names.includes(ctx.task.name)) { - ctx.skip(); - } - }); + beforeEach((ctx) => { + if (ctx.task.suite?.name === "common" && names.includes(ctx.task.name)) { + ctx.skip(); + } + }); } diff --git a/tests/node/sqlite.test.ts b/tests/node/sqlite.test.ts index 6236eec..65c8d83 100644 --- a/tests/node/sqlite.test.ts +++ b/tests/node/sqlite.test.ts @@ -2,12 +2,17 @@ * Integration tests using Drizzle's official shared test suite. */ +import process from "node:process"; import { Name, sql } from "drizzle-orm"; import type { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy"; import { drizzle as proxyDrizzle } from "drizzle-orm/sqlite-proxy"; import { afterAll, beforeAll, beforeEach, expect, test } from "vitest"; import { skipTests } from "./common"; -import { createCallback, createBatchCallback, createClient } from "../../mod.ts"; +import { + createBatchCallback, + createCallback, + createClient, +} from "../../mod.ts"; import { tests, usersTable, @@ -20,6 +25,7 @@ let client: ReturnType; function hasArrayMode(): boolean { const testClient = createClient(":memory:"); const stmt = testClient.db.prepare("SELECT 1"); + // deno-lint-ignore no-explicit-any const has = typeof (stmt as any).setReturnArrays === "function"; testClient.db.close(); return has; @@ -27,7 +33,7 @@ function hasArrayMode(): boolean { const supportsArrayMode = hasArrayMode(); -beforeAll(async () => { +beforeAll(() => { const dbPath = process.env["SQLITE_DB_PATH"] ?? ":memory:"; client = createClient(dbPath); @@ -43,7 +49,7 @@ beforeEach((ctx) => { }; }); -afterAll(async () => { +afterAll(() => { client?.db.close(); }); @@ -68,7 +74,9 @@ const requiresArrayMode = [ "cross join", ]; -skipTests(supportsArrayMode ? alwaysSkip : [...alwaysSkip, ...requiresArrayMode]); +skipTests( + supportsArrayMode ? alwaysSkip : [...alwaysSkip, ...requiresArrayMode], +); tests(); @@ -87,14 +95,23 @@ beforeEach(async () => { }); test("insert via db.get w/ query builder", async () => { - const inserted = await db.get>( - db.insert(usersTable).values({ name: "John" }).returning({ id: usersTable.id, name: usersTable.name }), + const inserted = await db.get< + Pick + >( + db.insert(usersTable).values({ name: "John" }).returning({ + id: usersTable.id, + name: usersTable.name, + }), ); expect(inserted).toEqual([1, "John"]); }); test("insert via db.run + select via db.get", async () => { - await db.run(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${"John"})`); + await db.run( + sql`insert into ${usersTable} (${new Name( + usersTable.name.name, + )}) values (${"John"})`, + ); const result = await db.get<{ id: number; name: string }>( sql`select ${usersTable.id}, ${usersTable.name} from ${usersTable}`, @@ -112,7 +129,11 @@ test("insert via db.get", async () => { }); test("insert via db.run + select via db.all", async () => { - await db.run(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${"John"})`); + await db.run( + sql`insert into ${usersTable} (${new Name( + usersTable.name.name, + )}) values (${"John"})`, + ); const result = await db.all<{ id: number; name: string }>( sql`select ${usersTable.id}, ${usersTable.name} from ${usersTable}`, diff --git a/tests/node/utils.ts b/tests/node/utils.ts index 18db1b6..ba462e6 100644 --- a/tests/node/utils.ts +++ b/tests/node/utils.ts @@ -5,14 +5,18 @@ // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars export function Expect() {} -export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true - : false; +export type Equal = (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true + : false; export function toLocalDate(date: Date) { - const localTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000); - localTime.setUTCHours(0); - return localTime; + const localTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000); + localTime.setUTCHours(0); + return localTime; } export const randomString = () => - Array.from({ length: 10 }, () => 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)]).join(''); + Array.from( + { length: 10 }, + () => "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)], + ).join("");