Skip to content

Commit d84435f

Browse files
committed
Document @thesis-co/cent-supabase
1 parent 7564a32 commit d84435f

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,10 @@ console.log(btcRange.toString({ preferredUnit: "sat" })) // "100,000 sats - 1,00
613613

614614
## Other features
615615

616+
### Supabase Integration
617+
618+
For Supabase/PostgREST applications, see [`@thesis-co/cent-supabase`](./packages/cent-supabase) which automatically handles `DECIMAL`/`NUMERIC` columns, preventing JavaScript precision loss.
619+
616620
### Zod Integration
617621

618622
For input validation and parsing, see [`@thesis-co/cent-zod`](./packages/cent-zod) which provides Zod schemas for all `cent` types.

packages/cent-supabase/README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# @thesis-co/cent-supabase
2+
3+
Integration for `@thesis-co/cent` for easy storage and querying in
4+
Supabase.
5+
6+
## The problem
7+
8+
The Supabase client returns `DECIMAL` columns as JSON numbers, losing
9+
precision:
10+
11+
```typescript
12+
// Database stores: 19.99
13+
const { data } = await supabase.from('products').select('price').single()
14+
console.log(data.price) // 19.990000000000002
15+
```
16+
17+
This package wraps the Supabase client to cast money columns to text on the wire, then converts them to `Money` objects in your app.
18+
19+
## Installation
20+
21+
```bash
22+
npm install @thesis-co/cent-supabase @thesis-co/cent @supabase/supabase-js
23+
```
24+
25+
## Quick start
26+
27+
```typescript
28+
import { createCentSupabaseClient } from '@thesis-co/cent-supabase'
29+
import { Money } from '@thesis-co/cent'
30+
31+
const supabase = createCentSupabaseClient(
32+
process.env.SUPABASE_URL!,
33+
process.env.SUPABASE_ANON_KEY!,
34+
{
35+
tables: {
36+
products: {
37+
money: {
38+
// statically defined currencies (every price is in USD)
39+
price: { currencyCode: 'USD' },
40+
cost: { currencyCode: 'USD' }
41+
}
42+
},
43+
orders: {
44+
money: {
45+
// for dynamic currencies (each row has a
46+
// total and total_currency)
47+
total: { currencyColumn: 'total_currency' },
48+
tax: { currencyColumn: 'tax_currency' }
49+
}
50+
}
51+
}
52+
}
53+
)
54+
55+
// SELECT — returns Money objects
56+
const { data } = await supabase.from('products').select('*')
57+
console.log(data[0].price.toString()) // "$29.99"
58+
59+
// INSERT — accepts Money objects
60+
await supabase.from('orders').insert({
61+
total: Money('€150.00'),
62+
tax: Money('€15.00')
63+
// 'currency' column auto-populated as 'EUR'
64+
})
65+
66+
// Aggregates work too
67+
const { data: stats } = await supabase.from('orders').select('sum(total)').single()
68+
console.log(stats.sum.toString()) // "$1,234.56"
69+
```
70+
71+
## Configuration
72+
73+
### Static currency
74+
75+
When all rows use the same currency:
76+
77+
```typescript
78+
products: {
79+
money: {
80+
price: { currencyCode: 'USD' }
81+
}
82+
}
83+
```
84+
85+
### Dynamic currency
86+
87+
When currency varies per row (stored in another column):
88+
89+
```typescript
90+
orders: {
91+
money: {
92+
total: { currencyColumn: 'currency' }
93+
}
94+
}
95+
```
96+
97+
On insert, the currency column is auto-populated from the Money object.
98+
99+
### Minor units
100+
101+
When storing cents, satoshis, or wei as integers:
102+
103+
```typescript
104+
transactions: {
105+
money: {
106+
amount_sats: { currencyCode: 'BTC', minorUnits: true }
107+
}
108+
}
109+
// Database: 150000000 → Money("1.5 BTC")
110+
```
111+
112+
## Realtime
113+
114+
Subscriptions automatically transform payloads:
115+
116+
```typescript
117+
supabase
118+
.channel('orders')
119+
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'orders' }, (payload) => {
120+
console.log(payload.new.total.toString()) // Money object
121+
})
122+
.subscribe()
123+
```
124+
125+
## Helper functions
126+
127+
For RPC results or manual transformations:
128+
129+
```typescript
130+
import { parseMoneyResult, serializeMoney, moneySelect } from '@thesis-co/cent-supabase'
131+
132+
// Transform RPC results
133+
const { data } = await supabase.rpc('calculate_total', { order_id: '...' })
134+
const result = parseMoneyResult(data, { total: { currencyCode: 'USD' } })
135+
136+
// Serialize Money for custom mutations
137+
const serialized = serializeMoney({ price: Money('$99.99') }, { price: { currencyCode: 'USD' } })
138+
// { price: '99.99' }
139+
140+
// Build select string with casts
141+
moneySelect('id, name, price', ['price']) // "id, name, price::text"
142+
```
143+
144+
## Limitations
145+
146+
- **Nested relations**: Money columns in nested selects (e.g., `orders(items(price))`) aren't auto-transformed. Use `parseMoneyResult` on nested data.
147+
- **Computed expressions**: Use explicit `::text` cast: `.select('(price * qty)::text as subtotal')`
148+
- **RPC functions**: Transform results with `parseMoneyResult`
149+
150+
## Database Schema
151+
152+
Use `DECIMAL`/`NUMERIC`, not PostgreSQL's `MONEY` type:
153+
154+
```sql
155+
CREATE TABLE orders (
156+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
157+
total DECIMAL(19,4) NOT NULL,
158+
currency TEXT NOT NULL,
159+
created_at TIMESTAMPTZ DEFAULT now()
160+
)
161+
```
162+
163+
| Use Case | PostgreSQL Type |
164+
|----------|-----------------|
165+
| USD, EUR | `DECIMAL(19,4)` |
166+
| BTC (8 decimals) | `DECIMAL(28,8)` |
167+
| ETH (18 decimals) | `DECIMAL(38,18)` |
168+
| Minor units | `BIGINT` |

0 commit comments

Comments
 (0)