From 9ac307adc5cb06be570efaa8c65d9d9895d99491 Mon Sep 17 00:00:00 2001 From: Joshua Jensch Date: Fri, 6 Mar 2020 08:19:06 +0100 Subject: [PATCH] feat: add support for new search endpoint(#1732) https://github.com/verdaccio/verdaccio/issues/310 * Add an incomplete implementation of the v1/search api * Use parseInt and || instead of ?? for processing numeric arguments * Remove res.end, as we already use response.json * Remove unused request parameters and add TODO comment * Fix eslint errors Co-authored-by: Joshua Jensch --- src/api/endpoint/api/v1/search.ts | 98 +++++++++++++++++++++++++++++++ src/api/endpoint/index.ts | 5 ++ 2 files changed, 103 insertions(+) create mode 100644 src/api/endpoint/api/v1/search.ts diff --git a/src/api/endpoint/api/v1/search.ts b/src/api/endpoint/api/v1/search.ts new file mode 100644 index 000000000000..70f491d6a39b --- /dev/null +++ b/src/api/endpoint/api/v1/search.ts @@ -0,0 +1,98 @@ +import semver from 'semver' +import { Package } from '@verdaccio/types'; + +function compileTextSearch(textSearch: string): ((pkg: Package) => boolean) { + const personMatch = (person, search) => { + if(typeof person === 'string') + return person.includes(search); + + if(typeof person === 'object') + for(const field of Object.values(person)) + if(typeof field === 'string' && field.includes(search)) + return true; + + return false; + } + const matcher = function(q) { + const match = q.match(/author:(.*)/) + if(match !== null) + return (pkg) => personMatch(pkg.author, match[1]) + + // TODO: maintainer, keywords, not/is unstable insecure, boost-exact + // TODO implement some scoring system for freetext + return (pkg) => { + return ['name', 'displayName', 'description'] + .map(k => pkg[k]) + .filter(x => x !== undefined) + .some(txt => txt.includes(q)) + }; + } + + const textMatchers = (textSearch || '').split(' ').map(matcher); + return (pkg) => textMatchers.every(m => m(pkg)); +} + +export default function(route, auth, storage): void { + route.get('/-/v1/search', (req, res)=>{ + // TODO: implement proper result scoring weighted by quality, popularity and maintenance query parameters + let [text, size, from /* , quality, popularity, maintenance */] = + ['text', 'size', 'from' /* , 'quality', 'popularity', 'maintenance' */] + .map(k => req.query[k]) + + size = parseInt(size) || 20; + from = parseInt(from) || 0; + + const isInteresting = compileTextSearch(text); + + const resultStream = storage.search(0, {req: {query: {local: true}}}); + const resultBuf = [] as any; + let completed = false; + + const sendResponse = (): void => { + completed = true; + resultStream.destroy() + + const final = resultBuf.slice(from, size).map(pkg => { + return { + package: pkg, + flags: { + unstable: + Object.keys(pkg.versions) + .some(v => semver.satisfies(v, '^1.0.0')) + ? undefined + : true + }, + score: { + final: 1, + detail: { + quality: 1, + popularity: 1, + maintenance: 0 + } + }, + searchScore: 100000 + } + }) + const response = { + objects: final, + total: final.length, + time: new Date().toUTCString() + } + + res.status(200) + .json(response) + } + + resultStream.on('data', (pkg)=>{ + if(!isInteresting(pkg)) + return; + resultBuf.push(pkg) + if(!completed && resultBuf.length >= size + from) + sendResponse(); + }) + resultStream.on('end', ()=>{ + if(!completed) + sendResponse() + }) + }) +} \ No newline at end of file diff --git a/src/api/endpoint/index.ts b/src/api/endpoint/index.ts index 20b58b25cc54..c651b5cd4719 100644 --- a/src/api/endpoint/index.ts +++ b/src/api/endpoint/index.ts @@ -15,6 +15,8 @@ import stars from './api/stars'; import profile from './api/v1/profile'; import token from './api/v1/token'; +import v1Search from './api/v1/search' + const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware'); export default function(config: Config, auth: IAuth, storage: IStorageHandler) { @@ -54,6 +56,9 @@ export default function(config: Config, auth: IAuth, storage: IStorageHandler) { publish(app, auth, storage, config); ping(app); stars(app, storage); + + v1Search(app, auth, storage) + if (_.get(config, 'experiments.token') === true) { token(app, auth, storage, config); }