@onyx.dev/onyx-database 0.1.4

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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Onyx Dev Tools
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,519 @@
1
+ # @onyx.dev/onyx-database
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) [![codecov](https://codecov.io/gh/OnyxDevTools/onyx-database/branch/main/graph/badge.svg)](https://codecov.io/gh/OnyxDevTools/onyx-database)
4
+
5
+ TypeScript client SDK for **Onyx Cloud Database** — a zero-dependency, strict-typed, builder-pattern API for querying and persisting data in Onyx from Node.js or edge runtimes like Cloudflare Workers. Ships ESM & CJS, includes a credential resolver, and an optional **schema code generator** that produces table-safe types and a `tables` enum.
6
+
7
+ - **Website:** <https://onyx.dev/>
8
+ - **Cloud Console:** <https://cloud.onyx.dev>
9
+ - **Docs hub:** <https://onyx.dev/documentation/>
10
+ - **Cloud API docs:** <https://onyx.dev/documentation/api-documentation/>
11
+ - **API Reference:** [./docs](./docs)
12
+ - **Quality:** 100% unit test coverage enforced in CI
13
+
14
+ ---
15
+
16
+ ## Getting started (Cloud ➜ keys ➜ connect)
17
+
18
+ 1. **Sign up & create resources** at **<https://cloud.onyx.dev>**
19
+ Create an **Organization**, then a **Database**, define your **Schema** (e.g., `User`, `Role`, `Permission`), and create **API Keys**.
20
+ 2. **Note your connection parameters**:
21
+ - `baseUrl` (e.g., `https://api.onyx.dev`)
22
+ - `databaseId`
23
+ - `apiKey`
24
+ - `apiSecret`
25
+ 3. **Install the SDK** in your project:
26
+
27
+ ```bash
28
+ npm i @onyx.dev/onyx-database
29
+ ```
30
+
31
+ 4. **Initialize the client** using env vars or explicit config.
32
+
33
+ > Supports Node.js **18+** and Cloudflare Workers.
34
+
35
+ ---
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ npm i @onyx.dev/onyx-database
41
+ ```
42
+
43
+ The package is dual-module (ESM + CJS) and has **no runtime or peer dependencies**.
44
+
45
+ ---
46
+
47
+ ## Initialize the client
48
+
49
+ This SDK resolves credentials automatically using the chain **explicit config ➜ environment variables ➜ `ONYX_CONFIG_PATH` file ➜ project config file ➜ home profile** _(Node.js only for file-based sources)_. Call `onyx.init({ databaseId: 'database-id' })` to target a specific database, or omit the `databaseId` to use the default. You can also pass credentials directly via config.
50
+
51
+ ### Option A) Environment variables (recommended for production)
52
+
53
+ Set the following environment variables for your database:
54
+
55
+ - `ONYX_DATABASE_ID`
56
+ - `ONYX_DATABASE_BASE_URL`
57
+ - `ONYX_DATABASE_API_KEY`
58
+ - `ONYX_DATABASE_API_SECRET`
59
+
60
+ ```ts
61
+ import { onyx } from '@onyx.dev/onyx-database';
62
+
63
+ const db = onyx.init({ databaseId: 'YOUR_DATABASE_ID' }); // uses env when ID matches
64
+ // credentials are cached for 5 minutes by default
65
+ ```
66
+
67
+ ### Option B) Explicit config
68
+
69
+ ```ts
70
+ import { onyx } from '@onyx.dev/onyx-database';
71
+
72
+ const db = onyx.init({
73
+ baseUrl: 'https://api.onyx.dev',
74
+ databaseId: 'YOUR_DATABASE_ID',
75
+ apiKey: 'YOUR_KEY',
76
+ apiSecret: 'YOUR_SECRET',
77
+ partition: 'tenantA',
78
+ requestLoggingEnabled: true, // logs HTTP requests
79
+ responseLoggingEnabled: true, // logs HTTP responses
80
+ });
81
+ ```
82
+
83
+ The `partition` option sets a default partition for queries, `findById`, and
84
+ deletes by primary key. Save operations use the partition field on the entity
85
+ itself. Enable `requestLoggingEnabled` to log each request and its body to the
86
+ console. Enable `responseLoggingEnabled` to log responses and bodies. Setting
87
+ the `ONYX_DEBUG=true` environment variable enables both request and response
88
+ logging even if these flags are not set. It also logs the source of resolved
89
+ credentials (explicit config, env vars, config path file, project file, or home profile).
90
+
91
+ ### Option C) Node-only config files
92
+
93
+ Set `ONYX_CONFIG_PATH` to a JSON file containing your credentials. This file is checked after environment variables and before project and home files. When unset, the resolver checks for JSON files matching the `OnyxConfig` shape in the following order:
94
+
95
+ - `./onyx-database-<databaseId>.json`
96
+ - `./onyx-database.json`
97
+ - `~/.onyx/onyx-database-<databaseId>.json`
98
+ - `~/.onyx/onyx-database.json`
99
+ - `~/onyx-database.json`
100
+
101
+ These files are ignored in non-Node runtimes like Cloudflare Workers.
102
+
103
+ ### Connection handling
104
+
105
+ Calling `onyx.init()` returns a lightweight client. Configuration is resolved once
106
+ and cached for 5 minutes to avoid repeated credential lookups (override with
107
+ `ttl` or reset via `onyx.clearCacheConfig()`). Each database instance keeps a
108
+ single internal `HttpClient`. Requests use the runtime's global `fetch`, which
109
+ already reuses connections and pools them for keep‑alive. Reuse the returned
110
+ `db` for multiple operations; extra SDK‑level connection pooling generally isn't
111
+ necessary unless you create many short‑lived clients.
112
+
113
+ ---
114
+
115
+ ## Optional: generate TypeScript types from your schema
116
+
117
+ The package ships a small codegen CLI that emits per-table interfaces, a `tables` enum, and a `Schema` mapping for compile-time safety and IntelliSense. Each generated interface also includes an index signature so extra properties (for graph attachments in cascade saves) don't trigger type errors.
118
+
119
+ Generate directly from the API (using the same credential resolver as `init()`):
120
+
121
+ ```bash
122
+ npx onyx-gen --source api --out ./src/onyx/types.ts --name OnyxSchema
123
+ ```
124
+
125
+ Timestamp attributes are emitted as `Date` fields by default. When saving,
126
+ `Date` values are automatically serialized to ISO timestamp strings. Pass
127
+ `--timestamps string` to keep timestamps as ISO strings in generated types.
128
+
129
+ Or from a local schema file you export from the console:
130
+
131
+ ```bash
132
+ npx onyx-gen --source file --schema ./onyx.schema.json --out ./src/onyx/types.ts --name OnyxSchema
133
+ ```
134
+
135
+ Use in code:
136
+
137
+ ```ts
138
+ import { onyx, eq, asc } from '@onyx.dev/onyx-database';
139
+ import { tables, Schema } from './src/onyx/types';
140
+
141
+ const db = onyx.init<Schema>();
142
+
143
+ const User = await db
144
+ .from(tables.User)
145
+ .where(eq('status', 'active'))
146
+ .orderBy(asc('createdAt'))
147
+ .limit(20)
148
+ .list();
149
+ ```
150
+
151
+ For a schema with `User`, `UserProfile`, `Role`, and `Permission` tables,
152
+ `onyx-gen` emits plain interfaces keyed by IDs. Each interface includes an
153
+ index signature so resolver-attached fields or embedded objects remain
154
+ type-safe:
155
+
156
+ ```ts
157
+ // AUTO-GENERATED BY onyx-gen. DO NOT EDIT.
158
+ export interface User {
159
+ id?: string;
160
+ name: string;
161
+ [key: string]: any;
162
+ }
163
+
164
+ export interface Role {
165
+ id?: string;
166
+ title: "",
167
+ [key: string]: any;
168
+ }
169
+
170
+ export interface Permission {
171
+ id?: string;
172
+ description: string;
173
+ [key: string]: any;
174
+ }
175
+
176
+ export interface UserProfile {
177
+ id?: string;
178
+ age: number;
179
+ [key: string]: any;
180
+ createdDate: Date;
181
+ }
182
+
183
+ const user = await db
184
+ .from(tables.User)
185
+ .selectFields('id', 'username')
186
+ .resolve('roles.permissions', 'profile')
187
+ .firstOrNull();
188
+
189
+ // user.roles -> Role[]
190
+ // user.roles[0]?.permissions -> Permission[]
191
+ // user.profile -> UserProfile | undefined
192
+ ```
193
+
194
+ > The generator defaults to not emitting the JSON copy of your schema. Use `--emit-json` if you want it.
195
+
196
+ ### Modeling users, roles, and permissions
197
+
198
+ `User` and `Role` form a many-to-many relationship through a `UserRole` join table. `Role` and `Permission` are connected the same way via `RolePermission`.
199
+
200
+ - **`userRoles` / `rolePermissions` resolvers** return join-table rows. Use these when cascading saves or deletes to add or remove associations.
201
+ - **`roles` / `permissions` resolvers** traverse those joins and return `Role` or `Permission` records for display.
202
+
203
+ Define these resolvers in your `onyx.schema.json`:
204
+
205
+ ```json
206
+ "resolvers": [
207
+ {
208
+ "name": "roles",
209
+ "resolver": "db.from(\"Role\")\n .where(\n inOp(\"id\", \n db.from(\"UserRole\")\n .where(eq(\"userId\", this.id))\n .list()\n .values('roleId')\n )\n)\n .list()"
210
+ },
211
+ {
212
+ "name": "profile",
213
+ "resolver": "db.from(\"UserProfile\")\n .where(eq(\"userId\", this.id))\n .firstOrNull()"
214
+ },
215
+ {
216
+ "name": "userRoles",
217
+ "resolver": "db.from(\"UserRole\")\n .where(eq(\"userId\", this.id))\n .list()"
218
+ }
219
+ ]
220
+ ```
221
+
222
+ Save a user and attach roles in one operation:
223
+
224
+ ```ts
225
+ await db.cascade('userRoles:UserRole(userId, id)').save('User', {
226
+ id: 'user_126',
227
+ email: 'dana@example.com',
228
+ userRoles: [
229
+ { roleId: 'role_admin' },
230
+ { roleId: 'role_editor' },
231
+ ],
232
+ });
233
+ ```
234
+
235
+ Fetch a user with roles and each role's permissions:
236
+
237
+ ```ts
238
+ const detailed = await db
239
+ .from('User')
240
+ .resolve('roles.permissions', 'profile')
241
+ .firstOrNull();
242
+
243
+ // detailed.roles -> Role[]
244
+ // detailed.roles[0]?.permissions -> Permission[]
245
+ ```
246
+
247
+ Remove a role and its permission links:
248
+
249
+ ```ts
250
+ await db.cascade('rolePermissions').delete('Role', 'role_temp');
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Query helpers at a glance
256
+
257
+ Importable helpers for conditions and sort:
258
+
259
+ ```ts
260
+ import {
261
+ eq, neq, inOp, notIn, between,
262
+ gt, gte, lt, lte,
263
+ like, notLike, contains, notContains,
264
+ startsWith, notStartsWith, matches, notMatches,
265
+ isNull, notNull,
266
+ asc, desc
267
+ } from '@onyx.dev/onyx-database';
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Usage examples with `User`, `Role`, `Permission`
273
+
274
+ > The examples assume your schema has tables named `User`, `Role`, and `Permission`.
275
+ > If you generated types, replace string literals with `tables.User`, `tables.Role`, etc., and type your results with `Schema`.
276
+
277
+ ### 1) List (query & paging)
278
+
279
+ ```ts
280
+ import { onyx, eq, contains, asc } from '@onyx.dev/onyx-database';
281
+
282
+ const db = onyx.init();
283
+
284
+ // Fetch first 25 active User whose email contains "@example.com"
285
+ const firstPage = await db
286
+ .from('User')
287
+ .where(eq('status', 'active'))
288
+ .and(contains('email', '@example.com'))
289
+ .orderBy(asc('createdAt'))
290
+ .limit(25)
291
+ .page(); // or .list() for array-like results with nextPage
292
+
293
+ // Iterate to fetch all pages:
294
+ const allActive = await db
295
+ .from('User')
296
+ .where(eq('status', 'active'))
297
+ .list();
298
+
299
+ // Collect IDs across all pages
300
+ const ids = await db.from('User').list().values('id');
301
+ // Get the first user across pages
302
+ const firstUser = await db.from('User').list().firstOrNull();
303
+ // Call any QueryResults helper before awaiting
304
+ const size = await db.from('User').list().size();
305
+ ```
306
+
307
+ ### 1b) First or null
308
+
309
+ ```ts
310
+ const maybeUser = await db
311
+ .from('User')
312
+ .where(eq('email', 'alice@example.com')) // avoid searching by indentifier with firstOrNull, it will throw not found error
313
+ .firstOrNull(); // or .one()
314
+ ```
315
+
316
+ ### 2) Save (create/update)
317
+
318
+ ```ts
319
+ import { onyx } from '@onyx.dev/onyx-database';
320
+ const db = onyx.init();
321
+
322
+ // Upsert a single user
323
+ await db.save('User', {
324
+ id: 'user_123',
325
+ email: 'alice@example.com',
326
+ status: 'active',
327
+ });
328
+
329
+ // Batch upsert User
330
+ await db.save('User', [
331
+ { id: 'user_124', email: 'bob@example.com', status: 'active' },
332
+ { id: 'user_125', email: 'carol@example.com', status: 'invited' },
333
+ ]);
334
+
335
+ // Save many users in batches of 500
336
+ await db.batchSave('User', largeUserArray, 500);
337
+
338
+ // Save with cascade relationships (example)
339
+ await db.cascade('userRoles:UserRole(userId, id)').save('User', {
340
+ id: 'user_126',
341
+ email: 'dana@example.com',
342
+ userRoles: [
343
+ { roleId: 'role_admin' },
344
+ { roleId: 'role_editor' },
345
+ ],
346
+ });
347
+
348
+ // Cascade relationship syntax:
349
+ // field:Type(target, source)
350
+ // field – property or path relative to the entity being saved
351
+ // Type – related table name
352
+ // target – foreign key on the related table
353
+ // source – field on the top level entity used as the key
354
+
355
+ // Using the CascadeRelationshipBuilder
356
+ const permission = { id: 'perm_edit_content', description: 'Edit content' };
357
+ const permissionsCascade = db
358
+ .cascadeBuilder()
359
+ .graph('permissions')
360
+ .graphType('Permission')
361
+ .targetField('roleId')
362
+ .sourceField('id');
363
+
364
+ await db.cascade(permissionsCascade).save('Role', {
365
+ id: 'role_editor',
366
+ name: 'Editor',
367
+ permissions: [permission],
368
+ });
369
+ ```
370
+
371
+ ### 3) Delete (by primary key)
372
+
373
+ ```ts
374
+ import { onyx } from '@onyx.dev/onyx-database';
375
+ const db = onyx.init();
376
+
377
+ // Simple delete returns the removed record
378
+ await db.delete('User', 'user_125');
379
+
380
+ // Delete cascading relationships (example)
381
+ await db.delete('Role', 'role_temp', { relationships: ['rolePermissions'] });
382
+ // this will delete all of the related permissions that come back from the rolePermissions resolver
383
+ // builder pattern equivalent
384
+ await db.cascade('rolePermissions').delete('Role', 'role_temp');
385
+ ```
386
+
387
+ ### 4) Delete using query
388
+
389
+ ```ts
390
+ import { onyx } from '@onyx.dev/onyx-database';
391
+ const db = onyx.init();
392
+
393
+ const delCount = await db
394
+ .from(tables.User)
395
+ .where(eq('status', 'inactive'))
396
+ .delete();
397
+ //this will delete all inactive users in the system
398
+
399
+ ```
400
+
401
+ ### 5) Documents API (binary assets)
402
+
403
+ ```ts
404
+ import { onyx, type OnyxDocument } from '@onyx.dev/onyx-database';
405
+ const db = onyx.init();
406
+
407
+ // Save / upload a document (Base64 content)
408
+ const logoPng = Buffer.from('89504E47...', 'hex').toString('base64');
409
+ const doc: OnyxDocument = {
410
+ documentId: 'logo.png',
411
+ path: '/brand/logo.png',
412
+ mimeType: 'image/png',
413
+ content: logoPng,
414
+ };
415
+ await db.saveDocument(doc);
416
+
417
+ // Get a document (optionally with resizing hints if supported)
418
+ const image = await db.getDocument('logo.png', { width: 128, height: 128 });
419
+
420
+ // Delete a document
421
+ await db.deleteDocument('logo.png');
422
+ ```
423
+
424
+ ### 6) Streaming (live changes)
425
+
426
+ ```ts
427
+ import { onyx, eq } from '@onyx.dev/onyx-database';
428
+ const db = onyx.init();
429
+
430
+ const stream = db
431
+ .from('User')
432
+ .where(eq('status', 'active'))
433
+ .onItemAdded((u) => console.log('USER ADDED', u))
434
+ .onItemUpdated((u) => console.log('USER UPDATED', u))
435
+ .onItemDeleted((u) => console.log('USER DELETED', u))
436
+ .onItem((entity, action) => console.log('STREAM EVENT', action, entity));
437
+
438
+ // Start the stream and keep the connection alive for new events:
439
+ const handle = await stream.stream(true, true);
440
+
441
+ // Later, cancel:
442
+ setTimeout(() => handle.cancel(), 60_000);
443
+ ```
444
+
445
+ > **Debugging**: set `ONYX_STREAM_DEBUG=1` to log stream connection details.
446
+
447
+ ---
448
+
449
+ ## Error handling
450
+
451
+ - **OnyxConfigError** – thrown by `init()` if required connection parameters are missing.
452
+ - **HttpError** – thrown for non-2xx API responses, with status and message from the server.
453
+
454
+ Use standard `try/catch` or `.catch()` patterns:
455
+
456
+ ```ts
457
+ try {
458
+ const db = onyx.init();
459
+ // ...perform queries...
460
+ } catch (err) {
461
+ console.error('Onyx error:', err);
462
+ // Handle configuration or HTTP errors here.
463
+ }
464
+ ```
465
+
466
+ ---
467
+
468
+ ## Runtime & bundlers
469
+
470
+ - **ESM**: `dist/index.js`
471
+ - **CJS**: `dist/index.cjs`
472
+ - **Types**: `dist/index.d.ts`
473
+
474
+ Works in Node 18+ and modern bundlers (Vite, esbuild, Webpack). For TypeScript, prefer:
475
+
476
+ ```json
477
+ {
478
+ "compilerOptions": {
479
+ "module": "NodeNext",
480
+ "moduleResolution": "NodeNext",
481
+ "target": "ES2022",
482
+ "strict": true
483
+ }
484
+ }
485
+ ```
486
+ ---
487
+
488
+ ## Release workflow
489
+
490
+ This repository uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
491
+
492
+ 1. Run `npm run changeset` to create a changeset entry.
493
+ 2. Push to `main` and the **Release** workflow opens a version PR.
494
+ 3. Tag the release to trigger `npm run release -- --dry-run` in CI.
495
+
496
+ ---
497
+
498
+ ## Related links
499
+
500
+ - Onyx website: <https://onyx.dev/>
501
+ - Cloud console: <https://cloud.onyx.dev>
502
+ - Docs hub: <https://onyx.dev/documentation/>
503
+ - Cloud API docs: <https://onyx.dev/documentation/api-documentation/>
504
+
505
+ ---
506
+
507
+ ## Security
508
+
509
+ See [SECURITY.md](./SECURITY.md) for our security policy and vulnerability reporting process.
510
+
511
+ ---
512
+
513
+ ## License
514
+
515
+ MIT © Onyx Dev Tools. See [LICENSE](./LICENSE).
516
+
517
+ ---
518
+
519
+ > **Keywords:** Onyx Database TypeScript SDK, Onyx Cloud Database, Onyx NoSQL Graph Database client, TypeScript query builder, tables enum, schema code generation, zero-dependency database client, ESM CJS, Node.js database SDK, User Role Permission example