perf(admin-next): optimize mongodb's performance on statistical quantities

pull/90/head
moonrailgun 2 years ago
parent 98e78e28f8
commit 11154f735b

@ -2512,7 +2512,7 @@ packages:
'@babel/traverse': 7.21.2(supports-color@5.5.0)
'@babel/types': 7.21.2
convert-source-map: 1.9.0
debug: 4.3.4(supports-color@9.2.2)
debug: 4.3.4(supports-color@5.5.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.0
@ -26063,7 +26063,7 @@ packages:
resolution: {integrity: sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==}
engines: {node: '>=12.0.0'}
dependencies:
debug: 4.3.4(supports-color@9.2.2)
debug: 4.3.4(supports-color@5.5.0)
regexp-clone: 1.0.0
sliced: 1.0.1
transitivePeerDependencies:
@ -27913,7 +27913,7 @@ packages:
dependencies:
lilconfig: 2.0.6
postcss: 8.4.21
ts-node: 10.9.1(@types/node@15.14.9)(typescript@4.9.5)
ts-node: 10.9.1(@types/node@18.16.1)(typescript@4.9.5)
yaml: 1.10.2
dev: false
@ -34370,6 +34370,7 @@ packages:
typescript: 4.9.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/ts-node@10.9.1(@types/node@16.11.7)(typescript@4.7.4):
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
@ -34554,7 +34555,6 @@ packages:
typescript: 4.9.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/ts-pnp@1.2.0(typescript@4.7.4):
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}

@ -0,0 +1,4 @@
fork from https://github.com/NathanAdhitya/express-mongoose-ra-json-server
modify:
- count logic in get `/`

@ -0,0 +1,259 @@
import { RequestHandler, Router } from 'express';
import { LeanDocument } from 'mongoose';
import statusMessages from './statusMessages';
import { ADPBaseModel, ADPBaseSchema } from './utils/baseModel.interface';
import castFilter from './utils/castFilter';
import convertId from './utils/convertId';
import filterGetList from './utils/filterGetList';
import { filterReadOnly } from './utils/filterReadOnly';
import parseQuery from './utils/parseQuery';
import virtualId from './utils/virtualId';
// Export certain helper functions for custom reuse.
export { default as virtualId } from './utils/virtualId';
export { default as convertId } from './utils/convertId';
export { default as castFilter } from './utils/castFilter';
export { default as parseQuery } from './utils/parseQuery';
export { default as filterGetList } from './utils/filterGetList';
export { filterReadOnly } from './utils/filterReadOnly';
export { default as statusMessages } from './statusMessages';
export interface raExpressMongooseCapabilities {
list?: boolean;
get?: boolean;
create?: boolean;
update?: boolean;
delete?: boolean;
}
export interface raExpressMongooseOptions<T> {
/** Fields to search from ?q (used for autofill and search) */
q?: string[];
/** Base name for ACLs (e.g. list operation does baseName.list) */
aclName?: string;
/** Fields to allow regex based search (non-exact search) */
allowedRegexFields?: string[];
/** Read-only fields to filter out during create and update */
readOnlyFields?: string[];
/** Function to transform inputs received in create and update */
inputTransformer?: (input: Partial<T>) => Promise<Partial<T>>;
/** Additional queries for list, e.g. deleted/hidden flag. */
listQuery?: Record<string, any>;
/** Max rows from a get operation to prevent accidental server suicide (default 100) */
maxRows?: number;
/** Extra selects for mongoose queries (in the case that certain fields are hidden by default) */
extraSelects?: string;
/** Disable or enable certain parts. */
capabilities?: raExpressMongooseCapabilities;
/** Specify a custom express.js router */
router?: Router;
/** Specify an ACL middleware to check against permissions */
ACLMiddleware?: (name: string) => RequestHandler;
}
export function raExpressMongoose<T extends ADPBaseModel, I>(
model: T,
options?: raExpressMongooseOptions<I>
) {
const {
q,
allowedRegexFields = [],
readOnlyFields,
inputTransformer = (input: any) => input,
listQuery,
extraSelects,
maxRows = 100,
capabilities,
aclName,
router = Router(),
ACLMiddleware,
} = options ?? {};
const {
list: canList = true,
get: canGet = true,
create: canCreate = true,
update: canUpdate = true,
delete: canDelete = true,
} = capabilities ?? {};
/** getList, getMany, getManyReference */
if (canList)
router.get(
'/',
aclName && ACLMiddleware
? ACLMiddleware(`${aclName}.list`)
: (req, res, next) => next(),
async (req, res) => {
const filterQuery = {
...listQuery,
...parseQuery(
castFilter(
convertId(filterGetList(req.query)),
model,
allowedRegexFields
),
model,
allowedRegexFields,
q
),
};
let query = model.find(filterQuery);
if (req.query._sort && req.query._order)
query = query.sort({
[typeof req.query._sort === 'string'
? req.query._sort === 'id'
? '_id'
: req.query._sort
: '_id']: req.query._order === 'ASC' ? 1 : -1,
});
if (req.query._start)
query = query.skip(
parseInt(
typeof req.query._start === 'string' ? req.query._start : '0'
)
);
if (req.query._end)
query = query.limit(
Math.min(
parseInt(
typeof req.query._end === 'string' ? req.query._end : '0'
) -
(req.query._start
? parseInt(
typeof req.query._start === 'string'
? req.query._start
: '0'
)
: 0),
maxRows
)
);
else query = query.limit(maxRows);
if (extraSelects) query = query.select(extraSelects);
if (Object.keys(filterQuery).length === 0) {
res.set(
'X-Total-Count',
(await model.estimatedDocumentCount()).toString()
);
} else {
res.set(
'X-Total-Count',
(await model.countDocuments(filterQuery)).toString()
);
}
return res.json(
virtualId((await query.lean()) as LeanDocument<ADPBaseSchema>)
);
}
);
/** getOne, getMany */
if (canGet)
router.get(
'/:id',
aclName && ACLMiddleware
? ACLMiddleware(`${aclName}.list`)
: (req, res, next) => next(),
async (req, res) => {
await model
.findById(req.params.id)
.select(extraSelects)
.lean()
.then((result) => res.json(virtualId(result)))
.catch((e) => {
return statusMessages.error(res, 400, e);
});
}
);
/** create */
if (canCreate)
router.post(
'/',
aclName && ACLMiddleware
? ACLMiddleware(`${aclName}.create`)
: (req, res, next) => next(),
async (req, res) => {
// eslint-disable-next-line new-cap
const result = convertId(
await inputTransformer(filterReadOnly<I>(req.body, readOnlyFields))
);
const newData = {
...result,
};
const newEntry = new model(newData);
await newEntry
.save()
.then((result) => res.json(virtualId(result)))
.catch((e: any) => {
return statusMessages.error(res, 400, e, 'Bad request');
});
}
);
/** update */
if (canUpdate)
router.put(
'/:id',
aclName && ACLMiddleware
? ACLMiddleware(`${aclName}.edit`)
: (req, res, next) => next(),
async (req, res) => {
const updateData = {
...(await convertId(
await inputTransformer(filterReadOnly<I>(req.body, readOnlyFields))
)),
};
await model
.findOneAndUpdate({ _id: req.params.id }, updateData, {
new: true,
runValidators: true,
})
.lean()
.then((result) => res.json(virtualId(result)))
.catch((e) => {
return statusMessages.error(res, 400, e, 'Bad request');
});
}
);
/**
* delete
*/
if (canDelete)
router.delete(
'/:id',
aclName && ACLMiddleware
? ACLMiddleware(`${aclName}.delete`)
: (req, res, next) => next(),
async (req, res) => {
await model
.findOneAndDelete({ _id: req.params.id })
.then((result) => res.json(virtualId(result)))
.catch((e) => {
return statusMessages.error(res, 404, e, 'Element does not exist');
});
}
);
return router;
}

@ -0,0 +1,24 @@
/**
* @file statusMessages
* @description handles status messages / error responses
*/
import { Response } from 'express';
/**
* Handles rejections other than errors. 400, 401, etc.
*/
function reject(res: Response, status: number, reason?: any) {
return res.status(status).json({ message: reason ?? 'Invalid request' });
}
/**
* Handles errors
*/
function error(res: Response, status: number, e: Error, message?: string) {
if (process.env.NODE_ENV !== 'production') {
return res.status(status).json({ message, error: e.message });
}
}
export default { reject, error };

@ -0,0 +1,7 @@
import { Model, Document } from 'mongoose';
export interface ADPBaseSchema {
_id: string;
}
export type ADPBaseModel = Model<ADPBaseSchema & Document & any>;

@ -0,0 +1,35 @@
import { ADPBaseModel } from './baseModel.interface';
/**
* Turns all the params into their proper types, string into regexes.
* Only works with shallow objects.
* Mutates original object and returns mutated object.
*/
export default function castFilter<T extends ADPBaseModel>(
obj: Record<string, any>,
model: T,
allowedRegexes: string[] = []
) {
const { path } = model.schema;
Object.keys(obj).forEach((key) => {
try {
obj[key] = path(key).cast(obj[key], null, null);
} catch (e) {}
if (allowedRegexes.includes(key) && typeof obj[key] === 'string') {
obj[key] = new RegExp(escapeStringRegexp(obj[key]));
}
});
return obj;
}
function escapeStringRegexp(string) {
if (typeof string !== 'string') {
throw new TypeError('Expected a string');
}
// Escape characters with special meaning either inside or outside character sets.
// Use a simple backslash escape when its always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns stricter grammar.
return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
}

@ -0,0 +1,14 @@
/** Turns id into _id for search queries */
export default function convertId<T extends Record<string, unknown>>(obj: T) {
if (obj.id) {
const newObject = {
_id: obj.id,
...obj,
};
delete newObject.id;
return newObject;
} else {
return obj;
}
}

@ -0,0 +1,17 @@
export const filterGetListParams = [
'_sort',
'_order',
'_start',
'_end',
] as const;
/** Removes _sort, _order, _start, _end from a query. */
export default function filterGetList<T extends Record<string, unknown>>(
obj: T
) {
const filtered: any = {};
Object.entries(obj).forEach(([index, value]) => {
if (!filterGetListParams.includes(index as any)) filtered[index] = value;
});
return filtered as Omit<T, (typeof filterGetListParams)[number]>;
}

@ -0,0 +1,13 @@
/** Makes sure that it does not modify crucial and sacred parts mutates the original object. */
export function filterReadOnly<T extends {}>(
obj: T,
readOnlyFields?: string[]
) {
if (!readOnlyFields) return obj as T;
readOnlyFields.forEach((v) => {
delete obj[v];
});
return obj as Partial<T>;
}

@ -0,0 +1,33 @@
import { ADPBaseModel } from './baseModel.interface';
import castFilter from './castFilter';
interface parseQueryParam {
q?: string;
$or?: any;
}
/**
* Turns ?q into $or queries, deletes q
* @param {Object} results Original object with the q field
* @param {string[]} fields Fields to apply q to
*/
export default function parseQuery<
T extends parseQueryParam,
M extends ADPBaseModel
>(
result: T,
model: M,
allowedRegexes: string[],
fields?: string[]
): T & { $or?: any } {
if (!fields) return result;
if (result.q) {
if (!Array.isArray(result.$or)) result.$or = [];
fields.forEach((field) => {
const newFilter = { [field]: result.q };
result.$or.push(castFilter(newFilter, model, allowedRegexes));
});
delete result.q;
}
return result;
}

@ -0,0 +1,21 @@
export default function virtualId<T extends { _id: string }>(
arr: T[]
): Array<T & { id: string }>;
export default function virtualId<T extends { _id: string }>(
doc: T
): T & { id: string };
/** Virtual ID (_id to id) for react-admin */
export default function virtualId<T extends { _id: string }>(el: Array<T> | T) {
if (Array.isArray(el)) {
return el.map((e) => {
return {
id: e._id,
...e,
_id: undefined,
};
});
}
return { id: el._id, ...el, _id: undefined };
}

@ -1,5 +1,4 @@
import { Router } from 'express';
import raExpressMongoose from 'express-mongoose-ra-json-server';
import jwt from 'jsonwebtoken';
import { callBrokerAction } from '../broker';
import { adminAuth, auth, authSecret } from '../middleware/auth';
@ -8,6 +7,7 @@ import { networkRouter } from './network';
import { fileRouter } from './file';
import dayjs from 'dayjs';
import messageModel from '../../../../models/chat/message';
import { raExpressMongoose } from '../middleware/express-mongoose-ra-json-server';
const router = Router();

Loading…
Cancel
Save