supalite 0.5.3 → 0.5.5
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/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/dist/postgres-client.d.ts +23 -7
- package/dist/postgres-client.js +130 -39
- package/dist/query-builder.js +8 -1
- package/docs/changelog/2025-11-21-support-is-in-or-method.md +33 -0
- package/docs/changelog/2025-11-26-rpc-single-support.md +29 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.5] - 2025-11-26
|
|
4
|
+
|
|
5
|
+
### ✨ Added
|
|
6
|
+
- `rpc()` 메서드 호출 시 `.single()` 및 `.maybeSingle()` 메서드 체이닝 지원을 추가했습니다. 이를 통해 RPC 결과에 대해 단일 행 제약 조건을 적용할 수 있습니다. (See [docs/changelog/2025-11-26-rpc-single-support.md](docs/changelog/2025-11-26-rpc-single-support.md))
|
|
7
|
+
|
|
3
8
|
## [0.5.2] - 2025-10-16
|
|
4
9
|
|
|
5
10
|
### 🐞 Fixed
|
package/README.md
CHANGED
|
@@ -289,7 +289,7 @@ const { data, error } = await client
|
|
|
289
289
|
- `is(column, value)`: IS 연산자
|
|
290
290
|
- `not(column, operator, value)`: Negates an operator (e.g., `not('column', 'is', null)`).
|
|
291
291
|
- `contains(column, value)`: 배열/JSON 포함 여부
|
|
292
|
-
- `or(conditions)`: OR 조건 (예: 'status.eq.active,role.eq.admin')
|
|
292
|
+
- `or(conditions)`: OR 조건 (예: 'status.eq.active,role.eq.admin', 'valid_until.is.null')
|
|
293
293
|
|
|
294
294
|
### 기타 메소드
|
|
295
295
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
1
2
|
import { QueryBuilder } from './query-builder';
|
|
2
3
|
import { PostgresError } from './errors';
|
|
3
4
|
import { TableOrViewName, SupaliteConfig, Row, QueryResult, SingleQueryResult } from './types';
|
|
@@ -19,6 +20,27 @@ type SchemaWithTables = {
|
|
|
19
20
|
Enums?: any;
|
|
20
21
|
CompositeTypes?: any;
|
|
21
22
|
};
|
|
23
|
+
export declare class RpcBuilder implements Promise<any> {
|
|
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<string, any>);
|
|
31
|
+
single(): this;
|
|
32
|
+
maybeSingle(): this;
|
|
33
|
+
then<TResult1 = any, TResult2 = never>(onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
34
|
+
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null): Promise<any | TResult>;
|
|
35
|
+
finally(onfinally?: (() => void) | null): Promise<any>;
|
|
36
|
+
execute(): Promise<{
|
|
37
|
+
data: any;
|
|
38
|
+
error: PostgresError | null;
|
|
39
|
+
count?: number | null;
|
|
40
|
+
status?: number;
|
|
41
|
+
statusText?: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
22
44
|
export declare class SupaLitePG<T extends {
|
|
23
45
|
[K: string]: SchemaWithTables;
|
|
24
46
|
}> {
|
|
@@ -46,13 +68,7 @@ export declare class SupaLitePG<T extends {
|
|
|
46
68
|
column: string;
|
|
47
69
|
foreignColumn: string;
|
|
48
70
|
} | null>;
|
|
49
|
-
rpc(procedureName: string, params?: Record<string, any>):
|
|
50
|
-
data: any;
|
|
51
|
-
error: PostgresError | null;
|
|
52
|
-
count?: number | null;
|
|
53
|
-
status?: number;
|
|
54
|
-
statusText?: string;
|
|
55
|
-
}>;
|
|
71
|
+
rpc(procedureName: string, params?: Record<string, any>): RpcBuilder;
|
|
56
72
|
testConnection(): Promise<boolean>;
|
|
57
73
|
close(): Promise<void>;
|
|
58
74
|
}
|
package/dist/postgres-client.js
CHANGED
|
@@ -1,12 +1,139 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var _a;
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.supalitePg = exports.SupaLitePG = void 0;
|
|
4
|
+
exports.supalitePg = exports.SupaLitePG = exports.RpcBuilder = void 0;
|
|
4
5
|
const pg_1 = require("pg"); // PoolConfig 추가
|
|
5
6
|
const query_builder_1 = require("./query-builder");
|
|
6
7
|
const errors_1 = require("./errors");
|
|
7
8
|
const dotenv_1 = require("dotenv");
|
|
8
9
|
// .env 파일 로드
|
|
9
10
|
(0, dotenv_1.config)();
|
|
11
|
+
class RpcBuilder {
|
|
12
|
+
constructor(pool, schema, procedureName, params = {}) {
|
|
13
|
+
this.pool = pool;
|
|
14
|
+
this.schema = schema;
|
|
15
|
+
this.procedureName = procedureName;
|
|
16
|
+
this.params = params;
|
|
17
|
+
this[_a] = 'RpcBuilder';
|
|
18
|
+
this.singleMode = null;
|
|
19
|
+
}
|
|
20
|
+
single() {
|
|
21
|
+
this.singleMode = 'strict';
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
maybeSingle() {
|
|
25
|
+
this.singleMode = 'maybe';
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
then(onfulfilled, onrejected) {
|
|
29
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
30
|
+
}
|
|
31
|
+
catch(onrejected) {
|
|
32
|
+
return this.execute().catch(onrejected);
|
|
33
|
+
}
|
|
34
|
+
finally(onfinally) {
|
|
35
|
+
return this.execute().finally(onfinally);
|
|
36
|
+
}
|
|
37
|
+
async execute() {
|
|
38
|
+
try {
|
|
39
|
+
const paramNames = Object.keys(this.params);
|
|
40
|
+
const paramValues = Object.values(this.params);
|
|
41
|
+
const paramPlaceholders = paramNames.length > 0
|
|
42
|
+
? paramNames.map((name, i) => `"${name}" := $${i + 1}`).join(', ')
|
|
43
|
+
: '';
|
|
44
|
+
const query = paramPlaceholders
|
|
45
|
+
? `SELECT * FROM "${this.schema}"."${this.procedureName}"(${paramPlaceholders})`
|
|
46
|
+
: `SELECT * FROM "${this.schema}"."${this.procedureName}"()`;
|
|
47
|
+
const result = await this.pool.query(query, paramValues);
|
|
48
|
+
// Handle scalar return values (Supabase special handling)
|
|
49
|
+
// If result has 1 row and 1 column, and we are not in strict table mode (which rpc generally isn't),
|
|
50
|
+
// we check if it looks like a scalar return.
|
|
51
|
+
// However, if single() is called, we must respect row constraints.
|
|
52
|
+
let data = result.rows;
|
|
53
|
+
// Unwrapping logic for scalar functions (legacy Supabase behavior emulation)
|
|
54
|
+
// If it returns a single row with a single column, treat as scalar IF not forcing array via logic.
|
|
55
|
+
// But here we'll stick to basic row handling first, then apply singleMode.
|
|
56
|
+
// NOTE: Original logic had:
|
|
57
|
+
// if (result.rows.length === 1 && Object.keys(result.rows[0]).length === 1) { ... return single value ... }
|
|
58
|
+
// This implies unwrapping happens by default if it looks like a scalar.
|
|
59
|
+
const isScalarCandidate = result.rows.length === 1 && Object.keys(result.rows[0]).length === 1;
|
|
60
|
+
if (this.singleMode) {
|
|
61
|
+
if (result.rows.length > 1) {
|
|
62
|
+
return {
|
|
63
|
+
data: null,
|
|
64
|
+
error: new errors_1.PostgresError('PGRST114: Multiple rows returned'),
|
|
65
|
+
count: null,
|
|
66
|
+
status: 406,
|
|
67
|
+
statusText: 'Not Acceptable. Expected a single row but found multiple.'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (result.rows.length === 0) {
|
|
71
|
+
if (this.singleMode === 'strict') {
|
|
72
|
+
return {
|
|
73
|
+
data: null,
|
|
74
|
+
error: new errors_1.PostgresError('PGRST116: No rows found'),
|
|
75
|
+
count: null,
|
|
76
|
+
status: 404,
|
|
77
|
+
statusText: 'Not Found. Expected a single row but found no rows.'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// maybeSingle -> null data, no error
|
|
81
|
+
return {
|
|
82
|
+
data: null,
|
|
83
|
+
error: null,
|
|
84
|
+
count: 0,
|
|
85
|
+
status: 200,
|
|
86
|
+
statusText: 'OK'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// 1 row found
|
|
90
|
+
// Check for scalar unwrapping
|
|
91
|
+
if (Object.keys(result.rows[0]).length === 1) {
|
|
92
|
+
data = Object.values(result.rows[0])[0];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
data = result.rows[0];
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
data,
|
|
99
|
+
error: null,
|
|
100
|
+
count: 1,
|
|
101
|
+
status: 200,
|
|
102
|
+
statusText: 'OK'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Default behavior (no .single() called)
|
|
106
|
+
if (isScalarCandidate) {
|
|
107
|
+
data = Object.values(result.rows[0])[0];
|
|
108
|
+
return {
|
|
109
|
+
data,
|
|
110
|
+
error: null,
|
|
111
|
+
count: 1,
|
|
112
|
+
status: 200,
|
|
113
|
+
statusText: 'OK'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
data: result.rows.length > 0 ? result.rows : null,
|
|
118
|
+
error: null,
|
|
119
|
+
count: result.rowCount,
|
|
120
|
+
status: 200,
|
|
121
|
+
statusText: 'OK'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
data: null,
|
|
127
|
+
error: new errors_1.PostgresError(err.message, err.code),
|
|
128
|
+
count: null,
|
|
129
|
+
status: 500,
|
|
130
|
+
statusText: 'Internal Server Error'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.RpcBuilder = RpcBuilder;
|
|
136
|
+
_a = Symbol.toStringTag;
|
|
10
137
|
class SupaLitePG {
|
|
11
138
|
constructor(config) {
|
|
12
139
|
this.client = null;
|
|
@@ -220,44 +347,8 @@ class SupaLitePG {
|
|
|
220
347
|
this.foreignKeyCache.set(cacheKey, null);
|
|
221
348
|
return null;
|
|
222
349
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const paramNames = Object.keys(params);
|
|
226
|
-
const paramValues = Object.values(params);
|
|
227
|
-
const paramPlaceholders = paramNames.length > 0
|
|
228
|
-
? paramNames.map((name, i) => `"${name}" := $${i + 1}`).join(', ')
|
|
229
|
-
: '';
|
|
230
|
-
const query = paramPlaceholders
|
|
231
|
-
? `SELECT * FROM "${this.schema}"."${procedureName}"(${paramPlaceholders})`
|
|
232
|
-
: `SELECT * FROM "${this.schema}"."${procedureName}"()`;
|
|
233
|
-
const result = await this.pool.query(query, paramValues);
|
|
234
|
-
if (result.rows.length === 1 && Object.keys(result.rows[0]).length === 1) {
|
|
235
|
-
const singleValue = Object.values(result.rows[0])[0];
|
|
236
|
-
return {
|
|
237
|
-
data: singleValue,
|
|
238
|
-
error: null,
|
|
239
|
-
count: 1,
|
|
240
|
-
status: 200,
|
|
241
|
-
statusText: 'OK'
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
data: result.rows.length > 0 ? result.rows : null,
|
|
246
|
-
error: null,
|
|
247
|
-
count: result.rowCount,
|
|
248
|
-
status: 200,
|
|
249
|
-
statusText: 'OK'
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
return {
|
|
254
|
-
data: null,
|
|
255
|
-
error: new errors_1.PostgresError(err.message, err.code),
|
|
256
|
-
count: null,
|
|
257
|
-
status: 500,
|
|
258
|
-
statusText: 'Internal Server Error'
|
|
259
|
-
};
|
|
260
|
-
}
|
|
350
|
+
rpc(procedureName, params = {}) {
|
|
351
|
+
return new RpcBuilder(this.pool, this.schema, procedureName, params);
|
|
261
352
|
}
|
|
262
353
|
// 연결 테스트 메서드
|
|
263
354
|
async testConnection() {
|
package/dist/query-builder.js
CHANGED
|
@@ -171,7 +171,7 @@ class QueryBuilder {
|
|
|
171
171
|
or(conditions) {
|
|
172
172
|
const orParts = conditions.split(',').map(condition => {
|
|
173
173
|
const [field, op, value] = condition.split('.');
|
|
174
|
-
const validOperators = ['eq', 'neq', 'ilike', 'like', 'gt', 'gte', 'lt', 'lte'];
|
|
174
|
+
const validOperators = ['eq', 'neq', 'ilike', 'like', 'gt', 'gte', 'lt', 'lte', 'is'];
|
|
175
175
|
if (!validOperators.includes(op)) {
|
|
176
176
|
throw new Error(`Invalid operator: ${op}`);
|
|
177
177
|
}
|
|
@@ -204,6 +204,13 @@ class QueryBuilder {
|
|
|
204
204
|
return `"${field}" < $${paramIndex}`;
|
|
205
205
|
case 'lte':
|
|
206
206
|
return `"${field}" <= $${paramIndex}`;
|
|
207
|
+
case 'is':
|
|
208
|
+
if (processedValue === null) {
|
|
209
|
+
// Remove the null value from whereValues since we don't need a parameter for IS NULL
|
|
210
|
+
this.whereValues.pop();
|
|
211
|
+
return `"${field}" IS NULL`;
|
|
212
|
+
}
|
|
213
|
+
return `"${field}" IS $${paramIndex}`;
|
|
207
214
|
default:
|
|
208
215
|
return '';
|
|
209
216
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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`
|
|
@@ -0,0 +1,29 @@
|
|
|
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.
|