mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Server: Improved how routes can be defined
This commit is contained in:
parent
7652a5a0a0
commit
7ad29577f9
@ -1,6 +1,6 @@
|
|||||||
import routes from '../routes/routes';
|
import routes from '../routes/routes';
|
||||||
import { ErrorNotFound } from '../utils/errors';
|
import { ErrorForbidden, ErrorNotFound } from '../utils/errors';
|
||||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from '../utils/routeUtils';
|
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute, findEndPoint } from '../utils/routeUtils';
|
||||||
import { AppContext, Env } from '../utils/types';
|
import { AppContext, Env } from '../utils/types';
|
||||||
import mustacheService, { isView, View } from '../services/MustacheService';
|
import mustacheService, { isView, View } from '../services/MustacheService';
|
||||||
|
|
||||||
@ -13,7 +13,16 @@ export default async function(ctx: AppContext) {
|
|||||||
const match = findMatchingRoute(ctx.path, routes);
|
const match = findMatchingRoute(ctx.path, routes);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
let responseObject = null;
|
||||||
|
|
||||||
|
if (match.route.endPoints) {
|
||||||
|
const routeHandler = findEndPoint(match.route, ctx.request.method, match.subPath.schema);
|
||||||
|
responseObject = await routeHandler(match.subPath, ctx);
|
||||||
|
|
||||||
|
if (!match.route.public && !ctx.owner) throw new ErrorForbidden();
|
||||||
|
} else {
|
||||||
|
responseObject = await match.route.exec(match.subPath, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
if (responseObject instanceof Response) {
|
if (responseObject instanceof Response) {
|
||||||
ctx.response = responseObject.response;
|
ctx.response = responseObject.response;
|
||||||
|
@ -6,6 +6,8 @@ const route: Route = {
|
|||||||
return { status: 'ok', message: 'Joplin Server is running' };
|
return { status: 'ok', message: 'Joplin Server is running' };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
public: true,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default route;
|
export default route;
|
||||||
|
@ -6,6 +6,8 @@ const route: Route = {
|
|||||||
return { status: 'ok', message: 'Joplin Server is running' };
|
return { status: 'ok', message: 'Joplin Server is running' };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
public: true,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default route;
|
export default route;
|
||||||
|
@ -28,6 +28,8 @@ const route: Route = {
|
|||||||
throw new ErrorNotFound(`Invalid link: ${path.link}`);
|
throw new ErrorNotFound(`Invalid link: ${path.link}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
public: true,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default route;
|
export default route;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SubPath, Route, respondWithFileContent, redirect } from '../../utils/routeUtils';
|
import { SubPath, Route, respondWithFileContent, redirect } from '../../utils/routeUtils';
|
||||||
import { AppContext } from '../../utils/types';
|
import { AppContext } from '../../utils/types';
|
||||||
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
||||||
import { ErrorMethodNotAllowed, ErrorNotFound } from '../../utils/errors';
|
import { ErrorNotFound } from '../../utils/errors';
|
||||||
import { File } from '../../db';
|
import { File } from '../../db';
|
||||||
import { createPaginationLinks, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
import { createPaginationLinks, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
||||||
import { setQueryParameters } from '../../utils/urlUtils';
|
import { setQueryParameters } from '../../utils/urlUtils';
|
||||||
@ -21,9 +21,14 @@ function makeFilePagination(query: any): Pagination {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endPoints = {
|
const route: Route = {
|
||||||
|
|
||||||
|
endPoints: {
|
||||||
|
|
||||||
'GET': {
|
'GET': {
|
||||||
|
|
||||||
|
'files': 'files/:id',
|
||||||
|
|
||||||
'files/:id': async function(path: SubPath, ctx: AppContext) {
|
'files/:id': async function(path: SubPath, ctx: AppContext) {
|
||||||
const dirId = path.id;
|
const dirId = path.id;
|
||||||
const query = ctx.query;
|
const query = ctx.query;
|
||||||
@ -103,6 +108,7 @@ const endPoints = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
'POST': {
|
'POST': {
|
||||||
|
|
||||||
'files': async function(_path: SubPath, ctx: AppContext) {
|
'files': async function(_path: SubPath, ctx: AppContext) {
|
||||||
const sessionId = contextSessionId(ctx);
|
const sessionId = contextSessionId(ctx);
|
||||||
|
|
||||||
@ -122,26 +128,6 @@ const endPoints = {
|
|||||||
return redirect(ctx, await ctx.models.file({ userId: user.id }).fileUrl(parentId, ctx.query));
|
return redirect(ctx, await ctx.models.file({ userId: user.id }).fileUrl(parentId, ctx.query));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
const route: Route = {
|
|
||||||
|
|
||||||
exec: async function(path: SubPath, ctx: AppContext) {
|
|
||||||
if (ctx.method === 'GET') {
|
|
||||||
if (!path.link) {
|
|
||||||
return endPoints.GET['files/:id'](path, ctx);
|
|
||||||
} else if (path.link === 'content') {
|
|
||||||
return endPoints.GET['files/:id/content'](path, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ErrorNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.method === 'POST') {
|
|
||||||
return endPoints.POST['files'](path, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ErrorMethodNotAllowed();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -35,6 +35,8 @@ const route: Route = {
|
|||||||
throw new ErrorMethodNotAllowed();
|
throw new ErrorMethodNotAllowed();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
public: true,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default route;
|
export default route;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SubPath, Route, redirect } from '../../utils/routeUtils';
|
import { SubPath, Route, redirect, findEndPoint } from '../../utils/routeUtils';
|
||||||
import { AppContext } from '../../utils/types';
|
import { AppContext } from '../../utils/types';
|
||||||
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
import { formParse } from '../../utils/requestUtils';
|
||||||
import { ErrorMethodNotAllowed, ErrorUnprocessableEntity } from '../../utils/errors';
|
import { ErrorUnprocessableEntity } from '../../utils/errors';
|
||||||
import { User } from '../../db';
|
import { User } from '../../db';
|
||||||
import { baseUrl } from '../../config';
|
import { baseUrl } from '../../config';
|
||||||
import { View } from '../../services/MustacheService';
|
import { View } from '../../services/MustacheService';
|
||||||
@ -31,9 +31,12 @@ function userIsMe(path: SubPath): boolean {
|
|||||||
return path.id === 'me';
|
return path.id === 'me';
|
||||||
}
|
}
|
||||||
|
|
||||||
const endPoints = {
|
const route: Route = {
|
||||||
|
|
||||||
|
endPoints: {
|
||||||
|
|
||||||
'GET': {
|
'GET': {
|
||||||
|
|
||||||
'users': async function(_path: SubPath, ctx: AppContext) {
|
'users': async function(_path: SubPath, ctx: AppContext) {
|
||||||
const userModel = ctx.models.user({ userId: ctx.owner.id });
|
const userModel = ctx.models.user({ userId: ctx.owner.id });
|
||||||
const users = await userModel.all();
|
const users = await userModel.all();
|
||||||
@ -76,6 +79,9 @@ const endPoints = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
'POST': {
|
'POST': {
|
||||||
|
|
||||||
|
'users/:id': 'users',
|
||||||
|
|
||||||
'users': async function(path: SubPath, ctx: AppContext) {
|
'users': async function(path: SubPath, ctx: AppContext) {
|
||||||
let user: User = {};
|
let user: User = {};
|
||||||
const userId = userIsMe(path) ? ctx.owner.id : path.id;
|
const userId = userIsMe(path) ? ctx.owner.id : path.id;
|
||||||
@ -102,30 +108,12 @@ const endPoints = {
|
|||||||
|
|
||||||
return redirect(ctx, `${baseUrl()}/users${userIsMe(path) ? '/me' : ''}`);
|
return redirect(ctx, `${baseUrl()}/users${userIsMe(path) ? '/me' : ''}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return endPoints.GET['users/:id'](path, ctx, user, error);
|
const endPoint = findEndPoint(route, 'GET', 'users/:id');
|
||||||
|
return endPoint(path, ctx, user, error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
const route: Route = {
|
|
||||||
|
|
||||||
exec: async function(path: SubPath, ctx: AppContext) {
|
|
||||||
contextSessionId(ctx);
|
|
||||||
|
|
||||||
if (ctx.method === 'GET') {
|
|
||||||
if (path.id) {
|
|
||||||
return endPoints.GET['users/:id'](path, ctx);
|
|
||||||
} else {
|
|
||||||
return endPoints.GET['users'](path, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.method === 'POST') {
|
|
||||||
return endPoints.POST['users'](path, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ErrorMethodNotAllowed();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ describe('routeUtils', function() {
|
|||||||
const link = t[2];
|
const link = t[2];
|
||||||
const addressingType = t[3];
|
const addressingType = t[3];
|
||||||
|
|
||||||
const parsed = parseSubPath(path);
|
const parsed = parseSubPath('', path);
|
||||||
expect(parsed.id).toBe(id);
|
expect(parsed.id).toBe(id);
|
||||||
expect(parsed.link).toBe(link);
|
expect(parsed.link).toBe(link);
|
||||||
expect(parsed.addressingType).toBe(addressingType);
|
expect(parsed.addressingType).toBe(addressingType);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { File, ItemAddressingType } from '../db';
|
import { File, ItemAddressingType } from '../db';
|
||||||
import { ErrorBadRequest } from './errors';
|
import { ErrorBadRequest, ErrorMethodNotAllowed, ErrorNotFound } from './errors';
|
||||||
import { AppContext } from './types';
|
import { AppContext } from './types';
|
||||||
|
|
||||||
const { ltrimSlashes, rtrimSlashes } = require('@joplin/lib/path-utils');
|
const { ltrimSlashes, rtrimSlashes } = require('@joplin/lib/path-utils');
|
||||||
@ -22,9 +22,23 @@ export enum RouteResponseFormat {
|
|||||||
Json = 'json',
|
Json = 'json',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RouteHandler = (path: SubPath, ctx: AppContext, ...args: any[])=> Promise<any>;
|
||||||
|
|
||||||
|
export interface RouteEndPoint {
|
||||||
|
[path: string]: RouteHandler | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteEndPoints {
|
||||||
|
[method: string]: RouteEndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Route {
|
export interface Route {
|
||||||
exec: Function;
|
exec?: RouteHandler;
|
||||||
responseFormat?: RouteResponseFormat;
|
responseFormat?: RouteResponseFormat;
|
||||||
|
endPoints?: RouteEndPoints;
|
||||||
|
|
||||||
|
// Public routes can be accessed without authentication.
|
||||||
|
public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Routes {
|
export interface Routes {
|
||||||
@ -36,6 +50,7 @@ export interface SubPath {
|
|||||||
link: string;
|
link: string;
|
||||||
addressingType: ItemAddressingType;
|
addressingType: ItemAddressingType;
|
||||||
raw: string;
|
raw: string;
|
||||||
|
schema: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchedRoute {
|
export interface MatchedRoute {
|
||||||
@ -70,6 +85,23 @@ export interface PathInfo {
|
|||||||
dirname: string;
|
dirname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findEndPoint(route: Route, method: string, schema: string): RouteHandler {
|
||||||
|
if (!route.endPoints[method]) throw new ErrorMethodNotAllowed(`Not allowed: ${method} ${schema}`);
|
||||||
|
const endPoint = route.endPoints[method][schema];
|
||||||
|
if (!endPoint) throw new ErrorNotFound(`Not found: ${method} ${schema}`);
|
||||||
|
|
||||||
|
let endPointFn = endPoint;
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
if (typeof endPointFn === 'string') {
|
||||||
|
endPointFn = route.endPoints[method]?.[endPointFn];
|
||||||
|
} else {
|
||||||
|
return endPointFn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ErrorNotFound(`Could not resolve: ${method} ${schema}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function redirect(ctx: AppContext, url: string): Response {
|
export function redirect(ctx: AppContext, url: string): Response {
|
||||||
ctx.redirect(url);
|
ctx.redirect(url);
|
||||||
ctx.response.status = 302;
|
ctx.response.status = 302;
|
||||||
@ -113,7 +145,7 @@ export function isPathBasedAddressing(fileId: string): boolean {
|
|||||||
//
|
//
|
||||||
// root:/Documents/MyFile.md:/content
|
// root:/Documents/MyFile.md:/content
|
||||||
// ABCDEFG/content
|
// ABCDEFG/content
|
||||||
export function parseSubPath(p: string): SubPath {
|
export function parseSubPath(basePath: string, p: string): SubPath {
|
||||||
p = rtrimSlashes(ltrimSlashes(p));
|
p = rtrimSlashes(ltrimSlashes(p));
|
||||||
|
|
||||||
const output: SubPath = {
|
const output: SubPath = {
|
||||||
@ -121,6 +153,7 @@ export function parseSubPath(p: string): SubPath {
|
|||||||
link: '',
|
link: '',
|
||||||
addressingType: ItemAddressingType.Id,
|
addressingType: ItemAddressingType.Id,
|
||||||
raw: p,
|
raw: p,
|
||||||
|
schema: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const colonIndex1 = p.indexOf(':');
|
const colonIndex1 = p.indexOf(':');
|
||||||
@ -141,6 +174,13 @@ export function parseSubPath(p: string): SubPath {
|
|||||||
if (s.length >= 2) output.link = s[1];
|
if (s.length >= 2) output.link = s[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (basePath) {
|
||||||
|
const schema = [basePath];
|
||||||
|
if (output.id) schema.push(':id');
|
||||||
|
if (output.link) schema.push(output.link);
|
||||||
|
output.schema = schema.join('/');
|
||||||
|
// }
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +219,7 @@ export function findMatchingRoute(path: string, routes: Routes): MatchedRoute {
|
|||||||
return {
|
return {
|
||||||
route: routes[basePath],
|
route: routes[basePath],
|
||||||
basePath: basePath,
|
basePath: basePath,
|
||||||
subPath: parseSubPath(`/${splittedPath.join('/')}`),
|
subPath: parseSubPath(basePath, `/${splittedPath.join('/')}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,7 +230,7 @@ export function findMatchingRoute(path: string, routes: Routes): MatchedRoute {
|
|||||||
return {
|
return {
|
||||||
route: routes[basePath],
|
route: routes[basePath],
|
||||||
basePath: basePath,
|
basePath: basePath,
|
||||||
subPath: parseSubPath(`/${splittedPath.join('/')}`),
|
subPath: parseSubPath(basePath, `/${splittedPath.join('/')}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +238,7 @@ export function findMatchingRoute(path: string, routes: Routes): MatchedRoute {
|
|||||||
return {
|
return {
|
||||||
route: routes[''],
|
route: routes[''],
|
||||||
basePath: '',
|
basePath: '',
|
||||||
subPath: parseSubPath(`/${splittedPath.join('/')}`),
|
subPath: parseSubPath('', `/${splittedPath.join('/')}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,4 +44,12 @@ export interface Config {
|
|||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum HttpMethod {
|
||||||
|
GET = 'GET',
|
||||||
|
POST = 'POST',
|
||||||
|
DELETE = 'DELETE',
|
||||||
|
PATCH = 'PATCH',
|
||||||
|
HEAD = 'HEAD',
|
||||||
|
}
|
||||||
|
|
||||||
export type KoaNext = ()=> Promise<void>;
|
export type KoaNext = ()=> Promise<void>;
|
||||||
|
Loading…
Reference in New Issue
Block a user