@divetocode/supa-query-builder
Version:
unofficial supabase query builder
782 lines (781 loc) • 22.1 kB
JavaScript
// 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
};