mirror of
https://github.com/zadam/trilium.git
synced 2025-01-01 12:52:17 +08:00
fix setting mime after import + cleanup of old search code
This commit is contained in:
parent
03d7ee9abb
commit
53c361945b
10 changed files with 13 additions and 371 deletions
|
@ -109,7 +109,7 @@ class Note extends Entity {
|
|||
return sql.getRow(`
|
||||
SELECT
|
||||
LENGTH(content) AS contentLength,
|
||||
dateModified,
|
||||
dateModified,
|
||||
utcDateModified
|
||||
FROM note_contents
|
||||
WHERE noteId = ?`, [this.noteId]);
|
||||
|
|
|
@ -176,7 +176,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
|||
* @returns {Promise<NoteShort[]>}
|
||||
*/
|
||||
this.searchForNotes = async searchString => {
|
||||
const noteIds = await this.runOnServer(async searchString => {
|
||||
const noteIds = await this.runOnBackend(async searchString => {
|
||||
const notes = await api.searchForNotes(searchString);
|
||||
|
||||
return notes.map(note => note.noteId);
|
||||
|
|
|
@ -127,7 +127,7 @@ class ImageTypeWidget extends TypeWidget {
|
|||
|
||||
this.$widget.show();
|
||||
|
||||
const noteComplement = await this.tabContext.getNoteComplement();
|
||||
const noteComplement = await this.tabContext.getNoteComplement();console.log(noteComplement, note);
|
||||
|
||||
this.$fileName.text(attributeMap.originalFileName || "?");
|
||||
this.$fileSize.text(noteComplement.contentLength + " bytes");
|
||||
|
|
|
@ -18,6 +18,7 @@ function updateFile(req) {
|
|||
noteRevisionService.createNoteRevision(note);
|
||||
|
||||
note.mime = file.mimetype.toLowerCase();
|
||||
note.save();
|
||||
|
||||
note.setContent(file.buffer);
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ function getNote(req) {
|
|||
|
||||
const contentMetadata = note.getContentMetadata();
|
||||
|
||||
note.contentLength = contentMetadata.contentLength;
|
||||
|
||||
note.combinedUtcDateModified = note.utcDateModified > contentMetadata.utcDateModified ? note.utcDateModified : contentMetadata.utcDateModified;
|
||||
note.combinedDateModified = note.utcDateModified > contentMetadata.utcDateModified ? note.dateModified : contentMetadata.dateModified;
|
||||
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
const utils = require('./utils');
|
||||
|
||||
const VIRTUAL_ATTRIBUTES = [
|
||||
"dateCreated",
|
||||
"dateModified",
|
||||
"utcDateCreated",
|
||||
"utcDateModified",
|
||||
"noteId",
|
||||
"isProtected",
|
||||
"title",
|
||||
"content",
|
||||
"type",
|
||||
"mime",
|
||||
"text",
|
||||
"parentCount"
|
||||
];
|
||||
|
||||
module.exports = function(filters, selectedColumns = 'notes.*') {
|
||||
// alias => join
|
||||
const joins = {
|
||||
"notes": null
|
||||
};
|
||||
|
||||
let attrFilterId = 1;
|
||||
|
||||
function getAccessor(property) {
|
||||
let accessor;
|
||||
|
||||
if (!VIRTUAL_ATTRIBUTES.includes(property)) {
|
||||
// not reusing existing filters to support multi-valued filters e.g. "@tag=christmas @tag=shopping"
|
||||
// can match notes because @tag can be both "shopping" and "christmas"
|
||||
const alias = "attr_" + property + "_" + attrFilterId++;
|
||||
|
||||
// forcing to use particular index since SQLite query planner would often choose something pretty bad
|
||||
joins[alias] = `LEFT JOIN attributes AS ${alias} INDEXED BY IDX_attributes_noteId_index `
|
||||
+ `ON ${alias}.noteId = notes.noteId `
|
||||
+ `AND ${alias}.name = '${property}' AND ${alias}.isDeleted = 0`;
|
||||
|
||||
accessor = `${alias}.value`;
|
||||
}
|
||||
else if (property === 'content') {
|
||||
const alias = "note_contents";
|
||||
|
||||
if (!(alias in joins)) {
|
||||
joins[alias] = `LEFT JOIN note_contents ON note_contents.noteId = notes.noteId`;
|
||||
}
|
||||
|
||||
accessor = `${alias}.${property}`;
|
||||
}
|
||||
else if (property === 'parentCount') {
|
||||
// need to cast as string for the equality operator to work
|
||||
// for >= etc. it is cast again to DECIMAL
|
||||
// also cannot use COUNT() in WHERE so using subquery ...
|
||||
accessor = `CAST((SELECT COUNT(1) FROM branches WHERE branches.noteId = notes.noteId AND isDeleted = 0) AS STRING)`;
|
||||
}
|
||||
else {
|
||||
accessor = "notes." + property;
|
||||
}
|
||||
|
||||
return accessor;
|
||||
}
|
||||
|
||||
let orderBy = [];
|
||||
|
||||
const orderByFilter = filters.find(filter => filter.name.toLowerCase() === 'orderby');
|
||||
|
||||
if (orderByFilter) {
|
||||
orderBy = orderByFilter.value.split(",").map(prop => {
|
||||
const direction = prop.includes("-") ? "DESC" : "ASC";
|
||||
const cleanedProp = prop.trim().replace(/-/g, "");
|
||||
|
||||
return getAccessor(cleanedProp) + " " + direction;
|
||||
});
|
||||
}
|
||||
|
||||
let where = '1';
|
||||
const params = [];
|
||||
|
||||
for (const filter of filters) {
|
||||
if (['isarchived', 'in', 'orderby', 'limit'].includes(filter.name.toLowerCase())) {
|
||||
continue; // these are not real filters
|
||||
}
|
||||
|
||||
where += " " + filter.relation + " ";
|
||||
|
||||
const accessor = getAccessor(filter.name);
|
||||
|
||||
if (filter.operator === 'exists') {
|
||||
where += `${accessor} IS NOT NULL`;
|
||||
}
|
||||
else if (filter.operator === 'not-exists') {
|
||||
where += `${accessor} IS NULL`;
|
||||
}
|
||||
else if (filter.operator === '=' || filter.operator === '!=') {
|
||||
where += `${accessor} ${filter.operator} ?`;
|
||||
params.push(filter.value);
|
||||
} else if (filter.operator === '*=' || filter.operator === '!*=') {
|
||||
where += `${accessor}`
|
||||
+ (filter.operator.includes('!') ? ' NOT' : '')
|
||||
+ ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '');
|
||||
} else if (filter.operator === '=*' || filter.operator === '!=*') {
|
||||
where += `${accessor}`
|
||||
+ (filter.operator.includes('!') ? ' NOT' : '')
|
||||
+ ` LIKE ` + utils.prepareSqlForLike('', filter.value, '%');
|
||||
} else if (filter.operator === '*=*' || filter.operator === '!*=*') {
|
||||
const columns = filter.name === 'text' ? [getAccessor("title"), getAccessor("content")] : [accessor];
|
||||
|
||||
let condition = "(" + columns.map(column =>
|
||||
`${column}` + ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '%'))
|
||||
.join(" OR ") + ")";
|
||||
|
||||
if (filter.operator.includes('!')) {
|
||||
condition = "NOT(" + condition + ")";
|
||||
}
|
||||
|
||||
if (['text', 'title', 'content'].includes(filter.name)) {
|
||||
// for title/content search does not make sense to search for protected notes
|
||||
condition = `(${condition} AND notes.isProtected = 0)`;
|
||||
}
|
||||
|
||||
where += condition;
|
||||
}
|
||||
else if ([">", ">=", "<", "<="].includes(filter.operator)) {
|
||||
let floatParam;
|
||||
|
||||
// from https://stackoverflow.com/questions/12643009/regular-expression-for-floating-point-numbers
|
||||
if (/^[+-]?([0-9]*[.])?[0-9]+$/.test(filter.value)) {
|
||||
floatParam = parseFloat(filter.value);
|
||||
}
|
||||
|
||||
if (floatParam === undefined || isNaN(floatParam)) {
|
||||
// if the value can't be parsed as float then we assume that string comparison should be used instead of numeric
|
||||
where += `${accessor} ${filter.operator} ?`;
|
||||
params.push(filter.value);
|
||||
} else {
|
||||
where += `CAST(${accessor} AS DECIMAL) ${filter.operator} ?`;
|
||||
params.push(floatParam);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unknown operator " + filter.operator);
|
||||
}
|
||||
}
|
||||
|
||||
if (orderBy.length === 0) {
|
||||
// if no ordering is given then order at least by note title
|
||||
orderBy.push("notes.title");
|
||||
}
|
||||
|
||||
const query = `SELECT ${selectedColumns} FROM notes
|
||||
${Object.values(joins).join('\r\n')}
|
||||
WHERE
|
||||
notes.isDeleted = 0
|
||||
AND (${where})
|
||||
GROUP BY notes.noteId
|
||||
ORDER BY ${orderBy.join(", ")}`;
|
||||
|
||||
return { query, params };
|
||||
};
|
|
@ -15,7 +15,7 @@ const isSvg = require('is-svg');
|
|||
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
|
||||
const origImageFormat = getImageType(uploadBuffer);
|
||||
|
||||
if (origImageFormat && ["webp", "svg"].includes(origImageFormat.ext)) {
|
||||
if (origImageFormat && ["webp", "svg", "gif"].includes(origImageFormat.ext)) {
|
||||
// JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144
|
||||
shrinkImageSwitch = false;
|
||||
}
|
||||
|
@ -61,6 +61,8 @@ function updateImage(noteId, uploadBuffer, originalName) {
|
|||
processImage(uploadBuffer, originalName, true).then(({buffer, imageFormat}) => {
|
||||
sql.transactional(() => {
|
||||
note.mime = getImageMimeFromExtension(imageFormat.ext);
|
||||
note.save();
|
||||
|
||||
note.setContent(buffer);
|
||||
})
|
||||
});
|
||||
|
@ -88,6 +90,8 @@ function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch)
|
|||
processImage(uploadBuffer, originalName, shrinkImageSwitch).then(({buffer, imageFormat}) => {
|
||||
sql.transactional(() => {
|
||||
note.mime = getImageMimeFromExtension(imageFormat.ext);
|
||||
note.save();
|
||||
|
||||
note.setContent(buffer);
|
||||
})
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ function createNewNote(params) {
|
|||
throw new Error(`Note title must not be empty`);
|
||||
}
|
||||
|
||||
sql.transactional(() => {
|
||||
return sql.transactional(() => {
|
||||
const note = new Note({
|
||||
noteId: params.noteId, // optionally can force specific noteId
|
||||
title: params.title,
|
||||
|
@ -744,8 +744,8 @@ function duplicateNote(noteId, parentNoteId) {
|
|||
const newNote = new Note(origNote);
|
||||
newNote.noteId = undefined; // force creation of new note
|
||||
newNote.title += " (dup)";
|
||||
|
||||
newNote.save();
|
||||
|
||||
newNote.setContent(origNote.getContent());
|
||||
|
||||
const newBranch = new Branch({
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* Missing things from the OLD search:
|
||||
* - orderBy
|
||||
* - limit
|
||||
* - in - replaced with note.ancestors
|
||||
* - content in attribute search
|
||||
* - not - pherhaps not necessary
|
||||
*
|
||||
* other potential additions:
|
||||
* - targetRelations - either named or not
|
||||
* - any relation without name
|
||||
*/
|
||||
|
||||
const repository = require('./repository');
|
||||
const sql = require('./sql');
|
||||
const log = require('./log');
|
||||
const parseFilters = require('./search/parse_filters.js');
|
||||
const buildSearchQuery = require('./build_search_query');
|
||||
const noteCacheService = require('./note_cache/note_cache_service');
|
||||
|
||||
function searchForNotes(searchString) {
|
||||
const noteIds = searchForNoteIds(searchString);
|
||||
|
||||
return repository.getNotes(noteIds);
|
||||
}
|
||||
|
||||
function searchForNoteIds(searchString) {
|
||||
const filters = parseFilters(searchString);
|
||||
|
||||
const {query, params} = buildSearchQuery(filters, 'notes.noteId');
|
||||
|
||||
try {
|
||||
let noteIds = sql.getColumn(query, params);
|
||||
|
||||
noteIds = noteIds.filter(noteCacheService.isAvailable);
|
||||
|
||||
const isArchivedFilter = filters.find(filter => filter.name.toLowerCase() === 'isarchived');
|
||||
|
||||
if (isArchivedFilter) {
|
||||
if (isArchivedFilter.operator === 'exists') {
|
||||
noteIds = noteIds.filter(noteCacheService.isArchived);
|
||||
}
|
||||
else if (isArchivedFilter.operator === 'not-exists') {
|
||||
noteIds = noteIds.filter(noteId => !noteCacheService.isArchived(noteId));
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized isArchived operator ${isArchivedFilter.operator}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isInFilters = filters.filter(filter => filter.name.toLowerCase() === 'in');
|
||||
|
||||
for (const isInFilter of isInFilters) {
|
||||
if (isInFilter.operator === '=') {
|
||||
noteIds = noteIds.filter(noteId => noteCacheService.isInAncestor(noteId, isInFilter.value));
|
||||
}
|
||||
else if (isInFilter.operator === '!=') {
|
||||
noteIds = noteIds.filter(noteId => !noteCacheService.isInAncestor(noteId, isInFilter.value));
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized isIn operator ${isInFilter.operator}`);
|
||||
}
|
||||
}
|
||||
|
||||
const limitFilter = filters.find(filter => filter.name.toLowerCase() === 'limit');
|
||||
|
||||
if (limitFilter) {
|
||||
const limit = parseInt(limitFilter.value);
|
||||
|
||||
return noteIds.splice(0, limit);
|
||||
}
|
||||
else {
|
||||
return noteIds;
|
||||
}
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
log.error("Search failed for " + query);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
searchForNotes,
|
||||
searchForNoteIds
|
||||
};
|
|
@ -1,118 +0,0 @@
|
|||
const dayjs = require("dayjs");
|
||||
|
||||
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([^\s=*"]+|"[^"]+"))?/igu;
|
||||
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
|
||||
|
||||
function calculateSmartValue(v) {
|
||||
const match = smartValueRegex.exec(v);
|
||||
if (match === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyword = match[1].toUpperCase();
|
||||
const num = match[2] ? parseInt(match[2].replace(/ /g, "")) : 0; // can contain spaces between sign and digits
|
||||
|
||||
let format, date;
|
||||
|
||||
if (keyword === 'NOW') {
|
||||
date = dayjs().add(num, 'second');
|
||||
format = "YYYY-MM-DD HH:mm:ss";
|
||||
}
|
||||
else if (keyword === 'TODAY') {
|
||||
date = dayjs().add(num, 'day');
|
||||
format = "YYYY-MM-DD";
|
||||
}
|
||||
else if (keyword === 'WEEK') {
|
||||
// FIXME: this will always use sunday as start of the week
|
||||
date = dayjs().startOf('week').add(7 * num, 'day');
|
||||
format = "YYYY-MM-DD";
|
||||
}
|
||||
else if (keyword === 'MONTH') {
|
||||
date = dayjs().add(num, 'month');
|
||||
format = "YYYY-MM";
|
||||
}
|
||||
else if (keyword === 'YEAR') {
|
||||
date = dayjs().add(num, 'year');
|
||||
format = "YYYY";
|
||||
}
|
||||
else {
|
||||
throw new Error("Unrecognized keyword: " + keyword);
|
||||
}
|
||||
|
||||
return date.format(format);
|
||||
}
|
||||
|
||||
module.exports = function (searchText) {
|
||||
searchText = searchText.trim();
|
||||
|
||||
// if the string doesn't start with attribute then we consider it as just standard full text search
|
||||
if (!searchText.startsWith("@")) {
|
||||
// replace with space instead of empty string since these characters are probably separators
|
||||
const filters = [];
|
||||
|
||||
if (searchText.startsWith('"') && searchText.endsWith('"')) {
|
||||
// "bla bla" will search for exact match
|
||||
searchText = searchText.substr(1, searchText.length - 2);
|
||||
|
||||
filters.push({
|
||||
relation: 'and',
|
||||
name: 'text',
|
||||
operator: '*=*',
|
||||
value: searchText
|
||||
});
|
||||
}
|
||||
else {
|
||||
const tokens = searchText.split(/\s+/);
|
||||
|
||||
for (const token of tokens) {
|
||||
filters.push({
|
||||
relation: 'and',
|
||||
name: 'text',
|
||||
operator: '*=*',
|
||||
value: token
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
filters.push({
|
||||
relation: 'and',
|
||||
name: 'isArchived',
|
||||
operator: 'not-exists'
|
||||
});
|
||||
|
||||
filters.push({
|
||||
relation: 'or',
|
||||
name: 'noteId',
|
||||
operator: '=',
|
||||
value: searchText
|
||||
});
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
const filters = [];
|
||||
|
||||
function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; }
|
||||
|
||||
let match;
|
||||
|
||||
while (match = filterRegex.exec(searchText)) {
|
||||
const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and';
|
||||
const operator = match[3] === '!' ? 'not-exists' : 'exists';
|
||||
|
||||
const value = match[7] !== undefined ? trimQuotes(match[7]) : null;
|
||||
|
||||
filters.push({
|
||||
relation: relation,
|
||||
name: trimQuotes(match[4]),
|
||||
operator: match[6] !== undefined ? match[6] : operator,
|
||||
value: (
|
||||
value && value.match(smartValueRegex)
|
||||
? calculateSmartValue(value)
|
||||
: value
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
Loading…
Reference in a new issue