import * as plugins from './npmextra.plugins.js'; import * as paths from './npmextra.paths.js'; import { KeyValueStore } from './npmextra.classes.keyvaluestore.js'; export interface IAppDataOptions { dirPath?: string; requiredKeys?: Array; /** * wether keys should be persisted on disk or not */ ephermal?: boolean; /** * kvStoreKey: 'MY_ENV_VAR' */ envMapping?: plugins.tsclass.typeFest.PartialDeep; overwriteObject?: plugins.tsclass.typeFest.PartialDeep; } export class AppData { /** * creates appdata. If no pathArg is given, data will be stored here: * ${PWD}/.nogit/appdata * @param pathArg * @returns */ public static async createAndInit( optionsArg: IAppDataOptions = {} ): Promise> { const appData = new AppData(optionsArg); await appData.readyDeferred.promise; return appData; } // instance public readyDeferred = plugins.smartpromise.defer(); public options: IAppDataOptions; private kvStore: KeyValueStore; constructor(optionsArg: IAppDataOptions = {}) { this.options = optionsArg; this.init(); } /** * inits app data * @param pathArg */ private async init(pathArg?: string) { if (this.options.dirPath || this.options.ephermal) { // ok, nothing to do here; } else { const appDataDir = '/app/data'; const dataDir = '/data'; const nogitAppData = '.nogit/appdata'; const appDataExists = plugins.smartfile.fs.isDirectory(appDataDir); const dataExists = plugins.smartfile.fs.isDirectory(dataDir); if (appDataExists) { this.options.dirPath = appDataDir; } else if (dataExists) { this.options.dirPath = dataDir; } else { await plugins.smartfile.fs.ensureDir(nogitAppData); this.options.dirPath = nogitAppData; } } this.kvStore = new KeyValueStore({ typeArg: this.options.ephermal ? 'ephemeral' : 'custom', identityArg: 'appkv', customPath: this.options.dirPath, mandatoryKeys: this.options.requiredKeys as Array, }); if (this.options.envMapping) { const qenvInstance = new plugins.qenv.Qenv( process.cwd(), plugins.path.join(process.cwd(), '.nogit') ); // Recursive function to handle nested objects, now includes key parameter const processEnvMapping = async ( key: keyof T, mappingValue: any, parentKey: keyof T | '' = '' ): Promise => { if (typeof mappingValue === 'string') { let envValue: string | boolean | T[keyof T]; let convert: 'none' | 'json' | 'base64' | 'boolean' = 'none'; switch (true) { case mappingValue.startsWith('hard:'): envValue = mappingValue.replace('hard:', '') as T[keyof T]; break; case mappingValue.startsWith('hard_boolean:'): envValue = mappingValue.replace('hard_boolean:', '') === 'true'; convert = 'boolean'; break; case mappingValue.startsWith('hard_json:'): envValue = JSON.parse(mappingValue.replace('hard_json:', '')) as T[keyof T]; convert = 'json'; break; case mappingValue.startsWith('hard_base64:'): envValue = Buffer.from( mappingValue.replace('hard_base64:', ''), 'base64' ).toString() as T[keyof T]; convert = 'base64'; break; case mappingValue.startsWith('boolean:'): envValue = (await qenvInstance.getEnvVarOnDemand( mappingValue.replace('boolean:', '') )) as T[keyof T]; convert = 'boolean'; break; case mappingValue.startsWith('json:'): envValue = (await qenvInstance.getEnvVarOnDemand( mappingValue.replace('json:', '') )) as T[keyof T]; convert = 'json'; break; case mappingValue.startsWith('base64:'): envValue = (await qenvInstance.getEnvVarOnDemand( mappingValue.replace('base64:', '') )) as T[keyof T]; convert = 'base64'; break; default: envValue = (await qenvInstance.getEnvVarOnDemand(mappingValue)) as T[keyof T]; break; } // lets format the env value if (envValue) { if (typeof envValue === 'string' && convert === 'boolean') { envValue = envValue === 'true'; } if ( typeof envValue === 'string' && (mappingValue.endsWith('_JSON') || convert === 'json') ) { envValue = JSON.parse(envValue as string) as T[keyof T]; } if ( typeof envValue === 'string' && (mappingValue.endsWith('_BASE64') || convert === 'base64') ) { envValue = Buffer.from(envValue as string, 'base64').toString(); } if (!parentKey) { await this.kvStore.writeKey(key, envValue as any); } else { return envValue; } } else { return undefined; } } else if (typeof mappingValue === 'object' && mappingValue !== null) { const resultObject: Partial = {}; for (const innerKey in mappingValue) { const nestedValue = mappingValue[innerKey]; // For nested objects, call recursively but do not immediately write to kvStore const nestedResult = await processEnvMapping(innerKey as keyof T, nestedValue, key); resultObject[innerKey as keyof T] = nestedResult; } if (parentKey === '') { // Only write to kvStore if at the top level await this.kvStore.writeKey(key, resultObject as T[keyof T]); } else { // For nested objects, return the constructed object instead of writing to kvStore return resultObject; } } }; for (const key in this.options.envMapping) { await processEnvMapping(key as keyof T, this.options.envMapping[key]); } for (const key in Object.keys(this.options.overwriteObject)) { console.log(`-> heads up: overwriting key ${key} from options.overwriteObject`); await this.kvStore.writeKey(key as keyof T, this.options.overwriteObject[key]); } } this.readyDeferred.resolve(); } /** * returns a kvstore that resides in appdata */ public async getKvStore(): Promise> { await this.readyDeferred.promise; return this.kvStore; } public async logMissingKeys(): Promise> { const kvStore = await this.getKvStore(); const missingMandatoryKeys = await kvStore.getMissingMandatoryKeys(); if (missingMandatoryKeys.length > 0) { console.log( `The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join( ',\n -> ' )}` ); } else { console.log('All mandatory keys are present in the appdata'); } return missingMandatoryKeys; } public async waitForAndGetKey(keyArg: K): Promise { await this.readyDeferred.promise; await this.kvStore.waitForKeysPresent([keyArg]); return this.kvStore.readKey(keyArg); } }