aurabase-js 0.1.0 → 0.2.1

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,7 +1,6 @@
1
- import { AuraBaseApiError } from './errors';
2
1
  import { PostgrestResponse, AuraBaseError } from './types';
3
2
 
4
- type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
3
+ type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
5
4
 
6
5
  export class QueryBuilder<T> {
7
6
  private url: string;
@@ -12,6 +11,13 @@ export class QueryBuilder<T> {
12
11
  private headers: Record<string, string>;
13
12
  private isSingle: boolean = false;
14
13
 
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
+
15
21
  constructor(
16
22
  url: string,
17
23
  anonKey: string,
@@ -27,173 +33,167 @@ export class QueryBuilder<T> {
27
33
  this.headers = { ...headers };
28
34
  }
29
35
 
30
- /**
31
- * Select columns
32
- * @example
33
- * .select('id, name, email')
34
- * .select('*')
35
- */
36
+ // ── SELECT ────────────────────────────────────────────────────────────────
37
+ // .select('id, title') → ?select=id,title
36
38
  select(columns: string = '*'): this {
37
- this.queryParams.set('select', columns);
39
+ if (columns !== '*') this.queryParams.set('select', columns.replace(/\s/g, ''));
38
40
  return this;
39
41
  }
40
42
 
41
- /**
42
- * Filter by column equality
43
- * @example
44
- * .eq('id', 1)
45
- * .eq('status', 'active')
46
- */
43
+ // ── FILTERS ───────────────────────────────────────────────────────────────
44
+ // .eq('id', 1) ?eq=id:1 (id 컬럼이면 URL path용으로도 저장)
47
45
  eq(column: string, value: unknown): this {
48
- this.queryParams.append(column, `eq.${value}`);
46
+ if (column === 'id') this._idValue = value;
47
+ this.queryParams.append('eq', `${column}:${value}`);
49
48
  return this;
50
49
  }
51
50
 
52
- /**
53
- * Filter by column inequality
54
- */
55
51
  neq(column: string, value: unknown): this {
56
- this.queryParams.append(column, `neq.${value}`);
52
+ this.queryParams.append('neq', `${column}:${value}`);
57
53
  return this;
58
54
  }
59
55
 
60
- /**
61
- * Filter by greater than
62
- */
63
56
  gt(column: string, value: unknown): this {
64
- this.queryParams.append(column, `gt.${value}`);
57
+ this.queryParams.append('gt', `${column}:${value}`);
65
58
  return this;
66
59
  }
67
60
 
68
- /**
69
- * Filter by greater than or equal
70
- */
71
61
  gte(column: string, value: unknown): this {
72
- this.queryParams.append(column, `gte.${value}`);
62
+ this.queryParams.append('gte', `${column}:${value}`);
73
63
  return this;
74
64
  }
75
65
 
76
- /**
77
- * Filter by less than
78
- */
79
66
  lt(column: string, value: unknown): this {
80
- this.queryParams.append(column, `lt.${value}`);
67
+ this.queryParams.append('lt', `${column}:${value}`);
81
68
  return this;
82
69
  }
83
70
 
84
- /**
85
- * Filter by less than or equal
86
- */
87
71
  lte(column: string, value: unknown): this {
88
- this.queryParams.append(column, `lte.${value}`);
72
+ this.queryParams.append('lte', `${column}:${value}`);
89
73
  return this;
90
74
  }
91
75
 
92
- /**
93
- * Filter by like pattern
94
- */
95
76
  like(column: string, pattern: string): this {
96
- this.queryParams.append(column, `like.${pattern}`);
77
+ this.queryParams.append('like', `${column}:${pattern}`);
97
78
  return this;
98
79
  }
99
80
 
100
- /**
101
- * Filter by case-insensitive like pattern
102
- */
103
81
  ilike(column: string, pattern: string): this {
104
- this.queryParams.append(column, `ilike.${pattern}`);
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(',')}`);
105
88
  return this;
106
89
  }
107
90
 
108
- /**
109
- * Filter by array contains
110
- */
111
91
  contains(column: string, value: unknown[]): this {
112
- this.queryParams.append(column, `cs.{${value.join(',')}}`);
92
+ this.queryParams.append('contains', `${column}:${JSON.stringify(value)}`);
113
93
  return this;
114
94
  }
115
95
 
116
- /**
117
- * Filter by value in array
118
- */
119
- in(column: string, values: unknown[]): this {
120
- this.queryParams.append(column, `in.(${values.join(',')})`);
96
+ containedBy(column: string, value: unknown[]): this {
97
+ this.queryParams.append('contained_by', `${column}:${JSON.stringify(value)}`);
121
98
  return this;
122
99
  }
123
100
 
124
- /**
125
- * Filter for null values
126
- */
127
101
  isNull(column: string): this {
128
- this.queryParams.append(column, 'is.null');
102
+ this.queryParams.append('is_null', `${column}:true`);
129
103
  return this;
130
104
  }
131
105
 
132
- /**
133
- * Filter for non-null values
134
- */
135
106
  isNotNull(column: string): this {
136
- this.queryParams.append(column, 'is.not.null');
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);
137
123
  return this;
138
124
  }
139
125
 
140
- /**
141
- * Order results
142
- * @example
143
- * .order('created_at', { ascending: false })
144
- */
126
+ // ── ORDER / PAGINATION ────────────────────────────────────────────────────
127
+ // .order('created_at', { ascending: false }) → ?order=created_at.desc
128
+ // 다중 호출 시 comma 연결 → ?order=price.desc,name.asc
145
129
  order(column: string, options: { ascending?: boolean; nullsFirst?: boolean } = {}): this {
146
130
  const { ascending = true, nullsFirst } = options;
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);
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);
153
135
  return this;
154
136
  }
155
137
 
156
- /**
157
- * Limit results
158
- */
159
138
  limit(count: number): this {
160
139
  this.queryParams.set('limit', String(count));
161
140
  return this;
162
141
  }
163
142
 
164
- /**
165
- * Offset results
166
- */
167
143
  offset(count: number): this {
168
144
  this.queryParams.set('offset', String(count));
169
145
  return this;
170
146
  }
171
147
 
172
- /**
173
- * Range of results (offset + limit)
174
- */
175
148
  range(from: number, to: number): this {
176
149
  this.queryParams.set('offset', String(from));
177
150
  this.queryParams.set('limit', String(to - from + 1));
178
151
  return this;
179
152
  }
180
153
 
181
- /**
182
- * Return single result
183
- */
184
154
  single(): this {
185
155
  this.isSingle = true;
186
156
  return this;
187
157
  }
188
158
 
189
- /**
190
- * Return single result or null
191
- */
192
159
  maybeSingle(): this {
193
160
  this.isSingle = true;
194
161
  return this;
195
162
  }
196
163
 
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,116 +204,77 @@ export class QueryBuilder<T> {
204
204
  };
205
205
  }
206
206
 
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}` : ''}`;
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
213
 
214
- const options: RequestInit = {
215
- method,
216
- headers: this.getHeaders(),
217
- };
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
218
 
219
- if (body && method !== 'GET') {
220
- options.body = JSON.stringify(body);
221
- (options.headers as Record<string, string>)['Prefer'] = 'return=representation';
219
+ // GET/POST: /api/{table}/?...params
220
+ const qs = this.queryParams.toString();
221
+ return `${this.url}/api/${this.tableName}/${qs ? `?${qs}` : ''}`;
222
+ }
223
+
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 };
228
+
229
+ if (this._body !== undefined && this._method !== 'GET') {
230
+ options.body = JSON.stringify(this._body);
231
+ (reqHeaders as Record<string, string>)['Prefer'] = 'return=representation';
222
232
  }
223
233
 
224
234
  try {
225
235
  const response = await fetch(fullUrl, options);
226
-
227
- let data: TResponse | null = null;
228
- let error: AuraBaseError | null = null;
229
-
230
236
  const text = await response.text();
237
+ let data: T | T[] | null = null;
238
+ let error: AuraBaseError | null = null;
231
239
 
232
240
  if (text) {
233
241
  try {
234
242
  const parsed = JSON.parse(text);
235
-
236
243
  if (!response.ok) {
237
244
  error = {
238
- message: parsed.message || parsed.error || `HTTP ${response.status}`,
245
+ message: parsed.message || parsed.error || parsed.detail || `HTTP ${response.status}`,
239
246
  code: parsed.code,
240
247
  details: parsed.details,
241
248
  };
242
249
  } else {
243
- data = this.isSingle && Array.isArray(parsed) ? parsed[0] ?? null : parsed;
250
+ data = this.isSingle && Array.isArray(parsed) ? (parsed[0] ?? null) : parsed;
244
251
  }
245
252
  } catch {
246
- if (!response.ok) {
247
- error = {
248
- message: text || `HTTP ${response.status}`,
249
- };
250
- }
253
+ if (!response.ok) error = { message: text || `HTTP ${response.status}` };
251
254
  }
252
255
  }
253
256
 
254
- return {
255
- data,
256
- error,
257
- status: response.status,
258
- statusText: response.statusText,
259
- };
257
+ return { data, error, status: response.status, statusText: response.statusText };
260
258
  } catch (err) {
261
259
  return {
262
260
  data: null,
263
- error: {
264
- message: err instanceof Error ? err.message : 'Network error',
265
- },
261
+ error: { message: err instanceof Error ? err.message : 'Network error' },
266
262
  status: 0,
267
263
  statusText: 'Network Error',
268
264
  };
269
265
  }
270
266
  }
271
267
 
272
- /**
273
- * Execute SELECT query
274
- */
275
- async then<TResult = T[]>(
276
- resolve: (value: PostgrestResponse<TResult>) => void | PromiseLike<void>,
268
+ // then() — await 시 자동 실행
269
+ async then(
270
+ resolve: (value: PostgrestResponse<T>) => void | PromiseLike<void>,
277
271
  reject?: (reason: unknown) => void | PromiseLike<void>
278
272
  ): Promise<void> {
279
273
  try {
280
- const result = await this.request<TResult>('GET');
274
+ const result = await this.execute();
281
275
  await resolve(result);
282
276
  } catch (err) {
283
- if (reject) {
284
- await reject(err);
285
- }
277
+ if (reject) await reject(err);
286
278
  }
287
279
  }
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
- }
319
280
  }
package/src/cli.ts ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ const args = process.argv.slice(2);
7
+ const command = args[0];
8
+
9
+ const BOLD = '\x1b[1m';
10
+ const GREEN = '\x1b[32m';
11
+ const YELLOW = '\x1b[33m';
12
+ const CYAN = '\x1b[36m';
13
+ const RESET = '\x1b[0m';
14
+
15
+ function log(msg: string) { console.log(msg); }
16
+ function success(msg: string) { console.log(`${GREEN}✔${RESET} ${msg}`); }
17
+ function warn(msg: string) { console.log(`${YELLOW}⚠${RESET} ${msg}`); }
18
+ function info(msg: string) { console.log(`${CYAN}ℹ${RESET} ${msg}`); }
19
+
20
+ // ─── 생성할 파일 내용 ──────────────────────────────────────────────────────────
21
+
22
+ const CLIENT_TS = `import { createClient } from 'aurabase-js';
23
+
24
+ export const client = createClient({
25
+ url: process.env.NEXT_PUBLIC_AURABASE_URL || '',
26
+ anonKey: process.env.NEXT_PUBLIC_AURABASE_ANON_KEY || '',
27
+ });
28
+ `;
29
+
30
+ const SERVER_TS = `import { createClient } from 'aurabase-js';
31
+ import { cookies } from 'next/headers';
32
+
33
+ export async function server() {
34
+ const cookieStore = await cookies();
35
+ const accessToken = cookieStore.get('access_token')?.value;
36
+
37
+ const client = createClient({
38
+ url: process.env.NEXT_PUBLIC_AURABASE_URL || '',
39
+ anonKey: process.env.NEXT_PUBLIC_AURABASE_ANON_KEY || '',
40
+ });
41
+
42
+ if (accessToken) {
43
+ client.setAccessToken(accessToken);
44
+ }
45
+
46
+ return client;
47
+ }
48
+ `;
49
+
50
+ const ADMIN_TS = `import { createClient } from 'aurabase-js';
51
+
52
+ /**
53
+ * Admin client with service_role key
54
+ * ⚠️ ONLY use on server-side (API routes, server components)
55
+ * ⚠️ Bypasses all RLS policies - use with caution!
56
+ */
57
+ export const admin = createClient({
58
+ url: process.env.NEXT_PUBLIC_AURABASE_URL || '',
59
+ anonKey: process.env.NEXT_PUBLIC_AURABASE_SERVICE_ROLE_KEY || '',
60
+ });
61
+ `;
62
+
63
+ const ENV_EXAMPLE = `# AuraBase
64
+ NEXT_PUBLIC_AURABASE_URL=https://your-project.cloudfront.net
65
+ NEXT_PUBLIC_AURABASE_ANON_KEY=your-anon-key
66
+ NEXT_PUBLIC_AURABASE_SERVICE_ROLE_KEY=your-service-role-key
67
+ `;
68
+
69
+ // ─── init 커맨드 ───────────────────────────────────────────────────────────────
70
+
71
+ function init() {
72
+ const cwd = process.cwd();
73
+ const libDir = path.join(cwd, 'lib');
74
+
75
+ log('');
76
+ log(`${BOLD}aurabase-js init${RESET}`);
77
+ log('──────────────────────────────────');
78
+
79
+ // lib 폴더 생성
80
+ if (!fs.existsSync(libDir)) {
81
+ fs.mkdirSync(libDir, { recursive: true });
82
+ success(`lib/ 폴더 생성`);
83
+ } else {
84
+ info(`lib/ 폴더가 이미 존재합니다`);
85
+ }
86
+
87
+ // 파일 생성 (이미 있으면 건너뜀)
88
+ const files: [string, string][] = [
89
+ ['lib/client.ts', CLIENT_TS],
90
+ ['lib/server.ts', SERVER_TS],
91
+ ['lib/admin.ts', ADMIN_TS],
92
+ ];
93
+
94
+ for (const [relPath, content] of files) {
95
+ const fullPath = path.join(cwd, relPath);
96
+ if (fs.existsSync(fullPath)) {
97
+ warn(`${relPath} 이미 존재 — 건너뜀`);
98
+ } else {
99
+ fs.writeFileSync(fullPath, content, 'utf8');
100
+ success(`${relPath} 생성`);
101
+ }
102
+ }
103
+
104
+ // .env.local 가이드
105
+ const envPath = path.join(cwd, '.env.local');
106
+ const envExists = fs.existsSync(envPath);
107
+
108
+ log('');
109
+ log(`${BOLD}📋 환경변수 설정${RESET}`);
110
+ log('──────────────────────────────────');
111
+
112
+ if (!envExists) {
113
+ fs.writeFileSync(envPath, ENV_EXAMPLE, 'utf8');
114
+ success(`.env.local 생성 — 아래 값을 채워주세요`);
115
+ } else {
116
+ info(`.env.local 이미 존재 — 아래 항목이 있는지 확인하세요`);
117
+ }
118
+
119
+ log('');
120
+ log(` ${CYAN}NEXT_PUBLIC_AURABASE_URL${RESET}=https://your-project.cloudfront.net`);
121
+ log(` ${CYAN}NEXT_PUBLIC_AURABASE_ANON_KEY${RESET}=your-anon-key`);
122
+ log(` ${CYAN}NEXT_PUBLIC_AURABASE_SERVICE_ROLE_KEY${RESET}=your-service-role-key`);
123
+ log('');
124
+ log(` → AuraBase 대시보드 ${BOLD}API Keys${RESET} 메뉴에서 확인하세요.`);
125
+ log('');
126
+ log(`${GREEN}✔ 완료!${RESET} 이제 아래처럼 사용할 수 있습니다:`);
127
+ log('');
128
+ log(` ${CYAN}// 클라이언트 컴포넌트${RESET}`);
129
+ log(` import { client } from '@/lib/client'`);
130
+ log(` const { data } = await client.from('todos').select('*')`);
131
+ log('');
132
+ log(` ${CYAN}// 서버 전용 (API Route, Server Component)${RESET}`);
133
+ log(` import { admin } from '@/lib/admin'`);
134
+ log(` const { data } = await admin.from('todos').select('*')`);
135
+ log('');
136
+ }
137
+
138
+ // ─── 진입점 ────────────────────────────────────────────────────────────────────
139
+
140
+ if (command === 'init') {
141
+ init();
142
+ } else {
143
+ log('');
144
+ log(`${BOLD}aurabase-js CLI${RESET}`);
145
+ log('');
146
+ log('사용법:');
147
+ log(` ${CYAN}npx aurabase-js init${RESET} lib/ 폴더 및 환경변수 파일 자동 생성`);
148
+ log('');
149
+ }