trilium-api 1.0.1 → 1.0.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/README.md +173 -59
- package/dist/index.cjs +386 -0
- package/{src/generated/trilium.d.ts → dist/index.d.cts} +472 -5
- package/dist/index.d.ts +2225 -0
- package/dist/index.js +344 -0
- package/package.json +36 -5
- package/.github/workflows/ci.yml +0 -37
- package/.github/workflows/publish.yml +0 -84
- package/src/client.test.ts +0 -477
- package/src/client.ts +0 -91
- package/src/demo-mapper.ts +0 -166
- package/src/demo-search.ts +0 -108
- package/src/demo.ts +0 -126
- package/src/index.ts +0 -35
- package/src/mapper.test.ts +0 -638
- package/src/mapper.ts +0 -534
- package/tsconfig.json +0 -42
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A type-safe TypeScript client for the [Trilium Notes](https://github.com/Trilium
|
|
|
10
10
|
- [API Reference](#api-reference)
|
|
11
11
|
- [Search Query Builder](#search-query-builder)
|
|
12
12
|
- [Note Mapper](#note-mapper)
|
|
13
|
+
- [Search and Map](#search-and-map)
|
|
13
14
|
- [Types](#types)
|
|
14
15
|
- [Error Handling](#error-handling)
|
|
15
16
|
- [Demo](#demo)
|
|
@@ -23,6 +24,7 @@ A type-safe TypeScript client for the [Trilium Notes](https://github.com/Trilium
|
|
|
23
24
|
- **Lightweight** - Built on [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) (~6kb)
|
|
24
25
|
- **Query Builder** - Type-safe search query construction
|
|
25
26
|
- **Mapper** - Declarative note-to-object mapping with transforms
|
|
27
|
+
- **StandardNote** - Consistent base fields (id, title, dates) on all mapped types
|
|
26
28
|
|
|
27
29
|
## Installation
|
|
28
30
|
|
|
@@ -38,7 +40,7 @@ pnpm add trilium-api
|
|
|
38
40
|
import { createTriliumClient } from 'trilium-api';
|
|
39
41
|
|
|
40
42
|
const client = createTriliumClient({
|
|
41
|
-
baseUrl: 'http://localhost:
|
|
43
|
+
baseUrl: 'http://localhost:8080',
|
|
42
44
|
apiKey: 'your-etapi-token',
|
|
43
45
|
});
|
|
44
46
|
|
|
@@ -65,7 +67,7 @@ const { data: results } = await client.GET('/notes', {
|
|
|
65
67
|
import { createTriliumClient } from 'trilium-api';
|
|
66
68
|
|
|
67
69
|
const client = createTriliumClient({
|
|
68
|
-
baseUrl: 'http://localhost:
|
|
70
|
+
baseUrl: 'http://localhost:8080', // Your Trilium server URL
|
|
69
71
|
apiKey: 'your-etapi-token', // ETAPI token from Trilium settings
|
|
70
72
|
});
|
|
71
73
|
```
|
|
@@ -261,54 +263,61 @@ const { data } = await client.GET('/notes', {
|
|
|
261
263
|
|
|
262
264
|
## Note Mapper
|
|
263
265
|
|
|
264
|
-
Map Trilium notes to strongly-typed objects using declarative field mappings
|
|
266
|
+
Map Trilium notes to strongly-typed objects using declarative field mappings.
|
|
267
|
+
|
|
268
|
+
### StandardNote Base Type
|
|
269
|
+
|
|
270
|
+
All mapped types should extend `StandardNote`, which provides consistent base fields:
|
|
265
271
|
|
|
266
272
|
```typescript
|
|
267
|
-
import {
|
|
273
|
+
import type { StandardNote } from 'trilium-api';
|
|
274
|
+
|
|
275
|
+
// StandardNote includes:
|
|
276
|
+
// - id: string (note ID)
|
|
277
|
+
// - title: string (note title)
|
|
278
|
+
// - dateCreatedUtc: Date
|
|
279
|
+
// - dateLastModifiedUtc: Date
|
|
268
280
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
interface BlogPost extends StandardNote {
|
|
282
|
+
slug: string;
|
|
283
|
+
tags: string[];
|
|
284
|
+
isPublished: boolean;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Using TriliumMapper Directly
|
|
289
|
+
|
|
290
|
+
For standalone mapping (outside of `searchAndMap`), use `TriliumMapper`:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { TriliumMapper, StandardNoteMapping, transforms, type StandardNote } from 'trilium-api';
|
|
294
|
+
|
|
295
|
+
interface BlogPost extends StandardNote {
|
|
273
296
|
slug: string;
|
|
274
|
-
publishDate: Date;
|
|
275
297
|
wordCount: number;
|
|
276
298
|
readTimeMinutes: number;
|
|
277
299
|
tags: string[];
|
|
278
300
|
isPublished: boolean;
|
|
279
301
|
}
|
|
280
302
|
|
|
281
|
-
//
|
|
282
|
-
const blogMapper = new TriliumMapper<BlogPost>(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// Computed field based on other mapped values
|
|
297
|
-
readTimeMinutes: {
|
|
298
|
-
computed: (partial) => Math.ceil((partial.wordCount || 0) / 200),
|
|
299
|
-
},
|
|
300
|
-
|
|
301
|
-
// Transform comma-separated string to array
|
|
302
|
-
tags: { from: '#tags', transform: transforms.commaSeparated, default: [] },
|
|
303
|
-
|
|
304
|
-
// Transform to boolean
|
|
305
|
-
isPublished: { from: '#published', transform: transforms.boolean, default: false },
|
|
306
|
-
});
|
|
303
|
+
// Merge StandardNoteMapping with your custom fields
|
|
304
|
+
const blogMapper = new TriliumMapper<BlogPost>(
|
|
305
|
+
TriliumMapper.merge(
|
|
306
|
+
StandardNoteMapping,
|
|
307
|
+
{
|
|
308
|
+
slug: { from: '#slug', required: true },
|
|
309
|
+
wordCount: { from: '#wordCount', transform: transforms.number, default: 0 },
|
|
310
|
+
readTimeMinutes: {
|
|
311
|
+
computed: (partial) => Math.ceil((partial.wordCount || 0) / 200),
|
|
312
|
+
},
|
|
313
|
+
tags: { from: '#tags', transform: transforms.commaSeparated, default: [] },
|
|
314
|
+
isPublished: { from: '#published', transform: transforms.boolean, default: false },
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
);
|
|
307
318
|
|
|
308
|
-
// Map
|
|
319
|
+
// Map notes
|
|
309
320
|
const post = blogMapper.map(note);
|
|
310
|
-
|
|
311
|
-
// Map an array of notes
|
|
312
321
|
const posts = blogMapper.map(notes);
|
|
313
322
|
```
|
|
314
323
|
|
|
@@ -369,44 +378,149 @@ const posts = blogMapper.map(notes);
|
|
|
369
378
|
| `transforms.date` | Parse date string | `"2024-01-15"` → `Date` |
|
|
370
379
|
| `transforms.trim` | Trim whitespace | `" hello "` → `"hello"` |
|
|
371
380
|
|
|
372
|
-
|
|
381
|
+
## Search and Map
|
|
373
382
|
|
|
374
|
-
|
|
383
|
+
The `searchAndMap` method combines searching and mapping in a single call. It **automatically includes `StandardNoteMapping`**, so you only need to define your custom fields!
|
|
375
384
|
|
|
376
385
|
```typescript
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
};
|
|
386
|
+
import { createTriliumClient, transforms, type StandardNote, type CustomMapping } from 'trilium-api';
|
|
387
|
+
|
|
388
|
+
const client = createTriliumClient({
|
|
389
|
+
baseUrl: 'http://localhost:8080',
|
|
390
|
+
apiKey: 'your-etapi-token',
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Extend StandardNote with your custom fields
|
|
394
|
+
interface BlogPost extends StandardNote {
|
|
395
|
+
slug: string;
|
|
396
|
+
published: boolean;
|
|
397
|
+
}
|
|
383
398
|
|
|
384
|
-
//
|
|
385
|
-
const blogMapping = {
|
|
399
|
+
// Use CustomMapping<T> for clean typing - excludes StandardNote fields automatically
|
|
400
|
+
const blogMapping: CustomMapping<BlogPost> = {
|
|
386
401
|
slug: '#slug',
|
|
387
|
-
|
|
402
|
+
published: { from: '#published', transform: transforms.boolean, default: false },
|
|
388
403
|
};
|
|
389
404
|
|
|
390
|
-
//
|
|
391
|
-
const
|
|
392
|
-
|
|
405
|
+
// Just pass your custom mapping - StandardNoteMapping is auto-merged!
|
|
406
|
+
const { data, failures } = await client.searchAndMap<BlogPost>({
|
|
407
|
+
query: { '#blog': true, '#published': true },
|
|
408
|
+
mapping: blogMapping,
|
|
409
|
+
limit: 10,
|
|
410
|
+
orderBy: 'dateModified',
|
|
411
|
+
orderDirection: 'desc',
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Each post has: id, title, dateCreatedUtc, dateLastModifiedUtc, slug, published
|
|
415
|
+
data.forEach(post => {
|
|
416
|
+
console.log(`${post.title} (${post.id}) - ${post.slug}`);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Check for mapping failures
|
|
420
|
+
if (failures.length > 0) {
|
|
421
|
+
console.warn(`${failures.length} notes failed to map:`);
|
|
422
|
+
failures.forEach(f => console.warn(` - ${f.noteTitle}: ${f.reason}`));
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Options
|
|
427
|
+
|
|
428
|
+
| Option | Type | Description |
|
|
429
|
+
|--------|------|-------------|
|
|
430
|
+
| `query` | `string \| object` | Search query string or structured query object |
|
|
431
|
+
| `mapping` | `CustomMapping<T>` | Field mapping for your custom fields (StandardNote fields auto-merged) |
|
|
432
|
+
| `limit` | `number` | Maximum number of results |
|
|
433
|
+
| `orderBy` | `string` | Field to order by (e.g., `'dateModified'`, `'title'`) |
|
|
434
|
+
| `orderDirection` | `'asc' \| 'desc'` | Sort direction |
|
|
435
|
+
| `fastSearch` | `boolean` | Enable fast search mode (less accurate but faster) |
|
|
436
|
+
|
|
437
|
+
### Return Value
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
{
|
|
441
|
+
data: T[], // Successfully mapped objects
|
|
442
|
+
failures: MappingFailure[] // Notes that failed to map
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Handling Failures
|
|
447
|
+
|
|
448
|
+
When a note fails to map (e.g., missing required field, transform error), it's added to the `failures` array instead of throwing:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
interface MappingFailure {
|
|
452
|
+
noteId: string; // The note ID that failed
|
|
453
|
+
noteTitle: string; // The note title for identification
|
|
454
|
+
reason: string; // Error message explaining the failure
|
|
455
|
+
note: TriliumNote; // The original note object for debugging
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
This allows you to process partial results while still knowing which notes had issues:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
interface BlogPost extends StandardNote {
|
|
463
|
+
slug: string;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const { data, failures } = await client.searchAndMap<BlogPost>({
|
|
467
|
+
query: '#blog',
|
|
468
|
+
mapping: {
|
|
469
|
+
slug: { from: '#slug', required: true }, // Will fail if missing
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// data contains all successfully mapped posts
|
|
474
|
+
// failures contains notes missing the required #slug label
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Error Handling
|
|
478
|
+
|
|
479
|
+
API or network errors throw an exception:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
try {
|
|
483
|
+
const { data, failures } = await client.searchAndMap<BlogPost>({
|
|
484
|
+
query: '#blog',
|
|
485
|
+
mapping: blogMapping,
|
|
486
|
+
});
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error('Search failed:', err);
|
|
489
|
+
}
|
|
393
490
|
```
|
|
394
491
|
|
|
395
492
|
## Types
|
|
396
493
|
|
|
397
|
-
The package exports
|
|
494
|
+
The package exports a focused set of types for common use cases:
|
|
398
495
|
|
|
399
496
|
```typescript
|
|
400
|
-
|
|
497
|
+
// Main imports for typical usage
|
|
498
|
+
import {
|
|
499
|
+
createTriliumClient,
|
|
500
|
+
transforms,
|
|
501
|
+
buildSearchQuery,
|
|
502
|
+
} from 'trilium-api';
|
|
503
|
+
|
|
504
|
+
import type {
|
|
505
|
+
// Your mapped types should extend this
|
|
506
|
+
StandardNote,
|
|
507
|
+
// For typing your custom field mappings
|
|
508
|
+
CustomMapping,
|
|
509
|
+
// For typing query objects
|
|
510
|
+
TriliumSearchHelpers,
|
|
511
|
+
// For error handling
|
|
512
|
+
MappingFailure,
|
|
513
|
+
// Trilium entity types (for API responses)
|
|
401
514
|
TriliumNote,
|
|
402
515
|
TriliumBranch,
|
|
403
516
|
TriliumAttribute,
|
|
404
517
|
TriliumAttachment,
|
|
405
518
|
TriliumAppInfo,
|
|
406
|
-
paths,
|
|
407
|
-
components,
|
|
408
|
-
operations,
|
|
409
519
|
} from 'trilium-api';
|
|
520
|
+
|
|
521
|
+
// Advanced: for standalone TriliumMapper usage (outside searchAndMap)
|
|
522
|
+
import { TriliumMapper, StandardNoteMapping } from 'trilium-api';
|
|
523
|
+
import type { MappingConfig } from 'trilium-api';
|
|
410
524
|
```
|
|
411
525
|
|
|
412
526
|
## Error Handling
|
|
@@ -620,7 +734,7 @@ function createMockResponse(body: any, status = 200, contentType = 'application/
|
|
|
620
734
|
|
|
621
735
|
describe('my new feature', () => {
|
|
622
736
|
const config = {
|
|
623
|
-
baseUrl: 'http://localhost:
|
|
737
|
+
baseUrl: 'http://localhost:8080',
|
|
624
738
|
apiKey: 'test-api-key',
|
|
625
739
|
};
|
|
626
740
|
|
|
@@ -647,7 +761,7 @@ describe('my new feature', () => {
|
|
|
647
761
|
|
|
648
762
|
// 4. Verify the request (openapi-fetch uses Request objects)
|
|
649
763
|
const request = mockFetch.mock.calls[0]![0] as Request;
|
|
650
|
-
expect(request.url).toBe('http://localhost:
|
|
764
|
+
expect(request.url).toBe('http://localhost:8080/etapi/notes/test123');
|
|
651
765
|
expect(request.method).toBe('GET');
|
|
652
766
|
expect(request.headers.get('Authorization')).toBe('test-api-key');
|
|
653
767
|
});
|