├── dist ├── __tests__ │ ├── rpc.test.d.ts │ ├── client.test.d.ts │ ├── types.test.d.ts │ ├── query-builder.test.d.ts │ ├── query-result.test.d.ts │ ├── postgres-client.test.d.ts │ ├── query-builder-join.test.d.ts │ ├── query-builder-bigint.test.d.ts │ ├── query-builder-count.test.d.ts │ ├── query-builder-or-is.test.d.ts │ ├── query-builder-reserved.test.d.ts │ ├── query-builder-select-is.test.d.ts │ ├── query-builder-single.test.d.ts │ ├── query-builder-update-is.test.d.ts │ ├── query-builder-native-array.test.d.ts │ ├── client.test.js │ ├── types.test.js │ ├── postgres-client.test.js │ ├── query-builder-or-is.test.js │ ├── query-builder-select-is.test.js │ ├── query-builder.test.js │ ├── query-builder-update-is.test.js │ ├── query-result.test.js │ ├── query-builder-count.test.js │ ├── query-builder-join.test.js │ ├── rpc.test.js │ ├── query-builder-native-array.test.js │ └── query-builder-bigint.test.js ├── types.js ├── database.types.js ├── index.d.ts ├── client.d.ts ├── database.types.d.ts ├── errors.d.ts ├── index.js ├── errors.js ├── client.js ├── postgres-client.d.ts ├── types.d.ts └── query-builder.d.ts ├── .prettierrc ├── src ├── index.ts ├── __tests__ │ ├── client.test.ts │ ├── types.test.ts │ ├── postgres-client.test.ts │ ├── query-builder-or-is.test.ts │ ├── query-builder-select-is.test.ts │ ├── query-builder.test.ts │ ├── query-builder-update-is.test.ts │ ├── query-result.test.ts │ ├── rpc.test.ts │ ├── query-builder-count.test.ts │ ├── query-builder-native-array.test.ts │ ├── query-builder-bigint.test.ts │ └── query-builder-join.test.ts ├── client.ts ├── database.types.ts ├── errors.ts └── types.ts ├── .env.example ├── jest.config.js ├── examples ├── setup.sql ├── test-table.ts ├── tests │ ├── query-builder.test.ts │ ├── setup-single-test.ts │ ├── in.ts │ ├── select.ts │ ├── where.ts │ ├── special.ts │ ├── mutation.ts │ ├── array-insert.ts │ ├── view-table-test.ts │ ├── transaction.ts │ ├── mock-query-result.ts │ ├── connection-string.ts │ ├── view-table.ts │ ├── query-result.ts │ └── query-result-simple.ts ├── test.ts ├── README.md ├── setup-views.sql └── types │ └── database.ts ├── setup-postgres-test.sql ├── tsconfig.json ├── .eslintrc.json ├── docs └── changelog │ ├── 2025-08-28-add-not-method.md │ ├── 2025-11-21-support-is-in-or-method.md │ ├── 2025-06-10-bigint-handling.md │ ├── 2025-06-10-reserved-keywords-handling.md │ ├── 2025-12-17-embed-many-to-one.md │ ├── 2025-11-26-rpc-single-support.md │ ├── 2025-06-10-jsonb-array-test.md │ ├── 2025-06-10-bigint-handling-enhancement.md │ └── 2025-05-26-maybeSingle-single-refactor.md ├── package.json ├── .gitignore ├── .npmignore ├── CHANGE_REPORT_LOG.md └── CHANGELOG.md /dist/__tests__/rpc.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/client.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/types.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-result.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/postgres-client.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-join.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-bigint.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-count.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-or-is.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-reserved.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-select-is.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-single.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-update-is.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-native-array.test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/database.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './types'; 3 | export * from './errors'; 4 | export * from './postgres-client'; 5 | export { SupaliteClient as default } from './client'; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteClient } from './client'; 2 | 3 | export * from './client'; 4 | export * from './types'; 5 | export * from './errors'; 6 | export * from './postgres-client'; 7 | 8 | export { SupaliteClient as default } from './client'; 9 | -------------------------------------------------------------------------------- /dist/client.d.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteConfig, DatabaseSchema } from './types'; 2 | import { SupaLitePG } from './postgres-client'; 3 | export declare class SupaliteClient extends SupaLitePG { 4 | constructor(config?: SupaliteConfig); 5 | } 6 | export { SupaLitePG } from './postgres-client'; 7 | export * from './types'; 8 | export * from './errors'; 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # PostgreSQL 연결 설정 2 | # 방법 1: 개별 매개변수 사용 3 | DB_USER=testuser 4 | DB_HOST=localhost 5 | DB_NAME=testdb 6 | DB_PASS=testpassword 7 | DB_PORT=5432 8 | DB_SSL=false 9 | 10 | # 방법 2: 연결 문자열(URI) 사용 (위의 개별 매개변수 대신 사용 가능) 11 | # DB_CONNECTION=postgresql://testuser:testpassword@localhost:5432/testdb 12 | 13 | # 주의: 이 파일을 .env로 복사하여 사용하세요 14 | # cp .env.example .env 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest', 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 10 | testPathIgnorePatterns: ['/src/__tests__/postgres-client.test.ts'], 11 | }; 12 | -------------------------------------------------------------------------------- /src/__tests__/client.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteClient } from '../client'; 2 | 3 | describe('SupaliteClient', () => { 4 | test('SupaliteClient 클래스가 정의되어 있어야 함', () => { 5 | expect(SupaliteClient).toBeDefined(); 6 | }); 7 | 8 | test('SupaliteClient 인스턴스를 생성할 수 있어야 함', () => { 9 | // 실제 DB 연결 없이 클래스 인스턴스만 확인 10 | const client = {} as SupaliteClient; 11 | expect(client).not.toBeNull(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteConfig, QueryOptions, FilterOptions, DatabaseSchema } from './types'; 2 | import { SupaLitePG } from './postgres-client'; 3 | 4 | export class SupaliteClient extends SupaLitePG { 5 | constructor(config?: SupaliteConfig) { 6 | super(config); 7 | } 8 | } 9 | 10 | export { SupaLitePG } from './postgres-client'; 11 | export * from './types'; 12 | export * from './errors'; 13 | -------------------------------------------------------------------------------- /src/__tests__/types.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteConfig } from '../types'; 2 | 3 | describe('Types', () => { 4 | test('SupaliteConfig 타입이 정의되어 있어야 함', () => { 5 | // 타입이 존재하는지 확인하는 간단한 테스트 6 | const config: SupaliteConfig = { 7 | connectionString: 'postgresql://user:pass@localhost:5432/db' 8 | }; 9 | expect(config).toBeDefined(); 10 | expect(config.connectionString).toBe('postgresql://user:pass@localhost:5432/db'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/setup.sql: -------------------------------------------------------------------------------- 1 | -- 기존 내용 유지 2 | CREATE TABLE IF NOT EXISTS public.shops ( 3 | id bigint NOT NULL, 4 | name text, 5 | created_at timestamp with time zone DEFAULT now() 6 | ); 7 | 8 | ALTER TABLE public.shops ENABLE ROW LEVEL SECURITY; 9 | 10 | CREATE POLICY "Enable read access for all users" ON public.shops FOR SELECT USING (true); 11 | 12 | CREATE TABLE IF NOT EXISTS public.test_table ( 13 | id SERIAL PRIMARY KEY, 14 | name TEXT, 15 | value INTEGER 16 | ); 17 | -------------------------------------------------------------------------------- /dist/__tests__/client.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const client_1 = require("../client"); 4 | describe('SupaliteClient', () => { 5 | test('SupaliteClient 클래스가 정의되어 있어야 함', () => { 6 | expect(client_1.SupaliteClient).toBeDefined(); 7 | }); 8 | test('SupaliteClient 인스턴스를 생성할 수 있어야 함', () => { 9 | // 실제 DB 연결 없이 클래스 인스턴스만 확인 10 | const client = {}; 11 | expect(client).not.toBeNull(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /dist/__tests__/types.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | describe('Types', () => { 4 | test('SupaliteConfig 타입이 정의되어 있어야 함', () => { 5 | // 타입이 존재하는지 확인하는 간단한 테스트 6 | const config = { 7 | connectionString: 'postgresql://user:pass@localhost:5432/db' 8 | }; 9 | expect(config).toBeDefined(); 10 | expect(config.connectionString).toBe('postgresql://user:pass@localhost:5432/db'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /setup-postgres-test.sql: -------------------------------------------------------------------------------- 1 | -- 테스트 사용자 생성 (이미 존재하면 삭제 후 재생성) 2 | DROP USER IF EXISTS testuser; 3 | CREATE USER testuser WITH PASSWORD 'testpassword'; 4 | 5 | -- 테스트 데이터베이스 생성 (이미 존재하면 삭제 후 재생성) 6 | DROP DATABASE IF EXISTS testdb; 7 | CREATE DATABASE testdb; 8 | 9 | -- 테스트 사용자에게 테스트 데이터베이스에 대한 모든 권한 부여 10 | GRANT ALL PRIVILEGES ON DATABASE testdb TO testuser; 11 | 12 | -- 테스트 데이터베이스에 접속 13 | \c testdb 14 | 15 | -- 테스트 사용자를 테스트 데이터베이스의 소유자로 설정 16 | ALTER DATABASE testdb OWNER TO testuser; 17 | 18 | -- 테스트 사용자에게 스키마 생성 권한 부여 19 | GRANT CREATE ON SCHEMA public TO testuser; 20 | -------------------------------------------------------------------------------- /src/database.types.ts: -------------------------------------------------------------------------------- 1 | export interface Database { 2 | public: { 3 | Tables: { 4 | [key: string]: { 5 | Row: Record; 6 | Insert: Record; 7 | Update: Record; 8 | }; 9 | }; 10 | Views: { 11 | [key: string]: { 12 | Row: Record; 13 | }; 14 | }; 15 | Functions: { 16 | [key: string]: { 17 | Args: Record; 18 | Returns: any; 19 | }; 20 | }; 21 | Enums: { 22 | [key: string]: string[]; 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "*": ["node_modules/*"] 17 | } 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist", "test"] 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended" 7 | ], 8 | "env": { 9 | "node": true, 10 | "es6": true 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2020, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "@typescript-eslint/explicit-function-return-type": "off", 18 | "@typescript-eslint/no-explicit-any": "warn", 19 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/changelog/2025-08-28-add-not-method.md: -------------------------------------------------------------------------------- 1 | # 변경 보고서: `not` 메소드 추가 2 | 3 | **날짜:** 2025년 8월 28일 4 | 5 | ## 변경 유형 6 | 7 | - [x] 기능 추가 8 | - [ ] 버그 수정 9 | - [ ] 성능 개선 10 | - [ ] 문서 업데이트 11 | - [ ] 기타 12 | 13 | ## 변경 내용 14 | 15 | ### `QueryBuilder` 16 | 17 | - **`not` 메소드 추가**: `is` 연산자와 함께 사용하여 `IS NOT NULL` 조건을 생성할 수 있는 `not` 메소드를 추가했습니다. 18 | 19 | **사용 예시:** 20 | 21 | ```typescript 22 | const { data } = await client 23 | .from('users') 24 | .select('id, name') 25 | .not('email', 'is', null); 26 | ``` 27 | 28 | 위 코드는 `SELECT "id", "name" FROM "public"."users" WHERE "email" IS NOT NULL` SQL 쿼리를 생성합니다. 29 | 30 | ## 관련 파일 31 | 32 | - `src/query-builder.ts` 33 | - `README.md` 34 | -------------------------------------------------------------------------------- /dist/database.types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Database { 2 | public: { 3 | Tables: { 4 | [key: string]: { 5 | Row: Record; 6 | Insert: Record; 7 | Update: Record; 8 | }; 9 | }; 10 | Views: { 11 | [key: string]: { 12 | Row: Record; 13 | }; 14 | }; 15 | Functions: { 16 | [key: string]: { 17 | Args: Record; 18 | Returns: any; 19 | }; 20 | }; 21 | Enums: { 22 | [key: string]: string[]; 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /dist/errors.d.ts: -------------------------------------------------------------------------------- 1 | export declare class PostgresError extends Error { 2 | code?: string; 3 | details?: string; 4 | hint?: string; 5 | position?: string; 6 | schema?: string; 7 | table?: string; 8 | column?: string; 9 | dataType?: string; 10 | constraint?: string; 11 | constructor(message: string, pgError?: any); 12 | toJSON(): { 13 | message: string; 14 | code: string | undefined; 15 | details: string | undefined; 16 | hint: string | undefined; 17 | position: string | undefined; 18 | schema: string | undefined; 19 | table: string | undefined; 20 | column: string | undefined; 21 | dataType: string | undefined; 22 | constraint: string | undefined; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /examples/test-table.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from './postgres-client'; 2 | import { Database } from './database.types'; 3 | 4 | export async function insertIntoTestTable( 5 | client: SupaLitePG, 6 | name: string, 7 | value: number 8 | ) { 9 | const { data, error } = await client 10 | .from('test_table') 11 | .insert({ name, value }) 12 | .select('*') 13 | .single(); 14 | 15 | if (error) { 16 | throw error; 17 | } 18 | 19 | return data; 20 | } 21 | 22 | export async function getFromTestTable( 23 | client: SupaLitePG, 24 | conditions: { [key: string]: any } 25 | ) { 26 | const { data, error } = await client.from('test_table').select('*').match(conditions); 27 | 28 | if (error) { 29 | throw error; 30 | } 31 | 32 | return data; 33 | } 34 | -------------------------------------------------------------------------------- /docs/changelog/2025-11-21-support-is-in-or-method.md: -------------------------------------------------------------------------------- 1 | # 변경 보고서: `or` 메소드 내 `is` 연산자 지원 2 | 3 | **날짜:** 2025년 11월 21일 4 | 5 | ## 변경 유형 6 | 7 | - [x] 기능 추가 8 | - [ ] 버그 수정 9 | - [ ] 성능 개선 10 | - [ ] 문서 업데이트 11 | - [ ] 기타 12 | 13 | ## 변경 내용 14 | 15 | ### `QueryBuilder` 16 | 17 | - **`or` 메소드 개선**: `or` 메소드 내부에서 `is` 연산자를 사용할 수 있도록 지원을 추가했습니다. 이제 `valid_until.is.null`과 같은 표현을 통해 `IS NULL` 조건을 `OR` 절 안에서 사용할 수 있습니다. 18 | 19 | **사용 예시:** 20 | 21 | ```typescript 22 | const { data } = await client 23 | .from('credits') 24 | .select('*') 25 | .or('valid_until.is.null,valid_until.gt.now()'); 26 | ``` 27 | 28 | 위 코드는 `SELECT * FROM "public"."credits" WHERE ("valid_until" IS NULL OR "valid_until" > 'now()')` SQL 쿼리를 생성합니다. 29 | 30 | ## 관련 파일 31 | 32 | - `src/query-builder.ts` 33 | - `README.md` 34 | -------------------------------------------------------------------------------- /src/__tests__/postgres-client.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../postgres-client'; 2 | 3 | // pg 모듈 모킹 4 | jest.mock('pg', () => { 5 | const mockPool = { 6 | connect: jest.fn().mockResolvedValue({ 7 | release: jest.fn() 8 | }), 9 | query: jest.fn(), 10 | end: jest.fn(), 11 | on: jest.fn() 12 | }; 13 | return { 14 | Pool: jest.fn(() => mockPool), 15 | types: { 16 | setTypeParser: jest.fn() 17 | } 18 | }; 19 | }); 20 | 21 | // dotenv 모듈 모킹 22 | jest.mock('dotenv', () => ({ 23 | config: jest.fn() 24 | })); 25 | 26 | describe('SupaLitePG', () => { 27 | test('SupaLitePG 클래스가 정의되어 있어야 함', () => { 28 | expect(SupaLitePG).toBeDefined(); 29 | }); 30 | 31 | test('testConnection 메서드가 성공적으로 동작해야 함', async () => { 32 | const client = new SupaLitePG(); 33 | const result = await client.testConnection(); 34 | expect(result).toBe(true); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /docs/changelog/2025-06-10-bigint-handling.md: -------------------------------------------------------------------------------- 1 | ## 2025-06-10: Improved BigInt Handling 2 | 3 | - **`QueryBuilder` Modifications (`src/query-builder.ts`)**: 4 | - Enhanced the `buildQuery()` method to automatically convert JavaScript `BigInt` values to their string representation before passing them as parameters to the database for `INSERT`, `UPSERT`, and `UPDATE` operations. This ensures correct serialization of `BigInt` values. 5 | - Note: Deserialization (reading `BIGINT` from DB to JS `BigInt`) was already handled by a type parser in `postgres-client.ts`. 6 | - **New Test Suite**: Created `src/__tests__/query-builder-bigint.test.ts` to specifically test operations involving `BIGINT` columns. 7 | - Includes tests for SELECT, INSERT, UPDATE, and filtering using `BigInt` values. 8 | - Corrected test setup in `beforeEach` to pass large integer literals as strings in direct `pool.query` calls to avoid "bigint out of range" errors during data seeding. 9 | -------------------------------------------------------------------------------- /dist/__tests__/postgres-client.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const postgres_client_1 = require("../postgres-client"); 4 | // pg 모듈 모킹 5 | jest.mock('pg', () => { 6 | const mockPool = { 7 | connect: jest.fn().mockResolvedValue({ 8 | release: jest.fn() 9 | }), 10 | query: jest.fn(), 11 | end: jest.fn(), 12 | on: jest.fn() 13 | }; 14 | return { 15 | Pool: jest.fn(() => mockPool), 16 | types: { 17 | setTypeParser: jest.fn() 18 | } 19 | }; 20 | }); 21 | // dotenv 모듈 모킹 22 | jest.mock('dotenv', () => ({ 23 | config: jest.fn() 24 | })); 25 | describe('SupaLitePG', () => { 26 | test('SupaLitePG 클래스가 정의되어 있어야 함', () => { 27 | expect(postgres_client_1.SupaLitePG).toBeDefined(); 28 | }); 29 | test('testConnection 메서드가 성공적으로 동작해야 함', async () => { 30 | const client = new postgres_client_1.SupaLitePG(); 31 | const result = await client.testConnection(); 32 | expect(result).toBe(true); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /examples/tests/query-builder.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src/postgres-client'; 2 | import { insertIntoTestTable, getFromTestTable } from '../test-table'; 3 | import { config } from 'dotenv'; 4 | 5 | config(); 6 | 7 | async function runTest() { 8 | const client = new SupaLitePG({ 9 | connectionString: process.env.DB_CONNECTION, 10 | }); 11 | 12 | try { 13 | // 테스트 데이터를 삽입합니다. 14 | await insertIntoTestTable(client, 'test1', 10); 15 | await insertIntoTestTable(client, 'test2', 20); 16 | 17 | // match 메서드를 사용하여 데이터를 조회합니다. 18 | const result1 = await getFromTestTable(client, { name: 'test1' }); 19 | console.log('Result 1:', result1); 20 | 21 | const result2 = await getFromTestTable(client, { value: 20 }); 22 | console.log('Result 2:', result2); 23 | 24 | const result3 = await getFromTestTable(client, { name: 'test1', value: 10 }); 25 | console.log('Result 3:', result3); 26 | 27 | const result4 = await getFromTestTable(client, { name: 'test3' }); 28 | console.log('Result 4:', result4); 29 | } catch (error) { 30 | console.error('Test error:', error); 31 | } finally { 32 | await client.close(); 33 | } 34 | } 35 | 36 | runTest(); 37 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 14 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | exports.default = void 0; 18 | __exportStar(require("./client"), exports); 19 | __exportStar(require("./types"), exports); 20 | __exportStar(require("./errors"), exports); 21 | __exportStar(require("./postgres-client"), exports); 22 | var client_1 = require("./client"); 23 | Object.defineProperty(exports, "default", { enumerable: true, get: function () { return client_1.SupaliteClient; } }); 24 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class PostgresError extends Error { 2 | code?: string; 3 | details?: string; 4 | hint?: string; 5 | position?: string; 6 | schema?: string; 7 | table?: string; 8 | column?: string; 9 | dataType?: string; 10 | constraint?: string; 11 | 12 | constructor(message: string, pgError?: any) { 13 | super(message); 14 | this.name = 'PostgresError'; 15 | if (pgError) { 16 | this.code = pgError.code; 17 | this.details = pgError.detail; 18 | this.hint = pgError.hint; 19 | this.position = pgError.position; 20 | this.schema = pgError.schema; 21 | this.table = pgError.table; 22 | this.column = pgError.column; 23 | this.dataType = pgError.dataType; 24 | this.constraint = pgError.constraint; 25 | } 26 | 27 | // Ensure proper prototype chain for ES5 28 | Object.setPrototypeOf(this, PostgresError.prototype); 29 | } 30 | 31 | toJSON() { 32 | return { 33 | message: this.message, 34 | code: this.code, 35 | details: this.details, 36 | hint: this.hint, 37 | position: this.position, 38 | schema: this.schema, 39 | table: this.table, 40 | column: this.column, 41 | dataType: this.dataType, 42 | constraint: this.constraint 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/changelog/2025-06-10-reserved-keywords-handling.md: -------------------------------------------------------------------------------- 1 | ## 2025-06-10: Improved Handling of Reserved Keywords as Column Names 2 | 3 | - **New Test Suite**: Created `src/__tests__/query-builder-reserved.test.ts` to specifically test scenarios where table column names are SQL reserved keywords (e.g., "order", "desc", "user", "limit", "group"). 4 | - Includes tests for SELECT, INSERT, UPDATE, ORDER BY, and UPSERT operations on these columns. 5 | - **`QueryBuilder` Modifications (`src/query-builder.ts`)**: 6 | - **`select()` method**: Enhanced to automatically quote individual column names if a comma-separated string of unquoted names is provided (e.g., `select('order, desc')` will now correctly generate `SELECT "order", "desc"`). This does not affect `select('*')` or already quoted identifiers. 7 | - **`upsert()` method**: The `onConflict` option, when provided as a simple unquoted column name, will now be automatically quoted (e.g., `onConflict: 'order'` becomes `ON CONFLICT ("order")`). Complex constraint names or multi-column conflict targets provided by the user are not modified. 8 | - **Previous JSONB Update Integration**: The work on reserved keywords was done on top of previous changes for automatic JSON stringification. The changelog entry `docs/changelog/2025-06-10-jsonb-array-test.md` covers those details. This entry focuses on reserved keyword handling. 9 | -------------------------------------------------------------------------------- /dist/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.PostgresError = void 0; 4 | class PostgresError extends Error { 5 | constructor(message, pgError) { 6 | super(message); 7 | this.name = 'PostgresError'; 8 | if (pgError) { 9 | this.code = pgError.code; 10 | this.details = pgError.detail; 11 | this.hint = pgError.hint; 12 | this.position = pgError.position; 13 | this.schema = pgError.schema; 14 | this.table = pgError.table; 15 | this.column = pgError.column; 16 | this.dataType = pgError.dataType; 17 | this.constraint = pgError.constraint; 18 | } 19 | // Ensure proper prototype chain for ES5 20 | Object.setPrototypeOf(this, PostgresError.prototype); 21 | } 22 | toJSON() { 23 | return { 24 | message: this.message, 25 | code: this.code, 26 | details: this.details, 27 | hint: this.hint, 28 | position: this.position, 29 | schema: this.schema, 30 | table: this.table, 31 | column: this.column, 32 | dataType: this.dataType, 33 | constraint: this.constraint 34 | }; 35 | } 36 | } 37 | exports.PostgresError = PostgresError; 38 | -------------------------------------------------------------------------------- /docs/changelog/2025-12-17-embed-many-to-one.md: -------------------------------------------------------------------------------- 1 | # PostgREST-style Embed: Many-to-One Support 2 | 3 | - **Date**: 2025-12-17 4 | - **Author**: Codex 5 | - **Status**: Completed 6 | 7 | ## Summary 8 | 9 | Fixed PostgREST-style embed syntax in `select()` (e.g. `related_table(*)`) so it works for both relationship directions: 10 | 11 | - **1:N** (foreign table references the base table) returns an **array** (defaults to `[]`). 12 | - **N:1** (base table references the foreign table) returns a **single object** (or `null`). 13 | 14 | ## Changes 15 | 16 | 1. **Bidirectional FK resolution** 17 | - `SupaLitePG.getForeignKey()` now checks both directions between `table` and `foreignTable` and returns whether the embed should be an array or object. 18 | 19 | 2. **Correct JSON shape in SQL generation** 20 | - `QueryBuilder` uses `json_agg` (with `COALESCE(..., '[]'::json)`) for 1:N embeds. 21 | - `QueryBuilder` uses `row_to_json` (with `LIMIT 1`) for N:1 embeds. 22 | 23 | 3. **Unit tests** 24 | - Added tests to cover N:1 embed behavior and nested column selection. 25 | 26 | ## Impact 27 | 28 | - Queries like `from('menu_item_opts').select('*, menu_item_opts_schema(*)')` now embed `menu_item_opts_schema` without warnings, matching PostgREST expectations. 29 | - Existing 1:N embed behavior remains compatible, with an improved empty-result shape (`[]` instead of `null`). 30 | 31 | -------------------------------------------------------------------------------- /dist/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 14 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | exports.SupaLitePG = exports.SupaliteClient = void 0; 18 | const postgres_client_1 = require("./postgres-client"); 19 | class SupaliteClient extends postgres_client_1.SupaLitePG { 20 | constructor(config) { 21 | super(config); 22 | } 23 | } 24 | exports.SupaliteClient = SupaliteClient; 25 | var postgres_client_2 = require("./postgres-client"); 26 | Object.defineProperty(exports, "SupaLitePG", { enumerable: true, get: function () { return postgres_client_2.SupaLitePG; } }); 27 | __exportStar(require("./types"), exports); 28 | __exportStar(require("./errors"), exports); 29 | -------------------------------------------------------------------------------- /examples/test.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { supalitePg } from '../dist'; 3 | 4 | // .env 파일 로드 5 | config(); 6 | 7 | async function test() { 8 | try { 9 | console.log('환경 변수 확인:'); 10 | console.log('DB_USER:', process.env.DB_USER); 11 | console.log('DB_HOST:', process.env.DB_HOST); 12 | console.log('DB_NAME:', process.env.DB_NAME); 13 | console.log('DB_PORT:', process.env.DB_PORT); 14 | console.log('DB_SSL:', process.env.DB_SSL); 15 | 16 | // 사용자 목록 조회 17 | console.log('\n사용자 목록 조회:'); 18 | const result = await supalitePg 19 | .from('users') 20 | .select('*') 21 | .limit(5); 22 | 23 | if (result.error) { 24 | console.error('Error:', result.error); 25 | return; 26 | } 27 | 28 | console.log('Users:', result.data); 29 | console.log('Count:', result.count); 30 | console.log('Status:', result.status); 31 | 32 | // 특정 사용자의 프로필 조회 33 | console.log('\n프로필 조회:'); 34 | const userProfile = await supalitePg 35 | .from('profiles') 36 | .select('*') 37 | .eq('user_id', 1) 38 | .single(); 39 | 40 | if (userProfile.error) { 41 | console.error('Error:', userProfile.error); 42 | return; 43 | } 44 | 45 | console.log('Profile:', userProfile.data); 46 | 47 | } catch (err) { 48 | console.error('Unexpected error:', err); 49 | } finally { 50 | // 연결 종료 51 | await supalitePg.close(); 52 | } 53 | } 54 | 55 | test(); 56 | -------------------------------------------------------------------------------- /docs/changelog/2025-11-26-rpc-single-support.md: -------------------------------------------------------------------------------- 1 | # RPC .single() and .maybeSingle() Support 2 | 3 | - **Date**: 2025-11-26 4 | - **Author**: Cline 5 | - **Status**: Completed 6 | 7 | ## Summary 8 | 9 | Implemented `.single()` and `.maybeSingle()` method chaining support for `rpc` calls in `SupaLitePG`. This brings the `rpc` method closer to the Supabase JS client API, allowing users to enforce single-row constraints on RPC results. 10 | 11 | ## Changes 12 | 13 | 1. **Refactored `rpc` Method**: 14 | - The `rpc` method in `src/postgres-client.ts` now returns an instance of `RpcBuilder` instead of a `Promise` directly. 15 | - `RpcBuilder` implements the `Promise` interface, ensuring backward compatibility for `await rpc(...)` usage. 16 | 17 | 2. **Introduced `RpcBuilder` Class**: 18 | - Encapsulates RPC parameters and execution logic. 19 | - Adds `single()` method: Expects exactly one row. Throws `PGRST116` if 0 rows, `PGRST114` if >1 rows. 20 | - Adds `maybeSingle()` method: Expects at most one row. Returns `null` if 0 rows, throws `PGRST114` if >1 rows. 21 | - Preserves existing scalar unwrapping logic for single-row, single-column results. 22 | 23 | 3. **Unit Tests**: 24 | - Added `src/__tests__/rpc.test.ts` covering various scenarios for standard calls, `.single()`, and `.maybeSingle()`. 25 | 26 | ## Impact 27 | 28 | - Users can now use `.single()` on `rpc` calls, resolving the `TypeError: ...single is not a function` error. 29 | - Existing code using `await rpc(...)` continues to work without changes. 30 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Supalite 예제 2 | 3 | 이 디렉토리에는 Supalite 라이브러리의 다양한 기능을 보여주는 예제들이 포함되어 있습니다. 4 | 5 | ## 설정 6 | 7 | 1. 데이터베이스 설정: 8 | ```bash 9 | # PostgreSQL에 접속하여 테스트 데이터베이스와 테이블 생성 10 | psql -U postgres 11 | CREATE DATABASE testdb; 12 | \c testdb 13 | \i setup.sql 14 | ``` 15 | 16 | 2. 환경 변수 설정: 17 | ```bash 18 | # .env.example 파일을 .env로 복사 19 | cp .env.example .env 20 | 21 | # .env 파일을 편집하여 데이터베이스 연결 정보 입력 22 | # DB_USER=postgres 23 | # DB_HOST=localhost 24 | # DB_NAME=testdb 25 | # DB_PASS=postgres 26 | # DB_PORT=5432 27 | # DB_SSL=false 28 | ``` 29 | 30 | ## 예제 실행 31 | 32 | 각 예제는 특정 기능을 테스트합니다: 33 | 34 | 1. SELECT 쿼리 테스트: 35 | ```bash 36 | npx ts-node examples/tests/select.ts 37 | ``` 38 | - 기본 SELECT 39 | - 특정 컬럼 선택 40 | - COUNT 쿼리 41 | - 정렬과 페이징 42 | 43 | 2. WHERE 조건 테스트: 44 | ```bash 45 | npx ts-node examples/tests/where.ts 46 | ``` 47 | - eq, neq, is 48 | - in, contains (배열) 49 | - ilike (패턴 매칭) 50 | - gte/lte (날짜 범위) 51 | - OR 조건 52 | 53 | 3. 데이터 변경 테스트: 54 | ```bash 55 | npx ts-node examples/tests/mutation.ts 56 | ``` 57 | - INSERT (단일/다중) 58 | - UPDATE (조건부) 59 | - DELETE 60 | - UPSERT 61 | 62 | 4. 트랜잭션 테스트: 63 | ```bash 64 | npx ts-node examples/tests/transaction.ts 65 | ``` 66 | - 성공 케이스 67 | - 롤백 케이스 68 | - 중첩 트랜잭션 69 | 70 | 5. 특수 케이스 테스트: 71 | ```bash 72 | npx ts-node examples/tests/special.ts 73 | ``` 74 | - single() 메서드 75 | - 복잡한 조인 쿼리 76 | - 에러 처리 77 | - 서브쿼리 78 | - 집계 함수 79 | 80 | ## 타입 시스템 81 | 82 | `examples/types/database.ts` 파일에는 테스트 데이터베이스의 타입 정의가 포함되어 있습니다. 이는 실제 프로젝트에서 Supabase의 타입 생성기를 사용하는 것과 유사한 방식으로 타입 안전성을 보여주기 위한 예시입니다. 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supalite", 3 | "version": "0.5.6", 4 | "description": "A lightweight TypeScript PostgreSQL client with Supabase-style API", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jest --passWithNoTests", 10 | "lint": "eslint src/**/*.ts", 11 | "format": "prettier --write \"src/**/*.ts\"", 12 | "prepare": "$npm_execpath run build", 13 | "prepublishOnly": "$npm_execpath test && $npm_execpath run lint", 14 | "dev": "ts-node src/index.ts" 15 | }, 16 | "packageManager": "npm@10.2.4", 17 | "engines": { 18 | "node": ">=16.17.0" 19 | }, 20 | "keywords": [ 21 | "supabase", 22 | "postgresql", 23 | "typescript", 24 | "database", 25 | "lightweight", 26 | "query-builder" 27 | ], 28 | "author": { 29 | "name": "Wondong Shin", 30 | "email": "wodshin@gmail.com", 31 | "organization": "Genideas Inc." 32 | }, 33 | "license": "MIT", 34 | "dependencies": { 35 | "cross-fetch": "^4.0.0", 36 | "dotenv": "^16.4.7", 37 | "jwt-decode": "^4.0.0", 38 | "pg": "^8.11.3", 39 | "websocket": "^1.0.34" 40 | }, 41 | "devDependencies": { 42 | "@types/jest": "^29.5.12", 43 | "@types/node": "^20.11.24", 44 | "@types/pg": "^8.11.0", 45 | "@types/websocket": "^1.0.10", 46 | "@typescript-eslint/eslint-plugin": "^7.1.0", 47 | "@typescript-eslint/parser": "^7.1.0", 48 | "eslint": "^8.57.0", 49 | "jest": "^29.7.0", 50 | "prettier": "^3.2.5", 51 | "ts-jest": "^29.1.2", 52 | "ts-node": "^10.9.2", 53 | "typescript": "^5.3.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/tests/setup-single-test.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | 3 | async function setupDatabase() { 4 | // 직접 Pool 인스턴스 생성 5 | const pool = new Pool({ 6 | connectionString: process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb', 7 | }); 8 | 9 | try { 10 | console.log('데이터베이스 설정을 시작합니다...'); 11 | 12 | // users 테이블이 이미 존재하는지 확인 13 | const checkTableResult = await pool.query(` 14 | SELECT EXISTS ( 15 | SELECT FROM information_schema.tables 16 | WHERE table_schema = 'public' 17 | AND table_name = 'users' 18 | ); 19 | `); 20 | 21 | const tableExists = checkTableResult.rows[0].exists; 22 | 23 | if (tableExists) { 24 | console.log('users 테이블이 이미 존재합니다. 기존 테이블을 사용합니다.'); 25 | } else { 26 | // users 테이블 생성 27 | await pool.query(` 28 | CREATE TABLE users ( 29 | id SERIAL PRIMARY KEY, 30 | name VARCHAR(100) NOT NULL, 31 | email VARCHAR(100) UNIQUE NOT NULL, 32 | status VARCHAR(20), 33 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 34 | ); 35 | `); 36 | console.log('users 테이블이 생성되었습니다.'); 37 | 38 | // 테스트 데이터 추가 39 | await pool.query(` 40 | INSERT INTO users (name, email, status) VALUES 41 | ('홍길동', 'hong@example.com', 'active'), 42 | ('김철수', 'kim@example.com', 'inactive'), 43 | ('이영희', 'lee@example.com', 'active'); 44 | `); 45 | console.log('테스트 데이터가 추가되었습니다.'); 46 | } 47 | 48 | console.log('데이터베이스 설정이 완료되었습니다.'); 49 | } catch (error) { 50 | console.error('데이터베이스 설정 중 오류 발생:', error); 51 | throw error; 52 | } finally { 53 | // 연결 종료 54 | await pool.end(); 55 | } 56 | } 57 | 58 | // 스크립트 실행 59 | setupDatabase().catch(console.error); 60 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-or-is.test.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import { SupaLitePG } from '../postgres-client'; 3 | import { QueryBuilder } from '../query-builder'; 4 | 5 | // Mock the Pool and its query method, and the types object 6 | jest.mock('pg', () => { 7 | const mPool = { 8 | query: jest.fn(), 9 | end: jest.fn(), 10 | on: jest.fn(), 11 | }; 12 | const mTypes = { 13 | setTypeParser: jest.fn(), 14 | }; 15 | return { Pool: jest.fn(() => mPool), types: mTypes }; 16 | }); 17 | 18 | describe('QueryBuilder: or() with .is()', () => { 19 | let client: SupaLitePG; 20 | let pool: Pool; 21 | 22 | beforeEach(() => { 23 | pool = new Pool(); 24 | client = new SupaLitePG({ connectionString: 'postgresql://mock' }); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | it('should generate correct SQL for or() with is(null) condition', async () => { 32 | const qb = new QueryBuilder(pool, client, 'credits', 'public') 33 | .select('*') 34 | .eq('wallet_id', 123) 35 | .gt('amount', 0) 36 | .or('valid_until.is.null,valid_until.gt.now()'); 37 | 38 | // Access the private buildQuery method for testing purposes 39 | const { query, values } = await (qb as any).buildQuery(); 40 | 41 | // Expected SQL structure: 42 | // SELECT * FROM "public"."credits" WHERE "wallet_id" = $1 AND "amount" > $2 AND ("valid_until" IS NULL OR "valid_until" > $3) 43 | 44 | expect(query).toContain('SELECT * FROM "public"."credits"'); 45 | expect(query).toContain('WHERE "wallet_id" = $1 AND "amount" > $2 AND ("valid_until" IS NULL OR "valid_until" > $3)'); 46 | 47 | expect(values).toHaveLength(3); 48 | expect(values[0]).toBe(123); 49 | expect(values[1]).toBe(0); 50 | expect(values[2]).toBe('now()'); // now() is treated as a string value 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-or-is.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const pg_1 = require("pg"); 4 | const postgres_client_1 = require("../postgres-client"); 5 | const query_builder_1 = require("../query-builder"); 6 | // Mock the Pool and its query method, and the types object 7 | jest.mock('pg', () => { 8 | const mPool = { 9 | query: jest.fn(), 10 | end: jest.fn(), 11 | on: jest.fn(), 12 | }; 13 | const mTypes = { 14 | setTypeParser: jest.fn(), 15 | }; 16 | return { Pool: jest.fn(() => mPool), types: mTypes }; 17 | }); 18 | describe('QueryBuilder: or() with .is()', () => { 19 | let client; 20 | let pool; 21 | beforeEach(() => { 22 | pool = new pg_1.Pool(); 23 | client = new postgres_client_1.SupaLitePG({ connectionString: 'postgresql://mock' }); 24 | }); 25 | afterEach(() => { 26 | jest.clearAllMocks(); 27 | }); 28 | it('should generate correct SQL for or() with is(null) condition', async () => { 29 | const qb = new query_builder_1.QueryBuilder(pool, client, 'credits', 'public') 30 | .select('*') 31 | .eq('wallet_id', 123) 32 | .gt('amount', 0) 33 | .or('valid_until.is.null,valid_until.gt.now()'); 34 | // Access the private buildQuery method for testing purposes 35 | const { query, values } = await qb.buildQuery(); 36 | // Expected SQL structure: 37 | // SELECT * FROM "public"."credits" WHERE "wallet_id" = $1 AND "amount" > $2 AND ("valid_until" IS NULL OR "valid_until" > $3) 38 | expect(query).toContain('SELECT * FROM "public"."credits"'); 39 | expect(query).toContain('WHERE "wallet_id" = $1 AND "amount" > $2 AND ("valid_until" IS NULL OR "valid_until" > $3)'); 40 | expect(values).toHaveLength(3); 41 | expect(values[0]).toBe(123); 42 | expect(values[1]).toBe(0); 43 | expect(values[2]).toBe('now()'); // now() is treated as a string value 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-select-is.test.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import { SupaLitePG } from '../postgres-client'; 3 | import { QueryBuilder } from '../query-builder'; 4 | 5 | // Mock the Pool and its query method, and the types object 6 | jest.mock('pg', () => { 7 | const mPool = { 8 | query: jest.fn(), 9 | end: jest.fn(), 10 | on: jest.fn(), 11 | }; 12 | const mTypes = { 13 | setTypeParser: jest.fn(), 14 | }; 15 | return { Pool: jest.fn(() => mPool), types: mTypes }; 16 | }); 17 | 18 | describe('QueryBuilder: select() with .is() and .order()', () => { 19 | let client: SupaLitePG; 20 | let pool: Pool; 21 | 22 | beforeEach(() => { 23 | pool = new Pool(); 24 | client = new SupaLitePG({ connectionString: 'postgresql://mock' }); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | it('should generate correct SQL for select with count, eq, is(null), and order', async () => { 32 | const qb = new QueryBuilder(pool, client, 'orders', 'public') 33 | .select('*', { count: 'exact' }) 34 | .eq('menu_id', 123) 35 | .eq('table_name', 'test_table') 36 | .is('order_closed_time', null) 37 | .order('created_at', { ascending: false }); 38 | 39 | // Access the private buildQuery method for testing purposes 40 | const { query, values } = await (qb as any).buildQuery(); 41 | 42 | // Expected SQL structure: 43 | // SELECT *, COUNT(*) OVER() as exact_count FROM (SELECT * FROM "public"."orders" WHERE "menu_id" = $1 AND "table_name" = $2 AND "order_closed_time" IS NULL) subquery ORDER BY "created_at" DESC 44 | 45 | expect(query).toContain('SELECT *, COUNT(*) OVER() as exact_count FROM'); 46 | expect(query).toContain('(SELECT * FROM "public"."orders"'); 47 | expect(query).toContain('WHERE "menu_id" = $1 AND "table_name" = $2 AND "order_closed_time" IS NULL'); 48 | expect(query).toContain(') subquery'); 49 | expect(query).toContain('ORDER BY "created_at" DESC'); 50 | 51 | expect(values).toHaveLength(2); 52 | expect(values[0]).toBe(123); 53 | expect(values[1]).toBe('test_table'); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/__tests__/query-builder.test.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import { SupaLitePG } from '../postgres-client'; 3 | import { QueryBuilder } from '../query-builder'; 4 | 5 | // Mock the Pool and its query method, and the types object 6 | jest.mock('pg', () => { 7 | const mPool = { 8 | query: jest.fn(), 9 | end: jest.fn(), 10 | on: jest.fn(), // Add 'on' method to the mock pool 11 | }; 12 | const mTypes = { 13 | setTypeParser: jest.fn(), 14 | }; 15 | return { Pool: jest.fn(() => mPool), types: mTypes }; 16 | }); 17 | 18 | describe('QueryBuilder: not()', () => { 19 | let client: SupaLitePG; 20 | let pool: Pool; 21 | 22 | beforeEach(() => { 23 | pool = new Pool(); 24 | // SupaLitePG는 내부적으로 Pool을 생성하므로, 테스트에서는 25 | // jest.mock을 통해 모의 Pool이 사용되도록 설정합니다. 26 | // 생성자에 pool을 직접 전달하는 대신, connectionString을 모의로 전달하거나 27 | // 아무것도 전달하지 않아도 됩니다. 28 | client = new SupaLitePG({ connectionString: 'postgresql://mock' }); 29 | }); 30 | 31 | afterEach(() => { 32 | jest.clearAllMocks(); 33 | }); 34 | 35 | it('should generate a "IS NOT NULL" condition for .not("column", "is", null)', async () => { 36 | const qb = new QueryBuilder(pool, client, 'users', 'public') 37 | .select('name') 38 | .not('email', 'is', null); 39 | 40 | // Access the private buildQuery method for testing purposes 41 | const { query } = await (qb as any).buildQuery(); 42 | 43 | expect(query).toContain('WHERE "email" IS NOT NULL'); 44 | }); 45 | 46 | it('should throw an error for unsupported operators in .not()', () => { 47 | const qb = new QueryBuilder(pool, client, 'users', 'public'); 48 | 49 | expect(() => { 50 | qb.not('status', 'eq', 'active'); 51 | }).toThrow('Operator "eq" is not supported for "not" operation.'); 52 | }); 53 | 54 | it('should handle multiple conditions including .not()', async () => { 55 | const qb = new QueryBuilder(pool, client, 'users', 'public') 56 | .select('*') 57 | .eq('status', 'active') 58 | .not('deleted_at', 'is', null); 59 | 60 | const { query } = await (qb as any).buildQuery(); 61 | 62 | expect(query).toContain('WHERE "status" = $1 AND "deleted_at" IS NOT NULL'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-update-is.test.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import { SupaLitePG } from '../postgres-client'; 3 | import { QueryBuilder } from '../query-builder'; 4 | 5 | // Mock the Pool and its query method, and the types object 6 | jest.mock('pg', () => { 7 | const mPool = { 8 | query: jest.fn(), 9 | end: jest.fn(), 10 | on: jest.fn(), 11 | }; 12 | const mTypes = { 13 | setTypeParser: jest.fn(), 14 | }; 15 | return { Pool: jest.fn(() => mPool), types: mTypes }; 16 | }); 17 | 18 | describe('QueryBuilder: update() with .is()', () => { 19 | let client: SupaLitePG; 20 | let pool: Pool; 21 | 22 | beforeEach(() => { 23 | pool = new Pool(); 24 | client = new SupaLitePG({ connectionString: 'postgresql://mock' }); 25 | // Mock getColumnPgType to return a default type 26 | (client as any).getColumnPgType = jest.fn().mockResolvedValue('text'); 27 | }); 28 | 29 | afterEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | 33 | it('should generate correct SQL for update with is(null) condition', async () => { 34 | const qb = new QueryBuilder(pool, client, 'order_menu_items', 'public') 35 | .update({ 36 | order_closed_time: new Date().toISOString(), 37 | last_act_member_owner_id: 123 38 | }) 39 | .eq('table_name', 'test_table') 40 | .eq('menu_id', 456) 41 | .is('order_closed_time', null) 42 | .select(); 43 | 44 | // Access the private buildQuery method for testing purposes 45 | const { query, values } = await (qb as any).buildQuery(); 46 | 47 | // The update values come first ($1, $2), then the where clause values ($3, $4) 48 | // "order_closed_time" IS NULL does not use a placeholder 49 | 50 | expect(query).toContain('UPDATE "public"."order_menu_items" SET'); 51 | expect(query).toContain('"order_closed_time" = $1'); 52 | expect(query).toContain('"last_act_member_owner_id" = $2'); 53 | expect(query).toContain('WHERE "table_name" = $3 AND "menu_id" = $4 AND "order_closed_time" IS NULL'); 54 | expect(query).toContain('RETURNING *'); // due to .select() 55 | 56 | expect(values).toHaveLength(4); 57 | expect(values[2]).toBe('test_table'); 58 | expect(values[3]).toBe(456); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-select-is.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const pg_1 = require("pg"); 4 | const postgres_client_1 = require("../postgres-client"); 5 | const query_builder_1 = require("../query-builder"); 6 | // Mock the Pool and its query method, and the types object 7 | jest.mock('pg', () => { 8 | const mPool = { 9 | query: jest.fn(), 10 | end: jest.fn(), 11 | on: jest.fn(), 12 | }; 13 | const mTypes = { 14 | setTypeParser: jest.fn(), 15 | }; 16 | return { Pool: jest.fn(() => mPool), types: mTypes }; 17 | }); 18 | describe('QueryBuilder: select() with .is() and .order()', () => { 19 | let client; 20 | let pool; 21 | beforeEach(() => { 22 | pool = new pg_1.Pool(); 23 | client = new postgres_client_1.SupaLitePG({ connectionString: 'postgresql://mock' }); 24 | }); 25 | afterEach(() => { 26 | jest.clearAllMocks(); 27 | }); 28 | it('should generate correct SQL for select with count, eq, is(null), and order', async () => { 29 | const qb = new query_builder_1.QueryBuilder(pool, client, 'orders', 'public') 30 | .select('*', { count: 'exact' }) 31 | .eq('menu_id', 123) 32 | .eq('table_name', 'test_table') 33 | .is('order_closed_time', null) 34 | .order('created_at', { ascending: false }); 35 | // Access the private buildQuery method for testing purposes 36 | const { query, values } = await qb.buildQuery(); 37 | // Expected SQL structure: 38 | // SELECT *, COUNT(*) OVER() as exact_count FROM (SELECT * FROM "public"."orders" WHERE "menu_id" = $1 AND "table_name" = $2 AND "order_closed_time" IS NULL) subquery ORDER BY "created_at" DESC 39 | expect(query).toContain('SELECT *, COUNT(*) OVER() as exact_count FROM'); 40 | expect(query).toContain('(SELECT * FROM "public"."orders"'); 41 | expect(query).toContain('WHERE "menu_id" = $1 AND "table_name" = $2 AND "order_closed_time" IS NULL'); 42 | expect(query).toContain(') subquery'); 43 | expect(query).toContain('ORDER BY "created_at" DESC'); 44 | expect(values).toHaveLength(2); 45 | expect(values[0]).toBe(123); 46 | expect(values[1]).toBe('test_table'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/tests/in.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testIn() { 14 | try { 15 | // 기본 IN 쿼리 테스트 16 | console.log('\n1. 기본 IN 쿼리 테스트:'); 17 | const users = await client 18 | .from('users') 19 | .select('name, email, status') 20 | .in('id', [1, 2, 3]); 21 | console.log('Users with IDs 1, 2, 3:', users.data); 22 | 23 | // 빈 배열 테스트 24 | console.log('\n2. 빈 배열 테스트:'); 25 | const emptyResult = await client 26 | .from('users') 27 | .select('*') 28 | .in('id', []); 29 | console.log('Empty array result (should be null):', emptyResult.data); 30 | 31 | // NULL 값 포함 테스트 32 | console.log('\n3. NULL 값 포함 테스트:'); 33 | const postsWithNulls = await client 34 | .from('posts') 35 | .select('title, content') 36 | .in('user_id', [1, null, 3]); 37 | console.log('Posts with user_ids [1, null, 3]:', postsWithNulls.data); 38 | 39 | // 다른 조건과 함께 사용 40 | console.log('\n4. 다른 조건과 함께 사용:'); 41 | const activeUsers = await client 42 | .from('users') 43 | .select('name, email, status') 44 | .in('id', [1, 2, 3, 4]) 45 | .eq('status', 'active'); 46 | console.log('Active users with specific IDs:', activeUsers.data); 47 | 48 | // 여러 컬럼에 대한 IN 쿼리 49 | console.log('\n5. 여러 컬럼에 대한 IN 쿼리:'); 50 | const multiColumnIn = await client 51 | .from('posts') 52 | .select('title, views') 53 | .in('user_id', [1, 2]) 54 | .in('views', [50, 100, 150]); 55 | console.log('Posts with specific user_ids and views:', multiColumnIn.data); 56 | 57 | // ORDER BY와 함께 사용 58 | console.log('\n6. ORDER BY와 함께 사용:'); 59 | const orderedUsers = await client 60 | .from('users') 61 | .select('name, status, last_login') 62 | .in('id', [1, 2, 3, 4, 5]) 63 | .order('last_login', { ascending: false }); 64 | console.log('Users ordered by last_login:', orderedUsers.data); 65 | 66 | } catch (err) { 67 | console.error('Error:', err); 68 | } finally { 69 | await client.close(); 70 | } 71 | } 72 | 73 | testIn(); 74 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const pg_1 = require("pg"); 4 | const postgres_client_1 = require("../postgres-client"); 5 | const query_builder_1 = require("../query-builder"); 6 | // Mock the Pool and its query method, and the types object 7 | jest.mock('pg', () => { 8 | const mPool = { 9 | query: jest.fn(), 10 | end: jest.fn(), 11 | on: jest.fn(), // Add 'on' method to the mock pool 12 | }; 13 | const mTypes = { 14 | setTypeParser: jest.fn(), 15 | }; 16 | return { Pool: jest.fn(() => mPool), types: mTypes }; 17 | }); 18 | describe('QueryBuilder: not()', () => { 19 | let client; 20 | let pool; 21 | beforeEach(() => { 22 | pool = new pg_1.Pool(); 23 | // SupaLitePG는 내부적으로 Pool을 생성하므로, 테스트에서는 24 | // jest.mock을 통해 모의 Pool이 사용되도록 설정합니다. 25 | // 생성자에 pool을 직접 전달하는 대신, connectionString을 모의로 전달하거나 26 | // 아무것도 전달하지 않아도 됩니다. 27 | client = new postgres_client_1.SupaLitePG({ connectionString: 'postgresql://mock' }); 28 | }); 29 | afterEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | it('should generate a "IS NOT NULL" condition for .not("column", "is", null)', async () => { 33 | const qb = new query_builder_1.QueryBuilder(pool, client, 'users', 'public') 34 | .select('name') 35 | .not('email', 'is', null); 36 | // Access the private buildQuery method for testing purposes 37 | const { query } = await qb.buildQuery(); 38 | expect(query).toContain('WHERE "email" IS NOT NULL'); 39 | }); 40 | it('should throw an error for unsupported operators in .not()', () => { 41 | const qb = new query_builder_1.QueryBuilder(pool, client, 'users', 'public'); 42 | expect(() => { 43 | qb.not('status', 'eq', 'active'); 44 | }).toThrow('Operator "eq" is not supported for "not" operation.'); 45 | }); 46 | it('should handle multiple conditions including .not()', async () => { 47 | const qb = new query_builder_1.QueryBuilder(pool, client, 'users', 'public') 48 | .select('*') 49 | .eq('status', 'active') 50 | .not('deleted_at', 'is', null); 51 | const { query } = await qb.buildQuery(); 52 | expect(query).toContain('WHERE "status" = $1 AND "deleted_at" IS NOT NULL'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-update-is.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const pg_1 = require("pg"); 4 | const postgres_client_1 = require("../postgres-client"); 5 | const query_builder_1 = require("../query-builder"); 6 | // Mock the Pool and its query method, and the types object 7 | jest.mock('pg', () => { 8 | const mPool = { 9 | query: jest.fn(), 10 | end: jest.fn(), 11 | on: jest.fn(), 12 | }; 13 | const mTypes = { 14 | setTypeParser: jest.fn(), 15 | }; 16 | return { Pool: jest.fn(() => mPool), types: mTypes }; 17 | }); 18 | describe('QueryBuilder: update() with .is()', () => { 19 | let client; 20 | let pool; 21 | beforeEach(() => { 22 | pool = new pg_1.Pool(); 23 | client = new postgres_client_1.SupaLitePG({ connectionString: 'postgresql://mock' }); 24 | // Mock getColumnPgType to return a default type 25 | client.getColumnPgType = jest.fn().mockResolvedValue('text'); 26 | }); 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | it('should generate correct SQL for update with is(null) condition', async () => { 31 | const qb = new query_builder_1.QueryBuilder(pool, client, 'order_menu_items', 'public') 32 | .update({ 33 | order_closed_time: new Date().toISOString(), 34 | last_act_member_owner_id: 123 35 | }) 36 | .eq('table_name', 'test_table') 37 | .eq('menu_id', 456) 38 | .is('order_closed_time', null) 39 | .select(); 40 | // Access the private buildQuery method for testing purposes 41 | const { query, values } = await qb.buildQuery(); 42 | // The update values come first ($1, $2), then the where clause values ($3, $4) 43 | // "order_closed_time" IS NULL does not use a placeholder 44 | expect(query).toContain('UPDATE "public"."order_menu_items" SET'); 45 | expect(query).toContain('"order_closed_time" = $1'); 46 | expect(query).toContain('"last_act_member_owner_id" = $2'); 47 | expect(query).toContain('WHERE "table_name" = $3 AND "menu_id" = $4 AND "order_closed_time" IS NULL'); 48 | expect(query).toContain('RETURNING *'); // due to .select() 49 | expect(values).toHaveLength(4); 50 | expect(values[2]).toBe('test_table'); 51 | expect(values[3]).toBe(456); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /docs/changelog/2025-06-10-jsonb-array-test.md: -------------------------------------------------------------------------------- 1 | ## 2025-06-10: Added JSONB Array Tests 2 | 3 | - Modified `src/__tests__/query-builder-single.test.ts`: 4 | - Updated `JsonbTestTable` related types to use the global `Json` type. 5 | - Added `another_json_field` (JSONB) to `jsonb_test_table` schema and tests. 6 | - Modified `beforeAll` to `DROP TABLE IF EXISTS jsonb_test_table` before `CREATE TABLE` to ensure schema updates are applied, fixing a "column does not exist" error during tests. 7 | - Removed explicit `JSON.stringify()` from test cases, as this is now handled automatically by `QueryBuilder` based on schema information. 8 | - Added a new test case for inserting/selecting an object into `another_json_field`. 9 | - Added a test case for inserting an empty JavaScript array `[]` into a `jsonb` field, now handled automatically. 10 | - Modified `src/postgres-client.ts`: 11 | - Added `schemaCache` to store column type information fetched from `information_schema.columns`. 12 | - Implemented `getColumnPgType(schema, table, column)` method to retrieve (and cache) PostgreSQL data types for columns. 13 | - Added `verbose` option to `SupaliteConfig` and `SupaLitePG` for logging. 14 | - Modified `from()` method to pass `SupaLitePG` instance and `verbose` setting to `QueryBuilder`. 15 | - Modified `src/query-builder.ts`: 16 | - Updated constructor to accept `SupaLitePG` client instance and `verbose` setting. 17 | - Made `buildQuery()` method `async`. 18 | - Implemented schema-aware value processing in `buildQuery()` for `INSERT`, `UPSERT`, and `UPDATE`: 19 | - Uses `client.getColumnPgType()` to get the PostgreSQL type of each column. 20 | - If `pgType` is 'json' or 'jsonb', JavaScript objects and arrays are `JSON.stringify()`'d. 21 | - If `pgType` is 'bigint', JavaScript `BigInt`s are `toString()`'d. 22 | - Otherwise (e.g., for `text[]`, `integer[]`), values (including JavaScript arrays) are passed as-is to the `pg` driver. 23 | - Updated `execute()` method to `await buildQuery()` and include verbose logging for SQL and values if enabled. 24 | - Ensured `src/types.ts` contains the `Json` type and `verbose` option in `SupaliteConfig`. 25 | - **Outcome**: `QueryBuilder` now intelligently handles serialization for `json`/`jsonb`, `bigint`, and native array types based on runtime schema information, providing a more seamless experience similar to `supabase-js`. All related tests pass. 26 | -------------------------------------------------------------------------------- /examples/tests/select.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testSelect() { 14 | try { 15 | // 기본 SELECT 16 | console.log('\n1. 기본 SELECT 테스트:'); 17 | const users = await client 18 | .from('users') 19 | .select('*') 20 | .limit(2); 21 | console.log('Users:', users.data); 22 | 23 | // 특정 컬럼 선택 24 | console.log('\n2. 특정 컬럼 SELECT 테스트:'); 25 | const profiles = await client 26 | .from('profiles') 27 | .select('user_id, bio, interests') 28 | .limit(2); 29 | console.log('Profiles:', profiles.data); 30 | 31 | // COUNT 쿼리 32 | console.log('\n3. COUNT 쿼리 테스트:'); 33 | const activeUsers = await client 34 | .from('users') 35 | .select('*', { count: 'exact' }) 36 | .eq('status', 'active'); 37 | console.log('Active users count:', activeUsers.count); 38 | 39 | // 정렬 40 | console.log('\n4. 정렬 테스트:'); 41 | const sortedPosts = await client 42 | .from('posts') 43 | .select('title, views') 44 | .order('views', { ascending: false }) 45 | .limit(3); 46 | console.log('Top 3 posts by views:', sortedPosts.data); 47 | 48 | // 다중 정렬 49 | console.log('\n5. 다중 정렬 테스트:'); 50 | const usersByStatus = await client 51 | .from('users') 52 | .select('name, status, last_login') 53 | .order('status', { ascending: true }) 54 | .order('last_login', { ascending: false }); 55 | console.log('Users sorted by status and last_login:', usersByStatus.data); 56 | 57 | // 페이지네이션 58 | console.log('\n6. 페이지네이션 테스트:'); 59 | const page1 = await client 60 | .from('posts') 61 | .select('*') 62 | .limit(2) 63 | .offset(0); 64 | console.log('Page 1:', page1.data); 65 | 66 | const page2 = await client 67 | .from('posts') 68 | .select('*') 69 | .limit(2) 70 | .offset(2); 71 | console.log('Page 2:', page2.data); 72 | 73 | // Range 74 | console.log('\n7. Range 테스트:'); 75 | const range = await client 76 | .from('comments') 77 | .select('*') 78 | .range(1, 3); 79 | console.log('Comments range 1-3:', range.data); 80 | 81 | } catch (err) { 82 | console.error('Error:', err); 83 | } finally { 84 | await client.close(); 85 | } 86 | } 87 | 88 | testSelect(); 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | .npm-cache 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # npm user config (may contain auth tokens) 86 | .npmrc 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | .parcel-cache 91 | 92 | # Next.js build output 93 | .next 94 | out 95 | 96 | # Nuxt.js build / generate output 97 | .nuxt 98 | # dist - 빌드된 파일을 GitHub에 포함하기 위해 주석 처리 99 | # dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | .cache 113 | 114 | # Docusaurus cache and generated files 115 | .docusaurus 116 | 117 | # Serverless directories 118 | .serverless/ 119 | 120 | # FuseBox cache 121 | .fusebox/ 122 | 123 | # DynamoDB Local files 124 | .dynamodb/ 125 | 126 | # TernJS port file 127 | .tern-port 128 | 129 | # Stores VSCode versions used for testing VSCode extensions 130 | .vscode-test 131 | 132 | # yarn v2 133 | .yarn/cache 134 | .yarn/unplugged 135 | .yarn/build-state.yml 136 | .yarn/install-state.gz 137 | .pnp.* 138 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Lock files 2 | package-lock.json 3 | yarn.lock 4 | pnpm-lock.yaml 5 | bun.lock 6 | .yarn/ 7 | .pnp.* 8 | 9 | # Source files (TypeScript) 10 | src/ 11 | examples/ 12 | tsconfig.json 13 | jest.config.js 14 | .eslintrc.json 15 | .prettierrc 16 | 17 | # Git files 18 | .git/ 19 | .gitignore 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | .pnpm-debug.log* 29 | 30 | # Diagnostic reports (https://nodejs.org/api/report.html) 31 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 32 | 33 | # Runtime data 34 | pids 35 | *.pid 36 | *.seed 37 | *.pid.lock 38 | 39 | # Directory for instrumented libs generated by jscoverage/JSCover 40 | lib-cov 41 | 42 | # Coverage directory used by tools like istanbul 43 | coverage 44 | *.lcov 45 | 46 | # nyc test coverage 47 | .nyc_output 48 | 49 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 50 | .grunt 51 | 52 | # Bower dependency directory (https://bower.io/) 53 | bower_components 54 | 55 | # node-waf configuration 56 | .lock-wscript 57 | 58 | # Compiled binary addons (https://nodejs.org/api/addons.html) 59 | build/Release 60 | 61 | # Dependency directories 62 | node_modules/ 63 | jspm_packages/ 64 | 65 | # Snowpack dependency directory (https://snowpack.dev/) 66 | web_modules/ 67 | 68 | # TypeScript cache 69 | *.tsbuildinfo 70 | 71 | # Optional npm cache directory 72 | .npm 73 | .npm-cache 74 | 75 | # Optional eslint cache 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | .stylelintcache 80 | 81 | # Microbundle cache 82 | .rpt2_cache/ 83 | .rts2_cache_cjs/ 84 | .rts2_cache_es/ 85 | .rts2_cache_umd/ 86 | 87 | # Optional REPL history 88 | .node_repl_history 89 | 90 | # Output of 'npm pack' 91 | *.tgz 92 | 93 | # Yarn Integrity file 94 | .yarn-integrity 95 | 96 | # dotenv environment variable files 97 | .env 98 | .env.example 99 | .env.development.local 100 | .env.test.local 101 | .env.production.local 102 | .env.local 103 | 104 | # npm user config (may contain auth tokens) 105 | .npmrc 106 | 107 | # parcel-bundler cache (https://parceljs.org/) 108 | .cache 109 | .parcel-cache 110 | 111 | # Next.js build output 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | .nuxt 117 | 118 | # Gatsby files 119 | .cache/ 120 | # Comment in the public line in if your project uses Gatsby and not Next.js 121 | # https://nextjs.org/blog/next-9-1#public-directory-support 122 | # public 123 | 124 | # vuepress build output 125 | .vuepress/dist 126 | 127 | # vuepress v2.x temp and cache directory 128 | .temp 129 | .cache 130 | 131 | # Docusaurus cache and generated files 132 | .docusaurus 133 | 134 | # Serverless directories 135 | .serverless/ 136 | 137 | # FuseBox cache 138 | .fusebox/ 139 | 140 | # DynamoDB Local files 141 | .dynamodb/ 142 | 143 | # TernJS port file 144 | .tern-port 145 | 146 | # Stores VSCode versions used for testing VSCode extensions 147 | .vscode-test 148 | 149 | # Test files 150 | **/*.test.ts 151 | **/__tests__/ 152 | -------------------------------------------------------------------------------- /dist/postgres-client.d.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import { QueryBuilder } from './query-builder'; 3 | import { PostgresError } from './errors'; 4 | import { TableOrViewName, SupaliteConfig, Row, QueryResult, SingleQueryResult } from './types'; 5 | type SchemaWithTables = { 6 | Tables: { 7 | [key: string]: { 8 | Row: any; 9 | Insert: any; 10 | Update: any; 11 | Relationships: unknown[]; 12 | }; 13 | }; 14 | Views?: { 15 | [key: string]: { 16 | Row: any; 17 | }; 18 | }; 19 | Functions?: any; 20 | Enums?: any; 21 | CompositeTypes?: any; 22 | }; 23 | export declare class RpcBuilder implements Promise { 24 | private pool; 25 | private schema; 26 | private procedureName; 27 | private params; 28 | readonly [Symbol.toStringTag] = "RpcBuilder"; 29 | private singleMode; 30 | constructor(pool: Pool, schema: string, procedureName: string, params?: Record); 31 | single(): this; 32 | maybeSingle(): this; 33 | then(onfulfilled?: ((value: any) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null): Promise; 34 | catch(onrejected?: ((reason: any) => TResult | PromiseLike) | null): Promise; 35 | finally(onfinally?: (() => void) | null): Promise; 36 | execute(): Promise<{ 37 | data: any; 38 | error: PostgresError | null; 39 | count?: number | null; 40 | status?: number; 41 | statusText?: string; 42 | }>; 43 | } 44 | export declare class SupaLitePG { 47 | private pool; 48 | private client; 49 | private isTransaction; 50 | private schema; 51 | private schemaCache; 52 | private foreignKeyCache; 53 | verbose: boolean; 54 | private bigintTransform; 55 | constructor(config?: SupaliteConfig); 56 | begin(): Promise; 57 | commit(): Promise; 58 | rollback(): Promise; 59 | transaction(callback: (client: SupaLitePG) => Promise): Promise; 60 | from>(table: K): QueryBuilder & Promise>> & { 61 | single(): Promise>>; 62 | }; 63 | from>(table: K, schema: S): QueryBuilder & Promise>> & { 64 | single(): Promise>>; 65 | }; 66 | getColumnPgType(dbSchema: string, tableName: string, columnName: string): Promise; 67 | getForeignKey(schema: string, table: string, foreignTable: string): Promise<{ 68 | column: string; 69 | foreignColumn: string; 70 | isArray: boolean; 71 | } | null>; 72 | rpc(procedureName: string, params?: Record): RpcBuilder; 73 | testConnection(): Promise; 74 | close(): Promise; 75 | } 76 | export declare const supalitePg: SupaLitePG<{ 77 | [K: string]: SchemaWithTables; 78 | }>; 79 | export {}; 80 | -------------------------------------------------------------------------------- /dist/__tests__/query-result.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const errors_1 = require("../errors"); 4 | describe('QueryResult 타입 테스트', () => { 5 | // 결과가 있는 쿼리 테스트 6 | test('데이터가 있는 경우 배열이 정상적으로 반환되는지 확인', () => { 7 | const mockResult = { 8 | data: [{ id: 1, name: 'test' }, { id: 2, name: 'test2' }], 9 | error: null, 10 | count: 2, 11 | status: 200, 12 | statusText: 'OK', 13 | }; 14 | // 배열 타입 확인 15 | expect(Array.isArray(mockResult.data)).toBe(true); 16 | // length 속성 확인 17 | expect(mockResult.data.length).toBe(2); 18 | // 배열 메서드 사용 확인 19 | const mapped = mockResult.data.map(item => item.id); 20 | expect(mapped).toEqual([1, 2]); 21 | const filtered = mockResult.data.filter(item => item.id === 1); 22 | expect(filtered).toEqual([{ id: 1, name: 'test' }]); 23 | }); 24 | // 결과가 없는 쿼리 테스트 25 | test('데이터가 없는 경우 빈 배열이 반환되는지 확인', () => { 26 | const mockResult = { 27 | data: [], 28 | error: null, 29 | count: 0, 30 | status: 200, 31 | statusText: 'OK', 32 | }; 33 | // 배열 타입 확인 34 | expect(Array.isArray(mockResult.data)).toBe(true); 35 | // length 속성 확인 36 | expect(mockResult.data.length).toBe(0); 37 | // 빈 배열에 배열 메서드 사용 확인 38 | const mapped = mockResult.data.map(item => item); 39 | expect(mapped).toEqual([]); 40 | const filtered = mockResult.data.filter(() => true); 41 | expect(filtered).toEqual([]); 42 | }); 43 | // 에러 발생 시 테스트 44 | test('에러가 발생했을 때 data 필드가 빈 배열을 반환하는지 확인', () => { 45 | const mockResult = { 46 | data: [], 47 | error: new errors_1.PostgresError('테스트 에러'), 48 | count: null, 49 | status: 500, 50 | statusText: 'Internal Server Error', 51 | }; 52 | // 배열 타입 확인 53 | expect(Array.isArray(mockResult.data)).toBe(true); 54 | // length 속성 확인 55 | expect(mockResult.data.length).toBe(0); 56 | // 에러 정보 확인 57 | expect(mockResult.error).not.toBeNull(); 58 | expect(mockResult.error?.message).toBe('테스트 에러'); 59 | }); 60 | // 타입 호환성 테스트 61 | test('배열 메서드를 사용할 수 있는지 확인', () => { 62 | const mockResult = { 63 | data: [{ id: 1, name: 'test' }, { id: 2, name: 'test2' }], 64 | error: null, 65 | count: 2, 66 | status: 200, 67 | statusText: 'OK', 68 | }; 69 | // forEach 메서드 사용 70 | const ids = []; 71 | mockResult.data.forEach(item => { 72 | ids.push(item.id); 73 | }); 74 | expect(ids).toEqual([1, 2]); 75 | // reduce 메서드 사용 76 | const sum = mockResult.data.reduce((acc, item) => acc + item.id, 0); 77 | expect(sum).toBe(3); 78 | // some 메서드 사용 79 | const hasId2 = mockResult.data.some(item => item.id === 2); 80 | expect(hasId2).toBe(true); 81 | // every 메서드 사용 82 | const allHaveNames = mockResult.data.every(item => item.name.length > 0); 83 | expect(allHaveNames).toBe(true); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /examples/tests/where.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testWhere() { 14 | try { 15 | // eq 테스트 16 | console.log('\n1. eq 테스트:'); 17 | const activeUser = await client 18 | .from('users') 19 | .select('*') 20 | .eq('status', 'active') 21 | .single(); 22 | console.log('Active user:', activeUser.data); 23 | 24 | // neq 테스트 25 | console.log('\n2. neq 테스트:'); 26 | const nonActiveUsers = await client 27 | .from('users') 28 | .select('name, status') 29 | .neq('status', 'active'); 30 | console.log('Non-active users:', nonActiveUsers.data); 31 | 32 | // is 테스트 (NULL 체크) 33 | console.log('\n3. is 테스트:'); 34 | const noAvatarProfiles = await client 35 | .from('profiles') 36 | .select('user_id, bio') 37 | .is('avatar_url', null); 38 | console.log('Profiles without avatar:', noAvatarProfiles.data); 39 | 40 | // in 테스트 41 | console.log('\n4. in 테스트:'); 42 | const specificUsers = await client 43 | .from('users') 44 | .select('*') 45 | .in('id', [1, 2, 3]); 46 | console.log('Users with specific IDs:', specificUsers.data); 47 | 48 | // contains 테스트 (배열) 49 | console.log('\n5. contains 테스트:'); 50 | const travelProfiles = await client 51 | .from('profiles') 52 | .select('*') 53 | .contains('interests', ['여행']); 54 | console.log('Profiles interested in travel:', travelProfiles.data); 55 | 56 | // ilike 테스트 57 | console.log('\n6. ilike 테스트:'); 58 | const searchUsers = await client 59 | .from('users') 60 | .select('*') 61 | .ilike('email', '%example.com'); 62 | console.log('Users with example.com email:', searchUsers.data); 63 | 64 | // gte/lte 테스트 (날짜 범위) 65 | console.log('\n7. gte/lte 테스트:'); 66 | const recentLogins = await client 67 | .from('users') 68 | .select('name, last_login') 69 | .gte('last_login', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()) 70 | .order('last_login', { ascending: false }); 71 | console.log('Users logged in within 7 days:', recentLogins.data); 72 | 73 | // or 테스트 74 | console.log('\n8. or 테스트:'); 75 | const popularOrRecent = await client 76 | .from('posts') 77 | .select('title, views, created_at') 78 | .or('views.gte.100,created_at.gte.' + new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()); 79 | console.log('Popular or recent posts:', popularOrRecent.data); 80 | 81 | // 복합 조건 82 | console.log('\n9. 복합 조건 테스트:'); 83 | const complexQuery = await client 84 | .from('posts') 85 | .select('*') 86 | .eq('user_id', 1) 87 | .gte('views', 50) 88 | .contains('tags', ['여행']) 89 | .order('created_at', { ascending: false }); 90 | console.log('Complex query result:', complexQuery.data); 91 | 92 | } catch (err) { 93 | console.error('Error:', err); 94 | } finally { 95 | await client.close(); 96 | } 97 | } 98 | 99 | testWhere(); 100 | -------------------------------------------------------------------------------- /src/__tests__/query-result.test.ts: -------------------------------------------------------------------------------- 1 | import { PostgresError } from '../errors'; 2 | import { QueryResult } from '../types'; 3 | 4 | // Jest 전역 변수 선언 5 | declare const describe: any; 6 | declare const test: any; 7 | declare const expect: any; 8 | 9 | describe('QueryResult 타입 테스트', () => { 10 | // 결과가 있는 쿼리 테스트 11 | test('데이터가 있는 경우 배열이 정상적으로 반환되는지 확인', () => { 12 | const mockResult: QueryResult = { 13 | data: [{ id: 1, name: 'test' }, { id: 2, name: 'test2' }], 14 | error: null, 15 | count: 2, 16 | status: 200, 17 | statusText: 'OK', 18 | }; 19 | 20 | // 배열 타입 확인 21 | expect(Array.isArray(mockResult.data)).toBe(true); 22 | 23 | // length 속성 확인 24 | expect(mockResult.data.length).toBe(2); 25 | 26 | // 배열 메서드 사용 확인 27 | const mapped = mockResult.data.map(item => item.id); 28 | expect(mapped).toEqual([1, 2]); 29 | 30 | const filtered = mockResult.data.filter(item => item.id === 1); 31 | expect(filtered).toEqual([{ id: 1, name: 'test' }]); 32 | }); 33 | 34 | // 결과가 없는 쿼리 테스트 35 | test('데이터가 없는 경우 빈 배열이 반환되는지 확인', () => { 36 | const mockResult: QueryResult = { 37 | data: [], 38 | error: null, 39 | count: 0, 40 | status: 200, 41 | statusText: 'OK', 42 | }; 43 | 44 | // 배열 타입 확인 45 | expect(Array.isArray(mockResult.data)).toBe(true); 46 | 47 | // length 속성 확인 48 | expect(mockResult.data.length).toBe(0); 49 | 50 | // 빈 배열에 배열 메서드 사용 확인 51 | const mapped = mockResult.data.map(item => item); 52 | expect(mapped).toEqual([]); 53 | 54 | const filtered = mockResult.data.filter(() => true); 55 | expect(filtered).toEqual([]); 56 | }); 57 | 58 | // 에러 발생 시 테스트 59 | test('에러가 발생했을 때 data 필드가 빈 배열을 반환하는지 확인', () => { 60 | const mockResult: QueryResult = { 61 | data: [], 62 | error: new PostgresError('테스트 에러'), 63 | count: null, 64 | status: 500, 65 | statusText: 'Internal Server Error', 66 | }; 67 | 68 | // 배열 타입 확인 69 | expect(Array.isArray(mockResult.data)).toBe(true); 70 | 71 | // length 속성 확인 72 | expect(mockResult.data.length).toBe(0); 73 | 74 | // 에러 정보 확인 75 | expect(mockResult.error).not.toBeNull(); 76 | expect(mockResult.error?.message).toBe('테스트 에러'); 77 | }); 78 | 79 | // 타입 호환성 테스트 80 | test('배열 메서드를 사용할 수 있는지 확인', () => { 81 | const mockResult: QueryResult<{ id: number; name: string }> = { 82 | data: [{ id: 1, name: 'test' }, { id: 2, name: 'test2' }], 83 | error: null, 84 | count: 2, 85 | status: 200, 86 | statusText: 'OK', 87 | }; 88 | 89 | // forEach 메서드 사용 90 | const ids: number[] = []; 91 | mockResult.data.forEach(item => { 92 | ids.push(item.id); 93 | }); 94 | expect(ids).toEqual([1, 2]); 95 | 96 | // reduce 메서드 사용 97 | const sum = mockResult.data.reduce((acc, item) => acc + item.id, 0); 98 | expect(sum).toBe(3); 99 | 100 | // some 메서드 사용 101 | const hasId2 = mockResult.data.some(item => item.id === 2); 102 | expect(hasId2).toBe(true); 103 | 104 | // every 메서드 사용 105 | const allHaveNames = mockResult.data.every(item => item.name.length > 0); 106 | expect(allHaveNames).toBe(true); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | import { PostgresError } from './errors'; 2 | export type Json = string | number | bigint | boolean | null | { 3 | [key: string]: Json | undefined; 4 | } | Json[]; 5 | export interface TableBase { 6 | Row: any; 7 | Insert: any; 8 | Update: any; 9 | Relationships: unknown[]; 10 | } 11 | export interface SchemaDefinition { 12 | Tables: { 13 | [key: string]: TableBase; 14 | }; 15 | Views?: { 16 | [key: string]: any; 17 | }; 18 | Functions?: { 19 | [key: string]: any; 20 | }; 21 | Enums?: { 22 | [key: string]: any; 23 | }; 24 | CompositeTypes?: { 25 | [key: string]: any; 26 | }; 27 | } 28 | export interface DatabaseSchema { 29 | [schema: string]: SchemaDefinition; 30 | } 31 | export type AsDatabaseSchema = { 32 | [K in keyof T]: T[K] extends { 33 | Tables: any; 34 | } ? SchemaDefinition & T[K] : never; 35 | }; 36 | export type SchemaName = keyof T; 37 | export type TableName = SchemaName> = keyof T[S]['Tables']; 38 | export type ViewName = SchemaName> = keyof NonNullable; 39 | export type TableOrViewName = SchemaName> = TableName | ViewName; 40 | export type Row, K extends TableOrViewName> = K extends TableName ? T[S]['Tables'][K]['Row'] : K extends ViewName ? NonNullable[K]['Row'] : never; 41 | export type InsertRow, K extends TableOrViewName> = K extends TableName ? T[S]['Tables'][K]['Insert'] : never; 42 | export type UpdateRow, K extends TableOrViewName> = K extends TableName ? T[S]['Tables'][K]['Update'] & { 43 | modified_at?: string | null; 44 | updated_at?: string | null; 45 | } : never; 46 | export type EnumType, E extends keyof NonNullable> = NonNullable[E]; 47 | export type BigintTransformType = 'bigint' | 'string' | 'number'; 48 | export interface SupaliteConfig { 49 | connectionString?: string; 50 | bigintTransform?: BigintTransformType; 51 | user?: string; 52 | host?: string; 53 | database?: string; 54 | password?: string; 55 | port?: number; 56 | ssl?: boolean; 57 | schema?: string; 58 | verbose?: boolean; 59 | } 60 | export type QueryType = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'UPSERT'; 61 | export interface QueryOptions { 62 | limit?: number; 63 | offset?: number; 64 | order?: { 65 | column: string; 66 | ascending?: boolean; 67 | }; 68 | } 69 | export interface FilterOptions { 70 | column: string; 71 | operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike'; 72 | value: any; 73 | } 74 | export type BaseResult = { 75 | error: PostgresError | null; 76 | count: number | null; 77 | status: number; 78 | statusText: string; 79 | statusCode?: number; 80 | }; 81 | export type QueryResult = BaseResult & { 82 | data: Array; 83 | }; 84 | export type SingleQueryResult = BaseResult & { 85 | data: T | null; 86 | }; 87 | -------------------------------------------------------------------------------- /CHANGE_REPORT_LOG.md: -------------------------------------------------------------------------------- 1 | # 변경 작업 보고서 2 | 3 | ## [2025-03-04] 누락된 dist 파일들을 포함하도록 수정 4 | 5 | ### 작업 내용 6 | - 빌드 시 생성되는 dist 디렉토리의 파일들이 이전 커밋에 포함되지 않은 문제를 발견했습니다. 7 | - dist 디렉토리를 삭제하고 다시 빌드하여 누락된 파일들을 생성했습니다. 8 | - package.json 파일의 버전을 0.1.8로 업데이트했습니다. 9 | - CHANGELOG.md 파일에 변경 사항을 기록했습니다. 10 | 11 | ### 변경된 파일 12 | - dist/* 13 | - package.json 14 | - CHANGELOG.md 15 | 16 | ### 개발 과정 17 | 1. dist 디렉토리 삭제 18 | 2. npm run build 명령어 실행 19 | 3. git add . 명령어로 변경된 파일 스테이징 20 | 4. git commit -m "fix: Include missing files in dist directory" 명령어로 커밋 21 | 5. package.json 파일의 버전 업데이트 22 | 6. CHANGELOG.md 파일에 변경 사항 기록 23 | 24 | ### 결론 25 | 이번 작업을 통해 누락되었던 dist 파일들을 커밋에 포함시켰습니다. 26 | 27 | --- 28 | 29 | ## [2025-03-04] QueryBuilder에 match 메서드 추가 30 | 31 | ### 작업 내용 32 | 33 | 1. **`match` 메서드 구현**: 34 | - `QueryBuilder` 클래스에 `match` 메서드를 추가했습니다. 35 | - 이 메서드는 객체를 인자로 받아, 객체의 각 키-값 쌍을 `"${key}" = $${index}` 형태의 조건으로 변환하여 `whereConditions` 배열에 추가합니다. 36 | - 이를 통해 사용자는 객체 리터럴을 사용하여 간편하게 쿼리 조건을 추가할 수 있습니다. 37 | 38 | 2. **테스트용 테이블 생성**: 39 | - `examples/setup.sql` 파일에 `test_table`을 생성하는 SQL 문을 추가했습니다. 40 | - 이 테이블은 `match` 메서드의 동작을 테스트하는 데 사용됩니다. 41 | 42 | 3. **테스트용 함수 구현**: 43 | - `src/test-table.ts` 파일에 `test_table`과 상호작용하는 함수를 작성했습니다. 44 | - `insertIntoTestTable` 함수는 `test_table`에 데이터를 삽입합니다. 45 | - `getFromTestTable` 함수는 `test_table`에서 데이터를 조회하고, `match` 메서드를 사용하여 조건을 적용합니다. 46 | 47 | 4. **테스트 코드 작성**: 48 | - `examples/tests/query-builder.test.ts` 파일에 `match` 메서드를 테스트하는 코드를 작성했습니다. 49 | - 이 코드는 `test_table`에 데이터를 삽입하고, `match` 메서드를 사용하여 다양한 조건으로 데이터를 조회합니다. 50 | - 조회 결과가 예상과 일치하는지 확인합니다. 51 | 52 | 5. **테스트 실행**: 53 | - `bun examples/tests/query-builder.test.ts` 명령어를 사용하여 테스트를 실행했습니다. 54 | - 모든 테스트가 성공적으로 완료되었습니다. 55 | 56 | 6. **문서화**: 57 | - CHANGELOG.md 파일에 `match` 메서드 추가 사항을 기록했습니다. 58 | - 버전을 0.1.7로 업데이트했습니다. 59 | 60 | ### 변경된 파일 61 | 62 | 1. `src/query-builder.ts`: `match` 메서드 추가 63 | 2. `examples/setup.sql`: `test_table` 생성 SQL 문 추가 64 | 3. `src/test-table.ts`: `test_table` 관련 함수 구현 65 | 4. `examples/tests/query-builder.test.ts`: `match` 메서드 테스트 코드 작성 66 | 5. `CHANGELOG.md`: 변경 사항 문서화 및 버전 업데이트 67 | 68 | ### 개발 과정 69 | 70 | 1. `feature/add-match-method` 브랜치 생성 71 | 2. `QueryBuilder` 클래스에 `match` 메서드 구현 72 | 3. `examples/setup.sql` 파일에 `test_table` 생성 SQL 문 추가 73 | 4. `src/test-table.ts` 파일에 `test_table` 관련 함수 구현 74 | 5. `examples/tests/query-builder.test.ts` 파일에 `match` 메서드 테스트 코드 작성 75 | 6. 테스트 실행 및 결과 확인 76 | 7. 문서화 및 버전 업데이트 77 | 8. 변경 사항 커밋 78 | 9. main 브랜치로 병합 79 | 80 | ### 테스트 결과 81 | 82 | 테스트 결과는 다음과 같습니다: 83 | 84 | ``` 85 | Result 1: [ 86 | { 87 | id: 1 88 | name: "test1" 89 | value: 10 90 | } 91 | ] 92 | Result 2: [ 93 | { 94 | id: 2 95 | name: "test2" 96 | value: 20 97 | } 98 | ] 99 | Result 3: [ 100 | { 101 | id: 1 102 | name: "test1" 103 | value: 10 104 | } 105 | ] 106 | Result 4: null 107 | ``` 108 | 109 | Result1, 2, 3은 예상대로 출력되었고, Result 4는 존재하지 않는 데이터에 대한 결과로 null을 반환했습니다. 110 | 111 | ### 결론 112 | 113 | 이번 작업을 통해 `QueryBuilder` 클래스에 `match` 메서드를 추가하여, 사용자가 객체 리터럴을 사용하여 간편하게 쿼리 조건을 추가할 수 있게 되었습니다. 또한, 테스트 코드를 통해 `match` 메서드의 동작을 검증했습니다. 114 | 115 | --- 116 | 117 | # 변경 작업 보고서 118 | 119 | ## [2025-03-01] corepack을 통한 다중 패키지 관리자 지원 추가 120 | 121 | ### 작업 내용 122 | 123 | ... (생략) ... 124 | -------------------------------------------------------------------------------- /docs/changelog/2025-06-10-bigint-handling-enhancement.md: -------------------------------------------------------------------------------- 1 | # Changelog - 2025-06-10: Enhanced BIGINT Handling 2 | 3 | ## Summary 4 | This update introduces a flexible mechanism for handling `BIGINT` data types retrieved from PostgreSQL, addressing potential `TypeError` issues during JSON serialization and providing users with more control over type conversion. 5 | 6 | ## Changes 7 | 8 | ### ✨ New Features 9 | * **Configurable `BIGINT` Transformation**: 10 | * Added a new `bigintTransform` option to the `SupaLitePG` constructor configuration (`SupaliteConfig`). 11 | * This option allows users to specify how `BIGINT` (OID 20) database columns should be transformed when read. 12 | * Possible values for `bigintTransform`: 13 | * `'bigint'` (Default): Converts `BIGINT` values to native JavaScript `BigInt` objects. This maintains precision but can cause issues with direct `JSON.stringify()` if not handled. 14 | * `'string'`: Converts `BIGINT` values to JavaScript strings. This is safe for JSON serialization and preserves the original value as text. 15 | * `'number'`: Converts `BIGINT` values to JavaScript `Number` objects. This is convenient for smaller numbers but may lead to precision loss for values exceeding `Number.MAX_SAFE_INTEGER` or `Number.MIN_SAFE_INTEGER`. A warning is logged via `console.warn` if `verbose: true` and potential precision loss is detected. 16 | * The chosen transformation mode is logged to the console if `verbose: true` during client initialization. 17 | 18 | ### 🛠 Improvements 19 | * **Type Definitions**: 20 | * The `Json` type in `src/types.ts` now explicitly includes `bigint`. This allows TypeScript to correctly type-check structures that may contain `BigInt` values. Users are reminded (via documentation) that standard `JSON.stringify` will require special handling for `BigInt` objects (e.g., a custom replacer or pre-conversion to string/number). 21 | * **Client Initialization**: 22 | * Refined `Pool` initialization in `src/postgres-client.ts` to more consistently use `PoolConfig` for both connection string and individual parameter setups. 23 | * Standardized some internal logging prefixes for verbosity and errors. 24 | 25 | ### 📄 Documentation 26 | * Updated `README.md` to include: 27 | * Detailed explanation of the new `bigintTransform` constructor option, its possible values, default behavior, and implications (especially the precision loss warning for the `'number'` option). 28 | * Clarification in `README.md` regarding the `Json` type including `bigint` and the user's responsibility for `JSON.stringify` handling of `BigInt` objects. 29 | * Examples of using `bigintTransform` and `verbose` options in the `SupaLitePG` constructor. 30 | * Mention of `SUPALITE_VERBOSE=true` as an environment variable option. 31 | 32 | ## Impact 33 | - **Error Resolution**: Users experiencing `TypeError: Do not know how to serialize a BigInt` can now resolve this by setting `bigintTransform: 'string'` or `bigintTransform: 'number'` in the `SupaLitePG` configuration. 34 | - **Flexibility**: Provides developers with greater control over how large integer types are handled, catering to different use cases (e.g., precise arithmetic vs. simple display/serialization). 35 | - **Backward Compatibility**: The default behavior (`'bigint'`) remains unchanged, minimizing impact on existing users who might rely on receiving `BigInt` objects, though they should be aware of JSON serialization implications. 36 | -------------------------------------------------------------------------------- /dist/query-builder.d.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import type { SupaLitePG } from './postgres-client'; 3 | import { TableName, TableOrViewName, QueryResult, SingleQueryResult, DatabaseSchema, SchemaName, Row, InsertRow, UpdateRow } from './types'; 4 | export declare class QueryBuilder = 'public', K extends TableOrViewName = TableOrViewName> implements Promise> | SingleQueryResult>> { 5 | private pool; 6 | readonly [Symbol.toStringTag] = "QueryBuilder"; 7 | private table; 8 | private schema; 9 | private selectColumns; 10 | private joinClauses; 11 | private whereConditions; 12 | private orConditions; 13 | private countOption?; 14 | private headOption?; 15 | private orderByColumns; 16 | private limitValue?; 17 | private offsetValue?; 18 | private whereValues; 19 | private singleMode; 20 | private queryType; 21 | private insertData?; 22 | private updateData?; 23 | private conflictTarget?; 24 | private client; 25 | private verbose; 26 | constructor(pool: Pool, client: SupaLitePG, // Accept SupaLitePG instance 27 | table: K, schema?: S, verbose?: boolean); 28 | then> | SingleQueryResult>, TResult2 = never>(onfulfilled?: ((value: QueryResult> | SingleQueryResult>) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null): Promise; 29 | catch(onrejected?: ((reason: any) => TResult | PromiseLike) | null): Promise> | SingleQueryResult> | TResult>; 30 | finally(onfinally?: (() => void) | null): Promise> | SingleQueryResult>>; 31 | select(columns?: string, options?: { 32 | count?: 'exact' | 'planned' | 'estimated'; 33 | head?: boolean; 34 | }): this; 35 | match(conditions: { 36 | [key: string]: any; 37 | }): this; 38 | eq(column: string, value: any): this; 39 | neq(column: string, value: any): this; 40 | is(column: string, value: any): this; 41 | not(column: string, operator: string, value: any): this; 42 | contains(column: string, value: any): this; 43 | in(column: string, values: any[]): this; 44 | gt(column: string, value: any): this; 45 | gte(column: string, value: any): this; 46 | lt(column: string, value: any): this; 47 | lte(column: string, value: any): this; 48 | order(column: string, options?: { 49 | ascending?: boolean; 50 | }): this; 51 | limit(value: number): this; 52 | offset(value: number): this; 53 | maybeSingle(): Promise>>; 54 | single(): Promise>>; 55 | ilike(column: string, pattern: string): this; 56 | or(conditions: string): this; 57 | returns, NewK extends TableName>(): QueryBuilder; 58 | range(from: number, to: number): this; 59 | upsert(values: InsertRow, options?: { 60 | onConflict: string; 61 | }): this; 62 | private shouldReturnData; 63 | private buildWhereClause; 64 | private buildQuery; 65 | execute(): Promise> | SingleQueryResult>>; 66 | insert(data: InsertRow | InsertRow[]): this; 67 | update(data: UpdateRow): this; 68 | delete(): this; 69 | } 70 | -------------------------------------------------------------------------------- /examples/tests/special.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testSpecial() { 14 | try { 15 | // single() 메서드 테스트 16 | console.log('\n1. single() 성공 케이스:'); 17 | const singleUser = await client 18 | .from('users') 19 | .select('*') 20 | .eq('email', 'hong@example.com') 21 | .single(); 22 | console.log('Single user:', singleUser.data); 23 | 24 | console.log('\n2. single() 실패 케이스 (여러 결과):'); 25 | const multipleUsers = await client 26 | .from('users') 27 | .select('*') 28 | .eq('status', 'active') 29 | .single(); 30 | console.log('Multiple users error:', multipleUsers.error); 31 | 32 | // 복잡한 조인 쿼리 33 | console.log('\n3. 사용자별 포스트 및 댓글 수:'); 34 | const userStats = await client 35 | .from('users') 36 | .select(` 37 | id, 38 | name, 39 | (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) as post_count, 40 | (SELECT COUNT(*) FROM comments WHERE comments.user_id = users.id) as comment_count 41 | `) 42 | .order('name', { ascending: true }); 43 | console.log('User statistics:', userStats.data); 44 | 45 | // 에러 처리 테스트 46 | console.log('\n4. 존재하지 않는 컬럼 테스트:'); 47 | const nonExistentColumn = await client 48 | .from('users') 49 | .select('non_existent_column'); 50 | console.log('Non-existent column error:', nonExistentColumn.error); 51 | 52 | console.log('\n5. 잘못된 테이블 테스트:'); 53 | const nonExistentTable = await (client as any) 54 | .from('non_existent_table') 55 | .select('*'); 56 | console.log('Non-existent table error:', nonExistentTable.error); 57 | 58 | // 복잡한 필터링과 정렬 59 | console.log('\n6. 복잡한 필터링과 정렬:'); 60 | const complexQuery = await client 61 | .from('posts') 62 | .select('title, content, views') 63 | .gte('views', 50) 64 | .contains('tags', ['여행']) 65 | .order('views', { ascending: false }) 66 | .limit(5); 67 | console.log('Complex query result:', complexQuery.data); 68 | 69 | // 서브쿼리 테스트 70 | console.log('\n7. 서브쿼리를 사용한 필터링:'); 71 | const subqueryTest = await client 72 | .from('users') 73 | .select(` 74 | name, 75 | email, 76 | ( 77 | SELECT json_agg(json_build_object('title', title, 'views', views)) 78 | FROM posts 79 | WHERE posts.user_id = users.id 80 | AND views > 100 81 | ) as popular_posts 82 | `) 83 | .eq('status', 'active'); 84 | console.log('Subquery test result:', subqueryTest.data); 85 | 86 | // NULL 처리 테스트 87 | console.log('\n8. NULL 값 처리:'); 88 | const nullTest = await client 89 | .from('profiles') 90 | .select('*') 91 | .is('avatar_url', null) 92 | .is('bio', null); 93 | console.log('Null test result:', nullTest.data); 94 | 95 | // 타입 변환 테스트 96 | console.log('\n9. 타입 변환 및 집계:'); 97 | const aggregationTest = await client 98 | .from('posts') 99 | .select('user_id, views') 100 | .gte('views', 0); 101 | console.log('Aggregation test result:', aggregationTest.data); 102 | 103 | } catch (err) { 104 | console.error('Unexpected error:', err); 105 | } finally { 106 | await client.close(); 107 | } 108 | } 109 | 110 | testSpecial(); 111 | -------------------------------------------------------------------------------- /examples/tests/mutation.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testMutation() { 14 | try { 15 | // INSERT 테스트 16 | console.log('\n1. 단일 INSERT 테스트:'); 17 | const newUser = await client 18 | .from('users') 19 | .insert({ 20 | name: '신규사용자', 21 | email: 'new@example.com', 22 | status: 'active', 23 | last_login: new Date().toISOString() 24 | }) 25 | .select() 26 | .single(); 27 | console.log('Inserted user:', newUser.data); 28 | 29 | // 다중 INSERT 테스트 30 | console.log('\n2. 다중 INSERT 테스트:'); 31 | if (newUser.data) { 32 | // 첫 번째 포스트 추가 33 | const post1 = await client 34 | .from('posts') 35 | .insert({ 36 | user_id: newUser.data.id, 37 | title: '첫 번째 글', 38 | content: '안녕하세요!', 39 | tags: ['인사', '소개'], 40 | updated_at: new Date().toISOString() 41 | }) 42 | .select() 43 | .single(); 44 | console.log('First post:', post1.data); 45 | 46 | // 두 번째 포스트 추가 47 | const post2 = await client 48 | .from('posts') 49 | .insert({ 50 | user_id: newUser.data.id, 51 | title: '두 번째 글', 52 | content: '반갑습니다!', 53 | tags: ['인사'], 54 | updated_at: new Date().toISOString() 55 | }) 56 | .select() 57 | .single(); 58 | console.log('Second post:', post2.data); 59 | } 60 | 61 | // UPDATE 테스트 62 | console.log('\n3. UPDATE 테스트:'); 63 | const updatedUser = await client 64 | .from('users') 65 | .update({ 66 | status: 'inactive', 67 | last_login: new Date().toISOString() 68 | }) 69 | .eq('email', 'new@example.com') 70 | .select() 71 | .single(); 72 | console.log('Updated user:', updatedUser.data); 73 | 74 | // UPSERT 테스트 75 | console.log('\n4. UPSERT 테스트:'); 76 | const upsertProfile = await client 77 | .from('profiles') 78 | .upsert({ 79 | user_id: newUser.data?.id ?? 0, 80 | bio: '새로운 프로필입니다.', 81 | interests: ['코딩', '음악'], 82 | updated_at: new Date().toISOString() 83 | }, { onConflict: 'id' }) 84 | .select() 85 | .single(); 86 | console.log('Upserted profile:', upsertProfile.data); 87 | 88 | // 조건부 UPDATE 테스트 89 | console.log('\n5. 조건부 UPDATE 테스트:'); 90 | const updatedPosts = await client 91 | .from('posts') 92 | .update({ 93 | views: 10, 94 | updated_at: new Date().toISOString() 95 | }) 96 | .eq('user_id', newUser.data?.id) 97 | .select(); 98 | console.log('Updated posts:', updatedPosts.data); 99 | 100 | // DELETE 테스트 101 | console.log('\n6. DELETE 테스트:'); 102 | const deletedPosts = await client 103 | .from('posts') 104 | .delete() 105 | .eq('user_id', newUser.data?.id) 106 | .select(); 107 | console.log('Deleted posts:', deletedPosts.data); 108 | 109 | const deletedUser = await client 110 | .from('users') 111 | .delete() 112 | .eq('id', newUser.data?.id) 113 | .select() 114 | .single(); 115 | console.log('Deleted user:', deletedUser.data); 116 | 117 | } catch (err) { 118 | console.error('Error:', err); 119 | } finally { 120 | await client.close(); 121 | } 122 | } 123 | 124 | testMutation(); 125 | -------------------------------------------------------------------------------- /examples/tests/array-insert.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testArrayInsert() { 14 | console.log('Starting array insert tests...'); 15 | try { 16 | // 기본 배열 INSERT 테스트 17 | console.log('\n1. 기본 배열 INSERT 테스트:'); 18 | const newUsers = await client 19 | .from('users') 20 | .insert([ 21 | { 22 | name: '배열1', 23 | email: 'array1@example.com', 24 | status: 'active', 25 | last_login: new Date().toISOString() 26 | }, 27 | { 28 | name: '배열2', 29 | email: 'array2@example.com', 30 | status: 'active', 31 | last_login: new Date().toISOString() 32 | } 33 | ]) 34 | .select(); 35 | console.log('Inserted users:', JSON.stringify(newUsers.data, null, 2)); 36 | 37 | // 빈 배열 테스트 38 | console.log('\n2. 빈 배열 테스트:'); 39 | try { 40 | await client 41 | .from('users') 42 | .insert([]) 43 | .select(); 44 | } catch (err) { 45 | if (err instanceof Error) { 46 | console.log('Empty array error (expected):', err.message); 47 | } 48 | } 49 | 50 | // 다양한 데이터 타입을 포함한 배열 INSERT 51 | console.log('\n3. 다양한 데이터 타입을 포함한 배열 INSERT:'); 52 | if (!newUsers.data || !Array.isArray(newUsers.data)) { 53 | throw new Error('Failed to insert users or invalid response type'); 54 | } 55 | 56 | const [user1, user2] = newUsers.data; 57 | if (!user1?.id || !user2?.id) { 58 | throw new Error('Invalid user data'); 59 | } 60 | 61 | const newPosts = await client 62 | .from('posts') 63 | .insert([ 64 | { 65 | user_id: user1.id, 66 | title: '첫 번째 글', 67 | content: '안녕하세요!', 68 | tags: ['인사', '소개'], 69 | updated_at: new Date().toISOString() 70 | }, 71 | { 72 | user_id: user2.id, 73 | title: '두 번째 글', 74 | content: '반갑습니다!', 75 | tags: ['인사'], 76 | updated_at: new Date().toISOString() 77 | } 78 | ]) 79 | .select(); 80 | console.log('Inserted posts:', JSON.stringify(newPosts.data, null, 2)); 81 | 82 | // NULL 값을 포함한 배열 INSERT 83 | console.log('\n4. NULL 값을 포함한 배열 INSERT:'); 84 | const newProfiles = await client 85 | .from('profiles') 86 | .insert([ 87 | { 88 | user_id: user1.id, 89 | bio: '안녕하세요!', 90 | avatar_url: null, 91 | interests: ['코딩', '음악'], 92 | updated_at: new Date().toISOString() 93 | }, 94 | { 95 | user_id: user2.id, 96 | bio: null, 97 | avatar_url: null, 98 | interests: ['여행'], 99 | updated_at: new Date().toISOString() 100 | } 101 | ]) 102 | .select(); 103 | console.log('Inserted profiles:', JSON.stringify(newProfiles.data, null, 2)); 104 | 105 | // 정리: 테스트 데이터 삭제 106 | console.log('\n5. 테스트 데이터 정리:'); 107 | await client 108 | .from('users') 109 | .delete() 110 | .in('email', ['array1@example.com', 'array2@example.com']); 111 | console.log('Cleanup completed'); 112 | console.log('All tests completed successfully'); 113 | 114 | } catch (err) { 115 | if (err instanceof Error) { 116 | console.error('Error:', err.message); 117 | } else { 118 | console.error('Unknown error:', err); 119 | } 120 | } finally { 121 | await client.close(); 122 | } 123 | } 124 | 125 | testArrayInsert(); 126 | -------------------------------------------------------------------------------- /docs/changelog/2025-05-26-maybeSingle-single-refactor.md: -------------------------------------------------------------------------------- 1 | # Changelog: 2025-05-26 2 | 3 | ## Feature: `maybeSingle()` Method and `single()` Refactor 4 | 5 | **File:** `src/query-builder.ts` 6 | 7 | ### Summary 8 | - The existing `single()` method has been renamed to `maybeSingle()`. This method returns `data: null` and `error: null` if no row is found. 9 | - A new `single()` method has been implemented. This method returns `data: null` and an error object (specifically `PostgresError('PGRST116: No rows found')` with status `404`) if no row is found. 10 | - Both methods will return an error (`PostgresError('PGRST114: Multiple rows returned')` with status `406`) if multiple rows are returned by the query. 11 | 12 | ### Detailed Changes 13 | 1. **Added `singleMode` Property**: 14 | * A new private property `singleMode: 'strict' | 'maybe' | null` was added to the `QueryBuilder` class to manage the behavior of single-row fetching. 15 | 16 | 2. **`maybeSingle()` Method (Formerly `single()`)**: 17 | * The original `single()` method was renamed to `maybeSingle()`. 18 | * It now sets `this.singleMode = 'maybe';`. 19 | * In the `execute()` method, when `singleMode` is `'maybe'`: 20 | * If 0 rows are found: returns `{ data: null, error: null, count: 0, status: 200, statusText: 'OK' }`. 21 | * If 1 row is found: returns `{ data: row, error: null, count: 1, status: 200, statusText: 'OK' }`. 22 | * If >1 rows are found: returns `{ data: null, error: new PostgresError('PGRST114: Multiple rows returned'), count: rowCount, status: 406, statusText: 'Not Acceptable. Expected a single row but found multiple.' }`. 23 | 24 | 3. **New `single()` Method**: 25 | * A new method named `single()` was introduced. 26 | * It sets `this.singleMode = 'strict';`. 27 | * In the `execute()` method, when `singleMode` is `'strict'`: 28 | * If 0 rows are found: returns `{ data: null, error: new PostgresError('PGRST116: No rows found'), count: 0, status: 404, statusText: 'Not Found. Expected a single row but found no rows.' }`. 29 | * If 1 row is found: returns `{ data: row, error: null, count: 1, status: 200, statusText: 'OK' }`. 30 | * If >1 rows are found: returns `{ data: null, error: new PostgresError('PGRST114: Multiple rows returned'), count: rowCount, status: 406, statusText: 'Not Acceptable. Expected a single row but found multiple.' }`. 31 | 32 | 4. **ESLint Fixes in `src/query-builder.ts`**: 33 | * Removed unused type imports `QueryOptions` and `FilterOptions`. 34 | * Scoped lexical declarations within `switch` `case` blocks in `buildQuery()` using `{}`. 35 | 5. **Unit Tests Added (`src/__tests__/query-builder-single.test.ts`)**: 36 | * Comprehensive Jest tests were added for both `single()` and `maybeSingle()`. 37 | * The test file was initially created in `examples/tests/` and then moved to `src/__tests__/` to align with Jest's `roots` configuration, ensuring test discovery. Import paths within the test file were updated accordingly. 38 | * Tests cover scenarios for finding one row, zero rows, and multiple rows. 39 | * Appropriate assertions for `data`, `error`, `status`, and `statusText` are included. 40 | * Test database setup (`users` and `test_table_for_multi_row` tables) and teardown are handled in `beforeAll`/`afterAll`. 41 | * Type definitions for the test database schema (`TestDatabase`) were created and refined to ensure compatibility and correctness. 42 | 43 | ### Impact 44 | - Developers now have two distinct methods for fetching a single row: 45 | - `maybeSingle()`: Use when a row might or might not exist, and its absence is not an error condition. 46 | - `single()`: Use when a row is expected to exist, and its absence should be treated as an error. 47 | - This change provides more explicit control over how "not found" scenarios are handled for single-row queries. 48 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { PostgresError } from './errors'; 2 | 3 | export type Json = 4 | | string 5 | | number 6 | | bigint // BigInt 타입 추가. JSON.stringify 시 사용자 처리 필요. 7 | | boolean 8 | | null 9 | | { [key: string]: Json | undefined } 10 | | Json[]; 11 | 12 | export interface TableBase { 13 | Row: any; 14 | Insert: any; 15 | Update: any; 16 | Relationships: unknown[]; 17 | } 18 | 19 | export interface SchemaDefinition { 20 | Tables: { [key: string]: TableBase }; 21 | Views?: { [key: string]: any }; 22 | Functions?: { [key: string]: any }; 23 | Enums?: { [key: string]: any }; 24 | CompositeTypes?: { [key: string]: any }; 25 | } 26 | 27 | export interface DatabaseSchema { 28 | [schema: string]: SchemaDefinition; 29 | } 30 | 31 | // Supabase 스타일 데이터베이스 타입을 DatabaseSchema로 변환 32 | export type AsDatabaseSchema = { 33 | [K in keyof T]: T[K] extends { Tables: any } 34 | ? SchemaDefinition & T[K] 35 | : never; 36 | }; 37 | 38 | export type SchemaName = keyof T; 39 | export type TableName< 40 | T extends DatabaseSchema, 41 | S extends SchemaName = SchemaName 42 | > = keyof T[S]['Tables']; 43 | 44 | export type ViewName< 45 | T extends DatabaseSchema, 46 | S extends SchemaName = SchemaName 47 | > = keyof NonNullable; 48 | 49 | export type TableOrViewName< 50 | T extends DatabaseSchema, 51 | S extends SchemaName = SchemaName 52 | > = TableName | ViewName; 53 | 54 | export type Row< 55 | T extends DatabaseSchema, 56 | S extends SchemaName, 57 | K extends TableOrViewName 58 | > = K extends TableName 59 | ? T[S]['Tables'][K]['Row'] 60 | : K extends ViewName 61 | ? NonNullable[K]['Row'] 62 | : never; 63 | 64 | export type InsertRow< 65 | T extends DatabaseSchema, 66 | S extends SchemaName, 67 | K extends TableOrViewName 68 | > = K extends TableName 69 | ? T[S]['Tables'][K]['Insert'] 70 | : never; // Views는 Insert 불가능 71 | 72 | export type UpdateRow< 73 | T extends DatabaseSchema, 74 | S extends SchemaName, 75 | K extends TableOrViewName 76 | > = K extends TableName 77 | ? T[S]['Tables'][K]['Update'] & { 78 | modified_at?: string | null; 79 | updated_at?: string | null; 80 | } 81 | : never; // Views는 Update 불가능 82 | 83 | export type EnumType< 84 | T extends DatabaseSchema, 85 | S extends SchemaName, 86 | E extends keyof NonNullable 87 | > = NonNullable[E]; 88 | 89 | export type BigintTransformType = 'bigint' | 'string' | 'number'; 90 | 91 | export interface SupaliteConfig { 92 | connectionString?: string; // 연결 문자열(URI) 지원 93 | bigintTransform?: BigintTransformType; // BIGINT 변환 방식 94 | user?: string; 95 | host?: string; 96 | database?: string; 97 | password?: string; 98 | port?: number; 99 | ssl?: boolean; 100 | schema?: string; 101 | verbose?: boolean; // Added for verbose logging 102 | } 103 | 104 | export type QueryType = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'UPSERT'; 105 | 106 | export interface QueryOptions { 107 | limit?: number; 108 | offset?: number; 109 | order?: { 110 | column: string; 111 | ascending?: boolean; 112 | }; 113 | } 114 | 115 | export interface FilterOptions { 116 | column: string; 117 | operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike'; 118 | value: any; 119 | } 120 | 121 | export type BaseResult = { 122 | error: PostgresError | null; 123 | count: number | null; 124 | status: number; 125 | statusText: string; 126 | statusCode?: number; 127 | }; 128 | 129 | export type QueryResult = BaseResult & { 130 | data: Array; // T[] 대신 Array를 사용하여 명시적으로 배열임을 강조 131 | }; 132 | 133 | export type SingleQueryResult = BaseResult & { 134 | data: T | null; 135 | }; 136 | -------------------------------------------------------------------------------- /examples/tests/view-table-test.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteClient } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | // 타입 테스트 함수 5 | function testViewTableTypes() { 6 | // 타입 정의만 확인하는 함수 (실제로 실행되지 않음) 7 | async function typeCheck() { 8 | const supalite = new SupaliteClient(); 9 | 10 | // 1. user_posts_view 조회 테스트 11 | const userPostsQuery = supalite 12 | .from('user_posts_view') 13 | .select('*'); 14 | 15 | // 타입 확인: user_posts_view의 Row 타입이 올바르게 추론되는지 확인 16 | type UserPostsViewRow = { 17 | user_id: number; 18 | user_name: string; 19 | post_id: number; 20 | post_title: string; 21 | post_content: string | null; 22 | post_created_at: string; 23 | }; 24 | 25 | // 타입 호환성 검사 (await 사용) 26 | const userPostsResult = await userPostsQuery; 27 | const userPostsData: UserPostsViewRow[] = userPostsResult.data; 28 | 29 | // 2. active_users_view 조회 테스트 30 | const activeUsersQuery = supalite 31 | .from('active_users_view') 32 | .select('*') 33 | .gte('post_count', 2); 34 | 35 | // 타입 확인: active_users_view의 Row 타입이 올바르게 추론되는지 확인 36 | type ActiveUsersViewRow = { 37 | id: number; 38 | name: string; 39 | email: string; 40 | last_login: string | null; 41 | post_count: number; 42 | }; 43 | 44 | // 타입 호환성 검사 (await 사용) 45 | const activeUsersResult = await activeUsersQuery; 46 | const activeUsersData: ActiveUsersViewRow[] = activeUsersResult.data; 47 | 48 | // 3. 단일 결과 조회 테스트 49 | const singlePostQuery = supalite 50 | .from('user_posts_view') 51 | .select('*') 52 | .eq('post_id', 1) 53 | .single(); 54 | 55 | // 타입 확인: single() 메서드 호출 시 타입이 올바르게 추론되는지 확인 (await 사용) 56 | const singlePostResult = await singlePostQuery; 57 | const singlePostData: UserPostsViewRow | null = singlePostResult.data; 58 | 59 | // 4. 일반 테이블과 View 테이블 함께 사용 테스트 60 | const profilesQuery = supalite 61 | .from('profiles') 62 | .select('*') 63 | .eq('user_id', 1); 64 | 65 | // 타입 확인: profiles 테이블의 Row 타입이 올바르게 추론되는지 확인 66 | type ProfileRow = { 67 | id: number; 68 | user_id: number; 69 | bio: string | null; 70 | avatar_url: string | null; 71 | interests: string[] | null; 72 | updated_at: string | null; 73 | }; 74 | 75 | // 타입 호환성 검사 (await 사용) 76 | const profilesResult = await profilesQuery; 77 | const profilesData: ProfileRow[] = profilesResult.data; 78 | 79 | // 5. 타입 안전성 테스트: 존재하지 않는 컬럼 접근 시 타입 오류 발생 확인 80 | // 다음 코드는 컴파일 오류가 발생해야 함 (주석 처리) 81 | /* 82 | const invalidQuery = await supalite 83 | .from('user_posts_view') 84 | .select('*'); 85 | 86 | // 존재하지 않는 컬럼 접근 시도 87 | const invalidData = invalidQuery.data[0].non_existent_column; 88 | */ 89 | 90 | // 6. 타입 안전성 테스트: 존재하지 않는 테이블 접근 시 타입 오류 발생 확인 91 | // 다음 코드는 컴파일 오류가 발생해야 함 (주석 처리) 92 | /* 93 | const invalidTableQuery = supalite 94 | .from('non_existent_view') 95 | .select('*'); 96 | */ 97 | 98 | // 7. Insert/Update 불가능 테스트: View는 읽기 전용이므로 Insert/Update 불가능 99 | // 다음 코드는 컴파일 오류가 발생해야 함 (주석 처리) 100 | /* 101 | const insertQuery = supalite 102 | .from('user_posts_view') 103 | .insert({ 104 | user_id: 1, 105 | user_name: '홍길동', 106 | post_id: 1, 107 | post_title: '새 게시물', 108 | post_content: '내용', 109 | post_created_at: '2025-01-01T00:00:00Z' 110 | }); 111 | 112 | const updateQuery = supalite 113 | .from('user_posts_view') 114 | .update({ 115 | post_title: '수정된 게시물' 116 | }); 117 | */ 118 | 119 | console.log('타입 검사 완료: 모든 타입이 올바르게 추론됨'); 120 | } 121 | 122 | // 실제로는 실행되지 않는 함수이므로 호출하지 않음 123 | // typeCheck(); 124 | 125 | console.log('View 테이블 타입 테스트 완료: 컴파일 오류 없음'); 126 | } 127 | 128 | // 테스트 실행 129 | testViewTableTypes(); 130 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.5.6] - 2025-12-17 4 | 5 | ### 🐞 Fixed 6 | - `select()`의 PostgREST-style embed(`related_table(*)`)가 **양방향 FK**를 지원하도록 개선했습니다. 이제 1:N 관계는 배열(`[]` 기본값), N:1 관계는 객체(또는 `null`)로 반환합니다. (See [docs/changelog/2025-12-17-embed-many-to-one.md](docs/changelog/2025-12-17-embed-many-to-one.md)) 7 | 8 | ## [0.5.5] - 2025-11-26 9 | 10 | ### ✨ Added 11 | - `rpc()` 메서드 호출 시 `.single()` 및 `.maybeSingle()` 메서드 체이닝 지원을 추가했습니다. 이를 통해 RPC 결과에 대해 단일 행 제약 조건을 적용할 수 있습니다. (See [docs/changelog/2025-11-26-rpc-single-support.md](docs/changelog/2025-11-26-rpc-single-support.md)) 12 | 13 | ## [0.5.2] - 2025-10-16 14 | 15 | ### 🐞 Fixed 16 | - `select()` 메서드에서 `count: 'exact'` 옵션 사용 시 `limit()` 또는 `range()`와 함께 호출될 때 전체 개수 대신 페이지네이션된 개수를 반환하는 버그를 수정했습니다. 이제 항상 정확한 전체 개수를 반환합니다. 17 | - `select()` 메서드에서 `count: 'exact'`와 `head: true` 옵션을 함께 사용할 때 `count`가 `null`로 반환되는 버그를 수정했습니다. 18 | 19 | ## [0.5.1] - 2025-10-16 20 | 21 | ### 🐞 Fixed 22 | - `select()` 메서드에서 `count: 'exact'` 옵션 사용 시 `limit()` 또는 `range()`와 함께 호출될 때 전체 개수 대신 페이지네이션된 개수를 반환하는 버그를 수정했습니다. 이제 항상 정확한 전체 개수를 반환합니다. 23 | 24 | ## [0.5.0] - 2025-07-01 25 | 26 | ### ✨ Added 27 | - **Join Query Support**: Implemented support for PostgREST-style join queries in the `.select()` method. You can now fetch related data from foreign tables using the syntax `related_table(*)` or `related_table(column1, column2)`. This is achieved by dynamically generating `json_agg` subqueries. 28 | 29 | ### 🛠 Changed 30 | - `SupaLitePG` client now includes a `getForeignKey` method to resolve foreign key relationships, with caching for better performance. 31 | - `QueryBuilder`'s `select` and `buildQuery` methods were enhanced to parse the new syntax and construct the appropriate SQL queries. 32 | 33 | ## [0.4.0] - 2025-06-10 34 | 35 | ### ✨ Added 36 | - **Configurable `BIGINT` Transformation**: Introduced `bigintTransform` option in `SupaLitePG` constructor to allow users to specify how `BIGINT` database types are transformed (to `'bigint'`, `'string'`, or `'number'`). Default is `'bigint'`. This provides flexibility and helps mitigate `JSON.stringify` errors with native `BigInt` objects. (See [docs/changelog/2025-06-10-bigint-handling-enhancement.md](docs/changelog/2025-06-10-bigint-handling-enhancement.md) for details) 37 | 38 | ### 🛠 Changed 39 | - The internal `Json` type in `src/types.ts` now explicitly includes `bigint`, with documentation clarifying user responsibility for `JSON.stringify` handling. 40 | - Improved client initialization logging for `bigintTransform` mode when `verbose` is enabled. 41 | 42 | ## [0.1.8] - 2025-03-04 43 | 44 | ### Fixed 45 | - 누락된 `dist` 파일들을 포함하도록 수정 46 | 47 | ## [0.1.7] - 2025-03-04 48 | 49 | ### Added 50 | - QueryBuilder에 `match` 메서드 추가 51 | - `match` 메서드 테스트 코드 작성 52 | 53 | ## [0.1.6] - 2025-03-01 54 | 55 | ### Added 56 | - corepack 지원 추가 (npm, yarn, pnpm, bun 패키지 관리자 지원) 57 | - 패키지 관리자 중립적인 스크립트 설정 ($npm_execpath 사용) 58 | - 각 패키지 관리자의 lock 파일 생성 (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock) 59 | - .npmignore 파일 추가하여 lock 파일들이 npm 배포 패키지에 포함되지 않도록 설정 60 | 61 | ## [0.1.5] - 2025-02-28 62 | 63 | ### Security 64 | - 예제 코드에서 민감한 Supabase 연결 문자열 제거 65 | - Git 히스토리에서 민감한 정보 제거 66 | 67 | ## [0.1.4] - 2025-02-28 68 | 69 | ### Fixed 70 | - GitHub 저장소에서 직접 설치 시 빌드된 파일이 포함되지 않는 문제 해결 71 | - .gitignore에서 dist 디렉토리 제외하여 빌드된 파일이 GitHub에 포함되도록 수정 72 | 73 | ## [0.1.3] - 2025-02-27 74 | 75 | ### Added 76 | - PostgreSQL bigint 타입 지원 추가 (JavaScript BigInt 타입으로 변환) 77 | - bigint 타입 테스트 코드 작성 78 | - Number 및 string 타입 값의 자동 변환 지원 확인 (bigint 컬럼에 Number나 string 값 전달 시 자동 변환) 79 | 80 | ## [0.1.2] - 2025-02-27 81 | 82 | ### Added 83 | - DB_CONNECTION URI 형식 지원 추가 84 | - 연결 테스트 메서드 추가 85 | - 연결 문자열 테스트 코드 작성 86 | 87 | ## [0.1.1] - 2025-02-25 88 | 89 | ### Added 90 | - 멀티 스키마 데이터베이스 지원 91 | - Supabase 스타일의 타입 시스템 지원 92 | - Json 타입 정의 추가 93 | - Views, Functions, Enums, CompositeTypes 지원 94 | 95 | ### Changed 96 | - 타입 시스템 개선 97 | - 스키마 인식 타입 유틸리티 업데이트 98 | - 기본 스키마를 'public'으로 설정 99 | 100 | ## [0.1.0] - 2025-02-25 101 | 102 | ### Added 103 | - 초기 릴리즈 104 | - PostgreSQL 클라이언트 구현 105 | - 기본적인 CRUD 작업 지원 106 | - 트랜잭션 지원 107 | - 타입 안전성 108 | - 테스트 및 예제 코드 109 | -------------------------------------------------------------------------------- /examples/setup-views.sql: -------------------------------------------------------------------------------- 1 | -- 기존 테이블 삭제 (있는 경우) 2 | DROP VIEW IF EXISTS user_posts_view; 3 | DROP VIEW IF EXISTS active_users_view; 4 | DROP TABLE IF EXISTS comments; 5 | DROP TABLE IF EXISTS posts; 6 | DROP TABLE IF EXISTS profiles; 7 | DROP TABLE IF EXISTS users; 8 | DROP TABLE IF EXISTS bigint_test; 9 | 10 | -- 사용자 테이블 생성 11 | CREATE TABLE users ( 12 | id SERIAL PRIMARY KEY, 13 | name VARCHAR(100) NOT NULL, 14 | email VARCHAR(100) NOT NULL UNIQUE, 15 | status VARCHAR(20) DEFAULT 'active', 16 | last_login TIMESTAMP, 17 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | ); 19 | 20 | -- 프로필 테이블 생성 21 | CREATE TABLE profiles ( 22 | id SERIAL PRIMARY KEY, 23 | user_id INTEGER REFERENCES users(id), 24 | bio TEXT, 25 | avatar_url VARCHAR(255), 26 | interests VARCHAR[] DEFAULT '{}', 27 | updated_at TIMESTAMP 28 | ); 29 | 30 | -- 게시물 테이블 생성 31 | CREATE TABLE posts ( 32 | id SERIAL PRIMARY KEY, 33 | user_id INTEGER REFERENCES users(id), 34 | title VARCHAR(200) NOT NULL, 35 | content TEXT, 36 | tags VARCHAR[] DEFAULT '{}', 37 | views INTEGER DEFAULT 0, 38 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 39 | updated_at TIMESTAMP 40 | ); 41 | 42 | -- 댓글 테이블 생성 43 | CREATE TABLE comments ( 44 | id SERIAL PRIMARY KEY, 45 | post_id INTEGER REFERENCES posts(id), 46 | user_id INTEGER REFERENCES users(id), 47 | content TEXT NOT NULL, 48 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 49 | ); 50 | 51 | -- BigInt 테스트 테이블 생성 52 | CREATE TABLE bigint_test ( 53 | id SERIAL PRIMARY KEY, 54 | small_int BIGINT, 55 | large_int BIGINT, 56 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 57 | ); 58 | 59 | -- 샘플 데이터 삽입: 사용자 60 | INSERT INTO users (name, email, status, last_login) VALUES 61 | ('홍길동', 'hong@example.com', 'active', NOW() - INTERVAL '1 day'), 62 | ('김철수', 'kim@example.com', 'active', NOW() - INTERVAL '3 day'), 63 | ('이영희', 'lee@example.com', 'inactive', NOW() - INTERVAL '10 day'), 64 | ('박지민', 'park@example.com', 'active', NOW() - INTERVAL '5 hour'), 65 | ('최민수', 'choi@example.com', 'active', NOW()); 66 | 67 | -- 샘플 데이터 삽입: 프로필 68 | INSERT INTO profiles (user_id, bio, avatar_url, interests) VALUES 69 | (1, '안녕하세요, 홍길동입니다.', 'https://example.com/avatar1.jpg', ARRAY['여행', '독서', '영화']), 70 | (2, '프로그래머 김철수입니다.', 'https://example.com/avatar2.jpg', ARRAY['코딩', '게임']), 71 | (3, '디자이너 이영희입니다.', 'https://example.com/avatar3.jpg', ARRAY['디자인', '그림', '음악']), 72 | (4, '학생 박지민입니다.', 'https://example.com/avatar4.jpg', ARRAY['공부', '독서']), 73 | (5, '요리사 최민수입니다.', 'https://example.com/avatar5.jpg', ARRAY['요리', '여행']); 74 | 75 | -- 샘플 데이터 삽입: 게시물 76 | INSERT INTO posts (user_id, title, content, tags, views) VALUES 77 | (1, '첫 번째 게시물', '안녕하세요, 첫 번째 게시물입니다.', ARRAY['인사', '소개'], 15), 78 | (1, '여행 후기', '지난 주말 여행 다녀왔습니다.', ARRAY['여행', '후기'], 32), 79 | (2, '프로그래밍 팁', '효율적인 코딩을 위한 팁을 공유합니다.', ARRAY['프로그래밍', '팁'], 45), 80 | (3, '디자인 포트폴리오', '최근 작업한 디자인 포트폴리오입니다.', ARRAY['디자인', '포트폴리오'], 28), 81 | (4, '공부 방법', '효과적인 공부 방법을 공유합니다.', ARRAY['공부', '팁'], 19), 82 | (5, '맛있는 레시피', '간단하게 만들 수 있는 레시피를 공유합니다.', ARRAY['요리', '레시피'], 37), 83 | (2, '두 번째 프로그래밍 팁', '더 많은 프로그래밍 팁을 공유합니다.', ARRAY['프로그래밍', '팁'], 22); 84 | 85 | -- 샘플 데이터 삽입: 댓글 86 | INSERT INTO comments (post_id, user_id, content) VALUES 87 | (1, 2, '환영합니다!'), 88 | (1, 3, '반갑습니다~'), 89 | (2, 4, '어디로 여행 다녀오셨나요?'), 90 | (2, 5, '사진도 공유해주세요!'), 91 | (3, 1, '유용한 정보 감사합니다.'), 92 | (4, 2, '멋진 디자인이네요!'), 93 | (5, 3, '공부 팁 감사합니다.'), 94 | (6, 1, '레시피 따라 해볼게요!'); 95 | 96 | -- 샘플 데이터 삽입: BigInt 테스트 97 | INSERT INTO bigint_test (small_int, large_int) VALUES 98 | (123, 9223372036854775807), 99 | (456, 9223372036854775806), 100 | (789, 9223372036854775805); 101 | 102 | -- View 생성: 사용자와 게시물 정보를 결합한 뷰 103 | CREATE VIEW user_posts_view AS 104 | SELECT 105 | u.id AS user_id, 106 | u.name AS user_name, 107 | p.id AS post_id, 108 | p.title AS post_title, 109 | p.content AS post_content, 110 | p.created_at AS post_created_at 111 | FROM 112 | users u 113 | JOIN 114 | posts p ON u.id = p.user_id 115 | ORDER BY 116 | p.created_at DESC; 117 | 118 | -- View 생성: 활성 사용자와 게시물 수를 보여주는 뷰 119 | CREATE VIEW active_users_view AS 120 | SELECT 121 | u.id, 122 | u.name, 123 | u.email, 124 | u.last_login, 125 | COUNT(p.id) AS post_count 126 | FROM 127 | users u 128 | LEFT JOIN 129 | posts p ON u.id = p.user_id 130 | WHERE 131 | u.status = 'active' 132 | GROUP BY 133 | u.id, u.name, u.email, u.last_login 134 | ORDER BY 135 | post_count DESC; 136 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-count.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const postgres_client_1 = require("../postgres-client"); 4 | const pg_1 = require("pg"); 5 | const dotenv_1 = require("dotenv"); 6 | (0, dotenv_1.config)(); // .env 변수 로드 7 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 8 | describe("QueryBuilder: select({ count: 'exact' })", () => { 9 | let client; 10 | let pool; 11 | const totalUsers = 10; 12 | beforeAll(async () => { 13 | pool = new pg_1.Pool({ connectionString }); 14 | // count_test_users 테이블이 없는 경우 생성 15 | await pool.query(` 16 | CREATE TABLE IF NOT EXISTS count_test_users ( 17 | id SERIAL PRIMARY KEY, 18 | name VARCHAR(100) NOT NULL, 19 | email VARCHAR(100) UNIQUE NOT NULL 20 | ); 21 | `); 22 | }); 23 | beforeEach(async () => { 24 | client = new postgres_client_1.SupaLitePG({ connectionString, verbose: true }); // verbose 활성화 25 | // 테스트 데이터 정리 및 삽입 26 | await pool.query('DELETE FROM count_test_users;'); 27 | const usersToInsert = []; 28 | for (let i = 1; i <= totalUsers; i++) { 29 | usersToInsert.push({ 30 | name: `User ${i}`, 31 | email: `user${i}@example.com`, 32 | }); 33 | } 34 | // SupaLite의 insert 메서드를 사용하여 데이터 삽입 35 | await client.from('count_test_users').insert(usersToInsert); 36 | }); 37 | afterEach(async () => { 38 | if (client) { 39 | await client.close(); 40 | } 41 | }); 42 | afterAll(async () => { 43 | await pool.end(); 44 | }); 45 | test('should return the exact total count without limit', async () => { 46 | const { data, error, count } = await client 47 | .from('count_test_users') 48 | .select('*', { count: 'exact' }); 49 | expect(error).toBeNull(); 50 | expect(data).not.toBeNull(); 51 | expect(data?.length).toBe(totalUsers); 52 | expect(count).toBe(totalUsers); 53 | }); 54 | test('should return the exact total count with limit', async () => { 55 | const limit = 3; 56 | const { data, error, count } = await client 57 | .from('count_test_users') 58 | .select('*', { count: 'exact' }) 59 | .limit(limit); 60 | expect(error).toBeNull(); 61 | expect(data).not.toBeNull(); 62 | expect(data?.length).toBe(limit); 63 | expect(count).toBe(totalUsers); // count는 limit과 상관없이 전체 개수여야 함 64 | }); 65 | test('should return a count of 0 when no rows are found', async () => { 66 | const { data, error, count } = await client 67 | .from('count_test_users') 68 | .select('*', { count: 'exact' }) 69 | .eq('name', 'NonExistentUser'); 70 | expect(error).toBeNull(); 71 | expect(data).not.toBeNull(); 72 | expect(data?.length).toBe(0); 73 | expect(count).toBe(0); 74 | }); 75 | test('data should not contain the exact_count column', async () => { 76 | const { data, error } = await client 77 | .from('count_test_users') 78 | .select('*', { count: 'exact' }) 79 | .limit(1); 80 | expect(error).toBeNull(); 81 | expect(data).not.toBeNull(); 82 | expect(data?.length).toBe(1); 83 | // exact_count 속성이 최종 결과에 포함되지 않았는지 확인 84 | expect(data[0]).not.toHaveProperty('exact_count'); 85 | }); 86 | test('should return only the exact count when head is true', async () => { 87 | const { data, error, count } = await client 88 | .from('count_test_users') 89 | .select('*', { count: 'exact', head: true }); 90 | expect(error).toBeNull(); 91 | expect(data).toEqual([]); // head: true일 때는 데이터가 비어있어야 함 92 | expect(count).toBe(totalUsers); 93 | }); 94 | test('should return the exact total count with range', async () => { 95 | const from = 2; 96 | const to = 5; 97 | const { data, error, count } = await client 98 | .from('count_test_users') 99 | .select('*', { count: 'exact' }) 100 | .range(from, to); 101 | expect(error).toBeNull(); 102 | expect(data).not.toBeNull(); 103 | expect(data?.length).toBe(to - from + 1); 104 | expect(count).toBe(totalUsers); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/__tests__/rpc.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../postgres-client'; 2 | import { PostgresError } from '../errors'; 3 | import { Pool } from 'pg'; 4 | 5 | // Mock pg 6 | jest.mock('pg', () => { 7 | const mQuery = jest.fn(); 8 | const mPool = { 9 | query: mQuery, 10 | on: jest.fn(), 11 | connect: jest.fn().mockResolvedValue({ 12 | release: jest.fn(), 13 | query: mQuery 14 | }), 15 | end: jest.fn() 16 | }; 17 | return { 18 | Pool: jest.fn(() => mPool), 19 | types: { 20 | setTypeParser: jest.fn() 21 | } 22 | }; 23 | }); 24 | 25 | describe('SupaLitePG rpc', () => { 26 | let client: SupaLitePG; 27 | let mockQuery: jest.Mock; 28 | 29 | beforeEach(() => { 30 | // Pool 생성자가 mockPool을 반환하므로, 새 Pool을 만들어서 mockQuery에 접근 31 | const pool = new Pool(); 32 | mockQuery = pool.query as jest.Mock; 33 | mockQuery.mockReset(); 34 | 35 | client = new SupaLitePG(); 36 | }); 37 | 38 | test('rpc() should return multiple rows by default', async () => { 39 | mockQuery.mockResolvedValueOnce({ 40 | rows: [{ id: 1 }, { id: 2 }], 41 | rowCount: 2 42 | }); 43 | 44 | const result = await client.rpc('get_users'); 45 | expect(result.data).toHaveLength(2); 46 | expect(result.error).toBeNull(); 47 | expect(result.count).toBe(2); 48 | }); 49 | 50 | test('rpc().single() should return single object if 1 row returned (multi-column)', async () => { 51 | mockQuery.mockResolvedValueOnce({ 52 | rows: [{ id: 1, name: 'user' }], 53 | rowCount: 1 54 | }); 55 | 56 | const result = await client.rpc('get_user').single(); 57 | expect(result.data).toEqual({ id: 1, name: 'user' }); 58 | expect(result.error).toBeNull(); 59 | }); 60 | 61 | test('rpc().single() should error if 0 rows returned', async () => { 62 | mockQuery.mockResolvedValueOnce({ 63 | rows: [], 64 | rowCount: 0 65 | }); 66 | 67 | const result = await client.rpc('get_user').single(); 68 | expect(result.data).toBeNull(); 69 | expect(result.error).toBeInstanceOf(PostgresError); 70 | expect(result.error?.message).toContain('PGRST116'); // No rows found 71 | }); 72 | 73 | test('rpc().single() should error if multiple rows returned', async () => { 74 | mockQuery.mockResolvedValueOnce({ 75 | rows: [{ id: 1 }, { id: 2 }], 76 | rowCount: 2 77 | }); 78 | 79 | const result = await client.rpc('get_user').single(); 80 | expect(result.data).toBeNull(); 81 | expect(result.error).toBeInstanceOf(PostgresError); 82 | expect(result.error?.message).toContain('PGRST114'); // Multiple rows returned 83 | }); 84 | 85 | test('rpc().maybeSingle() should return single object if 1 row returned (multi-column)', async () => { 86 | mockQuery.mockResolvedValueOnce({ 87 | rows: [{ id: 1, name: 'user' }], 88 | rowCount: 1 89 | }); 90 | 91 | const result = await client.rpc('get_user').maybeSingle(); 92 | expect(result.data).toEqual({ id: 1, name: 'user' }); 93 | expect(result.error).toBeNull(); 94 | }); 95 | 96 | test('rpc().maybeSingle() should return null data if 0 rows returned', async () => { 97 | mockQuery.mockResolvedValueOnce({ 98 | rows: [], 99 | rowCount: 0 100 | }); 101 | 102 | const result = await client.rpc('get_user').maybeSingle(); 103 | expect(result.data).toBeNull(); 104 | expect(result.error).toBeNull(); 105 | }); 106 | 107 | test('rpc().maybeSingle() should error if multiple rows returned', async () => { 108 | mockQuery.mockResolvedValueOnce({ 109 | rows: [{ id: 1 }, { id: 2 }], 110 | rowCount: 2 111 | }); 112 | 113 | const result = await client.rpc('get_user').maybeSingle(); 114 | expect(result.data).toBeNull(); 115 | expect(result.error).toBeInstanceOf(PostgresError); 116 | expect(result.error?.message).toContain('PGRST114'); 117 | }); 118 | 119 | test('rpc() should unwrap scalar return values', async () => { 120 | mockQuery.mockResolvedValueOnce({ 121 | rows: [{ get_count: 42 }], // scalar return is 1 row, 1 column 122 | rowCount: 1 123 | }); 124 | 125 | const result = await client.rpc('get_count'); 126 | expect(result.data).toBe(42); 127 | expect(result.error).toBeNull(); 128 | }); 129 | 130 | test('rpc().single() should unwrap scalar return values', async () => { 131 | mockQuery.mockResolvedValueOnce({ 132 | rows: [{ get_count: 42 }], 133 | rowCount: 1 134 | }); 135 | 136 | const result = await client.rpc('get_count').single(); 137 | expect(result.data).toBe(42); 138 | expect(result.error).toBeNull(); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-join.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const postgres_client_1 = require("../postgres-client"); 4 | const pg_1 = require("pg"); 5 | const dotenv_1 = require("dotenv"); 6 | (0, dotenv_1.config)(); 7 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 8 | describe('QueryBuilder with Join Queries', () => { 9 | let client; 10 | let pool; 11 | beforeAll(async () => { 12 | pool = new pg_1.Pool({ connectionString }); 13 | await pool.query(` 14 | CREATE TABLE IF NOT EXISTS authors ( 15 | id SERIAL PRIMARY KEY, 16 | name TEXT NOT NULL 17 | ); 18 | `); 19 | await pool.query(` 20 | CREATE TABLE IF NOT EXISTS books ( 21 | id SERIAL PRIMARY KEY, 22 | title TEXT NOT NULL, 23 | author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE 24 | ); 25 | `); 26 | }); 27 | beforeEach(async () => { 28 | client = new postgres_client_1.SupaLitePG({ connectionString, verbose: true }); 29 | // Clear and re-populate data for each test 30 | await pool.query('TRUNCATE TABLE books, authors RESTART IDENTITY CASCADE;'); 31 | await pool.query(` 32 | INSERT INTO authors (name) VALUES ('George Orwell'), ('Jane Austen'); 33 | INSERT INTO books (title, author_id) VALUES 34 | ('1984', 1), 35 | ('Animal Farm', 1), 36 | ('Pride and Prejudice', 2); 37 | `); 38 | }); 39 | afterEach(async () => { 40 | if (client) { 41 | await client.close(); 42 | } 43 | }); 44 | afterAll(async () => { 45 | await pool.query(`DROP TABLE IF EXISTS books;`); 46 | await pool.query(`DROP TABLE IF EXISTS authors;`); 47 | await pool.end(); 48 | }); 49 | test('should fetch main records and nested foreign records', async () => { 50 | const { data, error } = await client 51 | .from('authors') 52 | .select('*, books(*)'); 53 | expect(error).toBeNull(); 54 | expect(data).not.toBeNull(); 55 | const typedData = data; 56 | expect(typedData).toHaveLength(2); 57 | const orwell = typedData.find(a => a.name === 'George Orwell'); 58 | const austen = typedData.find(a => a.name === 'Jane Austen'); 59 | expect(orwell).toBeDefined(); 60 | expect(orwell?.books).toHaveLength(2); 61 | expect(orwell?.books.map(b => b.title)).toEqual(expect.arrayContaining(['1984', 'Animal Farm'])); 62 | expect(austen).toBeDefined(); 63 | expect(austen?.books).toHaveLength(1); 64 | expect(austen?.books[0].title).toBe('Pride and Prejudice'); 65 | }); 66 | test('should fetch specific columns from main and nested records', async () => { 67 | const { data, error } = await client 68 | .from('authors') 69 | .select('name, books(title)'); 70 | expect(error).toBeNull(); 71 | expect(data).not.toBeNull(); 72 | const typedData = data; 73 | expect(typedData).toHaveLength(2); 74 | const orwell = typedData.find(a => a.name === 'George Orwell'); 75 | expect(orwell).toBeDefined(); 76 | expect(orwell?.books).toHaveLength(2); 77 | expect(orwell?.books[0]).toHaveProperty('title'); 78 | expect(orwell?.books[0]).not.toHaveProperty('id'); 79 | }); 80 | test('should fetch main records and nested referenced record (many-to-one)', async () => { 81 | const { data, error } = await client 82 | .from('books') 83 | .select('*, authors(*)') 84 | .order('id'); 85 | expect(error).toBeNull(); 86 | expect(data).not.toBeNull(); 87 | const typedData = data; 88 | expect(typedData).toHaveLength(3); 89 | const first = typedData[0]; 90 | expect(first.title).toBe('1984'); 91 | expect(first.authors).toBeDefined(); 92 | expect(Array.isArray(first.authors)).toBe(false); 93 | expect(first.authors?.name).toBe('George Orwell'); 94 | const third = typedData[2]; 95 | expect(third.title).toBe('Pride and Prejudice'); 96 | expect(third.authors?.name).toBe('Jane Austen'); 97 | }); 98 | test('should fetch specific columns from nested referenced record (many-to-one)', async () => { 99 | const { data, error } = await client 100 | .from('books') 101 | .select('title, authors(name)') 102 | .order('id'); 103 | expect(error).toBeNull(); 104 | expect(data).not.toBeNull(); 105 | const typedData = data; 106 | expect(typedData).toHaveLength(3); 107 | expect(typedData[0].authors?.name).toBe('George Orwell'); 108 | expect(typedData[0].authors).not.toHaveProperty('id'); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-count.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../postgres-client'; 2 | import { Pool } from 'pg'; 3 | import { config } from 'dotenv'; 4 | import { DatabaseSchema, TableBase } from '../types'; 5 | 6 | config(); // .env 변수 로드 7 | 8 | // 테스트용 데이터베이스 스키마 정의 9 | type CountTestUsersTableRow = { id: number; name: string; email: string; }; 10 | type CountTestUsersTableInsert = { id?: number; name: string; email: string; }; 11 | type CountTestUsersTableUpdate = { id?: number; name?: string; email?: string; }; 12 | 13 | interface TestDatabase extends DatabaseSchema { 14 | public: { 15 | Tables: { 16 | count_test_users: TableBase & { Row: CountTestUsersTableRow; Insert: CountTestUsersTableInsert; Update: CountTestUsersTableUpdate; Relationships: [] }; 17 | }; 18 | Views: Record; 19 | Functions: Record; 20 | Enums: Record; 21 | CompositeTypes: Record; 22 | }; 23 | } 24 | 25 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 26 | 27 | describe("QueryBuilder: select({ count: 'exact' })", () => { 28 | let client: SupaLitePG; 29 | let pool: Pool; 30 | const totalUsers = 10; 31 | 32 | beforeAll(async () => { 33 | pool = new Pool({ connectionString }); 34 | // count_test_users 테이블이 없는 경우 생성 35 | await pool.query(` 36 | CREATE TABLE IF NOT EXISTS count_test_users ( 37 | id SERIAL PRIMARY KEY, 38 | name VARCHAR(100) NOT NULL, 39 | email VARCHAR(100) UNIQUE NOT NULL 40 | ); 41 | `); 42 | }); 43 | 44 | beforeEach(async () => { 45 | client = new SupaLitePG({ connectionString, verbose: true }); // verbose 활성화 46 | // 테스트 데이터 정리 및 삽입 47 | await pool.query('DELETE FROM count_test_users;'); 48 | const usersToInsert = []; 49 | for (let i = 1; i <= totalUsers; i++) { 50 | usersToInsert.push({ 51 | name: `User ${i}`, 52 | email: `user${i}@example.com`, 53 | }); 54 | } 55 | // SupaLite의 insert 메서드를 사용하여 데이터 삽입 56 | await client.from('count_test_users').insert(usersToInsert); 57 | }); 58 | 59 | afterEach(async () => { 60 | if (client) { 61 | await client.close(); 62 | } 63 | }); 64 | 65 | afterAll(async () => { 66 | await pool.end(); 67 | }); 68 | 69 | test('should return the exact total count without limit', async () => { 70 | const { data, error, count } = await client 71 | .from('count_test_users') 72 | .select('*', { count: 'exact' }); 73 | 74 | expect(error).toBeNull(); 75 | expect(data).not.toBeNull(); 76 | expect(data?.length).toBe(totalUsers); 77 | expect(count).toBe(totalUsers); 78 | }); 79 | 80 | test('should return the exact total count with limit', async () => { 81 | const limit = 3; 82 | const { data, error, count } = await client 83 | .from('count_test_users') 84 | .select('*', { count: 'exact' }) 85 | .limit(limit); 86 | 87 | expect(error).toBeNull(); 88 | expect(data).not.toBeNull(); 89 | expect(data?.length).toBe(limit); 90 | expect(count).toBe(totalUsers); // count는 limit과 상관없이 전체 개수여야 함 91 | }); 92 | 93 | test('should return a count of 0 when no rows are found', async () => { 94 | const { data, error, count } = await client 95 | .from('count_test_users') 96 | .select('*', { count: 'exact' }) 97 | .eq('name', 'NonExistentUser'); 98 | 99 | expect(error).toBeNull(); 100 | expect(data).not.toBeNull(); 101 | expect(data?.length).toBe(0); 102 | expect(count).toBe(0); 103 | }); 104 | 105 | test('data should not contain the exact_count column', async () => { 106 | const { data, error } = await client 107 | .from('count_test_users') 108 | .select('*', { count: 'exact' }) 109 | .limit(1); 110 | 111 | expect(error).toBeNull(); 112 | expect(data).not.toBeNull(); 113 | expect(data?.length).toBe(1); 114 | // exact_count 속성이 최종 결과에 포함되지 않았는지 확인 115 | expect(data![0]).not.toHaveProperty('exact_count'); 116 | }); 117 | 118 | test('should return only the exact count when head is true', async () => { 119 | const { data, error, count } = await client 120 | .from('count_test_users') 121 | .select('*', { count: 'exact', head: true }); 122 | 123 | expect(error).toBeNull(); 124 | expect(data).toEqual([]); // head: true일 때는 데이터가 비어있어야 함 125 | expect(count).toBe(totalUsers); 126 | }); 127 | 128 | test('should return the exact total count with range', async () => { 129 | const from = 2; 130 | const to = 5; 131 | const { data, error, count } = await client 132 | .from('count_test_users') 133 | .select('*', { count: 'exact' }) 134 | .range(from, to); 135 | 136 | expect(error).toBeNull(); 137 | expect(data).not.toBeNull(); 138 | expect(data?.length).toBe(to - from + 1); 139 | expect(count).toBe(totalUsers); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /examples/tests/transaction.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | const client = new SupaLitePG({ 5 | user: 'testuser', 6 | password: 'testpassword', 7 | host: 'localhost', 8 | database: 'testdb', 9 | port: 5432, 10 | ssl: false 11 | }); 12 | 13 | async function testTransaction() { 14 | try { 15 | // 성공 케이스 16 | console.log('\n1. 트랜잭션 성공 케이스:'); 17 | const successResult = await client.transaction(async (tx) => { 18 | // 사용자 생성 19 | const user = await tx 20 | .from('users') 21 | .insert({ 22 | name: '트랜잭션테스트', 23 | email: 'transaction@example.com', 24 | status: 'active', 25 | last_login: new Date().toISOString() 26 | }) 27 | .select() 28 | .single(); 29 | 30 | if (!user.data?.id) { 31 | throw new Error('Failed to create user'); 32 | } 33 | 34 | // 프로필 생성 35 | const profile = await tx 36 | .from('profiles') 37 | .insert({ 38 | user_id: user.data.id, 39 | bio: '트랜잭션으로 생성된 프로필', 40 | interests: ['테스트'], 41 | updated_at: new Date().toISOString() 42 | }) 43 | .select() 44 | .single(); 45 | 46 | return { user: user.data, profile: profile.data }; 47 | }); 48 | console.log('Transaction success:', successResult); 49 | 50 | // 실패 케이스 (롤백) 51 | console.log('\n2. 트랜잭션 실패 케이스 (중복 이메일):'); 52 | try { 53 | await client.transaction(async (tx) => { 54 | // 첫 번째 사용자 생성 (성공) 55 | await tx 56 | .from('users') 57 | .insert({ 58 | name: '트랜잭션실패1', 59 | email: 'fail@example.com', 60 | status: 'active', 61 | last_login: new Date().toISOString() 62 | }); 63 | 64 | // 두 번째 사용자 생성 (같은 이메일로 시도 - 실패) 65 | await tx 66 | .from('users') 67 | .insert({ 68 | name: '트랜잭션실패2', 69 | email: 'fail@example.com', // 중복 이메일 70 | status: 'active', 71 | last_login: new Date().toISOString() 72 | }); 73 | }); 74 | } catch (err) { 75 | const errorMessage = err instanceof Error ? err.message : String(err); 76 | console.log('Expected error (rollback successful):', errorMessage); 77 | 78 | // 롤백 확인 79 | const checkUsers = await client 80 | .from('users') 81 | .select('*') 82 | .eq('email', 'fail@example.com'); 83 | const users = checkUsers.data as Database['public']['Tables']['users']['Row'][]; 84 | console.log('Rollback verified - no users found:', !users || users.length === 0); 85 | } 86 | 87 | // 중첩 트랜잭션 88 | console.log('\n3. 중첩 트랜잭션 테스트:'); 89 | const nestedResult = await client.transaction(async (tx1) => { 90 | // 외부 트랜잭션: 사용자 생성 91 | const user = await tx1 92 | .from('users') 93 | .insert({ 94 | name: '중첩트랜잭션', 95 | email: 'nested@example.com', 96 | status: 'active', 97 | last_login: new Date().toISOString() 98 | }) 99 | .select() 100 | .single(); 101 | 102 | if (!user.data?.id) { 103 | throw new Error('Failed to create user'); 104 | } 105 | 106 | // 내부 트랜잭션: 포스트와 댓글 생성 107 | return await tx1.transaction(async (tx2) => { 108 | const post = await tx2 109 | .from('posts') 110 | .insert({ 111 | user_id: user.data!.id, 112 | title: '중첩 트랜잭션 테스트', 113 | content: '트랜잭션 안의 트랜잭션', 114 | tags: ['테스트'], 115 | updated_at: new Date().toISOString() 116 | }) 117 | .select() 118 | .single(); 119 | 120 | if (!post.data?.id) { 121 | throw new Error('Failed to create post'); 122 | } 123 | 124 | const comment = await tx2 125 | .from('comments') 126 | .insert({ 127 | post_id: post.data.id, 128 | user_id: user.data!.id, 129 | content: '자동 생성된 댓글' 130 | }) 131 | .select() 132 | .single(); 133 | 134 | return { 135 | user: user.data, 136 | post: post.data, 137 | comment: comment.data 138 | }; 139 | }); 140 | }); 141 | console.log('Nested transaction result:', nestedResult); 142 | 143 | // 정리: 테스트 데이터 삭제 144 | console.log('\n4. 테스트 데이터 정리:'); 145 | await client.transaction(async (tx) => { 146 | await tx 147 | .from('users') 148 | .delete() 149 | .in('email', ['transaction@example.com', 'nested@example.com']); 150 | }); 151 | console.log('Cleanup completed'); 152 | 153 | } catch (err) { 154 | const errorMessage = err instanceof Error ? err.message : String(err); 155 | console.error('Unexpected error:', errorMessage); 156 | } finally { 157 | await client.close(); 158 | } 159 | } 160 | 161 | testTransaction(); 162 | -------------------------------------------------------------------------------- /dist/__tests__/rpc.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const postgres_client_1 = require("../postgres-client"); 4 | const errors_1 = require("../errors"); 5 | const pg_1 = require("pg"); 6 | // Mock pg 7 | jest.mock('pg', () => { 8 | const mQuery = jest.fn(); 9 | const mPool = { 10 | query: mQuery, 11 | on: jest.fn(), 12 | connect: jest.fn().mockResolvedValue({ 13 | release: jest.fn(), 14 | query: mQuery 15 | }), 16 | end: jest.fn() 17 | }; 18 | return { 19 | Pool: jest.fn(() => mPool), 20 | types: { 21 | setTypeParser: jest.fn() 22 | } 23 | }; 24 | }); 25 | describe('SupaLitePG rpc', () => { 26 | let client; 27 | let mockQuery; 28 | beforeEach(() => { 29 | // Pool 생성자가 mockPool을 반환하므로, 새 Pool을 만들어서 mockQuery에 접근 30 | const pool = new pg_1.Pool(); 31 | mockQuery = pool.query; 32 | mockQuery.mockReset(); 33 | client = new postgres_client_1.SupaLitePG(); 34 | }); 35 | test('rpc() should return multiple rows by default', async () => { 36 | mockQuery.mockResolvedValueOnce({ 37 | rows: [{ id: 1 }, { id: 2 }], 38 | rowCount: 2 39 | }); 40 | const result = await client.rpc('get_users'); 41 | expect(result.data).toHaveLength(2); 42 | expect(result.error).toBeNull(); 43 | expect(result.count).toBe(2); 44 | }); 45 | test('rpc().single() should return single object if 1 row returned (multi-column)', async () => { 46 | mockQuery.mockResolvedValueOnce({ 47 | rows: [{ id: 1, name: 'user' }], 48 | rowCount: 1 49 | }); 50 | const result = await client.rpc('get_user').single(); 51 | expect(result.data).toEqual({ id: 1, name: 'user' }); 52 | expect(result.error).toBeNull(); 53 | }); 54 | test('rpc().single() should error if 0 rows returned', async () => { 55 | mockQuery.mockResolvedValueOnce({ 56 | rows: [], 57 | rowCount: 0 58 | }); 59 | const result = await client.rpc('get_user').single(); 60 | expect(result.data).toBeNull(); 61 | expect(result.error).toBeInstanceOf(errors_1.PostgresError); 62 | expect(result.error?.message).toContain('PGRST116'); // No rows found 63 | }); 64 | test('rpc().single() should error if multiple rows returned', async () => { 65 | mockQuery.mockResolvedValueOnce({ 66 | rows: [{ id: 1 }, { id: 2 }], 67 | rowCount: 2 68 | }); 69 | const result = await client.rpc('get_user').single(); 70 | expect(result.data).toBeNull(); 71 | expect(result.error).toBeInstanceOf(errors_1.PostgresError); 72 | expect(result.error?.message).toContain('PGRST114'); // Multiple rows returned 73 | }); 74 | test('rpc().maybeSingle() should return single object if 1 row returned (multi-column)', async () => { 75 | mockQuery.mockResolvedValueOnce({ 76 | rows: [{ id: 1, name: 'user' }], 77 | rowCount: 1 78 | }); 79 | const result = await client.rpc('get_user').maybeSingle(); 80 | expect(result.data).toEqual({ id: 1, name: 'user' }); 81 | expect(result.error).toBeNull(); 82 | }); 83 | test('rpc().maybeSingle() should return null data if 0 rows returned', async () => { 84 | mockQuery.mockResolvedValueOnce({ 85 | rows: [], 86 | rowCount: 0 87 | }); 88 | const result = await client.rpc('get_user').maybeSingle(); 89 | expect(result.data).toBeNull(); 90 | expect(result.error).toBeNull(); 91 | }); 92 | test('rpc().maybeSingle() should error if multiple rows returned', async () => { 93 | mockQuery.mockResolvedValueOnce({ 94 | rows: [{ id: 1 }, { id: 2 }], 95 | rowCount: 2 96 | }); 97 | const result = await client.rpc('get_user').maybeSingle(); 98 | expect(result.data).toBeNull(); 99 | expect(result.error).toBeInstanceOf(errors_1.PostgresError); 100 | expect(result.error?.message).toContain('PGRST114'); 101 | }); 102 | test('rpc() should unwrap scalar return values', async () => { 103 | mockQuery.mockResolvedValueOnce({ 104 | rows: [{ get_count: 42 }], // scalar return is 1 row, 1 column 105 | rowCount: 1 106 | }); 107 | const result = await client.rpc('get_count'); 108 | expect(result.data).toBe(42); 109 | expect(result.error).toBeNull(); 110 | }); 111 | test('rpc().single() should unwrap scalar return values', async () => { 112 | mockQuery.mockResolvedValueOnce({ 113 | rows: [{ get_count: 42 }], 114 | rowCount: 1 115 | }); 116 | const result = await client.rpc('get_count').single(); 117 | expect(result.data).toBe(42); 118 | expect(result.error).toBeNull(); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-native-array.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const postgres_client_1 = require("../postgres-client"); 4 | const pg_1 = require("pg"); 5 | const dotenv_1 = require("dotenv"); 6 | (0, dotenv_1.config)(); // Load .env variables 7 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 8 | describe('QueryBuilder with Native Array Columns (TEXT[], INTEGER[])', () => { 9 | let client; 10 | let pool; 11 | beforeAll(async () => { 12 | pool = new pg_1.Pool({ connectionString }); 13 | await pool.query(`DROP TABLE IF EXISTS native_array_test_table;`); 14 | await pool.query(` 15 | CREATE TABLE native_array_test_table ( 16 | id SERIAL PRIMARY KEY, 17 | tags TEXT[], 18 | scores INTEGER[], 19 | descriptions TEXT[] DEFAULT '{}' -- Example with default empty array 20 | ); 21 | `); 22 | }); 23 | beforeEach(async () => { 24 | client = new postgres_client_1.SupaLitePG({ connectionString }); 25 | await pool.query('DELETE FROM native_array_test_table;'); 26 | // Insert initial data using JS arrays, pg driver should handle conversion 27 | await pool.query(` 28 | INSERT INTO native_array_test_table (id, tags, scores) VALUES 29 | (1, ARRAY['initial_tag1', 'initial_tag2'], ARRAY[100, 200]), 30 | (2, '{}', ARRAY[]::INTEGER[]), -- Empty arrays 31 | (3, null, null); -- Null arrays 32 | `); 33 | }); 34 | afterEach(async () => { 35 | if (client) { 36 | await client.close(); 37 | } 38 | }); 39 | afterAll(async () => { 40 | await pool.query(`DROP TABLE IF EXISTS native_array_test_table;`); 41 | await pool.end(); 42 | }); 43 | test('should INSERT and SELECT TEXT[] with data', async () => { 44 | const newTags = ['alpha', 'beta', 'gamma']; 45 | const { data, error } = await client 46 | .from('native_array_test_table') 47 | .insert({ id: 4, tags: newTags }) 48 | .select('tags') 49 | .single(); 50 | expect(error).toBeNull(); 51 | expect(data).not.toBeNull(); 52 | expect(data?.tags).toEqual(newTags); 53 | }); 54 | test('should INSERT and SELECT empty TEXT[]', async () => { 55 | const emptyTags = []; 56 | const { data, error } = await client 57 | .from('native_array_test_table') 58 | .insert({ id: 5, tags: emptyTags }) 59 | .select('tags') 60 | .single(); 61 | expect(error).toBeNull(); 62 | expect(data).not.toBeNull(); 63 | expect(data?.tags).toEqual(emptyTags); 64 | }); 65 | test('should INSERT and SELECT INTEGER[] with data', async () => { 66 | const newScores = [10, 20, 30, 40]; 67 | const { data, error } = await client 68 | .from('native_array_test_table') 69 | .insert({ id: 6, scores: newScores }) 70 | .select('scores') 71 | .single(); 72 | expect(error).toBeNull(); 73 | expect(data).not.toBeNull(); 74 | expect(data?.scores).toEqual(newScores); 75 | }); 76 | test('should INSERT and SELECT empty INTEGER[]', async () => { 77 | const emptyScores = []; 78 | const { data, error } = await client 79 | .from('native_array_test_table') 80 | .insert({ id: 7, scores: emptyScores }) 81 | .select('scores') 82 | .single(); 83 | expect(error).toBeNull(); 84 | expect(data).not.toBeNull(); 85 | expect(data?.scores).toEqual(emptyScores); 86 | }); 87 | test('should INSERT and SELECT NULL for array types', async () => { 88 | const { data, error } = await client 89 | .from('native_array_test_table') 90 | .insert({ id: 8, tags: null, scores: null }) 91 | .select('tags, scores') 92 | .single(); 93 | expect(error).toBeNull(); 94 | expect(data).not.toBeNull(); 95 | expect(data?.tags).toBeNull(); 96 | expect(data?.scores).toBeNull(); 97 | }); 98 | test('should UPDATE TEXT[] column', async () => { 99 | const updatedTags = ['updated_initial_tag1', 'new_tag_xyz']; 100 | const { data, error } = await client 101 | .from('native_array_test_table') 102 | .update({ tags: updatedTags }) 103 | .eq('id', 1) 104 | .select('tags') 105 | .single(); 106 | expect(error).toBeNull(); 107 | expect(data).not.toBeNull(); 108 | expect(data?.tags).toEqual(updatedTags); 109 | }); 110 | test('should filter using array contains @>', async () => { 111 | const { data, error } = await client 112 | .from('native_array_test_table') 113 | .select('id') 114 | .contains('tags', ['initial_tag1']) 115 | .single(); 116 | expect(error).toBeNull(); 117 | expect(data).not.toBeNull(); 118 | expect(data?.id).toBe(1); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /examples/tests/mock-query-result.ts: -------------------------------------------------------------------------------- 1 | import { PostgresError } from '../../src/errors'; 2 | import { QueryResult } from '../../src/types'; 3 | 4 | // 모의 데이터 5 | const mockUsers = [ 6 | { id: 1, name: '홍길동', email: 'hong@example.com', created_at: '2025-01-01T00:00:00Z' }, 7 | { id: 2, name: '김철수', email: 'kim@example.com', created_at: '2025-01-02T00:00:00Z' }, 8 | { id: 3, name: '이영희', email: 'lee@gmail.com', created_at: '2025-01-03T00:00:00Z' } 9 | ]; 10 | 11 | // 모의 QueryResult 생성 함수 12 | function createMockQueryResult(data: T[]): QueryResult { 13 | return { 14 | data, 15 | error: null, 16 | count: data.length, 17 | status: 200, 18 | statusText: 'OK' 19 | }; 20 | } 21 | 22 | /** 23 | * 예제 1: 데이터 조회 및 배열 메서드 사용 24 | * 25 | * 이 예제는 users 테이블에서 데이터를 조회하고, 26 | * 결과가 항상 배열로 반환되므로 배열 메서드를 안전하게 사용할 수 있음을 보여줍니다. 27 | */ 28 | function example1() { 29 | // 모의 데이터로 QueryResult 생성 30 | const result = createMockQueryResult(mockUsers); 31 | 32 | // 타입 가드: result.data가 배열인지 확인 33 | if (Array.isArray(result.data)) { 34 | // 결과가 항상 배열이므로 length 속성 사용 가능 35 | console.log(`조회된 사용자 수: ${result.data.length}`); 36 | 37 | // 배열 메서드 사용 (map) 38 | const userNames = result.data.map(user => user.name); 39 | console.log('사용자 이름 목록:', userNames); 40 | 41 | // 배열 메서드 사용 (filter) 42 | const filteredUsers = result.data.filter(user => 43 | user.email.endsWith('@example.com') 44 | ); 45 | console.log('example.com 이메일을 사용하는 사용자:', filteredUsers); 46 | 47 | // 배열 메서드 사용 (forEach) 48 | console.log('모든 사용자 정보:'); 49 | result.data.forEach(user => { 50 | console.log(`ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}`); 51 | }); 52 | } 53 | 54 | return result; 55 | } 56 | 57 | /** 58 | * 예제 2: 결과가 없을 때의 처리 59 | * 60 | * 이 예제는 조건에 맞는 데이터가 없을 때도 61 | * 빈 배열이 반환되므로 안전하게 배열 메서드를 사용할 수 있음을 보여줍니다. 62 | */ 63 | function example2() { 64 | // 빈 배열로 QueryResult 생성 (타입 정보 추가) 65 | const result = createMockQueryResult<{id: number; name: string; email: string; created_at: string}>([]); 66 | 67 | // 타입 가드: result.data가 배열인지 확인 68 | if (Array.isArray(result.data)) { 69 | // 결과가 없어도 빈 배열이 반환되므로 length 속성 사용 가능 70 | console.log(`조회된 사용자 수: ${result.data.length}`); // 0 출력 71 | 72 | // 빈 배열에도 배열 메서드 사용 가능 73 | const userNames = result.data.map(user => user.name); 74 | console.log('사용자 이름 목록:', userNames); // [] 출력 75 | 76 | // 조건부 처리 77 | if (result.data.length === 0) { 78 | console.log('조건에 맞는 사용자가 없습니다.'); 79 | } else { 80 | console.log('조건에 맞는 사용자가 있습니다.'); 81 | } 82 | } 83 | 84 | return result; 85 | } 86 | 87 | /** 88 | * 예제 3: 에러 처리 89 | * 90 | * 이 예제는 에러가 발생했을 때도 91 | * data 필드가 빈 배열을 반환하므로 안전하게 사용할 수 있음을 보여줍니다. 92 | */ 93 | function example3() { 94 | try { 95 | // 에러 발생 시뮬레이션 96 | throw new Error('테이블이 존재하지 않습니다.'); 97 | } catch (error) { 98 | // 에러 객체 생성 99 | const errorResult = { 100 | data: [], // 에러 발생 시에도 빈 배열 반환 101 | error: new PostgresError('테이블이 존재하지 않습니다.'), 102 | count: null, 103 | status: 500, 104 | statusText: 'Internal Server Error', 105 | }; 106 | 107 | // 에러가 발생해도 data 필드는 빈 배열이므로 안전하게 사용 가능 108 | console.log(`에러 발생! 데이터 수: ${errorResult.data.length}`); // 0 출력 109 | console.log(`에러 메시지: ${errorResult.error?.message}`); 110 | 111 | return errorResult; 112 | } 113 | } 114 | 115 | /** 116 | * 예제 4: Supabase 호환성 예제 117 | * 118 | * 이 예제는 Supabase 코드를 Supalite로 마이그레이션할 때 119 | * 타입 호환성 문제 없이 사용할 수 있음을 보여줍니다. 120 | */ 121 | function example4() { 122 | // 모의 데이터로 QueryResult 생성 123 | const result = createMockQueryResult(mockUsers); 124 | 125 | // 타입 가드: result.data가 배열인지 확인 126 | if (Array.isArray(result.data)) { 127 | // Supabase 코드와 동일하게 length 속성 사용 128 | const userCount = result.data.length; 129 | console.log(`사용자 수: ${userCount}`); 130 | 131 | // Supabase 코드와 동일하게 배열 메서드 사용 132 | const processedUsers = result.data.map(user => ({ 133 | id: user.id, 134 | displayName: user.name, 135 | emailAddress: user.email, 136 | })); 137 | 138 | // 배열 파라미터로 전달 139 | const processUsers = (users: any[]) => { 140 | return users.map(user => `${user.name} (${user.email})`); 141 | }; 142 | 143 | // result.data를 배열 파라미터로 전달 (Supabase 호환) 144 | const userList = processUsers(result.data); 145 | console.log('사용자 목록:', userList); 146 | 147 | return { 148 | userCount, 149 | processedUsers, 150 | userList, 151 | }; 152 | } 153 | 154 | return { userCount: 0, processedUsers: [], userList: [] }; 155 | } 156 | 157 | // 예제 실행 함수 158 | function runExamples() { 159 | console.log('===== 예제 1: 데이터 조회 및 배열 메서드 사용 ====='); 160 | example1(); 161 | 162 | console.log('\n===== 예제 2: 결과가 없을 때의 처리 ====='); 163 | example2(); 164 | 165 | console.log('\n===== 예제 3: 에러 처리 ====='); 166 | example3(); 167 | 168 | console.log('\n===== 예제 4: Supabase 호환성 예제 ====='); 169 | example4(); 170 | } 171 | 172 | // 예제 실행 173 | runExamples(); 174 | 175 | export { 176 | example1, 177 | example2, 178 | example3, 179 | example4, 180 | runExamples, 181 | }; 182 | -------------------------------------------------------------------------------- /examples/tests/connection-string.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../../src'; 2 | import { Database } from '../types/database'; 3 | import { config } from 'dotenv'; 4 | 5 | // .env 파일 로드 6 | config(); 7 | 8 | // 테스트 1: 직접 connectionString 사용 9 | async function testDirectConnectionString() { 10 | console.log('\n테스트 1: 직접 connectionString 사용'); 11 | 12 | const connectionString = 'postgresql://testuser:testpassword@localhost:5432/testdb'; 13 | 14 | const client = new SupaLitePG({ 15 | connectionString, 16 | ssl: false 17 | }); 18 | 19 | try { 20 | console.log('연결 문자열 사용하여 연결 시도...'); 21 | 22 | const isConnected = await client.testConnection(); 23 | if (!isConnected) { 24 | console.error('연결 테스트 실패'); 25 | return; 26 | } 27 | 28 | console.log('연결 성공!'); 29 | 30 | const users = await client 31 | .from('users') 32 | .select('name, email') 33 | .limit(3); 34 | 35 | if (users.error) { 36 | console.error('Error:', users.error); 37 | } else { 38 | console.log('사용자 데이터:', users.data); 39 | } 40 | } catch (err) { 41 | console.error('예상치 못한 오류:', err); 42 | } finally { 43 | await client.close(); 44 | } 45 | } 46 | 47 | // 테스트 2: 환경 변수 DB_CONNECTION 사용 48 | async function testEnvConnectionString() { 49 | console.log('\n테스트 2: 환경 변수 DB_CONNECTION 사용'); 50 | 51 | // 기존 환경 변수 백업 52 | const originalConnection = process.env.DB_CONNECTION; 53 | 54 | // 테스트용 환경 변수 설정 55 | process.env.DB_CONNECTION = 'postgresql://testuser:testpassword@localhost:5432/testdb'; 56 | 57 | const client = new SupaLitePG(); 58 | 59 | try { 60 | console.log('환경 변수 DB_CONNECTION 사용하여 연결 시도...'); 61 | 62 | const isConnected = await client.testConnection(); 63 | if (!isConnected) { 64 | console.error('연결 테스트 실패'); 65 | return; 66 | } 67 | 68 | console.log('연결 성공!'); 69 | 70 | const users = await client 71 | .from('users') 72 | .select('name, email') 73 | .limit(3); 74 | 75 | if (users.error) { 76 | console.error('Error:', users.error); 77 | } else { 78 | console.log('사용자 데이터:', users.data); 79 | } 80 | } catch (err) { 81 | console.error('예상치 못한 오류:', err); 82 | } finally { 83 | // 환경 변수 복원 84 | process.env.DB_CONNECTION = originalConnection; 85 | await client.close(); 86 | } 87 | } 88 | 89 | // 테스트 3: Supabase 형식 연결 문자열 테스트 90 | async function testSupabaseConnectionString() { 91 | console.log('\n테스트 3: Supabase 형식 연결 문자열 테스트'); 92 | 93 | const connectionString = 'postgresql://testuser:testpassword@localhost:5432/testdb'; 94 | 95 | const client = new SupaLitePG({ 96 | connectionString, 97 | ssl: true 98 | }); 99 | 100 | try { 101 | console.log('Supabase 형식 연결 문자열 사용하여 연결 시도...'); 102 | 103 | const isConnected = await client.testConnection(); 104 | if (!isConnected) { 105 | console.error('연결 테스트 실패'); 106 | return; 107 | } 108 | 109 | console.log('연결 성공!'); 110 | 111 | const users = await client 112 | .from('users') 113 | .select('name, email') 114 | .limit(3); 115 | 116 | if (users.error) { 117 | console.error('Error:', users.error); 118 | } else { 119 | console.log('사용자 데이터:', users.data); 120 | } 121 | } catch (err) { 122 | console.error('예상치 못한 오류:', err); 123 | } finally { 124 | await client.close(); 125 | } 126 | } 127 | 128 | // 테스트 4: 잘못된 연결 문자열 테스트 129 | async function testInvalidConnectionString() { 130 | console.log('\n테스트 4: 잘못된 연결 문자열 테스트'); 131 | 132 | try { 133 | // 잘못된 형식의 연결 문자열 134 | const invalidConnectionString = 'invalid://user:pass@host:port/db'; 135 | 136 | console.log('잘못된 형식의 연결 문자열로 연결 시도...'); 137 | 138 | const client = new SupaLitePG({ 139 | connectionString: invalidConnectionString 140 | }); 141 | 142 | // 이 부분은 실행되지 않아야 함 (위에서 예외가 발생해야 함) 143 | console.error('오류: 잘못된 연결 문자열로 인스턴스 생성 성공'); 144 | await client.close(); 145 | } catch (err: any) { 146 | console.log('예상된 오류 발생:', err.message); 147 | } 148 | 149 | try { 150 | // 존재하지 않는 서버 연결 문자열 151 | const nonExistentServerString = 'postgresql://user:pass@non-existent-server:5432/db'; 152 | 153 | console.log('존재하지 않는 서버에 연결 시도...'); 154 | 155 | const client = new SupaLitePG({ 156 | connectionString: nonExistentServerString 157 | }); 158 | 159 | const isConnected = await client.testConnection(); 160 | 161 | if (!isConnected) { 162 | console.log('예상대로 연결 실패'); 163 | } else { 164 | console.error('오류: 존재하지 않는 서버에 연결 성공함'); 165 | } 166 | 167 | await client.close(); 168 | } catch (err: any) { 169 | console.log('예상된 오류 발생:', err.message); 170 | } 171 | } 172 | 173 | // 모든 테스트 실행 174 | async function runAllTests() { 175 | console.log('=== 연결 문자열(URI) 테스트 시작 ==='); 176 | 177 | await testDirectConnectionString(); 178 | await testEnvConnectionString(); 179 | await testSupabaseConnectionString(); 180 | await testInvalidConnectionString(); 181 | 182 | console.log('\n=== 모든 테스트 완료 ==='); 183 | } 184 | 185 | runAllTests(); 186 | -------------------------------------------------------------------------------- /dist/__tests__/query-builder-bigint.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const postgres_client_1 = require("../postgres-client"); 4 | const pg_1 = require("pg"); 5 | const dotenv_1 = require("dotenv"); 6 | (0, dotenv_1.config)(); // Load .env variables 7 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 8 | describe('QueryBuilder with BigInt Column', () => { 9 | let client; 10 | let pool; 11 | beforeAll(async () => { 12 | pool = new pg_1.Pool({ connectionString }); 13 | await pool.query(`DROP TABLE IF EXISTS bigint_test_table;`); 14 | await pool.query(` 15 | CREATE TABLE bigint_test_table ( 16 | id SERIAL PRIMARY KEY, 17 | bigint_value BIGINT, 18 | description TEXT 19 | ); 20 | `); 21 | }); 22 | beforeEach(async () => { 23 | client = new postgres_client_1.SupaLitePG({ connectionString }); 24 | await pool.query('DELETE FROM bigint_test_table;'); 25 | // Insert initial data using BigInt literals, ensuring they are passed as strings and are within PostgreSQL bigint range 26 | await pool.query(` 27 | INSERT INTO bigint_test_table (id, bigint_value, description) VALUES 28 | (1, '1234567890123456789', 'First BigInt'), 29 | (2, '9007199254740992', 'Second BigInt (MAX_SAFE_INTEGER + 1)'), 30 | (3, null, 'Null BigInt'); 31 | `); 32 | }); 33 | afterEach(async () => { 34 | if (client) { 35 | await client.close(); 36 | } 37 | }); 38 | afterAll(async () => { 39 | await pool.query(`DROP TABLE IF EXISTS bigint_test_table;`); 40 | await pool.end(); 41 | }); 42 | test('should SELECT BigInt data correctly', async () => { 43 | const { data, error } = await client 44 | .from('bigint_test_table') 45 | .select('id, bigint_value, description') 46 | .eq('id', 1) 47 | .single(); 48 | expect(error).toBeNull(); 49 | expect(data).not.toBeNull(); 50 | expect(data?.id).toBe(1); 51 | expect(data?.bigint_value).toBe(1234567890123456789n); // Expect BigInt 52 | expect(data?.description).toBe('First BigInt'); 53 | }); 54 | test('should INSERT BigInt data correctly', async () => { 55 | const newId = 4; 56 | const newBigIntValue = 8000000000000000000n; // Within range 57 | const newDescription = 'Inserted BigInt'; 58 | const insertValues = { 59 | id: newId, 60 | bigint_value: newBigIntValue, 61 | description: newDescription 62 | }; 63 | const { data, error } = await client 64 | .from('bigint_test_table') 65 | .insert(insertValues) 66 | .select() 67 | .single(); 68 | expect(error).toBeNull(); 69 | expect(data).not.toBeNull(); 70 | expect(data?.id).toBe(newId); 71 | expect(data?.bigint_value).toBe(newBigIntValue); 72 | expect(data?.description).toBe(newDescription); 73 | }); 74 | test('should UPDATE BigInt data correctly', async () => { 75 | const idToUpdate = 2; 76 | const updatedBigIntValue = 7000000000000000000n; // Within range 77 | const updatedDescription = 'Updated Second BigInt'; 78 | const updateValues = { 79 | bigint_value: updatedBigIntValue, 80 | description: updatedDescription 81 | }; 82 | const { data, error } = await client 83 | .from('bigint_test_table') 84 | .update(updateValues) 85 | .eq('id', idToUpdate) 86 | .select() 87 | .single(); 88 | expect(error).toBeNull(); 89 | expect(data).not.toBeNull(); 90 | expect(data?.id).toBe(idToUpdate); 91 | expect(data?.bigint_value).toBe(updatedBigIntValue); 92 | expect(data?.description).toBe(updatedDescription); 93 | }); 94 | test('should filter using BigInt data in WHERE clause', async () => { 95 | const { data, error } = await client 96 | .from('bigint_test_table') 97 | .select('id, description') 98 | .eq('bigint_value', 1234567890123456789n) // Filter by BigInt 99 | .single(); 100 | expect(error).toBeNull(); 101 | expect(data).not.toBeNull(); 102 | expect(data?.id).toBe(1); 103 | expect(data?.description).toBe('First BigInt'); 104 | }); 105 | test('should handle NULL BigInt values correctly on INSERT and SELECT', async () => { 106 | const newId = 5; 107 | const { error: insertError } = await client 108 | .from('bigint_test_table') 109 | .insert({ id: newId, bigint_value: null, description: 'Explicit Null BigInt' }) 110 | .select() 111 | .single(); 112 | expect(insertError).toBeNull(); 113 | const { data, error: selectError } = await client 114 | .from('bigint_test_table') 115 | .select('id, bigint_value') 116 | .eq('id', newId) 117 | .single(); 118 | expect(selectError).toBeNull(); 119 | expect(data).not.toBeNull(); 120 | expect(data?.id).toBe(newId); 121 | expect(data?.bigint_value).toBeNull(); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-native-array.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../postgres-client'; 2 | import { Pool } from 'pg'; 3 | import { config } from 'dotenv'; 4 | import { DatabaseSchema, TableBase } from '../types'; 5 | 6 | config(); // Load .env variables 7 | 8 | // Define Row/Insert/Update types for the native_array_test_table 9 | type NativeArrayTestTableRow = { id: number; tags: string[] | null; scores: number[] | null; descriptions?: string[] | null }; 10 | type NativeArrayTestTableInsert = { id?: number; tags?: string[] | null; scores?: number[] | null; descriptions?: string[] | null }; 11 | type NativeArrayTestTableUpdate = { id?: number; tags?: string[] | null; scores?: number[] | null; descriptions?: string[] | null }; 12 | 13 | // Define our test-specific database schema including the new table 14 | interface TestDatabaseWithNativeArray extends DatabaseSchema { 15 | public: { 16 | Tables: { 17 | native_array_test_table: TableBase & { Row: NativeArrayTestTableRow; Insert: NativeArrayTestTableInsert; Update: NativeArrayTestTableUpdate; Relationships: [] }; 18 | }; 19 | Views: Record; 20 | Functions: Record; 21 | Enums: Record; 22 | CompositeTypes: Record; 23 | }; 24 | } 25 | 26 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 27 | 28 | describe('QueryBuilder with Native Array Columns (TEXT[], INTEGER[])', () => { 29 | let client: SupaLitePG; 30 | let pool: Pool; 31 | 32 | beforeAll(async () => { 33 | pool = new Pool({ connectionString }); 34 | await pool.query(`DROP TABLE IF EXISTS native_array_test_table;`); 35 | await pool.query(` 36 | CREATE TABLE native_array_test_table ( 37 | id SERIAL PRIMARY KEY, 38 | tags TEXT[], 39 | scores INTEGER[], 40 | descriptions TEXT[] DEFAULT '{}' -- Example with default empty array 41 | ); 42 | `); 43 | }); 44 | 45 | beforeEach(async () => { 46 | client = new SupaLitePG({ connectionString }); 47 | await pool.query('DELETE FROM native_array_test_table;'); 48 | // Insert initial data using JS arrays, pg driver should handle conversion 49 | await pool.query(` 50 | INSERT INTO native_array_test_table (id, tags, scores) VALUES 51 | (1, ARRAY['initial_tag1', 'initial_tag2'], ARRAY[100, 200]), 52 | (2, '{}', ARRAY[]::INTEGER[]), -- Empty arrays 53 | (3, null, null); -- Null arrays 54 | `); 55 | }); 56 | 57 | afterEach(async () => { 58 | if (client) { 59 | await client.close(); 60 | } 61 | }); 62 | 63 | afterAll(async () => { 64 | await pool.query(`DROP TABLE IF EXISTS native_array_test_table;`); 65 | await pool.end(); 66 | }); 67 | 68 | test('should INSERT and SELECT TEXT[] with data', async () => { 69 | const newTags = ['alpha', 'beta', 'gamma']; 70 | const { data, error } = await client 71 | .from('native_array_test_table') 72 | .insert({ id: 4, tags: newTags }) 73 | .select('tags') 74 | .single(); 75 | 76 | expect(error).toBeNull(); 77 | expect(data).not.toBeNull(); 78 | expect(data?.tags).toEqual(newTags); 79 | }); 80 | 81 | test('should INSERT and SELECT empty TEXT[]', async () => { 82 | const emptyTags: string[] = []; 83 | const { data, error } = await client 84 | .from('native_array_test_table') 85 | .insert({ id: 5, tags: emptyTags }) 86 | .select('tags') 87 | .single(); 88 | 89 | expect(error).toBeNull(); 90 | expect(data).not.toBeNull(); 91 | expect(data?.tags).toEqual(emptyTags); 92 | }); 93 | 94 | test('should INSERT and SELECT INTEGER[] with data', async () => { 95 | const newScores = [10, 20, 30, 40]; 96 | const { data, error } = await client 97 | .from('native_array_test_table') 98 | .insert({ id: 6, scores: newScores }) 99 | .select('scores') 100 | .single(); 101 | 102 | expect(error).toBeNull(); 103 | expect(data).not.toBeNull(); 104 | expect(data?.scores).toEqual(newScores); 105 | }); 106 | 107 | test('should INSERT and SELECT empty INTEGER[]', async () => { 108 | const emptyScores: number[] = []; 109 | const { data, error } = await client 110 | .from('native_array_test_table') 111 | .insert({ id: 7, scores: emptyScores }) 112 | .select('scores') 113 | .single(); 114 | 115 | expect(error).toBeNull(); 116 | expect(data).not.toBeNull(); 117 | expect(data?.scores).toEqual(emptyScores); 118 | }); 119 | 120 | test('should INSERT and SELECT NULL for array types', async () => { 121 | const { data, error } = await client 122 | .from('native_array_test_table') 123 | .insert({ id: 8, tags: null, scores: null }) 124 | .select('tags, scores') 125 | .single(); 126 | 127 | expect(error).toBeNull(); 128 | expect(data).not.toBeNull(); 129 | expect(data?.tags).toBeNull(); 130 | expect(data?.scores).toBeNull(); 131 | }); 132 | 133 | test('should UPDATE TEXT[] column', async () => { 134 | const updatedTags = ['updated_initial_tag1', 'new_tag_xyz']; 135 | const { data, error } = await client 136 | .from('native_array_test_table') 137 | .update({ tags: updatedTags }) 138 | .eq('id', 1) 139 | .select('tags') 140 | .single(); 141 | 142 | expect(error).toBeNull(); 143 | expect(data).not.toBeNull(); 144 | expect(data?.tags).toEqual(updatedTags); 145 | }); 146 | 147 | test('should filter using array contains @>', async () => { 148 | const { data, error } = await client 149 | .from('native_array_test_table') 150 | .select('id') 151 | .contains('tags', ['initial_tag1']) 152 | .single(); 153 | 154 | expect(error).toBeNull(); 155 | expect(data).not.toBeNull(); 156 | expect(data?.id).toBe(1); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /examples/tests/view-table.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteClient } from '../../src'; 2 | import { Database } from '../types/database'; 3 | 4 | // Node.js 타입 정의 5 | declare const process: { 6 | env: { 7 | [key: string]: string | undefined; 8 | }; 9 | }; 10 | 11 | // Supalite 클라이언트 인스턴스 생성 12 | const supalite = new SupaliteClient({ 13 | connectionString: process.env.DB_CONNECTION || 'postgresql://postgres:postgres@localhost:5432/testdb', 14 | }); 15 | 16 | /** 17 | * 예제 1: View 테이블 조회 18 | * 19 | * 이 예제는 user_posts_view 뷰에서 데이터를 조회하는 방법을 보여줍니다. 20 | */ 21 | async function example1() { 22 | try { 23 | // View 테이블 조회 24 | const result = await supalite 25 | .from('user_posts_view') 26 | .select('*'); 27 | 28 | console.log('===== user_posts_view 조회 결과 ====='); 29 | console.log(`조회된 데이터 수: ${result.data.length}`); 30 | 31 | // 결과 데이터 출력 32 | result.data.forEach(row => { 33 | console.log(`사용자: ${row.user_name} (ID: ${row.user_id})`); 34 | console.log(`게시물: ${row.post_title} (ID: ${row.post_id})`); 35 | console.log(`내용: ${row.post_content || '(내용 없음)'}`); 36 | console.log(`작성일: ${row.post_created_at}`); 37 | console.log('-------------------'); 38 | }); 39 | 40 | return result; 41 | } catch (error) { 42 | console.error('에러 발생:', error); 43 | throw error; 44 | } 45 | } 46 | 47 | /** 48 | * 예제 2: View 테이블에 조건 적용 49 | * 50 | * 이 예제는 active_users_view 뷰에서 조건을 적용하여 데이터를 조회하는 방법을 보여줍니다. 51 | */ 52 | async function example2() { 53 | try { 54 | // View 테이블에 조건 적용하여 조회 55 | const result = await supalite 56 | .from('active_users_view') 57 | .select('*') 58 | .gte('post_count', 2); // 게시물이 2개 이상인 사용자만 조회 59 | 60 | console.log('===== active_users_view 조회 결과 (게시물 2개 이상) ====='); 61 | console.log(`조회된 데이터 수: ${result.data.length}`); 62 | 63 | // 결과 데이터 출력 64 | result.data.forEach(row => { 65 | console.log(`사용자: ${row.name} (ID: ${row.id})`); 66 | console.log(`이메일: ${row.email}`); 67 | console.log(`마지막 로그인: ${row.last_login || '(로그인 기록 없음)'}`); 68 | console.log(`게시물 수: ${row.post_count}`); 69 | console.log('-------------------'); 70 | }); 71 | 72 | return result; 73 | } catch (error) { 74 | console.error('에러 발생:', error); 75 | throw error; 76 | } 77 | } 78 | 79 | /** 80 | * 예제 3: View 테이블과 일반 테이블 조인 81 | * 82 | * 이 예제는 View 테이블과 일반 테이블을 함께 사용하는 방법을 보여줍니다. 83 | * (실제 조인은 SQL 쿼리에서 이루어지지만, 여기서는 두 테이블을 개별적으로 조회하여 결합하는 방식으로 구현) 84 | */ 85 | async function example3() { 86 | try { 87 | // 1. active_users_view에서 사용자 조회 88 | const activeUsers = await supalite 89 | .from('active_users_view') 90 | .select('*') 91 | .order('post_count', { ascending: false }) 92 | .limit(3); // 게시물이 많은 상위 3명의 사용자만 조회 93 | 94 | console.log('===== 게시물이 많은 상위 3명의 사용자 ====='); 95 | 96 | // 2. 각 사용자의 프로필 정보 조회 97 | const userProfiles = await Promise.all( 98 | activeUsers.data.map(async user => { 99 | const profileResult = await supalite 100 | .from('profiles') 101 | .select('*') 102 | .eq('user_id', user.id) 103 | .single(); 104 | 105 | return { 106 | user: user, 107 | profile: profileResult.data 108 | }; 109 | }) 110 | ); 111 | 112 | // 결과 데이터 출력 113 | userProfiles.forEach(({ user, profile }) => { 114 | console.log(`사용자: ${user.name} (ID: ${user.id})`); 115 | console.log(`이메일: ${user.email}`); 116 | console.log(`게시물 수: ${user.post_count}`); 117 | 118 | if (profile) { 119 | console.log(`프로필 정보:`); 120 | console.log(` 소개: ${profile.bio || '(소개 없음)'}`); 121 | console.log(` 아바타: ${profile.avatar_url || '(아바타 없음)'}`); 122 | console.log(` 관심사: ${profile.interests ? profile.interests.join(', ') : '(관심사 없음)'}`); 123 | } else { 124 | console.log('프로필 정보 없음'); 125 | } 126 | 127 | console.log('-------------------'); 128 | }); 129 | 130 | return userProfiles; 131 | } catch (error) { 132 | console.error('에러 발생:', error); 133 | throw error; 134 | } 135 | } 136 | 137 | /** 138 | * 예제 4: View 테이블에 단일 결과 조회 139 | * 140 | * 이 예제는 View 테이블에서 단일 결과를 조회하는 방법을 보여줍니다. 141 | */ 142 | async function example4() { 143 | try { 144 | // View 테이블에서 단일 결과 조회 145 | const result = await supalite 146 | .from('user_posts_view') 147 | .select('*') 148 | .eq('post_id', 1) // ID가 1인 게시물 조회 149 | .single(); 150 | 151 | console.log('===== user_posts_view에서 단일 게시물 조회 ====='); 152 | 153 | if (result.data) { 154 | console.log(`사용자: ${result.data.user_name} (ID: ${result.data.user_id})`); 155 | console.log(`게시물: ${result.data.post_title} (ID: ${result.data.post_id})`); 156 | console.log(`내용: ${result.data.post_content || '(내용 없음)'}`); 157 | console.log(`작성일: ${result.data.post_created_at}`); 158 | } else { 159 | console.log('게시물을 찾을 수 없습니다.'); 160 | } 161 | 162 | return result; 163 | } catch (error) { 164 | console.error('에러 발생:', error); 165 | throw error; 166 | } 167 | } 168 | 169 | // 예제 실행 함수 170 | async function runExamples() { 171 | console.log('===== View 테이블 조회 예제 ====='); 172 | 173 | console.log('\n===== 예제 1: View 테이블 조회 ====='); 174 | await example1().catch(console.error); 175 | 176 | console.log('\n===== 예제 2: View 테이블에 조건 적용 ====='); 177 | await example2().catch(console.error); 178 | 179 | console.log('\n===== 예제 3: View 테이블과 일반 테이블 조인 ====='); 180 | await example3().catch(console.error); 181 | 182 | console.log('\n===== 예제 4: View 테이블에 단일 결과 조회 ====='); 183 | await example4().catch(console.error); 184 | } 185 | 186 | // 예제 실행 187 | console.log('===== View 테이블 조회 예제 실행 ====='); 188 | runExamples().catch(console.error); 189 | 190 | export { 191 | example1, 192 | example2, 193 | example3, 194 | example4, 195 | runExamples, 196 | }; 197 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-bigint.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../postgres-client'; 2 | import { Pool } from 'pg'; 3 | import { config } from 'dotenv'; 4 | import { DatabaseSchema, TableBase } from '../types'; 5 | 6 | config(); // Load .env variables 7 | 8 | // Define Row/Insert/Update types for the bigint_test_table 9 | type BigintTestTableRow = { id: number; bigint_value: bigint | null; description?: string | null }; 10 | type BigintTestTableInsert = { id?: number; bigint_value?: bigint | null; description?: string | null }; 11 | type BigintTestTableUpdate = { id?: number; bigint_value?: bigint | null; description?: string | null }; 12 | 13 | // Define our test-specific database schema including the new table 14 | interface TestDatabaseWithBigint extends DatabaseSchema { 15 | public: { 16 | Tables: { 17 | bigint_test_table: TableBase & { Row: BigintTestTableRow; Insert: BigintTestTableInsert; Update: BigintTestTableUpdate; Relationships: [] }; 18 | }; 19 | Views: Record; 20 | Functions: Record; 21 | Enums: Record; 22 | CompositeTypes: Record; 23 | }; 24 | } 25 | 26 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 27 | 28 | describe('QueryBuilder with BigInt Column', () => { 29 | let client: SupaLitePG; 30 | let pool: Pool; 31 | 32 | beforeAll(async () => { 33 | pool = new Pool({ connectionString }); 34 | await pool.query(`DROP TABLE IF EXISTS bigint_test_table;`); 35 | await pool.query(` 36 | CREATE TABLE bigint_test_table ( 37 | id SERIAL PRIMARY KEY, 38 | bigint_value BIGINT, 39 | description TEXT 40 | ); 41 | `); 42 | }); 43 | 44 | beforeEach(async () => { 45 | client = new SupaLitePG({ connectionString }); 46 | await pool.query('DELETE FROM bigint_test_table;'); 47 | // Insert initial data using BigInt literals, ensuring they are passed as strings and are within PostgreSQL bigint range 48 | await pool.query(` 49 | INSERT INTO bigint_test_table (id, bigint_value, description) VALUES 50 | (1, '1234567890123456789', 'First BigInt'), 51 | (2, '9007199254740992', 'Second BigInt (MAX_SAFE_INTEGER + 1)'), 52 | (3, null, 'Null BigInt'); 53 | `); 54 | }); 55 | 56 | afterEach(async () => { 57 | if (client) { 58 | await client.close(); 59 | } 60 | }); 61 | 62 | afterAll(async () => { 63 | await pool.query(`DROP TABLE IF EXISTS bigint_test_table;`); 64 | await pool.end(); 65 | }); 66 | 67 | test('should SELECT BigInt data correctly', async () => { 68 | const { data, error } = await client 69 | .from('bigint_test_table') 70 | .select('id, bigint_value, description') 71 | .eq('id', 1) 72 | .single(); 73 | 74 | expect(error).toBeNull(); 75 | expect(data).not.toBeNull(); 76 | expect(data?.id).toBe(1); 77 | expect(data?.bigint_value).toBe(1234567890123456789n); // Expect BigInt 78 | expect(data?.description).toBe('First BigInt'); 79 | }); 80 | 81 | test('should INSERT BigInt data correctly', async () => { 82 | const newId = 4; 83 | const newBigIntValue = 8000000000000000000n; // Within range 84 | const newDescription = 'Inserted BigInt'; 85 | 86 | const insertValues: BigintTestTableInsert = { 87 | id: newId, 88 | bigint_value: newBigIntValue, 89 | description: newDescription 90 | }; 91 | const { data, error } = await client 92 | .from('bigint_test_table') 93 | .insert(insertValues) 94 | .select() 95 | .single(); 96 | 97 | expect(error).toBeNull(); 98 | expect(data).not.toBeNull(); 99 | expect(data?.id).toBe(newId); 100 | expect(data?.bigint_value).toBe(newBigIntValue); 101 | expect(data?.description).toBe(newDescription); 102 | }); 103 | 104 | test('should UPDATE BigInt data correctly', async () => { 105 | const idToUpdate = 2; 106 | const updatedBigIntValue = 7000000000000000000n; // Within range 107 | const updatedDescription = 'Updated Second BigInt'; 108 | 109 | const updateValues: BigintTestTableUpdate = { 110 | bigint_value: updatedBigIntValue, 111 | description: updatedDescription 112 | }; 113 | const { data, error } = await client 114 | .from('bigint_test_table') 115 | .update(updateValues) 116 | .eq('id', idToUpdate) 117 | .select() 118 | .single(); 119 | 120 | expect(error).toBeNull(); 121 | expect(data).not.toBeNull(); 122 | expect(data?.id).toBe(idToUpdate); 123 | expect(data?.bigint_value).toBe(updatedBigIntValue); 124 | expect(data?.description).toBe(updatedDescription); 125 | }); 126 | 127 | test('should filter using BigInt data in WHERE clause', async () => { 128 | const { data, error } = await client 129 | .from('bigint_test_table') 130 | .select('id, description') 131 | .eq('bigint_value', 1234567890123456789n) // Filter by BigInt 132 | .single(); 133 | 134 | expect(error).toBeNull(); 135 | expect(data).not.toBeNull(); 136 | expect(data?.id).toBe(1); 137 | expect(data?.description).toBe('First BigInt'); 138 | }); 139 | 140 | test('should handle NULL BigInt values correctly on INSERT and SELECT', async () => { 141 | const newId = 5; 142 | const { error: insertError } = await client 143 | .from('bigint_test_table') 144 | .insert({ id: newId, bigint_value: null, description: 'Explicit Null BigInt' }) 145 | .select() 146 | .single(); 147 | expect(insertError).toBeNull(); 148 | 149 | const { data, error: selectError } = await client 150 | .from('bigint_test_table') 151 | .select('id, bigint_value') 152 | .eq('id', newId) 153 | .single(); 154 | 155 | expect(selectError).toBeNull(); 156 | expect(data).not.toBeNull(); 157 | expect(data?.id).toBe(newId); 158 | expect(data?.bigint_value).toBeNull(); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /examples/types/database.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json | undefined } 7 | | Json[]; 8 | 9 | export interface Database { 10 | public: { 11 | Tables: { 12 | bigint_test: { 13 | Row: { 14 | id: number; 15 | small_int: bigint; 16 | large_int: bigint; 17 | created_at: string; 18 | }; 19 | Insert: { 20 | id?: number; 21 | small_int: bigint | number | string; 22 | large_int: bigint | number | string; 23 | created_at?: string; 24 | }; 25 | Update: { 26 | id?: number; 27 | small_int?: bigint | number | string; 28 | large_int?: bigint | number | string; 29 | created_at?: string; 30 | }; 31 | Relationships: []; 32 | }; 33 | users: { 34 | Row: { 35 | id: number; 36 | name: string; 37 | email: string; 38 | status: string; 39 | last_login: string | null; 40 | created_at: string; 41 | }; 42 | Insert: { 43 | name: string; 44 | email: string; 45 | status?: string; 46 | last_login?: string | null; 47 | }; 48 | Update: { 49 | name?: string; 50 | email?: string; 51 | status?: string; 52 | last_login?: string | null; 53 | }; 54 | Relationships: []; 55 | }; 56 | profiles: { 57 | Row: { 58 | id: number; 59 | user_id: number; 60 | bio: string | null; 61 | avatar_url: string | null; 62 | interests: string[] | null; 63 | updated_at: string | null; 64 | }; 65 | Insert: { 66 | user_id: number; 67 | bio?: string | null; 68 | avatar_url?: string | null; 69 | interests?: string[] | null; 70 | updated_at?: string | null; 71 | }; 72 | Update: { 73 | user_id?: number; 74 | bio?: string | null; 75 | avatar_url?: string | null; 76 | interests?: string[] | null; 77 | updated_at?: string | null; 78 | }; 79 | Relationships: []; 80 | }; 81 | posts: { 82 | Row: { 83 | id: number; 84 | user_id: number; 85 | title: string; 86 | content: string | null; 87 | tags: string[] | null; 88 | views: number; 89 | created_at: string; 90 | updated_at: string | null; 91 | }; 92 | Insert: { 93 | user_id: number; 94 | title: string; 95 | content?: string | null; 96 | tags?: string[] | null; 97 | updated_at?: string | null; 98 | }; 99 | Update: { 100 | user_id?: number; 101 | title?: string; 102 | content?: string | null; 103 | tags?: string[] | null; 104 | updated_at?: string | null; 105 | views?: number; 106 | }; 107 | Relationships: []; 108 | }; 109 | comments: { 110 | Row: { 111 | id: number; 112 | post_id: number; 113 | user_id: number; 114 | content: string; 115 | created_at: string; 116 | }; 117 | Insert: { 118 | post_id: number; 119 | user_id: number; 120 | content: string; 121 | }; 122 | Update: { 123 | post_id?: number; 124 | user_id?: number; 125 | content?: string; 126 | }; 127 | Relationships: []; 128 | }; 129 | authors: { 130 | Row: { 131 | id: number; 132 | name: string; 133 | }; 134 | Insert: { 135 | id?: number; 136 | name: string; 137 | }; 138 | Update: { 139 | id?: number; 140 | name?: string; 141 | }; 142 | Relationships: []; 143 | }; 144 | books: { 145 | Row: { 146 | id: number; 147 | title: string; 148 | author_id: number; 149 | }; 150 | Insert: { 151 | id?: number; 152 | title: string; 153 | author_id: number; 154 | }; 155 | Update: { 156 | id?: number; 157 | title?: string; 158 | author_id?: number; 159 | }; 160 | Relationships: [ 161 | { 162 | foreignKeyName: "books_author_id_fkey"; 163 | columns: ["author_id"]; 164 | referencedRelation: "authors"; 165 | referencedColumns: ["id"]; 166 | } 167 | ]; 168 | }; 169 | }; 170 | Views: { 171 | user_posts_view: { 172 | Row: { 173 | user_id: number; 174 | user_name: string; 175 | post_id: number; 176 | post_title: string; 177 | post_content: string | null; 178 | post_created_at: string; 179 | }; 180 | }; 181 | active_users_view: { 182 | Row: { 183 | id: number; 184 | name: string; 185 | email: string; 186 | last_login: string | null; 187 | post_count: number; 188 | }; 189 | }; 190 | }; 191 | Functions: { 192 | [_ in never]: never; 193 | }; 194 | Enums: { 195 | [_ in never]: never; 196 | }; 197 | CompositeTypes: { 198 | [_ in never]: never; 199 | }; 200 | }; 201 | [schema: string]: { 202 | Tables: { 203 | [key: string]: { 204 | Row: any; 205 | Insert: any; 206 | Update: any; 207 | Relationships: unknown[]; 208 | }; 209 | }; 210 | Views: { 211 | [_ in never]: never; 212 | }; 213 | Functions: { 214 | [_ in never]: never; 215 | }; 216 | Enums: { 217 | [_ in never]: never; 218 | }; 219 | CompositeTypes: { 220 | [_ in never]: never; 221 | }; 222 | }; 223 | } 224 | -------------------------------------------------------------------------------- /src/__tests__/query-builder-join.test.ts: -------------------------------------------------------------------------------- 1 | import { SupaLitePG } from '../postgres-client'; 2 | import { Pool } from 'pg'; 3 | import { config } from 'dotenv'; 4 | import { DatabaseSchema, TableBase } from '../types'; 5 | 6 | config(); 7 | 8 | // Define types for the test tables 9 | type AuthorsTableRow = { id: number; name: string }; 10 | type AuthorsTableInsert = { id?: number; name: string }; 11 | type AuthorsTableUpdate = { id?: number; name?: string }; 12 | 13 | type BooksTableRow = { id: number; title: string; author_id: number }; 14 | type BooksTableInsert = { id?: number; title: string; author_id: number }; 15 | type BooksTableUpdate = { id?: number; title?: string; author_id?: number }; 16 | 17 | // Define the test-specific database schema 18 | interface TestDatabaseWithJoin extends DatabaseSchema { 19 | public: { 20 | Tables: { 21 | authors: TableBase & { Row: AuthorsTableRow; Insert: AuthorsTableInsert; Update: AuthorsTableUpdate; Relationships: [] }; 22 | books: TableBase & { 23 | Row: BooksTableRow; 24 | Insert: BooksTableInsert; 25 | Update: BooksTableUpdate; 26 | Relationships: [ 27 | { 28 | foreignKeyName: "books_author_id_fkey"; 29 | columns: ["author_id"]; 30 | referencedRelation: "authors"; 31 | referencedColumns: ["id"]; 32 | } 33 | ]; 34 | }; 35 | }; 36 | Views: Record; 37 | Functions: Record; 38 | Enums: Record; 39 | CompositeTypes: Record; 40 | }; 41 | } 42 | 43 | const connectionString = process.env.DB_CONNECTION || 'postgresql://testuser:testpassword@localhost:5432/testdb'; 44 | 45 | describe('QueryBuilder with Join Queries', () => { 46 | let client: SupaLitePG; 47 | let pool: Pool; 48 | 49 | beforeAll(async () => { 50 | pool = new Pool({ connectionString }); 51 | await pool.query(` 52 | CREATE TABLE IF NOT EXISTS authors ( 53 | id SERIAL PRIMARY KEY, 54 | name TEXT NOT NULL 55 | ); 56 | `); 57 | await pool.query(` 58 | CREATE TABLE IF NOT EXISTS books ( 59 | id SERIAL PRIMARY KEY, 60 | title TEXT NOT NULL, 61 | author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE 62 | ); 63 | `); 64 | }); 65 | 66 | beforeEach(async () => { 67 | client = new SupaLitePG({ connectionString, verbose: true }); 68 | // Clear and re-populate data for each test 69 | await pool.query('TRUNCATE TABLE books, authors RESTART IDENTITY CASCADE;'); 70 | await pool.query(` 71 | INSERT INTO authors (name) VALUES ('George Orwell'), ('Jane Austen'); 72 | INSERT INTO books (title, author_id) VALUES 73 | ('1984', 1), 74 | ('Animal Farm', 1), 75 | ('Pride and Prejudice', 2); 76 | `); 77 | }); 78 | 79 | afterEach(async () => { 80 | if (client) { 81 | await client.close(); 82 | } 83 | }); 84 | 85 | afterAll(async () => { 86 | await pool.query(`DROP TABLE IF EXISTS books;`); 87 | await pool.query(`DROP TABLE IF EXISTS authors;`); 88 | await pool.end(); 89 | }); 90 | 91 | test('should fetch main records and nested foreign records', async () => { 92 | type AuthorWithBooks = AuthorsTableRow & { 93 | books: BooksTableRow[]; 94 | }; 95 | 96 | const { data, error } = await client 97 | .from('authors') 98 | .select('*, books(*)'); 99 | 100 | expect(error).toBeNull(); 101 | expect(data).not.toBeNull(); 102 | 103 | const typedData = data as AuthorWithBooks[]; 104 | expect(typedData).toHaveLength(2); 105 | 106 | const orwell = typedData.find(a => a.name === 'George Orwell'); 107 | const austen = typedData.find(a => a.name === 'Jane Austen'); 108 | 109 | expect(orwell).toBeDefined(); 110 | expect(orwell?.books).toHaveLength(2); 111 | expect(orwell?.books.map(b => b.title)).toEqual(expect.arrayContaining(['1984', 'Animal Farm'])); 112 | 113 | expect(austen).toBeDefined(); 114 | expect(austen?.books).toHaveLength(1); 115 | expect(austen?.books[0].title).toBe('Pride and Prejudice'); 116 | }); 117 | 118 | test('should fetch specific columns from main and nested records', async () => { 119 | type AuthorWithBookTitles = { name: string } & { 120 | books: { title: string }[]; 121 | }; 122 | 123 | const { data, error } = await client 124 | .from('authors') 125 | .select('name, books(title)'); 126 | 127 | expect(error).toBeNull(); 128 | expect(data).not.toBeNull(); 129 | 130 | const typedData = data as AuthorWithBookTitles[]; 131 | expect(typedData).toHaveLength(2); 132 | 133 | const orwell = typedData.find(a => a.name === 'George Orwell'); 134 | expect(orwell).toBeDefined(); 135 | expect(orwell?.books).toHaveLength(2); 136 | expect(orwell?.books[0]).toHaveProperty('title'); 137 | expect(orwell?.books[0]).not.toHaveProperty('id'); 138 | }); 139 | 140 | test('should fetch main records and nested referenced record (many-to-one)', async () => { 141 | type BookWithAuthor = BooksTableRow & { 142 | authors: AuthorsTableRow | null; 143 | }; 144 | 145 | const { data, error } = await client 146 | .from('books') 147 | .select('*, authors(*)') 148 | .order('id'); 149 | 150 | expect(error).toBeNull(); 151 | expect(data).not.toBeNull(); 152 | 153 | const typedData = data as BookWithAuthor[]; 154 | expect(typedData).toHaveLength(3); 155 | 156 | const first = typedData[0]; 157 | expect(first.title).toBe('1984'); 158 | expect(first.authors).toBeDefined(); 159 | expect(Array.isArray(first.authors)).toBe(false); 160 | expect(first.authors?.name).toBe('George Orwell'); 161 | 162 | const third = typedData[2]; 163 | expect(third.title).toBe('Pride and Prejudice'); 164 | expect(third.authors?.name).toBe('Jane Austen'); 165 | }); 166 | 167 | test('should fetch specific columns from nested referenced record (many-to-one)', async () => { 168 | type BookWithAuthorName = { title: string } & { 169 | authors: { name: string } | null; 170 | }; 171 | 172 | const { data, error } = await client 173 | .from('books') 174 | .select('title, authors(name)') 175 | .order('id'); 176 | 177 | expect(error).toBeNull(); 178 | expect(data).not.toBeNull(); 179 | 180 | const typedData = data as BookWithAuthorName[]; 181 | expect(typedData).toHaveLength(3); 182 | expect(typedData[0].authors?.name).toBe('George Orwell'); 183 | expect(typedData[0].authors).not.toHaveProperty('id'); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /examples/tests/query-result.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteClient } from '../../src'; 2 | import { PostgresError } from '../../src/errors'; 3 | import { QueryResult } from '../../src/types'; 4 | 5 | // Node.js 타입 정의 6 | declare const process: { 7 | env: { 8 | [key: string]: string | undefined; 9 | }; 10 | }; 11 | 12 | // 데이터베이스 스키마 타입 정의 13 | type Database = { 14 | public: { 15 | Tables: { 16 | users: { 17 | Row: { 18 | id: number; 19 | name: string; 20 | email: string; 21 | created_at: string; 22 | }; 23 | Insert: { 24 | id?: number; 25 | name: string; 26 | email: string; 27 | created_at?: string; 28 | }; 29 | Update: { 30 | id?: number; 31 | name?: string; 32 | email?: string; 33 | created_at?: string; 34 | }; 35 | Relationships: []; 36 | }; 37 | }; 38 | }; 39 | }; 40 | 41 | // Supalite 클라이언트 인스턴스 생성 42 | const supalite = new SupaliteClient({ 43 | connectionString: process.env.DB_CONNECTION || 'postgresql://postgres:postgres@localhost:5432/testdb', 44 | }); 45 | 46 | /** 47 | * 예제 1: 데이터 조회 및 배열 메서드 사용 48 | * 49 | * 이 예제는 users 테이블에서 데이터를 조회하고, 50 | * 결과가 항상 배열로 반환되므로 배열 메서드를 안전하게 사용할 수 있음을 보여줍니다. 51 | */ 52 | async function example1() { 53 | try { 54 | // 데이터 조회 55 | const result = await supalite 56 | .from('users') 57 | .select('*'); 58 | 59 | // 타입 가드: result.data가 배열인지 확인 60 | if (Array.isArray(result.data)) { 61 | // 결과가 항상 배열이므로 length 속성 사용 가능 62 | console.log(`조회된 사용자 수: ${result.data.length}`); 63 | 64 | // 배열 메서드 사용 (map) 65 | const userNames = result.data.map(user => user.name); 66 | console.log('사용자 이름 목록:', userNames); 67 | 68 | // 배열 메서드 사용 (filter) 69 | const filteredUsers = result.data.filter(user => 70 | user.email.endsWith('@example.com') 71 | ); 72 | console.log('example.com 이메일을 사용하는 사용자:', filteredUsers); 73 | 74 | // 배열 메서드 사용 (forEach) 75 | console.log('모든 사용자 정보:'); 76 | result.data.forEach(user => { 77 | console.log(`ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}`); 78 | }); 79 | } 80 | 81 | return result; 82 | } catch (error) { 83 | console.error('에러 발생:', error); 84 | throw error; 85 | } 86 | } 87 | 88 | /** 89 | * 예제 2: 결과가 없을 때의 처리 90 | * 91 | * 이 예제는 조건에 맞는 데이터가 없을 때도 92 | * 빈 배열이 반환되므로 안전하게 배열 메서드를 사용할 수 있음을 보여줍니다. 93 | */ 94 | async function example2() { 95 | try { 96 | // 존재하지 않는 조건으로 데이터 조회 97 | const result = await supalite 98 | .from('users') 99 | .select('*') 100 | .eq('id', -1); // 존재하지 않는 ID 101 | 102 | // 타입 가드: result.data가 배열인지 확인 103 | if (Array.isArray(result.data)) { 104 | // 결과가 없어도 빈 배열이 반환되므로 length 속성 사용 가능 105 | console.log(`조회된 사용자 수: ${result.data.length}`); // 0 출력 106 | 107 | // 빈 배열에도 배열 메서드 사용 가능 108 | const userNames = result.data.map(user => user.name); 109 | console.log('사용자 이름 목록:', userNames); // [] 출력 110 | 111 | // 조건부 처리 112 | if (result.data.length === 0) { 113 | console.log('조건에 맞는 사용자가 없습니다.'); 114 | } else { 115 | console.log('조건에 맞는 사용자가 있습니다.'); 116 | } 117 | } 118 | 119 | return result; 120 | } catch (error) { 121 | console.error('에러 발생:', error); 122 | throw error; 123 | } 124 | } 125 | 126 | /** 127 | * 예제 3: 에러 처리 128 | * 129 | * 이 예제는 에러가 발생했을 때도 130 | * data 필드가 빈 배열을 반환하므로 안전하게 사용할 수 있음을 보여줍니다. 131 | */ 132 | async function example3() { 133 | try { 134 | // 잘못된 테이블 이름으로 데이터 조회 시도 (타입 캐스팅으로 컴파일 에러 회피) 135 | const result = await (supalite 136 | .from('non_existent_table' as any) 137 | .select('*')); 138 | 139 | // 이 코드는 실행되지 않음 (에러가 발생하여 catch 블록으로 이동) 140 | // 타입 가드: result.data가 배열인지 확인 141 | if (Array.isArray(result.data)) { 142 | console.log(`조회된 데이터 수: ${result.data.length}`); 143 | } 144 | return result; 145 | } catch (error) { 146 | // 에러 객체 생성 (실제로는 supalite에서 반환한 에러 객체를 사용) 147 | const errorResult = { 148 | data: [], // 에러 발생 시에도 빈 배열 반환 149 | error: new PostgresError('테이블이 존재하지 않습니다.'), 150 | count: null, 151 | status: 500, 152 | statusText: 'Internal Server Error', 153 | }; 154 | 155 | // 에러가 발생해도 data 필드는 빈 배열이므로 안전하게 사용 가능 156 | console.log(`에러 발생! 데이터 수: ${errorResult.data.length}`); // 0 출력 157 | console.log(`에러 메시지: ${errorResult.error?.message}`); 158 | 159 | return errorResult; 160 | } 161 | } 162 | 163 | /** 164 | * 예제 4: Supabase 호환성 예제 165 | * 166 | * 이 예제는 Supabase 코드를 Supalite로 마이그레이션할 때 167 | * 타입 호환성 문제 없이 사용할 수 있음을 보여줍니다. 168 | */ 169 | async function example4() { 170 | try { 171 | // 데이터 조회 172 | const result = await supalite 173 | .from('users') 174 | .select('*'); 175 | 176 | // 타입 가드: result.data가 배열인지 확인 177 | if (Array.isArray(result.data)) { 178 | // Supabase 코드와 동일하게 length 속성 사용 179 | const userCount = result.data.length; 180 | console.log(`사용자 수: ${userCount}`); 181 | 182 | // Supabase 코드와 동일하게 배열 메서드 사용 183 | const processedUsers = result.data.map(user => ({ 184 | id: user.id, 185 | displayName: user.name, 186 | emailAddress: user.email, 187 | })); 188 | 189 | // 배열 파라미터로 전달 190 | const processUsers = (users: any[]) => { 191 | return users.map(user => `${user.name} (${user.email})`); 192 | }; 193 | 194 | // result.data를 배열 파라미터로 전달 (Supabase 호환) 195 | const userList = processUsers(result.data); 196 | console.log('사용자 목록:', userList); 197 | 198 | return { 199 | userCount, 200 | processedUsers, 201 | userList, 202 | }; 203 | } 204 | 205 | return { userCount: 0, processedUsers: [], userList: [] }; 206 | } catch (error) { 207 | console.error('에러 발생:', error); 208 | throw error; 209 | } 210 | } 211 | 212 | // 예제 실행 함수 213 | async function runExamples() { 214 | console.log('===== 예제 1: 데이터 조회 및 배열 메서드 사용 ====='); 215 | await example1().catch(console.error); 216 | 217 | console.log('\n===== 예제 2: 결과가 없을 때의 처리 ====='); 218 | await example2().catch(console.error); 219 | 220 | console.log('\n===== 예제 3: 에러 처리 ====='); 221 | await example3().catch(console.error); 222 | 223 | console.log('\n===== 예제 4: Supabase 호환성 예제 ====='); 224 | await example4().catch(console.error); 225 | } 226 | 227 | // 예제 실행 228 | console.log('===== 실제 데이터베이스 연결을 통한 예제 실행 ====='); 229 | runExamples().catch(console.error); 230 | 231 | export { 232 | example1, 233 | example2, 234 | example3, 235 | example4, 236 | runExamples, 237 | }; 238 | -------------------------------------------------------------------------------- /examples/tests/query-result-simple.ts: -------------------------------------------------------------------------------- 1 | import { SupaliteClient } from '../../src'; 2 | import { PostgresError } from '../../src/errors'; 3 | import { QueryResult } from '../../src/types'; 4 | 5 | // Node.js 타입 정의 6 | declare const process: { 7 | env: { 8 | [key: string]: string | undefined; 9 | }; 10 | }; 11 | 12 | // 데이터베이스 스키마 타입 정의 13 | type Database = { 14 | public: { 15 | Tables: { 16 | users: { 17 | Row: { 18 | id: number; 19 | name: string; 20 | email: string; 21 | status?: string; 22 | created_at: string; 23 | }; 24 | Insert: { 25 | id?: number; 26 | name: string; 27 | email: string; 28 | created_at?: string; 29 | }; 30 | Update: { 31 | id?: number; 32 | name?: string; 33 | email?: string; 34 | created_at?: string; 35 | }; 36 | Relationships: []; 37 | }; 38 | }; 39 | }; 40 | }; 41 | 42 | // 쿼리 결과 타입 정의 43 | type UserRow = Database['public']['Tables']['users']['Row']; 44 | 45 | // Supalite 클라이언트 인스턴스 생성 46 | const supalite = new SupaliteClient({ 47 | connectionString: process.env.DB_CONNECTION || 'postgresql://postgres:postgres@localhost:5432/testdb', 48 | }); 49 | 50 | /** 51 | * 예제 1: Supabase 스타일로 데이터 조회 및 배열 메서드 사용 52 | * 53 | * 이 예제는 Supabase 클라이언트 코드와 동일한 방식으로 54 | * result.data를 안전하게 사용하는 방법을 보여줍니다. 55 | */ 56 | async function example1() { 57 | try { 58 | // 데이터 조회 59 | const result = await supalite 60 | .from('users') 61 | .select('*'); 62 | 63 | // 구조 분해 할당으로 data와 error 추출 64 | const { data, error } = result; 65 | 66 | // 에러 확인 67 | if (error) { 68 | throw new Error(`데이터 조회 중 에러 발생: ${error.message}`); 69 | } 70 | 71 | // data가 존재하고 요소가 있는지 확인 72 | if (data && data.length > 0) { 73 | console.log(`조회된 사용자 수: ${data.length}`); 74 | 75 | // 배열 메서드 사용 (map) 76 | const userNames = data.map(user => user.name); 77 | console.log('사용자 이름 목록:', userNames); 78 | 79 | // 배열 메서드 사용 (filter) 80 | const filteredUsers = data.filter(user => 81 | user.email.endsWith('@example.com') 82 | ); 83 | console.log('example.com 이메일을 사용하는 사용자:', filteredUsers); 84 | 85 | // 배열 메서드 사용 (forEach) 86 | console.log('모든 사용자 정보:'); 87 | data.forEach(user => { 88 | console.log(`ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}`); 89 | }); 90 | 91 | // 첫 번째 사용자 정보 가져오기 (Supabase 스타일) 92 | const firstUser = data[0]; 93 | console.log('첫 번째 사용자:', firstUser); 94 | } else { 95 | console.log('조회된 사용자가 없습니다.'); 96 | } 97 | 98 | return { data, error }; 99 | } catch (error) { 100 | console.error('에러 발생:', error); 101 | throw error; 102 | } 103 | } 104 | 105 | /** 106 | * 예제 2: 결과가 없을 때의 처리 (Supabase 스타일) 107 | * 108 | * 이 예제는 Supabase 스타일로 결과가 없을 때의 처리 방법을 보여줍니다. 109 | */ 110 | async function example2() { 111 | try { 112 | // 존재하지 않는 조건으로 데이터 조회 113 | const { data, error } = await supalite 114 | .from('users') 115 | .select('*') 116 | .eq('id', -1); // 존재하지 않는 ID 117 | 118 | // 에러 확인 119 | if (error) { 120 | throw new Error(`데이터 조회 중 에러 발생: ${error.message}`); 121 | } 122 | 123 | // data가 존재하는지 확인 124 | if (data) { 125 | console.log('데이터가 있거나 빈 배열입니다'); 126 | 127 | // 빈 배열 확인을 위해서는 length 속성 사용 128 | if (data.length === 0) { 129 | console.log('조회된 사용자가 없습니다'); 130 | } else { 131 | console.log(`조회된 사용자 수: ${data.length}`); 132 | } 133 | 134 | // 빈 배열에도 배열 메서드 사용 가능 135 | const userNames = data.map(user => user.name); 136 | console.log('사용자 이름 목록:', userNames); // [] 출력 137 | } else { 138 | console.log('데이터가 없습니다 (이 메시지는 출력되지 않음)'); 139 | } 140 | 141 | return { data, error }; 142 | } catch (error) { 143 | console.error('에러 발생:', error); 144 | throw error; 145 | } 146 | } 147 | 148 | /** 149 | * 예제 3: 에러 처리 (Supabase 스타일) 150 | * 151 | * 이 예제는 Supabase 스타일로 에러를 처리하는 방법을 보여줍니다. 152 | */ 153 | async function example3() { 154 | try { 155 | // 잘못된 테이블 이름으로 데이터 조회 시도 156 | const { data, error } = await supalite 157 | .from('non_existent_table' as any) 158 | .select('*'); 159 | 160 | // 에러 확인 (Supabase 스타일) 161 | if (error) { 162 | console.log(`에러 발생! 메시지: ${error.message}`); 163 | return { data: [], error }; // 에러 발생 시에도 data는 빈 배열 164 | } 165 | 166 | // 이 코드는 에러가 없을 때만 실행됨 167 | // data가 존재하고 요소가 있는지 확인 168 | if (data && data.length > 0) { 169 | console.log(`조회된 데이터 수: ${data.length}`); 170 | data.forEach(item => { 171 | console.log(item); 172 | }); 173 | } else { 174 | console.log('조회된 데이터가 없습니다'); 175 | } 176 | 177 | return { data, error }; 178 | } catch (error) { 179 | console.error('예상치 못한 에러 발생:', error); 180 | 181 | // 에러 객체 생성 (Supabase 스타일) 182 | const errorResult = { 183 | data: [], // 에러 발생 시에도 빈 배열 반환 184 | error: new PostgresError('테이블이 존재하지 않습니다.'), 185 | count: null, 186 | status: 500, 187 | statusText: 'Internal Server Error', 188 | }; 189 | 190 | return errorResult; 191 | } 192 | } 193 | 194 | /** 195 | * 예제 4: 데이터 존재 여부 확인 (Supabase 스타일) 196 | * 197 | * 이 예제는 Supabase 스타일로 데이터 존재 여부를 확인하는 방법을 보여줍니다. 198 | */ 199 | async function example4() { 200 | try { 201 | // 데이터 조회 202 | const { data, error } = await supalite 203 | .from('users') 204 | .select('*') 205 | .eq('status', 'active'); 206 | 207 | // 에러 확인 208 | if (error) { 209 | throw new Error(`데이터 조회 중 에러 발생: ${error.message}`); 210 | } 211 | 212 | // 데이터 존재 여부 확인 (Supabase 스타일) 213 | // data가 존재하고 요소가 있는지 확인 214 | if (data && data.length > 0) { 215 | console.log(`활성 사용자 수: ${data.length}`); 216 | 217 | // 첫 번째 사용자 정보 출력 (Supabase 스타일) 218 | const firstUser = data[0]; 219 | console.log('첫 번째 활성 사용자:', firstUser); 220 | 221 | // 특정 사용자 찾기 (Supabase 스타일) 222 | const specificUser = data.find(user => user.name === '홍길동'); 223 | if (specificUser) { 224 | console.log('홍길동 사용자 정보:', specificUser); 225 | } 226 | } else { 227 | console.log('활성 사용자가 없습니다'); 228 | } 229 | 230 | return { data, error }; 231 | } catch (error) { 232 | console.error('예상치 못한 에러 발생:', error); 233 | throw error; 234 | } 235 | } 236 | 237 | // 예제 실행 함수 238 | async function runExamples() { 239 | console.log('===== 예제 1: 타입 가드 없이 데이터 조회 및 배열 메서드 사용 ====='); 240 | await example1().catch(console.error); 241 | 242 | console.log('\n===== 예제 2: if (!data) 방식의 조건 처리 ====='); 243 | await example2().catch(console.error); 244 | 245 | console.log('\n===== 예제 3: 에러 처리 (if (!data) 방식) ====='); 246 | await example3().catch(console.error); 247 | 248 | console.log('\n===== 예제 4: 데이터 존재 여부 확인 (Supabase 스타일) ====='); 249 | await example4().catch(console.error); 250 | } 251 | 252 | // 예제 실행 253 | console.log('===== 타입 가드 없이 안전하게 사용하는 예제 ====='); 254 | runExamples().catch(console.error); 255 | 256 | export { 257 | example1, 258 | example2, 259 | example3, 260 | example4, 261 | runExamples, 262 | }; 263 | --------------------------------------------------------------------------------