aurabase-js 0.2.1 → 0.4.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.
@@ -1,6 +1,7 @@
1
+ import { AuraBaseApiError } from './errors';
1
2
  import { PostgrestResponse, AuraBaseError } from './types';
2
3
 
3
- type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
4
+ type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
4
5
 
5
6
  export class QueryBuilder<T> {
6
7
  private url: string;
@@ -11,13 +12,6 @@ export class QueryBuilder<T> {
11
12
  private headers: Record<string, string>;
12
13
  private isSingle: boolean = false;
13
14
 
14
- // CUD state — stored so filters can be chained AFTER insert/update/delete
15
- private _method: HttpMethod = 'GET';
16
- private _body: unknown = undefined;
17
- private _isUpsert: boolean = false;
18
- private _onConflict: string | undefined = undefined;
19
- private _idValue: unknown = undefined; // .eq('id', x) → URL path for PATCH/DELETE
20
-
21
15
  constructor(
22
16
  url: string,
23
17
  anonKey: string,
@@ -33,167 +27,173 @@ export class QueryBuilder<T> {
33
27
  this.headers = { ...headers };
34
28
  }
35
29
 
36
- // ── SELECT ────────────────────────────────────────────────────────────────
37
- // .select('id, title') → ?select=id,title
30
+ /**
31
+ * Select columns
32
+ * @example
33
+ * .select('id, name, email')
34
+ * .select('*')
35
+ */
38
36
  select(columns: string = '*'): this {
39
- if (columns !== '*') this.queryParams.set('select', columns.replace(/\s/g, ''));
37
+ this.queryParams.set('select', columns);
40
38
  return this;
41
39
  }
42
40
 
43
- // ── FILTERS ───────────────────────────────────────────────────────────────
44
- // .eq('id', 1) ?eq=id:1 (id 컬럼이면 URL path용으로도 저장)
41
+ /**
42
+ * Filter by column equality
43
+ * @example
44
+ * .eq('id', 1)
45
+ * .eq('status', 'active')
46
+ */
45
47
  eq(column: string, value: unknown): this {
46
- if (column === 'id') this._idValue = value;
47
- this.queryParams.append('eq', `${column}:${value}`);
48
+ this.queryParams.append(column, `eq.${value}`);
48
49
  return this;
49
50
  }
50
51
 
52
+ /**
53
+ * Filter by column inequality
54
+ */
51
55
  neq(column: string, value: unknown): this {
52
- this.queryParams.append('neq', `${column}:${value}`);
56
+ this.queryParams.append(column, `neq.${value}`);
53
57
  return this;
54
58
  }
55
59
 
60
+ /**
61
+ * Filter by greater than
62
+ */
56
63
  gt(column: string, value: unknown): this {
57
- this.queryParams.append('gt', `${column}:${value}`);
64
+ this.queryParams.append(column, `gt.${value}`);
58
65
  return this;
59
66
  }
60
67
 
68
+ /**
69
+ * Filter by greater than or equal
70
+ */
61
71
  gte(column: string, value: unknown): this {
62
- this.queryParams.append('gte', `${column}:${value}`);
72
+ this.queryParams.append(column, `gte.${value}`);
63
73
  return this;
64
74
  }
65
75
 
76
+ /**
77
+ * Filter by less than
78
+ */
66
79
  lt(column: string, value: unknown): this {
67
- this.queryParams.append('lt', `${column}:${value}`);
80
+ this.queryParams.append(column, `lt.${value}`);
68
81
  return this;
69
82
  }
70
83
 
84
+ /**
85
+ * Filter by less than or equal
86
+ */
71
87
  lte(column: string, value: unknown): this {
72
- this.queryParams.append('lte', `${column}:${value}`);
88
+ this.queryParams.append(column, `lte.${value}`);
73
89
  return this;
74
90
  }
75
91
 
92
+ /**
93
+ * Filter by like pattern
94
+ */
76
95
  like(column: string, pattern: string): this {
77
- this.queryParams.append('like', `${column}:${pattern}`);
96
+ this.queryParams.append(column, `like.${pattern}`);
78
97
  return this;
79
98
  }
80
99
 
100
+ /**
101
+ * Filter by case-insensitive like pattern
102
+ */
81
103
  ilike(column: string, pattern: string): this {
82
- this.queryParams.append('ilike', `${column}:${pattern}`);
83
- return this;
84
- }
85
-
86
- in(column: string, values: unknown[]): this {
87
- this.queryParams.append('in', `${column}:${values.join(',')}`);
104
+ this.queryParams.append(column, `ilike.${pattern}`);
88
105
  return this;
89
106
  }
90
107
 
108
+ /**
109
+ * Filter by array contains
110
+ */
91
111
  contains(column: string, value: unknown[]): this {
92
- this.queryParams.append('contains', `${column}:${JSON.stringify(value)}`);
112
+ this.queryParams.append(column, `cs.{${value.join(',')}}`);
93
113
  return this;
94
114
  }
95
115
 
96
- containedBy(column: string, value: unknown[]): this {
97
- this.queryParams.append('contained_by', `${column}:${JSON.stringify(value)}`);
116
+ /**
117
+ * Filter by value in array
118
+ */
119
+ in(column: string, values: unknown[]): this {
120
+ this.queryParams.append(column, `in.(${values.join(',')})`);
98
121
  return this;
99
122
  }
100
123
 
124
+ /**
125
+ * Filter for null values
126
+ */
101
127
  isNull(column: string): this {
102
- this.queryParams.append('is_null', `${column}:true`);
128
+ this.queryParams.append(column, 'is.null');
103
129
  return this;
104
130
  }
105
131
 
132
+ /**
133
+ * Filter for non-null values
134
+ */
106
135
  isNotNull(column: string): this {
107
- this.queryParams.append('is_null', `${column}:false`);
108
- return this;
109
- }
110
-
111
- search(query: string): this {
112
- this.queryParams.set('search', query);
113
- return this;
114
- }
115
-
116
- not(operator: string, column: string, value: unknown): this {
117
- this.queryParams.append('not', `${operator}:${column}:${value}`);
118
- return this;
119
- }
120
-
121
- or(conditions: string): this {
122
- this.queryParams.set('or', conditions);
136
+ this.queryParams.append(column, 'is.not.null');
123
137
  return this;
124
138
  }
125
139
 
126
- // ── ORDER / PAGINATION ────────────────────────────────────────────────────
127
- // .order('created_at', { ascending: false }) → ?order=created_at.desc
128
- // 다중 호출 시 comma 연결 → ?order=price.desc,name.asc
140
+ /**
141
+ * Order results
142
+ * @example
143
+ * .order('created_at', { ascending: false })
144
+ */
129
145
  order(column: string, options: { ascending?: boolean; nullsFirst?: boolean } = {}): this {
130
146
  const { ascending = true, nullsFirst } = options;
131
- let str = ascending ? column : `${column}.desc`;
132
- if (nullsFirst !== undefined) str += nullsFirst ? '.nullsfirst' : '.nullslast';
133
- const existing = this.queryParams.get('order');
134
- this.queryParams.set('order', existing ? `${existing},${str}` : str);
147
+ let orderStr = column;
148
+ if (!ascending) orderStr += '.desc';
149
+ if (nullsFirst !== undefined) {
150
+ orderStr += nullsFirst ? '.nullsfirst' : '.nullslast';
151
+ }
152
+ this.queryParams.append('order', orderStr);
135
153
  return this;
136
154
  }
137
155
 
156
+ /**
157
+ * Limit results
158
+ */
138
159
  limit(count: number): this {
139
160
  this.queryParams.set('limit', String(count));
140
161
  return this;
141
162
  }
142
163
 
164
+ /**
165
+ * Offset results
166
+ */
143
167
  offset(count: number): this {
144
168
  this.queryParams.set('offset', String(count));
145
169
  return this;
146
170
  }
147
171
 
172
+ /**
173
+ * Range of results (offset + limit)
174
+ */
148
175
  range(from: number, to: number): this {
149
176
  this.queryParams.set('offset', String(from));
150
177
  this.queryParams.set('limit', String(to - from + 1));
151
178
  return this;
152
179
  }
153
180
 
181
+ /**
182
+ * Return single result
183
+ */
154
184
  single(): this {
155
185
  this.isSingle = true;
156
186
  return this;
157
187
  }
158
188
 
189
+ /**
190
+ * Return single result or null
191
+ */
159
192
  maybeSingle(): this {
160
193
  this.isSingle = true;
161
194
  return this;
162
195
  }
163
196
 
164
- // ── CUD — Supabase 스타일: 모두 this 반환 → .eq() 체이닝 가능 ─────────────
165
- // await client.from('todos').insert({ title: '할일' })
166
- insert(row: Partial<T> | Partial<T>[]): this {
167
- this._method = 'POST';
168
- const rows = Array.isArray(row) ? row : [row];
169
- this._body = rows.length === 1 ? rows[0] : rows;
170
- return this;
171
- }
172
-
173
- // await client.from('todos').update({ completed: true }).eq('id', 1)
174
- update(data: Partial<T>): this {
175
- this._method = 'PATCH';
176
- this._body = data;
177
- return this;
178
- }
179
-
180
- // await client.from('todos').upsert({ id: 1, title: '수정됨' }, { onConflict: 'id' })
181
- upsert(row: Partial<T> | Partial<T>[], options: { onConflict?: string } = {}): this {
182
- this._method = 'POST';
183
- this._isUpsert = true;
184
- this._onConflict = options.onConflict;
185
- const rows = Array.isArray(row) ? row : [row];
186
- this._body = rows.length === 1 ? rows[0] : rows;
187
- return this;
188
- }
189
-
190
- // await client.from('todos').delete().eq('id', 1)
191
- delete(): this {
192
- this._method = 'DELETE';
193
- return this;
194
- }
195
-
196
- // ── INTERNALS ─────────────────────────────────────────────────────────────
197
197
  private getHeaders(): Record<string, string> {
198
198
  const token = this.accessToken || this.anonKey;
199
199
  return {
@@ -204,77 +204,116 @@ export class QueryBuilder<T> {
204
204
  };
205
205
  }
206
206
 
207
- private buildUrl(): string {
208
- // upsert: POST /api/{table}/upsert?on_conflict=col
209
- if (this._isUpsert) {
210
- const qs = this._onConflict ? `?on_conflict=${this._onConflict}` : '';
211
- return `${this.url}/api/${this.tableName}/upsert${qs}`;
212
- }
213
-
214
- // PATCH/DELETE with id: /api/{table}/{id}/ (query params 제외)
215
- if ((this._method === 'PATCH' || this._method === 'DELETE') && this._idValue !== undefined) {
216
- return `${this.url}/api/${this.tableName}/${this._idValue}/`;
217
- }
218
-
219
- // GET/POST: /api/{table}/?...params
220
- const qs = this.queryParams.toString();
221
- return `${this.url}/api/${this.tableName}/${qs ? `?${qs}` : ''}`;
222
- }
207
+ private async request<TResponse>(
208
+ method: HttpMethod,
209
+ body?: unknown
210
+ ): Promise<PostgrestResponse<TResponse>> {
211
+ const queryString = this.queryParams.toString();
212
+ const fullUrl = `${this.url}/rest/v1/${this.tableName}${queryString ? `?${queryString}` : ''}`;
223
213
 
224
- private async execute(): Promise<PostgrestResponse<T>> {
225
- const fullUrl = this.buildUrl();
226
- const reqHeaders = this.getHeaders();
227
- const options: RequestInit = { method: this._method, headers: reqHeaders };
214
+ const options: RequestInit = {
215
+ method,
216
+ headers: this.getHeaders(),
217
+ };
228
218
 
229
- if (this._body !== undefined && this._method !== 'GET') {
230
- options.body = JSON.stringify(this._body);
231
- (reqHeaders as Record<string, string>)['Prefer'] = 'return=representation';
219
+ if (body && method !== 'GET') {
220
+ options.body = JSON.stringify(body);
221
+ (options.headers as Record<string, string>)['Prefer'] = 'return=representation';
232
222
  }
233
223
 
234
224
  try {
235
225
  const response = await fetch(fullUrl, options);
236
- const text = await response.text();
237
- let data: T | T[] | null = null;
226
+
227
+ let data: TResponse | null = null;
238
228
  let error: AuraBaseError | null = null;
239
229
 
230
+ const text = await response.text();
231
+
240
232
  if (text) {
241
233
  try {
242
234
  const parsed = JSON.parse(text);
235
+
243
236
  if (!response.ok) {
244
237
  error = {
245
- message: parsed.message || parsed.error || parsed.detail || `HTTP ${response.status}`,
238
+ message: parsed.message || parsed.error || `HTTP ${response.status}`,
246
239
  code: parsed.code,
247
240
  details: parsed.details,
248
241
  };
249
242
  } else {
250
- data = this.isSingle && Array.isArray(parsed) ? (parsed[0] ?? null) : parsed;
243
+ data = this.isSingle && Array.isArray(parsed) ? parsed[0] ?? null : parsed;
251
244
  }
252
245
  } catch {
253
- if (!response.ok) error = { message: text || `HTTP ${response.status}` };
246
+ if (!response.ok) {
247
+ error = {
248
+ message: text || `HTTP ${response.status}`,
249
+ };
250
+ }
254
251
  }
255
252
  }
256
253
 
257
- return { data, error, status: response.status, statusText: response.statusText };
254
+ return {
255
+ data,
256
+ error,
257
+ status: response.status,
258
+ statusText: response.statusText,
259
+ };
258
260
  } catch (err) {
259
261
  return {
260
262
  data: null,
261
- error: { message: err instanceof Error ? err.message : 'Network error' },
263
+ error: {
264
+ message: err instanceof Error ? err.message : 'Network error',
265
+ },
262
266
  status: 0,
263
267
  statusText: 'Network Error',
264
268
  };
265
269
  }
266
270
  }
267
271
 
268
- // then() — await 시 자동 실행
269
- async then(
270
- resolve: (value: PostgrestResponse<T>) => void | PromiseLike<void>,
272
+ /**
273
+ * Execute SELECT query
274
+ */
275
+ async then<TResult = T[]>(
276
+ resolve: (value: PostgrestResponse<TResult>) => void | PromiseLike<void>,
271
277
  reject?: (reason: unknown) => void | PromiseLike<void>
272
278
  ): Promise<void> {
273
279
  try {
274
- const result = await this.execute();
280
+ const result = await this.request<TResult>('GET');
275
281
  await resolve(result);
276
282
  } catch (err) {
277
- if (reject) await reject(err);
283
+ if (reject) {
284
+ await reject(err);
285
+ }
278
286
  }
279
287
  }
288
+
289
+ /**
290
+ * Insert row(s)
291
+ */
292
+ async insert(row: Partial<T> | Partial<T>[]): Promise<PostgrestResponse<T>> {
293
+ const rows = Array.isArray(row) ? row : [row];
294
+ return this.request<T>('POST', rows.length === 1 ? rows[0] : rows);
295
+ }
296
+
297
+ /**
298
+ * Update row(s)
299
+ */
300
+ async update(data: Partial<T>): Promise<PostgrestResponse<T>> {
301
+ return this.request<T>('PATCH', data);
302
+ }
303
+
304
+ /**
305
+ * Upsert row(s)
306
+ */
307
+ async upsert(row: Partial<T> | Partial<T>[]): Promise<PostgrestResponse<T>> {
308
+ const rows = Array.isArray(row) ? row : [row];
309
+ this.headers['Prefer'] = 'resolution=merge-duplicates,return=representation';
310
+ return this.request<T>('POST', rows.length === 1 ? rows[0] : rows);
311
+ }
312
+
313
+ /**
314
+ * Delete row(s)
315
+ */
316
+ async delete(): Promise<PostgrestResponse<T>> {
317
+ return this.request<T>('DELETE');
318
+ }
280
319
  }