Skip to content

Commit 9be85ed

Browse files
committed
feat: add BLite.Client.TypeScript to monorepo and npm publish pipeline
1 parent 25cc527 commit 9be85ed

39 files changed

Lines changed: 4684 additions & 13 deletions

.github/workflows/release.yml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,48 @@ jobs:
225225
path: "blite-server-${{ steps.version.outputs.version }}-linux-x64.tar.gz"
226226
if-no-files-found: error
227227

228+
# ── npm package (TypeScript client) ─────────────────────────────────────────
229+
npm-client:
230+
name: Pack & Publish blite-client (npm)
231+
runs-on: ubuntu-latest
232+
233+
steps:
234+
- name: Checkout
235+
uses: actions/checkout@v4
236+
237+
- name: Extract version
238+
id: version
239+
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
240+
241+
- name: Setup Node.js
242+
uses: actions/setup-node@v4
243+
with:
244+
node-version: "22"
245+
registry-url: "https://registry.npmjs.org"
246+
247+
- name: Install dependencies
248+
working-directory: src/BLite.Client.TypeScript
249+
run: npm ci
250+
251+
- name: Set version
252+
working-directory: src/BLite.Client.TypeScript
253+
run: npm version ${{ steps.version.outputs.version }} --no-git-tag-version
254+
255+
- name: Build
256+
working-directory: src/BLite.Client.TypeScript
257+
run: npm run build
258+
259+
- name: Publish to npm
260+
working-directory: src/BLite.Client.TypeScript
261+
run: npm publish --access public
262+
env:
263+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
264+
228265
# ── GitHub Release ────────────────────────────────────────────────────────────
229266
release:
230267
name: Create GitHub Release
231268
runs-on: ubuntu-latest
232-
needs: [nuget-client, docker, windows-installer, linux-installer]
269+
needs: [nuget-client, npm-client, docker, windows-installer, linux-installer]
233270

234271
steps:
235272
- name: Download Windows installer

BLite.Server.code-workspace

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
},
66
{
77
"path": "../BLite"
8+
},
9+
{
10+
"path": "../BLite.Website"
811
}
912
],
1013
"settings": {}

src/BLite.Client.SourceGenerators/BLite.Client.SourceGenerators.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<RootNamespace>BLite.Client.SourceGenerators</RootNamespace>
1010
<AssemblyName>BLite.Client.SourceGenerators</AssemblyName>
1111

12-
<Version>1.0.0</Version>
12+
<Version>0.1.2</Version>
1313
<Authors>BLite Team</Authors>
1414
<Company>EntglDb</Company>
1515
<Copyright>Copyright © 2026 Luca Fabbri</Copyright>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
*.js.map
4+
npm-debug.log*
5+
.env
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# @blite/client
2+
3+
TypeScript/Node.js client for [BLite](https://github.com/EntglDb/BLite.Server) — the embedded document database server.
4+
5+
## Install
6+
7+
```bash
8+
npm install @blite/client
9+
```
10+
11+
## Quick start
12+
13+
```ts
14+
import { BLiteClient, BsonId } from '@blite/client';
15+
16+
const client = new BLiteClient({
17+
host: 'localhost',
18+
port: 2626, // default gRPC port
19+
apiKey: 'your-api-key',
20+
useTls: true, // set false only for local dev
21+
});
22+
23+
const users = client.getCollection('users');
24+
25+
// Insert
26+
const id = await users.insert({ name: 'Alice', age: 30, active: true });
27+
console.log(id.toString()); // hex ObjectId or string, etc.
28+
29+
// Find by ID
30+
const doc = await users.findById(id);
31+
32+
// Query (fluent builder)
33+
const results = await users.query()
34+
.whereEq('active', true)
35+
.whereGte('age', 18)
36+
.orderBy('name')
37+
.skip(0).take(20)
38+
.toArray();
39+
40+
// Stream large result sets
41+
for await (const doc of users.query().whereEq('status', 'pending').execute()) {
42+
console.log(doc);
43+
}
44+
45+
// Update
46+
await users.update(id, { ...doc, age: 31 });
47+
48+
// Delete
49+
await users.delete(id);
50+
```
51+
52+
## Typed collection
53+
54+
Use a `CollectionMapper<T>` to map between a TypeScript type and `BsonDocument`. This gives you compile-time safety on read/write paths.
55+
56+
```ts
57+
import { BLiteClient, CollectionMapper, BsonId, BsonDocument } from '@blite/client';
58+
59+
interface User {
60+
id?: BsonId;
61+
name: string;
62+
email: string;
63+
age: number;
64+
}
65+
66+
const userMapper: CollectionMapper<User> = {
67+
collectionName: 'users',
68+
getId(u) { return u.id!; },
69+
toDocument(u): BsonDocument {
70+
return { _id: u.id?.toString(), name: u.name, email: u.email, age: u.age };
71+
},
72+
fromDocument(doc): User {
73+
return {
74+
id: doc._id ? BsonId.fromString(String(doc._id)) : undefined,
75+
name: String(doc.name),
76+
email: String(doc.email),
77+
age: Number(doc.age),
78+
};
79+
},
80+
};
81+
82+
const client = new BLiteClient({ host: 'localhost', apiKey: 'key' });
83+
const users = client.getTypedCollection(userMapper);
84+
85+
const id = await users.insert({ name: 'Bob', email: 'bob@example.com', age: 25 });
86+
const user = await users.findById(id); // → User | null
87+
const list = await users.query().whereGt('age', 20).toArray(); // → User[]
88+
```
89+
90+
## Transactions
91+
92+
```ts
93+
const tx = await client.beginTransaction();
94+
try {
95+
await col.insert({ key: 'a' }, tx);
96+
await col.insert({ key: 'b' }, tx);
97+
await tx.commit();
98+
} catch {
99+
await tx.rollback();
100+
}
101+
102+
// Or with AsyncDisposable (Node.js 18.2+, TypeScript 5.2+):
103+
await using tx = await client.beginTransaction();
104+
await col.insert({ key: 'a' }, tx);
105+
await col.insert({ key: 'b' }, tx);
106+
await tx.commit();
107+
```
108+
109+
## Indexes
110+
111+
```ts
112+
// BTree index (supports range queries)
113+
await users.createIndex('email', { unique: true });
114+
115+
// Vector index (HNSW)
116+
await products.createVectorIndex('embedding', 1536, { metric: 'Cosine' });
117+
118+
// Vector similarity search
119+
for await (const doc of products.vectorSearch(queryEmbedding, { k: 10 })) {
120+
console.log(doc);
121+
}
122+
123+
// Spatial index
124+
await locations.createSpatialIndex('coordinates');
125+
```
126+
127+
## Change Data Capture (CDC)
128+
129+
```ts
130+
// Stream all write events for a collection
131+
for await (const event of users.watch(true /* capture payload */)) {
132+
console.log(event.operation, event.documentId.toString(), event.payload);
133+
}
134+
```
135+
136+
## Key-Value store
137+
138+
```ts
139+
const kv = client.kv;
140+
141+
await kv.set('session:abc', Buffer.from(JSON.stringify({ userId: 1 })), 3_600_000); // 1 h TTL
142+
const buf = await kv.get('session:abc');
143+
const keys = await kv.scanKeys('session:');
144+
145+
// Batch
146+
await kv.batch((b) => {
147+
b.set('k1', Buffer.from('v1'));
148+
b.set('k2', Buffer.from('v2'), 60_000);
149+
b.delete('k3');
150+
});
151+
```
152+
153+
## Admin API
154+
155+
```ts
156+
import { BLiteOperation } from '@blite/client';
157+
158+
const admin = client.admin;
159+
160+
// Create a user with scoped permissions
161+
const apiKey = await admin.createUser('bob', {
162+
permissions: [
163+
{ collection: 'orders', ops: BLiteOperation.Query | BLiteOperation.Insert },
164+
],
165+
});
166+
167+
// Rotate key
168+
const newKey = await admin.rotateKey('bob');
169+
170+
// Tenant provisioning (multi-tenant mode)
171+
await admin.provisionTenant('tenant-acme');
172+
await admin.deprovisionTenant('tenant-old', /* deleteFiles */ true);
173+
```
174+
175+
## Schema management
176+
177+
```ts
178+
// Define a schema (enforced on write)
179+
await users.setSchema([
180+
{ name: 'name', typeCode: 0x02 /* String */, nullable: false },
181+
{ name: 'age', typeCode: 0x10 /* Int32 */, nullable: true },
182+
{ name: 'email', typeCode: 0x02 /* String */, nullable: false },
183+
], 'UserSchema');
184+
185+
const info = await users.getSchema();
186+
console.log(info.title, info.version, info.fields);
187+
```
188+
189+
## Time series
190+
191+
```ts
192+
await metrics.configureTimeSeries('timestamp', 7 * 24 * 3_600_000); // 7-day retention
193+
const info = await metrics.getTimeSeriesInfo();
194+
await metrics.forcePrune(); // prune expired documents on demand
195+
```
196+
197+
## Configuration reference
198+
199+
| Option | Type | Default | Description |
200+
|--------|------|---------|-------------|
201+
| `host` | string | `'localhost'` | Server hostname |
202+
| `port` | number | `2626` | gRPC port |
203+
| `apiKey` | string | **required** | API key sent as `x-api-key` |
204+
| `useTls` | boolean | `true` | Enable TLS |
205+
| `address` | string || Full address override; skips host/port/useTls |
206+
207+
## Key-map refresh
208+
209+
BLite uses a compact binary format (C-BSON) where field names are replaced by 2-byte IDs.
210+
The client keeps a local cache of `fieldName ↔ ID` mappings and registers new fields automatically on every write.
211+
212+
If you need to read documents written by other clients before making any writes yourself, pre-populate the cache:
213+
214+
```ts
215+
await client.refreshKeyMap('users'); // any collection you have Query access to
216+
```
217+
218+
## Notes
219+
220+
- **Node.js 18+** required (AsyncDisposable, `Buffer.writeBigInt64LE`, ES2020 target).
221+
- **int64** values in filter predicates: pass as `bigint` for values outside `[-2^53, 2^53]`.
222+
- **Decimal128** fields are decoded as a raw string representation for lossless round-trip; no arithmetic support.
223+
- The TypeScript client uses `DynamicService` only; `DocumentService` (typed BSON path with CLR type hints) is a C#-only concept.
224+
225+
## License
226+
227+
AGPL-3.0 © EntglDb
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Configuration for integration tests (require a live BLite server on localhost).
2+
// Run with: npm run test:integration
3+
//
4+
// Environment variables:
5+
// BLITE_HOST — server hostname (default: localhost)
6+
// BLITE_PORT — gRPC port (default: 2626)
7+
// BLITE_API_KEY — API key (default: dev)
8+
// BLITE_TLS — 'true' to use TLS (default: false)
9+
10+
/** @type {import('jest').Config} */
11+
module.exports = {
12+
testEnvironment: 'node',
13+
testMatch: ['**/tests/integration/**/*.test.ts'],
14+
transform: {
15+
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
16+
},
17+
testTimeout: 15000,
18+
verbose: true,
19+
forceExit: true,
20+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@blite/client",
3+
"version": "0.1.2",
4+
"description": "TypeScript/Node.js client for BLite database server",
5+
"author": "EntglDb",
6+
"license": "AGPL-3.0",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/EntglDb/BLite.Server",
10+
"directory": "src/BLite.Client.TypeScript"
11+
},
12+
"keywords": [
13+
"blite",
14+
"database",
15+
"grpc",
16+
"nosql",
17+
"bson"
18+
],
19+
"main": "dist/index.js",
20+
"types": "dist/index.d.ts",
21+
"files": [
22+
"dist/",
23+
"src/proto/blite.proto"
24+
],
25+
"scripts": {
26+
"build": "tsc && node -e \"require('fs').cpSync('src/proto', 'dist/proto', { recursive: true })\"",
27+
"build:watch": "tsc --watch",
28+
"test": "jest",
29+
"test:integration": "jest --config jest.integration.config.js",
30+
"lint": "eslint src/**/*.ts"
31+
},
32+
"jest": {
33+
"testEnvironment": "node",
34+
"testMatch": ["**/tests/**/*.test.ts"],
35+
"testPathIgnorePatterns": ["tests/integration"],
36+
"transform": {
37+
"^.+\\.tsx?$": ["ts-jest", { "tsconfig": "tsconfig.json" }]
38+
}
39+
},
40+
"dependencies": {
41+
"@grpc/grpc-js": "^1.12.0",
42+
"@grpc/proto-loader": "^0.7.13",
43+
"@msgpack/msgpack": "^3.0.0"
44+
},
45+
"devDependencies": {
46+
"@types/jest": "^30.0.0",
47+
"@types/node": "^22.0.0",
48+
"jest": "^30.3.0",
49+
"ts-jest": "^29.4.6",
50+
"typescript": "^5.6.0"
51+
}
52+
}

0 commit comments

Comments
 (0)