mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Move Notifier to /ext
Summary: This makes it possible to configure a SendGrid-based Notifier instance via a JSON configuration file. Test Plan: Tested manually. Reviewers: alexmojaki Reviewed By: alexmojaki Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D3432
This commit is contained in:
parent
365f3c7ae2
commit
2fd8a34ff8
@ -35,7 +35,7 @@ import {
|
|||||||
UserAttributeRule
|
UserAttributeRule
|
||||||
} from 'app/common/GranularAccessClause';
|
} from 'app/common/GranularAccessClause';
|
||||||
import {isHiddenCol} from 'app/common/gristTypes';
|
import {isHiddenCol} from 'app/common/gristTypes';
|
||||||
import {isObject} from 'app/common/gutil';
|
import {isNonNullish} from 'app/common/gutil';
|
||||||
import {SchemaTypes} from 'app/common/schema';
|
import {SchemaTypes} from 'app/common/schema';
|
||||||
import {MetaRowRecord} from 'app/common/TableData';
|
import {MetaRowRecord} from 'app/common/TableData';
|
||||||
import {
|
import {
|
||||||
@ -1331,7 +1331,7 @@ function syncRecords(tableData: TableData, newRecords: RowRecord[],
|
|||||||
const newRec = newRecordMap.get(uniqueId(r));
|
const newRec = newRecordMap.get(uniqueId(r));
|
||||||
const updated = newRec && {...r, ...newRec, id: r.id};
|
const updated = newRec && {...r, ...newRec, id: r.id};
|
||||||
return updated && !isEqual(updated, r) ? [r, updated] : null;
|
return updated && !isEqual(updated, r) ? [r, updated] : null;
|
||||||
}).filter(isObject);
|
}).filter(isNonNullish);
|
||||||
|
|
||||||
console.log("syncRecords: removing [%s], adding [%s], updating [%s]",
|
console.log("syncRecords: removing [%s], adding [%s], updating [%s]",
|
||||||
removedRecords.map(uniqueId).join(", "),
|
removedRecords.map(uniqueId).join(", "),
|
||||||
|
@ -899,9 +899,9 @@ export function isAffirmative(parameter: any): boolean {
|
|||||||
* Returns whether a value is neither null nor undefined, with a type guard for the return type.
|
* Returns whether a value is neither null nor undefined, with a type guard for the return type.
|
||||||
*
|
*
|
||||||
* This is particularly useful for filtering, e.g. if `array` includes values of type
|
* This is particularly useful for filtering, e.g. if `array` includes values of type
|
||||||
* T|null|undefined, then TypeScript can tell that `array.filter(isObject)` has the type T[].
|
* T|null|undefined, then TypeScript can tell that `array.filter(isNonNullish)` has the type T[].
|
||||||
*/
|
*/
|
||||||
export function isObject<T>(value: T | null | undefined): value is T {
|
export function isNonNullish<T>(value: T | null | undefined): value is T {
|
||||||
return value !== null && value !== undefined;
|
return value !== null && value !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import escapeRegExp = require('lodash/escapeRegExp');
|
import escapeRegExp = require('lodash/escapeRegExp');
|
||||||
import last = require('lodash/last');
|
import last = require('lodash/last');
|
||||||
import memoize = require('lodash/memoize');
|
import memoize = require('lodash/memoize');
|
||||||
import {getDistinctValues, isObject} from 'app/common/gutil';
|
import {getDistinctValues, isNonNullish} from 'app/common/gutil';
|
||||||
// Simply importing 'moment-guess' inconsistently imports bundle.js or bundle.esm.js depending on environment
|
// Simply importing 'moment-guess' inconsistently imports bundle.js or bundle.esm.js depending on environment
|
||||||
import * as guessFormat from '@gristlabs/moment-guess/dist/bundle.js';
|
import * as guessFormat from '@gristlabs/moment-guess/dist/bundle.js';
|
||||||
import * as moment from 'moment-timezone';
|
import * as moment from 'moment-timezone';
|
||||||
@ -346,7 +346,7 @@ export function guessDateFormat(values: Array<string | null>, timezone: string =
|
|||||||
* May return null if there are no matching formats or choosing one is too expensive.
|
* May return null if there are no matching formats or choosing one is too expensive.
|
||||||
*/
|
*/
|
||||||
export function guessDateFormats(values: Array<string | null>, timezone: string = 'UTC'): string[] | null {
|
export function guessDateFormats(values: Array<string | null>, timezone: string = 'UTC'): string[] | null {
|
||||||
const dateStrings: string[] = values.filter(isObject);
|
const dateStrings: string[] = values.filter(isNonNullish);
|
||||||
const sample = getDistinctValues(dateStrings, 100);
|
const sample = getDistinctValues(dateStrings, 100);
|
||||||
const formats: Record<string, number> = {};
|
const formats: Record<string, number> = {};
|
||||||
for (const dateString of sample) {
|
for (const dateString of sample) {
|
||||||
|
@ -6,7 +6,6 @@ import {encodeUrl, getSlugIfNeeded, GristLoadConfig, IGristUrlState, isOrgInPath
|
|||||||
import {getOrgUrlInfo} from 'app/common/gristUrls';
|
import {getOrgUrlInfo} from 'app/common/gristUrls';
|
||||||
import {UserProfile} from 'app/common/LoginSessionAPI';
|
import {UserProfile} from 'app/common/LoginSessionAPI';
|
||||||
import {tbind} from 'app/common/tbind';
|
import {tbind} from 'app/common/tbind';
|
||||||
import {UserConfig} from 'app/common/UserConfig';
|
|
||||||
import * as version from 'app/common/version';
|
import * as version from 'app/common/version';
|
||||||
import {ApiServer} from 'app/gen-server/ApiServer';
|
import {ApiServer} from 'app/gen-server/ApiServer';
|
||||||
import {Document} from "app/gen-server/entity/Document";
|
import {Document} from "app/gen-server/entity/Document";
|
||||||
@ -103,7 +102,7 @@ export class FlexServer implements GristServer {
|
|||||||
public housekeeper: Housekeeper;
|
public housekeeper: Housekeeper;
|
||||||
public server: http.Server;
|
public server: http.Server;
|
||||||
public httpsServer?: https.Server;
|
public httpsServer?: https.Server;
|
||||||
public settings: any;
|
public settings?: Readonly<Record<string, unknown>>;
|
||||||
public worker: DocWorkerInfo;
|
public worker: DocWorkerInfo;
|
||||||
public electronServerMethods: ElectronServerMethods;
|
public electronServerMethods: ElectronServerMethods;
|
||||||
public readonly docsRoot: string;
|
public readonly docsRoot: string;
|
||||||
@ -802,12 +801,17 @@ export class FlexServer implements GristServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load user config file from standard location. Alternatively, a config object
|
/**
|
||||||
// can be supplied, in which case no file is needed. The notion of a user config
|
* Load user config file from standard location (if present).
|
||||||
// file doesn't mean much in hosted grist, so it is convenient to be able to skip it.
|
*
|
||||||
public async loadConfig(settings?: UserConfig) {
|
* Note that the user config file doesn't do anything today, but may be useful in
|
||||||
|
* the future for configuring things that don't fit well into environment variables.
|
||||||
|
*
|
||||||
|
* TODO: Revisit this, and update `GristServer.settings` type to match the expected shape
|
||||||
|
* of config.json. (ts-interface-checker could be useful here for runtime validation.)
|
||||||
|
*/
|
||||||
|
public async loadConfig() {
|
||||||
if (this._check('config')) { return; }
|
if (this._check('config')) { return; }
|
||||||
if (!settings) {
|
|
||||||
const settingsPath = path.join(this.instanceRoot, 'config.json');
|
const settingsPath = path.join(this.instanceRoot, 'config.json');
|
||||||
if (await fse.pathExists(settingsPath)) {
|
if (await fse.pathExists(settingsPath)) {
|
||||||
log.info(`Loading config from ${settingsPath}`);
|
log.info(`Loading config from ${settingsPath}`);
|
||||||
@ -816,9 +820,6 @@ export class FlexServer implements GristServer {
|
|||||||
log.info(`Loading empty config because ${settingsPath} missing`);
|
log.info(`Loading empty config because ${settingsPath} missing`);
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.settings = settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: We could include a third mock provider of login/logout URLs for better tests. Or we
|
// TODO: We could include a third mock provider of login/logout URLs for better tests. Or we
|
||||||
// could create a mock SAML identity provider for testing this using the SAML flow.
|
// could create a mock SAML identity provider for testing this using the SAML flow.
|
||||||
|
@ -16,7 +16,7 @@ import { ErrorWithCode } from 'app/common/ErrorWithCode';
|
|||||||
import { AclMatchInput, InfoEditor, InfoView } from 'app/common/GranularAccessClause';
|
import { AclMatchInput, InfoEditor, InfoView } from 'app/common/GranularAccessClause';
|
||||||
import { UserInfo } from 'app/common/GranularAccessClause';
|
import { UserInfo } from 'app/common/GranularAccessClause';
|
||||||
import { isCensored } from 'app/common/gristTypes';
|
import { isCensored } from 'app/common/gristTypes';
|
||||||
import { getSetMapValue, isObject, pruneArray } from 'app/common/gutil';
|
import { getSetMapValue, isNonNullish, pruneArray } from 'app/common/gutil';
|
||||||
import { canEdit, canView, isValidRole, Role } from 'app/common/roles';
|
import { canEdit, canView, isValidRole, Role } from 'app/common/roles';
|
||||||
import { FullUser, UserAccessData } from 'app/common/UserAPI';
|
import { FullUser, UserAccessData } from 'app/common/UserAPI';
|
||||||
import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
|
import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
|
||||||
@ -1039,7 +1039,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
|||||||
this._makeAdditions(rowsAfter, forceAdds),
|
this._makeAdditions(rowsAfter, forceAdds),
|
||||||
this._removeRows(action, removals),
|
this._removeRows(action, removals),
|
||||||
this._makeRemovals(rowsAfter, forceRemoves),
|
this._makeRemovals(rowsAfter, forceRemoves),
|
||||||
].filter(isObject);
|
].filter(isNonNullish);
|
||||||
|
|
||||||
// Check whether there are column rules for this table, and if so whether they are row
|
// Check whether there are column rules for this table, and if so whether they are row
|
||||||
// dependent. If so, we may need to update visibility of cells not mentioned in the
|
// dependent. If so, we may need to update visibility of cells not mentioned in the
|
||||||
|
@ -22,6 +22,7 @@ import * as express from 'express';
|
|||||||
*/
|
*/
|
||||||
export interface GristServer {
|
export interface GristServer {
|
||||||
readonly create: ICreate;
|
readonly create: ICreate;
|
||||||
|
settings?: Readonly<Record<string, unknown>>;
|
||||||
getHost(): string;
|
getHost(): string;
|
||||||
getHomeUrl(req: express.Request, relPath?: string): string;
|
getHomeUrl(req: express.Request, relPath?: string): string;
|
||||||
getHomeUrlByDocId(docId: string, relPath?: string): Promise<string>;
|
getHomeUrlByDocId(docId: string, relPath?: string): Promise<string>;
|
||||||
|
@ -46,10 +46,15 @@ export interface ICreateStorageOptions {
|
|||||||
create(purpose: 'doc'|'meta', extraPrefix: string): ExternalStorage|undefined;
|
create(purpose: 'doc'|'meta', extraPrefix: string): ExternalStorage|undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICreateNotifierOptions {
|
||||||
|
create(dbManager: HomeDBManager, gristConfig: GristServer): INotifier|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function makeSimpleCreator(opts: {
|
export function makeSimpleCreator(opts: {
|
||||||
sessionSecret?: string,
|
sessionSecret?: string,
|
||||||
storage?: ICreateStorageOptions[],
|
storage?: ICreateStorageOptions[],
|
||||||
activationMiddleware?: (db: HomeDBManager, app: express.Express) => Promise<void>,
|
activationMiddleware?: (db: HomeDBManager, app: express.Express) => Promise<void>,
|
||||||
|
notifier?: ICreateNotifierOptions,
|
||||||
}): ICreate {
|
}): ICreate {
|
||||||
return {
|
return {
|
||||||
Billing(db) {
|
Billing(db) {
|
||||||
@ -63,8 +68,9 @@ export function makeSimpleCreator(opts: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
Notifier() {
|
Notifier(dbManager, gristConfig) {
|
||||||
return {
|
const {notifier} = opts;
|
||||||
|
return notifier?.create(dbManager, gristConfig) ?? {
|
||||||
get testPending() { return false; },
|
get testPending() { return false; },
|
||||||
deleteUser() { throw new Error('deleteUser unavailable'); },
|
deleteUser() { throw new Error('deleteUser unavailable'); },
|
||||||
};
|
};
|
||||||
|
@ -40,3 +40,10 @@ export function makeForkIds(options: { userId: number|null, isAnonymous: boolean
|
|||||||
export function getAssignmentId(docWorkerMap: IDocWorkerMap, docId: string): string {
|
export function getAssignmentId(docWorkerMap: IDocWorkerMap, docId: string): string {
|
||||||
return docId;
|
return docId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the externalId to use for an AppSumo user. AppSumo identifies users by
|
||||||
|
// an activation email, so we just use that (with an appsumo/ prefix it allow
|
||||||
|
// for other families of id in the future).
|
||||||
|
export function getExternalIdForAppSumoUser(email: string) {
|
||||||
|
return `appsumo/${email}`;
|
||||||
|
}
|
||||||
|
@ -154,6 +154,7 @@ export async function createDocManager(
|
|||||||
export function createDummyGristServer(): GristServer {
|
export function createDummyGristServer(): GristServer {
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
|
settings: {},
|
||||||
getHost() { return 'localhost:4242'; },
|
getHost() { return 'localhost:4242'; },
|
||||||
getHomeUrl() { return 'http://localhost:4242'; },
|
getHomeUrl() { return 'http://localhost:4242'; },
|
||||||
getHomeUrlByDocId() { return Promise.resolve('http://localhost:4242'); },
|
getHomeUrlByDocId() { return Promise.resolve('http://localhost:4242'); },
|
||||||
|
@ -22,7 +22,7 @@ async function activateServer(home: FlexServer, docManager: DocManager) {
|
|||||||
home.addDocWorkerMap();
|
home.addDocWorkerMap();
|
||||||
home.addAccessMiddleware();
|
home.addAccessMiddleware();
|
||||||
dbManager = home.getHomeDBManager();
|
dbManager = home.getHomeDBManager();
|
||||||
await home.loadConfig({});
|
await home.loadConfig();
|
||||||
home.addSessions();
|
home.addSessions();
|
||||||
home.addHealthCheck();
|
home.addHealthCheck();
|
||||||
docManager.testSetHomeDbManager(dbManager);
|
docManager.testSetHomeDbManager(dbManager);
|
||||||
|
Loading…
Reference in New Issue
Block a user