diff --git a/app/client/aclui/AccessRules.ts b/app/client/aclui/AccessRules.ts index e3949f65..8bed5276 100644 --- a/app/client/aclui/AccessRules.ts +++ b/app/client/aclui/AccessRules.ts @@ -35,7 +35,7 @@ import { UserAttributeRule } from 'app/common/GranularAccessClause'; 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 {MetaRowRecord} from 'app/common/TableData'; import { @@ -1331,7 +1331,7 @@ function syncRecords(tableData: TableData, newRecords: RowRecord[], const newRec = newRecordMap.get(uniqueId(r)); const updated = newRec && {...r, ...newRec, id: r.id}; return updated && !isEqual(updated, r) ? [r, updated] : null; - }).filter(isObject); + }).filter(isNonNullish); console.log("syncRecords: removing [%s], adding [%s], updating [%s]", removedRecords.map(uniqueId).join(", "), diff --git a/app/common/gutil.ts b/app/common/gutil.ts index 1887eb50..5ca6af0d 100644 --- a/app/common/gutil.ts +++ b/app/common/gutil.ts @@ -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. * * 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(value: T | null | undefined): value is T { +export function isNonNullish(value: T | null | undefined): value is T { return value !== null && value !== undefined; } diff --git a/app/common/parseDate.ts b/app/common/parseDate.ts index 0d7d98db..a98b29d3 100644 --- a/app/common/parseDate.ts +++ b/app/common/parseDate.ts @@ -1,7 +1,7 @@ import escapeRegExp = require('lodash/escapeRegExp'); import last = require('lodash/last'); 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 import * as guessFormat from '@gristlabs/moment-guess/dist/bundle.js'; import * as moment from 'moment-timezone'; @@ -346,7 +346,7 @@ export function guessDateFormat(values: Array, timezone: string = * May return null if there are no matching formats or choosing one is too expensive. */ export function guessDateFormats(values: Array, timezone: string = 'UTC'): string[] | null { - const dateStrings: string[] = values.filter(isObject); + const dateStrings: string[] = values.filter(isNonNullish); const sample = getDistinctValues(dateStrings, 100); const formats: Record = {}; for (const dateString of sample) { diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 1d8370fb..9d9c1cfb 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -6,7 +6,6 @@ import {encodeUrl, getSlugIfNeeded, GristLoadConfig, IGristUrlState, isOrgInPath import {getOrgUrlInfo} from 'app/common/gristUrls'; import {UserProfile} from 'app/common/LoginSessionAPI'; import {tbind} from 'app/common/tbind'; -import {UserConfig} from 'app/common/UserConfig'; import * as version from 'app/common/version'; import {ApiServer} from 'app/gen-server/ApiServer'; import {Document} from "app/gen-server/entity/Document"; @@ -103,7 +102,7 @@ export class FlexServer implements GristServer { public housekeeper: Housekeeper; public server: http.Server; public httpsServer?: https.Server; - public settings: any; + public settings?: Readonly>; public worker: DocWorkerInfo; public electronServerMethods: ElectronServerMethods; public readonly docsRoot: string; @@ -802,22 +801,24 @@ 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 - // file doesn't mean much in hosted grist, so it is convenient to be able to skip it. - public async loadConfig(settings?: UserConfig) { + /** + * Load user config file from standard location (if present). + * + * 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 (!settings) { - const settingsPath = path.join(this.instanceRoot, 'config.json'); - if (await fse.pathExists(settingsPath)) { - log.info(`Loading config from ${settingsPath}`); - this.settings = JSON.parse(await fse.readFile(settingsPath, 'utf8')); - } else { - log.info(`Loading empty config because ${settingsPath} missing`); - this.settings = {}; - } + const settingsPath = path.join(this.instanceRoot, 'config.json'); + if (await fse.pathExists(settingsPath)) { + log.info(`Loading config from ${settingsPath}`); + this.settings = JSON.parse(await fse.readFile(settingsPath, 'utf8')); } else { - this.settings = settings; + log.info(`Loading empty config because ${settingsPath} missing`); + this.settings = {}; } // TODO: We could include a third mock provider of login/logout URLs for better tests. Or we diff --git a/app/server/lib/GranularAccess.ts b/app/server/lib/GranularAccess.ts index 3c8709b0..29624e60 100644 --- a/app/server/lib/GranularAccess.ts +++ b/app/server/lib/GranularAccess.ts @@ -16,7 +16,7 @@ import { ErrorWithCode } from 'app/common/ErrorWithCode'; import { AclMatchInput, InfoEditor, InfoView } from 'app/common/GranularAccessClause'; import { UserInfo } from 'app/common/GranularAccessClause'; 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 { FullUser, UserAccessData } from 'app/common/UserAPI'; import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager'; @@ -1039,7 +1039,7 @@ export class GranularAccess implements GranularAccessForBundle { this._makeAdditions(rowsAfter, forceAdds), this._removeRows(action, removals), this._makeRemovals(rowsAfter, forceRemoves), - ].filter(isObject); + ].filter(isNonNullish); // 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 diff --git a/app/server/lib/GristServer.ts b/app/server/lib/GristServer.ts index 92f66cbe..04e0e113 100644 --- a/app/server/lib/GristServer.ts +++ b/app/server/lib/GristServer.ts @@ -22,6 +22,7 @@ import * as express from 'express'; */ export interface GristServer { readonly create: ICreate; + settings?: Readonly>; getHost(): string; getHomeUrl(req: express.Request, relPath?: string): string; getHomeUrlByDocId(docId: string, relPath?: string): Promise; diff --git a/app/server/lib/ICreate.ts b/app/server/lib/ICreate.ts index ed4a6650..7b36ef49 100644 --- a/app/server/lib/ICreate.ts +++ b/app/server/lib/ICreate.ts @@ -46,10 +46,15 @@ export interface ICreateStorageOptions { create(purpose: 'doc'|'meta', extraPrefix: string): ExternalStorage|undefined; } +export interface ICreateNotifierOptions { + create(dbManager: HomeDBManager, gristConfig: GristServer): INotifier|undefined; +} + export function makeSimpleCreator(opts: { sessionSecret?: string, storage?: ICreateStorageOptions[], activationMiddleware?: (db: HomeDBManager, app: express.Express) => Promise, + notifier?: ICreateNotifierOptions, }): ICreate { return { Billing(db) { @@ -63,8 +68,9 @@ export function makeSimpleCreator(opts: { } }; }, - Notifier() { - return { + Notifier(dbManager, gristConfig) { + const {notifier} = opts; + return notifier?.create(dbManager, gristConfig) ?? { get testPending() { return false; }, deleteUser() { throw new Error('deleteUser unavailable'); }, }; diff --git a/app/server/lib/idUtils.ts b/app/server/lib/idUtils.ts index 23227f37..9c57430e 100644 --- a/app/server/lib/idUtils.ts +++ b/app/server/lib/idUtils.ts @@ -40,3 +40,10 @@ export function makeForkIds(options: { userId: number|null, isAnonymous: boolean export function getAssignmentId(docWorkerMap: IDocWorkerMap, docId: string): string { 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}`; +} diff --git a/test/server/docTools.ts b/test/server/docTools.ts index 32d7be52..aa272222 100644 --- a/test/server/docTools.ts +++ b/test/server/docTools.ts @@ -154,6 +154,7 @@ export async function createDocManager( export function createDummyGristServer(): GristServer { return { create, + settings: {}, getHost() { return 'localhost:4242'; }, getHomeUrl() { return 'http://localhost:4242'; }, getHomeUrlByDocId() { return Promise.resolve('http://localhost:4242'); }, diff --git a/test/server/lib/Authorizer.ts b/test/server/lib/Authorizer.ts index 6c4af796..8b90a1bf 100644 --- a/test/server/lib/Authorizer.ts +++ b/test/server/lib/Authorizer.ts @@ -22,7 +22,7 @@ async function activateServer(home: FlexServer, docManager: DocManager) { home.addDocWorkerMap(); home.addAccessMiddleware(); dbManager = home.getHomeDBManager(); - await home.loadConfig({}); + await home.loadConfig(); home.addSessions(); home.addHealthCheck(); docManager.testSetHomeDbManager(dbManager);