supalite 0.4.0 → 0.5.0
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 +9 -0
- package/dist/postgres-client.d.ts +5 -0
- package/dist/postgres-client.js +42 -0
- package/dist/query-builder.d.ts +1 -0
- package/dist/query-builder.js +69 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2025-07-01
|
|
4
|
+
|
|
5
|
+
### ✨ Added
|
|
6
|
+
- **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.
|
|
7
|
+
|
|
8
|
+
### 🛠 Changed
|
|
9
|
+
- `SupaLitePG` client now includes a `getForeignKey` method to resolve foreign key relationships, with caching for better performance.
|
|
10
|
+
- `QueryBuilder`'s `select` and `buildQuery` methods were enhanced to parse the new syntax and construct the appropriate SQL queries.
|
|
11
|
+
|
|
3
12
|
## [0.4.0] - 2025-06-10
|
|
4
13
|
|
|
5
14
|
### ✨ Added
|
|
@@ -27,6 +27,7 @@ export declare class SupaLitePG<T extends {
|
|
|
27
27
|
private isTransaction;
|
|
28
28
|
private schema;
|
|
29
29
|
private schemaCache;
|
|
30
|
+
private foreignKeyCache;
|
|
30
31
|
verbose: boolean;
|
|
31
32
|
private bigintTransform;
|
|
32
33
|
constructor(config?: SupaliteConfig);
|
|
@@ -41,6 +42,10 @@ export declare class SupaLitePG<T extends {
|
|
|
41
42
|
single(): Promise<SingleQueryResult<Row<T, S, K>>>;
|
|
42
43
|
};
|
|
43
44
|
getColumnPgType(dbSchema: string, tableName: string, columnName: string): Promise<string | undefined>;
|
|
45
|
+
getForeignKey(schema: string, table: string, foreignTable: string): Promise<{
|
|
46
|
+
column: string;
|
|
47
|
+
foreignColumn: string;
|
|
48
|
+
} | null>;
|
|
44
49
|
rpc(procedureName: string, params?: Record<string, any>): Promise<{
|
|
45
50
|
data: any;
|
|
46
51
|
error: PostgresError | null;
|
package/dist/postgres-client.js
CHANGED
|
@@ -12,6 +12,7 @@ class SupaLitePG {
|
|
|
12
12
|
this.client = null;
|
|
13
13
|
this.isTransaction = false;
|
|
14
14
|
this.schemaCache = new Map(); // schemaName.tableName -> Map<columnName, pgDataType>
|
|
15
|
+
this.foreignKeyCache = new Map();
|
|
15
16
|
this.verbose = false;
|
|
16
17
|
this.verbose = config?.verbose || process.env.SUPALITE_VERBOSE === 'true' || false;
|
|
17
18
|
this.bigintTransform = config?.bigintTransform || 'bigint'; // 기본값 'bigint'
|
|
@@ -178,6 +179,47 @@ class SupaLitePG {
|
|
|
178
179
|
console.log(`[SupaLite VERBOSE] pgType for ${tableKey}.${columnName}: ${pgType}`);
|
|
179
180
|
return pgType;
|
|
180
181
|
}
|
|
182
|
+
async getForeignKey(schema, table, foreignTable) {
|
|
183
|
+
const cacheKey = `${schema}.${table}.${foreignTable}`;
|
|
184
|
+
if (this.foreignKeyCache.has(cacheKey)) {
|
|
185
|
+
return this.foreignKeyCache.get(cacheKey);
|
|
186
|
+
}
|
|
187
|
+
const query = `
|
|
188
|
+
SELECT
|
|
189
|
+
kcu.column_name,
|
|
190
|
+
ccu.column_name AS foreign_column_name
|
|
191
|
+
FROM
|
|
192
|
+
information_schema.table_constraints AS tc
|
|
193
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
194
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
195
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
196
|
+
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
|
197
|
+
WHERE
|
|
198
|
+
tc.constraint_type = 'FOREIGN KEY'
|
|
199
|
+
AND tc.table_schema = $1
|
|
200
|
+
AND tc.table_name = $2
|
|
201
|
+
AND ccu.table_name = $3;
|
|
202
|
+
`;
|
|
203
|
+
const activeClient = this.isTransaction && this.client ? this.client : await this.pool.connect();
|
|
204
|
+
try {
|
|
205
|
+
const result = await activeClient.query(query, [schema, foreignTable, table]);
|
|
206
|
+
if (result.rows.length > 0) {
|
|
207
|
+
const relationship = {
|
|
208
|
+
column: result.rows[0].foreign_column_name,
|
|
209
|
+
foreignColumn: result.rows[0].column_name,
|
|
210
|
+
};
|
|
211
|
+
this.foreignKeyCache.set(cacheKey, relationship);
|
|
212
|
+
return relationship;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (!(this.isTransaction && this.client)) {
|
|
217
|
+
activeClient.release();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.foreignKeyCache.set(cacheKey, null);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
181
223
|
async rpc(procedureName, params = {}) {
|
|
182
224
|
try {
|
|
183
225
|
const paramNames = Object.keys(params);
|
package/dist/query-builder.d.ts
CHANGED
package/dist/query-builder.js
CHANGED
|
@@ -10,6 +10,7 @@ class QueryBuilder {
|
|
|
10
10
|
this.pool = pool;
|
|
11
11
|
this[_a] = 'QueryBuilder';
|
|
12
12
|
this.selectColumns = null;
|
|
13
|
+
this.joinClauses = [];
|
|
13
14
|
this.whereConditions = [];
|
|
14
15
|
this.orConditions = [];
|
|
15
16
|
this.orderByColumns = [];
|
|
@@ -32,17 +33,41 @@ class QueryBuilder {
|
|
|
32
33
|
return this.execute().finally(onfinally);
|
|
33
34
|
}
|
|
34
35
|
select(columns = '*', options) {
|
|
35
|
-
if (columns && columns !== '*') {
|
|
36
|
-
this.selectColumns = columns.split(',')
|
|
37
|
-
.map(col => col.trim())
|
|
38
|
-
.map(col => col.startsWith('"') && col.endsWith('"') ? col : `"${col}"`)
|
|
39
|
-
.join(', ');
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
this.selectColumns = columns;
|
|
43
|
-
}
|
|
44
36
|
this.countOption = options?.count;
|
|
45
37
|
this.headOption = options?.head;
|
|
38
|
+
if (!columns || columns === '*') {
|
|
39
|
+
this.selectColumns = '*';
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
const joinRegex = /(\w+)\(([^)]+)\)/g;
|
|
43
|
+
let match;
|
|
44
|
+
const regularColumns = [];
|
|
45
|
+
let lastIndex = 0;
|
|
46
|
+
while ((match = joinRegex.exec(columns)) !== null) {
|
|
47
|
+
const foreignTable = match[1];
|
|
48
|
+
const innerColumns = match[2];
|
|
49
|
+
this.joinClauses.push({ foreignTable, columns: innerColumns });
|
|
50
|
+
// Add the part of the string before this match to regular columns, if any
|
|
51
|
+
const preceding = columns.substring(lastIndex, match.index).trim();
|
|
52
|
+
if (preceding) {
|
|
53
|
+
regularColumns.push(...preceding.split(',').filter(c => c.trim()));
|
|
54
|
+
}
|
|
55
|
+
lastIndex = joinRegex.lastIndex;
|
|
56
|
+
}
|
|
57
|
+
// Add the rest of the string after the last match
|
|
58
|
+
const remaining = columns.substring(lastIndex).trim();
|
|
59
|
+
if (remaining) {
|
|
60
|
+
regularColumns.push(...remaining.split(',').filter(c => c.trim()));
|
|
61
|
+
}
|
|
62
|
+
const processedColumns = regularColumns
|
|
63
|
+
.map(col => col.trim())
|
|
64
|
+
.filter(col => col)
|
|
65
|
+
.map(col => {
|
|
66
|
+
if (col === '*')
|
|
67
|
+
return '*';
|
|
68
|
+
return col.startsWith('"') && col.endsWith('"') ? col : `"${col}"`;
|
|
69
|
+
});
|
|
70
|
+
this.selectColumns = processedColumns.length > 0 ? processedColumns.join(', ') : null;
|
|
46
71
|
return this;
|
|
47
72
|
}
|
|
48
73
|
match(conditions) {
|
|
@@ -207,18 +232,48 @@ class QueryBuilder {
|
|
|
207
232
|
const returning = this.shouldReturnData() ? ` RETURNING ${this.selectColumns || '*'}` : '';
|
|
208
233
|
const schemaTable = `"${String(this.schema)}"."${String(this.table)}"`;
|
|
209
234
|
switch (this.queryType) {
|
|
210
|
-
case 'SELECT':
|
|
235
|
+
case 'SELECT': {
|
|
211
236
|
if (this.headOption) {
|
|
212
237
|
query = `SELECT COUNT(*) FROM ${schemaTable}`;
|
|
238
|
+
values = [...this.whereValues];
|
|
239
|
+
break;
|
|
213
240
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
241
|
+
let selectClause = this.selectColumns;
|
|
242
|
+
if (!selectClause) {
|
|
243
|
+
selectClause = this.joinClauses.length > 0 ? `${schemaTable}.*` : '*';
|
|
244
|
+
}
|
|
245
|
+
else if (selectClause.includes('*') && this.joinClauses.length > 0) {
|
|
246
|
+
selectClause = selectClause.replace('*', `${schemaTable}.*`);
|
|
247
|
+
}
|
|
248
|
+
const joinSubqueries = await Promise.all(this.joinClauses.map(async (join) => {
|
|
249
|
+
const fk = await this.client.getForeignKey(String(this.schema), String(this.table), join.foreignTable);
|
|
250
|
+
if (!fk) {
|
|
251
|
+
// In a real scenario, you might want to throw an error or handle this case
|
|
252
|
+
console.warn(`[SupaLite WARNING] No foreign key found from ${join.foreignTable} to ${String(this.table)}`);
|
|
253
|
+
return null;
|
|
218
254
|
}
|
|
255
|
+
const foreignSchemaTable = `"${String(this.schema)}"."${join.foreignTable}"`;
|
|
256
|
+
return `(
|
|
257
|
+
SELECT json_agg(j)
|
|
258
|
+
FROM (
|
|
259
|
+
SELECT ${join.columns}
|
|
260
|
+
FROM ${foreignSchemaTable}
|
|
261
|
+
WHERE "${fk.foreignColumn}" = ${schemaTable}."${fk.column}"
|
|
262
|
+
) as j
|
|
263
|
+
) as "${join.foreignTable}"`;
|
|
264
|
+
}));
|
|
265
|
+
const validSubqueries = joinSubqueries.filter(Boolean).join(', ');
|
|
266
|
+
if (validSubqueries) {
|
|
267
|
+
selectClause += `, ${validSubqueries}`;
|
|
268
|
+
}
|
|
269
|
+
query = `SELECT ${selectClause} FROM ${schemaTable}`;
|
|
270
|
+
if (this.countOption === 'exact') {
|
|
271
|
+
// This part might need adjustment if subqueries are complex
|
|
272
|
+
query = `SELECT *, COUNT(*) OVER() as exact_count FROM (${query}) subquery`;
|
|
219
273
|
}
|
|
220
274
|
values = [...this.whereValues];
|
|
221
275
|
break;
|
|
276
|
+
}
|
|
222
277
|
case 'INSERT':
|
|
223
278
|
case 'UPSERT': {
|
|
224
279
|
if (!this.insertData)
|