mirror of
https://github.com/zadam/trilium.git
synced 2024-09-20 07:35:59 +08:00
ETAPI delete/patch, refactoring
This commit is contained in:
parent
82b2871a08
commit
9ee1c9f3da
|
@ -3,7 +3,7 @@
|
|||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="openjdk-16" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
14
db/migrations/0190__add_token_name.sql
Normal file
14
db/migrations/0190__add_token_name.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE IF NOT EXISTS "mig_api_tokens"
|
||||
(
|
||||
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0);
|
||||
|
||||
INSERT INTO mig_api_tokens (apiTokenId, name, token, utcDateCreated, isDeleted)
|
||||
SELECT apiTokenId, 'Trilium Sender', token, utcDateCreated, isDeleted FROM api_tokens;
|
||||
|
||||
DROP TABLE api_tokens;
|
||||
|
||||
ALTER TABLE mig_api_tokens RENAME TO api_tokens;
|
|
@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
|
|||
CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||
(
|
||||
apiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0);
|
||||
|
|
11453
package-lock.json
generated
11453
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -4,12 +4,15 @@ const dateUtils = require('../../services/date_utils.js');
|
|||
const AbstractEntity = require("./abstract_entity.js");
|
||||
|
||||
/**
|
||||
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
|
||||
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications.
|
||||
* Used by:
|
||||
* - Trilium Sender
|
||||
* - ETAPI clients
|
||||
*/
|
||||
class ApiToken extends AbstractEntity {
|
||||
static get entityName() { return "api_tokens"; }
|
||||
static get primaryKeyName() { return "apiTokenId"; }
|
||||
static get hashedProperties() { return ["apiTokenId", "token", "utcDateCreated"]; }
|
||||
static get hashedProperties() { return ["apiTokenId", "name", "token", "utcDateCreated"]; }
|
||||
|
||||
constructor(row) {
|
||||
super();
|
||||
|
@ -17,6 +20,8 @@ class ApiToken extends AbstractEntity {
|
|||
/** @type {string} */
|
||||
this.apiTokenId = row.apiTokenId;
|
||||
/** @type {string} */
|
||||
this.name = row.name;
|
||||
/** @type {string} */
|
||||
this.token = row.token;
|
||||
/** @type {string} */
|
||||
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
|
||||
|
@ -25,6 +30,7 @@ class ApiToken extends AbstractEntity {
|
|||
getPojo() {
|
||||
return {
|
||||
apiTokenId: this.apiTokenId,
|
||||
name: this.name,
|
||||
token: this.token,
|
||||
utcDateCreated: this.utcDateCreated
|
||||
}
|
||||
|
|
64
src/etapi/attributes.js
Normal file
64
src/etapi/attributes.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
const becca = require("../becca/becca");
|
||||
const ru = require("./route_utils");
|
||||
const mappers = require("./mappers");
|
||||
const attributeService = require("../services/attributes");
|
||||
const validators = require("./validators.js");
|
||||
|
||||
function register(router) {
|
||||
ru.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => {
|
||||
const attribute = ru.getAndCheckAttribute(req.params.attributeId);
|
||||
|
||||
res.json(mappers.mapAttributeToPojo(attribute));
|
||||
});
|
||||
|
||||
ru.route(router, 'post' ,'/etapi/attributes', (req, res, next) => {
|
||||
const params = req.body;
|
||||
|
||||
ru.getAndCheckNote(params.noteId);
|
||||
|
||||
if (params.type === 'relation') {
|
||||
ru.getAndCheckNote(params.value);
|
||||
}
|
||||
|
||||
if (params.type !== 'relation' && params.type !== 'label') {
|
||||
throw new ru.EtapiError(400, ru.GENERIC_CODE, `Only "relation" and "label" are supported attribute types, "${params.type}" given.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const attr = attributeService.createAttribute(params);
|
||||
|
||||
res.json(mappers.mapAttributeToPojo(attr));
|
||||
}
|
||||
catch (e) {
|
||||
throw new ru.EtapiError(400, ru.GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
'value': validators.isString
|
||||
};
|
||||
|
||||
ru.route(router, 'patch' ,'/etapi/attributes/:attributeId', (req, res, next) => {
|
||||
const attribute = ru.getAndCheckAttribute(req.params.attributeId);
|
||||
|
||||
ru.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
|
||||
res.json(mappers.mapAttributeToPojo(attribute));
|
||||
});
|
||||
|
||||
ru.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => {
|
||||
const attribute = becca.getAttribute(req.params.attributeId);
|
||||
|
||||
if (!attribute) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
attribute.markAsDeleted();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
78
src/etapi/branches.js
Normal file
78
src/etapi/branches.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const becca = require("../becca/becca.js");
|
||||
const ru = require("./route_utils");
|
||||
const mappers = require("./mappers");
|
||||
const Branch = require("../becca/entities/branch");
|
||||
const noteService = require("../services/notes");
|
||||
const TaskContext = require("../services/task_context");
|
||||
const entityChangesService = require("../services/entity_changes");
|
||||
const validators = require("./validators.js");
|
||||
|
||||
function register(router) {
|
||||
ru.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
|
||||
const branch = ru.getAndCheckBranch(req.params.branchId);
|
||||
|
||||
res.json(mappers.mapBranchToPojo(branch));
|
||||
});
|
||||
|
||||
ru.route(router, 'post' ,'/etapi/branches', (req, res, next) => {
|
||||
const params = req.body;
|
||||
|
||||
ru.getAndCheckNote(params.noteId);
|
||||
ru.getAndCheckNote(params.parentNoteId);
|
||||
|
||||
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
|
||||
|
||||
if (existing) {
|
||||
existing.notePosition = params.notePosition;
|
||||
existing.prefix = params.prefix;
|
||||
existing.save();
|
||||
|
||||
return res.json(mappers.mapBranchToPojo(existing));
|
||||
}
|
||||
|
||||
try {
|
||||
const branch = new Branch(params).save();
|
||||
|
||||
res.json(mappers.mapBranchToPojo(branch));
|
||||
}
|
||||
catch (e) {
|
||||
throw new ru.EtapiError(400, ru.GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
'notePosition': validators.isInteger,
|
||||
'prefix': validators.isStringOrNull,
|
||||
'isExpanded': validators.isBoolean
|
||||
};
|
||||
|
||||
ru.route(router, 'patch' ,'/etapi/branches/:branchId', (req, res, next) => {
|
||||
const branch = ru.getAndCheckBranch(req.params.branchId);
|
||||
|
||||
ru.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
|
||||
res.json(mappers.mapBranchToPojo(branch));
|
||||
});
|
||||
|
||||
ru.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
|
||||
const branch = becca.getBranch(req.params.branchId);
|
||||
|
||||
if (!branch) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
noteService.deleteBranch(branch, null, new TaskContext('no-progress-reporting'));
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
ru.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
|
||||
ru.getAndCheckNote(req.params.parentNoteId);
|
||||
|
||||
entityChangesService.addNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
49
src/etapi/mappers.js
Normal file
49
src/etapi/mappers.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
function mapNoteToPojo(note) {
|
||||
return {
|
||||
noteId: note.noteId,
|
||||
isProtected: note.isProtected,
|
||||
title: note.title,
|
||||
type: note.type,
|
||||
mime: note.mime,
|
||||
dateCreated: note.dateCreated,
|
||||
dateModified: note.dateModified,
|
||||
utcDateCreated: note.utcDateCreated,
|
||||
utcDateModified: note.utcDateModified,
|
||||
parentNoteIds: note.getParentNotes().map(p => p.noteId),
|
||||
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
|
||||
parentBranchIds: note.getParentBranches().map(p => p.branchId),
|
||||
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
|
||||
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
|
||||
};
|
||||
}
|
||||
|
||||
function mapBranchToPojo(branch) {
|
||||
return {
|
||||
branchId: branch.branchId,
|
||||
noteId: branch.noteId,
|
||||
parentNoteId: branch.parentNoteId,
|
||||
prefix: branch.prefix,
|
||||
notePosition: branch.notePosition,
|
||||
isExpanded: branch.isExpanded,
|
||||
utcDateModified: branch.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
function mapAttributeToPojo(attr) {
|
||||
return {
|
||||
attributeId: attr.attributeId,
|
||||
noteId: attr.noteId,
|
||||
type: attr.type,
|
||||
name: attr.name,
|
||||
value: attr.value,
|
||||
position: attr.position,
|
||||
isInheritable: attr.isInheritable,
|
||||
utcDateModified: attr.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapNoteToPojo,
|
||||
mapBranchToPojo,
|
||||
mapAttributeToPojo
|
||||
};
|
82
src/etapi/notes.js
Normal file
82
src/etapi/notes.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
const becca = require("../becca/becca");
|
||||
const utils = require("../services/utils");
|
||||
const ru = require("./route_utils");
|
||||
const mappers = require("./mappers");
|
||||
const noteService = require("../services/notes");
|
||||
const TaskContext = require("../services/task_context");
|
||||
const validators = require("./validators");
|
||||
|
||||
function register(router) {
|
||||
ru.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
|
||||
const note = ru.getAndCheckNote(req.params.noteId);
|
||||
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
ru.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
|
||||
const note = ru.getAndCheckNote(req.params.noteId);
|
||||
|
||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
ru.route(router, 'post' ,'/etapi/create-note', (req, res, next) => {
|
||||
const params = req.body;
|
||||
|
||||
ru.getAndCheckNote(params.parentNoteId);
|
||||
|
||||
try {
|
||||
const resp = noteService.createNewNote(params);
|
||||
|
||||
res.json({
|
||||
note: mappers.mapNoteToPojo(resp.note),
|
||||
branch: mappers.mapBranchToPojo(resp.branch)
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
return ru.sendError(res, 400, ru.GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
'title': validators.isString,
|
||||
'type': validators.isString,
|
||||
'mime': validators.isString
|
||||
};
|
||||
|
||||
ru.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
|
||||
const note = ru.getAndCheckNote(req.params.noteId)
|
||||
|
||||
if (note.isProtected) {
|
||||
throw new ru.EtapiError(404, "NOTE_IS_PROTECTED", `Note ${req.params.noteId} is protected and cannot be modified through ETAPI`);
|
||||
}
|
||||
|
||||
ru.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
ru.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
noteService.deleteNote(note, null, new TaskContext('no-progress-reporting'));
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
132
src/etapi/route_utils.js
Normal file
132
src/etapi/route_utils.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
const cls = require("../services/cls.js");
|
||||
const sql = require("../services/sql.js");
|
||||
const log = require("../services/log.js");
|
||||
const becca = require("../becca/becca.js");
|
||||
const GENERIC_CODE = "GENERIC";
|
||||
|
||||
class EtapiError extends Error {
|
||||
constructor(statusCode, code, message) {
|
||||
super();
|
||||
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
function sendError(res, statusCode, code, message) {
|
||||
return res
|
||||
.set('Content-Type', 'application/json')
|
||||
.status(statusCode)
|
||||
.send(JSON.stringify({
|
||||
"status": statusCode,
|
||||
"code": code,
|
||||
"message": message
|
||||
}));
|
||||
}
|
||||
|
||||
function checkEtapiAuth(req, res, next) {
|
||||
if (false) {
|
||||
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function route(router, method, path, routeHandler) {
|
||||
router[method](path, checkEtapiAuth, (req, res, next) => {
|
||||
try {
|
||||
cls.namespace.bindEmitter(req);
|
||||
cls.namespace.bindEmitter(res);
|
||||
|
||||
cls.init(() => {
|
||||
cls.set('sourceId', "etapi");
|
||||
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
|
||||
|
||||
const cb = () => routeHandler(req, res, next);
|
||||
|
||||
return sql.transactional(cb);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);
|
||||
|
||||
if (e instanceof EtapiError) {
|
||||
sendError(res, e.statusCode, e.code, e.message);
|
||||
}
|
||||
else {
|
||||
sendError(res, 500, GENERIC_CODE, e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAndCheckNote(noteId) {
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (note) {
|
||||
return note;
|
||||
}
|
||||
else {
|
||||
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found`);
|
||||
}
|
||||
}
|
||||
|
||||
function getAndCheckBranch(branchId) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
|
||||
if (branch) {
|
||||
return branch;
|
||||
}
|
||||
else {
|
||||
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found`);
|
||||
}
|
||||
}
|
||||
|
||||
function getAndCheckAttribute(attributeId) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
|
||||
if (attribute) {
|
||||
return attribute;
|
||||
}
|
||||
else {
|
||||
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateAndPatch(entity, props, allowedProperties) {
|
||||
for (const key of Object.keys(props)) {
|
||||
if (!(key in allowedProperties)) {
|
||||
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED_FOR_PATCH", `Property '${key}' is not allowed for PATCH.`);
|
||||
}
|
||||
else {
|
||||
const validator = allowedProperties[key];
|
||||
const validationResult = validator(props[key]);
|
||||
|
||||
if (validationResult) {
|
||||
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validation passed, let's patch
|
||||
for (const propName of Object.keys(props)) {
|
||||
entity[propName] = props[propName];
|
||||
}
|
||||
|
||||
entity.save();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EtapiError,
|
||||
sendError,
|
||||
checkEtapiAuth,
|
||||
route,
|
||||
GENERIC_CODE,
|
||||
validateAndPatch,
|
||||
getAndCheckNote,
|
||||
getAndCheckBranch,
|
||||
getAndCheckAttribute,
|
||||
getNotAllowedPatchPropertyError: (propertyName, allowedProperties) => new EtapiError(400, "PROPERTY_NOT_ALLOWED_FOR_PATCH", `Property '${propertyName}' is not allowed to be patched, allowed properties are ${allowedProperties}.`),
|
||||
}
|
95
src/etapi/spec.openapi.yaml
Normal file
95
src/etapi/spec.openapi.yaml
Normal file
|
@ -0,0 +1,95 @@
|
|||
openapi: "3.1.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: ETAPI
|
||||
description: External Trilium API
|
||||
contact:
|
||||
name: zadam
|
||||
email: zadam.apps@gmail.com
|
||||
url: https://github.com/zadam/trilium
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
servers:
|
||||
- url: http://localhost:37740/etapi
|
||||
- url: http://localhost:8080/etapi
|
||||
paths:
|
||||
/pets/{id}:
|
||||
get:
|
||||
description: Returns a user based on a single ID, if the user does not have access to the pet
|
||||
operationId: find pet by id
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: ID of pet to fetch
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: pet response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Pet'
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
delete:
|
||||
description: deletes a single pet based on the ID supplied
|
||||
operationId: deletePet
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: ID of pet to delete
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'204':
|
||||
description: pet deleted
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
components:
|
||||
schemas:
|
||||
Pet:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NewPet'
|
||||
- type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
NewPet:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
message:
|
||||
type: string
|
77
src/etapi/special_notes.js
Normal file
77
src/etapi/special_notes.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
const specialNotesService = require("../services/special_notes");
|
||||
const dateNotesService = require("../services/date_notes");
|
||||
const ru = require("./route_utils");
|
||||
const mappers = require("./mappers");
|
||||
|
||||
const getDateInvalidError = date => new ru.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
|
||||
const getMonthInvalidError = month => new ru.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
|
||||
const getYearInvalidError = year => new ru.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
|
||||
|
||||
function isValidDate(date) {
|
||||
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!Date.parse(date);
|
||||
}
|
||||
|
||||
function register(router) {
|
||||
ru.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(res, date);
|
||||
}
|
||||
|
||||
const note = specialNotesService.getInboxNote(date);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
ru.route(router, 'get', '/etapi/date/:date', (req, res, next) => {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(res, date);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getDateNote(date);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
ru.route(router, 'get', '/etapi/week/:date', (req, res, next) => {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(res, date);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getWeekNote(date);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
ru.route(router, 'get', '/etapi/month/:month', (req, res, next) => {
|
||||
const {month} = req.params;
|
||||
|
||||
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
|
||||
throw getMonthInvalidError(res, month);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getMonthNote(month);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
ru.route(router, 'get', '/etapi/year/:year', (req, res, next) => {
|
||||
const {year} = req.params;
|
||||
|
||||
if (!/[0-9]{4}/.test(year)) {
|
||||
throw getYearInvalidError(res, year);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getYearNote(year);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
};
|
30
src/etapi/validators.js
Normal file
30
src/etapi/validators.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
function isString(obj) {
|
||||
if (typeof obj !== 'string') {
|
||||
return `'${obj}' is not a string`;
|
||||
}
|
||||
}
|
||||
|
||||
function isStringOrNull(obj) {
|
||||
if (obj) {
|
||||
return isString(obj);
|
||||
}
|
||||
}
|
||||
|
||||
function isBoolean(obj) {
|
||||
if (typeof obj !== 'boolean') {
|
||||
return `'${obj}' is not a boolean`;
|
||||
}
|
||||
}
|
||||
|
||||
function isInteger(obj) {
|
||||
if (!Number.isInteger(obj)) {
|
||||
return `'${obj}' is not an integer`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isString,
|
||||
isStringOrNull,
|
||||
isBoolean,
|
||||
isInteger
|
||||
};
|
|
@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
|
|||
|
||||
const HIDDEN_ATTRIBUTES = [
|
||||
'originalFileName',
|
||||
'fileSize',
|
||||
'template',
|
||||
'cssClass',
|
||||
'iconClass',
|
||||
|
|
|
@ -3,16 +3,17 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
|
|||
const CODE_MIRROR = {
|
||||
js: [
|
||||
"libraries/codemirror/codemirror.js",
|
||||
"libraries/codemirror/addon/mode/loadmode.js",
|
||||
"libraries/codemirror/addon/mode/simple.js",
|
||||
"libraries/codemirror/addon/fold/xml-fold.js",
|
||||
"libraries/codemirror/addon/display/placeholder.js",
|
||||
"libraries/codemirror/addon/edit/matchbrackets.js",
|
||||
"libraries/codemirror/addon/edit/matchtags.js",
|
||||
"libraries/codemirror/addon/fold/xml-fold.js",
|
||||
"libraries/codemirror/addon/lint/lint.js",
|
||||
"libraries/codemirror/addon/lint/eslint.js",
|
||||
"libraries/codemirror/addon/mode/loadmode.js",
|
||||
"libraries/codemirror/addon/mode/simple.js",
|
||||
"libraries/codemirror/addon/search/match-highlighter.js",
|
||||
"libraries/codemirror/mode/meta.js",
|
||||
"libraries/codemirror/keymap/vim.js",
|
||||
"libraries/codemirror/addon/lint/lint.js",
|
||||
"libraries/codemirror/addon/lint/eslint.js"
|
||||
"libraries/codemirror/keymap/vim.js"
|
||||
],
|
||||
css: [
|
||||
"libraries/codemirror/codemirror.css",
|
||||
|
|
|
@ -218,6 +218,13 @@ class NoteContext extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasNoteList() {
|
||||
return this.note.hasChildren()
|
||||
&& ['book', 'text', 'code'].includes(this.note.type)
|
||||
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
|
||||
&& !this.note.hasLabel('hideChildrenOverview');
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteContext;
|
||||
|
|
|
@ -29,6 +29,10 @@ const TPL = `
|
|||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
}
|
||||
|
||||
.note-detail.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
@ -128,7 +132,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
|||
|
||||
await typeWidget.handleEvent('setNoteContext', {noteContext: this.noteContext});
|
||||
|
||||
// this is happening in update() so note has been already set and we need to reflect this
|
||||
// this is happening in update() so note has been already set, and we need to reflect this
|
||||
await typeWidget.handleEvent('noteSwitched', {
|
||||
noteContext: this.noteContext,
|
||||
notePath: this.noteContext.notePath
|
||||
|
@ -136,6 +140,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
|||
|
||||
this.child(typeWidget);
|
||||
}
|
||||
|
||||
this.checkFullHeight();
|
||||
}
|
||||
|
||||
checkFullHeight() {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
this.$widget.toggleClass("full-height",
|
||||
!this.noteContext.hasNoteList()
|
||||
&& ['editable-text', 'editable-code'].includes(this.type));
|
||||
}
|
||||
|
||||
getTypeWidget() {
|
||||
|
|
|
@ -5,8 +5,6 @@ const TPL = `
|
|||
<div class="note-list-widget">
|
||||
<style>
|
||||
.note-list-widget {
|
||||
flex-grow: 100000;
|
||||
flex-shrink: 100000;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
@ -22,11 +20,7 @@ const TPL = `
|
|||
|
||||
export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& ['book', 'text', 'code'].includes(this.note.type)
|
||||
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
|
||||
&& this.note.hasChildren()
|
||||
&& !this.note.hasLabel('hideChildrenOverview');
|
||||
return super.isEnabled() && this.noteContext.hasNoteList();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
|
|
|
@ -13,10 +13,12 @@ const TPL = `
|
|||
<style>
|
||||
.note-detail-code {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-code-editor {
|
||||
min-height: 50px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -105,7 +107,8 @@ export default class EditableCodeTypeWidget extends TypeWidget {
|
|||
// we linewrap partly also because without it horizontal scrollbar displays only when you scroll
|
||||
// all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
|
||||
lineWrapping: true,
|
||||
dragDrop: false // with true the editor inlines dropped files which is not what we expect
|
||||
dragDrop: false, // with true the editor inlines dropped files which is not what we expect
|
||||
placeholder: "Type the content of your code note here..."
|
||||
});
|
||||
|
||||
this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());
|
||||
|
|
|
@ -36,6 +36,7 @@ const TPL = `
|
|||
font-family: var(--detail-font-family);
|
||||
padding-left: 14px;
|
||||
padding-top: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-editable-text a:hover {
|
||||
|
@ -73,6 +74,7 @@ const TPL = `
|
|||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
min-height: 50px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -241,6 +241,10 @@ body .CodeMirror {
|
|||
background-color: #eeeeee
|
||||
}
|
||||
|
||||
.CodeMirror pre.CodeMirror-placeholder {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
#sql-console-query {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
|
|
|
@ -3,320 +3,17 @@ const utils = require("../../services/utils");
|
|||
const noteService = require("../../services/notes");
|
||||
const attributeService = require("../../services/attributes");
|
||||
const Branch = require("../../becca/entities/branch");
|
||||
const cls = require("../../services/cls");
|
||||
const sql = require("../../services/sql");
|
||||
const log = require("../../services/log");
|
||||
const specialNotesService = require("../../services/special_notes");
|
||||
const dateNotesService = require("../../services/date_notes");
|
||||
const entityChangesService = require("../../services/entity_changes.js");
|
||||
|
||||
const GENERIC_CODE = "GENERIC";
|
||||
|
||||
function sendError(res, statusCode, code, message) {
|
||||
return res
|
||||
.set('Content-Type', 'application/json')
|
||||
.status(statusCode)
|
||||
.send(JSON.stringify({
|
||||
"status": statusCode,
|
||||
"code": code,
|
||||
"message": message
|
||||
}));
|
||||
}
|
||||
|
||||
const sendNoteNotFoundError = (res, noteId) => sendError(res, 404, "NOTE_NOT_FOUND", `Note ${noteId} not found`);
|
||||
const sendBranchNotFoundError = (res, branchId) => sendError(res, 404, "BRANCH_NOT_FOUND", `Branch ${branchId} not found`);
|
||||
const sendAttributeNotFoundError = (res, attributeId) => sendError(res, 404, "ATTRIBUTE_NOT_FOUND", `Attribute ${attributeId} not found`);
|
||||
const sendDateInvalidError = (res, date) => sendError(res, 400, "DATE_INVALID", `Date "${date}" is not valid.`);
|
||||
const sendMonthInvalidError = (res, month) => sendError(res, 400, "MONTH_INVALID", `Month "${month}" is not valid.`);
|
||||
const sendYearInvalidError = (res, year) => sendError(res, 400, "YEAR_INVALID", `Year "${year}" is not valid.`);
|
||||
|
||||
function isValidDate(date) {
|
||||
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!Date.parse(date);
|
||||
}
|
||||
|
||||
function checkEtapiAuth(req, res, next) {
|
||||
if (false) {
|
||||
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
const TaskContext = require("../../services/task_context.js");
|
||||
|
||||
function register(router) {
|
||||
function route(method, path, routeHandler) {
|
||||
router[method](path, checkEtapiAuth, (req, res, next) => {
|
||||
try {
|
||||
cls.namespace.bindEmitter(req);
|
||||
cls.namespace.bindEmitter(res);
|
||||
|
||||
cls.init(() => {
|
||||
cls.set('sourceId', "etapi");
|
||||
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
|
||||
|
||||
const cb = () => routeHandler(req, res, next);
|
||||
|
||||
return sql.transactional(cb);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`${method} ${path} threw exception: ` + e.stack);
|
||||
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
route('get', '/etapi/inbox/:date', (req, res, next) => {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
return sendDateInvalidError(res, date);
|
||||
}
|
||||
|
||||
const note = specialNotesService.getInboxNote(date);
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/date/:date', (req, res, next) => {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
return sendDateInvalidError(res, date);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getDateNote(date);
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/week/:date', (req, res, next) => {
|
||||
const {date} = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
return sendDateInvalidError(res, date);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getWeekNote(date);
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/month/:month', (req, res, next) => {
|
||||
const {month} = req.params;
|
||||
|
||||
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
|
||||
return sendMonthInvalidError(res, month);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getMonthNote(month);
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/year/:year', (req, res, next) => {
|
||||
const {year} = req.params;
|
||||
|
||||
if (!/[0-9]{4}/.test(year)) {
|
||||
return sendYearInvalidError(res, year);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getYearNote(year);
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/notes/:noteId', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return sendNoteNotFoundError(res, noteId);
|
||||
}
|
||||
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/notes/:noteId', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return sendNoteNotFoundError(res, noteId);
|
||||
}
|
||||
|
||||
res.json(mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
route('get', '/etapi/notes/:noteId/content', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return sendNoteNotFoundError(res, noteId);
|
||||
}
|
||||
|
||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader('Content-Type', note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
route('get', '/etapi/branches/:branchId', (req, res, next) => {
|
||||
const {branchId} = req.params;
|
||||
const branch = becca.getBranch(branchId);
|
||||
|
||||
if (!branch) {
|
||||
return sendBranchNotFoundError(res, branchId);
|
||||
}
|
||||
|
||||
res.json(mapBranchToPojo(branch));
|
||||
});
|
||||
|
||||
route('get', '/etapi/attributes/:attributeId', (req, res, next) => {
|
||||
const {attributeId} = req.params;
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
|
||||
if (!attribute) {
|
||||
return sendAttributeNotFoundError(res, attributeId);
|
||||
}
|
||||
|
||||
res.json(mapAttributeToPojo(attribute));
|
||||
});
|
||||
|
||||
route('post' ,'/etapi/notes', (req, res, next) => {
|
||||
const params = req.body;
|
||||
|
||||
if (!becca.getNote(params.parentNoteId)) {
|
||||
return sendNoteNotFoundError(res, params.parentNoteId);
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = noteService.createNewNote(params);
|
||||
|
||||
res.json({
|
||||
note: mapNoteToPojo(resp.note),
|
||||
branch: mapBranchToPojo(resp.branch)
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
return sendError(res, 400, GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
route('post' ,'/etapi/branches', (req, res, next) => {
|
||||
const params = req.body;
|
||||
|
||||
if (!becca.getNote(params.noteId)) {
|
||||
return sendNoteNotFoundError(res, params.noteId);
|
||||
}
|
||||
|
||||
if (!becca.getNote(params.parentNoteId)) {
|
||||
return sendNoteNotFoundError(res, params.parentNoteId);
|
||||
}
|
||||
|
||||
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
|
||||
|
||||
if (existing) {
|
||||
existing.notePosition = params.notePosition;
|
||||
existing.prefix = params.prefix;
|
||||
existing.save();
|
||||
|
||||
return res.json(mapBranchToPojo(existing));
|
||||
}
|
||||
|
||||
try {
|
||||
const branch = new Branch(params).save();
|
||||
|
||||
res.json(mapBranchToPojo(branch));
|
||||
}
|
||||
catch (e) {
|
||||
return sendError(res, 400, GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
route('post' ,'/etapi/attributes', (req, res, next) => {
|
||||
const params = req.body;
|
||||
|
||||
if (!becca.getNote(params.noteId)) {
|
||||
return sendNoteNotFoundError(res, params.noteId);
|
||||
}
|
||||
|
||||
if (params.type === 'relation' && !becca.getNote(params.value)) {
|
||||
return sendNoteNotFoundError(res, params.value);
|
||||
}
|
||||
|
||||
if (params.type !== 'relation' && params.type !== 'label') {
|
||||
return sendError(res, 400, GENERIC_CODE, `Only "relation" and "label" are supported attribute types, "${params.type}" given.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const attr = attributeService.createAttribute(params);
|
||||
|
||||
res.json(mapAttributeToPojo(attr));
|
||||
}
|
||||
catch (e) {
|
||||
return sendError(res, 400, GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
route('post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
|
||||
const {parentNoteId} = req.params;
|
||||
|
||||
if (!becca.getNote(parentNoteId)) {
|
||||
return sendNoteNotFoundError(res, parentNoteId);
|
||||
}
|
||||
|
||||
entityChangesService.addNoteReorderingEntityChange(parentNoteId, "etapi");
|
||||
});
|
||||
}
|
||||
|
||||
function mapNoteToPojo(note) {
|
||||
return {
|
||||
noteId: note.noteId,
|
||||
isProtected: note.isProtected,
|
||||
title: note.title,
|
||||
type: note.type,
|
||||
mime: note.mime,
|
||||
dateCreated: note.dateCreated,
|
||||
dateModified: note.dateModified,
|
||||
utcDateCreated: note.utcDateCreated,
|
||||
utcDateModified: note.utcDateModified,
|
||||
parentNoteIds: note.getParentNotes().map(p => p.noteId),
|
||||
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
|
||||
parentBranchIds: note.getParentBranches().map(p => p.branchId),
|
||||
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
|
||||
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
|
||||
};
|
||||
}
|
||||
|
||||
function mapBranchToPojo(branch) {
|
||||
return {
|
||||
branchId: branch.branchId,
|
||||
noteId: branch.noteId,
|
||||
parentNoteId: branch.parentNoteId,
|
||||
prefix: branch.prefix,
|
||||
notePosition: branch.notePosition,
|
||||
isExpanded: branch.isExpanded,
|
||||
utcDateModified: branch.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
function mapAttributeToPojo(attr) {
|
||||
return {
|
||||
attributeId: attr.attributeId,
|
||||
noteId: attr.noteId,
|
||||
type: attr.type,
|
||||
name: attr.name,
|
||||
value: attr.value,
|
||||
position: attr.position,
|
||||
isInheritable: attr.isInheritable,
|
||||
utcDateModified: attr.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -10,7 +10,6 @@ const appInfo = require('../../services/app_info');
|
|||
const eventService = require('../../services/events');
|
||||
const sqlInit = require('../../services/sql_init');
|
||||
const sql = require('../../services/sql');
|
||||
const optionService = require('../../services/options');
|
||||
const ApiToken = require('../../becca/entities/api_token');
|
||||
const ws = require("../../services/ws");
|
||||
|
||||
|
@ -92,6 +91,8 @@ function token(req) {
|
|||
}
|
||||
|
||||
const apiToken = new ApiToken({
|
||||
// for backwards compatibility with Sender which does not send the name
|
||||
name: req.body.tokenName || "Trilium Sender",
|
||||
token: utils.randomSecureToken()
|
||||
}).save();
|
||||
|
||||
|
|
|
@ -73,9 +73,7 @@ function deleteNote(req) {
|
|||
|
||||
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
|
||||
|
||||
for (const branch of note.getParentBranches()) {
|
||||
noteService.deleteBranch(branch, deleteId, taskContext);
|
||||
}
|
||||
noteService.deleteNote(note, deleteId, taskContext);
|
||||
|
||||
if (eraseNotes) {
|
||||
noteService.eraseNotesWithDeleteId(deleteId);
|
||||
|
|
|
@ -40,7 +40,10 @@ const backendLogRoute = require('./api/backend_log');
|
|||
const statsRoute = require('./api/stats');
|
||||
const fontsRoute = require('./api/fonts');
|
||||
const shareRoutes = require('../share/routes');
|
||||
const etapiRoutes = require('./api/etapi');
|
||||
const etapiAttributeRoutes = require('../etapi/attributes');
|
||||
const etapiBranchRoutes = require('../etapi/branches');
|
||||
const etapiNoteRoutes = require('../etapi/notes');
|
||||
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
|
||||
|
||||
const log = require('../services/log');
|
||||
const express = require('express');
|
||||
|
@ -376,7 +379,10 @@ function register(app) {
|
|||
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
|
||||
shareRoutes.register(router);
|
||||
etapiRoutes.register(router);
|
||||
etapiAttributeRoutes.register(router);
|
||||
etapiBranchRoutes.register(router);
|
||||
etapiNoteRoutes.register(router);
|
||||
etapiSpecialNoteRoutes.register(router);
|
||||
|
||||
app.use('', router);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ const build = require('./build');
|
|||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 189;
|
||||
const SYNC_VERSION = 24;
|
||||
const APP_DB_VERSION = 190;
|
||||
const SYNC_VERSION = 25;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -88,7 +88,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
|
|||
}
|
||||
|
||||
/** @returns {Note} */
|
||||
function getMonthNote(dateStr, rootNote) {
|
||||
function getMonthNote(dateStr, rootNote = null) {
|
||||
if (!rootNote) {
|
||||
rootNote = getRootCalendarNote();
|
||||
}
|
||||
|
|
|
@ -105,6 +105,10 @@ function createNewNote(params) {
|
|||
if (!params.title || params.title.trim().length === 0) {
|
||||
throw new Error(`Note title must not be empty`);
|
||||
}
|
||||
|
||||
if (params.content === null || params.content === undefined) {
|
||||
throw new Error(`Note content must be set`);
|
||||
}
|
||||
|
||||
return sql.transactional(() => {
|
||||
const note = new Note({
|
||||
|
@ -519,7 +523,7 @@ function updateNote(noteId, noteUpdates) {
|
|||
|
||||
/**
|
||||
* @param {Branch} branch
|
||||
* @param {string} deleteId
|
||||
* @param {string|null} deleteId
|
||||
* @param {TaskContext} taskContext
|
||||
*
|
||||
* @return {boolean} - true if note has been deleted, false otherwise
|
||||
|
@ -569,6 +573,17 @@ function deleteBranch(branch, deleteId, taskContext) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Note} note
|
||||
* @param {string|null} deleteId
|
||||
* @param {TaskContext} taskContext
|
||||
*/
|
||||
function deleteNote(note, deleteId, taskContext) {
|
||||
for (const branch of note.getParentBranches()) {
|
||||
deleteBranch(branch, deleteId, taskContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} noteId
|
||||
* @param {TaskContext} taskContext
|
||||
|
@ -914,6 +929,7 @@ module.exports = {
|
|||
createNewNoteWithTarget,
|
||||
updateNote,
|
||||
deleteBranch,
|
||||
deleteNote,
|
||||
undeleteNote,
|
||||
protectNoteRecursively,
|
||||
scanForLinks,
|
||||
|
|
|
@ -28,7 +28,15 @@ function initNotSyncedOptions(initialized, opts = {}) {
|
|||
optionService.createOption('lastSyncedPull', '0', false);
|
||||
optionService.createOption('lastSyncedPush', '0', false);
|
||||
|
||||
optionService.createOption('theme', opts.theme || 'white', false);
|
||||
let theme = 'dark'; // default based on the poll in https://github.com/zadam/trilium/issues/2516
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const {nativeTheme} = require('electron');
|
||||
|
||||
theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
optionService.createOption('theme', theme, false);
|
||||
|
||||
optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
|
||||
optionService.createOption('syncServerTimeout', '120000', false);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
POST {{triliumHost}}/etapi/notes
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
|
@ -15,12 +15,33 @@ Content-Type: application/json
|
|||
client.assert(response.body.branch.parentNoteId == "root");
|
||||
});
|
||||
|
||||
client.log(`Created note "${createdNoteId}" and branch ${createdBranchId}`);
|
||||
client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId);
|
||||
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
### Clone to another location
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"parentNoteId": "hidden"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.test("Request executed successfully", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.body.parentNoteId == "hidden");
|
||||
});
|
||||
|
||||
client.global.set("clonedBranchId", response.body.branchId);
|
||||
|
||||
client.log(`Created cloned branch ` + response.body.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
@ -30,6 +51,9 @@ GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
|||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.body.noteId == client.global.get("createdNoteId"));
|
||||
client.assert(response.body.title == "Hello");
|
||||
// order is not defined and may fail in the future
|
||||
client.assert(response.body.parentBranchIds[0] == client.global.get("clonedBranchId"))
|
||||
client.assert(response.body.parentBranchIds[1] == client.global.get("createdBranchId"));
|
||||
});
|
||||
%}
|
||||
|
||||
|
@ -58,6 +82,18 @@ GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
|||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
|
||||
> {%
|
||||
client.test("Request executed successfully", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.body.branchId == client.global.get("clonedBranchId"));
|
||||
client.assert(response.body.parentNoteId == "hidden");
|
||||
});
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Content-Type: application/json
|
||||
|
||||
|
@ -74,7 +110,7 @@ Content-Type: application/json
|
|||
client.assert(response.status === 200, "Response status is not 200");
|
||||
});
|
||||
|
||||
client.log(`Created attribute ${response.body.attributeId}`);
|
||||
client.log(`Created attribute ` + response.body.attributeId);
|
||||
|
||||
client.global.set("createdAttributeId", response.body.attributeId);
|
||||
%}
|
||||
|
|
56
test-etapi/delete-attribute.http
Normal file
56
test-etapi/delete-attribute.http
Normal file
|
@ -0,0 +1,56 @@
|
|||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": "true"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "ATTRIBUTE_NOT_FOUND");
|
||||
%}
|
71
test-etapi/delete-cloned-branch.http
Normal file
71
test-etapi/delete-cloned-branch.http
Normal file
|
@ -0,0 +1,71 @@
|
|||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
### Clone to another location
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"parentNoteId": "hidden"
|
||||
}
|
||||
|
||||
> {% client.global.set("clonedBranchId", response.body.branchId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "BRANCH_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
107
test-etapi/delete-note-with-all-branches.http
Normal file
107
test-etapi/delete-note-with-all-branches.http
Normal file
|
@ -0,0 +1,107 @@
|
|||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": "true"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
### Clone to another location
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"parentNoteId": "hidden"
|
||||
}
|
||||
|
||||
> {% client.global.set("clonedBranchId", response.body.branchId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
|
||||
> {% client.assert(response.status === 200, "Response status is not 200"); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "BRANCH_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "BRANCH_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "NOTE_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "ATTRIBUTE_NOT_FOUND");
|
||||
%}
|
74
test-etapi/patch-attribute.http
Normal file
74
test-etapi/patch-attribute.http
Normal file
|
@ -0,0 +1,74 @@
|
|||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": "true"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"value": "CHANGED"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.body.value === "CHANGED");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "root"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED_FOR_PATCH");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"value": null
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
61
test-etapi/patch-branch.http
Normal file
61
test-etapi/patch-branch.http
Normal file
|
@ -0,0 +1,61 @@
|
|||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"type": "text",
|
||||
"title": "Hello",
|
||||
"content": ""
|
||||
}
|
||||
|
||||
> {% client.global.set("createdBranchId", response.body.branch.branchId); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"prefix": "pref",
|
||||
"notePosition": 666,
|
||||
"isExpanded": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.prefix === 'pref');
|
||||
client.assert(response.body.notePosition === 666);
|
||||
client.assert(response.body.isExpanded === true);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED_FOR_PATCH");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"prefix": 123
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
73
test-etapi/patch-note.http
Normal file
73
test-etapi/patch-note.http
Normal file
|
@ -0,0 +1,73 @@
|
|||
POST {{triliumHost}}/etapi/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "code",
|
||||
"mime": "application/json",
|
||||
"content": "{}"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.title === 'Hello');
|
||||
client.assert(response.body.type === 'code');
|
||||
client.assert(response.body.mime === 'application/json');
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Wassup",
|
||||
"type": "html",
|
||||
"mime": "text/html"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.title === 'Wassup');
|
||||
client.assert(response.body.type === 'html');
|
||||
client.assert(response.body.mime === 'text/html');
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"isProtected": true
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED_FOR_PATCH");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": true
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
Loading…
Reference in a new issue