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 +16 -2
- package/dist/postgres-client.d.ts +3 -0
- package/dist/postgres-client.js +55 -1
- package/dist/query-builder.d.ts +5 -1
- package/dist/query-builder.js +48 -18
- package/dist/types.d.ts +1 -0
- package/docs/changelog/2025-06-10-jsonb-array-test.md +18 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SupaLite
|
|
2
2
|
|
|
3
|
-
[](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:
|
|
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;
|
package/dist/postgres-client.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/query-builder.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>>>;
|
package/dist/query-builder.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
235
|
-
})
|
|
236
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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
|
@@ -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,
|
|
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
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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.
|