@nestcorehq/node 0.0.0-stage → 0.16.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 CHANGED
@@ -1,3 +1,282 @@
1
- # Temporary Holding Version
1
+ # @nestcorehq/node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official Node.js SDK for the NestCore platform — database, auth, edge
4
+ functions, storage, realtime, analytics, and migrations.
5
+
6
+ > Server-side SDK. The API key is a credential — keep it on the server, never
7
+ > ship it to a browser. For client apps, use the anon key (RLS-enforced).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @nestcorehq/node
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { createClient } from '@nestcorehq/node';
19
+
20
+ // Only the API key is required — the project slug is decoded from the key's
21
+ // JWT, and baseUrl defaults to https://api.nestcore.io.
22
+ const client = createClient({ apiKey: process.env.NESTCORE_API_KEY! });
23
+
24
+ // Local dev / self-host:
25
+ // createClient({ apiKey, baseUrl: 'http://localhost:3002' })
26
+ ```
27
+
28
+ ## Database
29
+
30
+ ### Fluent query builder (recommended)
31
+
32
+ A Supabase-style, chainable builder. Every value is bound (`$1, $2, …`) — never
33
+ string-interpolated — and RLS still applies.
34
+
35
+ ```ts
36
+ const { data, error } = await client.db
37
+ .from('posts')
38
+ .eq('status', 'published')
39
+ .gt('views', 100)
40
+ .order('created_at', { ascending: false })
41
+ .limit(10)
42
+ .select('id, title, views'); // → { rows, total }
43
+
44
+ // One row
45
+ const { data: post } = await client.db.from('posts').eq('id', id).single();
46
+ // First row or null (no error when empty)
47
+ const { data: maybe } = await client.db.from('posts').eq('slug', s).maybeSingle();
48
+ ```
49
+
50
+ Filter operators: `eq, neq, gt, gte, lt, lte, like, ilike, in, is`.
51
+ Pagination: `limit(n)`, `range(from, to)` (inclusive), `page(n, size)`.
52
+ Ordering: `order(col, { ascending })`.
53
+
54
+ Writes:
55
+
56
+ ```ts
57
+ await client.db.from('posts').insert({ title: 'Hello' });
58
+ await client.db.from('posts').update(id, { title: 'Edited' });
59
+ await client.db.from('posts').delete(id);
60
+ ```
61
+
62
+ ### Raw SQL (full power)
63
+
64
+ ```ts
65
+ const { data } = await client.db.query(
66
+ 'SELECT * FROM users WHERE email = $1', [email], // always parameterize
67
+ );
68
+ // data: { rows, rowCount, fields }
69
+ ```
70
+
71
+ ## Type generation
72
+
73
+ Generate TypeScript interfaces from your live schema (NestCore's
74
+ `supabase gen types` equivalent):
75
+
76
+ ```ts
77
+ import { writeFileSync } from 'fs';
78
+ import { generateTypes } from '@nestcorehq/node';
79
+
80
+ writeFileSync('nestcore.types.ts', await generateTypes(client.db));
81
+ ```
82
+
83
+ ```ts
84
+ // then:
85
+ import type { Database, Posts } from './nestcore.types';
86
+ const { data } = await client.db.from('posts').eq('id', id).single<Posts>();
87
+ ```
88
+
89
+ Excludes `auth_*` / internal tables by default; pass `{ tables }` or `{ exclude }`
90
+ to customize.
91
+
92
+ ## Migrations
93
+
94
+ Versioned, checksummed SQL migrations with a registry table, sequential
95
+ execution, and rollback.
96
+
97
+ ```bash
98
+ migrations/
99
+ 001_init.sql
100
+ 002_add_posts.sql
101
+ ```
102
+
103
+ Each file may declare a rollback with a `-- migrate:down` marker:
104
+
105
+ ```sql
106
+ -- 002: add posts
107
+ CREATE TABLE posts (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), title text);
108
+
109
+ -- migrate:down
110
+ DROP TABLE posts;
111
+ ```
112
+
113
+ ```ts
114
+ // Migrations run DDL — use a service-role key.
115
+ const admin = createClient({ apiKey: process.env.NESTCORE_SERVICE_KEY!, migrationsDir: 'migrations' });
116
+
117
+ await admin.migrations.runAll(); // apply all pending (sequential, stop-on-failure)
118
+ await admin.migrations.runOne('002'); // apply one
119
+ await admin.migrations.rollback(); // roll back the most recent (runs its -- migrate:down)
120
+ await admin.migrations.rollback('002'); // roll back a specific one
121
+ await admin.migrations.getStatus(); // [{ migrationId, status, applied, checksumMismatch }]
122
+ admin.migrations.createMigration('add tags'); // scaffold 003_add_tags.sql
123
+ await admin.migrations.seed('seed.sql'); // run a (re-runnable) seed script, not recorded
124
+ ```
125
+
126
+ Safety: migrations never run in parallel, completed ones are never re-run, and a
127
+ migration whose file changed after being applied (checksum mismatch) aborts the
128
+ run. Each file/section runs inside one transaction — keep scripts
129
+ transaction-safe (no `CREATE INDEX CONCURRENTLY`).
130
+
131
+ ## Auth
132
+
133
+ ```ts
134
+ await client.auth.signUp({ email, password });
135
+ await client.auth.signIn({ email, password });
136
+ ```
137
+
138
+ ### Calling auth from your server
139
+
140
+ Auth endpoints are throttled per client IP. When your server calls them for its users (a Next.js server action, an API route), every user would share your server's IP and its limit. Pass the user's IP with the **service-role key** and each user gets their own bucket; the same IP goes into the auth audit log:
141
+
142
+ ```ts
143
+ const client = createClient({
144
+ apiKey: process.env.NESTCORE_SERVICE_ROLE_KEY!, // never ship this key to a browser
145
+ clientIp: userIp, // e.g. the first address of x-forwarded-for set by YOUR proxy
146
+ });
147
+ await client.auth.signIn({ email, password });
148
+ ```
149
+
150
+ `clientIp` is sent as `X-NestCore-Client-IP` and must be a single IPv4 or IPv6 address. The API ignores it on requests made with the anon key or a user token, so a browser can't choose its own bucket. Create one client per request.
151
+
152
+ Project auth answers `429` with a `code` when a limit from the project's auth settings is reached: `OTP_RATE_LIMITED` (too many wrong codes), `OTP_REQUEST_LIMIT` (too many codes requested for one email in an hour), `ACCOUNT_LOCKED` (too many wrong passwords).
153
+
154
+ ## Edge functions
155
+
156
+ ```ts
157
+ await client.functions.invoke('my-function', { body: { data: 'test' } });
158
+ ```
159
+
160
+ ## Storage
161
+
162
+ ```ts
163
+ await client.storage.from('avatars').upload(path, file);
164
+ ```
165
+
166
+ ## Realtime
167
+
168
+ ```ts
169
+ const ch = client.channel('orders')
170
+ .on('postgres_changes', { event: 'INSERT', table: 'orders' }, (e) => console.log(e.new))
171
+ .subscribe();
172
+ ```
173
+
174
+ On Node < 22 pass a WebSocket constructor: `createClient({ ..., realtimeOptions: { WebSocket } })`.
175
+
176
+ ## AI
177
+
178
+ Chat, streaming, tool calls and embeddings through the project's own provider keys (Console → Project Settings → AI). Calls need a signed-in user or the service-role key.
179
+
180
+ ```ts
181
+ const { data } = await client.ai.chat({ messages: [{ role: 'user', content: 'Summarise my last order' }] });
182
+
183
+ for await (const ev of client.ai.chatStream({ messages })) {
184
+ if (ev.type === 'delta') process.stdout.write(ev.text);
185
+ if (ev.type === 'error') console.error(ev.code, ev.message);
186
+ }
187
+
188
+ const { data: vectors } = await client.ai.embed(['first', 'second']); // 768 dimensions
189
+ ```
190
+
191
+ Tools: pass `tools: [{ name, description, parameters }]`; run each of `data.toolCalls`, then send the reply back as `{ role: 'assistant', content: null, toolCalls }` plus one `{ role: 'tool', toolCallId, content }` per call. See https://nestcore.io/docs/ai-gateway.
192
+
193
+ ### Memory, collections, threads, semantic search
194
+
195
+ ```ts
196
+ // Memory: facts about the signed-in user (near-duplicates update the same memory)
197
+ await client.memory.add('Prefers vegetarian recipes', { kind: 'preference' });
198
+ const { data: mem } = await client.memory.search('what should I cook?'); // mem.degraded = keyword fallback
199
+
200
+ // Collections: documents chunked, embedded and searched (hybrid vector + keyword)
201
+ await admin.collections.create({ name: 'handbook' }); // service-role key
202
+ await admin.collections.from('handbook').ingest({ url: 'https://example.com/guide.pdf' }, { wait: true });
203
+ const { data: hits } = await client.collections.from('handbook').search('How do refunds work?');
204
+
205
+ // Threads: saved conversations; replies stream and are stored as they arrive
206
+ const { data: thread } = await client.ai.threads.create({ title: 'Trip' });
207
+ for await (const ev of client.ai.threads.from(thread.id).run({ input: 'Plan a weekend', memory: true, collections: ['handbook'] })) {
208
+ if (ev.type === 'delta') process.stdout.write(ev.text);
209
+ if (ev.type === 'citation') console.log(`[${ev.index}]`, ev.title);
210
+ }
211
+
212
+ // Automatic embeddings (Console → AI → Embeddings), then search a table by meaning under RLS
213
+ const { data } = await client.db.from('articles').eq('published', true).semantic('body_embedding', 'reset my password', { k: 5 });
214
+ ```
215
+
216
+ See https://nestcore.io/docs/memory-api, /docs/collections, /docs/threads and /docs/embeddings.
217
+
218
+ #### Your own embeddings, trust and batches
219
+
220
+ Memory and collections also take vectors from your own (e.g. local) embedding model; the server then calls no provider and charges nothing. Search compares only vectors of the same `model` and size.
221
+
222
+ ```ts
223
+ const model = 'nomic-embed-text-v1.5';
224
+ await client.memory.add('Prefers tea', { embedding: vec, model, metadata: { trust: 'user' } });
225
+ await client.memory.search({ queryEmbedding: q, model, trust: ['user', 'tool'] }); // text query optional
226
+
227
+ // Batches: up to 100 adds, 20 searches, 500 ids / 20 subjects per call
228
+ await client.memory.addMany([{ content: 'a', embedding: va, model }, { content: 'b', embedding: vb, model }]);
229
+ const { data: batch } = await client.memory.searchMany([{ queryEmbedding: q1, model }, { query: 'tea' }]);
230
+ await client.memory.forgetMany({ ids: ['…'], subjects: ['owner:…'] });
231
+
232
+ // A collection document from chunks you embedded (ready at once; the first chunks fix the collection's model)
233
+ await client.collections.from('notes').addChunks([{ content: 'Refunds take 5 days', embedding: v }], { model, title: 'FAQ' });
234
+ await client.collections.from('notes').search({ queryEmbedding: q, model, trust: 'user' });
235
+ ```
236
+
237
+ ### AI credits and the OpenAI-compatible API
238
+
239
+ A project's own provider key always wins. Without one, the workspace's prepaid AI credit can pay (bought in Console → Settings → Billing).
240
+
241
+ ```ts
242
+ const { data: credits } = await client.ai.credits();
243
+ // { available, active, balanceUsd, monthlyCapUsd, spentThisMonthUsd, ownKeys, creditProviders }
244
+
245
+ try {
246
+ unwrap(await client.ai.chat({ messages }));
247
+ } catch (e) {
248
+ if (e instanceof NestCoreError && e.isCreditsExhausted) {
249
+ // e.reason: 'balance' | 'spend_cap'. Don't retry: offer e.buyUrl, or adding your own key.
250
+ }
251
+ }
252
+
253
+ // Any OpenAI client works against the gateway (anon key as `apikey`, the user's token as the API key)
254
+ new OpenAI({ baseURL: client.ai.openaiBaseUrl, apiKey: accessToken, defaultHeaders: { apikey: anonKey } });
255
+ ```
256
+
257
+ ## Errors
258
+
259
+ Every call returns `{ data, error }` (never throws) — check `error` before using
260
+ `data`:
261
+
262
+ ```ts
263
+ const { data, error } = await client.db.from('posts').select();
264
+ if (error) { /* handle */ }
265
+ ```
266
+
267
+ ## Migrating from `@orbitneststudio/node`
268
+
269
+ The SDK was renamed from OrbitNest to NestCore. Switch the dependency to
270
+ `@nestcorehq/node` and update your imports; nothing else has to change at once:
271
+
272
+ - The old export names still work as deprecated aliases: `OrbitNestError`
273
+ (same class as `NestCoreError`, so `instanceof` checks keep working),
274
+ `OrbitNestClient` and `OrbitNestConfig`.
275
+ - The default `baseUrl` is now `https://api.nestcore.io`.
276
+ - `clientIp` is now sent as `X-NestCore-Client-IP`.
277
+ - Analytics ids stored under the old `orbitnest_*` keys are moved to the new
278
+ `nestcore_*` keys on first read.
279
+
280
+ ## License
281
+
282
+ MIT