diff --git a/source/server/routes/api/v1/scenes/get.ts b/source/server/routes/api/v1/scenes/get.ts index ecb1e29a..04e4a280 100644 --- a/source/server/routes/api/v1/scenes/get.ts +++ b/source/server/routes/api/v1/scenes/get.ts @@ -18,7 +18,9 @@ export default async function getScenes(req :Request, res :Response){ match, access, limit, - offset + offset, + orderBy, + orderDirection, } = req.query; access = ((Array.isArray(access))?access : (access?[access]:undefined)) as any; @@ -51,14 +53,17 @@ export default async function getScenes(req :Request, res :Response){ scenes = await Promise.all(scenesList.map(name=>vfs.getScene(name))); }else{ /**@fixme ugly hach to bypass permissions when not performing a search */ - scenes = await vfs.getScenes((u.isAdministrator && !access && !match)?undefined: u.uid, { + const requester_id = (u.isAdministrator && !access && !match)?undefined: u.uid; + scenes = await vfs.getScenes(requester_id, { match: match as string, + orderBy: orderBy as any, + orderDirection: orderDirection as any, access: access as AccessType[], limit: limit? parseInt(limit as string): undefined, offset: offset? parseInt(offset as string): undefined, }); } - + await (await import("node:timers/promises")).setTimeout(1000); //canonicalize scenes' thumb names scenes = scenes.map(s=>({...s, thumb: (s.thumb? new URL(encodeURI(path.join("/scenes/", s.name, s.thumb)), getHost(req)).toString() : undefined)})) diff --git a/source/server/vfs/Scenes.ts b/source/server/vfs/Scenes.ts index 0a50f80d..e678c890 100644 --- a/source/server/vfs/Scenes.ts +++ b/source/server/vfs/Scenes.ts @@ -6,7 +6,6 @@ import BaseVfs from "./Base.js"; import { ItemEntry, Scene, SceneQuery } from "./types.js"; - export default abstract class ScenesVfs extends BaseVfs{ async createScene(name :string):Promise @@ -87,14 +86,23 @@ export default abstract class ScenesVfs extends BaseVfs{ * get all scenes when called without params * Search scenes with structured queries when called with filters */ - async getScenes(user_id ?:number, {access, match, limit =10, offset = 0} :SceneQuery = {}) :Promise{ + async getScenes(user_id ?:number, {access, match, limit =10, offset = 0, orderBy="name", orderDirection="asc"} :SceneQuery = {}) :Promise{ + + //Check various parameters compliance if(Array.isArray(access) && access.find(a=>AccessTypes.indexOf(a) === -1)){ throw new BadRequestError(`Bad access type requested : ${access.join(", ")}`); } + if(typeof limit !="number" || Number.isNaN(limit) || limit < 0) throw new BadRequestError(`When provided, limit must be a number`); if(typeof offset != "number" || Number.isNaN(offset) || offset < 0) throw new BadRequestError(`When provided, offset must be a number`); + + if(["asc", "desc"].indexOf(orderDirection.toLowerCase()) === -1) throw new BadRequestError(`Invalid orderDirection: ${orderDirection}`); + if(["ctime", "mtime", "name"].indexOf(orderBy.toLowerCase()) === -1) throw new BadRequestError(`Invalid orderBy: ${orderBy}`); + let with_filter = typeof user_id === "number" || match; + const sortString = (orderBy == "name")? "LOWER(scene_name)": orderBy; + let likeness = ""; let mParams :Record = {}; @@ -155,6 +163,7 @@ export default abstract class ScenesVfs extends BaseVfs{ last_docs.fk_scene_id = documents.fk_scene_id AND last_docs.generation = documents.generation ) + SELECT IFNULL(mtime, scenes.ctime) as mtime, scenes.ctime AS ctime, @@ -172,9 +181,11 @@ export default abstract class ScenesVfs extends BaseVfs{ "any", json_extract(scenes.access, '$.1'), "default", json_extract(scenes.access, '$.0') ) AS access + FROM scenes LEFT JOIN last_docs AS document ON fk_scene_id = scene_id LEFT JOIN json_tree(document.metas) AS thumb ON thumb.fullkey LIKE "$[_].images[_]" AND json_extract(thumb.value, '$.quality') = 'Thumb' + ${with_filter? "WHERE true": ""} ${typeof user_id === "number"? `AND COALESCE( @@ -185,8 +196,9 @@ export default abstract class ScenesVfs extends BaseVfs{ `:""} ${(access?.length)? `AND json_extract(scenes.access, '$.' || $user_id) IN (${ access.map(s=>`'${s}'`).join(", ") })`:""} ${likeness} + GROUP BY scene_id - ORDER BY LOWER(scene_name) ASC + ORDER BY ${sortString} ${orderDirection.toUpperCase()} LIMIT $offset, $limit `, { ...mParams, diff --git a/source/server/vfs/types.ts b/source/server/vfs/types.ts index 6e25c1be..38cf0887 100644 --- a/source/server/vfs/types.ts +++ b/source/server/vfs/types.ts @@ -81,4 +81,6 @@ export interface SceneQuery { match ?:string; offset ?:number; limit ?:number; + orderBy ?:"ctime"|"mtime"|"name"; + orderDirection ?:"asc"|"desc"; } diff --git a/source/server/vfs/vfs.test.ts b/source/server/vfs/vfs.test.ts index ced4ea8d..f8712bfa 100644 --- a/source/server/vfs/vfs.test.ts +++ b/source/server/vfs/vfs.test.ts @@ -319,6 +319,22 @@ describe("Vfs", function(){ }); + describe("ordering", function(){ + it("rejects bad orderBy key", async function(){ + await expect(vfs.getScenes(0, {orderBy: "bad" as any})).to.be.rejectedWith("Invalid orderBy: bad"); + }) + it("rejects bad orderDirection key", async function(){ + await expect(vfs.getScenes(0, {orderDirection: "bad" as any})).to.be.rejectedWith("Invalid orderDirection: bad"); + }); + it("can order by name descending", async function(){ + for(let i = 0; i < 10; i++){ + await vfs.createScene(`${i}_scene`); + } + const scenes = await vfs.getScenes(0, {orderBy: "name", orderDirection: "desc"}); + expect(scenes.map(s=>s.name)).to.deep.equal([9,8,7,6,5,4,3,2,1,0].map(n=>n+"_scene")); + }); + }); + describe("pagination", function(){ it("rejects bad LIMIT", async function(){ let fixtures = [-1, "10", null];