mirror of https://github.com/TriliumNext/Notes
order & limit implementation WIP
parent
b5627b138a
commit
a1a744bb00
@ -0,0 +1,70 @@
|
||||
const Note = require('../src/services/note_cache/entities/note');
|
||||
const Branch = require('../src/services/note_cache/entities/branch');
|
||||
const Attribute = require('../src/services/note_cache/entities/attribute');
|
||||
const noteCache = require('../src/services/note_cache/note_cache');
|
||||
const randtoken = require('rand-token').generator({source: 'crypto'});
|
||||
|
||||
/** @return {Note} */
|
||||
function findNoteByTitle(searchResults, title) {
|
||||
return searchResults
|
||||
.map(sr => noteCache.notes[sr.noteId])
|
||||
.find(note => note.title === title);
|
||||
}
|
||||
|
||||
class NoteBuilder {
|
||||
constructor(note) {
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
label(name, value = '', isInheritable = false) {
|
||||
new Attribute(noteCache, {
|
||||
attributeId: id(),
|
||||
noteId: this.note.noteId,
|
||||
type: 'label',
|
||||
isInheritable,
|
||||
name,
|
||||
value
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
relation(name, targetNote) {
|
||||
new Attribute(noteCache, {
|
||||
attributeId: id(),
|
||||
noteId: this.note.noteId,
|
||||
type: 'relation',
|
||||
name,
|
||||
value: targetNote.noteId
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
child(childNoteBuilder, prefix = "") {
|
||||
new Branch(noteCache, {
|
||||
branchId: id(),
|
||||
noteId: childNoteBuilder.note.noteId,
|
||||
parentNoteId: this.note.noteId,
|
||||
prefix
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function id() {
|
||||
return randtoken.generate(10);
|
||||
}
|
||||
|
||||
function note(title) {
|
||||
const note = new Note(noteCache, {noteId: id(), title});
|
||||
|
||||
return new NoteBuilder(note);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NoteBuilder,
|
||||
findNoteByTitle,
|
||||
note
|
||||
};
|
||||
@ -0,0 +1,86 @@
|
||||
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking');
|
||||
const ValueExtractor = require('../src/services/search/value_extractor');
|
||||
const noteCache = require('../src/services/note_cache/note_cache');
|
||||
|
||||
describe("Value extractor", () => {
|
||||
beforeEach(() => {
|
||||
noteCache.reset();
|
||||
});
|
||||
|
||||
it("simple title extraction", async () => {
|
||||
const europe = note("Europe").note;
|
||||
|
||||
const valueExtractor = new ValueExtractor(["note", "title"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(europe)).toEqual("Europe");
|
||||
});
|
||||
|
||||
it("label extraction", async () => {
|
||||
const austria = note("Austria")
|
||||
.label("Capital", "Vienna")
|
||||
.note;
|
||||
|
||||
let valueExtractor = new ValueExtractor(["note", "labels", "capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria)).toEqual("vienna");
|
||||
|
||||
valueExtractor = new ValueExtractor(["#capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria)).toEqual("vienna");
|
||||
});
|
||||
|
||||
it("parent/child property extraction", async () => {
|
||||
const vienna = note("Vienna");
|
||||
const europe = note("Europe")
|
||||
.child(note("Austria")
|
||||
.child(vienna));
|
||||
|
||||
let valueExtractor = new ValueExtractor(["note", "children", "children", "title"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(europe.note)).toEqual("Vienna");
|
||||
|
||||
valueExtractor = new ValueExtractor(["note", "parents", "parents", "title"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(vienna.note)).toEqual("Europe");
|
||||
});
|
||||
|
||||
it("extract through relation", async () => {
|
||||
const czechRepublic = note("Czech Republic").label("capital", "Prague");
|
||||
const slovakia = note("Slovakia").label("capital", "Bratislava");
|
||||
const austria = note("Austria")
|
||||
.relation('neighbor', czechRepublic.note)
|
||||
.relation('neighbor', slovakia.note);
|
||||
|
||||
let valueExtractor = new ValueExtractor(["note", "relations", "neighbor", "labels", "capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria.note)).toEqual("prague");
|
||||
|
||||
valueExtractor = new ValueExtractor(["~neighbor", "labels", "capital"]);
|
||||
|
||||
expect(valueExtractor.validate()).toBeFalsy();
|
||||
expect(valueExtractor.extract(austria.note)).toEqual("prague");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Invalid value extractor property path", () => {
|
||||
it('each path must start with "note" (or label/relation)',
|
||||
() => expect(new ValueExtractor(["neighbor"]).validate()).toBeTruthy());
|
||||
|
||||
it("extra path element after terminal label",
|
||||
() => expect(new ValueExtractor(["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy());
|
||||
|
||||
it("extra path element after terminal title",
|
||||
() => expect(new ValueExtractor(["note", "title", "isProtected"]).validate()).toBeTruthy());
|
||||
|
||||
it("relation name and note property is missing",
|
||||
() => expect(new ValueExtractor(["note", "relations"]).validate()).toBeTruthy());
|
||||
|
||||
it("relation is specified but target note property is not specified",
|
||||
() => expect(new ValueExtractor(["note", "relations", "myrel"]).validate()).toBeTruthy());
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
|
||||
const Expression = require('./expression');
|
||||
const NoteSet = require('../note_set');
|
||||
|
||||
class OrderByAndLimitExp extends Expression {
|
||||
constructor(orderDefinitions, limit) {
|
||||
super();
|
||||
|
||||
this.orderDefinitions = orderDefinitions;
|
||||
|
||||
for (const od of this.orderDefinitions) {
|
||||
od.smaller = od.direction === "asc" ? -1 : 1;
|
||||
od.larger = od.direction === "asc" ? 1 : -1;
|
||||
}
|
||||
|
||||
this.limit = limit;
|
||||
|
||||
/** @type {Expression} */
|
||||
this.subExpression = null; // it's expected to be set after construction
|
||||
}
|
||||
|
||||
execute(inputNoteSet, searchContext) {
|
||||
let {notes} = this.subExpression.execute(inputNoteSet, searchContext);
|
||||
|
||||
notes.sort((a, b) => {
|
||||
for (const {valueExtractor, smaller, larger} of this.orderDefinitions) {
|
||||
let valA = valueExtractor.extract(a);
|
||||
let valB = valueExtractor.extract(b);
|
||||
|
||||
if (!isNaN(valA) && !isNaN(valB)) {
|
||||
valA = parseFloat(valA);
|
||||
valB = parseFloat(valB);
|
||||
}
|
||||
|
||||
if (valA < valB) {
|
||||
return smaller;
|
||||
} else if (valA > valB) {
|
||||
return larger;
|
||||
}
|
||||
// else go to next order definition
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (this.limit) {
|
||||
notes = notes.slice(0, this.limit);
|
||||
}
|
||||
|
||||
const noteSet = new NoteSet(notes);
|
||||
noteSet.sorted = true;
|
||||
|
||||
return noteSet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderByAndLimitExp;
|
||||
@ -0,0 +1,110 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Search string is lower cased for case insensitive comparison. But when retrieving properties
|
||||
* we need case sensitive form so we have this translation object.
|
||||
*/
|
||||
const PROP_MAPPING = {
|
||||
"noteid": "noteId",
|
||||
"title": "title",
|
||||
"type": "type",
|
||||
"mime": "mime",
|
||||
"isprotected": "isProtected",
|
||||
"isarhived": "isArchived",
|
||||
"datecreated": "dateCreated",
|
||||
"datemodified": "dateModified",
|
||||
"utcdatecreated": "utcDateCreated",
|
||||
"utcdatemodified": "utcDateModified",
|
||||
"contentlength": "contentLength",
|
||||
"parentcount": "parentCount",
|
||||
"childrencount": "childrenCount",
|
||||
"attributecount": "attributeCount",
|
||||
"labelcount": "labelCount",
|
||||
"relationcount": "relationCount"
|
||||
};
|
||||
|
||||
class ValueExtractor {
|
||||
constructor(propertyPath) {
|
||||
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
|
||||
|
||||
if (this.propertyPath[0].startsWith('#')) {
|
||||
this.propertyPath = ['note', 'labels', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
||||
}
|
||||
else if (this.propertyPath[0].startsWith('~')) {
|
||||
this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
||||
}
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (this.propertyPath[0] !== 'note') {
|
||||
return `property specifier must start with 'note', but starts with '${this.propertyPath[0]}'`;
|
||||
}
|
||||
|
||||
for (let i = 1; i < this.propertyPath.length; i++) {
|
||||
const pathEl = this.propertyPath[i];
|
||||
|
||||
if (pathEl === 'labels') {
|
||||
if (i !== this.propertyPath.length - 2) {
|
||||
return `label is a terminal property specifier and must be at the end`;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
else if (pathEl === 'relations') {
|
||||
if (i >= this.propertyPath.length - 2) {
|
||||
return `relation name or property name is missing`;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
else if (pathEl in PROP_MAPPING) {
|
||||
if (i !== this.propertyPath.length - 1) {
|
||||
return `${pathEl} is a terminal property specifier and must be at the end`;
|
||||
}
|
||||
}
|
||||
else if (!["parents", "children"].includes(pathEl)) {
|
||||
return `Unrecognized property specifier ${pathEl}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extract(note) {
|
||||
let cursor = note;
|
||||
|
||||
let i;
|
||||
|
||||
const cur = () => this.propertyPath[i];
|
||||
|
||||
for (i = 0; i < this.propertyPath.length; i++) {
|
||||
if (!cursor) {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
if (cur() === 'labels') {
|
||||
i++;
|
||||
|
||||
return cursor.getLabelValue(cur());
|
||||
}
|
||||
|
||||
if (cur() === 'relations') {
|
||||
i++;
|
||||
|
||||
cursor = cursor.getRelationTarget(cur());
|
||||
}
|
||||
else if (cur() === 'parents') {
|
||||
cursor = cursor.parents[0];
|
||||
}
|
||||
else if (cur() === 'children') {
|
||||
cursor = cursor.children[0];
|
||||
}
|
||||
else if (cur() in PROP_MAPPING) {
|
||||
return cursor[PROP_MAPPING[cur()]];
|
||||
}
|
||||
else {
|
||||
// FIXME
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ValueExtractor;
|
||||
Loading…
Reference in New Issue