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/dist/index.js ADDED
@@ -0,0 +1,344 @@
1
+ // src/client.ts
2
+ import createClient from "openapi-fetch";
3
+
4
+ // src/mapper.ts
5
+ function buildSearchQuery(helpers) {
6
+ if ("AND" in helpers && Array.isArray(helpers.AND)) {
7
+ return helpers.AND.map((h) => {
8
+ const query = buildSearchQuery(h);
9
+ return query.includes(" OR ") ? `(${query})` : query;
10
+ }).join(" AND ");
11
+ }
12
+ if ("OR" in helpers && Array.isArray(helpers.OR)) {
13
+ return helpers.OR.map((h) => {
14
+ const query = buildSearchQuery(h);
15
+ return query.includes(" AND ") || query.includes(" OR ") ? `(${query})` : query;
16
+ }).join(" OR ");
17
+ }
18
+ if ("NOT" in helpers && helpers.NOT !== void 0) {
19
+ const notValue = helpers.NOT;
20
+ if (typeof notValue === "object" && notValue !== null && !("value" in notValue)) {
21
+ const query = buildSearchQuery(notValue);
22
+ return `not(${query})`;
23
+ }
24
+ throw new Error("NOT operator requires a query object, not a simple value");
25
+ }
26
+ const parts = [];
27
+ for (const [key, value] of Object.entries(helpers)) {
28
+ if (value === void 0 || value === null) continue;
29
+ if (key.startsWith("#")) {
30
+ const labelName = key.slice(1);
31
+ if (labelName.includes(".")) {
32
+ if (typeof value === "object" && "value" in value) {
33
+ const operator = value.operator || "=";
34
+ const val = typeof value.value === "string" ? `'${value.value}'` : value.value;
35
+ parts.push(`${key} ${operator} ${val}`);
36
+ } else if (typeof value === "string") {
37
+ parts.push(`${key} = '${value}'`);
38
+ } else {
39
+ parts.push(`${key} = ${value}`);
40
+ }
41
+ } else {
42
+ if (value === true) {
43
+ parts.push(`#${labelName}`);
44
+ } else if (value === false) {
45
+ parts.push(`#!${labelName}`);
46
+ } else if (typeof value === "object" && "value" in value) {
47
+ const operator = value.operator || "=";
48
+ const val = typeof value.value === "string" ? `'${value.value}'` : value.value;
49
+ parts.push(`#${labelName} ${operator} ${val}`);
50
+ } else if (typeof value === "string") {
51
+ parts.push(`#${labelName} = '${value}'`);
52
+ } else {
53
+ parts.push(`#${labelName} = ${value}`);
54
+ }
55
+ }
56
+ } else if (key.startsWith("~")) {
57
+ const relationName = key.slice(1);
58
+ if (relationName.includes(".")) {
59
+ if (typeof value === "object" && "value" in value) {
60
+ const operator = value.operator || "=";
61
+ const val = typeof value.value === "string" ? `'${value.value}'` : value.value;
62
+ parts.push(`${key} ${operator} ${val}`);
63
+ } else if (typeof value === "string") {
64
+ parts.push(`${key} = '${value}'`);
65
+ } else {
66
+ parts.push(`${key} = ${value}`);
67
+ }
68
+ } else {
69
+ if (typeof value === "object" && "value" in value) {
70
+ const operator = value.operator || "*=*";
71
+ const val = typeof value.value === "string" ? `'${value.value}'` : value.value;
72
+ parts.push(`${key} ${operator} ${val}`);
73
+ } else if (typeof value === "string") {
74
+ parts.push(`${key} *=* '${value}'`);
75
+ }
76
+ }
77
+ } else {
78
+ const path = key.startsWith("note.") ? key : `note.${key}`;
79
+ if (typeof value === "object" && "value" in value) {
80
+ const operator = value.operator || "=";
81
+ const val = typeof value.value === "string" ? `'${value.value}'` : value.value;
82
+ parts.push(`${path} ${operator} ${val}`);
83
+ } else if (typeof value === "string") {
84
+ parts.push(`${path} = '${value}'`);
85
+ } else if (typeof value === "boolean") {
86
+ parts.push(`${path} = ${value}`);
87
+ } else {
88
+ parts.push(`${path} = ${value}`);
89
+ }
90
+ }
91
+ }
92
+ return parts.join(" AND ");
93
+ }
94
+ var TriliumMapper = class {
95
+ /** The mapping configuration for this mapper */
96
+ config;
97
+ /**
98
+ * Creates a new TriliumMapper instance
99
+ * @param config - The mapping configuration defining how to map note fields to the target type
100
+ */
101
+ constructor(config) {
102
+ this.config = config;
103
+ }
104
+ /**
105
+ * Merges multiple mapping configurations into a single configuration
106
+ * Later configs override earlier ones for the same keys
107
+ * Supports merging configs from base types into derived types
108
+ *
109
+ * @template T - The target type for the merged configuration
110
+ * @param configs - One or more mapping configurations to merge
111
+ * @returns A new merged mapping configuration
112
+ *
113
+ * @example
114
+ * const merged = TriliumMapper.merge<BlogPost>(
115
+ * StandardNoteMapping,
116
+ * BlogSpecificMapping,
117
+ * OverrideMapping
118
+ * );
119
+ */
120
+ static merge(...configs) {
121
+ return Object.assign({}, ...configs);
122
+ }
123
+ /**
124
+ * Maps one or more Trilium notes to the target type
125
+ * @param noteOrNotes - A single note or array of notes to map
126
+ * @returns A single mapped object or array of mapped objects
127
+ */
128
+ map(noteOrNotes) {
129
+ return Array.isArray(noteOrNotes) ? noteOrNotes.map((note) => this.mapSingle(note)) : this.mapSingle(noteOrNotes);
130
+ }
131
+ /**
132
+ * Maps a single note to the target type using the configured field mappings
133
+ * Processes in two passes: first regular fields, then computed fields
134
+ * @param note - The Trilium note to map
135
+ * @returns The mapped object
136
+ * @throws Error if a required field is missing
137
+ * @private
138
+ */
139
+ mapSingle(note) {
140
+ const result = {};
141
+ const computedFields = [];
142
+ for (const [key, fieldMapping] of Object.entries(this.config)) {
143
+ if (!fieldMapping) continue;
144
+ if (typeof fieldMapping === "object" && "computed" in fieldMapping) {
145
+ computedFields.push([key, fieldMapping]);
146
+ continue;
147
+ }
148
+ const mapping = typeof fieldMapping === "string" ? { from: fieldMapping } : fieldMapping;
149
+ let value = typeof mapping.from === "function" ? mapping.from(note) : this.extractValue(note, mapping.from);
150
+ if (mapping.transform) {
151
+ value = mapping.transform(value, note);
152
+ }
153
+ if (value === void 0 && mapping.default !== void 0) {
154
+ value = mapping.default;
155
+ }
156
+ if (mapping.required && value === void 0) {
157
+ throw new Error(`Required field '${String(key)}' missing from note ${note.noteId} (${note.title})`);
158
+ }
159
+ result[key] = value;
160
+ }
161
+ for (const [key, mapping] of computedFields) {
162
+ let value = mapping.computed(result, note);
163
+ if (value === void 0 && mapping.default !== void 0) {
164
+ value = mapping.default;
165
+ }
166
+ result[key] = value;
167
+ }
168
+ return result;
169
+ }
170
+ /**
171
+ * Extracts a value from a note using a string path
172
+ *
173
+ * Supports:
174
+ * - Label attributes: #labelName
175
+ * - Relation attributes: ~relationName
176
+ * - Note properties: note.property.path
177
+ *
178
+ * @param note - The Trilium note to extract from
179
+ * @param path - The path string indicating where to extract the value
180
+ * @returns The extracted value or undefined if not found
181
+ * @private
182
+ *
183
+ * @example
184
+ * extractValue(note, 'note.title') // => note.title
185
+ * extractValue(note, '#slug') // => label attribute 'slug'
186
+ * extractValue(note, '~template') // => relation attribute 'template'
187
+ */
188
+ extractValue(note, path) {
189
+ if (!path) return void 0;
190
+ if (path.startsWith("#")) {
191
+ return note.attributes?.find((attr) => attr.type === "label" && attr.name === path.slice(1))?.value;
192
+ }
193
+ if (path.startsWith("~")) {
194
+ return note.attributes?.find((attr) => attr.type === "relation" && attr.name === path.slice(1))?.value;
195
+ }
196
+ if (path.startsWith("note.")) {
197
+ return path.slice(5).split(".").reduce((obj, key) => obj?.[key], note);
198
+ }
199
+ return void 0;
200
+ }
201
+ };
202
+ var transforms = {
203
+ /** Convert to number */
204
+ number: (value) => {
205
+ if (value === void 0 || value === null || value === "") return void 0;
206
+ const num = Number(value);
207
+ return isNaN(num) ? void 0 : num;
208
+ },
209
+ /** Convert to boolean */
210
+ boolean: (value) => {
211
+ if (value === void 0 || value === null) return void 0;
212
+ if (typeof value === "boolean") return value;
213
+ if (typeof value === "string") {
214
+ const lower = value.toLowerCase();
215
+ if (lower === "true" || lower === "1" || lower === "yes") return true;
216
+ if (lower === "false" || lower === "0" || lower === "no") return false;
217
+ }
218
+ return void 0;
219
+ },
220
+ /** Split comma-separated string into array */
221
+ commaSeparated: (value) => {
222
+ if (value === void 0 || value === null || value === "") return void 0;
223
+ if (typeof value !== "string") return void 0;
224
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
225
+ },
226
+ /** Parse JSON string */
227
+ json: (value) => {
228
+ if (value === void 0 || value === null || value === "") return void 0;
229
+ if (typeof value !== "string") return void 0;
230
+ try {
231
+ return JSON.parse(value);
232
+ } catch {
233
+ return void 0;
234
+ }
235
+ },
236
+ /** Parse date string */
237
+ date: (value) => {
238
+ if (value === void 0 || value === null || value === "") return void 0;
239
+ const date = new Date(String(value));
240
+ return isNaN(date.getTime()) ? void 0 : date;
241
+ },
242
+ /** Trim whitespace from string */
243
+ trim: (value) => {
244
+ if (value === void 0 || value === null) return void 0;
245
+ return String(value).trim() || void 0;
246
+ }
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
+ };
268
+
269
+ // src/client.ts
270
+ function createTriliumClient(config) {
271
+ const baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl.slice(0, -1) : config.baseUrl;
272
+ const client = createClient({
273
+ baseUrl: `${baseUrl}/etapi`,
274
+ headers: {
275
+ Authorization: config.apiKey
276
+ }
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 });
335
+ }
336
+ var client_default = createTriliumClient;
337
+ export {
338
+ StandardNoteMapping,
339
+ TriliumMapper,
340
+ buildSearchQuery,
341
+ client_default as createClient,
342
+ createTriliumClient,
343
+ transforms
344
+ };
package/package.json CHANGED
@@ -1,9 +1,26 @@
1
1
  {
2
2
  "name": "trilium-api",
3
- "version": "1.0.1",
4
- "description": "",
3
+ "version": "1.0.4",
4
+ "description": "A type-safe TypeScript client for the Trilium Notes ETAPI",
5
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
7
24
  "scripts": {
8
25
  "build": "tsup src/index.ts --format cjs,esm --dts",
9
26
  "demo": "tsx src/demo.ts",
@@ -16,9 +33,23 @@
16
33
  "test:ts": "tsc --noEmit",
17
34
  "prepublishOnly": "pnpm build"
18
35
  },
19
- "keywords": [],
20
- "author": "",
36
+ "keywords": [
37
+ "trilium",
38
+ "trilium-notes",
39
+ "etapi",
40
+ "api-client",
41
+ "typescript"
42
+ ],
43
+ "author": "lzinga",
21
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",
22
53
  "packageManager": "pnpm@10.25.0",
23
54
  "devDependencies": {
24
55
  "@types/node": "^25.0.3",
@@ -1,37 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main, master]
6
- pull_request:
7
- branches: [main, master]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [18, 20, 22]
16
-
17
- steps:
18
- - name: Checkout repository
19
- uses: actions/checkout@v4
20
-
21
- - name: Install pnpm
22
- uses: pnpm/action-setup@v4
23
-
24
- - name: Setup Node.js ${{ matrix.node-version }}
25
- uses: actions/setup-node@v4
26
- with:
27
- node-version: ${{ matrix.node-version }}
28
- cache: 'pnpm'
29
-
30
- - name: Install dependencies
31
- run: pnpm install
32
-
33
- - name: Type check
34
- run: pnpm test:ts
35
-
36
- - name: Run tests
37
- run: pnpm test:run
@@ -1,84 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- version:
7
- description: 'Version bump type'
8
- required: true
9
- default: 'patch'
10
- type: choice
11
- options:
12
- - patch
13
- - minor
14
- - major
15
-
16
- jobs:
17
- publish:
18
- runs-on: ubuntu-latest
19
-
20
- permissions:
21
- contents: write
22
- id-token: write # Required for npm trusted publishing (OIDC)
23
-
24
- steps:
25
- - name: Checkout repository
26
- uses: actions/checkout@v4
27
- with:
28
- fetch-depth: 0
29
- token: ${{ secrets.GITHUB_TOKEN }}
30
-
31
- - name: Configure Git
32
- run: |
33
- git config user.name "github-actions[bot]"
34
- git config user.email "github-actions[bot]@users.noreply.github.com"
35
-
36
- - name: Install pnpm
37
- uses: pnpm/action-setup@v4
38
-
39
- - name: Setup Node.js
40
- uses: actions/setup-node@v4
41
- with:
42
- node-version: 22
43
- cache: 'pnpm'
44
- registry-url: 'https://registry.npmjs.org'
45
-
46
- - name: Update npm to latest
47
- run: npm install -g npm@latest
48
-
49
- - name: Install dependencies
50
- run: pnpm install
51
-
52
- - name: Type check
53
- run: pnpm test:ts
54
-
55
- - name: Run tests
56
- run: pnpm test:run
57
-
58
- - name: Bump version
59
- id: version
60
- run: |
61
- pnpm version ${{ inputs.version }} --no-git-tag-version
62
- VERSION=$(node -p "require('./package.json').version")
63
- echo "version=$VERSION" >> $GITHUB_OUTPUT
64
-
65
- - name: Commit and tag
66
- run: |
67
- git add package.json
68
- git commit -m "chore: release v${{ steps.version.outputs.version }}"
69
- git tag "v${{ steps.version.outputs.version }}"
70
- git push && git push --tags
71
-
72
- - name: Build
73
- run: pnpm build
74
-
75
- - name: Publish to npm
76
- run: npm publish --access public
77
- # Uses OIDC trusted publishing - no NPM_TOKEN needed
78
-
79
- - name: Create GitHub Release
80
- uses: softprops/action-gh-release@v2
81
- with:
82
- tag_name: v${{ steps.version.outputs.version }}
83
- name: v${{ steps.version.outputs.version }}
84
- generate_release_notes: true