WellNuo/backend/src/config/supabase.js
Sergei ec63a2c1e2 Add admin panel, optimized API, OTP auth, migrations
Admin Panel (Next.js):
- Dashboard with stats
- Users list with relationships (watches/watched_by)
- User detail pages
- Deployments list and detail pages
- Devices, Orders, Subscriptions pages
- OTP-based admin authentication

Backend Optimizations:
- Fixed N+1 query problem in admin APIs
- Added pagination support
- Added .range() and count support to Supabase wrapper
- Optimized batch queries with lookup maps

Database:
- Added migrations for schema evolution
- New tables: push_tokens, notification_settings
- Updated access model

iOS Build Scripts:
- build-ios.sh, clear-apple-cache.sh
- EAS configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-20 11:05:39 -08:00

393 lines
9.5 KiB
JavaScript

/**
* Supabase-like wrapper for direct PostgreSQL
* Provides similar API to @supabase/supabase-js but uses pg pool directly
*/
const { pool } = require('./database');
class QueryBuilder {
constructor(tableName) {
this.tableName = tableName;
this._select = '*';
this._where = [];
this._whereParams = [];
this._orderBy = null;
this._limit = null;
this._offset = null;
this._single = false;
this._returning = true;
this._count = false;
}
select(columns, options = {}) {
if (typeof columns === 'string') {
this._select = columns || '*';
} else {
this._select = '*';
}
if (options && options.count === 'exact') {
this._count = true;
}
return this;
}
eq(column, value) {
this._where.push(`"${column}" = $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
neq(column, value) {
this._where.push(`"${column}" != $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
gt(column, value) {
this._where.push(`"${column}" > $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
gte(column, value) {
this._where.push(`"${column}" >= $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
lt(column, value) {
this._where.push(`"${column}" < $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
lte(column, value) {
this._where.push(`"${column}" <= $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
is(column, value) {
if (value === null) {
this._where.push(`"${column}" IS NULL`);
} else {
this._where.push(`"${column}" IS $${this._whereParams.length + 1}`);
this._whereParams.push(value);
}
return this;
}
in(column, values) {
const placeholders = values.map((_, i) => `$${this._whereParams.length + i + 1}`).join(', ');
this._where.push(`"${column}" IN (${placeholders})`);
this._whereParams.push(...values);
return this;
}
order(column, options = {}) {
const direction = options.ascending === false ? 'DESC' : 'ASC';
this._orderBy = `"${column}" ${direction}`;
return this;
}
limit(count) {
this._limit = count;
return this;
}
range(from, to) {
this._offset = from;
this._limit = to - from + 1;
return this;
}
single() {
this._single = true;
this._limit = 1;
return this;
}
async _buildSelectQuery() {
let query = `SELECT ${this._select} FROM "${this.tableName}"`;
if (this._where.length > 0) {
query += ` WHERE ${this._where.join(' AND ')}`;
}
if (this._orderBy) {
query += ` ORDER BY ${this._orderBy}`;
}
if (this._limit) {
query += ` LIMIT ${this._limit}`;
}
if (this._offset) {
query += ` OFFSET ${this._offset}`;
}
return query;
}
async _buildCountQuery() {
let query = `SELECT COUNT(*) as count FROM "${this.tableName}"`;
if (this._where.length > 0) {
query += ` WHERE ${this._where.join(' AND ')}`;
}
return query;
}
async then(resolve, reject) {
try {
const query = await this._buildSelectQuery();
const result = await pool.query(query, this._whereParams);
let count = null;
if (this._count) {
const countQuery = await this._buildCountQuery();
const countResult = await pool.query(countQuery, this._whereParams);
count = parseInt(countResult.rows[0]?.count || 0);
}
if (this._single) {
if (result.rows.length === 0) {
resolve({ data: null, error: { code: 'PGRST116', message: 'No rows returned' }, count });
} else {
resolve({ data: result.rows[0], error: null, count });
}
} else {
resolve({ data: result.rows, error: null, count });
}
} catch (error) {
resolve({ data: null, error: { code: error.code, message: error.message, details: error.detail, hint: error.hint }, count: null });
}
}
}
class InsertBuilder {
constructor(tableName, data) {
this.tableName = tableName;
this.data = Array.isArray(data) ? data : [data];
this._select = null;
this._single = false;
}
select(columns) {
this._select = columns || '*';
return this;
}
single() {
this._single = true;
return this;
}
async then(resolve, reject) {
try {
const record = this.data[0];
const columns = Object.keys(record);
const values = Object.values(record);
const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
let query = `INSERT INTO "${this.tableName}" ("${columns.join('", "')}") VALUES (${placeholders})`;
if (this._select) {
query += ` RETURNING ${this._select}`;
} else {
query += ' RETURNING *';
}
const result = await pool.query(query, values);
if (this._single) {
resolve({ data: result.rows[0] || null, error: null });
} else {
resolve({ data: result.rows, error: null });
}
} catch (error) {
resolve({ data: null, error: { code: error.code, message: error.message, details: error.detail, hint: error.hint } });
}
}
}
class UpdateBuilder {
constructor(tableName, data) {
this.tableName = tableName;
this.data = data;
this._where = [];
this._whereParams = [];
this._select = null;
this._single = false;
}
eq(column, value) {
this._where.push({ column, value });
return this;
}
select(columns) {
this._select = columns || '*';
return this;
}
single() {
this._single = true;
return this;
}
async then(resolve, reject) {
try {
const columns = Object.keys(this.data);
const values = Object.values(this.data);
const setClause = columns.map((col, i) => `"${col}" = $${i + 1}`).join(', ');
let paramIndex = columns.length + 1;
const whereClause = this._where.map(w => {
values.push(w.value);
return `"${w.column}" = $${paramIndex++}`;
}).join(' AND ');
let query = `UPDATE "${this.tableName}" SET ${setClause}`;
if (whereClause) {
query += ` WHERE ${whereClause}`;
}
if (this._select) {
query += ` RETURNING ${this._select}`;
} else {
query += ' RETURNING *';
}
const result = await pool.query(query, values);
if (this._single) {
resolve({ data: result.rows[0] || null, error: null });
} else {
resolve({ data: result.rows, error: null });
}
} catch (error) {
resolve({ data: null, error: { code: error.code, message: error.message, details: error.detail, hint: error.hint } });
}
}
}
class UpsertBuilder {
constructor(tableName, data, options = {}) {
this.tableName = tableName;
this.data = Array.isArray(data) ? data : [data];
this.onConflict = options.onConflict;
this._select = null;
this._single = false;
}
select(columns) {
this._select = columns || '*';
return this;
}
single() {
this._single = true;
return this;
}
async then(resolve, reject) {
try {
const record = this.data[0];
const columns = Object.keys(record);
const values = Object.values(record);
const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
const updateClause = columns
.filter(col => col !== this.onConflict)
.map(col => `"${col}" = EXCLUDED."${col}"`)
.join(', ');
let query = `INSERT INTO "${this.tableName}" ("${columns.join('", "')}") VALUES (${placeholders})`;
query += ` ON CONFLICT ("${this.onConflict}") DO UPDATE SET ${updateClause}`;
query += ' RETURNING *';
const result = await pool.query(query, values);
if (this._single) {
resolve({ data: result.rows[0] || null, error: null });
} else {
resolve({ data: result.rows, error: null });
}
} catch (error) {
resolve({ data: null, error: { code: error.code, message: error.message, details: error.detail, hint: error.hint } });
}
}
}
class DeleteBuilder {
constructor(tableName) {
this.tableName = tableName;
this._where = [];
this._whereParams = [];
}
eq(column, value) {
this._where.push(`"${column}" = $${this._whereParams.length + 1}`);
this._whereParams.push(value);
return this;
}
is(column, value) {
if (value === null) {
this._where.push(`"${column}" IS NULL`);
}
return this;
}
async then(resolve, reject) {
try {
let query = `DELETE FROM "${this.tableName}"`;
if (this._where.length > 0) {
query += ` WHERE ${this._where.join(' AND ')}`;
}
const result = await pool.query(query, this._whereParams);
resolve({ data: null, error: null, count: result.rowCount });
} catch (error) {
resolve({ data: null, error: { code: error.code, message: error.message, details: error.detail, hint: error.hint } });
}
}
}
class TableClient {
constructor(tableName) {
this.tableName = tableName;
}
select(columns) {
const builder = new QueryBuilder(this.tableName);
return builder.select(columns);
}
insert(data) {
return new InsertBuilder(this.tableName, data);
}
update(data) {
return new UpdateBuilder(this.tableName, data);
}
upsert(data, options) {
return new UpsertBuilder(this.tableName, data, options);
}
delete() {
return new DeleteBuilder(this.tableName);
}
}
// Supabase-compatible client
const supabase = {
from: (tableName) => new TableClient(tableName)
};
module.exports = { supabase };