Summary: Previously, absence of `GRIST_DOCS_S3_BUCKET` was equated with absence of external storage, but that is no longer true now that Azure is available. Azure could be used by setting `GRIST_DOCS_S3_BUCKET` but the alternative `GRIST_AZURE_CONTAINER` flag is friendlier. Test Plan: confirmed manually that Azure can be configured and used now without `GRIST_DOCS_S3_BUCKET` Reviewers: alexmojaki Reviewed By: alexmojaki Subscribers: alexmojaki Differential Revision: https://phab.getgrist.com/D3448pull/214/head
parent
acddd25cfd
commit
1c6f80f956
@ -0,0 +1,204 @@
|
||||
import { isAffirmative } from 'app/common/gutil';
|
||||
|
||||
/**
|
||||
* A bundle of settings for the application. May contain
|
||||
* a value directly, and/or via nested settings. Also
|
||||
* may have some information about where we looked for
|
||||
* the value, for reporting as a diagnostic.
|
||||
*/
|
||||
export class AppSettings {
|
||||
private _value?: JSONValue;
|
||||
private _children?: {[key: string]: AppSettings};
|
||||
private _info?: AppSettingQueryResult;
|
||||
|
||||
public constructor(public readonly name: string) {}
|
||||
|
||||
/* access the setting - undefined if not set */
|
||||
public get(): JSONValue|undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/* access the setting as a boolean using isAffirmative - undefined if not set */
|
||||
public getAsBool(): boolean|undefined {
|
||||
return (this._value !== undefined) ? isAffirmative(this._value) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to read the setting from the environment. Even if
|
||||
* we fail, we record information about how we tried to
|
||||
* find the setting, so we can report on that.
|
||||
*/
|
||||
public read(query: AppSettingQuery) {
|
||||
this._value = undefined;
|
||||
this._info = undefined;
|
||||
let value = undefined;
|
||||
let found = false;
|
||||
const envVars = getEnvVarsFromQuery(query);
|
||||
if (!envVars.length) {
|
||||
throw new Error('could not find an environment variable to read');
|
||||
}
|
||||
let envVar = envVars[0];
|
||||
for (const synonym of envVars) {
|
||||
value = process.env[synonym];
|
||||
if (value !== undefined) {
|
||||
envVar = synonym;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._info = {
|
||||
envVar: found ? envVar : undefined,
|
||||
found,
|
||||
query,
|
||||
};
|
||||
if (value !== undefined) {
|
||||
this._value = value;
|
||||
} else if (query.defaultValue !== undefined) {
|
||||
this._value = query.defaultValue;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* As for read() but type the result as a string.
|
||||
*/
|
||||
public readString(query: AppSettingQuery): string|undefined {
|
||||
this.read(query);
|
||||
if (this._value === undefined) { return undefined; }
|
||||
this._value = String(this._value);
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* As for readString() but fail if nothing was found.
|
||||
*/
|
||||
public requireString(query: AppSettingQuery): string {
|
||||
const result = this.readString(query);
|
||||
if (result === undefined) {
|
||||
throw new Error(`missing environment variable: ${query.envVar}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* set this setting 'manually' */
|
||||
public set(value: JSONValue): void {
|
||||
this._value = value;
|
||||
this._info = undefined;
|
||||
}
|
||||
|
||||
/* access any nested settings */
|
||||
public get nested(): {[key: string]: AppSettings} {
|
||||
return this._children || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a named nested setting, returning an AppSettings
|
||||
* object that can be used to access it. This method is
|
||||
* named "section" to suggest that the nested setting
|
||||
* will itself contain multiple settings, but doesn't
|
||||
* require that.
|
||||
*/
|
||||
public section(fname: string): AppSettings {
|
||||
if (!this._children) { this._children = {}; }
|
||||
let child = this._children[fname];
|
||||
if (!child) {
|
||||
this._children[fname] = child = new AppSettings(fname);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a named nested setting, returning an AppSettings
|
||||
* object that can be used to access it. This method is
|
||||
* named "flag" to suggest that tthe nested setting will
|
||||
* not iself be nested, but doesn't require that - it is
|
||||
* currently just an alias for the section() method.
|
||||
*/
|
||||
public flag(fname: string): AppSettings {
|
||||
return this.section(fname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a summary description of the setting and how it was
|
||||
* derived.
|
||||
*/
|
||||
public describe(): AppSettingDescription {
|
||||
return {
|
||||
name: this.name,
|
||||
value: (this._info?.query.censor && this._value !== undefined) ? '*****' : this._value,
|
||||
foundInEnvVar: this._info?.envVar,
|
||||
wouldFindInEnvVar: this._info?.query.preferredEnvVar || getEnvVarsFromQuery(this._info?.query)[0],
|
||||
usedDefault: this._value !== undefined && this._info !== undefined && !this._info?.found,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* As for describe(), but include all nested settings also.
|
||||
* Used dotted notation for setting names. Omit settings that
|
||||
* are undefined and without useful information about how they
|
||||
* might be defined.
|
||||
*/
|
||||
public describeAll(): AppSettingDescription[] {
|
||||
const inv: AppSettingDescription[] = [];
|
||||
inv.push(this.describe());
|
||||
if (this._children) {
|
||||
for (const child of Object.values(this._children)) {
|
||||
for (const item of child.describeAll()) {
|
||||
inv.push({...item, name: this.name + '.' + item.name});
|
||||
}
|
||||
}
|
||||
}
|
||||
return inv.filter(item => item.value !== undefined ||
|
||||
item.wouldFindInEnvVar !== undefined ||
|
||||
item.usedDefault);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A global object for Grist application settings.
|
||||
*/
|
||||
export const appSettings = new AppSettings('grist');
|
||||
|
||||
/**
|
||||
* Hints for how to define a setting, including possible
|
||||
* environment variables and default values.
|
||||
*/
|
||||
export interface AppSettingQuery {
|
||||
envVar: string|string[]; // environment variable(s) to check.
|
||||
preferredEnvVar?: string; // "Canonical" environment variable to suggest.
|
||||
// Should be in envVar (though this is not checked).
|
||||
defaultValue?: JSONValue; // value to use if variable(s) unavailable.
|
||||
censor?: boolean; // should the value of the setting be obscured when printed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a query specifying whether the setting
|
||||
* was found, and if so in what environment variable, and using
|
||||
* what query.
|
||||
*/
|
||||
export interface AppSettingQueryResult {
|
||||
envVar?: string;
|
||||
found: boolean;
|
||||
query: AppSettingQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output of AppSettings.describe().
|
||||
*/
|
||||
interface AppSettingDescription {
|
||||
name: string; // name of the setting.
|
||||
value?: JSONValue; // value of the setting, if available.
|
||||
foundInEnvVar?: string; // environment variable the setting was read from, if available.
|
||||
wouldFindInEnvVar?: string; // environment variable that would be checked for the setting.
|
||||
usedDefault: boolean; // whether a default value was used for the setting.
|
||||
}
|
||||
|
||||
// Helper function to normalize the AppSettingQuery.envVar list.
|
||||
function getEnvVarsFromQuery(q?: AppSettingQuery): string[] {
|
||||
if (!q) { return []; }
|
||||
return Array.isArray(q.envVar) ? q.envVar : [q.envVar];
|
||||
}
|
||||
|
||||
// Keep app settings JSON-like, in case later we decide to load them from
|
||||
// a JSON source.
|
||||
type JSONValue = string | number | boolean | null | { [member: string]: JSONValue } | JSONValue[];
|
Loading…
Reference in new issue