mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add API endpoint to get site usage summary
Summary: The summary includes a count of documents that are approaching limits, in grace period, or delete-only. The endpoint is only accessible to site owners, and is currently unused. A follow-up diff will add usage banners to the site home page, which will use the response from the endpoint to communicate usage information to owners. Test Plan: Browser and server tests. Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3420
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import {mapGetOrSet, mapSetOrClear, MapWithTTL} from 'app/common/AsyncCreate';
|
||||
import {getDataLimitStatus} from 'app/common/DocLimits';
|
||||
import {createEmptyOrgUsageSummary, DocumentUsage, OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {normalizeEmail} from 'app/common/emails';
|
||||
import {canAddOrgMembers, Features} from 'app/common/Features';
|
||||
import {buildUrlId, MIN_URLID_PREFIX_LENGTH, parseUrlId} from 'app/common/gristUrls';
|
||||
@@ -231,6 +233,12 @@ function stringifyUrlIdOrg(urlId: string, org?: string): string {
|
||||
return `${urlId}:${org}`;
|
||||
}
|
||||
|
||||
export interface DocumentMetadata {
|
||||
// ISO 8601 UTC date (e.g. the output of new Date().toISOString()).
|
||||
updatedAt?: string;
|
||||
usage?: DocumentUsage|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HomeDBManager handles interaction between the ApiServer and the Home database,
|
||||
* encapsulating the typeorm logic.
|
||||
@@ -922,6 +930,44 @@ export class HomeDBManager extends EventEmitter {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an organization's usage summary (e.g. count of documents that are approaching or exceeding
|
||||
* limits).
|
||||
*/
|
||||
public async getOrgUsageSummary(scope: Scope, orgKey: string|number): Promise<OrgUsageSummary> {
|
||||
// Check that an owner of the org is making the request.
|
||||
const markPermissions = Permissions.OWNER;
|
||||
let orgQuery = this.org(scope, orgKey, {
|
||||
markPermissions,
|
||||
needRealOrg: true
|
||||
});
|
||||
orgQuery = this._addFeatures(orgQuery);
|
||||
const orgQueryResult = await verifyIsPermitted(orgQuery);
|
||||
const org: Organization = this.unwrapQueryResult(orgQueryResult);
|
||||
const productFeatures = org.billingAccount.product.features;
|
||||
|
||||
// Grab all the non-removed documents in the org.
|
||||
let docsQuery = this._docs()
|
||||
.innerJoin('docs.workspace', 'workspaces')
|
||||
.innerJoin('workspaces.org', 'orgs')
|
||||
.where('docs.workspace_id = workspaces.id')
|
||||
.andWhere('workspaces.removed_at IS NULL AND docs.removed_at IS NULL');
|
||||
docsQuery = this._whereOrg(docsQuery, orgKey);
|
||||
if (this.isMergedOrg(orgKey)) {
|
||||
docsQuery = docsQuery.andWhere('orgs.owner_id = :userId', {userId: scope.userId});
|
||||
}
|
||||
const docsQueryResult = await this._verifyAclPermissions(docsQuery, { scope, emptyAllowed: true });
|
||||
const docs: Document[] = this.unwrapQueryResult(docsQueryResult);
|
||||
|
||||
// Return an aggregate count of documents, grouped by data limit status.
|
||||
const summary = createEmptyOrgUsageSummary();
|
||||
for (const {usage: docUsage, gracePeriodStart} of docs) {
|
||||
const dataLimitStatus = getDataLimitStatus({docUsage, gracePeriodStart, productFeatures});
|
||||
if (dataLimitStatus) { summary[dataLimitStatus] += 1; }
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the best access option for an organization, from the
|
||||
* users available to the client. If none of the options can access
|
||||
@@ -2364,12 +2410,13 @@ export class HomeDBManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the updatedAt values for several docs. Takes a map where each entry maps a docId to
|
||||
* an ISO date string representing the new updatedAt time. This is not a part of the API, it
|
||||
* should be called only by the HostedMetadataManager when a change is made to a doc.
|
||||
* Updates the updatedAt and usage values for several docs. Takes a map where each entry maps
|
||||
* a docId to a metadata object containing the updatedAt and/or usage values. This is not a part
|
||||
* of the API, it should be called only by the HostedMetadataManager when a change is made to a
|
||||
* doc.
|
||||
*/
|
||||
public async setDocsUpdatedAt(
|
||||
docUpdateMap: {[docId: string]: string}
|
||||
public async setDocsMetadata(
|
||||
docUpdateMap: {[docId: string]: DocumentMetadata}
|
||||
): Promise<QueryResult<void>> {
|
||||
if (!docUpdateMap || Object.keys(docUpdateMap).length === 0) {
|
||||
return {
|
||||
@@ -2382,7 +2429,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
const updateTasks = docIds.map(docId => {
|
||||
return manager.createQueryBuilder()
|
||||
.update(Document)
|
||||
.set({updatedAt: docUpdateMap[docId]})
|
||||
.set(docUpdateMap[docId])
|
||||
.where("id = :docId", {docId})
|
||||
.execute();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user