supalite 0.5.0 β†’ 0.5.2

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.2] - 2025-10-16
4
+
5
+ ### 🐞 Fixed
6
+ - `select()` λ©”μ„œλ“œμ—μ„œ `count: 'exact'` μ˜΅μ…˜ μ‚¬μš© μ‹œ `limit()` λ˜λŠ” `range()`와 ν•¨κ»˜ 호좜될 λ•Œ 전체 개수 λŒ€μ‹  νŽ˜μ΄μ§€λ„€μ΄μ…˜λœ 개수λ₯Ό λ°˜ν™˜ν•˜λŠ” 버그λ₯Ό μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. 이제 항상 μ •ν™•ν•œ 전체 개수λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
7
+ - `select()` λ©”μ„œλ“œμ—μ„œ `count: 'exact'`와 `head: true` μ˜΅μ…˜μ„ ν•¨κ»˜ μ‚¬μš©ν•  λ•Œ `count`κ°€ `null`둜 λ°˜ν™˜λ˜λŠ” 버그λ₯Ό μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.
8
+
9
+ ## [0.5.1] - 2025-10-16
10
+
11
+ ### 🐞 Fixed
12
+ - `select()` λ©”μ„œλ“œμ—μ„œ `count: 'exact'` μ˜΅μ…˜ μ‚¬μš© μ‹œ `limit()` λ˜λŠ” `range()`와 ν•¨κ»˜ 호좜될 λ•Œ 전체 개수 λŒ€μ‹  νŽ˜μ΄μ§€λ„€μ΄μ…˜λœ 개수λ₯Ό λ°˜ν™˜ν•˜λŠ” 버그λ₯Ό μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. 이제 항상 μ •ν™•ν•œ 전체 개수λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
13
+
3
14
  ## [0.5.0] - 2025-07-01
4
15
 
5
16
  ### ✨ Added
package/README.md CHANGED
@@ -268,6 +268,8 @@ const { data, error } = await client
268
268
  ### 쿼리 λ©”μ†Œλ“œ
269
269
 
270
270
  - `select(columns?: string, options?: { count?: 'exact' | 'planned' | 'estimated', head?: boolean })`: μ‘°νšŒν•  컬럼 μ§€μ •
271
+ - `options.count`: `'exact'`둜 μ„€μ •ν•˜λ©΄ `limit`의 영ν–₯을 λ°›μ§€ μ•ŠλŠ” 전체 결과의 개수λ₯Ό `count` μ†μ„±μœΌλ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€.
272
+ - `options.head`: `true`둜 μ„€μ •ν•˜λ©΄ 데이터 없이 `count`만 κ°€μ Έμ˜΅λ‹ˆλ‹€. `count` μ˜΅μ…˜κ³Ό ν•¨κ»˜ μ‚¬μš©ν•˜λ©΄ 효율적으둜 전체 개수만 μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
271
273
  - `insert(data: T['Tables'][K]['Insert'] | T['Tables'][K]['Insert'][])`: 단일 λ˜λŠ” 닀쀑 λ ˆμ½”λ“œ μ‚½μž…
272
274
  - `update(data: T['Tables'][K]['Update'])`: λ ˆμ½”λ“œ μ—…λ°μ΄νŠΈ
273
275
  - `delete()`: λ ˆμ½”λ“œ μ‚­μ œ
@@ -285,6 +287,7 @@ const { data, error } = await client
285
287
  - `ilike(column, pattern)`: λŒ€μ†Œλ¬Έμž ꡬ뢄 μ—†λŠ” LIKE
286
288
  - `in(column, values)`: IN μ—°μ‚°μž
287
289
  - `is(column, value)`: IS μ—°μ‚°μž
290
+ - `not(column, operator, value)`: Negates an operator (e.g., `not('column', 'is', null)`).
288
291
  - `contains(column, value)`: λ°°μ—΄/JSON 포함 μ—¬λΆ€
289
292
  - `or(conditions)`: OR 쑰건 (예: 'status.eq.active,role.eq.admin')
290
293
 
@@ -38,6 +38,7 @@ export declare class QueryBuilder<T extends DatabaseSchema, S extends SchemaName
38
38
  eq(column: string, value: any): this;
39
39
  neq(column: string, value: any): this;
40
40
  is(column: string, value: any): this;
41
+ not(column: string, operator: string, value: any): this;
41
42
  contains(column: string, value: any): this;
42
43
  in(column: string, values: any[]): this;
43
44
  gte(column: string, value: any): this;
@@ -97,6 +97,16 @@ class QueryBuilder {
97
97
  }
98
98
  return this;
99
99
  }
100
+ not(column, operator, value) {
101
+ if (operator === 'is' && value === null) {
102
+ this.whereConditions.push(`"${column}" IS NOT NULL`);
103
+ }
104
+ else {
105
+ // μΆ”ν›„ λ‹€λ₯Έ not μ—°μ‚°μžλ“€μ„ μœ„ν•΄ 남겨둠
106
+ throw new Error(`Operator "${operator}" is not supported for "not" operation.`);
107
+ }
108
+ return this;
109
+ }
100
110
  contains(column, value) {
101
111
  this.whereConditions.push(`"${column}" @> $${this.whereValues.length + 1}`);
102
112
  this.whereValues.push(value);
@@ -235,6 +245,7 @@ class QueryBuilder {
235
245
  case 'SELECT': {
236
246
  if (this.headOption) {
237
247
  query = `SELECT COUNT(*) FROM ${schemaTable}`;
248
+ query += this.buildWhereClause();
238
249
  values = [...this.whereValues];
239
250
  break;
240
251
  }
@@ -248,7 +259,6 @@ class QueryBuilder {
248
259
  const joinSubqueries = await Promise.all(this.joinClauses.map(async (join) => {
249
260
  const fk = await this.client.getForeignKey(String(this.schema), String(this.table), join.foreignTable);
250
261
  if (!fk) {
251
- // In a real scenario, you might want to throw an error or handle this case
252
262
  console.warn(`[SupaLite WARNING] No foreign key found from ${join.foreignTable} to ${String(this.table)}`);
253
263
  return null;
254
264
  }
@@ -266,12 +276,15 @@ class QueryBuilder {
266
276
  if (validSubqueries) {
267
277
  selectClause += `, ${validSubqueries}`;
268
278
  }
269
- query = `SELECT ${selectClause} FROM ${schemaTable}`;
279
+ let baseQuery = `SELECT ${selectClause} FROM ${schemaTable}`;
280
+ baseQuery += this.buildWhereClause();
281
+ values = [...this.whereValues];
270
282
  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`;
283
+ query = `SELECT *, COUNT(*) OVER() as exact_count FROM (${baseQuery}) subquery`;
284
+ }
285
+ else {
286
+ query = baseQuery;
273
287
  }
274
- values = [...this.whereValues];
275
288
  break;
276
289
  }
277
290
  case 'INSERT':
@@ -378,17 +391,19 @@ class QueryBuilder {
378
391
  break;
379
392
  }
380
393
  }
394
+ // Append clauses that apply to the outermost query
381
395
  if (this.queryType === 'SELECT') {
382
- query += this.buildWhereClause();
383
- }
384
- if (this.orderByColumns.length > 0 && this.queryType === 'SELECT') {
385
- query += ` ORDER BY ${this.orderByColumns.join(', ')}`;
386
- }
387
- if (this.limitValue !== undefined && this.queryType === 'SELECT') {
388
- query += ` LIMIT ${this.limitValue}`;
389
- }
390
- if (this.offsetValue !== undefined && this.queryType === 'SELECT') {
391
- query += ` OFFSET ${this.offsetValue}`;
396
+ // WHERE is already in the query or subquery
397
+ // ORDER BY, LIMIT, and OFFSET always apply to the outer query
398
+ if (this.orderByColumns.length > 0) {
399
+ query += ` ORDER BY ${this.orderByColumns.join(', ')}`;
400
+ }
401
+ if (this.limitValue !== undefined) {
402
+ query += ` LIMIT ${this.limitValue}`;
403
+ }
404
+ if (this.offsetValue !== undefined) {
405
+ query += ` OFFSET ${this.offsetValue}`;
406
+ }
392
407
  }
393
408
  return { query, values };
394
409
  }
@@ -427,27 +442,49 @@ class QueryBuilder {
427
442
  statusText: 'Created',
428
443
  };
429
444
  }
445
+ let countResult = null;
446
+ let dataResult = result.rows;
447
+ if (this.headOption) {
448
+ countResult = Number(result.rows[0].count);
449
+ dataResult = [];
450
+ }
451
+ else if (this.countOption === 'exact') {
452
+ if (result.rows.length > 0) {
453
+ countResult = Number(result.rows[0].exact_count);
454
+ // exact_count 열을 λͺ¨λ“  데이터 κ°μ²΄μ—μ„œ 제거
455
+ dataResult = result.rows.map(row => {
456
+ const newRow = { ...row };
457
+ delete newRow.exact_count;
458
+ return newRow;
459
+ });
460
+ }
461
+ else {
462
+ countResult = 0;
463
+ }
464
+ }
465
+ else {
466
+ countResult = result.rowCount;
467
+ }
430
468
  if (this.singleMode) {
431
- if (result.rows.length > 1) {
469
+ if (dataResult.length > 1) {
432
470
  return {
433
471
  data: null,
434
- error: new errors_1.PostgresError('PGRST114: Multiple rows returned'), // PGRST114: More than one row was returned
435
- count: result.rowCount,
436
- status: 406, // Not Acceptable
472
+ error: new errors_1.PostgresError('PGRST114: Multiple rows returned'),
473
+ count: countResult,
474
+ status: 406,
437
475
  statusText: 'Not Acceptable. Expected a single row but found multiple.',
438
476
  };
439
477
  }
440
- if (result.rows.length === 0) {
478
+ if (dataResult.length === 0) {
441
479
  if (this.singleMode === 'strict') {
442
480
  return {
443
481
  data: null,
444
- error: new errors_1.PostgresError('PGRST116: No rows found'), // PGRST116: Not found
482
+ error: new errors_1.PostgresError('PGRST116: No rows found'),
445
483
  count: 0,
446
- status: 404, // Not Found (more appropriate than 200 for strict single when not found)
484
+ status: 404,
447
485
  statusText: 'Not Found. Expected a single row but found no rows.',
448
486
  };
449
487
  }
450
- // this.singleMode === 'maybe'
451
488
  return {
452
489
  data: null,
453
490
  error: null,
@@ -456,9 +493,8 @@ class QueryBuilder {
456
493
  statusText: 'OK',
457
494
  };
458
495
  }
459
- // result.rows.length === 1
460
496
  return {
461
- data: result.rows[0],
497
+ data: dataResult[0],
462
498
  error: null,
463
499
  count: 1,
464
500
  status: 200,
@@ -466,9 +502,9 @@ class QueryBuilder {
466
502
  };
467
503
  }
468
504
  return {
469
- data: result.rows.length > 0 ? result.rows : [],
505
+ data: dataResult,
470
506
  error: null,
471
- count: result.rowCount,
507
+ count: countResult,
472
508
  status: 200,
473
509
  statusText: 'OK',
474
510
  };
@@ -0,0 +1,33 @@
1
+ # λ³€κ²½ λ³΄κ³ μ„œ: `not` λ©”μ†Œλ“œ μΆ”κ°€
2
+
3
+ **λ‚ μ§œ:** 2025λ…„ 8μ›” 28일
4
+
5
+ ## λ³€κ²½ μœ ν˜•
6
+
7
+ - [x] κΈ°λŠ₯ μΆ”κ°€
8
+ - [ ] 버그 μˆ˜μ •
9
+ - [ ] μ„±λŠ₯ κ°œμ„ 
10
+ - [ ] λ¬Έμ„œ μ—…λ°μ΄νŠΈ
11
+ - [ ] 기타
12
+
13
+ ## λ³€κ²½ λ‚΄μš©
14
+
15
+ ### `QueryBuilder`
16
+
17
+ - **`not` λ©”μ†Œλ“œ μΆ”κ°€**: `is` μ—°μ‚°μžμ™€ ν•¨κ»˜ μ‚¬μš©ν•˜μ—¬ `IS NOT NULL` 쑰건을 생성할 수 μžˆλŠ” `not` λ©”μ†Œλ“œλ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.
18
+
19
+ **μ‚¬μš© μ˜ˆμ‹œ:**
20
+
21
+ ```typescript
22
+ const { data } = await client
23
+ .from('users')
24
+ .select('id, name')
25
+ .not('email', 'is', null);
26
+ ```
27
+
28
+ μœ„ μ½”λ“œλŠ” `SELECT "id", "name" FROM "public"."users" WHERE "email" IS NOT NULL` SQL 쿼리λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
29
+
30
+ ## κ΄€λ ¨ 파일
31
+
32
+ - `src/query-builder.ts`
33
+ - `README.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supalite",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",