trilium-api 1.0.1 → 1.0.2
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.cjs +307 -0
- package/{src/generated/trilium.d.ts → dist/index.d.cts} +317 -5
- package/dist/index.d.ts +2070 -0
- package/dist/index.js +266 -0
- package/package.json +20 -3
- 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/src/mapper.test.ts
DELETED
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildSearchQuery, TriliumMapper, transforms } from './mapper.js';
|
|
3
|
-
import type { TriliumNote } from './client.js';
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// buildSearchQuery Tests
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
describe('buildSearchQuery', () => {
|
|
10
|
-
describe('label conditions', () => {
|
|
11
|
-
it('should handle boolean true label (presence check)', () => {
|
|
12
|
-
expect(buildSearchQuery({ '#blog': true })).toBe('#blog');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should handle boolean false label (absence check)', () => {
|
|
16
|
-
expect(buildSearchQuery({ '#draft': false })).toBe('#!draft');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should handle label with string value', () => {
|
|
20
|
-
expect(buildSearchQuery({ '#status': 'published' })).toBe("#status = 'published'");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should handle label with number value', () => {
|
|
24
|
-
expect(buildSearchQuery({ '#priority': 5 })).toBe('#priority = 5');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should handle label with operator', () => {
|
|
28
|
-
expect(buildSearchQuery({ '#wordCount': { value: 1000, operator: '>=' } })).toBe('#wordCount >= 1000');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should handle label with contains operator', () => {
|
|
32
|
-
expect(buildSearchQuery({ '#tags': { value: 'javascript', operator: '*=*' } })).toBe("#tags *=* 'javascript'");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should handle nested label property', () => {
|
|
36
|
-
expect(buildSearchQuery({ '#template.title': 'Blog Post' })).toBe("#template.title = 'Blog Post'");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('relation conditions', () => {
|
|
41
|
-
it('should handle relation with string value (default contains)', () => {
|
|
42
|
-
expect(buildSearchQuery({ '~author': 'John' })).toBe("~author *=* 'John'");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should handle relation with operator', () => {
|
|
46
|
-
expect(buildSearchQuery({ '~category': { value: 'Tech', operator: '=' } })).toBe("~category = 'Tech'");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should handle nested relation property', () => {
|
|
50
|
-
expect(buildSearchQuery({ '~author.title': 'John Doe' })).toBe("~author.title = 'John Doe'");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('note property conditions', () => {
|
|
55
|
-
it('should handle note property with note. prefix', () => {
|
|
56
|
-
expect(buildSearchQuery({ 'note.type': 'text' })).toBe("note.type = 'text'");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should handle note property without note. prefix', () => {
|
|
60
|
-
expect(buildSearchQuery({ type: 'text' })).toBe("note.type = 'text'");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should handle note property with boolean value', () => {
|
|
64
|
-
expect(buildSearchQuery({ isProtected: false })).toBe('note.isProtected = false');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should handle note property with operator', () => {
|
|
68
|
-
expect(buildSearchQuery({ title: { value: 'Blog', operator: '*=' } })).toBe("note.title *= 'Blog'");
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('logical operators', () => {
|
|
73
|
-
it('should handle AND operator', () => {
|
|
74
|
-
const query = buildSearchQuery({
|
|
75
|
-
AND: [{ '#blog': true }, { '#published': true }],
|
|
76
|
-
});
|
|
77
|
-
expect(query).toBe('#blog AND #published');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should handle OR operator', () => {
|
|
81
|
-
const query = buildSearchQuery({
|
|
82
|
-
OR: [{ '#status': 'draft' }, { '#status': 'review' }],
|
|
83
|
-
});
|
|
84
|
-
expect(query).toBe("#status = 'draft' OR #status = 'review'");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle NOT operator', () => {
|
|
88
|
-
const query = buildSearchQuery({
|
|
89
|
-
NOT: { '#archived': true },
|
|
90
|
-
});
|
|
91
|
-
expect(query).toBe('not(#archived)');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should handle nested AND with OR', () => {
|
|
95
|
-
const query = buildSearchQuery({
|
|
96
|
-
AND: [{ '#blog': true }, { OR: [{ '#status': 'published' }, { '#status': 'featured' }] }],
|
|
97
|
-
});
|
|
98
|
-
expect(query).toBe("#blog AND (#status = 'published' OR #status = 'featured')");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should handle complex nested conditions', () => {
|
|
102
|
-
const query = buildSearchQuery({
|
|
103
|
-
AND: [
|
|
104
|
-
{ '#blog': true },
|
|
105
|
-
{ 'note.type': 'text' },
|
|
106
|
-
{
|
|
107
|
-
OR: [{ '#category': 'tech' }, { '#category': 'programming' }],
|
|
108
|
-
},
|
|
109
|
-
{ NOT: { '#draft': true } },
|
|
110
|
-
],
|
|
111
|
-
});
|
|
112
|
-
expect(query).toBe("#blog AND note.type = 'text' AND (#category = 'tech' OR #category = 'programming') AND not(#draft)");
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('multiple conditions (implicit AND)', () => {
|
|
117
|
-
it('should join multiple conditions with AND', () => {
|
|
118
|
-
const query = buildSearchQuery({
|
|
119
|
-
'#blog': true,
|
|
120
|
-
'note.type': 'text',
|
|
121
|
-
});
|
|
122
|
-
expect(query).toBe("#blog AND note.type = 'text'");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should skip undefined values', () => {
|
|
126
|
-
const query = buildSearchQuery({
|
|
127
|
-
'#blog': true,
|
|
128
|
-
'#draft': undefined,
|
|
129
|
-
'note.type': 'text',
|
|
130
|
-
});
|
|
131
|
-
expect(query).toBe("#blog AND note.type = 'text'");
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('edge cases', () => {
|
|
136
|
-
it('should return empty string for empty object', () => {
|
|
137
|
-
expect(buildSearchQuery({})).toBe('');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should throw error for invalid NOT usage', () => {
|
|
141
|
-
expect(() => buildSearchQuery({ NOT: { value: 'test' } as any })).toThrow('NOT operator requires a query object');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// ============================================================================
|
|
147
|
-
// TriliumMapper Tests
|
|
148
|
-
// ============================================================================
|
|
149
|
-
|
|
150
|
-
describe('TriliumMapper', () => {
|
|
151
|
-
// Helper to create mock notes
|
|
152
|
-
function createMockNote(overrides: Partial<TriliumNote> = {}): TriliumNote {
|
|
153
|
-
return {
|
|
154
|
-
noteId: 'test123',
|
|
155
|
-
title: 'Test Note',
|
|
156
|
-
type: 'text',
|
|
157
|
-
mime: 'text/html',
|
|
158
|
-
isProtected: false,
|
|
159
|
-
blobId: 'blob123',
|
|
160
|
-
attributes: [],
|
|
161
|
-
parentNoteIds: ['root'],
|
|
162
|
-
childNoteIds: [],
|
|
163
|
-
parentBranchIds: ['branch123'],
|
|
164
|
-
childBranchIds: [],
|
|
165
|
-
dateCreated: '2024-01-01 12:00:00.000+0000',
|
|
166
|
-
dateModified: '2024-01-01 12:00:00.000+0000',
|
|
167
|
-
utcDateCreated: '2024-01-01 12:00:00.000Z',
|
|
168
|
-
utcDateModified: '2024-01-01 12:00:00.000Z',
|
|
169
|
-
...overrides,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
describe('basic mapping', () => {
|
|
174
|
-
it('should map note properties using shorthand syntax', () => {
|
|
175
|
-
interface SimpleNote {
|
|
176
|
-
id: string;
|
|
177
|
-
name: string;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const mapper = new TriliumMapper<SimpleNote>({
|
|
181
|
-
id: 'note.noteId',
|
|
182
|
-
name: 'note.title',
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const note = createMockNote({ noteId: 'abc123', title: 'My Note' });
|
|
186
|
-
const result = mapper.map(note);
|
|
187
|
-
|
|
188
|
-
expect(result.id).toBe('abc123');
|
|
189
|
-
expect(result.name).toBe('My Note');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should map label attributes', () => {
|
|
193
|
-
interface BlogPost {
|
|
194
|
-
slug: string;
|
|
195
|
-
category: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const mapper = new TriliumMapper<BlogPost>({
|
|
199
|
-
slug: '#slug',
|
|
200
|
-
category: '#category',
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const note = createMockNote({
|
|
204
|
-
attributes: [
|
|
205
|
-
{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'slug', value: 'my-blog-post', position: 0, isInheritable: false },
|
|
206
|
-
{ attributeId: 'a2', noteId: 'test123', type: 'label', name: 'category', value: 'tech', position: 1, isInheritable: false },
|
|
207
|
-
],
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const result = mapper.map(note);
|
|
211
|
-
|
|
212
|
-
expect(result.slug).toBe('my-blog-post');
|
|
213
|
-
expect(result.category).toBe('tech');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('should map relation attributes', () => {
|
|
217
|
-
interface LinkedNote {
|
|
218
|
-
authorId: string;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const mapper = new TriliumMapper<LinkedNote>({
|
|
222
|
-
authorId: '~author',
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const note = createMockNote({
|
|
226
|
-
attributes: [{ attributeId: 'a1', noteId: 'test123', type: 'relation', name: 'author', value: 'author123', position: 0, isInheritable: false }],
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const result = mapper.map(note);
|
|
230
|
-
|
|
231
|
-
expect(result.authorId).toBe('author123');
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe('transform functions', () => {
|
|
236
|
-
it('should apply transform function', () => {
|
|
237
|
-
interface PostWithCount {
|
|
238
|
-
wordCount: number;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const mapper = new TriliumMapper<PostWithCount>({
|
|
242
|
-
wordCount: {
|
|
243
|
-
from: '#wordCount',
|
|
244
|
-
transform: transforms.number,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const note = createMockNote({
|
|
249
|
-
attributes: [{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'wordCount', value: '1500', position: 0, isInheritable: false }],
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const result = mapper.map(note);
|
|
253
|
-
|
|
254
|
-
expect(result.wordCount).toBe(1500);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('should use custom transform function', () => {
|
|
258
|
-
interface PostWithTags {
|
|
259
|
-
tags: string[];
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const mapper = new TriliumMapper<PostWithTags>({
|
|
263
|
-
tags: {
|
|
264
|
-
from: '#tags',
|
|
265
|
-
transform: (value) => (typeof value === 'string' ? value.split(',').map((s) => s.trim()) : []),
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const note = createMockNote({
|
|
270
|
-
attributes: [{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'tags', value: 'javascript, typescript, nodejs', position: 0, isInheritable: false }],
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const result = mapper.map(note);
|
|
274
|
-
|
|
275
|
-
expect(result.tags).toEqual(['javascript', 'typescript', 'nodejs']);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe('default values', () => {
|
|
280
|
-
it('should use default value when field is undefined', () => {
|
|
281
|
-
interface PostWithDefaults {
|
|
282
|
-
views: number;
|
|
283
|
-
status: string;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const mapper = new TriliumMapper<PostWithDefaults>({
|
|
287
|
-
views: { from: '#views', transform: transforms.number, default: 0 },
|
|
288
|
-
status: { from: '#status', default: 'draft' },
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
const note = createMockNote({ attributes: [] });
|
|
292
|
-
const result = mapper.map(note);
|
|
293
|
-
|
|
294
|
-
expect(result.views).toBe(0);
|
|
295
|
-
expect(result.status).toBe('draft');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('should not use default when value exists', () => {
|
|
299
|
-
interface PostWithDefaults {
|
|
300
|
-
status: string;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const mapper = new TriliumMapper<PostWithDefaults>({
|
|
304
|
-
status: { from: '#status', default: 'draft' },
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const note = createMockNote({
|
|
308
|
-
attributes: [{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'status', value: 'published', position: 0, isInheritable: false }],
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const result = mapper.map(note);
|
|
312
|
-
|
|
313
|
-
expect(result.status).toBe('published');
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
describe('required fields', () => {
|
|
318
|
-
it('should throw error when required field is missing', () => {
|
|
319
|
-
interface RequiredPost {
|
|
320
|
-
slug: string;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const mapper = new TriliumMapper<RequiredPost>({
|
|
324
|
-
slug: { from: '#slug', required: true },
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const note = createMockNote({ attributes: [] });
|
|
328
|
-
|
|
329
|
-
expect(() => mapper.map(note)).toThrow("Required field 'slug' missing from note test123 (Test Note)");
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('should not throw when required field exists', () => {
|
|
333
|
-
interface RequiredPost {
|
|
334
|
-
slug: string;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const mapper = new TriliumMapper<RequiredPost>({
|
|
338
|
-
slug: { from: '#slug', required: true },
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const note = createMockNote({
|
|
342
|
-
attributes: [{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'slug', value: 'my-post', position: 0, isInheritable: false }],
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const result = mapper.map(note);
|
|
346
|
-
expect(result.slug).toBe('my-post');
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
describe('computed fields', () => {
|
|
351
|
-
it('should compute value from other mapped fields', () => {
|
|
352
|
-
interface PostWithReadTime {
|
|
353
|
-
wordCount: number;
|
|
354
|
-
readTimeMinutes: number;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const mapper = new TriliumMapper<PostWithReadTime>({
|
|
358
|
-
wordCount: { from: '#wordCount', transform: transforms.number, default: 0 },
|
|
359
|
-
readTimeMinutes: {
|
|
360
|
-
computed: (partial) => Math.ceil((partial.wordCount || 0) / 200),
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const note = createMockNote({
|
|
365
|
-
attributes: [{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'wordCount', value: '1000', position: 0, isInheritable: false }],
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const result = mapper.map(note);
|
|
369
|
-
|
|
370
|
-
expect(result.wordCount).toBe(1000);
|
|
371
|
-
expect(result.readTimeMinutes).toBe(5);
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('should have access to note in computed function', () => {
|
|
375
|
-
interface PostWithFullTitle {
|
|
376
|
-
fullTitle: string;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const mapper = new TriliumMapper<PostWithFullTitle>({
|
|
380
|
-
fullTitle: {
|
|
381
|
-
computed: (_partial, note) => `[${note.noteId}] ${note.title}`,
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
const note = createMockNote({ noteId: 'xyz', title: 'Hello World' });
|
|
386
|
-
const result = mapper.map(note);
|
|
387
|
-
|
|
388
|
-
expect(result.fullTitle).toBe('[xyz] Hello World');
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('should use default for computed when undefined', () => {
|
|
392
|
-
interface PostWithComputed {
|
|
393
|
-
summary: string;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const mapper = new TriliumMapper<PostWithComputed>({
|
|
397
|
-
summary: {
|
|
398
|
-
computed: () => undefined,
|
|
399
|
-
default: 'No summary available',
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const note = createMockNote();
|
|
404
|
-
const result = mapper.map(note);
|
|
405
|
-
|
|
406
|
-
expect(result.summary).toBe('No summary available');
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
describe('custom extractor functions', () => {
|
|
411
|
-
it('should use custom extractor function', () => {
|
|
412
|
-
interface PostWithCustom {
|
|
413
|
-
labelCount: number;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const mapper = new TriliumMapper<PostWithCustom>({
|
|
417
|
-
labelCount: {
|
|
418
|
-
from: (note) => note.attributes?.filter((a) => a.type === 'label').length || 0,
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
const note = createMockNote({
|
|
423
|
-
attributes: [
|
|
424
|
-
{ attributeId: 'a1', noteId: 'test123', type: 'label', name: 'one', value: '1', position: 0, isInheritable: false },
|
|
425
|
-
{ attributeId: 'a2', noteId: 'test123', type: 'label', name: 'two', value: '2', position: 1, isInheritable: false },
|
|
426
|
-
{ attributeId: 'a3', noteId: 'test123', type: 'relation', name: 'rel', value: 'val', position: 2, isInheritable: false },
|
|
427
|
-
],
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const result = mapper.map(note);
|
|
431
|
-
|
|
432
|
-
expect(result.labelCount).toBe(2);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
describe('array mapping', () => {
|
|
437
|
-
it('should map array of notes', () => {
|
|
438
|
-
interface SimpleNote {
|
|
439
|
-
title: string;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const mapper = new TriliumMapper<SimpleNote>({
|
|
443
|
-
title: 'note.title',
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
const notes = [createMockNote({ title: 'Note 1' }), createMockNote({ title: 'Note 2' }), createMockNote({ title: 'Note 3' })];
|
|
447
|
-
|
|
448
|
-
const results = mapper.map(notes);
|
|
449
|
-
|
|
450
|
-
expect(results).toHaveLength(3);
|
|
451
|
-
expect(results[0]!.title).toBe('Note 1');
|
|
452
|
-
expect(results[1]!.title).toBe('Note 2');
|
|
453
|
-
expect(results[2]!.title).toBe('Note 3');
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
describe('merge configurations', () => {
|
|
458
|
-
it('should merge multiple configurations', () => {
|
|
459
|
-
interface BaseNote {
|
|
460
|
-
id: string;
|
|
461
|
-
title: string;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
interface ExtendedNote extends BaseNote {
|
|
465
|
-
slug: string;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const baseConfig = {
|
|
469
|
-
id: 'note.noteId',
|
|
470
|
-
title: 'note.title',
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
const extendedConfig = {
|
|
474
|
-
slug: '#slug',
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
const merged = TriliumMapper.merge<ExtendedNote>(baseConfig, extendedConfig);
|
|
478
|
-
const mapper = new TriliumMapper<ExtendedNote>(merged);
|
|
479
|
-
|
|
480
|
-
const note = createMockNote({
|
|
481
|
-
noteId: 'abc',
|
|
482
|
-
title: 'Test',
|
|
483
|
-
attributes: [{ attributeId: 'a1', noteId: 'abc', type: 'label', name: 'slug', value: 'test-slug', position: 0, isInheritable: false }],
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
const result = mapper.map(note);
|
|
487
|
-
|
|
488
|
-
expect(result.id).toBe('abc');
|
|
489
|
-
expect(result.title).toBe('Test');
|
|
490
|
-
expect(result.slug).toBe('test-slug');
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it('should override earlier configs with later ones', () => {
|
|
494
|
-
interface Note {
|
|
495
|
-
title: string;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const config1 = { title: 'note.noteId' }; // Wrong mapping
|
|
499
|
-
const config2 = { title: 'note.title' }; // Correct mapping
|
|
500
|
-
|
|
501
|
-
const merged = TriliumMapper.merge<Note>(config1, config2);
|
|
502
|
-
const mapper = new TriliumMapper<Note>(merged);
|
|
503
|
-
|
|
504
|
-
const note = createMockNote({ noteId: 'abc', title: 'Correct Title' });
|
|
505
|
-
const result = mapper.map(note);
|
|
506
|
-
|
|
507
|
-
expect(result.title).toBe('Correct Title');
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// ============================================================================
|
|
513
|
-
// transforms Tests
|
|
514
|
-
// ============================================================================
|
|
515
|
-
|
|
516
|
-
describe('transforms', () => {
|
|
517
|
-
describe('number', () => {
|
|
518
|
-
it('should convert string to number', () => {
|
|
519
|
-
expect(transforms.number('123')).toBe(123);
|
|
520
|
-
expect(transforms.number('45.67')).toBe(45.67);
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it('should return undefined for invalid values', () => {
|
|
524
|
-
expect(transforms.number(undefined)).toBeUndefined();
|
|
525
|
-
expect(transforms.number(null)).toBeUndefined();
|
|
526
|
-
expect(transforms.number('')).toBeUndefined();
|
|
527
|
-
expect(transforms.number('not a number')).toBeUndefined();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it('should pass through numbers', () => {
|
|
531
|
-
expect(transforms.number(42)).toBe(42);
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
describe('boolean', () => {
|
|
536
|
-
it('should convert string to boolean', () => {
|
|
537
|
-
expect(transforms.boolean('true')).toBe(true);
|
|
538
|
-
expect(transforms.boolean('TRUE')).toBe(true);
|
|
539
|
-
expect(transforms.boolean('1')).toBe(true);
|
|
540
|
-
expect(transforms.boolean('yes')).toBe(true);
|
|
541
|
-
expect(transforms.boolean('false')).toBe(false);
|
|
542
|
-
expect(transforms.boolean('FALSE')).toBe(false);
|
|
543
|
-
expect(transforms.boolean('0')).toBe(false);
|
|
544
|
-
expect(transforms.boolean('no')).toBe(false);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('should return undefined for invalid values', () => {
|
|
548
|
-
expect(transforms.boolean(undefined)).toBeUndefined();
|
|
549
|
-
expect(transforms.boolean(null)).toBeUndefined();
|
|
550
|
-
expect(transforms.boolean('maybe')).toBeUndefined();
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('should pass through booleans', () => {
|
|
554
|
-
expect(transforms.boolean(true)).toBe(true);
|
|
555
|
-
expect(transforms.boolean(false)).toBe(false);
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
describe('commaSeparated', () => {
|
|
560
|
-
it('should split comma-separated string', () => {
|
|
561
|
-
expect(transforms.commaSeparated('a,b,c')).toEqual(['a', 'b', 'c']);
|
|
562
|
-
expect(transforms.commaSeparated('a, b, c')).toEqual(['a', 'b', 'c']);
|
|
563
|
-
expect(transforms.commaSeparated(' a , b , c ')).toEqual(['a', 'b', 'c']);
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it('should filter empty values', () => {
|
|
567
|
-
expect(transforms.commaSeparated('a,,b')).toEqual(['a', 'b']);
|
|
568
|
-
expect(transforms.commaSeparated(',a,b,')).toEqual(['a', 'b']);
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it('should return undefined for invalid values', () => {
|
|
572
|
-
expect(transforms.commaSeparated(undefined)).toBeUndefined();
|
|
573
|
-
expect(transforms.commaSeparated(null)).toBeUndefined();
|
|
574
|
-
expect(transforms.commaSeparated('')).toBeUndefined();
|
|
575
|
-
expect(transforms.commaSeparated(123)).toBeUndefined();
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
describe('json', () => {
|
|
580
|
-
it('should parse JSON string', () => {
|
|
581
|
-
expect(transforms.json('{"a":1}')).toEqual({ a: 1 });
|
|
582
|
-
expect(transforms.json('[1,2,3]')).toEqual([1, 2, 3]);
|
|
583
|
-
expect(transforms.json('"string"')).toBe('string');
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('should return undefined for invalid JSON', () => {
|
|
587
|
-
expect(transforms.json('not json')).toBeUndefined();
|
|
588
|
-
expect(transforms.json('{invalid}')).toBeUndefined();
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('should return undefined for empty values', () => {
|
|
592
|
-
expect(transforms.json(undefined)).toBeUndefined();
|
|
593
|
-
expect(transforms.json(null)).toBeUndefined();
|
|
594
|
-
expect(transforms.json('')).toBeUndefined();
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
describe('date', () => {
|
|
599
|
-
it('should parse date string', () => {
|
|
600
|
-
const result = transforms.date('2024-01-15T00:00:00Z');
|
|
601
|
-
expect(result).toBeInstanceOf(Date);
|
|
602
|
-
expect(result?.toISOString().startsWith('2024-01-15')).toBe(true);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it('should parse ISO date string', () => {
|
|
606
|
-
const result = transforms.date('2024-01-15T10:30:00Z');
|
|
607
|
-
expect(result).toBeInstanceOf(Date);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it('should return undefined for invalid dates', () => {
|
|
611
|
-
expect(transforms.date('not a date')).toBeUndefined();
|
|
612
|
-
expect(transforms.date(undefined)).toBeUndefined();
|
|
613
|
-
expect(transforms.date(null)).toBeUndefined();
|
|
614
|
-
expect(transforms.date('')).toBeUndefined();
|
|
615
|
-
});
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
describe('trim', () => {
|
|
619
|
-
it('should trim whitespace', () => {
|
|
620
|
-
expect(transforms.trim(' hello ')).toBe('hello');
|
|
621
|
-
expect(transforms.trim('\n\thello\n\t')).toBe('hello');
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it('should return undefined for empty results', () => {
|
|
625
|
-
expect(transforms.trim(' ')).toBeUndefined();
|
|
626
|
-
expect(transforms.trim('')).toBeUndefined();
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('should return undefined for null/undefined', () => {
|
|
630
|
-
expect(transforms.trim(undefined)).toBeUndefined();
|
|
631
|
-
expect(transforms.trim(null)).toBeUndefined();
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it('should convert non-strings to string', () => {
|
|
635
|
-
expect(transforms.trim(123)).toBe('123');
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
});
|