Files
genieacs/lib/ui/db.ts
T
Zaid Humaimidi 3169e829e7 Refactor expression to Mongo query translation
The previous approach was simplistic and could result in inefficient queries
in certain edge cases, notably causing MongoDB to not utilize existing indexes
when querying by last inform timestamps given upper and lower bounds. The new
implementation leverages our existing expression minimizer code and can
therefore generate more efficient queries or at least queries that can take
advantage of existing indexes.
2023-08-09 19:48:49 -07:00

345 lines
9.6 KiB
TypeScript

/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { Db, GridFSBucket, ObjectId } from "mongodb";
import { Script } from "vm";
import { onConnect, optimizeProjection } from "../db";
import * as mongodbFunctions from "../mongodb-functions";
import { evaluate } from "../common/expression/util";
import { QueryOptions, Expression } from "../types";
import { Readable } from "stream";
const RESOURCE_COLLECTION = {
files: "fs.files",
};
let db: Db;
onConnect(async (_db) => {
db = _db;
});
export function query(
resource: string,
filter: Expression,
options?: QueryOptions
): Promise<any[]>;
export function query(
resource: string,
filter: Expression,
options: QueryOptions,
callback: (doc: any) => void
): Promise<void>;
export function query(
resource: string,
filter: Expression,
options?: QueryOptions,
callback?: (doc: any) => void
): Promise<void | any[]> {
options = options || {};
filter = evaluate(filter, null, Date.now());
const q = mongodbFunctions.toMongoQuery(filter, resource);
if (!q) return Promise.resolve([]);
return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
const cursor = collection.find(q);
if (options.projection) {
let projection = options.projection;
if (resource === "devices") {
projection = mongodbFunctions.processDeviceProjection(
options.projection
);
}
if (resource === "presets") projection.configurations = 1;
projection = optimizeProjection(projection);
cursor.project(projection);
}
if (resource === "users") cursor.project({ password: 0, salt: 0 });
if (options.skip) cursor.skip(options.skip);
if (options.limit) cursor.limit(options.limit);
if (options.sort) {
let s = Object.entries(options.sort)
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
.reduce(
(obj, [k, v]) =>
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
{}
);
if (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
cursor.sort(s);
}
if (!callback) {
cursor.toArray((err, docs) => {
if (err) return reject(err);
if (resource === "devices")
return resolve(docs.map((d) => mongodbFunctions.flattenDevice(d)));
else if (resource === "faults")
return resolve(docs.map((d) => mongodbFunctions.flattenFault(d)));
else if (resource === "tasks")
return resolve(docs.map((d) => mongodbFunctions.flattenTask(d)));
else if (resource === "presets")
return resolve(docs.map((d) => mongodbFunctions.flattenPreset(d)));
else if (resource === "files")
return resolve(docs.map((d) => mongodbFunctions.flattenFile(d)));
return resolve(docs);
});
} else {
cursor.forEach(
(doc) => {
if (resource === "devices")
callback(mongodbFunctions.flattenDevice(doc));
else if (resource === "faults")
callback(mongodbFunctions.flattenFault(doc));
else if (resource === "tasks")
callback(mongodbFunctions.flattenTask(doc));
else if (resource === "presets")
callback(mongodbFunctions.flattenPreset(doc));
else if (resource === "files")
callback(mongodbFunctions.flattenFile(doc));
else callback(doc);
},
(err) => {
if (err) reject(err);
else resolve();
}
);
}
});
}
export function count(resource: string, filter: Expression): Promise<number> {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
filter = evaluate(filter, null, Date.now());
const q = mongodbFunctions.toMongoQuery(filter, resource);
if (!q) return Promise.resolve(0);
return collection.countDocuments(q);
}
export async function updateDeviceTags(
deviceId: string,
tags: Record<string, boolean>
): Promise<void> {
const add = [];
const pull = [];
for (let [tag, onOff] of Object.entries(tags)) {
tag = tag.trim();
if (onOff) add.push(tag);
else pull.push(tag);
}
const collection = db.collection("devices");
const object = {};
if (add?.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull?.length) object["$pullAll"] = { _tags: pull };
await collection.updateOne({ _id: deviceId }, object);
}
function putResource(resource, id, object): Promise<void> {
return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
collection.replaceOne({ _id: id }, object, { upsert: true }, (err) => {
if (err) return void reject(err);
resolve();
});
});
}
function deleteResource(
resource: string,
id: string | ObjectId
): Promise<void> {
return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
collection.deleteOne({ _id: id }, (err) => {
if (err) return void reject(err);
resolve();
});
});
}
export function putPreset(
id: string,
object: Record<string, unknown>
): Promise<void> {
object = mongodbFunctions.preProcessPreset(object);
return putResource("presets", id, object);
}
export function deletePreset(id: string): Promise<void> {
return deleteResource("presets", id);
}
export function putProvision(
id: string,
object: Record<string, unknown>
): Promise<void> {
if (!object.script) object.script = "";
try {
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
filename: id,
lineOffset: -1,
});
} catch (err) {
if (err.stack?.startsWith(`${id}:`)) {
return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
);
}
return Promise.reject(err);
}
return putResource("provisions", id, object);
}
export function deleteProvision(id: string): Promise<void> {
return deleteResource("provisions", id);
}
export function putVirtualParameter(
id: string,
object: Record<string, unknown>
): Promise<void> {
if (!object.script) object.script = "";
try {
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
filename: id,
lineOffset: -1,
});
} catch (err) {
if (err.stack?.startsWith(`${id}:`)) {
return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
);
}
return Promise.reject(err);
}
return putResource("virtualParameters", id, object);
}
export function deleteVirtualParameter(id: string): Promise<void> {
return deleteResource("virtualParameters", id);
}
export function putConfig(
id: string,
object: Record<string, unknown>
): Promise<void> {
return putResource("config", id, object);
}
export function deleteConfig(id: string): Promise<void> {
return deleteResource("config", id);
}
export function putPermission(
id: string,
object: Record<string, unknown>
): Promise<void> {
return putResource("permissions", id, object);
}
export function deletePermission(id: string): Promise<void> {
return deleteResource("permissions", id);
}
export function putUser(
id: string,
object: Record<string, unknown>
): Promise<void> {
return new Promise((resolve, reject) => {
const collection = db.collection("users");
// update instead of replace to keep the password if not set by user
collection.updateOne(
{ _id: id },
{ $set: object },
{ upsert: true },
(err) => {
if (err) return void reject(err);
resolve();
}
);
});
}
export function deleteUser(id: string): Promise<void> {
return deleteResource("users", id);
}
export function downloadFile(filename: string): Readable {
const bucket = new GridFSBucket(db);
return bucket.openDownloadStreamByName(filename);
}
export function putFile(
filename: string,
metadata: Record<string, string>,
contentStream: Readable
): Promise<void> {
return new Promise((resolve, reject) => {
const bucket = new GridFSBucket(db);
const uploadStream = bucket.openUploadStreamWithId(
filename as unknown as ObjectId,
filename,
{
metadata: metadata,
}
);
contentStream.on("close", () => {
if (!contentStream.readableEnded)
uploadStream.destroy(new Error("Stream closed prematurely"));
});
contentStream.on("error", (err) => {
uploadStream.destroy(err);
});
uploadStream.on("error", reject);
uploadStream.on("finish", resolve);
contentStream.pipe(uploadStream);
});
}
export function deleteFile(filename: string): Promise<void> {
return new Promise((resolve, reject) => {
const bucket = new GridFSBucket(db);
bucket.delete(filename as any, (err) => {
if (err) return void reject(err);
resolve();
});
});
}
export function deleteFault(id: string): Promise<void> {
return deleteResource("faults", id);
}
export function deleteTask(id: ObjectId): Promise<void> {
return deleteResource("tasks", id);
}