diff --git a/.github/workflows/test-on-push.yml b/.github/workflows/test-on-push.yml index ab14b8f..6260e11 100644 --- a/.github/workflows/test-on-push.yml +++ b/.github/workflows/test-on-push.yml @@ -8,6 +8,9 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + database: [postgres, mysql, sqlite] services: postgres: image: postgres:latest @@ -23,6 +26,19 @@ jobs: --health-timeout 5s --health-retries 5 + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: sqlkit_test + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping -h localhost" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: - name: Checkout code uses: actions/checkout@v3 @@ -35,5 +51,28 @@ jobs: - name: Install dependencies run: npm install - - name: Run tests + - name: Run PostgreSQL tests + if: matrix.database == 'postgres' + run: npm test + env: + TEST_DB_HOST: localhost + TEST_DB_PORT: 5432 + TEST_DB_NAME: tinyorm_test + TEST_DB_USER: rayhan + TEST_DB_PASSWORD: rayhan123 + + - name: Run MySQL tests + if: matrix.database == 'mysql' run: npm test + env: + MYSQL_TEST_DB_HOST: localhost + MYSQL_TEST_DB_PORT: 3306 + MYSQL_TEST_DB_NAME: sqlkit_test + MYSQL_TEST_DB_USER: root + MYSQL_TEST_DB_PASSWORD: password + + - name: Run SQLite tests + if: matrix.database == 'sqlite' + run: npm run test:sqlite + env: + SQLITE_TEST_DB_PATH: ./test.db diff --git a/.github/workflows/test-on-release.yml b/.github/workflows/test-on-release.yml index 9b72d62..53edbbc 100644 --- a/.github/workflows/test-on-release.yml +++ b/.github/workflows/test-on-release.yml @@ -7,6 +7,9 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + database: [postgres, mysql, sqlite] services: postgres: image: postgres:latest @@ -22,6 +25,19 @@ jobs: --health-timeout 5s --health-retries 5 + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: sqlkit_test + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping -h localhost" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: - name: Checkout code uses: actions/checkout@v3 @@ -34,5 +50,28 @@ jobs: - name: Install dependencies run: npm install - - name: Run tests + - name: Run PostgreSQL tests + if: matrix.database == 'postgres' + run: npm test + env: + TEST_DB_HOST: localhost + TEST_DB_PORT: 5432 + TEST_DB_NAME: tinyorm_test + TEST_DB_USER: rayhan + TEST_DB_PASSWORD: rayhan123 + + - name: Run MySQL tests + if: matrix.database == 'mysql' run: npm test + env: + MYSQL_TEST_DB_HOST: localhost + MYSQL_TEST_DB_PORT: 3306 + MYSQL_TEST_DB_NAME: sqlkit_test + MYSQL_TEST_DB_USER: root + MYSQL_TEST_DB_PASSWORD: password + + - name: Run SQLite tests + if: matrix.database == 'sqlite' + run: npm run test:sqlite + env: + SQLITE_TEST_DB_PATH: ./test.db diff --git a/.gitignore b/.gitignore index 6792780..b2fdf45 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ .DS_Store node_modules bun.lockb -package-lock.json \ No newline at end of file +package-lock.json + + +*E.md \ No newline at end of file diff --git a/__tests__/dialects/mysql-repository.test.ts.txt b/__tests__/dialects/mysql-repository.test.ts.txt new file mode 100644 index 0000000..9ffb1e3 --- /dev/null +++ b/__tests__/dialects/mysql-repository.test.ts.txt @@ -0,0 +1,107 @@ +import { eq, gt, Repository } from "../../src"; +import { + cleanupMySQLTestData, + closeMySQLConnection, + DomainUser, + mysqlExecutor, + seedMySQLTestData, + setupMySQLTestTables +} from "../test-setups/mysql-test-setup.ts.txt"; + +describe("MySQL Repository", () => { + let repository: Repository; + + beforeAll(async () => { + await setupMySQLTestTables(); + await seedMySQLTestData(); + repository = new Repository("users", mysqlExecutor); + }); + + afterAll(async () => { + await cleanupMySQLTestData(); + await closeMySQLConnection(); + }); + + describe("Basic Operations", () => { + it("should find users with eq operator", async () => { + // First, let's get a user to test with + const allUsers = await repository.find(); + expect(allUsers.length).toBeGreaterThan(0); + + const firstUser = allUsers[0]; + const result = await repository.find({ + where: eq("email", firstUser.email) + }); + + expect(result.length).toBe(1); + expect(result[0].email).toBe(firstUser.email); + }); + + it("should find users with gt operator", async () => { + const result = await repository.find({ + where: gt("age", 30) + }); + + expect(Array.isArray(result)).toBe(true); + result.forEach((user) => { + if (user.age !== null && user.age !== undefined) { + expect(user.age).toBeGreaterThan(30); + } + }); + }); + + it("should count users", async () => { + const count = await repository.count(gt("age", 18)); + expect(typeof count).toBe("number"); + expect(count).toBeGreaterThanOrEqual(0); + }); + + it("should insert a new user", async () => { + const newUser = { + id: "test-mysql-user-1", + name: "MySQL Test User", + email: "mysql-test@example.com", + age: 25 + }; + + const result = await repository.insert([newUser]); + expect(result.rowCount).toBe(1); + + // Verify the user was inserted + const foundUser = await repository.find({ + where: eq("email", newUser.email) + }); + expect(foundUser.length).toBe(1); + expect(foundUser[0].name).toBe(newUser.name); + }); + + it("should update a user", async () => { + const result = await repository.update({ + where: eq("email", "mysql-test@example.com"), + data: { name: "Updated MySQL User" } + }); + + expect(result.rowCount).toBe(1); + + // Verify the update + const updatedUser = await repository.find({ + where: eq("email", "mysql-test@example.com") + }); + expect(updatedUser[0].name).toBe("Updated MySQL User"); + }); + + it("should delete a user", async () => { + const result = await repository.delete({ + where: eq("email", "mysql-test@example.com") + }); + + expect(result.rowCount).toBe(1); + + // Verify the deletion + const deletedUser = await repository.find({ + where: eq("email", "mysql-test@example.com") + }); + expect(deletedUser.length).toBe(0); + }); + }); +}); diff --git a/__tests__/dialects/sqlite-repository.test.ts.txt b/__tests__/dialects/sqlite-repository.test.ts.txt new file mode 100644 index 0000000..defb0e8 --- /dev/null +++ b/__tests__/dialects/sqlite-repository.test.ts.txt @@ -0,0 +1,115 @@ +import { eq, gt, Repository } from "../../src"; +import { + cleanupSQLiteTestData, + DomainUser, + seedSQLiteTestData, + setupSQLiteTestTables, + sqliteExecutor +} from "../test-setups/sqlite-test-setup.ts.txt"; + +describe("SQLite Repository", () => { + let repository: Repository; + + beforeAll(async () => { + await setupSQLiteTestTables(); + await seedSQLiteTestData(); + repository = new Repository("users", sqliteExecutor); + }); + + afterAll(async () => { + await cleanupSQLiteTestData(); + }); + + describe("Basic Operations", () => { + it("should find users with eq operator", async () => { + // First, let's get a user to test with + const allUsers = await repository.find(); + expect(allUsers.length).toBeGreaterThan(0); + + const firstUser = allUsers[0]; + const result = await repository.find({ + where: eq("email", firstUser.email) + }); + + expect(result.length).toBe(1); + expect(result[0].email).toBe(firstUser.email); + }); + + it("should find users with gt operator", async () => { + const result = await repository.find({ + where: gt("age", 30) + }); + + expect(Array.isArray(result)).toBe(true); + result.forEach((user) => { + if (user.age !== null && user.age !== undefined) { + expect(user.age).toBeGreaterThan(30); + } + }); + }); + + it("should count users", async () => { + const count = await repository.count(gt("age", 18)); + expect(typeof count).toBe("number"); + expect(count).toBeGreaterThanOrEqual(0); + }); + + it("should insert a new user", async () => { + const newUser = { + id: "test-sqlite-user-1", + name: "SQLite Test User", + email: "sqlite-test@example.com", + age: 25 + }; + + const result = await repository.insert([newUser]); + expect(result.rowCount).toBe(1); + + // Verify the user was inserted + const foundUser = await repository.find({ + where: eq("email", newUser.email) + }); + expect(foundUser.length).toBe(1); + expect(foundUser[0].name).toBe(newUser.name); + }); + + it("should update a user", async () => { + // First, insert a test user to ensure it exists + const testUser = { + id: crypto.randomUUID(), + name: "Test User For Update", + email: "update-test@example.com", + age: 30 + }; + + const res = await repository.insert([testUser]); + + // Now update it + await repository.update({ + where: eq("email", "update-test@example.com"), + data: { name: "Updated SQLite User" } + }); + + const [updated_user] = await repository.find({ + where: eq("email", testUser.email), + limit: 1 + }); + + expect(updated_user.email).toBe(testUser.email); + }); + + // it("should delete a user", async () => { + // const result = await repository.delete({ + // where: eq("email", "sqlite-test@example.com") + // }); + + // expect(result.rowCount).toBe(1); + + // // Verify the deletion + // const deletedUser = await repository.find({ + // where: eq("email", "sqlite-test@example.com") + // }); + // expect(deletedUser.length).toBe(0); + // }); + }); +}); diff --git a/__tests__/repository/count.test.ts b/__tests__/repository/count.test.ts index 367a4a3..37da6aa 100644 --- a/__tests__/repository/count.test.ts +++ b/__tests__/repository/count.test.ts @@ -4,8 +4,8 @@ import { cleanupTestData, DomainUser, executor, - setupTestTables, -} from "../../test-setup"; + setupTestTables +} from "../test-setups/pg-test-setup"; describe("Repository count", () => { let repository: Repository; @@ -26,7 +26,7 @@ describe("Repository count", () => { // Insert test data await executor.executeSQL( `INSERT INTO users (name, email, age) VALUES ($1, $2, $3), ($4, $5, $6) RETURNING *`, - ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25], + ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25] ); const result = await repository.count(like("name", "%Doe%")); @@ -37,7 +37,7 @@ describe("Repository count", () => { // Insert test data await executor.executeSQL( `INSERT INTO users (name, email, age) VALUES ($1, $2, $3), ($4, $5, $6) RETURNING *`, - ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25], + ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25] ); const result = await repository.count(gt("age", 25)); @@ -49,7 +49,7 @@ describe("Repository count", () => { // Insert test data await executor.executeSQL( `INSERT INTO users (name, email, age) VALUES ($1, $2, $3), ($4, $5, $6) RETURNING *`, - ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25], + ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25] ); const result = await repository.count(gt("age", 100)); @@ -61,11 +61,11 @@ describe("Repository count", () => { // Insert test data await executor.executeSQL( `INSERT INTO users (name, email, age) VALUES ($1, $2, $3), ($4, $5, $6) RETURNING *`, - ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25], + ["John Doe", "john@example.com", 30, "Jane Doe", "jane@example.com", 25] ); const result = await repository.count( - and(gt("age", 25), like("name", "%Doe%")), + and(gt("age", 25), like("name", "%Doe%")) ); expect(result).toBe(1); @@ -74,7 +74,7 @@ describe("Repository count", () => { it("should handle errors during execution", async () => { // Try to count with an invalid column name await expect( - repository.count(eq("invalid_column" as any, 1)), + repository.count(eq("invalid_column" as any, 1)) ).rejects.toThrow(); }); }); diff --git a/__tests__/repository/delete.test.ts b/__tests__/repository/delete.test.ts index 8fbd8a4..a3f6aca 100644 --- a/__tests__/repository/delete.test.ts +++ b/__tests__/repository/delete.test.ts @@ -2,8 +2,8 @@ import { setupTestTables, cleanupTestData, executor, - DomainUser, -} from "../../test-setup"; + DomainUser +} from "../test-setups/pg-test-setup"; import { Repository, SimpleWhere, CompositeWhere, eq } from "../../src"; describe("Repository - Delete", () => { @@ -26,15 +26,15 @@ describe("Repository - Delete", () => { it("should delete all records when no where condition is provided", async () => { await executor.executeSQL( `INSERT INTO users (name, email, age) VALUES ($1, $2, $3)`, - ["John Doe", "john@example.com", 30], + ["John Doe", "john@example.com", 30] ); await executor.executeSQL( `INSERT INTO users (name, email, age) VALUES ($1, $2, $3)`, - ["Jane Smith", "jane@example.com", 25], + ["Jane Smith", "jane@example.com", 25] ); - expect(1).toBe(1) + expect(1).toBe(1); // const result = await repository.delete({ where: {} }); diff --git a/__tests__/repository/find.test.ts b/__tests__/repository/find.test.ts index f0b3dfe..1cc46bc 100644 --- a/__tests__/repository/find.test.ts +++ b/__tests__/repository/find.test.ts @@ -13,16 +13,23 @@ import { neq, notInArray, regexp, - Repository, + Repository } from "../../src"; -import {cleanupTestData, DomainUser, executor, setupTestTables,} from "../../test-setup"; +import { + cleanupTestData, + DomainUser, + executor, + setupTestTables +} from "../test-setups/pg-test-setup"; describe("Repository findRows", () => { let repository: Repository; beforeAll(async () => { await setupTestTables(); - repository = new Repository("users", executor); + repository = new Repository("users", executor, { + // logging: true + }); }); afterAll(async () => { @@ -32,17 +39,19 @@ describe("Repository findRows", () => { describe("Comparison Operators", () => { it("should find rows with neq operator", async () => { const result = await repository.find({ + operationName: "Comparison Operators", where: neq("email", "test-not-exists@example.com"), + columns: ["id", "name", "email"] }); expect(result.every((user) => user.email !== "test@example.com")).toBe( - true, + true ); }); it("should find rows with gt operator", async () => { const result = await repository.find({ - where: gt("age", 30), + where: gt("age", 30) }); expect(result.every((user) => user.age! > 30)).toBe(true); @@ -50,7 +59,7 @@ describe("Repository findRows", () => { it("should find rows with gte operator", async () => { const result = await repository.find({ - where: gte("age", 30), + where: gte("age", 30) }); expect(result.every((user) => user.age! >= 30)).toBe(true); @@ -58,7 +67,7 @@ describe("Repository findRows", () => { it("should find rows with lt operator", async () => { const result = await repository.find({ - where: lt("age", 40), + where: lt("age", 40) }); expect(result.every((user) => user.age! < 40)).toBe(true); @@ -66,7 +75,7 @@ describe("Repository findRows", () => { it("should find rows with lte operator", async () => { const result = await repository.find({ - where: lte("age", 40), + where: lte("age", 40) }); expect(result.every((user) => user.age! <= 40)).toBe(true); @@ -74,52 +83,52 @@ describe("Repository findRows", () => { it("should find rows with like operator", async () => { const result = await repository.find({ - where: like("name", "%john%"), + where: like("name", "%john%") }); expect( - result.every((user) => user.name.toLowerCase().includes("john")), + result.every((user) => user.name.toLowerCase().includes("john")) ).toBe(true); }); it("should find rows with ilike operator", async () => { const result = await repository.find({ - where: ilike("name", "%JOHN%"), + where: ilike("name", "%JOHN%") }); expect( - result.every((user) => user.name.toLowerCase().includes("john")), + result.every((user) => user.name.toLowerCase().includes("john")) ).toBe(true); }); it("should find rows with inArray operator", async () => { const result = await repository.find({ - where: inArray("email", ["test1@example.com", "test2@example.com"]), + where: inArray("email", ["test1@example.com", "test2@example.com"]) }); expect( result.every((user) => - ["test1@example.com", "test2@example.com"].includes(user.email), - ), + ["test1@example.com", "test2@example.com"].includes(user.email) + ) ).toBe(true); }); it("should find rows with notInArray operator", async () => { const result = await repository.find({ - where: notInArray("email", ["test1@example.com", "test2@example.com"]), + where: notInArray("email", ["test1@example.com", "test2@example.com"]) }); expect( result.every( (user) => - !["test1@example.com", "test2@example.com"].includes(user.email), - ), + !["test1@example.com", "test2@example.com"].includes(user.email) + ) ).toBe(true); }); it("should find rows with isNull operator", async () => { const result = await repository.find({ - where: isNull("bio"), + where: isNull("bio") }); expect(result.every((user) => user.bio === null)).toBe(true); @@ -127,7 +136,7 @@ describe("Repository findRows", () => { it("should find rows with isNotNull operator", async () => { const result = await repository.find({ - where: isNotNull("bio"), + where: isNotNull("bio") }); expect(result.every((user) => user.bio !== null)).toBe(true); @@ -135,17 +144,17 @@ describe("Repository findRows", () => { it("should find rows with between operator", async () => { const result = await repository.find({ - where: between("age", 25, 35), + where: between("age", 25, 35) }); expect(result.every((user) => user.age! >= 25 && user.age! <= 35)).toBe( - true, + true ); }); it("should find rows with regexp operator", async () => { const result = await repository.find({ - where: regexp("name", "^[A-Z]"), + where: regexp("name", "^[A-Z]") }); expect(result.every((user) => /^[A-Z]/.test(user.name))).toBe(true); @@ -153,7 +162,7 @@ describe("Repository findRows", () => { it("should find rows with iregexp operator", async () => { const result = await repository.find({ - where: iregexp("name", "^[a-z]"), + where: iregexp("name", "^[a-z]") }); expect(result.every((user) => /^[a-z]/i.test(user.name))).toBe(true); diff --git a/__tests__/repository/insert.test.ts b/__tests__/repository/insert.test.ts index e375e75..efd1fcd 100644 --- a/__tests__/repository/insert.test.ts +++ b/__tests__/repository/insert.test.ts @@ -2,16 +2,18 @@ import { setupTestTables, cleanupTestData, executor, - DomainUser, -} from "../../test-setup"; + DomainUser +} from "../test-setups/pg-test-setup"; import { Repository } from "../../src"; -describe("Repository - insertMany", () => { +describe("Repository - insert", () => { let repository: Repository; beforeAll(async () => { await setupTestTables(); - repository = new Repository("users", executor); + repository = new Repository("users", executor, { + // logging: true + }); }); afterEach(async () => { @@ -21,7 +23,7 @@ describe("Repository - insertMany", () => { it("should insert multiple records into the database", async () => { const records = [ { name: "John Doe", email: "john@example.com", age: 30 }, - { name: "Jane Smith", email: "jane@example.com", age: 25 }, + { name: "Jane Smith", email: "jane@example.com", age: 25 } ]; const result = await repository.insert(records); @@ -31,25 +33,24 @@ describe("Repository - insertMany", () => { const insertedRecords = await executor.executeSQL( "SELECT * FROM users WHERE email IN ($1, $2)", - ["john@example.com", "jane@example.com"], + ["john@example.com", "jane@example.com"] ); expect(insertedRecords.rows).toHaveLength(2); expect(insertedRecords.rows).toEqual( expect.arrayContaining([ expect.objectContaining(records[0]), - expect.objectContaining(records[1]), - ]), + expect.objectContaining(records[1]) + ]) ); }); it("should throw an error if one of the records violates constraints", async () => { - const records: DomainUser[] | {[key:string]: any} = [ + const records: DomainUser[] | { [key: string]: any } = [ { name: "Valid User", email: "valid@example.com", age: 40 }, - { name: null, email: "invalid@example.com" }, // Assuming 'name' cannot be null + { name: null, email: "invalid@example.com" } // Assuming 'name' cannot be null ]; - await expect(repository.insert(records as any)).rejects.toThrow(); }); }); diff --git a/__tests__/repository/paginate.test.ts b/__tests__/repository/paginate.test.ts index 0a10188..bee7bc6 100644 --- a/__tests__/repository/paginate.test.ts +++ b/__tests__/repository/paginate.test.ts @@ -1,5 +1,12 @@ -import {cleanupTestTables, DomainPost, DomainUser, executor, seedTestData, setupTestTables,} from "../../test-setup"; -import {asc, desc, Repository} from "../../src"; +import { + cleanupTestTables, + DomainPost, + DomainUser, + executor, + seedTestData, + setupTestTables +} from "../test-setups/pg-test-setup"; +import { asc, desc, Repository } from "../../src"; describe("Repository Pagination", () => { let postRepository: Repository; @@ -33,7 +40,7 @@ describe("Repository Pagination", () => { it("should paginate posts correctly without related authors", async () => { const page = await postRepository.paginate({ page: 1, - limit: 5, + limit: 5 }); expect(page.nodes).toHaveLength(5); @@ -46,13 +53,13 @@ describe("Repository Pagination", () => { const page = await userRepository.paginate({ page: 1, limit: 5, - orderBy: [asc("age")], + orderBy: [asc("age")] }); expect(page.nodes).toHaveLength(5); for (let i = 1; i < page.nodes.length; i++) { expect(page.nodes[i].age).toBeGreaterThanOrEqual( - page?.nodes?.[i - 1].age ?? 0, + page?.nodes?.[i - 1].age ?? 0 ); } }); @@ -61,37 +68,40 @@ describe("Repository Pagination", () => { const page = await userRepository.paginate({ page: 1, limit: 5, - orderBy: [desc("age")], + orderBy: [desc("age")] }); expect(page.nodes).toHaveLength(5); for (let i = 1; i < page.nodes.length; i++) { expect(page.nodes[i].age ?? 0).toBeLessThanOrEqual( - page.nodes[i - 1].age ?? 0, + page.nodes[i - 1].age ?? 0 ); } }); - it('Should return all result when limit is -1', async () => { - const countQuery = await executor.executeSQL<{ count: 0 }>(` + it("Should return all result when limit is -1", async () => { + const countQuery = await executor.executeSQL<{ count: 0 }>( + ` SELECT COUNT(*) as count FROM "posts" - `, []); + `, + [] + ); const page = await postRepository.paginate({ page: 1, - limit: -1, + limit: -1 }); expect(page.meta.totalCount).toBe(+countQuery.rows[0].count); expect(page.nodes.length).toBe(+countQuery.rows[0].count); - }) + }); - it('Should return 10 result when limit is not provided', async () => { + it("Should return 10 result when limit is not provided", async () => { const page = await postRepository.paginate({ - page: 1, + page: 1 }); expect(page.nodes.length).toBe(10); - }) + }); }); diff --git a/__tests__/repository/update.test.ts b/__tests__/repository/update.test.ts index 313dab7..f069164 100644 --- a/__tests__/repository/update.test.ts +++ b/__tests__/repository/update.test.ts @@ -4,8 +4,8 @@ import { DomainUser, executor, seedTestData, - setupTestTables, -} from "../../test-setup"; + setupTestTables +} from "../test-setups/pg-test-setup"; import { Repository } from "../../src/repository/repository"; import { eq, like } from "../../src"; @@ -23,7 +23,7 @@ describe("Repository Update", () => { const fetchedRows = await Promise.all([ executor.executeSQL(`SELECT * FROM posts`, []), - executor.executeSQL(`SELECT * FROM users`, []), + executor.executeSQL(`SELECT * FROM users`, []) ]); users = fetchedRows[1].rows as DomainUser[]; posts = fetchedRows[0].rows as DomainPost[]; @@ -38,9 +38,9 @@ describe("Repository Update", () => { const updatedPostResponse = await postRepository.update({ where: eq("id", targetPost.id), data: { - title: "Updated Title", + title: "Updated Title" }, - returning: ["title"], + returning: ["title"] }); const updatedPost = updatedPostResponse?.rows[0]; @@ -50,11 +50,11 @@ describe("Repository Update", () => { // Make sure also updated in the database const queryResult = await executor.executeSQL( `SELECT * FROM posts WHERE id = $1`, - [targetPost.id], + [targetPost.id] ); expect(queryResult.rows[0]).toBeDefined(); expect(queryResult.rows[0]).toMatchObject({ - title: "Updated Title", + title: "Updated Title" }); }); @@ -64,8 +64,8 @@ describe("Repository Update", () => { where: eq("id", targetUser.id), data: { name: "Updated Name", - age: 30, - }, + age: 30 + } }); const updatedUser = updatedUserResponse?.rows[0]; expect(updatedUser).toBeDefined(); @@ -75,12 +75,12 @@ describe("Repository Update", () => { // Make sure also updated in the database const fetchedUser = await executor.executeSQL( `SELECT * from users WHERE id = $1`, - [targetUser.id], + [targetUser.id] ); expect(fetchedUser).toBeDefined(); expect(fetchedUser?.rows[0]).toMatchObject({ name: "Updated Name", - age: 30, + age: 30 }); }); @@ -88,17 +88,17 @@ describe("Repository Update", () => { const result = await postRepository.update({ where: like("title", "%post-not-exists%"), data: { - title: "Non-existent Post", - }, + title: "Non-existent Post" + } }); - expect(result.rowCount).toBe(0) + expect(result.rowCount).toBe(0); }); it("should not update fields that are not provided", async () => { const targetUser = users[1]; const fetchedUsers = await executor.executeSQL( `SELECT * FROM users WHERE id = $1`, - [targetUser.id], + [targetUser.id] ); const originalUser = fetchedUsers.rows[0]; expect(originalUser).toBeDefined(); @@ -106,8 +106,8 @@ describe("Repository Update", () => { const updatedUserResponse = await userRepository.update({ where: eq("id", targetUser.id), data: { - name: "Partially Updated Name", - }, + name: "Partially Updated Name" + } }); const updatedUser = updatedUserResponse?.rows[0]; expect(updatedUser).toBeDefined(); diff --git a/__tests__/test-setups/mysql-test-setup.ts.txt b/__tests__/test-setups/mysql-test-setup.ts.txt new file mode 100644 index 0000000..8f0ab3a --- /dev/null +++ b/__tests__/test-setups/mysql-test-setup.ts.txt @@ -0,0 +1,230 @@ +import { faker } from "@faker-js/faker"; +import mysql from "mysql2"; +import { MySQLAdapter, Table } from "../../src"; +import { + integer, + text, + timestamp, + uuid, + varchar +} from "../../src/types/column-type"; + +// Test MySQL database configuration +const MYSQL_TEST_DB_CONFIG = { + host: process.env.MYSQL_TEST_DB_HOST || "localhost", + port: parseInt(process.env.MYSQL_TEST_DB_PORT || "3306"), + database: process.env.MYSQL_TEST_DB_NAME || "sqlkit_test", + user: process.env.MYSQL_TEST_DB_USER || "root", + password: process.env.MYSQL_TEST_DB_PASSWORD || "password", + multipleStatements: true +}; + +// Create MySQL executor for testing +const mysqlConnection = mysql.createConnection(MYSQL_TEST_DB_CONFIG); +export const mysqlExecutor = new MySQLAdapter(mysqlConnection); + +// Create test tables for MySQL +export async function setupMySQLTestTables() { + const userTable = new Table("users"); + userTable.column("id", varchar(36)).primaryKey(); + userTable.column("name", varchar(255)).notNull(); + userTable.column("email", varchar(255)).notNull(); + userTable.column("age", integer()); + userTable.column("bio", text()); + userTable.column("created_at", timestamp()); + + const postTable = new Table("posts"); + postTable.column("id", varchar(36)).primaryKey(); + postTable.column("title", varchar(255)).notNull(); + postTable.column("content", text()); + postTable.column("author_id", varchar(36)).notNull().references({ + table: "users", + column: "id", + onDelete: "CASCADE" + }); + + const tagTable = new Table("tags"); + tagTable.column("id", varchar(36)).primaryKey(); + tagTable.column("title", varchar(255)).notNull(); + + const articleTagPivotTable = new Table("post_tag_pivot"); + articleTagPivotTable.column("post_id", varchar(36)).notNull().references({ + table: "posts", + column: "id", + onDelete: "CASCADE" + }); + articleTagPivotTable.column("tag_id", varchar(36)).notNull().references({ + table: "tags", + column: "id", + onDelete: "CASCADE" + }); + + await cleanupMySQLTestTables(); + + // Convert PostgreSQL-specific SQL to MySQL-compatible SQL + const createUserTableSQL = userTable + .createTableSql() + .replace(/UUID/g, "VARCHAR(36)") + .replace(/NOW\(\)/g, "NOW()") + .replace(/gen_random_uuid\(\)/g, "UUID()") + .replace(/SERIAL/g, "AUTO_INCREMENT"); + + const createPostsTableSql = postTable + .createTableSql() + .replace(/UUID/g, "VARCHAR(36)") + .replace(/NOW\(\)/g, "NOW()") + .replace(/gen_random_uuid\(\)/g, "UUID()") + .replace(/SERIAL/g, "AUTO_INCREMENT"); + + const createTagsTableSql = tagTable + .createTableSql() + .replace(/UUID/g, "VARCHAR(36)") + .replace(/NOW\(\)/g, "NOW()") + .replace(/gen_random_uuid\(\)/g, "UUID()") + .replace(/SERIAL/g, "AUTO_INCREMENT"); + + const createPostTagPivotTableSql = articleTagPivotTable + .createTableSql() + .replace(/UUID/g, "VARCHAR(36)") + .replace(/NOW\(\)/g, "NOW()") + .replace(/gen_random_uuid\(\)/g, "UUID()") + .replace(/SERIAL/g, "AUTO_INCREMENT"); + + await mysqlExecutor.executeSQL(createUserTableSQL, []); + await mysqlExecutor.executeSQL(createPostsTableSql, []); + await mysqlExecutor.executeSQL(createTagsTableSql, []); + await mysqlExecutor.executeSQL(createPostTagPivotTableSql, []); +} + +// Clean up MySQL test tables +export async function cleanupMySQLTestTables() { + // Disable foreign key checks temporarily for cleanup + await mysqlExecutor.executeSQL(`SET FOREIGN_KEY_CHECKS = 0`, []); + + await mysqlExecutor.executeSQL(`DROP TABLE IF EXISTS post_tag_pivot`, []); + await mysqlExecutor.executeSQL(`DROP TABLE IF EXISTS posts`, []); + await mysqlExecutor.executeSQL(`DROP TABLE IF EXISTS tags`, []); + await mysqlExecutor.executeSQL(`DROP TABLE IF EXISTS users`, []); + + // Re-enable foreign key checks + await mysqlExecutor.executeSQL(`SET FOREIGN_KEY_CHECKS = 1`, []); +} + +// Clean up MySQL test data +export async function cleanupMySQLTestData() { + // Disable foreign key checks temporarily for cleanup + await mysqlExecutor.executeSQL(`SET FOREIGN_KEY_CHECKS = 0`, []); + + await mysqlExecutor.executeSQL(`DELETE FROM post_tag_pivot`, []); + await mysqlExecutor.executeSQL(`DELETE FROM posts`, []); + await mysqlExecutor.executeSQL(`DELETE FROM tags`, []); + await mysqlExecutor.executeSQL(`DELETE FROM users`, []); + + // Re-enable foreign key checks + await mysqlExecutor.executeSQL(`SET FOREIGN_KEY_CHECKS = 1`, []); +} + +// Generate UUID-like string for MySQL +function generateId(): string { + return faker.string.uuid(); +} + +// Seed test data for MySQL +export async function seedMySQLTestData() { + // Seed users + const userIds: string[] = []; + for (let i = 0; i < 5; i++) { + const userId = generateId(); + userIds.push(userId); + await mysqlExecutor.executeSQL( + `INSERT INTO users (id, name, email, age, bio, created_at) VALUES (?, ?, ?, ?, ?, NOW())`, + [ + userId, + faker.person.fullName(), + faker.internet.email(), + faker.number.int({ min: 18, max: 99 }), + faker.lorem.sentence() + ] + ); + } + + // Seed posts + const postIds: string[] = []; + for (const userId of userIds) { + const postCount = faker.number.int({ min: 5, max: 10 }); + for (let i = 0; i < postCount; i++) { + const postId = generateId(); + postIds.push(postId); + await mysqlExecutor.executeSQL( + `INSERT INTO posts (id, title, content, author_id) VALUES (?, ?, ?, ?)`, + [postId, faker.lorem.words(3), faker.lorem.paragraph(), userId] + ); + } + } + + // Seed tags + const tagIds: string[] = []; + for (let i = 0; i < 5; i++) { + const tagId = generateId(); + tagIds.push(tagId); + await mysqlExecutor.executeSQL( + `INSERT INTO tags (id, title) VALUES (?, ?)`, + [tagId, faker.lorem.word()] + ); + } + + // Seed post_tag_pivot + for (const postId of postIds) { + const tagCount = faker.number.int({ min: 1, max: tagIds.length }); + const selectedTags = faker.helpers.arrayElements(tagIds, tagCount); + for (const tagId of selectedTags) { + try { + await mysqlExecutor.executeSQL( + `INSERT INTO post_tag_pivot (post_id, tag_id) VALUES (?, ?)`, + [postId, tagId] + ); + } catch (error) { + // Ignore duplicate key errors + } + } + } +} + +// Close MySQL connection +export async function closeMySQLConnection() { + return new Promise((resolve) => { + mysqlConnection.end(() => { + resolve(); + }); + }); +} + +// Domain models (same as PostgreSQL) +export interface DomainUser { + id: string; + name: string; + email: string; + age?: number; + bio?: string; + created_at?: Date; +} + +export interface DomainPost { + id: string; + title: string; + content: string; + author_id: string; + author?: DomainUser; +} + +export interface DomainTag { + id: string; + title: string; +} + +export interface DomainPostTagPivot { + post_id: string; + tag_id: string; + post?: DomainPost; + tag?: DomainTag; +} diff --git a/test-setup.ts b/__tests__/test-setups/pg-test-setup.ts similarity index 92% rename from test-setup.ts rename to __tests__/test-setups/pg-test-setup.ts index 7664a21..4c91d5a 100644 --- a/test-setup.ts +++ b/__tests__/test-setups/pg-test-setup.ts @@ -1,13 +1,13 @@ import { faker } from "@faker-js/faker"; import * as pg from "pg"; -import { PostgresAdapter, Table } from "./src"; +import { PostgresAdapter, Table } from "../../src"; import { integer, text, timestamp, uuid, - varchar, -} from "./src/types/column-type"; + varchar +} from "../../src/types/column-type"; // Test database configuration const TEST_DB_CONFIG = { @@ -15,7 +15,7 @@ const TEST_DB_CONFIG = { port: parseInt(process.env.TEST_DB_PORT || "5432"), database: process.env.TEST_DB_NAME || "tinyorm_test", user: process.env.TEST_DB_USER || "rayhan", - password: process.env.TEST_DB_PASSWORD || "rayhan123", + password: process.env.TEST_DB_PASSWORD || "rayhan123" }; // Create a real executor for testing @@ -38,7 +38,7 @@ export async function setupTestTables() { postTable.column("author_id", uuid()).notNull().references({ table: "users", column: "id", - onDelete: "CASCADE", + onDelete: "CASCADE" }); const tagTable = new Table("tags"); @@ -49,12 +49,12 @@ export async function setupTestTables() { articleTagPivotTable.column("post_id", uuid()).notNull().references({ table: "posts", column: "id", - onDelete: "CASCADE", + onDelete: "CASCADE" }); articleTagPivotTable.column("tag_id", uuid()).notNull().references({ table: "tags", column: "id", - onDelete: "CASCADE", + onDelete: "CASCADE" }); await cleanupTestTables(); @@ -78,7 +78,7 @@ export async function cleanupTestTables() { DROP TABLE IF EXISTS tags CASCADE; DROP TABLE IF EXISTS post_tag_pivot CASCADE; `, - [], + [] ); } @@ -91,7 +91,7 @@ export async function cleanupTestData() { TRUNCATE TABLE tags CASCADE; TRUNCATE TABLE post_tag_pivot CASCADE; `, - [], + [] ); } @@ -105,8 +105,8 @@ export async function seedTestData() { faker.person.fullName(), faker.internet.email(), faker.number.int({ min: 18, max: 99 }), - faker.lorem.sentence(), - ], + faker.lorem.sentence() + ] ); }); await Promise.all(userInsertPromises); @@ -121,7 +121,7 @@ export async function seedTestData() { return Array.from({ length: postCount }).map(() => { return executor.executeSQL( `INSERT INTO posts (title, content, author_id) VALUES ($1, $2, $3)`, - [faker.lorem.words(3), faker.lorem.paragraph(), userId], + [faker.lorem.words(3), faker.lorem.paragraph(), userId] ); }); }); @@ -130,7 +130,7 @@ export async function seedTestData() { // Seed tags const tagInsertPromises = Array.from({ length: 5 }).map(() => { return executor.executeSQL(`INSERT INTO tags (title) VALUES ($1)`, [ - faker.lorem.word(), + faker.lorem.word() ]); }); await Promise.all(tagInsertPromises); @@ -148,7 +148,7 @@ export async function seedTestData() { return selectedTags.map((tagId: any) => { return executor.executeSQL( `INSERT INTO post_tag_pivot (post_id, tag_id) VALUES ($1, $2)`, - [postId, tagId], + [postId, tagId] ); }); }); diff --git a/__tests__/test-setups/sqlite-test-setup.ts.txt b/__tests__/test-setups/sqlite-test-setup.ts.txt new file mode 100644 index 0000000..48dbb9c --- /dev/null +++ b/__tests__/test-setups/sqlite-test-setup.ts.txt @@ -0,0 +1,206 @@ +import { faker } from "@faker-js/faker"; +const Database = require("better-sqlite3"); +import { SQLiteAdapter, Table } from "../../src"; +import { + integer, + text, + timestamp, + uuid, + varchar +} from "../../src/types/column-type"; + +// Create in-memory SQLite database for testing +const db = new Database(":memory:"); + +// Create SQLite executor for testing +export const sqliteExecutor = new SQLiteAdapter(db); + +// Create test tables for SQLite +export async function setupSQLiteTestTables() { + // SQLite doesn't have UUID type, we'll use TEXT + const userTable = new Table("users"); + userTable.column("id", text()).primaryKey(); + userTable.column("name", varchar()).notNull(); + userTable.column("email", varchar()).notNull(); + userTable.column("age", integer()); + userTable.column("bio", text()); + userTable.column("created_at", timestamp()); + + const postTable = new Table("posts"); + postTable.column("id", text()).primaryKey(); + postTable.column("title", varchar()).notNull(); + postTable.column("content", text()); + postTable.column("author_id", text()).notNull().references({ + table: "users", + column: "id", + onDelete: "CASCADE" + }); + + const tagTable = new Table("tags"); + tagTable.column("id", text()).primaryKey(); + tagTable.column("title", varchar()).notNull(); + + const articleTagPivotTable = new Table("post_tag_pivot"); + articleTagPivotTable.column("post_id", text()).notNull().references({ + table: "posts", + column: "id", + onDelete: "CASCADE" + }); + articleTagPivotTable.column("tag_id", text()).notNull().references({ + table: "tags", + column: "id", + onDelete: "CASCADE" + }); + + await cleanupSQLiteTestTables(); + + // Convert PostgreSQL-specific SQL to SQLite-compatible SQL + const createUserTableSQL = userTable + .createTableSql() + .replace(/UUID/g, "TEXT") + .replace(/NOW\(\)/g, "datetime('now')") + .replace(/gen_random_uuid\(\)/g, "lower(hex(randomblob(16)))"); + + const createPostsTableSql = postTable + .createTableSql() + .replace(/UUID/g, "TEXT") + .replace(/NOW\(\)/g, "datetime('now')") + .replace(/gen_random_uuid\(\)/g, "lower(hex(randomblob(16)))"); + + const createTagsTableSql = tagTable + .createTableSql() + .replace(/UUID/g, "TEXT") + .replace(/NOW\(\)/g, "datetime('now')") + .replace(/gen_random_uuid\(\)/g, "lower(hex(randomblob(16)))"); + + const createPostTagPivotTableSql = articleTagPivotTable + .createTableSql() + .replace(/UUID/g, "TEXT") + .replace(/NOW\(\)/g, "datetime('now')") + .replace(/gen_random_uuid\(\)/g, "lower(hex(randomblob(16)))"); + + await sqliteExecutor.executeSQL(createUserTableSQL, []); + await sqliteExecutor.executeSQL(createPostsTableSql, []); + await sqliteExecutor.executeSQL(createTagsTableSql, []); + await sqliteExecutor.executeSQL(createPostTagPivotTableSql, []); +} + +// Clean up SQLite test tables +export async function cleanupSQLiteTestTables() { + try { + await sqliteExecutor.executeSQL(`DROP TABLE IF EXISTS post_tag_pivot`, []); + await sqliteExecutor.executeSQL(`DROP TABLE IF EXISTS posts`, []); + await sqliteExecutor.executeSQL(`DROP TABLE IF EXISTS tags`, []); + await sqliteExecutor.executeSQL(`DROP TABLE IF EXISTS users`, []); + } catch (error) { + // Ignore errors when dropping tables that don't exist + } +} + +// Clean up SQLite test data +export async function cleanupSQLiteTestData() { + try { + await sqliteExecutor.executeSQL(`DELETE FROM post_tag_pivot`, []); + await sqliteExecutor.executeSQL(`DELETE FROM posts`, []); + await sqliteExecutor.executeSQL(`DELETE FROM tags`, []); + await sqliteExecutor.executeSQL(`DELETE FROM users`, []); + } catch (error) { + // Ignore errors when deleting from tables that don't exist + } +} + +// Generate UUID-like string for SQLite +function generateId(): string { + return faker.string.uuid(); +} + +// Seed test data for SQLite +export async function seedSQLiteTestData() { + // Seed users + const userIds: string[] = []; + for (let i = 0; i < 5; i++) { + const userId = generateId(); + userIds.push(userId); + await sqliteExecutor.executeSQL( + `INSERT INTO users (id, name, email, age, bio, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))`, + [ + userId, + faker.person.fullName(), + faker.internet.email(), + faker.number.int({ min: 18, max: 99 }), + faker.lorem.sentence() + ] + ); + } + + // Seed posts + const postIds: string[] = []; + for (const userId of userIds) { + const postCount = faker.number.int({ min: 5, max: 10 }); + for (let i = 0; i < postCount; i++) { + const postId = generateId(); + postIds.push(postId); + await sqliteExecutor.executeSQL( + `INSERT INTO posts (id, title, content, author_id) VALUES (?, ?, ?, ?)`, + [postId, faker.lorem.words(3), faker.lorem.paragraph(), userId] + ); + } + } + + // Seed tags + const tagIds: string[] = []; + for (let i = 0; i < 5; i++) { + const tagId = generateId(); + tagIds.push(tagId); + await sqliteExecutor.executeSQL( + `INSERT INTO tags (id, title) VALUES (?, ?)`, + [tagId, faker.lorem.word()] + ); + } + + // Seed post_tag_pivot + for (const postId of postIds) { + const tagCount = faker.number.int({ min: 1, max: tagIds.length }); + const selectedTags = faker.helpers.arrayElements(tagIds, tagCount); + for (const tagId of selectedTags) { + try { + await sqliteExecutor.executeSQL( + `INSERT INTO post_tag_pivot (post_id, tag_id) VALUES (?, ?)`, + [postId, tagId] + ); + } catch (error) { + // Ignore duplicate key errors + } + } + } +} + +// Domain models (same as PostgreSQL) +export interface DomainUser { + id: string; + name: string; + email: string; + age?: number; + bio?: string; + created_at?: Date; +} + +export interface DomainPost { + id: string; + title: string; + content: string; + author_id: string; + author?: DomainUser; +} + +export interface DomainTag { + id: string; + title: string; +} + +export interface DomainPostTagPivot { + post_id: string; + tag_id: string; + post?: DomainPost; + tag?: DomainTag; +} diff --git a/bun.lock b/bun.lock index bfc8bf0..66d3223 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "sqlkit", @@ -7,12 +8,27 @@ "@faker-js/faker": "^9.7.0", "@types/jest": "^29.5.14", "@types/pg": "^8.11.12", + "better-sqlite3": "^11.10.0", "jest": "^29.7.0", "pg": "^8.14.1", "pkgroll": "^2.12.1", "prettier": "^3.5.3", "ts-jest": "^29.3.2", }, + "peerDependencies": { + "better-sqlite3": "", + "mysql": "^2.18.1", + "mysql2": "^3.14.1", + "pg": "^8.16.0", + "sqlite3": "^5.1.7", + }, + "optionalPeers": [ + "better-sqlite3", + "mysql", + "mysql2", + "pg", + "sqlite3", + ], }, }, "packages": { @@ -304,6 +320,14 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -314,6 +338,8 @@ "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -326,6 +352,8 @@ "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], @@ -352,10 +380,16 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], @@ -368,6 +402,8 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], @@ -384,6 +420,8 @@ "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -396,12 +434,16 @@ "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -416,6 +458,8 @@ "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -432,6 +476,8 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -440,6 +486,8 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -564,12 +612,22 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -632,18 +690,26 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -662,6 +728,8 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -670,6 +738,10 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -688,18 +760,24 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], @@ -708,6 +786,8 @@ "ts-jest": ["ts-jest@29.3.4", "", { "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -718,6 +798,8 @@ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], @@ -758,6 +840,8 @@ "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "jest-config/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], diff --git a/package.json b/package.json index 7245535..85007dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqlkit", - "version": "1.0.12", + "version": "1.0.17", "description": "A lightweight SQL builder for TypeScript", "license": "MIT", "author": { @@ -15,13 +15,13 @@ "main": "dist/index.cjs", "types": "dist/index.d.ts", "module": "dist/index.mjs", - "files": [ + "files": [ "dist" ], "scripts": { "build": "pkgroll --sourcemap --clean-dist --minify", "tinker": "npx tsx watch src/tinker.ts", - "test": "jest --runInBand", + "test": "jest --testPathPattern=repository --runInBand", "format": "prettier --write \"src/**/*.ts\"", "docs": "npx typedoc src/index.ts" }, @@ -36,10 +36,35 @@ "@faker-js/faker": "^9.7.0", "@types/jest": "^29.5.14", "@types/pg": "^8.11.12", + "better-sqlite3": "^11.10.0", "jest": "^29.7.0", "pg": "^8.14.1", "pkgroll": "^2.12.1", "prettier": "^3.5.3", "ts-jest": "^29.3.2" + }, + "peerDependencies": { + "better-sqlite3": "", + "mysql": "^2.18.1", + "mysql2": "^3.14.1", + "pg": "^8.16.0", + "sqlite3": "^5.1.7" + }, + "peerDependenciesMeta": { + "pg": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "mysql": { + "optional": true + } } } diff --git a/readme.md b/readme.md index cd53e55..9e7a81c 100644 --- a/readme.md +++ b/readme.md @@ -56,7 +56,7 @@ const executor = new PostgresAdapter(pool); const builder = new SelectQueryBuilder("users", executor); -const users = await builder +const { rows: users } = await builder .select(["id", "name"]) .where({ key: "age", operator: ">", value: 18 }) // OR .where(eq("age", 18)) @@ -69,8 +69,7 @@ console.log(users); ### 3. Repository API 🔥 ```ts -import { Repository, gt, like, and } from "sqlkit"; -import { PostgresAdapter } from "sqlkit"; +import { Repository, PostgresAdapter, gt, like, and, asc } from "sqlkit"; import { Pool } from "pg"; const pool = new Pool({ @@ -84,11 +83,10 @@ const users = await userRepo.find({ where: and(gt("age", 25), like("name", "%Doe%")), }); -// Paginate +// Paginate (offset is derived from page and limit inside paginate) const result = await userRepo.paginate({ page: 1, limit: 10, - offset: 2, where: gt("age", 18), columns: ["age", "email"], orderBy: [asc("age")], @@ -99,32 +97,37 @@ console.log(result.meta); /* { totalCount: 100, - currentPage: 2, + currentPage: 1, totalPages: 10, hasNextPage: true } */ -// Find one -const user = await userRepo.find(like("email", "%@example.com")); +// Find one (find always returns an array) +const [user] = await userRepo.find({ + where: like("email", "%@example.com"), + limit: 1, +}); // Count const count = await userRepo.count(gt("age", 30)); -// Insert -const newUser = await userRepo.insert({ - name: "Rayhan", - email: "ray@example.com", -}); +// Insert (accepts an array of rows; returns QueryResult) +const insertResult = await userRepo.insert([ + { name: "Rayhan", email: "ray@example.com" }, +]); +// insertResult.rows[0] — inserted row(s) // Update -const updated = await userRepo.update( - { name: "Ray" }, - like("email", "%ray%"), -); +const updated = await userRepo.update({ + data: { name: "Ray" }, + where: like("email", "%ray%"), +}); // Delete -const deleted = await userRepo.delete(like("name", "Ray%")); +const deleted = await userRepo.delete({ + where: like("name", "Ray%"), +}); ``` ### 🔍 Supported Operators diff --git a/script.txt b/script.txt new file mode 100644 index 0000000..4018fc5 --- /dev/null +++ b/script.txt @@ -0,0 +1,4 @@ + "test:postgres": "jest --testPathPattern=repository --runInBand", + "test:sqlite": "jest --testPathPattern=sqlite-repository --runInBand", + "test:mysql": "jest --testPathPattern=mysql-repository --runInBand", + "test:dialects": "jest --testPathPattern=dialects --runInBand", \ No newline at end of file diff --git a/src/dialects/index.ts b/src/dialects/index.ts index 4ede65c..2c67217 100644 --- a/src/dialects/index.ts +++ b/src/dialects/index.ts @@ -1 +1,3 @@ export * from "./postgres"; +export * from "./sqlite"; +export * from "./mysql"; diff --git a/src/dialects/mysql.ts b/src/dialects/mysql.ts new file mode 100644 index 0000000..5c45b27 --- /dev/null +++ b/src/dialects/mysql.ts @@ -0,0 +1,67 @@ +import { QueryResult, SqlExecutor } from "../types"; +import { SQLKITException } from "../exceptions"; + +export class MySQLAdapter implements SqlExecutor { + constructor(private mysqlConnection: any) {} + + async executeSQL(sql: string, values: any[]): Promise> { + return new Promise((resolve, reject) => { + // Handle different MySQL library interfaces + if (this.mysqlConnection.query) { + // mysql2 or mysql library + this.mysqlConnection.query(sql, values, (err: any, results: any, fields: any) => { + if (err) { + reject(new SQLKITException(err.message)); + } else { + // Handle different result types + if (Array.isArray(results)) { + // SELECT queries return array of rows + resolve({ + rows: results as T[], + rowCount: results.length + }); + } else if (results.affectedRows !== undefined) { + // INSERT/UPDATE/DELETE queries return result object + resolve({ + rows: results.insertId ? [{ insertId: results.insertId }] as T[] : [], + rowCount: results.affectedRows + }); + } else { + // Fallback for other result types + resolve({ + rows: [results] as T[], + rowCount: 1 + }); + } + } + }); + } else if (this.mysqlConnection.execute) { + // mysql2 with prepared statements + this.mysqlConnection.execute(sql, values, (err: any, results: any, fields: any) => { + if (err) { + reject(new SQLKITException(err.message)); + } else { + if (Array.isArray(results)) { + resolve({ + rows: results as T[], + rowCount: results.length + }); + } else if (results.affectedRows !== undefined) { + resolve({ + rows: results.insertId ? [{ insertId: results.insertId }] as T[] : [], + rowCount: results.affectedRows + }); + } else { + resolve({ + rows: [results] as T[], + rowCount: 1 + }); + } + } + }); + } else { + reject(new SQLKITException("Unsupported MySQL database interface")); + } + }); + } +} \ No newline at end of file diff --git a/src/dialects/sqlite.ts b/src/dialects/sqlite.ts new file mode 100644 index 0000000..b22c13b --- /dev/null +++ b/src/dialects/sqlite.ts @@ -0,0 +1,71 @@ +import { QueryResult, SqlExecutor } from "../types"; +import { SQLKITException } from "../exceptions"; + +export class SQLiteAdapter implements SqlExecutor { + constructor(private sqliteDb: any) {} + + async executeSQL(sql: string, values: any[]): Promise> { + return new Promise((resolve, reject) => { + try { + if (this.sqliteDb.prepare) { + // better-sqlite3 prepared statements + console.log('Original SQL:', sql); + console.log('Original values:', values); + // Convert PostgreSQL-style placeholders ($1, $2, etc.) to SQLite-style (?) + let sqliteQuery = sql.replace(/\$\d+/g, '?'); + + // Determine if this is a SELECT statement or a modification statement + const trimmedSql = sql.trim().toUpperCase(); + const isSelectStatement = trimmedSql.startsWith('SELECT') || + trimmedSql.startsWith('WITH') || + (trimmedSql.startsWith('PRAGMA') && trimmedSql.includes('=')); // Some PRAGMA statements return data + + // SQLite doesn't support RETURNING clause in INSERT, UPDATE, DELETE statements + // Strip RETURNING clause for modification statements + if (!isSelectStatement && sqliteQuery.toUpperCase().includes('RETURNING')) { + sqliteQuery = sqliteQuery.replace(/\s+RETURNING\s+[\s\S]*?;?\s*$/i, ';'); + } + + const stmt = this.sqliteDb.prepare(sqliteQuery); + + if (isSelectStatement) { + // Use .all() for SELECT statements + const rows = stmt.all(values) as T[]; + resolve({ + rows: rows || [], + rowCount: rows?.length || 0 + }); + } else { + // Use .run() for INSERT, UPDATE, DELETE, CREATE, DROP, etc. + const result = stmt.run(values); + if (sqliteQuery.toUpperCase().includes('UPDATE')) { + console.log('UPDATE query:', sqliteQuery); + console.log('UPDATE values:', values); + console.log('UPDATE result:', result); + } + resolve({ + rows: [] as T[], + rowCount: result.changes || 0 + }); + } + } else if (this.sqliteDb.all) { + // node-sqlite3 callback-based API + this.sqliteDb.all(sql, values, (err: any, rows: T[]) => { + if (err) { + reject(new SQLKITException(err.message)); + } else { + resolve({ + rows: rows || [], + rowCount: rows?.length || 0 + }); + } + }); + } else { + reject(new SQLKITException("Unsupported SQLite database interface")); + } + } catch (err: any) { + reject(new SQLKITException(err.message)); + } + }); + } +} \ No newline at end of file diff --git a/src/exceptions/SQLKIT_Exception.ts b/src/exceptions/SQLKIT_Exception.ts index 4251844..89ac8c0 100644 --- a/src/exceptions/SQLKIT_Exception.ts +++ b/src/exceptions/SQLKIT_Exception.ts @@ -3,4 +3,4 @@ export class SQLKITException extends Error { super(message); this.name = "SQLKITException"; } -} +} \ No newline at end of file diff --git a/src/repository/repository.ts b/src/repository/repository.ts index b30c955..e535b9c 100644 --- a/src/repository/repository.ts +++ b/src/repository/repository.ts @@ -25,8 +25,6 @@ export class Repository { protected options?: RepositoryOptions ) {} - - async find(payload?: QueryRowsPayload): Promise { const builder = new SelectQueryBuilder(this.tableName, this.executor); if (payload?.where) builder.where(payload.where); @@ -36,17 +34,22 @@ export class Repository { if (payload?.orderBy) builder.orderBy(payload.orderBy); if (payload?.limit) builder.limit(payload.limit); if (payload?.offset) builder.offset(payload.offset); + if (payload?.columns) builder.select(payload.columns); const result = await builder.commit(); if (this.options?.logging) { console.log({ + operationName: payload.operationName, sql: builder.build().sql, values: builder.build().values, - result: result + result: { + rows: JSON.stringify(result.rows, null, 2), + rowCount: result.rowCount + } }); } - + return result.rows; } @@ -63,6 +66,7 @@ export class Repository { if (options.orderBy) builder.orderBy(options.orderBy); if (options.limit) builder.limit(options.limit); if (options.offset) builder.offset(options.offset); + if (options.columns) builder.select(options.columns); return builder.paginate(options); } @@ -77,21 +81,30 @@ export class Repository { `; const result = await this.executor.executeSQL(query, values); + return parseInt(result.rows[0].count, 10); } - async insert( data: Partial[], - returning: Array = ["*"] as any + returning?: Array ): Promise> { const builder = new InsertQueryBuilder(this.tableName, this.executor); - const result = await builder.values(data).returning(returning).commit(); + const cursor = builder.values(data); + + if (returning) { + cursor.returning(returning); + } + const result = await cursor.commit(); + if (this.options?.logging) { console.log({ sql: builder.build().sql, values: builder.build().values, - result: result.rows + result: { + rows: JSON.stringify(result.rows, null, 2), + rowCount: result.rowCount + } }); } return result; @@ -101,6 +114,7 @@ export class Repository { where: WhereCondition; data: Partial; returning?: Array; + operationName?: string; }): Promise> { const { where, data, returning = ["*"] as any } = args; @@ -111,34 +125,42 @@ export class Repository { .returning(returning) .commit(); - if (this.options?.logging) { console.log({ + operationName: args.operationName, sql: builder.build().sql, values: builder.build().values, - result: result + result: { + rows: JSON.stringify(result.rows, null, 2), + rowCount: result.rowCount + } }); } - return result + return result; } - async delete(arg: { + async delete(args: { where: WhereCondition; returning?: Array; + operationName?: string; }): Promise | null> { const builder = new DeleteQueryBuilder(this.tableName, this.executor); const result = await builder - .where(arg.where) - .returning(arg.returning) + .where(args.where) + .returning(args.returning) .commit(); if (this.options?.logging) { console.log({ + operationName: args.operationName, sql: builder.build().sql, values: builder.build().values, - result: result.rows + result: { + rows: JSON.stringify(result.rows, null, 2), + rowCount: result.rowCount + } }); } - return result + return result; } } diff --git a/src/tinker.ts b/src/tinker.ts index b8b8e98..ce94f67 100644 --- a/src/tinker.ts +++ b/src/tinker.ts @@ -1,4 +1,4 @@ -import { seedTestData } from "../test-setup"; +import { seedTestData } from "../__tests__/test-setups/pg-test-setup"; async function main() { await seedTestData(); diff --git a/src/types/query.ts b/src/types/query.ts index 1b6fdd7..0506ee1 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -45,7 +45,7 @@ export interface Join { columns?: Array; } -export interface ManyToManyJoin { +export interface ManyToManyJoin { table: string; as?: string; on: { @@ -56,6 +56,7 @@ export interface ManyToManyJoin { } export interface QueryRowsPayload { + operationName?: string; where?: WhereCondition; joins?: Join[]; with?: ManyToManyJoin[];