UNPKG

@divetocode/supa-query-builder

Version:
782 lines (781 loc) 22.1 kB
// src/index.ts var SupabaseClient = class { constructor(url, apiKey, options = {}) { this.url = url; this.apiKey = apiKey; this.options = options; } from(table) { return new SupabaseQueryBuilder( table, this.url, this.apiKey, this.apiKey, // authKey도 apiKey 사용 this.options ); } // RLS 정책 조회 (읽기 전용) rls(table) { return new SupabaseRLSReader( table, this.url, this.apiKey, this.options ); } }; var SupabaseServer = class { constructor(url, apiKey, serverKey, options = {}) { this.url = url; this.apiKey = apiKey; this.serverKey = serverKey; this.options = options; } from(table) { return new SupabaseQueryBuilder( table, this.url, this.apiKey, this.serverKey, // authKey는 serverKey 사용 this.options ); } // RLS 정책 관리 (생성/수정/삭제 가능) rls(table) { return new SupabaseRLSManager( table, this.url, this.apiKey, this.serverKey, this.options ); } // 스키마 관리 (테이블 CRUD) schema() { return new SupabaseSchemaManager( this.url, this.apiKey, this.serverKey, this.options ); } }; var SupabaseSchemaManager = class { constructor(url, apiKey, serverKey, options = {}) { this.url = url; this.apiKey = apiKey; this.serverKey = serverKey; this.options = options; } // 모든 테이블 목록 조회 async getTables(schema = "public") { try { const response = await this.executeRPC("get_tables_info", { target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 특정 테이블 정보 조회 (컬럼 정보 포함) async getTableInfo(tableName, schema = "public") { try { const response = await this.executeRPC("get_table_info", { table_name: tableName, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } async reloadSchemaCache() { await this.executeRPC("request_schema_cache_reload", {}); } async waitUntilVisible(tableName, schema = "public", tries = 5, delayMs = 300) { for (let i = 0; i < tries; i++) { const { data, error } = await this.executeRPC("table_exists", { table_name: tableName, target_schema: schema }); if (!error && (data === true || (data == null ? void 0 : data.exists) === true)) return true; await new Promise((res) => setTimeout(res, delayMs)); } return false; } // 테이블 생성 async createTable(tableName, columns, options) { var _a, _b; try { const schema = (options == null ? void 0 : options.schema) || "public"; const resp = await this.executeRPC("create_table", { table_name: tableName, columns, schema, enable_rls: (options == null ? void 0 : options.enableRLS) || false, add_created_at: (_a = options == null ? void 0 : options.addCreatedAt) != null ? _a : true, add_updated_at: (_b = options == null ? void 0 : options.addUpdatedAt) != null ? _b : true }); if (resp.error) throw resp.error; await this.reloadSchemaCache(); await this.waitUntilVisible(tableName, schema, 8, 250); return resp; } catch (error) { const msg = (error == null ? void 0 : error.message) || String(error); if (msg.includes("PGRST205")) { return { data: null, error: new Error(`Schema cache not updated yet for ${tableName}. Try again shortly.`) }; } return { data: null, error }; } } // 테이블 삭제 async dropTable(tableName, schema = "public", cascade = false) { try { const response = await this.executeRPC("drop_table", { table_name: tableName, target_schema: schema, cascade }); return response; } catch (error) { return { data: null, error }; } } // 컬럼 추가 async addColumn(tableName, column, schema = "public") { try { const response = await this.executeRPC("add_column", { table_name: tableName, column, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 컬럼 수정 async alterColumn(tableName, columnName, changes, schema = "public") { try { const response = await this.executeRPC("alter_column", { table_name: tableName, column_name: columnName, changes, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 컬럼 삭제 async dropColumn(tableName, columnName, schema = "public") { try { const response = await this.executeRPC("drop_column", { table_name: tableName, column_name: columnName, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 테이블 이름 변경 async renameTable(oldName, newName, schema = "public") { try { const response = await this.executeRPC("rename_table", { old_name: oldName, new_name: newName, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 컬럼 이름 변경 async renameColumn(tableName, oldColumnName, newColumnName, schema = "public") { try { const response = await this.executeRPC("rename_column", { table_name: tableName, old_column_name: oldColumnName, new_column_name: newColumnName, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 인덱스 생성 async createIndex(tableName, indexName, columns, options) { try { const response = await this.executeRPC("create_index", { table_name: tableName, index_name: indexName, columns, unique: (options == null ? void 0 : options.unique) || false, method: (options == null ? void 0 : options.method) || "btree", target_schema: (options == null ? void 0 : options.schema) || "public" }); return response; } catch (error) { return { data: null, error }; } } // 인덱스 삭제 async dropIndex(indexName, schema = "public") { try { const response = await this.executeRPC("drop_index", { index_name: indexName, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 테이블 복사 (구조만 또는 데이터 포함) async copyTable(sourceTable, targetTable, includeData = false, schema = "public") { try { const response = await this.executeRPC("copy_table", { source_table: sourceTable, target_table: targetTable, include_data: includeData, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } // 테이블이 존재하는지 확인 async tableExists(tableName, schema = "public") { try { const response = await this.executeRPC("table_exists", { table_name: tableName, target_schema: schema }); return response; } catch (error) { return { data: null, error }; } } async executeRPC(functionName, params) { try { const url = `${this.url}/rest/v1/rpc/${functionName}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.serverKey}`, // SERVER_ROLE 키 사용 "apikey": this.apiKey }, body: JSON.stringify(params) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); return { data, error: null }; } catch (error) { return { data: null, error }; } } }; var SupabaseRLSReader = class { constructor(table, url, apiKey, options = {}) { this.table = table; this.url = url; this.apiKey = apiKey; this.options = options; } // 테이블 접근 권한 테스트 (실제 데이터 조회 시도) async testAccess() { try { const url = `${this.url}/rest/v1/${this.table}?select=*&limit=0`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.apiKey}`, "apikey": this.apiKey } }); return { data: { canAccess: response.ok, status: response.status, message: response.ok ? "Access granted" : "Access denied" }, error: null }; } catch (error) { return { data: null, error }; } } // RPC 함수 호출을 통한 정책 조회 async getPolicies() { try { const response = await this.executeRPC("get_table_policies", { table_name: this.table }); return response; } catch (error) { return { data: null, error }; } } async executeRPC(functionName, params) { try { const url = `${this.url}/rest/v1/rpc/${functionName}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.apiKey}`, "apikey": this.apiKey }, body: JSON.stringify(params) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); return { data, error: null }; } catch (error) { return { data: null, error }; } } }; var SupabaseRLSManager = class { constructor(table, url, apiKey, serverKey, options = {}) { this.table = table; this.url = url; this.apiKey = apiKey; this.serverKey = serverKey; this.options = options; } // RLS 활성화 async enableRLS() { try { const response = await this.executeRPC("enable_rls", { table_name: this.table }); return response; } catch (error) { return { data: null, error }; } } // RLS 비활성화 async disableRLS() { try { const response = await this.executeRPC("disable_rls", { table_name: this.table }); return response; } catch (error) { return { data: null, error }; } } // 정책 생성 async createPolicy(policyName, operation, condition) { try { const response = await this.executeRPC("create_rls_policy", { table_name: this.table, policy_name: policyName, operation, condition }); return response; } catch (error) { return { data: null, error }; } } // 공개 읽기 정책 생성 (is_public = true) async createPublicReadPolicy() { return this.createPolicy( `${this.table}_public_read`, "SELECT", "is_public = true" ); } // 전체 공개 정책 생성 (보안 주의!) async createOpenPolicy() { return this.createPolicy( `${this.table}_open_access`, "ALL", "true" ); } // 정책 삭제 async dropPolicy(policyName) { try { const response = await this.executeRPC("drop_rls_policy", { table_name: this.table, policy_name: policyName }); return response; } catch (error) { return { data: null, error }; } } // 정책 목록 조회 async getPolicies() { try { const response = await this.executeRPC("get_table_policies", { table_name: this.table }); return response; } catch (error) { return { data: null, error }; } } // RLS 상태 확인 async checkRLSStatus() { try { const response = await this.executeRPC("check_rls_status", { table_name: this.table }); return response; } catch (error) { return { data: null, error }; } } async executeRPC(functionName, params) { try { const url = `${this.url}/rest/v1/rpc/${functionName}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.serverKey}`, // SERVER_ROLE 키 사용 "apikey": this.apiKey }, body: JSON.stringify(params) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); return { data, error: null }; } catch (error) { return { data: null, error }; } } }; var SupabaseQueryBuilder = class { constructor(table, url, apiKey, authKey, options = {}) { this.table = table; this.url = url; this.apiKey = apiKey; this.authKey = authKey; this.options = options; } select(columns = "*") { return new SupabaseSelectBuilder(this.table, this.url, this.apiKey, this.authKey, columns); } insert(data) { return new SupabaseInsertBuilder(this.table, this.url, this.apiKey, this.authKey, data); } update(data) { return new SupabaseUpdateBuilder(this.table, this.url, this.apiKey, this.authKey, data); } delete() { return new SupabaseDeleteBuilder(this.table, this.url, this.apiKey, this.authKey); } }; var SupabaseSelectBuilder = class { constructor(table, url, apiKey, authKey, columns, options) { this.table = table; this.url = url; this.apiKey = apiKey; this.authKey = authKey; this.columns = columns; this.filters = []; this.orderClause = ""; this.singleRow = false; this.headOnly = false; var _a; this.countMode = options == null ? void 0 : options.count; this.headOnly = (_a = options == null ? void 0 : options.head) != null ? _a : false; } // ====== 필터들 ====== eq(column, value) { this.filters.push([column, `eq.${value}`]); return this; } neq(column, value) { this.filters.push([column, `neq.${value}`]); return this; } gt(column, value) { this.filters.push([column, `gt.${value}`]); return this; } gte(column, value) { this.filters.push([column, `gte.${value}`]); return this; } lt(column, value) { this.filters.push([column, `lt.${value}`]); return this; } lte(column, value) { this.filters.push([column, `lte.${value}`]); return this; } // not: op에는 eq/neq/gt/gte/lt/lte/like/ilike/is/in 등 PostgREST 연산자 사용 not(column, operator, value) { this.filters.push([column, `not.${operator}.${value}`]); return this; } // values는 원시값 배열 in(column, values) { const list = values.join(","); this.filters.push([column, `in.(${list})`]); return this; } like(column, pattern) { this.filters.push([column, `like.${pattern}`]); return this; } ilike(column, pattern) { this.filters.push([column, `ilike.${pattern}`]); return this; } // 복합 OR or(query) { this.filters.push(["or", `(${query})`]); return this; } order(column, options) { const direction = (options == null ? void 0 : options.ascending) === false ? ".desc" : ".asc"; this.orderClause = `${column}${direction}`; return this; } // 페이징(쿼리 파라미터 버전) limit(n) { this.limitCount = n; return this; } offset(n) { this.offsetCount = n; return this; } // Range 헤더 기반 페이징 range(from, to) { this.rangeFrom = from; this.rangeTo = to; return this; } single() { this.singleRow = true; return this; } async then(resolve) { try { let url = `${this.url}/rest/v1/${this.table}`; const params = new URLSearchParams(); params.append("select", this.columns || "*"); for (const [k, v] of this.filters) { params.append(k, v); } if (this.orderClause) params.append("order", this.orderClause); if (this.limitCount !== void 0 && this.rangeFrom === void 0) { params.append("limit", `${this.limitCount}`); } if (this.offsetCount !== void 0 && this.rangeFrom === void 0) { params.append("offset", `${this.offsetCount}`); } if (params.toString()) url += "?" + params.toString(); const headers = { "Content-Type": "application/json", Authorization: `Bearer ${this.authKey}`, apikey: this.apiKey }; if (this.countMode) headers["Prefer"] = `count=${this.countMode}`; if (this.rangeFrom !== void 0 && this.rangeTo !== void 0) { headers["Range-Unit"] = "items"; headers["Range"] = `${this.rangeFrom}-${this.rangeTo}`; } if (this.singleRow) { headers["Accept"] = "application/vnd.pgrst.object+json"; } if (this.headOnly) { const response2 = await fetch(url, { method: "HEAD", headers }); if (!response2.ok) { const errorText = await response2.text(); throw new Error(`HTTP ${response2.status}: ${errorText}`); } const cr = response2.headers.get("Content-Range") || ""; const totalStr = cr.split("/")[1] || "0"; const total = Number(totalStr); resolve({ data: { length: Number.isFinite(total) ? total : 0 }, error: null }); return; } const response = await fetch(url, { method: "GET", headers }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); resolve({ data, error: null }); } catch (error) { resolve({ data: null, error }); } } }; var SupabaseInsertBuilder = class { constructor(table, url, apiKey, authKey, data) { this.table = table; this.url = url; this.apiKey = apiKey; this.authKey = authKey; this.data = data; this.selectColumns = ""; } select(columns = "") { this.selectColumns = columns; return this; } single() { return this; } async then(resolve) { try { let url = `${this.url}/rest/v1/${this.table}`; if (this.selectColumns) url += `?select=${this.selectColumns}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.authKey}`, "apikey": this.apiKey, "Prefer": "return=representation" }, body: JSON.stringify(this.data) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); resolve({ data: Array.isArray(data) ? data[0] : data, error: null }); } catch (error) { resolve({ data: null, error }); } } }; var SupabaseUpdateBuilder = class { constructor(table, url, apiKey, authKey, data) { this.table = table; this.url = url; this.apiKey = apiKey; this.authKey = authKey; this.data = data; this.whereClause = ""; this.selectColumns = ""; } eq(column, value) { this.whereClause = `${column}=eq.${encodeURIComponent(value)}`; return this; } select(columns = "") { this.selectColumns = columns; return this; } single() { return this; } async then(resolve) { try { let url = `${this.url}/rest/v1/${this.table}`; const params = new URLSearchParams(); if (this.whereClause) params.append(this.whereClause.split("=")[0], this.whereClause.split("=").slice(1).join("=")); if (this.selectColumns) params.append("select", this.selectColumns); if (params.toString()) url += "?" + params.toString(); const response = await fetch(url, { method: "PATCH", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.authKey}`, "apikey": this.apiKey, "Prefer": "return=representation" }, body: JSON.stringify(this.data) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); resolve({ data: Array.isArray(data) ? data[0] : data, error: null }); } catch (error) { resolve({ data: null, error }); } } }; var SupabaseDeleteBuilder = class { constructor(table, url, apiKey, authKey) { this.table = table; this.url = url; this.apiKey = apiKey; this.authKey = authKey; this.whereClause = ""; } eq(column, value) { this.whereClause = `${column}=eq.${encodeURIComponent(value)}`; return this; } async then(resolve) { try { let url = `${this.url}/rest/v1/${this.table}`; if (this.whereClause) url += "?" + this.whereClause; const response = await fetch(url, { method: "DELETE", headers: { "Authorization": `Bearer ${this.authKey}`, "apikey": this.apiKey } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } resolve({ data: null, error: null }); } catch (error) { resolve({ data: null, error }); } } }; export { SupabaseClient, SupabaseDeleteBuilder, SupabaseInsertBuilder, SupabaseQueryBuilder, SupabaseRLSManager, SupabaseRLSReader, SupabaseSchemaManager, SupabaseSelectBuilder, SupabaseServer, SupabaseUpdateBuilder };