trilium-api 1.0.2 → 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
  });
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ StandardNoteMapping: () => StandardNoteMapping,
33
34
  TriliumMapper: () => TriliumMapper,
34
35
  buildSearchQuery: () => buildSearchQuery,
35
36
  createClient: () => client_default,
@@ -285,20 +286,98 @@ var transforms = {
285
286
  return String(value).trim() || void 0;
286
287
  }
287
288
  };
289
+ var StandardNoteMapping = {
290
+ id: {
291
+ from: "note.noteId",
292
+ required: true
293
+ },
294
+ title: {
295
+ from: "note.title",
296
+ required: true
297
+ },
298
+ dateCreatedUtc: {
299
+ from: "note.utcDateCreated",
300
+ transform: transforms.date,
301
+ required: true
302
+ },
303
+ dateLastModifiedUtc: {
304
+ from: "note.utcDateModified",
305
+ transform: transforms.date,
306
+ required: true
307
+ }
308
+ };
288
309
 
289
310
  // src/client.ts
290
311
  function createTriliumClient(config) {
291
312
  const baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl.slice(0, -1) : config.baseUrl;
292
- return (0, import_openapi_fetch.default)({
313
+ const client = (0, import_openapi_fetch.default)({
293
314
  baseUrl: `${baseUrl}/etapi`,
294
315
  headers: {
295
316
  Authorization: config.apiKey
296
317
  }
297
318
  });
319
+ const searchAndMap = async (options) => {
320
+ const searchQuery = typeof options.query === "string" ? options.query : buildSearchQuery(options.query);
321
+ const params = [];
322
+ if (options.orderBy) {
323
+ params.push(`orderBy:${options.orderBy}`);
324
+ if (options.orderDirection) {
325
+ params.push(options.orderDirection);
326
+ }
327
+ }
328
+ if (options.limit) {
329
+ params.push(`limit:${options.limit}`);
330
+ }
331
+ if (options.fastSearch) {
332
+ params.push("fastSearch");
333
+ }
334
+ const fullQuery = params.length > 0 ? `${searchQuery} ${params.join(" ")}` : searchQuery;
335
+ const { data, error } = await client.GET("/notes", {
336
+ params: { query: { search: fullQuery } }
337
+ });
338
+ if (error) {
339
+ throw error;
340
+ }
341
+ if (!data?.results) {
342
+ throw new Error("No results returned from search");
343
+ }
344
+ const fullMapping = TriliumMapper.merge(
345
+ StandardNoteMapping,
346
+ options.mapping
347
+ );
348
+ const mapper = new TriliumMapper(fullMapping);
349
+ const mappedData = [];
350
+ const failures = [];
351
+ for (const note of data.results) {
352
+ try {
353
+ const [mapped] = mapper.map([note]);
354
+ if (mapped !== void 0) {
355
+ mappedData.push(mapped);
356
+ } else {
357
+ failures.push({
358
+ noteId: note.noteId ?? "unknown",
359
+ noteTitle: note.title ?? "Untitled",
360
+ reason: "Mapping returned undefined",
361
+ note
362
+ });
363
+ }
364
+ } catch (err) {
365
+ failures.push({
366
+ noteId: note.noteId ?? "unknown",
367
+ noteTitle: note.title ?? "Untitled",
368
+ reason: err instanceof Error ? err.message : String(err),
369
+ note
370
+ });
371
+ }
372
+ }
373
+ return { data: mappedData, failures };
374
+ };
375
+ return Object.assign(client, { searchAndMap });
298
376
  }
299
377
  var client_default = createTriliumClient;
300
378
  // Annotate the CommonJS export names for ESM import in node:
301
379
  0 && (module.exports = {
380
+ StandardNoteMapping,
302
381
  TriliumMapper,
303
382
  buildSearchQuery,
304
383
  createClient,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import * as openapi_fetch from 'openapi-fetch';
1
+ import { Client } from 'openapi-fetch';
2
2
 
3
3
  /**
4
4
  * This file was auto-generated by openapi-typescript.
@@ -1848,12 +1848,6 @@ type TriliumSearchHelpers = TriliumSearchLogical | (TriliumSearchConditions & Pa
1848
1848
  * // => "note.type = 'text' AND #wordCount >= 1000"
1849
1849
  */
1850
1850
  declare function buildSearchQuery(helpers: TriliumSearchHelpers): string;
1851
- /**
1852
- * Transform function that converts a raw value into the target type
1853
- * @template T - The target object type
1854
- * @template K - The specific key in the target type
1855
- */
1856
- type TransformFunction<T, K extends keyof T> = (value: unknown, note: TriliumNote) => T[K] | undefined;
1857
1851
  /**
1858
1852
  * Computed function that calculates a value from the partially mapped object
1859
1853
  * @template T - The target object type
@@ -1889,8 +1883,8 @@ type ComputedFunction<T, K extends keyof T> = (partial: Partial<T>, note: Triliu
1889
1883
  type FieldMapping<T, K extends keyof T = keyof T> = string | {
1890
1884
  /** Source path (string) or extractor function */
1891
1885
  from: string | ((note: TriliumNote) => unknown);
1892
- /** Optional transform function to convert the raw value */
1893
- transform?: TransformFunction<T, K>;
1886
+ /** Optional transform function to convert the raw value - accepts any input type */
1887
+ transform?: (value: any, note: TriliumNote) => T[K] | undefined;
1894
1888
  /** Default value if extraction returns undefined */
1895
1889
  default?: T[K];
1896
1890
  /** Whether this field is required (throws if missing) */
@@ -1939,7 +1933,7 @@ type MappingConfig<T> = {
1939
1933
  */
1940
1934
  declare class TriliumMapper<T> {
1941
1935
  /** The mapping configuration for this mapper */
1942
- private readonly config;
1936
+ readonly config: MappingConfig<T>;
1943
1937
  /**
1944
1938
  * Creates a new TriliumMapper instance
1945
1939
  * @param config - The mapping configuration defining how to map note fields to the target type
@@ -2020,6 +2014,83 @@ declare const transforms: {
2020
2014
  /** Trim whitespace from string */
2021
2015
  trim: (value: unknown) => string | undefined;
2022
2016
  };
2017
+ /**
2018
+ * Standard note fields that all mapped types must include.
2019
+ * Extend this interface when creating your own mapped types.
2020
+ *
2021
+ * @example
2022
+ * ```ts
2023
+ * interface BlogPost extends StandardNote {
2024
+ * slug: string;
2025
+ * published: boolean;
2026
+ * }
2027
+ * ```
2028
+ */
2029
+ interface StandardNote {
2030
+ /** The unique note ID */
2031
+ id: string;
2032
+ /** The note title */
2033
+ title: string;
2034
+ /** UTC date when the note was created */
2035
+ dateCreatedUtc: Date;
2036
+ /** UTC date when the note was last modified */
2037
+ dateLastModifiedUtc: Date;
2038
+ }
2039
+ /**
2040
+ * Standard mapping configuration for StandardNote fields.
2041
+ * Use with TriliumMapper.merge() to create mappings for types extending StandardNote.
2042
+ *
2043
+ * @example
2044
+ * ```ts
2045
+ * interface BlogPost extends StandardNote {
2046
+ * slug: string;
2047
+ * published: boolean;
2048
+ * }
2049
+ *
2050
+ * const blogMapping = TriliumMapper.merge<BlogPost>(
2051
+ * StandardNoteMapping,
2052
+ * {
2053
+ * slug: '#slug',
2054
+ * published: { from: '#published', transform: transforms.boolean, default: false },
2055
+ * }
2056
+ * );
2057
+ * ```
2058
+ */
2059
+ declare const StandardNoteMapping: MappingConfig<StandardNote>;
2060
+ /**
2061
+ * Helper type for defining custom field mappings.
2062
+ * Use this when defining mappings for types that extend StandardNote.
2063
+ * It automatically excludes StandardNote fields since they're auto-merged.
2064
+ *
2065
+ * @template T - Your custom type that extends StandardNote
2066
+ *
2067
+ * @example
2068
+ * ```ts
2069
+ * interface BlogPost extends StandardNote {
2070
+ * slug: string;
2071
+ * published: boolean;
2072
+ * }
2073
+ *
2074
+ * // Clean type - no need for verbose Omit<>
2075
+ * const blogMapping: CustomMapping<BlogPost> = {
2076
+ * slug: '#slug',
2077
+ * published: { from: '#published', transform: transforms.boolean, default: false },
2078
+ * };
2079
+ *
2080
+ * const { data } = await client.searchAndMap<BlogPost>({
2081
+ * query: '#blog',
2082
+ * mapping: blogMapping,
2083
+ * });
2084
+ * ```
2085
+ */
2086
+ type CustomMapping<T extends StandardNote> = MappingConfig<Omit<T, keyof StandardNote>>;
2087
+
2088
+ /**
2089
+ * Trilium API Client using openapi-fetch
2090
+ *
2091
+ * This provides a type-safe client for the Trilium ETAPI.
2092
+ * Types are auto-generated from the OpenAPI specification.
2093
+ */
2023
2094
 
2024
2095
  type TriliumNote = components['schemas']['Note'];
2025
2096
  type TriliumBranch = components['schemas']['Branch'];
@@ -2031,13 +2102,88 @@ interface TriliumClientConfig {
2031
2102
  baseUrl: string;
2032
2103
  apiKey: string;
2033
2104
  }
2105
+ interface SearchAndMapOptions<T extends StandardNote> {
2106
+ /** Search query - either a string or structured search helpers */
2107
+ query: string | TriliumSearchHelpers;
2108
+ /**
2109
+ * Mapping configuration for your custom fields only.
2110
+ * StandardNoteMapping (id, title, dates) is automatically merged.
2111
+ */
2112
+ mapping: CustomMapping<T>;
2113
+ /** Optional: limit number of results */
2114
+ limit?: number;
2115
+ /** Optional: order by field (e.g., 'dateModified', 'title') */
2116
+ orderBy?: string;
2117
+ /** Optional: order direction */
2118
+ orderDirection?: 'asc' | 'desc';
2119
+ /** Optional: fast search mode (less accurate but faster) */
2120
+ fastSearch?: boolean;
2121
+ }
2122
+ /** Details about a note that failed to map */
2123
+ interface MappingFailure {
2124
+ /** The note ID that failed to map */
2125
+ noteId: string;
2126
+ /** The note title for easier identification */
2127
+ noteTitle: string;
2128
+ /** The error message explaining why mapping failed */
2129
+ reason: string;
2130
+ /** The original note object */
2131
+ note: TriliumNote;
2132
+ }
2133
+ interface SearchAndMapResult<T extends StandardNote> {
2134
+ /** Mapped results as typed objects */
2135
+ data: T[];
2136
+ /** Notes that failed to map (e.g., missing required fields) */
2137
+ failures: MappingFailure[];
2138
+ }
2139
+ /** Extended Trilium client with search and map helper */
2140
+ interface TriliumClient extends Client<paths> {
2141
+ /**
2142
+ * Search notes and automatically map results to typed objects.
2143
+ * Type T must extend StandardNote to ensure consistent base fields.
2144
+ * StandardNoteMapping is automatically included - just define your custom fields!
2145
+ * Throws on API/network errors.
2146
+ *
2147
+ * @see {@link https://triliumnext.github.io/Docs/Wiki/search.html} for Trilium search syntax
2148
+ *
2149
+ * @example
2150
+ * ```ts
2151
+ * interface BlogPost extends StandardNote {
2152
+ * slug: string;
2153
+ * published: boolean;
2154
+ * }
2155
+ *
2156
+ * // Just define your custom fields - StandardNoteMapping is auto-merged!
2157
+ * const { data: posts, failures } = await client.searchAndMap<BlogPost>({
2158
+ * query: { '#blog': true, '#published': true },
2159
+ * mapping: {
2160
+ * slug: '#slug',
2161
+ * published: { from: '#published', transform: transforms.boolean, default: false },
2162
+ * },
2163
+ * limit: 10,
2164
+ * orderBy: 'dateModified',
2165
+ * orderDirection: 'desc',
2166
+ * });
2167
+ *
2168
+ * // Each post has id, title, dateCreatedUtc, dateLastModifiedUtc + your custom fields
2169
+ * posts.forEach(post => {
2170
+ * console.log(`${post.title} (${post.slug}) - ${post.id}`);
2171
+ * });
2172
+ *
2173
+ * if (failures.length > 0) {
2174
+ * console.warn(`${failures.length} notes failed to map`);
2175
+ * }
2176
+ * ```
2177
+ */
2178
+ searchAndMap<T extends StandardNote>(options: SearchAndMapOptions<T>): Promise<SearchAndMapResult<T>>;
2179
+ }
2034
2180
  /**
2035
2181
  * Create a type-safe Trilium API client
2036
2182
  *
2037
2183
  * @example
2038
2184
  * ```ts
2039
2185
  * const client = createTriliumClient({
2040
- * baseUrl: 'http://localhost:37840',
2186
+ * baseUrl: 'http://localhost:8080',
2041
2187
  * apiKey: 'your-etapi-token'
2042
2188
  * });
2043
2189
  *
@@ -2063,8 +2209,17 @@ interface TriliumClientConfig {
2063
2209
  * const { data: searchResults } = await client.GET('/notes', {
2064
2210
  * params: { query: { search: '#blog' } }
2065
2211
  * });
2212
+ *
2213
+ * // Search and map to typed objects
2214
+ * const { data: posts } = await client.searchAndMap<BlogPost>({
2215
+ * query: { '#blog': true },
2216
+ * mapping: {
2217
+ * title: 'note.title',
2218
+ * slug: '#slug',
2219
+ * },
2220
+ * });
2066
2221
  * ```
2067
2222
  */
2068
- declare function createTriliumClient(config: TriliumClientConfig): openapi_fetch.Client<paths, `${string}/${string}`>;
2223
+ declare function createTriliumClient(config: TriliumClientConfig): TriliumClient;
2069
2224
 
2070
- export { type ComparisonOperator, type ComputedFunction, type ConditionValue, type FieldMapping, type MappingConfig, type SearchValue, type TransformFunction, type TriliumAppInfo, type TriliumAttachment, type TriliumAttribute, type TriliumBranch, type TriliumClientConfig, TriliumMapper, type TriliumNote, type TriliumSearchConditions, type TriliumSearchHelpers, type TriliumSearchLogical, buildSearchQuery, type components, createTriliumClient as createClient, createTriliumClient, type operations, type paths, transforms };
2225
+ export { type CustomMapping, type MappingConfig, type MappingFailure, type StandardNote, StandardNoteMapping, type TriliumAppInfo, type TriliumAttachment, type TriliumAttribute, type TriliumBranch, type TriliumClientConfig, TriliumMapper, type TriliumNote, type TriliumSearchHelpers, buildSearchQuery, type components, createTriliumClient as createClient, createTriliumClient, type operations, type paths, transforms };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as openapi_fetch from 'openapi-fetch';
1
+ import { Client } from 'openapi-fetch';
2
2
 
3
3
  /**
4
4
  * This file was auto-generated by openapi-typescript.
@@ -1848,12 +1848,6 @@ type TriliumSearchHelpers = TriliumSearchLogical | (TriliumSearchConditions & Pa
1848
1848
  * // => "note.type = 'text' AND #wordCount >= 1000"
1849
1849
  */
1850
1850
  declare function buildSearchQuery(helpers: TriliumSearchHelpers): string;
1851
- /**
1852
- * Transform function that converts a raw value into the target type
1853
- * @template T - The target object type
1854
- * @template K - The specific key in the target type
1855
- */
1856
- type TransformFunction<T, K extends keyof T> = (value: unknown, note: TriliumNote) => T[K] | undefined;
1857
1851
  /**
1858
1852
  * Computed function that calculates a value from the partially mapped object
1859
1853
  * @template T - The target object type
@@ -1889,8 +1883,8 @@ type ComputedFunction<T, K extends keyof T> = (partial: Partial<T>, note: Triliu
1889
1883
  type FieldMapping<T, K extends keyof T = keyof T> = string | {
1890
1884
  /** Source path (string) or extractor function */
1891
1885
  from: string | ((note: TriliumNote) => unknown);
1892
- /** Optional transform function to convert the raw value */
1893
- transform?: TransformFunction<T, K>;
1886
+ /** Optional transform function to convert the raw value - accepts any input type */
1887
+ transform?: (value: any, note: TriliumNote) => T[K] | undefined;
1894
1888
  /** Default value if extraction returns undefined */
1895
1889
  default?: T[K];
1896
1890
  /** Whether this field is required (throws if missing) */
@@ -1939,7 +1933,7 @@ type MappingConfig<T> = {
1939
1933
  */
1940
1934
  declare class TriliumMapper<T> {
1941
1935
  /** The mapping configuration for this mapper */
1942
- private readonly config;
1936
+ readonly config: MappingConfig<T>;
1943
1937
  /**
1944
1938
  * Creates a new TriliumMapper instance
1945
1939
  * @param config - The mapping configuration defining how to map note fields to the target type
@@ -2020,6 +2014,83 @@ declare const transforms: {
2020
2014
  /** Trim whitespace from string */
2021
2015
  trim: (value: unknown) => string | undefined;
2022
2016
  };
2017
+ /**
2018
+ * Standard note fields that all mapped types must include.
2019
+ * Extend this interface when creating your own mapped types.
2020
+ *
2021
+ * @example
2022
+ * ```ts
2023
+ * interface BlogPost extends StandardNote {
2024
+ * slug: string;
2025
+ * published: boolean;
2026
+ * }
2027
+ * ```
2028
+ */
2029
+ interface StandardNote {
2030
+ /** The unique note ID */
2031
+ id: string;
2032
+ /** The note title */
2033
+ title: string;
2034
+ /** UTC date when the note was created */
2035
+ dateCreatedUtc: Date;
2036
+ /** UTC date when the note was last modified */
2037
+ dateLastModifiedUtc: Date;
2038
+ }
2039
+ /**
2040
+ * Standard mapping configuration for StandardNote fields.
2041
+ * Use with TriliumMapper.merge() to create mappings for types extending StandardNote.
2042
+ *
2043
+ * @example
2044
+ * ```ts
2045
+ * interface BlogPost extends StandardNote {
2046
+ * slug: string;
2047
+ * published: boolean;
2048
+ * }
2049
+ *
2050
+ * const blogMapping = TriliumMapper.merge<BlogPost>(
2051
+ * StandardNoteMapping,
2052
+ * {
2053
+ * slug: '#slug',
2054
+ * published: { from: '#published', transform: transforms.boolean, default: false },
2055
+ * }
2056
+ * );
2057
+ * ```
2058
+ */
2059
+ declare const StandardNoteMapping: MappingConfig<StandardNote>;
2060
+ /**
2061
+ * Helper type for defining custom field mappings.
2062
+ * Use this when defining mappings for types that extend StandardNote.
2063
+ * It automatically excludes StandardNote fields since they're auto-merged.
2064
+ *
2065
+ * @template T - Your custom type that extends StandardNote
2066
+ *
2067
+ * @example
2068
+ * ```ts
2069
+ * interface BlogPost extends StandardNote {
2070
+ * slug: string;
2071
+ * published: boolean;
2072
+ * }
2073
+ *
2074
+ * // Clean type - no need for verbose Omit<>
2075
+ * const blogMapping: CustomMapping<BlogPost> = {
2076
+ * slug: '#slug',
2077
+ * published: { from: '#published', transform: transforms.boolean, default: false },
2078
+ * };
2079
+ *
2080
+ * const { data } = await client.searchAndMap<BlogPost>({
2081
+ * query: '#blog',
2082
+ * mapping: blogMapping,
2083
+ * });
2084
+ * ```
2085
+ */
2086
+ type CustomMapping<T extends StandardNote> = MappingConfig<Omit<T, keyof StandardNote>>;
2087
+
2088
+ /**
2089
+ * Trilium API Client using openapi-fetch
2090
+ *
2091
+ * This provides a type-safe client for the Trilium ETAPI.
2092
+ * Types are auto-generated from the OpenAPI specification.
2093
+ */
2023
2094
 
2024
2095
  type TriliumNote = components['schemas']['Note'];
2025
2096
  type TriliumBranch = components['schemas']['Branch'];
@@ -2031,13 +2102,88 @@ interface TriliumClientConfig {
2031
2102
  baseUrl: string;
2032
2103
  apiKey: string;
2033
2104
  }
2105
+ interface SearchAndMapOptions<T extends StandardNote> {
2106
+ /** Search query - either a string or structured search helpers */
2107
+ query: string | TriliumSearchHelpers;
2108
+ /**
2109
+ * Mapping configuration for your custom fields only.
2110
+ * StandardNoteMapping (id, title, dates) is automatically merged.
2111
+ */
2112
+ mapping: CustomMapping<T>;
2113
+ /** Optional: limit number of results */
2114
+ limit?: number;
2115
+ /** Optional: order by field (e.g., 'dateModified', 'title') */
2116
+ orderBy?: string;
2117
+ /** Optional: order direction */
2118
+ orderDirection?: 'asc' | 'desc';
2119
+ /** Optional: fast search mode (less accurate but faster) */
2120
+ fastSearch?: boolean;
2121
+ }
2122
+ /** Details about a note that failed to map */
2123
+ interface MappingFailure {
2124
+ /** The note ID that failed to map */
2125
+ noteId: string;
2126
+ /** The note title for easier identification */
2127
+ noteTitle: string;
2128
+ /** The error message explaining why mapping failed */
2129
+ reason: string;
2130
+ /** The original note object */
2131
+ note: TriliumNote;
2132
+ }
2133
+ interface SearchAndMapResult<T extends StandardNote> {
2134
+ /** Mapped results as typed objects */
2135
+ data: T[];
2136
+ /** Notes that failed to map (e.g., missing required fields) */
2137
+ failures: MappingFailure[];
2138
+ }
2139
+ /** Extended Trilium client with search and map helper */
2140
+ interface TriliumClient extends Client<paths> {
2141
+ /**
2142
+ * Search notes and automatically map results to typed objects.
2143
+ * Type T must extend StandardNote to ensure consistent base fields.
2144
+ * StandardNoteMapping is automatically included - just define your custom fields!
2145
+ * Throws on API/network errors.
2146
+ *
2147
+ * @see {@link https://triliumnext.github.io/Docs/Wiki/search.html} for Trilium search syntax
2148
+ *
2149
+ * @example
2150
+ * ```ts
2151
+ * interface BlogPost extends StandardNote {
2152
+ * slug: string;
2153
+ * published: boolean;
2154
+ * }
2155
+ *
2156
+ * // Just define your custom fields - StandardNoteMapping is auto-merged!
2157
+ * const { data: posts, failures } = await client.searchAndMap<BlogPost>({
2158
+ * query: { '#blog': true, '#published': true },
2159
+ * mapping: {
2160
+ * slug: '#slug',
2161
+ * published: { from: '#published', transform: transforms.boolean, default: false },
2162
+ * },
2163
+ * limit: 10,
2164
+ * orderBy: 'dateModified',
2165
+ * orderDirection: 'desc',
2166
+ * });
2167
+ *
2168
+ * // Each post has id, title, dateCreatedUtc, dateLastModifiedUtc + your custom fields
2169
+ * posts.forEach(post => {
2170
+ * console.log(`${post.title} (${post.slug}) - ${post.id}`);
2171
+ * });
2172
+ *
2173
+ * if (failures.length > 0) {
2174
+ * console.warn(`${failures.length} notes failed to map`);
2175
+ * }
2176
+ * ```
2177
+ */
2178
+ searchAndMap<T extends StandardNote>(options: SearchAndMapOptions<T>): Promise<SearchAndMapResult<T>>;
2179
+ }
2034
2180
  /**
2035
2181
  * Create a type-safe Trilium API client
2036
2182
  *
2037
2183
  * @example
2038
2184
  * ```ts
2039
2185
  * const client = createTriliumClient({
2040
- * baseUrl: 'http://localhost:37840',
2186
+ * baseUrl: 'http://localhost:8080',
2041
2187
  * apiKey: 'your-etapi-token'
2042
2188
  * });
2043
2189
  *
@@ -2063,8 +2209,17 @@ interface TriliumClientConfig {
2063
2209
  * const { data: searchResults } = await client.GET('/notes', {
2064
2210
  * params: { query: { search: '#blog' } }
2065
2211
  * });
2212
+ *
2213
+ * // Search and map to typed objects
2214
+ * const { data: posts } = await client.searchAndMap<BlogPost>({
2215
+ * query: { '#blog': true },
2216
+ * mapping: {
2217
+ * title: 'note.title',
2218
+ * slug: '#slug',
2219
+ * },
2220
+ * });
2066
2221
  * ```
2067
2222
  */
2068
- declare function createTriliumClient(config: TriliumClientConfig): openapi_fetch.Client<paths, `${string}/${string}`>;
2223
+ declare function createTriliumClient(config: TriliumClientConfig): TriliumClient;
2069
2224
 
2070
- export { type ComparisonOperator, type ComputedFunction, type ConditionValue, type FieldMapping, type MappingConfig, type SearchValue, type TransformFunction, type TriliumAppInfo, type TriliumAttachment, type TriliumAttribute, type TriliumBranch, type TriliumClientConfig, TriliumMapper, type TriliumNote, type TriliumSearchConditions, type TriliumSearchHelpers, type TriliumSearchLogical, buildSearchQuery, type components, createTriliumClient as createClient, createTriliumClient, type operations, type paths, transforms };
2225
+ export { type CustomMapping, type MappingConfig, type MappingFailure, type StandardNote, StandardNoteMapping, type TriliumAppInfo, type TriliumAttachment, type TriliumAttribute, type TriliumBranch, type TriliumClientConfig, TriliumMapper, type TriliumNote, type TriliumSearchHelpers, buildSearchQuery, type components, createTriliumClient as createClient, createTriliumClient, type operations, type paths, transforms };
package/dist/index.js CHANGED
@@ -245,19 +245,97 @@ var transforms = {
245
245
  return String(value).trim() || void 0;
246
246
  }
247
247
  };
248
+ var StandardNoteMapping = {
249
+ id: {
250
+ from: "note.noteId",
251
+ required: true
252
+ },
253
+ title: {
254
+ from: "note.title",
255
+ required: true
256
+ },
257
+ dateCreatedUtc: {
258
+ from: "note.utcDateCreated",
259
+ transform: transforms.date,
260
+ required: true
261
+ },
262
+ dateLastModifiedUtc: {
263
+ from: "note.utcDateModified",
264
+ transform: transforms.date,
265
+ required: true
266
+ }
267
+ };
248
268
 
249
269
  // src/client.ts
250
270
  function createTriliumClient(config) {
251
271
  const baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl.slice(0, -1) : config.baseUrl;
252
- return createClient({
272
+ const client = createClient({
253
273
  baseUrl: `${baseUrl}/etapi`,
254
274
  headers: {
255
275
  Authorization: config.apiKey
256
276
  }
257
277
  });
278
+ const searchAndMap = async (options) => {
279
+ const searchQuery = typeof options.query === "string" ? options.query : buildSearchQuery(options.query);
280
+ const params = [];
281
+ if (options.orderBy) {
282
+ params.push(`orderBy:${options.orderBy}`);
283
+ if (options.orderDirection) {
284
+ params.push(options.orderDirection);
285
+ }
286
+ }
287
+ if (options.limit) {
288
+ params.push(`limit:${options.limit}`);
289
+ }
290
+ if (options.fastSearch) {
291
+ params.push("fastSearch");
292
+ }
293
+ const fullQuery = params.length > 0 ? `${searchQuery} ${params.join(" ")}` : searchQuery;
294
+ const { data, error } = await client.GET("/notes", {
295
+ params: { query: { search: fullQuery } }
296
+ });
297
+ if (error) {
298
+ throw error;
299
+ }
300
+ if (!data?.results) {
301
+ throw new Error("No results returned from search");
302
+ }
303
+ const fullMapping = TriliumMapper.merge(
304
+ StandardNoteMapping,
305
+ options.mapping
306
+ );
307
+ const mapper = new TriliumMapper(fullMapping);
308
+ const mappedData = [];
309
+ const failures = [];
310
+ for (const note of data.results) {
311
+ try {
312
+ const [mapped] = mapper.map([note]);
313
+ if (mapped !== void 0) {
314
+ mappedData.push(mapped);
315
+ } else {
316
+ failures.push({
317
+ noteId: note.noteId ?? "unknown",
318
+ noteTitle: note.title ?? "Untitled",
319
+ reason: "Mapping returned undefined",
320
+ note
321
+ });
322
+ }
323
+ } catch (err) {
324
+ failures.push({
325
+ noteId: note.noteId ?? "unknown",
326
+ noteTitle: note.title ?? "Untitled",
327
+ reason: err instanceof Error ? err.message : String(err),
328
+ note
329
+ });
330
+ }
331
+ }
332
+ return { data: mappedData, failures };
333
+ };
334
+ return Object.assign(client, { searchAndMap });
258
335
  }
259
336
  var client_default = createTriliumClient;
260
337
  export {
338
+ StandardNoteMapping,
261
339
  TriliumMapper,
262
340
  buildSearchQuery,
263
341
  client_default as createClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trilium-api",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A type-safe TypeScript client for the Trilium Notes ETAPI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -33,9 +33,23 @@
33
33
  "test:ts": "tsc --noEmit",
34
34
  "prepublishOnly": "pnpm build"
35
35
  },
36
- "keywords": [],
37
- "author": "",
36
+ "keywords": [
37
+ "trilium",
38
+ "trilium-notes",
39
+ "etapi",
40
+ "api-client",
41
+ "typescript"
42
+ ],
43
+ "author": "lzinga",
38
44
  "license": "AGPL-3.0",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/lzinga/trilium-api"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/lzinga/trilium-api/issues"
51
+ },
52
+ "homepage": "https://github.com/lzinga/trilium-api#readme",
39
53
  "packageManager": "pnpm@10.25.0",
40
54
  "devDependencies": {
41
55
  "@types/node": "^25.0.3",