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 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;
@@ -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);
@@ -7,6 +7,7 @@ export declare class QueryBuilder<T extends DatabaseSchema, S extends SchemaName
7
7
  private table;
8
8
  private schema;
9
9
  private selectColumns;
10
+ private joinClauses;
10
11
  private whereConditions;
11
12
  private orConditions;
12
13
  private countOption?;
@@ -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
- else {
215
- query = `SELECT ${this.selectColumns || '*'} FROM ${schemaTable}`;
216
- if (this.countOption === 'exact') {
217
- query = `SELECT *, COUNT(*) OVER() as exact_count FROM (${query}) subquery`;
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supalite",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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",