@surf-ai/sdk 0.1.0
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/README.md +209 -0
- package/dist/chunk-J4OMYO3F.js +70 -0
- package/dist/client-3YMIRPDV.js +8 -0
- package/dist/db/index.cjs +112 -0
- package/dist/db/index.d.cts +48 -0
- package/dist/db/index.d.ts +48 -0
- package/dist/db/index.js +81 -0
- package/dist/react/index.d.ts +3382 -0
- package/dist/react/index.js +980 -0
- package/dist/server/index.cjs +798 -0
- package/dist/server/index.d.cts +3342 -0
- package/dist/server/index.d.ts +3342 -0
- package/dist/server/index.js +677 -0
- package/package.json +58 -0
- package/src/theme/index.css +314 -0
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @surf-ai/sdk
|
|
2
|
+
|
|
3
|
+
Surf platform SDK — data API client, Express server runtime, React hooks, and database helpers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @surf-ai/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Frontend (React hooks)
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useMarketPrice, cn, useToast } from '@surf-ai/sdk/react'
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
const { data, isLoading } = useMarketPrice({ symbol: 'BTC', time_range: '1d' })
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn('p-4', isLoading && 'opacity-50')}>
|
|
22
|
+
BTC: ${data?.data?.[0]?.value}
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Backend (data API)
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { dataApi } = require('@surf-ai/sdk/server')
|
|
32
|
+
|
|
33
|
+
// Typed methods grouped by category
|
|
34
|
+
const btc = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
|
|
35
|
+
const holders = await dataApi.token.holders({ address: '0x...', chain: 'ethereum' })
|
|
36
|
+
const trades = await dataApi.onchain.sql({ sql: 'SELECT ...', max_rows: 100 })
|
|
37
|
+
|
|
38
|
+
// Escape hatch for new endpoints
|
|
39
|
+
const data = await dataApi.get('newcategory/endpoint', { foo: 'bar' })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Backend (Express server)
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const { createServer } = require('@surf-ai/sdk/server')
|
|
46
|
+
|
|
47
|
+
// Starts Express with /proxy/*, route auto-loading, DB sync, cron, health check
|
|
48
|
+
createServer({ port: 3001 }).start()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Routes in `routes/*.js` are auto-mounted at `/api/{name}`:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// routes/btc.js → /api/btc
|
|
55
|
+
const { dataApi } = require('@surf-ai/sdk/server')
|
|
56
|
+
const router = require('express').Router()
|
|
57
|
+
|
|
58
|
+
router.get('/', async (req, res) => {
|
|
59
|
+
const data = await dataApi.market.price({ symbol: 'BTC' })
|
|
60
|
+
res.json(data)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
module.exports = router
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Subpath Exports
|
|
67
|
+
|
|
68
|
+
| Import | What |
|
|
69
|
+
|--------|------|
|
|
70
|
+
| `@surf-ai/sdk/server` | `createServer()`, `dataApi` — Express runtime + typed data API |
|
|
71
|
+
| `@surf-ai/sdk/react` | `useMarketPrice()`, `cn()`, `useToast()` — React hooks + utilities |
|
|
72
|
+
| `@surf-ai/sdk/db` | `dbQuery()`, `dbProvision()`, `dbTables()` — Drizzle/Neon database |
|
|
73
|
+
|
|
74
|
+
## Built-in Endpoints
|
|
75
|
+
|
|
76
|
+
`createServer()` provides these automatically:
|
|
77
|
+
|
|
78
|
+
| Endpoint | Method | Purpose |
|
|
79
|
+
|----------|--------|---------|
|
|
80
|
+
| `/api/health` | GET | Health check — `{ status: 'ok' }` |
|
|
81
|
+
| `/api/__sync-schema` | POST | Sync `db/schema.js` tables to database |
|
|
82
|
+
| `/api/cron` | GET | List cron jobs and status |
|
|
83
|
+
| `/api/cron` | POST | Update cron.json and reload |
|
|
84
|
+
| `/proxy/*` | ANY | Data API passthrough to hermod |
|
|
85
|
+
|
|
86
|
+
Routes in `routes/*.js` are auto-loaded as `/api/{filename}`.
|
|
87
|
+
|
|
88
|
+
DB schema is auto-synced on startup and when `db/schema.js` changes (file watcher).
|
|
89
|
+
|
|
90
|
+
## Environment Variables
|
|
91
|
+
|
|
92
|
+
The SDK auto-detects the runtime mode from environment variables. New prefixed names take priority; legacy names are supported for backward compatibility.
|
|
93
|
+
|
|
94
|
+
### Data API routing
|
|
95
|
+
|
|
96
|
+
| Env Var | Legacy | Mode | Set By |
|
|
97
|
+
|---------|--------|------|--------|
|
|
98
|
+
| `SURF_SANDBOX_PROXY_BASE` | `DATA_PROXY_BASE` | Sandbox | urania executor |
|
|
99
|
+
| `SURF_DEPLOYED_GATEWAY_URL` | `GATEWAY_URL` | Deployed | Bifrost |
|
|
100
|
+
| `SURF_DEPLOYED_APP_TOKEN` | `APP_TOKEN` | Deployed | Bifrost |
|
|
101
|
+
|
|
102
|
+
**Routing logic:**
|
|
103
|
+
```
|
|
104
|
+
if SURF_SANDBOX_PROXY_BASE → sandbox (OutboundProxy handles auth)
|
|
105
|
+
elif SURF_DEPLOYED_GATEWAY_URL + SURF_DEPLOYED_APP_TOKEN → deployed (hermod with Bearer)
|
|
106
|
+
else → public (api.ask.surf, no auth)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Server
|
|
110
|
+
|
|
111
|
+
| Env Var | Default | Purpose |
|
|
112
|
+
|---------|---------|---------|
|
|
113
|
+
| `PORT` | `3001` | Express listen port |
|
|
114
|
+
|
|
115
|
+
### Vite dev server (scaffold, not SDK)
|
|
116
|
+
|
|
117
|
+
| Env Var | Default | Purpose |
|
|
118
|
+
|---------|---------|---------|
|
|
119
|
+
| `VITE_PORT` | `5173` | Frontend dev server port |
|
|
120
|
+
| `VITE_BACKEND_PORT` | `3001` | Backend port for Vite proxy target |
|
|
121
|
+
|
|
122
|
+
### How routing works
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Sandbox (urania preview):
|
|
126
|
+
Frontend hook: useMarketPrice()
|
|
127
|
+
→ fetch /proxy/market/price (same-origin to Vite)
|
|
128
|
+
→ Vite proxy → Express /proxy/* → OutboundProxy (JWT) → hermod
|
|
129
|
+
|
|
130
|
+
Backend route: dataApi.market.price()
|
|
131
|
+
→ fetch SURF_SANDBOX_PROXY_BASE/market/price
|
|
132
|
+
→ OutboundProxy (JWT) → hermod
|
|
133
|
+
|
|
134
|
+
Deployed (surf.computer):
|
|
135
|
+
Frontend hook: useMarketPrice()
|
|
136
|
+
→ fetch /proxy/market/price (same-origin to Express)
|
|
137
|
+
→ Express /proxy/* → hermod (APP_TOKEN)
|
|
138
|
+
|
|
139
|
+
Backend route: dataApi.market.price()
|
|
140
|
+
→ fetch http://127.0.0.1:PORT/proxy/market/price (loopback)
|
|
141
|
+
→ Express /proxy/* → hermod (APP_TOKEN)
|
|
142
|
+
|
|
143
|
+
Public (local dev):
|
|
144
|
+
Frontend hook: useMarketPrice()
|
|
145
|
+
→ fetch /proxy/market/price (same-origin to Vite)
|
|
146
|
+
→ Vite proxy → Express /proxy/* → hermod (GATEWAY_URL + APP_TOKEN)
|
|
147
|
+
|
|
148
|
+
Backend route: dataApi.market.price()
|
|
149
|
+
→ fetch SURF_DEPLOYED_GATEWAY_URL/gateway/v1/market/price (Bearer APP_TOKEN)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Available Categories
|
|
153
|
+
|
|
154
|
+
| Category | Example Methods |
|
|
155
|
+
|----------|----------------|
|
|
156
|
+
| `market` | `price`, `ranking`, `etf`, `futures`, `options`, `fear_greed` |
|
|
157
|
+
| `token` | `holders`, `transfers`, `dex_trades`, `tokenomics` |
|
|
158
|
+
| `wallet` | `detail`, `net_worth`, `labels_batch`, `transfers` |
|
|
159
|
+
| `onchain` | `sql`, `tx`, `gas_price`, `schema`, `structured_query` |
|
|
160
|
+
| `social` | `detail`, `mindshare`, `tweets`, `user`, `ranking` |
|
|
161
|
+
| `project` | `detail`, `defi_metrics`, `defi_ranking` |
|
|
162
|
+
| `news` | `detail`, `feed` |
|
|
163
|
+
| `exchange` | `price`, `depth`, `klines`, `funding_history`, `perp` |
|
|
164
|
+
| `fund` | `detail`, `portfolio`, `ranking` |
|
|
165
|
+
| `search` | `project`, `news`, `wallet`, `web` |
|
|
166
|
+
| `web` | `fetch` |
|
|
167
|
+
| `polymarket` | `events`, `markets`, `prices`, `volumes` |
|
|
168
|
+
| `kalshi` | `events`, `markets`, `prices`, `volumes` |
|
|
169
|
+
| `prediction_market` | `category_metrics` |
|
|
170
|
+
|
|
171
|
+
## Codegen
|
|
172
|
+
|
|
173
|
+
API methods and React hooks are auto-generated from hermod's OpenAPI spec via the surf CLI:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Regenerate all endpoints
|
|
177
|
+
python scripts/gen_sdk.py
|
|
178
|
+
|
|
179
|
+
# Regenerate specific endpoints
|
|
180
|
+
python scripts/gen_sdk.py --ops market-price token-holders
|
|
181
|
+
|
|
182
|
+
# Build
|
|
183
|
+
bun run build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Requires `surf` CLI installed and authenticated (`surf login`).
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
bun install
|
|
192
|
+
bun run build # compile TypeScript
|
|
193
|
+
bun test # run tests
|
|
194
|
+
bun run codegen # regenerate from OpenAPI spec
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Testing
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Unit tests
|
|
201
|
+
bun test ./tests/data-client.test.ts
|
|
202
|
+
|
|
203
|
+
# E2E (auto-detects mode from env vars)
|
|
204
|
+
bun test ./tests/e2e-all-envs.test.ts
|
|
205
|
+
|
|
206
|
+
# E2E with specific mode:
|
|
207
|
+
SURF_SANDBOX_PROXY_BASE=http://127.0.0.1:9999/s/<session>/proxy bun test ./tests/e2e-all-envs.test.ts
|
|
208
|
+
SURF_DEPLOYED_GATEWAY_URL=https://api.ask.surf SURF_DEPLOYED_APP_TOKEN=<token> bun test ./tests/e2e-all-envs.test.ts
|
|
209
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/data/client.ts
|
|
9
|
+
var DEFAULT_PUBLIC_URL = "https://api.ask.surf/gateway/v1";
|
|
10
|
+
function env(surfName, legacyName) {
|
|
11
|
+
return process.env[surfName] || process.env[legacyName];
|
|
12
|
+
}
|
|
13
|
+
function resolveConfig() {
|
|
14
|
+
const proxyBase = env("SURF_SANDBOX_PROXY_BASE", "DATA_PROXY_BASE");
|
|
15
|
+
if (proxyBase) {
|
|
16
|
+
return { baseUrl: proxyBase, headers: {} };
|
|
17
|
+
}
|
|
18
|
+
const gatewayUrl = env("SURF_DEPLOYED_GATEWAY_URL", "GATEWAY_URL");
|
|
19
|
+
const appToken = env("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
20
|
+
if (gatewayUrl && appToken) {
|
|
21
|
+
return {
|
|
22
|
+
baseUrl: `${gatewayUrl.replace(/\/$/, "")}/gateway/v1`,
|
|
23
|
+
headers: { Authorization: `Bearer ${appToken}` }
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { baseUrl: DEFAULT_PUBLIC_URL, headers: {} };
|
|
27
|
+
}
|
|
28
|
+
function normalizePath(path) {
|
|
29
|
+
return String(path || "").replace(/^\/+/, "").replace(/^(?:proxy\/)+/, "");
|
|
30
|
+
}
|
|
31
|
+
async function fetchJson(url, init, retries = 1) {
|
|
32
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
33
|
+
const res = await fetch(url, init);
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text2 = await res.text();
|
|
36
|
+
throw new Error(`API error ${res.status}: ${text2.slice(0, 200)}`);
|
|
37
|
+
}
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
if (text) return JSON.parse(text);
|
|
40
|
+
if (attempt < retries) await new Promise((r) => setTimeout(r, 1e3));
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Empty response from ${url}`);
|
|
43
|
+
}
|
|
44
|
+
async function get(path, params) {
|
|
45
|
+
const config = resolveConfig();
|
|
46
|
+
const cleaned = {};
|
|
47
|
+
if (params) {
|
|
48
|
+
for (const [k, v] of Object.entries(params)) {
|
|
49
|
+
if (v != null) cleaned[k] = String(v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const qs = Object.keys(cleaned).length ? "?" + new URLSearchParams(cleaned).toString() : "";
|
|
53
|
+
return fetchJson(`${config.baseUrl}/${normalizePath(path)}${qs}`, {
|
|
54
|
+
headers: config.headers
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function post(path, body) {
|
|
58
|
+
const config = resolveConfig();
|
|
59
|
+
return fetchJson(`${config.baseUrl}/${normalizePath(path)}`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { ...config.headers, "Content-Type": "application/json" },
|
|
62
|
+
body: body ? JSON.stringify(body) : void 0
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
__require,
|
|
68
|
+
get,
|
|
69
|
+
post
|
|
70
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/db/index.ts
|
|
21
|
+
var db_exports = {};
|
|
22
|
+
__export(db_exports, {
|
|
23
|
+
dbProvision: () => dbProvision,
|
|
24
|
+
dbQuery: () => dbQuery,
|
|
25
|
+
dbStatus: () => dbStatus,
|
|
26
|
+
dbTableSchema: () => dbTableSchema,
|
|
27
|
+
dbTables: () => dbTables
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(db_exports);
|
|
30
|
+
|
|
31
|
+
// src/data/client.ts
|
|
32
|
+
var DEFAULT_PUBLIC_URL = "https://api.ask.surf/gateway/v1";
|
|
33
|
+
function env(surfName, legacyName) {
|
|
34
|
+
return process.env[surfName] || process.env[legacyName];
|
|
35
|
+
}
|
|
36
|
+
function resolveConfig() {
|
|
37
|
+
const proxyBase = env("SURF_SANDBOX_PROXY_BASE", "DATA_PROXY_BASE");
|
|
38
|
+
if (proxyBase) {
|
|
39
|
+
return { baseUrl: proxyBase, headers: {} };
|
|
40
|
+
}
|
|
41
|
+
const gatewayUrl = env("SURF_DEPLOYED_GATEWAY_URL", "GATEWAY_URL");
|
|
42
|
+
const appToken = env("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
43
|
+
if (gatewayUrl && appToken) {
|
|
44
|
+
return {
|
|
45
|
+
baseUrl: `${gatewayUrl.replace(/\/$/, "")}/gateway/v1`,
|
|
46
|
+
headers: { Authorization: `Bearer ${appToken}` }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { baseUrl: DEFAULT_PUBLIC_URL, headers: {} };
|
|
50
|
+
}
|
|
51
|
+
function normalizePath(path) {
|
|
52
|
+
return String(path || "").replace(/^\/+/, "").replace(/^(?:proxy\/)+/, "");
|
|
53
|
+
}
|
|
54
|
+
async function fetchJson(url, init, retries = 1) {
|
|
55
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
56
|
+
const res = await fetch(url, init);
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const text2 = await res.text();
|
|
59
|
+
throw new Error(`API error ${res.status}: ${text2.slice(0, 200)}`);
|
|
60
|
+
}
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
if (text) return JSON.parse(text);
|
|
63
|
+
if (attempt < retries) await new Promise((r) => setTimeout(r, 1e3));
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Empty response from ${url}`);
|
|
66
|
+
}
|
|
67
|
+
async function get(path, params) {
|
|
68
|
+
const config = resolveConfig();
|
|
69
|
+
const cleaned = {};
|
|
70
|
+
if (params) {
|
|
71
|
+
for (const [k, v] of Object.entries(params)) {
|
|
72
|
+
if (v != null) cleaned[k] = String(v);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const qs = Object.keys(cleaned).length ? "?" + new URLSearchParams(cleaned).toString() : "";
|
|
76
|
+
return fetchJson(`${config.baseUrl}/${normalizePath(path)}${qs}`, {
|
|
77
|
+
headers: config.headers
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function post(path, body) {
|
|
81
|
+
const config = resolveConfig();
|
|
82
|
+
return fetchJson(`${config.baseUrl}/${normalizePath(path)}`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { ...config.headers, "Content-Type": "application/json" },
|
|
85
|
+
body: body ? JSON.stringify(body) : void 0
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/db/index.ts
|
|
90
|
+
async function dbProvision() {
|
|
91
|
+
return post("db/provision");
|
|
92
|
+
}
|
|
93
|
+
async function dbQuery(sql, params, options) {
|
|
94
|
+
return post("db/query", { sql, params, method: options?.arrayMode ? "all" : "execute" });
|
|
95
|
+
}
|
|
96
|
+
async function dbTables() {
|
|
97
|
+
return get("db/tables");
|
|
98
|
+
}
|
|
99
|
+
async function dbTableSchema(table) {
|
|
100
|
+
return get("db/table-schema", { table });
|
|
101
|
+
}
|
|
102
|
+
async function dbStatus() {
|
|
103
|
+
return get("db/status");
|
|
104
|
+
}
|
|
105
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
106
|
+
0 && (module.exports = {
|
|
107
|
+
dbProvision,
|
|
108
|
+
dbQuery,
|
|
109
|
+
dbStatus,
|
|
110
|
+
dbTableSchema,
|
|
111
|
+
dbTables
|
|
112
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @surf-ai/sdk/db — Drizzle ORM + Neon PostgreSQL database helpers.
|
|
3
|
+
*
|
|
4
|
+
* Replaces scaffold lib/db.js and db/index.js.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { db, dbQuery, dbProvision } = require('@surf-ai/sdk/db')
|
|
8
|
+
*
|
|
9
|
+
* // In a backend route:
|
|
10
|
+
* const users = await db.select().from(schema.users)
|
|
11
|
+
*
|
|
12
|
+
* // Raw SQL:
|
|
13
|
+
* const result = await dbQuery('SELECT * FROM users WHERE id = $1', [userId])
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Provision a database for the current user via /proxy/db/provision.
|
|
17
|
+
* Returns connection metadata. Neon auto-creates the DB if it doesn't exist.
|
|
18
|
+
*/
|
|
19
|
+
declare function dbProvision(): Promise<{
|
|
20
|
+
host: string;
|
|
21
|
+
database: string;
|
|
22
|
+
user: string;
|
|
23
|
+
password: string;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Execute a SQL query via /proxy/db/query.
|
|
27
|
+
* Uses pg-proxy driver under the hood — Drizzle ORM calls this automatically.
|
|
28
|
+
*/
|
|
29
|
+
declare function dbQuery(sql: string, params?: any[], options?: {
|
|
30
|
+
arrayMode?: boolean;
|
|
31
|
+
}): Promise<any>;
|
|
32
|
+
/**
|
|
33
|
+
* List tables in the user's database.
|
|
34
|
+
*/
|
|
35
|
+
declare function dbTables(): Promise<string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Get schema for a specific table.
|
|
38
|
+
*/
|
|
39
|
+
declare function dbTableSchema(table: string): Promise<any>;
|
|
40
|
+
/**
|
|
41
|
+
* Check database connection status.
|
|
42
|
+
*/
|
|
43
|
+
declare function dbStatus(): Promise<{
|
|
44
|
+
connected: boolean;
|
|
45
|
+
database?: string;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
export { dbProvision, dbQuery, dbStatus, dbTableSchema, dbTables };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @surf-ai/sdk/db — Drizzle ORM + Neon PostgreSQL database helpers.
|
|
3
|
+
*
|
|
4
|
+
* Replaces scaffold lib/db.js and db/index.js.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { db, dbQuery, dbProvision } = require('@surf-ai/sdk/db')
|
|
8
|
+
*
|
|
9
|
+
* // In a backend route:
|
|
10
|
+
* const users = await db.select().from(schema.users)
|
|
11
|
+
*
|
|
12
|
+
* // Raw SQL:
|
|
13
|
+
* const result = await dbQuery('SELECT * FROM users WHERE id = $1', [userId])
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Provision a database for the current user via /proxy/db/provision.
|
|
17
|
+
* Returns connection metadata. Neon auto-creates the DB if it doesn't exist.
|
|
18
|
+
*/
|
|
19
|
+
declare function dbProvision(): Promise<{
|
|
20
|
+
host: string;
|
|
21
|
+
database: string;
|
|
22
|
+
user: string;
|
|
23
|
+
password: string;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Execute a SQL query via /proxy/db/query.
|
|
27
|
+
* Uses pg-proxy driver under the hood — Drizzle ORM calls this automatically.
|
|
28
|
+
*/
|
|
29
|
+
declare function dbQuery(sql: string, params?: any[], options?: {
|
|
30
|
+
arrayMode?: boolean;
|
|
31
|
+
}): Promise<any>;
|
|
32
|
+
/**
|
|
33
|
+
* List tables in the user's database.
|
|
34
|
+
*/
|
|
35
|
+
declare function dbTables(): Promise<string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Get schema for a specific table.
|
|
38
|
+
*/
|
|
39
|
+
declare function dbTableSchema(table: string): Promise<any>;
|
|
40
|
+
/**
|
|
41
|
+
* Check database connection status.
|
|
42
|
+
*/
|
|
43
|
+
declare function dbStatus(): Promise<{
|
|
44
|
+
connected: boolean;
|
|
45
|
+
database?: string;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
export { dbProvision, dbQuery, dbStatus, dbTableSchema, dbTables };
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/data/client.ts
|
|
2
|
+
var DEFAULT_PUBLIC_URL = "https://api.ask.surf/gateway/v1";
|
|
3
|
+
function env(surfName, legacyName) {
|
|
4
|
+
return process.env[surfName] || process.env[legacyName];
|
|
5
|
+
}
|
|
6
|
+
function resolveConfig() {
|
|
7
|
+
const proxyBase = env("SURF_SANDBOX_PROXY_BASE", "DATA_PROXY_BASE");
|
|
8
|
+
if (proxyBase) {
|
|
9
|
+
return { baseUrl: proxyBase, headers: {} };
|
|
10
|
+
}
|
|
11
|
+
const gatewayUrl = env("SURF_DEPLOYED_GATEWAY_URL", "GATEWAY_URL");
|
|
12
|
+
const appToken = env("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
13
|
+
if (gatewayUrl && appToken) {
|
|
14
|
+
return {
|
|
15
|
+
baseUrl: `${gatewayUrl.replace(/\/$/, "")}/gateway/v1`,
|
|
16
|
+
headers: { Authorization: `Bearer ${appToken}` }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { baseUrl: DEFAULT_PUBLIC_URL, headers: {} };
|
|
20
|
+
}
|
|
21
|
+
function normalizePath(path) {
|
|
22
|
+
return String(path || "").replace(/^\/+/, "").replace(/^(?:proxy\/)+/, "");
|
|
23
|
+
}
|
|
24
|
+
async function fetchJson(url, init, retries = 1) {
|
|
25
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
26
|
+
const res = await fetch(url, init);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const text2 = await res.text();
|
|
29
|
+
throw new Error(`API error ${res.status}: ${text2.slice(0, 200)}`);
|
|
30
|
+
}
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
if (text) return JSON.parse(text);
|
|
33
|
+
if (attempt < retries) await new Promise((r) => setTimeout(r, 1e3));
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Empty response from ${url}`);
|
|
36
|
+
}
|
|
37
|
+
async function get(path, params) {
|
|
38
|
+
const config = resolveConfig();
|
|
39
|
+
const cleaned = {};
|
|
40
|
+
if (params) {
|
|
41
|
+
for (const [k, v] of Object.entries(params)) {
|
|
42
|
+
if (v != null) cleaned[k] = String(v);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const qs = Object.keys(cleaned).length ? "?" + new URLSearchParams(cleaned).toString() : "";
|
|
46
|
+
return fetchJson(`${config.baseUrl}/${normalizePath(path)}${qs}`, {
|
|
47
|
+
headers: config.headers
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function post(path, body) {
|
|
51
|
+
const config = resolveConfig();
|
|
52
|
+
return fetchJson(`${config.baseUrl}/${normalizePath(path)}`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { ...config.headers, "Content-Type": "application/json" },
|
|
55
|
+
body: body ? JSON.stringify(body) : void 0
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/db/index.ts
|
|
60
|
+
async function dbProvision() {
|
|
61
|
+
return post("db/provision");
|
|
62
|
+
}
|
|
63
|
+
async function dbQuery(sql, params, options) {
|
|
64
|
+
return post("db/query", { sql, params, method: options?.arrayMode ? "all" : "execute" });
|
|
65
|
+
}
|
|
66
|
+
async function dbTables() {
|
|
67
|
+
return get("db/tables");
|
|
68
|
+
}
|
|
69
|
+
async function dbTableSchema(table) {
|
|
70
|
+
return get("db/table-schema", { table });
|
|
71
|
+
}
|
|
72
|
+
async function dbStatus() {
|
|
73
|
+
return get("db/status");
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
dbProvision,
|
|
77
|
+
dbQuery,
|
|
78
|
+
dbStatus,
|
|
79
|
+
dbTableSchema,
|
|
80
|
+
dbTables
|
|
81
|
+
};
|