UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

384 lines (370 loc) 12.6 kB
import type { ModelsConfig } from '../models'; import { DEFAULT_DATABASE_NAME, DEFAULT_ENV_PREFIX } from '../constants'; import { Access, AccessLevel } from '../typings'; const models: ModelsConfig = { models: [], }; /** * Datastore environment variable prefix name */ const DATASTORE_ENV_PREFIX: string = process.env.DATASTORE_ENV_PREFIX || DEFAULT_ENV_PREFIX; /** * Database name */ const DB_NAME: string = process.env.DATASTORE_DB_NAME || DEFAULT_DATABASE_NAME; function getEnv(name: string, defaultValue?: any): string { return ( process.env[`${DATASTORE_ENV_PREFIX}_${name}`] || process.env[name] || defaultValue ); } function mapTokens(name: string, level: AccessLevel): Access[] { return getEnv(name, '') .split(',') .map((t) => t.trim()) .filter((t) => !!t) .map((token) => ({ id: token.slice(0, 10), level, token, })); } const configPath = getEnv('ENV_FILE', ''); function getAMQPUrl() { const url = getEnv('AMQP_URL', 'amqp://guest:guest@localhost:5672'); /* istanbul ignore next */ if (url[0] === '[') { return JSON.parse(url); } return url; } /* istanbul ignore next */ export const envConfig = { exitOnUnavailabilityAfterMilliseconds: Number.parseInt( getEnv('EXIT_ON_UNVAILABILITY_AFTER_MILLISECONDS', '10000'), ), port: getEnv('PORT', 3001), mode: getEnv('NODE_ENV', 'production'), exitTimeout: Number.parseInt(getEnv('EXIT_TIMEOUT', '1000'), 10), security: { tokens: [ ...JSON.parse(getEnv('TOKENS', '[]')), ...mapTokens('READ_ACCESS_TOKENS', 'read'), ...mapTokens('DECRYPT_ACCESS_TOKENS', 'decrypt'), ...mapTokens('WRITE_ACCESS_TOKENS', 'write'), ...mapTokens('ADMIN_ACCESS_TOKENS', 'admin'), ], apiSecret: getEnv('API_DOC_SECRET', 'api-docs'), encryptionKeys: JSON.parse(getEnv('SECURITY_ENCRYPTION_KEYS', '{}')), activeNumberEncryptionKeys: Number.parseInt( getEnv('SECURITY_ACTIVE_NUMBER_ENCRYPTION_KEYS', '1'), 10, ), accessTokenByCookie: getEnv('SECURITY_ACCESS_TOKEN_BY_COOKIE') === 'true', }, features: { properties: { is_readonly: getEnv('FEATURE_PROPERTY_IS_READONLY', 'is_readonly'), is_archived: getEnv('FEATURE_PROPERTY_IS_ARCHIVED', 'is_archived'), is_deleted: getEnv('FEATURE_PROPERTY_IS_DELETED', 'is_deleted'), }, cookies: { options: { httpOnly: getEnv('FEATURE_COOKIES_HTTP_ONLY') !== 'false', // true by default domain: getEnv('FEATURE_COOKIES_DOMAIN'), secure: getEnv('FEATURE_COOKIES_SECURE') !== 'false', // true by default sameSite: getEnv('FEATURE_COOKIES_SAME_SITE', 'none') as | boolean | 'none' | 'lax' | 'strict' | undefined, maxAge: Number.parseInt( getEnv('FEATURE_COOKIES_MAX_AGE', '172800'), 10, ), }, maxAges: JSON.parse(getEnv('FEATURE_COOKIES_MAX_AGES', '{}')), }, cors: { isEnabled: getEnv('FEATURE_CORS_ENABLED') !== 'false', allowCredentials: getEnv('FEATURE_CORS_ALLOW_CREDENTIALS'), allowHeaders: getEnv('FEATURE_CORS_ALLOW_HEADERS', ''), allowMethods: getEnv('FEATURE_CORS_ALLOW_METHODS'), allowOrigin: getEnv('FEATURE_CORS_ALLOW_ORIGIN'), exposeHeaders: getEnv('FEATURE_CORS_EXPOSE_HEADERS'), requestHeaders: getEnv('FEATURE_CORS_REQUEST_HEADERS'), requestMethod: getEnv('FEATURE_CORS_REQUEST_METHOD'), }, cache: { isEnabled: getEnv('FEATURE_CACHE_ENABLED') === 'true', scope: getEnv('FEATURE_CACHE_SCOPE', 'ds'), }, api: { timeout: { models: Number.parseInt( getEnv('FEATURE_API_TIMEOUT_MODELS_IN_MILLISECONDS', '5000'), 10, ), aggregate: Number.parseInt( getEnv('FEATURE_API_TIMEOUT_AGGREGATE_IN_MILLISECONDS', '30000'), 10, ), }, /** * Are examples models templates exposed in API or not? */ templates: getEnv('FEATURE_API_TEMPLATES') === 'true', /** * Are administration routes enabled or not? */ admin: getEnv('FEATURE_API_ADMIN') === 'true', aggregate: getEnv('FEATURE_API_AGGREGATE') === 'true', openAPI: { isEnabled: getEnv('FEATURE_API_OPEN_API_ENABLED') !== 'false', warnOnInvalidSpecificationOnly: getEnv('FEATURE_API_OPENAPI_WARN_ON_INVALID_SPECIFICATION_ONLY') === 'true', }, json: { limit: getEnv('FEATURE_API_JSON_LIMIT', '50mb'), }, updateSpecOnModelsChange: getEnv('FEATURE_API_UPDATE_SPEC_ON_MODELS_CHANGE') === 'true', sseKeepAliveTimeout: Number.parseInt( getEnv( 'FEATURE_API_SERVER_SENT_EVENTS_KEEP_ALIVE_TIMEOUT_IN_MILLISECONDS', '60000', ), 10, ), checkProcessingAuthorization: getEnv('FEATURE_API_CHECK_PROCESSING_AUTHORIZATION') === 'true', stream: { reconnectDelayOnError: Number.parseInt( getEnv('FEATURE_API_STREAM_RECONNECT_DELAY_ON_ERROR', '1'), 10, ), maxWaitOnReconnectInMilliseconds: Number.parseInt( getEnv( 'FEATURE_API_STREAM_MAX_WAIT_ON_RECONNECT_IN_MILLISECONDS', '30000', ), 10, ), }, }, events: { throwOnInvalidEvent: getEnv('FEATURE_EVENTS_THROW_ON_INVALID_EVENT', 'true') !== 'false', }, fhe: { isEnabled: getEnv('FEATURE_FHE_IS_ENABLED', 'false') === 'true', }, mqtt: { isEnabled: getEnv('FEATURE_MQTT_IS_ENABLED', 'false') === 'true', }, amqp: { isEnabled: getEnv('FEATURE_AMQP_IS_ENABLED', 'false') === 'true', }, mongodb: { maxTimeMS: Number.parseInt( getEnv('FEATURE_MONGODB_MAX_TIME_MS', '5000'), 10, ), explain: getEnv('FEATURE_MONGODB_EXPLAIN', 'false') === 'true', slowQueryThresholdInMilliseconds: Number.parseInt( getEnv('FEATURE_MONGODB_SLOW_QUERY_THRESHOLD_IN_MILLISECONDS', '1000'), 10, ), }, mustWaitStatePersistence: getEnv('FEATURE_MUST_STATE_PERSISTENCE', 'true') !== 'false', retryDuration: Number.parseInt( getEnv( 'FEATURE_RETRY_DURATION_IN_MILLISECONDS', getEnv('DEFAULT_RETRY_DURATION_IN_MILLISECONDS', '0'), ), 10, ), deleteAfterArchiveDurationInSeconds: Number.parseInt( getEnv('FEATURE_DELETE_AFTER_ARCHIVE_DURATION_IN_SECONDS', '1209600'), // 14 days per default 10, ), initInternalModels: getEnv('FEATURE_INIT_INTERNAL_MODELS', 'false') === 'true', loadOnlyModels: JSON.parse(getEnv('FEATURE_LOAD_ONLY_MODELS', 'null')), }, authz: { isEnabled: getEnv('AUTHORIZATION_ENABLED') === 'true', noPolicyVerb: getEnv('AUTHORIZATION_NO_POLICY_VERB', 'allow'), skipModels: getEnv('AUTHORIZATION_SKIP_MODELS', '') .split(',') .map((t) => t.trim()) .filter((t) => !!t), onlyModels: getEnv('AUTHORIZATION_ONLY_MODELS', '') .split(',') .map((t) => t.trim()) .filter((t) => !!t), }, fhe: { scheme: getEnv('FHE_SCHEME', 'bgv'), polyModulusDegree: Number.parseInt( getEnv('FHE_POLY_MODULUS_DEGREE', '4096'), 10, ), bitSize: Number.parseInt(getEnv('FHE_BIT_SIZE', '20'), 10), bitSizes: JSON.parse(getEnv('FHE_BIT_SIZES', '[36, 36, 37]')), precision: Number.parseInt(getEnv('FHE_PRECISION', '2'), 10), }, mqtt: { namespace: getEnv('MQTT_NAMESPACE', ''), url: getEnv('MQTT_URL', 'mqtt://localhost:1883'), options: JSON.parse(getEnv('MQTT_OPTIONS', '{}')), group: getEnv('FEATURE_MQTT_GROUP', 'datastore'), }, amqp: { namespace: getEnv('AMQP_NAMESPACE', ''), url: getAMQPUrl(), options: JSON.parse(getEnv('AMQP_OPTIONS', '{}')), failover: { reconnectionTimeoutInMilliseconds: Number.parseInt( getEnv('AMQP_FAILOVER_RECONNECTION_TIMEOUT_IN_MILLISECONDS', '1000'), 10, ), }, channel: { prefetch: Number.parseInt(getEnv('AMQP_CHANNEL_PREFETCH', '100'), 10), }, exchange: { consumer: { name: getEnv('AMQP_EXCHANGE_CONSUMER_NAME', 'datastore'), type: getEnv('AMQP_EXCHANGE_CONSUMER_TYPE', 'topic'), options: JSON.parse(getEnv('AMQP_EXCHANGE_CONSUMER_OPTIONS', '{}')), }, producer: { name: getEnv('AMQP_EXCHANGE_PRODUCER_NAME', 'datastore'), type: getEnv('AMQP_EXCHANGE_PRODUCER_TYPE', 'topic'), options: JSON.parse(getEnv('AMQP_EXCHANGE_PRODUCER_OPTIONS', '{}')), }, }, queue: { consumer: { name: getEnv('AMQP_QUEUE_CONSUMER_NAME', 'datastore'), options: JSON.parse(getEnv('AMQP_QUEUE_CONSUMER_OPTIONS', '{}')), }, errors: { isEnabled: getEnv('AMQP_QUEUE_ERRORS_IS_ENABLED', 'false') === 'true', name: getEnv('AMQP_QUEUE_ERRORS_NAME', 'errors'), options: JSON.parse(getEnv('AMQP_QUEUE_ERRORS_OPTIONS', '{}')), }, }, headers: JSON.parse(getEnv('AMQP_HEADERS', '{}')), }, mongodb: { databases: [ { /** * @see * * https://mongodb.github.io/node-mongodb-native/4.5/interfaces/ConnectionOptions.html * https://mongodb.github.io/node-mongodb-native/4.3/interfaces/MongoClientOptions.html * * @note * * Replicaset configuration to test the stream API: * > docker run -d --name mongo4.4 -p 27017:27017 mongo:4.4 mongod --replSet "rs" * > docker exec -it mongo4.4 mongo * > * > db = (new Mongo('localhost:27017')).getDB('test') * > config={"_id":"rs","members":[{"_id":0,"host":"localhost:27017"}]} * > rs.initiate(config) */ name: `${DB_NAME}_write`, url: getEnv('MONGO_WRITE_URL') || getEnv('MONGO_URL') || 'mongodb://localhost:27017/datastore', options: { connectTimeoutMS: Number.parseInt( getEnv('MONGO_CONNECT_TIMEOUT_IN_MILLISECONDS', '10000'), 10, ), heartbeatFrequencyMS: Number.parseInt( getEnv('MONGO_HEARTBEAT_FREQUENCY_IN_MILLISECONDS', '1000'), 10, ), minHeartbeatFrequencyMS: Number.parseInt( getEnv('MONGO_MIN_HEARTBEAT_FREQUENCY_IN_MILLISECONDS', '1000'), 10, ), serverApi: getEnv('MONGO_SERVER_API'), ssl: getEnv('MONGO_SSL') === 'true', tlsAllowInvalidCertificates: getEnv('MONGO_SSL_VALIDATE') !== 'false', ...(getEnv('MONGO_USERNAME') || getEnv('MONGO_PASSWORD') ? { auth: { username: getEnv('MONGO_USERNAME'), password: getEnv('MONGO_PASSWORD'), }, } : {}), ...JSON.parse(getEnv('MONGO_WRITE_OPTIONS', '{}')), }, }, { name: `${DB_NAME}_read`, url: getEnv('MONGO_READ_URL') || getEnv('MONGO_URL') || 'mongodb://localhost:27017/datastore', options: { connectTimeoutMS: Number.parseInt( getEnv('MONGO_CONNECT_TIMEOUT_IN_MILLISECONDS', '10000'), 10, ), heartbeatFrequencyMS: Number.parseInt( getEnv('MONGO_HEARTBEAT_FREQUENCY_IN_MILLISECONDS', '1000'), 10, ), minHeartbeatFrequencyMS: Number.parseInt( getEnv('MONGO_MIN_HEARTBEAT_FREQUENCY_IN_MILLISECONDS', '1000'), 10, ), serverApi: getEnv('MONGO_SERVER_API'), ssl: getEnv('MONGO_SSL') === 'true', tlsAllowInvalidCertificates: getEnv('MONGO_SSL_VALIDATE') !== 'false', ...(getEnv('MONGO_USERNAME') || getEnv('MONGO_PASSWORD') ? { auth: { username: getEnv('MONGO_USERNAME'), password: getEnv('MONGO_PASSWORD'), }, } : {}), ...JSON.parse(getEnv('MONGO_READ_OPTIONS', '{}')), }, }, ], ensureIndexInBackground: true, }, pg: { namespace: getEnv('PG_NAMESPACE', ''), client: { connectionString: getEnv( 'PG_CONNECTION_STRING', 'postgresql://postgres:password@localhost:5432/datastore', ), }, }, models, datastores: JSON.parse(getEnv('DATASTORE_CONFIGS', '[]')), openApi: { spec: JSON.parse(getEnv('OPENAPI_SPEC', '{}')), }, }; /* istanbul ignore next */ const config = configPath !== '' ? require(configPath) : envConfig; export default config;