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.
- package/README.md +70 -2
- package/dist/cli.js +144 -0
- package/dist/index.js +116 -75
- package/dist/index.mjs +111 -81
- package/package.json +9 -5
- package/src/AuraBaseClient.ts +6 -16
- package/src/QueryBuilder.ts +122 -161
- package/src/cli.ts +149 -0
package/src/QueryBuilder.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { AuraBaseApiError } from './errors';
|
|
2
1
|
import { PostgrestResponse, AuraBaseError } from './types';
|
|
3
2
|
|
|
4
|
-
type HttpMethod = 'GET' | 'POST' | 'PATCH' | '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
92
|
+
this.queryParams.append('contains', `${column}:${JSON.stringify(value)}`);
|
|
113
93
|
return this;
|
|
114
94
|
}
|
|
115
95
|
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|