supalite 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SupaLite
2
2
 
3
- [![npm version](https://img.shields.io/badge/version-0.3.2-blue.svg)](https://www.npmjs.com/package/supalite)
3
+ [![npm version](https://img.shields.io/badge/version-0.3.3-blue.svg)](https://www.npmjs.com/package/supalite)
4
4
 
5
5
  가볍고 효율적인 PostgreSQL 클라이언트 라이브러리입니다. Supabase와 동일한 API를 제공하면서도 더 가볍고 빠른 구현을 제공합니다.
6
6
 
@@ -231,14 +231,28 @@ const { data, error } = await client
231
231
  ]);
232
232
 
233
233
  // JSONB 배열 데이터 처리
234
+ // 중요: JSON/JSONB 컬럼에 배열을 삽입/업데이트할 경우, 사용자가 직접 JSON.stringify()를 사용해야 합니다.
235
+ // SupaLite는 일반 객체에 대해서만 자동 stringify를 수행합니다.
236
+ const myJsonArray = ['tag1', 2025, { active: true }];
234
237
  const { data: jsonData, error: jsonError } = await client
235
238
  .from('your_jsonb_table') // 'your_jsonb_table'을 실제 테이블명으로 변경
236
239
  .insert({
237
- metadata_array: ['tag1', 2025, { active: true }]
240
+ metadata_array: JSON.stringify(myJsonArray) // 배열은 직접 stringify
238
241
  })
239
242
  .select('metadata_array')
240
243
  .single();
241
244
 
245
+ // 네이티브 배열(TEXT[], INTEGER[] 등) 데이터 처리
246
+ // 이 경우, JavaScript 배열을 직접 전달하면 pg 드라이버가 올바르게 처리합니다.
247
+ const { data: nativeArrayData, error: nativeArrayError } = await client
248
+ .from('your_native_array_table') // 실제 테이블명으로 변경
249
+ .insert({
250
+ tags_column: ['tech', 'event'], // TEXT[] 컬럼 예시
251
+ scores_column: [100, 95, 88] // INTEGER[] 컬럼 예시
252
+ })
253
+ .select('tags_column, scores_column')
254
+ .single();
255
+
242
256
  // 다른 스키마 사용
243
257
  const { data, error } = await client
244
258
  .from('users', 'other_schema')
@@ -26,6 +26,8 @@ export declare class SupaLitePG<T extends {
26
26
  private client;
27
27
  private isTransaction;
28
28
  private schema;
29
+ private schemaCache;
30
+ verbose: boolean;
29
31
  constructor(config?: SupaliteConfig);
30
32
  begin(): Promise<void>;
31
33
  commit(): Promise<void>;
@@ -37,6 +39,7 @@ export declare class SupaLitePG<T extends {
37
39
  from<S extends keyof T, K extends TableOrViewName<T, S>>(table: K, schema: S): QueryBuilder<T, S, K> & Promise<QueryResult<Row<T, S, K>>> & {
38
40
  single(): Promise<SingleQueryResult<Row<T, S, K>>>;
39
41
  };
42
+ getColumnPgType(dbSchema: string, tableName: string, columnName: string): Promise<string | undefined>;
40
43
  rpc(procedureName: string, params?: Record<string, any>): Promise<{
41
44
  data: any;
42
45
  error: PostgresError | null;
@@ -15,6 +15,9 @@ class SupaLitePG {
15
15
  constructor(config) {
16
16
  this.client = null;
17
17
  this.isTransaction = false;
18
+ this.schemaCache = new Map(); // schemaName.tableName -> Map<columnName, pgDataType>
19
+ this.verbose = false;
20
+ this.verbose = config?.verbose || process.env.SUPALITE_VERBOSE === 'true' || false;
18
21
  // connectionString이 제공되면 이를 우선 사용
19
22
  if (config?.connectionString || process.env.DB_CONNECTION) {
20
23
  try {
@@ -105,7 +108,58 @@ class SupaLitePG {
105
108
  }
106
109
  }
107
110
  from(table, schema) {
108
- return new query_builder_1.QueryBuilder(this.pool, table, schema || 'public');
111
+ // QueryBuilder constructor will be updated to accept these arguments
112
+ return new query_builder_1.QueryBuilder(// Use 'as any' temporarily if QueryBuilder constructor not yet updated
113
+ this.pool, this, // Pass the SupaLitePG instance itself
114
+ table, schema || 'public', this.verbose // Pass verbose setting
115
+ );
116
+ }
117
+ async getColumnPgType(dbSchema, tableName, columnName) {
118
+ const tableKey = `${dbSchema}.${tableName}`;
119
+ if (this.verbose)
120
+ console.log(`[SupaLite VERBOSE] getColumnPgType called for ${tableKey}.${columnName}`);
121
+ let tableInfo = this.schemaCache.get(tableKey);
122
+ if (!tableInfo) {
123
+ if (this.verbose)
124
+ console.log(`[SupaLite VERBOSE] Cache miss for table ${tableKey}. Querying information_schema.`);
125
+ try {
126
+ const query = `
127
+ SELECT column_name, data_type
128
+ FROM information_schema.columns
129
+ WHERE table_schema = $1 AND table_name = $2;
130
+ `;
131
+ // Use a temporary client from the pool for this schema query
132
+ // if not in a transaction, or use the transaction client if in one.
133
+ const activeClient = this.isTransaction && this.client ? this.client : await this.pool.connect();
134
+ try {
135
+ const result = await activeClient.query(query, [dbSchema, tableName]);
136
+ tableInfo = new Map();
137
+ result.rows.forEach((row) => {
138
+ tableInfo.set(row.column_name, row.data_type.toLowerCase());
139
+ });
140
+ this.schemaCache.set(tableKey, tableInfo);
141
+ if (this.verbose)
142
+ console.log(`[SupaLite VERBOSE] Cached schema for ${tableKey}:`, tableInfo);
143
+ }
144
+ finally {
145
+ if (!(this.isTransaction && this.client)) { // Only release if it's a temp client not managed by transaction
146
+ activeClient.release(); // Cast to any if 'release' is not on type PoolClient from transaction
147
+ }
148
+ }
149
+ }
150
+ catch (err) {
151
+ console.error(`[SupaLite ERROR] Failed to query information_schema for ${tableKey}:`, err.message);
152
+ return undefined;
153
+ }
154
+ }
155
+ else {
156
+ if (this.verbose)
157
+ console.log(`[SupaLite VERBOSE] Cache hit for table ${tableKey}.`);
158
+ }
159
+ const pgType = tableInfo?.get(columnName);
160
+ if (this.verbose)
161
+ console.log(`[SupaLite VERBOSE] pgType for ${tableKey}.${columnName}: ${pgType}`);
162
+ return pgType;
109
163
  }
110
164
  async rpc(procedureName, params = {}) {
111
165
  try {
@@ -1,4 +1,5 @@
1
1
  import { Pool } from 'pg';
2
+ import type { SupaLitePG } from './postgres-client';
2
3
  import { TableName, TableOrViewName, QueryResult, SingleQueryResult, DatabaseSchema, SchemaName, Row, InsertRow, UpdateRow } from './types';
3
4
  export declare class QueryBuilder<T extends DatabaseSchema, S extends SchemaName<T> = 'public', K extends TableOrViewName<T, S> = TableOrViewName<T, S>> implements Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>> {
4
5
  private pool;
@@ -19,7 +20,10 @@ export declare class QueryBuilder<T extends DatabaseSchema, S extends SchemaName
19
20
  private insertData?;
20
21
  private updateData?;
21
22
  private conflictTarget?;
22
- constructor(pool: Pool, table: K, schema?: S);
23
+ private client;
24
+ private verbose;
25
+ constructor(pool: Pool, client: SupaLitePG<T>, // Accept SupaLitePG instance
26
+ table: K, schema?: S, verbose?: boolean);
23
27
  then<TResult1 = QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>, TResult2 = never>(onfulfilled?: ((value: QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
24
28
  catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null): Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>> | TResult>;
25
29
  finally(onfinally?: (() => void) | null): Promise<QueryResult<Row<T, S, K>> | SingleQueryResult<Row<T, S, K>>>;
@@ -4,7 +4,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.QueryBuilder = void 0;
5
5
  const errors_1 = require("./errors");
6
6
  class QueryBuilder {
7
- constructor(pool, table, schema = 'public') {
7
+ constructor(pool, client, // Accept SupaLitePG instance
8
+ table, schema = 'public', verbose = false // Accept verbose setting
9
+ ) {
8
10
  this.pool = pool;
9
11
  this[_a] = 'QueryBuilder';
10
12
  this.selectColumns = null;
@@ -14,8 +16,11 @@ class QueryBuilder {
14
16
  this.whereValues = [];
15
17
  this.singleMode = null;
16
18
  this.queryType = 'SELECT';
19
+ this.verbose = false;
20
+ this.client = client;
17
21
  this.table = table;
18
22
  this.schema = schema;
23
+ this.verbose = verbose;
19
24
  }
20
25
  then(onfulfilled, onrejected) {
21
26
  return this.execute().then(onfulfilled, onrejected);
@@ -195,7 +200,7 @@ class QueryBuilder {
195
200
  }
196
201
  return ' WHERE ' + conditions.join(' AND ');
197
202
  }
198
- buildQuery() {
203
+ async buildQuery() {
199
204
  let query = '';
200
205
  let values = [];
201
206
  let insertColumns = [];
@@ -223,32 +228,45 @@ class QueryBuilder {
223
228
  if (rows.length === 0)
224
229
  throw new Error('Empty array provided for insert');
225
230
  insertColumns = Object.keys(rows[0]);
226
- // Process each row for potential JSON stringification
227
- const processedRowsValues = rows.map(row => Object.values(row).map(val => {
228
- if (typeof val === 'bigint') {
229
- return val.toString();
230
- }
231
- if (Array.isArray(val) || (val !== null && typeof val === 'object' && !(val instanceof Date))) {
232
- return JSON.stringify(val);
231
+ const processedRowsValuesPromises = rows.map(async (row) => {
232
+ const rowValues = [];
233
+ for (const colName of insertColumns) { // Ensure order of values matches order of columns
234
+ const val = row[colName];
235
+ const pgType = await this.client.getColumnPgType(String(this.schema), String(this.table), colName);
236
+ if (typeof val === 'bigint') {
237
+ rowValues.push(val.toString());
238
+ }
239
+ else if ((pgType === 'json' || pgType === 'jsonb') &&
240
+ (Array.isArray(val) || (val !== null && typeof val === 'object' && !(val instanceof Date)))) {
241
+ rowValues.push(JSON.stringify(val));
242
+ }
243
+ else {
244
+ rowValues.push(val);
245
+ }
233
246
  }
234
- return val;
235
- }));
236
- values = processedRowsValues.flat();
247
+ return rowValues;
248
+ });
249
+ const processedRowsValuesArrays = await Promise.all(processedRowsValuesPromises);
250
+ values = processedRowsValuesArrays.flat();
237
251
  const placeholders = rows.map((_, i) => `(${insertColumns.map((_, j) => `$${i * insertColumns.length + j + 1}`).join(',')})`).join(',');
238
252
  query = `INSERT INTO ${schemaTable} ("${insertColumns.join('","')}") VALUES ${placeholders}`;
239
253
  }
240
254
  else {
241
255
  const insertData = this.insertData;
242
256
  insertColumns = Object.keys(insertData);
243
- values = Object.values(insertData).map(val => {
257
+ const valuePromises = insertColumns.map(async (colName) => {
258
+ const val = insertData[colName];
259
+ const pgType = await this.client.getColumnPgType(String(this.schema), String(this.table), colName);
244
260
  if (typeof val === 'bigint') {
245
261
  return val.toString();
246
262
  }
247
- if (Array.isArray(val) || (val !== null && typeof val === 'object' && !(val instanceof Date))) {
263
+ if ((pgType === 'json' || pgType === 'jsonb') &&
264
+ (Array.isArray(val) || (val !== null && typeof val === 'object' && !(val instanceof Date)))) {
248
265
  return JSON.stringify(val);
249
266
  }
250
267
  return val;
251
268
  });
269
+ values = await Promise.all(valuePromises);
252
270
  const insertPlaceholders = values.map((_, i) => `$${i + 1}`).join(',');
253
271
  query = `INSERT INTO ${schemaTable} ("${insertColumns.join('","')}") VALUES (${insertPlaceholders})`;
254
272
  }
@@ -276,16 +294,21 @@ class QueryBuilder {
276
294
  if ('updated_at' in updateData && !updateData.updated_at) {
277
295
  updateData.updated_at = now;
278
296
  }
279
- const processedUpdateValues = Object.values(updateData).map(val => {
297
+ const updateColumns = Object.keys(updateData);
298
+ const processedUpdateValuesPromises = updateColumns.map(async (colName) => {
299
+ const val = updateData[colName];
300
+ const pgType = await this.client.getColumnPgType(String(this.schema), String(this.table), colName);
280
301
  if (typeof val === 'bigint') {
281
302
  return val.toString();
282
303
  }
283
- if (Array.isArray(val) || (val !== null && typeof val === 'object' && !(val instanceof Date))) {
304
+ if ((pgType === 'json' || pgType === 'jsonb') &&
305
+ (Array.isArray(val) || (val !== null && typeof val === 'object' && !(val instanceof Date)))) {
284
306
  return JSON.stringify(val);
285
307
  }
286
308
  return val;
287
309
  });
288
- const setColumns = Object.keys(updateData).map((key, index) => `"${String(key)}" = $${index + 1}`);
310
+ const processedUpdateValues = await Promise.all(processedUpdateValuesPromises);
311
+ const setColumns = updateColumns.map((key, index) => `"${String(key)}" = $${index + 1}`);
289
312
  query = `UPDATE ${schemaTable} SET ${setColumns.join(', ')}`;
290
313
  values = [...processedUpdateValues, ...this.whereValues];
291
314
  query += this.buildWhereClause(processedUpdateValues);
@@ -316,7 +339,11 @@ class QueryBuilder {
316
339
  }
317
340
  async execute() {
318
341
  try {
319
- const { query, values } = this.buildQuery();
342
+ const { query, values } = await this.buildQuery(); // await buildQuery
343
+ if (this.verbose) {
344
+ console.log('[SupaLite VERBOSE] SQL:', query);
345
+ console.log('[SupaLite VERBOSE] Values:', values);
346
+ }
320
347
  const result = await this.pool.query(query, values);
321
348
  if (this.queryType === 'DELETE' && !this.shouldReturnData()) {
322
349
  return {
@@ -392,6 +419,9 @@ class QueryBuilder {
392
419
  };
393
420
  }
394
421
  catch (err) {
422
+ if (this.verbose) {
423
+ console.error('[SupaLite VERBOSE] Error:', err);
424
+ }
395
425
  return {
396
426
  data: [],
397
427
  error: new errors_1.PostgresError(err.message),
package/dist/types.d.ts CHANGED
@@ -53,6 +53,7 @@ export interface SupaliteConfig {
53
53
  port?: number;
54
54
  ssl?: boolean;
55
55
  schema?: string;
56
+ verbose?: boolean;
56
57
  }
57
58
  export type QueryType = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'UPSERT';
58
59
  export interface QueryOptions {
@@ -4,9 +4,22 @@
4
4
  - Updated `JsonbTestTable` related types to use the global `Json` type.
5
5
  - Added `another_json_field` (JSONB) to `jsonb_test_table` schema and tests.
6
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, relying on internal handling.
7
+ - Removed explicit `JSON.stringify()` from test cases, as this is now handled automatically by `QueryBuilder` based on schema information.
8
8
  - Added a new test case for inserting/selecting an object into `another_json_field`.
9
- - Modified `src/query-builder.ts` (`buildQuery` method):
10
- - Implemented automatic `JSON.stringify()` for array or object values (excluding `Date` instances) when preparing data for `INSERT`, `UPSERT`, and `UPDATE` operations. This allows users to pass JavaScript objects/arrays directly for `json`/`jsonb` columns.
11
- - Corrected a TypeScript error (`Cannot find name 'updateValues'`) in the `UPDATE` case of `buildQuery`.
12
- - Ensured `src/types.ts` contains the `Json` type definition.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supalite",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A lightweight TypeScript PostgreSQL client with Supabase-style API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",