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 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:37840',
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:37840', // Your Trilium server URL
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 { TriliumMapper, transforms } from 'trilium-api';
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
- // Define your target type
270
- interface BlogPost {
271
- id: string;
272
- title: string;
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
- // Create a mapper
282
- const blogMapper = new TriliumMapper<BlogPost>({
283
- // Simple property mapping (shorthand)
284
- id: 'note.noteId',
285
- title: 'note.title',
286
-
287
- // Label attribute with required validation
288
- slug: { from: '#slug', required: true },
289
-
290
- // Transform string to Date
291
- publishDate: { from: '#publishDate', transform: transforms.date },
292
-
293
- // Transform string to number with default
294
- wordCount: { from: '#wordCount', transform: transforms.number, default: 0 },
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 a single note
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
- ### Merging Configurations
381
+ ## Search and Map
373
382
 
374
- Reuse and extend mapping configurations:
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
- // Base configuration for common fields
378
- const baseMapping = {
379
- id: 'note.noteId',
380
- title: 'note.title',
381
- createdAt: { from: 'note.utcDateCreated', transform: transforms.date },
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
- // Extended configuration for blog posts
385
- const blogMapping = {
399
+ // Use CustomMapping<T> for clean typing - excludes StandardNote fields automatically
400
+ const blogMapping: CustomMapping<BlogPost> = {
386
401
  slug: '#slug',
387
- tags: { from: '#tags', transform: transforms.commaSeparated, default: [] },
402
+ published: { from: '#published', transform: transforms.boolean, default: false },
388
403
  };
389
404
 
390
- // Merge configurations
391
- const merged = TriliumMapper.merge<BlogPost>(baseMapping, blogMapping);
392
- const mapper = new TriliumMapper<BlogPost>(merged);
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 all types from the OpenAPI specification:
494
+ The package exports a focused set of types for common use cases:
398
495
 
399
496
  ```typescript
400
- import type {
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:37840',
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:37840/etapi/notes/test123');
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
  });