aurabase-js 0.6.0 → 0.7.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/dist/cli.js CHANGED
@@ -52,12 +52,30 @@ export async function server() {
52
52
 
53
53
  return client;
54
54
  }
55
+ `,
56
+ "lib/admin.ts": `import { createClient } from 'aurabase-js';
57
+
58
+ /**
59
+ * Admin client with service_role key
60
+ * \u26A0\uFE0F ONLY use on server-side (API routes, server components)
61
+ * \u26A0\uFE0F Bypasses all RLS policies - use with caution!
62
+ */
63
+ export const admin = createClient({
64
+ url: process.env.NEXT_PUBLIC_AURABASE_URL || '',
65
+ anonKey: process.env.AURABASE_SERVICE_ROLE_KEY || '',
66
+ });
55
67
  `
56
68
  };
57
69
  var ENV_TEMPLATE = `
58
70
  # AuraBase Configuration (.env.local)
71
+
72
+ # Public keys (client + server)
59
73
  NEXT_PUBLIC_AURABASE_URL=https://your-project.cloudfront.net
60
74
  NEXT_PUBLIC_AURABASE_ANON_KEY=your-anon-key-here
75
+
76
+ # Service role key (server only - bypasses RLS!)
77
+ # \u26A0\uFE0F NEVER expose this to the client!
78
+ AURABASE_SERVICE_ROLE_KEY=your-service-role-key-here
61
79
  `;
62
80
  function findProjectRoot() {
63
81
  let dir = process.cwd();
@@ -93,6 +111,7 @@ function init() {
93
111
  console.log("\x1B[1m\n\u{1F680} Initializing AuraBase...\x1B[0m\n");
94
112
  createFile(projectRoot, "lib/client.ts");
95
113
  createFile(projectRoot, "lib/server.ts");
114
+ createFile(projectRoot, "lib/admin.ts");
96
115
  console.log("\x1B[36m%s\x1B[0m", ENV_TEMPLATE);
97
116
  }
98
117
  function showHelp() {
@@ -107,8 +126,9 @@ function showHelp() {
107
126
  -v, --version Show version
108
127
 
109
128
  \x1B[1mGenerated files:\x1B[0m
110
- lib/client.ts Browser client
129
+ lib/client.ts Browser client (anon key)
111
130
  lib/server.ts Server client (with cookie auth)
131
+ lib/admin.ts Admin client (service role, bypasses RLS)
112
132
  `);
113
133
  }
114
134
  function showVersion() {
package/dist/cli.mjs CHANGED
@@ -29,12 +29,30 @@ export async function server() {
29
29
 
30
30
  return client;
31
31
  }
32
+ `,
33
+ "lib/admin.ts": `import { createClient } from 'aurabase-js';
34
+
35
+ /**
36
+ * Admin client with service_role key
37
+ * \u26A0\uFE0F ONLY use on server-side (API routes, server components)
38
+ * \u26A0\uFE0F Bypasses all RLS policies - use with caution!
39
+ */
40
+ export const admin = createClient({
41
+ url: process.env.NEXT_PUBLIC_AURABASE_URL || '',
42
+ anonKey: process.env.AURABASE_SERVICE_ROLE_KEY || '',
43
+ });
32
44
  `
33
45
  };
34
46
  var ENV_TEMPLATE = `
35
47
  # AuraBase Configuration (.env.local)
48
+
49
+ # Public keys (client + server)
36
50
  NEXT_PUBLIC_AURABASE_URL=https://your-project.cloudfront.net
37
51
  NEXT_PUBLIC_AURABASE_ANON_KEY=your-anon-key-here
52
+
53
+ # Service role key (server only - bypasses RLS!)
54
+ # \u26A0\uFE0F NEVER expose this to the client!
55
+ AURABASE_SERVICE_ROLE_KEY=your-service-role-key-here
38
56
  `;
39
57
  function findProjectRoot() {
40
58
  let dir = process.cwd();
@@ -70,6 +88,7 @@ function init() {
70
88
  console.log("\x1B[1m\n\u{1F680} Initializing AuraBase...\x1B[0m\n");
71
89
  createFile(projectRoot, "lib/client.ts");
72
90
  createFile(projectRoot, "lib/server.ts");
91
+ createFile(projectRoot, "lib/admin.ts");
73
92
  console.log("\x1B[36m%s\x1B[0m", ENV_TEMPLATE);
74
93
  }
75
94
  function showHelp() {
@@ -84,8 +103,9 @@ function showHelp() {
84
103
  -v, --version Show version
85
104
 
86
105
  \x1B[1mGenerated files:\x1B[0m
87
- lib/client.ts Browser client
106
+ lib/client.ts Browser client (anon key)
88
107
  lib/server.ts Server client (with cookie auth)
108
+ lib/admin.ts Admin client (service role, bypasses RLS)
89
109
  `);
90
110
  }
91
111
  function showVersion() {
package/dist/index.d.mts CHANGED
@@ -26,6 +26,7 @@ interface PostgrestResponse<T> {
26
26
  error: AuraBaseError | null;
27
27
  status: number;
28
28
  statusText: string;
29
+ count?: number | null;
29
30
  }
30
31
  interface AuraBaseError {
31
32
  message: string;
@@ -65,6 +66,9 @@ declare class QueryBuilder<T> {
65
66
  private queryParams;
66
67
  private headers;
67
68
  private isSingle;
69
+ private _countOption;
70
+ private _head;
71
+ private _idValue;
68
72
  constructor(url: string, anonKey: string, accessToken: string | null, tableName: string, headers?: Record<string, string>);
69
73
  /**
70
74
  * Select columns
@@ -72,7 +76,10 @@ declare class QueryBuilder<T> {
72
76
  * .select('id, name, email')
73
77
  * .select('*')
74
78
  */
75
- select(columns?: string): this;
79
+ select(columns?: string, options?: {
80
+ count?: 'exact' | 'planned' | 'estimated';
81
+ head?: boolean;
82
+ }): this;
76
83
  /**
77
84
  * Filter by column equality
78
85
  * @example
@@ -124,6 +131,21 @@ declare class QueryBuilder<T> {
124
131
  * Filter for non-null values
125
132
  */
126
133
  isNotNull(column: string): this;
134
+ /**
135
+ * Negate a filter
136
+ */
137
+ not(column: string, operator: string, value?: unknown): this;
138
+ /**
139
+ * Filter by null or not null
140
+ */
141
+ is(column: string, value: null | 'not.null'): this;
142
+ /**
143
+ * Full-text search
144
+ */
145
+ textSearch(column: string, query: string, options?: {
146
+ config?: string;
147
+ type?: 'plain' | 'phrase' | 'websearch';
148
+ }): this;
127
149
  /**
128
150
  * Order results
129
151
  * @example
@@ -266,6 +288,10 @@ declare class AuraBaseClient {
266
288
  * Set access token for authenticated requests
267
289
  */
268
290
  setAccessToken(token: string | null): void;
291
+ /**
292
+ * Manually set session (Supabase-compatible alias)
293
+ */
294
+ setSession(token: string | null): void;
269
295
  /**
270
296
  * Get the current access token
271
297
  */
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ interface PostgrestResponse<T> {
26
26
  error: AuraBaseError | null;
27
27
  status: number;
28
28
  statusText: string;
29
+ count?: number | null;
29
30
  }
30
31
  interface AuraBaseError {
31
32
  message: string;
@@ -65,6 +66,9 @@ declare class QueryBuilder<T> {
65
66
  private queryParams;
66
67
  private headers;
67
68
  private isSingle;
69
+ private _countOption;
70
+ private _head;
71
+ private _idValue;
68
72
  constructor(url: string, anonKey: string, accessToken: string | null, tableName: string, headers?: Record<string, string>);
69
73
  /**
70
74
  * Select columns
@@ -72,7 +76,10 @@ declare class QueryBuilder<T> {
72
76
  * .select('id, name, email')
73
77
  * .select('*')
74
78
  */
75
- select(columns?: string): this;
79
+ select(columns?: string, options?: {
80
+ count?: 'exact' | 'planned' | 'estimated';
81
+ head?: boolean;
82
+ }): this;
76
83
  /**
77
84
  * Filter by column equality
78
85
  * @example
@@ -124,6 +131,21 @@ declare class QueryBuilder<T> {
124
131
  * Filter for non-null values
125
132
  */
126
133
  isNotNull(column: string): this;
134
+ /**
135
+ * Negate a filter
136
+ */
137
+ not(column: string, operator: string, value?: unknown): this;
138
+ /**
139
+ * Filter by null or not null
140
+ */
141
+ is(column: string, value: null | 'not.null'): this;
142
+ /**
143
+ * Full-text search
144
+ */
145
+ textSearch(column: string, query: string, options?: {
146
+ config?: string;
147
+ type?: 'plain' | 'phrase' | 'websearch';
148
+ }): this;
127
149
  /**
128
150
  * Order results
129
151
  * @example
@@ -266,6 +288,10 @@ declare class AuraBaseClient {
266
288
  * Set access token for authenticated requests
267
289
  */
268
290
  setAccessToken(token: string | null): void;
291
+ /**
292
+ * Manually set session (Supabase-compatible alias)
293
+ */
294
+ setSession(token: string | null): void;
269
295
  /**
270
296
  * Get the current access token
271
297
  */
package/dist/index.js CHANGED
@@ -30,6 +30,9 @@ module.exports = __toCommonJS(index_exports);
30
30
  var QueryBuilder = class {
31
31
  constructor(url, anonKey, accessToken, tableName, headers = {}) {
32
32
  this.isSingle = false;
33
+ this._countOption = null;
34
+ this._head = false;
35
+ this._idValue = void 0;
33
36
  this.url = url;
34
37
  this.anonKey = anonKey;
35
38
  this.accessToken = accessToken;
@@ -43,8 +46,10 @@ var QueryBuilder = class {
43
46
  * .select('id, name, email')
44
47
  * .select('*')
45
48
  */
46
- select(columns = "*") {
49
+ select(columns = "*", options) {
47
50
  this.queryParams.set("select", columns);
51
+ if (options?.count) this._countOption = options.count;
52
+ if (options?.head) this._head = true;
48
53
  return this;
49
54
  }
50
55
  /**
@@ -55,6 +60,7 @@ var QueryBuilder = class {
55
60
  */
56
61
  eq(column, value) {
57
62
  this.queryParams.append(column, `eq.${value}`);
63
+ if (column === "id") this._idValue = value;
58
64
  return this;
59
65
  }
60
66
  /**
@@ -134,6 +140,29 @@ var QueryBuilder = class {
134
140
  this.queryParams.append(column, "is.not.null");
135
141
  return this;
136
142
  }
143
+ /**
144
+ * Negate a filter
145
+ */
146
+ not(column, operator, value) {
147
+ this.queryParams.append(column, `not.${operator}.${value ?? ""}`);
148
+ return this;
149
+ }
150
+ /**
151
+ * Filter by null or not null
152
+ */
153
+ is(column, value) {
154
+ if (value === null) return this.isNull(column);
155
+ return this.isNotNull(column);
156
+ }
157
+ /**
158
+ * Full-text search
159
+ */
160
+ textSearch(column, query, options) {
161
+ const prefix = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : "fts";
162
+ const configStr = options?.config ? `(${options.config})` : "";
163
+ this.queryParams.append(column, `${prefix}${configStr}.${query}`);
164
+ return this;
165
+ }
137
166
  /**
138
167
  * Order results
139
168
  * @example
@@ -187,28 +216,61 @@ var QueryBuilder = class {
187
216
  }
188
217
  getHeaders() {
189
218
  const token = this.accessToken || this.anonKey;
190
- return {
219
+ const headers = {
191
220
  "Content-Type": "application/json",
192
221
  "apikey": this.anonKey,
193
222
  "Authorization": `Bearer ${token}`,
194
223
  ...this.headers
195
224
  };
225
+ if (this._countOption) {
226
+ headers["Prefer"] = `count=${this._countOption}`;
227
+ }
228
+ return headers;
196
229
  }
197
230
  async request(method, body) {
198
- const queryString = this.queryParams.toString();
199
- const fullUrl = `${this.url}/rest/v1/${this.tableName}${queryString ? `?${queryString}` : ""}`;
231
+ const effectiveMethod = this._head ? "HEAD" : method;
232
+ let fullUrl;
233
+ if ((method === "PATCH" || method === "DELETE") && this._idValue !== void 0) {
234
+ const params = new URLSearchParams(this.queryParams);
235
+ params.delete("id");
236
+ const qs = params.toString();
237
+ fullUrl = `${this.url}/api/${this.tableName}/${this._idValue}/${qs ? `?${qs}` : ""}`;
238
+ } else {
239
+ const queryString = this.queryParams.toString();
240
+ fullUrl = `${this.url}/api/${this.tableName}/${queryString ? `?${queryString}` : ""}`;
241
+ }
200
242
  const options = {
201
- method,
243
+ method: effectiveMethod,
202
244
  headers: this.getHeaders()
203
245
  };
204
- if (body && method !== "GET") {
246
+ if (body && method !== "GET" && method !== "HEAD") {
205
247
  options.body = JSON.stringify(body);
206
- options.headers["Prefer"] = "return=representation";
248
+ const hdrs = options.headers;
249
+ if (hdrs["Prefer"]) {
250
+ hdrs["Prefer"] = hdrs["Prefer"] + ",return=representation";
251
+ } else {
252
+ hdrs["Prefer"] = "return=representation";
253
+ }
207
254
  }
208
255
  try {
209
256
  const response = await fetch(fullUrl, options);
210
257
  let data = null;
211
258
  let error = null;
259
+ const contentRange = response.headers.get("content-range");
260
+ let count = null;
261
+ if (contentRange) {
262
+ const match = contentRange.match(/\/(\d+)$/);
263
+ if (match) count = parseInt(match[1], 10);
264
+ }
265
+ if (effectiveMethod === "HEAD") {
266
+ return {
267
+ data: null,
268
+ error: null,
269
+ status: response.status,
270
+ statusText: response.statusText,
271
+ count
272
+ };
273
+ }
212
274
  const text = await response.text();
213
275
  if (text) {
214
276
  try {
@@ -234,7 +296,8 @@ var QueryBuilder = class {
234
296
  data,
235
297
  error,
236
298
  status: response.status,
237
- statusText: response.statusText
299
+ statusText: response.statusText,
300
+ count
238
301
  };
239
302
  } catch (err) {
240
303
  return {
@@ -243,7 +306,8 @@ var QueryBuilder = class {
243
306
  message: err instanceof Error ? err.message : "Network error"
244
307
  },
245
308
  status: 0,
246
- statusText: "Network Error"
309
+ statusText: "Network Error",
310
+ count: null
247
311
  };
248
312
  }
249
313
  }
@@ -550,6 +614,17 @@ var AuraBaseClient = class {
550
614
  this.anonKey = options.anonKey;
551
615
  this.customHeaders = options.headers || {};
552
616
  this.auth = new AuthClient(this.url, this.anonKey);
617
+ const { data: { session } } = this.auth.getSession();
618
+ if (session?.access_token) {
619
+ this.accessToken = session.access_token;
620
+ }
621
+ this.auth.onAuthStateChange((event, session2) => {
622
+ if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
623
+ this.accessToken = session2?.access_token ?? null;
624
+ } else if (event === "SIGNED_OUT") {
625
+ this.accessToken = null;
626
+ }
627
+ });
553
628
  }
554
629
  /**
555
630
  * Set access token for authenticated requests
@@ -557,6 +632,12 @@ var AuraBaseClient = class {
557
632
  setAccessToken(token) {
558
633
  this.accessToken = token;
559
634
  }
635
+ /**
636
+ * Manually set session (Supabase-compatible alias)
637
+ */
638
+ setSession(token) {
639
+ this.accessToken = token;
640
+ }
560
641
  /**
561
642
  * Get the current access token
562
643
  */
@@ -585,7 +666,7 @@ var AuraBaseClient = class {
585
666
  async rpc(functionName, params) {
586
667
  const token = this.accessToken || this.anonKey;
587
668
  try {
588
- const response = await fetch(`${this.url}/rpc/v1/${functionName}`, {
669
+ const response = await fetch(`${this.url}/rest/v1/rpc/${functionName}`, {
589
670
  method: "POST",
590
671
  headers: {
591
672
  "Content-Type": "application/json",
package/dist/index.mjs CHANGED
@@ -2,6 +2,9 @@
2
2
  var QueryBuilder = class {
3
3
  constructor(url, anonKey, accessToken, tableName, headers = {}) {
4
4
  this.isSingle = false;
5
+ this._countOption = null;
6
+ this._head = false;
7
+ this._idValue = void 0;
5
8
  this.url = url;
6
9
  this.anonKey = anonKey;
7
10
  this.accessToken = accessToken;
@@ -15,8 +18,10 @@ var QueryBuilder = class {
15
18
  * .select('id, name, email')
16
19
  * .select('*')
17
20
  */
18
- select(columns = "*") {
21
+ select(columns = "*", options) {
19
22
  this.queryParams.set("select", columns);
23
+ if (options?.count) this._countOption = options.count;
24
+ if (options?.head) this._head = true;
20
25
  return this;
21
26
  }
22
27
  /**
@@ -27,6 +32,7 @@ var QueryBuilder = class {
27
32
  */
28
33
  eq(column, value) {
29
34
  this.queryParams.append(column, `eq.${value}`);
35
+ if (column === "id") this._idValue = value;
30
36
  return this;
31
37
  }
32
38
  /**
@@ -106,6 +112,29 @@ var QueryBuilder = class {
106
112
  this.queryParams.append(column, "is.not.null");
107
113
  return this;
108
114
  }
115
+ /**
116
+ * Negate a filter
117
+ */
118
+ not(column, operator, value) {
119
+ this.queryParams.append(column, `not.${operator}.${value ?? ""}`);
120
+ return this;
121
+ }
122
+ /**
123
+ * Filter by null or not null
124
+ */
125
+ is(column, value) {
126
+ if (value === null) return this.isNull(column);
127
+ return this.isNotNull(column);
128
+ }
129
+ /**
130
+ * Full-text search
131
+ */
132
+ textSearch(column, query, options) {
133
+ const prefix = options?.type === "plain" ? "plfts" : options?.type === "phrase" ? "phfts" : "fts";
134
+ const configStr = options?.config ? `(${options.config})` : "";
135
+ this.queryParams.append(column, `${prefix}${configStr}.${query}`);
136
+ return this;
137
+ }
109
138
  /**
110
139
  * Order results
111
140
  * @example
@@ -159,28 +188,61 @@ var QueryBuilder = class {
159
188
  }
160
189
  getHeaders() {
161
190
  const token = this.accessToken || this.anonKey;
162
- return {
191
+ const headers = {
163
192
  "Content-Type": "application/json",
164
193
  "apikey": this.anonKey,
165
194
  "Authorization": `Bearer ${token}`,
166
195
  ...this.headers
167
196
  };
197
+ if (this._countOption) {
198
+ headers["Prefer"] = `count=${this._countOption}`;
199
+ }
200
+ return headers;
168
201
  }
169
202
  async request(method, body) {
170
- const queryString = this.queryParams.toString();
171
- const fullUrl = `${this.url}/rest/v1/${this.tableName}${queryString ? `?${queryString}` : ""}`;
203
+ const effectiveMethod = this._head ? "HEAD" : method;
204
+ let fullUrl;
205
+ if ((method === "PATCH" || method === "DELETE") && this._idValue !== void 0) {
206
+ const params = new URLSearchParams(this.queryParams);
207
+ params.delete("id");
208
+ const qs = params.toString();
209
+ fullUrl = `${this.url}/api/${this.tableName}/${this._idValue}/${qs ? `?${qs}` : ""}`;
210
+ } else {
211
+ const queryString = this.queryParams.toString();
212
+ fullUrl = `${this.url}/api/${this.tableName}/${queryString ? `?${queryString}` : ""}`;
213
+ }
172
214
  const options = {
173
- method,
215
+ method: effectiveMethod,
174
216
  headers: this.getHeaders()
175
217
  };
176
- if (body && method !== "GET") {
218
+ if (body && method !== "GET" && method !== "HEAD") {
177
219
  options.body = JSON.stringify(body);
178
- options.headers["Prefer"] = "return=representation";
220
+ const hdrs = options.headers;
221
+ if (hdrs["Prefer"]) {
222
+ hdrs["Prefer"] = hdrs["Prefer"] + ",return=representation";
223
+ } else {
224
+ hdrs["Prefer"] = "return=representation";
225
+ }
179
226
  }
180
227
  try {
181
228
  const response = await fetch(fullUrl, options);
182
229
  let data = null;
183
230
  let error = null;
231
+ const contentRange = response.headers.get("content-range");
232
+ let count = null;
233
+ if (contentRange) {
234
+ const match = contentRange.match(/\/(\d+)$/);
235
+ if (match) count = parseInt(match[1], 10);
236
+ }
237
+ if (effectiveMethod === "HEAD") {
238
+ return {
239
+ data: null,
240
+ error: null,
241
+ status: response.status,
242
+ statusText: response.statusText,
243
+ count
244
+ };
245
+ }
184
246
  const text = await response.text();
185
247
  if (text) {
186
248
  try {
@@ -206,7 +268,8 @@ var QueryBuilder = class {
206
268
  data,
207
269
  error,
208
270
  status: response.status,
209
- statusText: response.statusText
271
+ statusText: response.statusText,
272
+ count
210
273
  };
211
274
  } catch (err) {
212
275
  return {
@@ -215,7 +278,8 @@ var QueryBuilder = class {
215
278
  message: err instanceof Error ? err.message : "Network error"
216
279
  },
217
280
  status: 0,
218
- statusText: "Network Error"
281
+ statusText: "Network Error",
282
+ count: null
219
283
  };
220
284
  }
221
285
  }
@@ -522,6 +586,17 @@ var AuraBaseClient = class {
522
586
  this.anonKey = options.anonKey;
523
587
  this.customHeaders = options.headers || {};
524
588
  this.auth = new AuthClient(this.url, this.anonKey);
589
+ const { data: { session } } = this.auth.getSession();
590
+ if (session?.access_token) {
591
+ this.accessToken = session.access_token;
592
+ }
593
+ this.auth.onAuthStateChange((event, session2) => {
594
+ if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
595
+ this.accessToken = session2?.access_token ?? null;
596
+ } else if (event === "SIGNED_OUT") {
597
+ this.accessToken = null;
598
+ }
599
+ });
525
600
  }
526
601
  /**
527
602
  * Set access token for authenticated requests
@@ -529,6 +604,12 @@ var AuraBaseClient = class {
529
604
  setAccessToken(token) {
530
605
  this.accessToken = token;
531
606
  }
607
+ /**
608
+ * Manually set session (Supabase-compatible alias)
609
+ */
610
+ setSession(token) {
611
+ this.accessToken = token;
612
+ }
532
613
  /**
533
614
  * Get the current access token
534
615
  */
@@ -557,7 +638,7 @@ var AuraBaseClient = class {
557
638
  async rpc(functionName, params) {
558
639
  const token = this.accessToken || this.anonKey;
559
640
  try {
560
- const response = await fetch(`${this.url}/rpc/v1/${functionName}`, {
641
+ const response = await fetch(`${this.url}/rest/v1/rpc/${functionName}`, {
561
642
  method: "POST",
562
643
  headers: {
563
644
  "Content-Type": "application/json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aurabase-js",
3
- "version": "0.6.0",
3
+ "version": "0.7.2",
4
4
  "description": "AuraBase client library - Supabase-style SDK for AuraBase",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -15,6 +15,10 @@
15
15
  "require": "./dist/index.js"
16
16
  }
17
17
  },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
18
22
  "scripts": {
19
23
  "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
20
24
  "dev": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --watch",
@@ -14,6 +14,21 @@ export class AuraBaseClient {
14
14
  this.anonKey = options.anonKey;
15
15
  this.customHeaders = options.headers || {};
16
16
  this.auth = new AuthClient(this.url, this.anonKey);
17
+
18
+ // 저장된 세션 복원 (페이지 새로고침 후에도 로그인 상태 유지)
19
+ const { data: { session } } = this.auth.getSession();
20
+ if (session?.access_token) {
21
+ this.accessToken = session.access_token;
22
+ }
23
+
24
+ // 로그인/로그아웃 시 accessToken 자동 동기화
25
+ this.auth.onAuthStateChange((event, session) => {
26
+ if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
27
+ this.accessToken = session?.access_token ?? null;
28
+ } else if (event === 'SIGNED_OUT') {
29
+ this.accessToken = null;
30
+ }
31
+ });
17
32
  }
18
33
 
19
34
  /**
@@ -23,6 +38,13 @@ export class AuraBaseClient {
23
38
  this.accessToken = token;
24
39
  }
25
40
 
41
+ /**
42
+ * Manually set session (Supabase-compatible alias)
43
+ */
44
+ setSession(token: string | null): void {
45
+ this.accessToken = token;
46
+ }
47
+
26
48
  /**
27
49
  * Get the current access token
28
50
  */
@@ -57,7 +79,7 @@ export class AuraBaseClient {
57
79
  const token = this.accessToken || this.anonKey;
58
80
 
59
81
  try {
60
- const response = await fetch(`${this.url}/rpc/v1/${functionName}`, {
82
+ const response = await fetch(`${this.url}/rest/v1/rpc/${functionName}`, {
61
83
  method: 'POST',
62
84
  headers: {
63
85
  'Content-Type': 'application/json',
@@ -1,7 +1,7 @@
1
1
  import { AuraBaseApiError } from './errors';
2
2
  import { PostgrestResponse, AuraBaseError } from './types';
3
3
 
4
- type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
4
+ type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'HEAD';
5
5
 
6
6
  export class QueryBuilder<T> {
7
7
  private url: string;
@@ -11,6 +11,9 @@ export class QueryBuilder<T> {
11
11
  private queryParams: URLSearchParams;
12
12
  private headers: Record<string, string>;
13
13
  private isSingle: boolean = false;
14
+ private _countOption: 'exact' | 'planned' | 'estimated' | null = null;
15
+ private _head: boolean = false;
16
+ private _idValue: unknown = undefined;
14
17
 
15
18
  constructor(
16
19
  url: string,
@@ -33,8 +36,10 @@ export class QueryBuilder<T> {
33
36
  * .select('id, name, email')
34
37
  * .select('*')
35
38
  */
36
- select(columns: string = '*'): this {
39
+ select(columns: string = '*', options?: { count?: 'exact' | 'planned' | 'estimated'; head?: boolean }): this {
37
40
  this.queryParams.set('select', columns);
41
+ if (options?.count) this._countOption = options.count;
42
+ if (options?.head) this._head = true;
38
43
  return this;
39
44
  }
40
45
 
@@ -46,6 +51,7 @@ export class QueryBuilder<T> {
46
51
  */
47
52
  eq(column: string, value: unknown): this {
48
53
  this.queryParams.append(column, `eq.${value}`);
54
+ if (column === 'id') this._idValue = value;
49
55
  return this;
50
56
  }
51
57
 
@@ -137,6 +143,32 @@ export class QueryBuilder<T> {
137
143
  return this;
138
144
  }
139
145
 
146
+ /**
147
+ * Negate a filter
148
+ */
149
+ not(column: string, operator: string, value?: unknown): this {
150
+ this.queryParams.append(column, `not.${operator}.${value ?? ''}`);
151
+ return this;
152
+ }
153
+
154
+ /**
155
+ * Filter by null or not null
156
+ */
157
+ is(column: string, value: null | 'not.null'): this {
158
+ if (value === null) return this.isNull(column);
159
+ return this.isNotNull(column);
160
+ }
161
+
162
+ /**
163
+ * Full-text search
164
+ */
165
+ textSearch(column: string, query: string, options?: { config?: string; type?: 'plain' | 'phrase' | 'websearch' }): this {
166
+ const prefix = options?.type === 'plain' ? 'plfts' : options?.type === 'phrase' ? 'phfts' : 'fts';
167
+ const configStr = options?.config ? `(${options.config})` : '';
168
+ this.queryParams.append(column, `${prefix}${configStr}.${query}`);
169
+ return this;
170
+ }
171
+
140
172
  /**
141
173
  * Order results
142
174
  * @example
@@ -196,29 +228,48 @@ export class QueryBuilder<T> {
196
228
 
197
229
  private getHeaders(): Record<string, string> {
198
230
  const token = this.accessToken || this.anonKey;
199
- return {
231
+ const headers: Record<string, string> = {
200
232
  'Content-Type': 'application/json',
201
233
  'apikey': this.anonKey,
202
234
  'Authorization': `Bearer ${token}`,
203
235
  ...this.headers,
204
236
  };
237
+ if (this._countOption) {
238
+ headers['Prefer'] = `count=${this._countOption}`;
239
+ }
240
+ return headers;
205
241
  }
206
242
 
207
243
  private async request<TResponse>(
208
244
  method: HttpMethod,
209
245
  body?: unknown
210
246
  ): Promise<PostgrestResponse<TResponse>> {
211
- const queryString = this.queryParams.toString();
212
- const fullUrl = `${this.url}/rest/v1/${this.tableName}${queryString ? `?${queryString}` : ''}`;
247
+ const effectiveMethod = this._head ? 'HEAD' : method;
248
+
249
+ let fullUrl: string;
250
+ if ((method === 'PATCH' || method === 'DELETE') && this._idValue !== undefined) {
251
+ const params = new URLSearchParams(this.queryParams);
252
+ params.delete('id');
253
+ const qs = params.toString();
254
+ fullUrl = `${this.url}/api/${this.tableName}/${this._idValue}/${qs ? `?${qs}` : ''}`;
255
+ } else {
256
+ const queryString = this.queryParams.toString();
257
+ fullUrl = `${this.url}/api/${this.tableName}/${queryString ? `?${queryString}` : ''}`;
258
+ }
213
259
 
214
260
  const options: RequestInit = {
215
- method,
261
+ method: effectiveMethod,
216
262
  headers: this.getHeaders(),
217
263
  };
218
264
 
219
- if (body && method !== 'GET') {
265
+ if (body && method !== 'GET' && method !== 'HEAD') {
220
266
  options.body = JSON.stringify(body);
221
- (options.headers as Record<string, string>)['Prefer'] = 'return=representation';
267
+ const hdrs = options.headers as Record<string, string>;
268
+ if (hdrs['Prefer']) {
269
+ hdrs['Prefer'] = hdrs['Prefer'] + ',return=representation';
270
+ } else {
271
+ hdrs['Prefer'] = 'return=representation';
272
+ }
222
273
  }
223
274
 
224
275
  try {
@@ -227,6 +278,24 @@ export class QueryBuilder<T> {
227
278
  let data: TResponse | null = null;
228
279
  let error: AuraBaseError | null = null;
229
280
 
281
+ // Parse Content-Range header for count
282
+ const contentRange = response.headers.get('content-range');
283
+ let count: number | null = null;
284
+ if (contentRange) {
285
+ const match = contentRange.match(/\/(\d+)$/);
286
+ if (match) count = parseInt(match[1], 10);
287
+ }
288
+
289
+ if (effectiveMethod === 'HEAD') {
290
+ return {
291
+ data: null,
292
+ error: null,
293
+ status: response.status,
294
+ statusText: response.statusText,
295
+ count,
296
+ };
297
+ }
298
+
230
299
  const text = await response.text();
231
300
 
232
301
  if (text) {
@@ -256,6 +325,7 @@ export class QueryBuilder<T> {
256
325
  error,
257
326
  status: response.status,
258
327
  statusText: response.statusText,
328
+ count,
259
329
  };
260
330
  } catch (err) {
261
331
  return {
@@ -265,6 +335,7 @@ export class QueryBuilder<T> {
265
335
  },
266
336
  status: 0,
267
337
  statusText: 'Network Error',
338
+ count: null,
268
339
  };
269
340
  }
270
341
  }
package/src/cli.ts CHANGED
@@ -29,13 +29,31 @@ export async function server() {
29
29
 
30
30
  return client;
31
31
  }
32
+ `,
33
+ 'lib/admin.ts': `import { createClient } from 'aurabase-js';
34
+
35
+ /**
36
+ * Admin client with service_role key
37
+ * ⚠️ ONLY use on server-side (API routes, server components)
38
+ * ⚠️ Bypasses all RLS policies - use with caution!
39
+ */
40
+ export const admin = createClient({
41
+ url: process.env.NEXT_PUBLIC_AURABASE_URL || '',
42
+ anonKey: process.env.AURABASE_SERVICE_ROLE_KEY || '',
43
+ });
32
44
  `,
33
45
  };
34
46
 
35
47
  const ENV_TEMPLATE = `
36
48
  # AuraBase Configuration (.env.local)
49
+
50
+ # Public keys (client + server)
37
51
  NEXT_PUBLIC_AURABASE_URL=https://your-project.cloudfront.net
38
52
  NEXT_PUBLIC_AURABASE_ANON_KEY=your-anon-key-here
53
+
54
+ # Service role key (server only - bypasses RLS!)
55
+ # ⚠️ NEVER expose this to the client!
56
+ AURABASE_SERVICE_ROLE_KEY=your-service-role-key-here
39
57
  `;
40
58
 
41
59
  function findProjectRoot(): string {
@@ -80,6 +98,7 @@ function init() {
80
98
 
81
99
  createFile(projectRoot, 'lib/client.ts');
82
100
  createFile(projectRoot, 'lib/server.ts');
101
+ createFile(projectRoot, 'lib/admin.ts');
83
102
 
84
103
  console.log('\x1b[36m%s\x1b[0m', ENV_TEMPLATE);
85
104
  }
@@ -96,8 +115,9 @@ function showHelp() {
96
115
  -v, --version Show version
97
116
 
98
117
  \x1b[1mGenerated files:\x1b[0m
99
- lib/client.ts Browser client
118
+ lib/client.ts Browser client (anon key)
100
119
  lib/server.ts Server client (with cookie auth)
120
+ lib/admin.ts Admin client (service role, bypasses RLS)
101
121
  `);
102
122
  }
103
123
 
package/src/types.ts CHANGED
@@ -38,6 +38,7 @@ export interface PostgrestResponse<T> {
38
38
  error: AuraBaseError | null;
39
39
  status: number;
40
40
  statusText: string;
41
+ count?: number | null;
41
42
  }
42
43
 
43
44
  export interface AuraBaseError {
@@ -1,6 +0,0 @@
1
- {
2
- "timestamp": "2026-03-09T12:13:53.326Z",
3
- "backgroundTasks": [],
4
- "sessionStartTimestamp": "2026-03-09T11:58:48.482Z",
5
- "sessionId": "faa7b8297096020e"
6
- }
@@ -1 +0,0 @@
1
- {"session_id":"1c1e8df6-335a-4058-8bd4-4c0a67d996cd","transcript_path":"C:\\Users\\Jay\\.claude\\projects\\D--000-FrontEnd-242-dino-game\\1c1e8df6-335a-4058-8bd4-4c0a67d996cd.jsonl","cwd":"D:\\000.FrontEnd\\242.dino_game\\packages\\aurabase-js","model":{"id":"GLM-5","display_name":"GLM-5"},"workspace":{"current_dir":"D:\\000.FrontEnd\\242.dino_game\\packages\\aurabase-js","project_dir":"D:\\000.FrontEnd\\242.dino_game","added_dirs":[]},"version":"2.1.71","output_style":{"name":"default"},"cost":{"total_cost_usd":3.1387229999999997,"total_duration_ms":1800053,"total_api_duration_ms":890803,"total_lines_added":356,"total_lines_removed":168},"context_window":{"total_input_tokens":80855,"total_output_tokens":22864,"context_window_size":200000,"current_usage":{"input_tokens":137,"output_tokens":64,"cache_creation_input_tokens":0,"cache_read_input_tokens":86976},"used_percentage":44,"remaining_percentage":56},"exceeds_200k_tokens":false}
package/lib/aurabase.ts DELETED
@@ -1,9 +0,0 @@
1
- import { createClient } from 'aurabase-js';
2
-
3
- const AURABASE_URL = process.env.NEXT_PUBLIC_AURABASE_URL || 'https://your-project.cloudfront.net';
4
- const AURABASE_ANON_KEY = process.env.NEXT_PUBLIC_AURABASE_ANON_KEY || '';
5
-
6
- export const aurabase = createClient({
7
- url: AURABASE_URL,
8
- anonKey: AURABASE_ANON_KEY,
9
- });
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "lib": ["ES2020", "DOM"],
6
- "declaration": true,
7
- "declarationMap": true,
8
- "sourceMap": true,
9
- "outDir": "./dist",
10
- "rootDir": "./src",
11
- "strict": true,
12
- "esModuleInterop": true,
13
- "skipLibCheck": true,
14
- "forceConsistentCasingInFileNames": true,
15
- "moduleResolution": "node",
16
- "resolveJsonModule": true
17
- },
18
- "include": ["src/**/*"],
19
- "exclude": ["node_modules", "dist"]
20
- }