mirror of https://github.com/TriliumNext/Notes
ETAPI delete/patch, refactoring
parent
82b2871a08
commit
9ee1c9f3da
@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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}.`),
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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");
|
||||||
|
%}
|
||||||
@ -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"); %}
|
||||||
@ -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");
|
||||||
|
%}
|
||||||
@ -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");
|
||||||
|
%}
|
||||||
@ -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");
|
||||||
|
%}
|
||||||
@ -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 New Issue