Add search mods request

Add paged search mods request suitable for showing a real time list of
results for a user provided search expression.
This commit is contained in:
Hornwitser committed 2022-09-24 21:10:38 +02:00
1 parent 6039715a53
commit 3eb63db313
5 files changed
+200 -2

No files matched your search

+60 -1
View File
@@ -1087,7 +1087,7 @@ modCommands.add(new libCommand.Command({
definition: [["list", "l"], "List mods stored in the cluster", (yargs) => {
yargs.options({
"fields": {
describe: "Fields",
describe: "Fields to show, supports 'all'.",
array: true,
type: "string",
default: ["name", "version", "title", "factorio_version"],
@@ -1109,6 +1109,65 @@ modCommands.add(new libCommand.Command({
},
}));
modCommands.add(new libCommand.Command({
definition: ["search <factorio-version> [query]", "Search mods stored in the cluster", (yargs) => {
yargs.positional("factorio-version", { describe: "Major version of Factorio to search for", type: "string" });
yargs.positional("query", { describe: "Search query", type: "string", default: "" });
yargs.options({
"page": {
describe: "Result page to show",
type: "number",
default: 1,
},
"page-size": {
describe: "Results per page to show",
type: "number",
default: 10,
},
"sort": {
describe: "sort results by given field",
type: "string",
},
"sort-order": {
describe: "order to sort results in (asc/desc)",
type: "string",
default: "asc",
},
"fields": {
describe: "Fields to show, supports 'all'.",
array: true,
type: "string",
default: ["name", "version", "title", "factorio_version"],
},
});
}],
handler: async function(args, control) {
let response = await libLink.messages.searchMods.send(control, {
"query": args.query,
"factorio_version": args.factorioVersion,
"page_size": args.pageSize,
"page": args.page,
"sort": args.sort,
"sort_order": args.sortOrder,
});
let results = response.results.flatMap(result => result.versions);
if (!args.fields.includes("all")) {
for (let entry of results) {
for (let field of Object.keys(entry)) {
if (!args.fields.includes(field)) {
delete entry[field];
}
}
}
}
for (let issue of response.query_issues) {
print(issue);
}
print(`page ${args.page} of ${response.page_count} (${response.result_count} results)`);
print(asTable(results));
},
}));
modCommands.add(new libCommand.Command({
definition: ["upload <file>", "Upload mod to the cluster", (yargs) => {
yargs.positional("file", { describe: "File to upload", type: "string" });
+36
View File
@@ -800,6 +800,42 @@ messages.listMods = new Request({
},
});
messages.searchMods = new Request({
type: "search_mods",
links: ["control-master"],
permission: "core.mod.search",
requestRequired: ["query", "factorio_version", "page"],
requestProperties: {
"query": { type: "string" },
"factorio_version": { type: "string" },
"page_size": { type: "integer" },
"page": { type: "integer" },
"sort": { type: "string" },
"sort_order": { type: "string" },
},
responseProperties: {
"query_issues": {
type: "array",
items: { type: "string" },
},
"page_count": { type: "integer" },
"result_count": { type: "integer" },
"results": {
type: "array",
items: {
type: "object",
properties: {
"name": { type: "string" },
"versions": {
type: "array",
items: libData.ModInfo.jsonSchema,
},
},
},
},
},
});
messages.setModSubscriptions = new Request({
type: "set_mod_subscriptions",
links: ["control-master"],
+7 -1
View File
@@ -553,7 +553,13 @@ definePermission({
definePermission({
name: "core.mod.list",
title: "List mods",
description: "Get the full list of mods stored on the master",
description: "Get the full list of mods stored on the master.",
grantByDefault: true,
});
definePermission({
name: "core.mod.search",
title: "Search mods",
description: "Search through the list of mods stored on the master.",
grantByDefault: true,
});
definePermission({
+90
View File
@@ -18,6 +18,8 @@ const libUsers = require("@clusterio/lib/users");
const BaseConnection = require("./BaseConnection");
const routes = require("./routes");
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
const lastQueryLogTime = new libPrometheus.Gauge(
"clusterio_master_last_query_log_duration_seconds",
"Time in seconds the last log query took to execute."
@@ -543,6 +545,94 @@ class ControlConnection extends BaseConnection {
return { list: [...this._master.mods.values()].map(mod => mod.toJSON()) };
}
static termsMatchesMod(terms, mod) {
for (let term of terms) {
if (term.type === "word") {
if (!libHelpers.wordMatches(term,
mod.name, mod.version, mod.title, mod.author, mod.contact,
mod.homepage, mod.description, mod.filename
)) {
return false;
}
} else if (term.type === "attribute") {
if (!libHelpers.wordMatches(term.value, mod[term.name])) {
return false;
}
}
}
return true;
}
async searchModsRequestHandler(message) {
let query = libHelpers.parseSearchString(message.data.query, {
name: "word",
// version
title: "word",
author: "word",
contact: "word",
homepage: "word",
description: "word",
// factorioVersion
// dependencies
filename: "word",
// size
hash: "word",
});
let factorioVersion = message.data.factorio_version;
let results = new Map();
for (let mod of this._master.mods.values()) {
if (
mod.factorioVersion !== factorioVersion
|| !ControlConnection.termsMatchesMod(query.terms, mod)
) {
continue;
}
let result = results.get(mod.name);
if (!result) {
result = {
name: mod.name,
versions: [],
};
results.set(mod.name, result);
}
result.versions.push(mod);
}
for (let result of results.values()) {
result.versions.sort((a, b) => b.integerVersion - a.integerVersion);
result.versions.map(e => e.toJSON());
}
let resultList = [...results.values()];
const sort = message.data.sort;
if (sort) {
const sorters = {
name: (a, b) => strcmp(a.versions[0].name, b.versions[0].name),
title: (a, b) => strcmp(a.versions[0].title, b.versions[0].title),
author: (a, b) => strcmp(a.versions[0].author, b.versions[0].author),
};
if (!Object.prototype.hasOwnProperty.call(sorters, sort)) {
throw new libErrors.RequestError(`Invalid value for sort: ${sort}`);
}
resultList.sort(sorters[sort]);
let order = message.data.sort_order;
if (order === "desc") {
resultList.reverse();
}
}
const page = message.data.page;
const pageSize = message.data.page_size || 10;
resultList = resultList.slice((page - 1) * pageSize, page * pageSize);
return {
query_issues: query.issues,
page_count: Math.ceil(results.size / pageSize),
result_count: results.size,
results: resultList,
};
}
async setModSubscriptionsRequestHandler(message) {
this.modSubscriptions = message.data;
}
+7
View File
@@ -929,6 +929,13 @@ describe("Integration of Clusterio", function() {
});
});
describe("mod search", function() {
it("searches the list of mods", async function() {
let result = await execCtl("mod search 1.1 name:empty_mod");
assert(result.stdout.indexOf("empty_mod") !== -1, "empty_mod is not in the result");
});
});
describe("mod download", function() {
it("downloads a mod", async function() {
await fs.unlink(path.join("temp", "test", "empty_mod_1.0.0.zip"));