2023-08-24 10:39:47 +02:00
|
|
|
import * as plugins from './npmextra.plugins.js';
|
|
|
|
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
|
|
|
|
|
2025-08-15 13:17:18 +00:00
|
|
|
// ============================================================================
|
|
|
|
// Singleton Qenv Provider
|
|
|
|
// ============================================================================
|
|
|
|
let sharedQenv: plugins.qenv.Qenv | undefined;
|
|
|
|
|
|
|
|
function getQenv(): plugins.qenv.Qenv {
|
|
|
|
if (!sharedQenv) {
|
|
|
|
sharedQenv = new plugins.qenv.Qenv(
|
|
|
|
process.cwd(),
|
|
|
|
plugins.path.join(process.cwd(), '.nogit')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return sharedQenv;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Type Converters - Centralized conversion logic
|
|
|
|
// ============================================================================
|
|
|
|
function toBoolean(value: unknown): boolean {
|
2025-08-15 18:14:57 +00:00
|
|
|
// If already boolean, return as-is
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
console.log(` 🔹 toBoolean: value is already boolean: ${value}`);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle null/undefined
|
|
|
|
if (value == null) {
|
|
|
|
console.log(` 🔹 toBoolean: value is null/undefined, returning false`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle string representations
|
|
|
|
const s = String(value).toLowerCase().trim();
|
|
|
|
|
|
|
|
// True values: "true", "1", "yes", "y", "on"
|
|
|
|
if (['true', '1', 'yes', 'y', 'on'].includes(s)) {
|
|
|
|
console.log(` 🔹 toBoolean: converting "${value}" to true`);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// False values: "false", "0", "no", "n", "off"
|
|
|
|
if (['false', '0', 'no', 'n', 'off'].includes(s)) {
|
|
|
|
console.log(` 🔹 toBoolean: converting "${value}" to false`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default: non-empty string = true, empty = false
|
|
|
|
const result = s.length > 0;
|
|
|
|
console.log(` 🔹 toBoolean: defaulting "${value}" to ${result}`);
|
|
|
|
return result;
|
2025-08-15 13:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function toJson<T = any>(value: unknown): T | undefined {
|
|
|
|
if (value == null) return undefined;
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
try {
|
|
|
|
return JSON.parse(value);
|
|
|
|
} catch {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value as T;
|
|
|
|
}
|
|
|
|
|
|
|
|
function fromBase64(value: unknown): string | undefined {
|
|
|
|
if (value == null) return undefined;
|
|
|
|
try {
|
|
|
|
return Buffer.from(String(value), 'base64').toString('utf8');
|
|
|
|
} catch {
|
|
|
|
return String(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function toNumber(value: unknown): number | undefined {
|
|
|
|
if (value == null) return undefined;
|
|
|
|
const num = Number(value);
|
|
|
|
return Number.isNaN(num) ? undefined : num;
|
|
|
|
}
|
|
|
|
|
|
|
|
function toString(value: unknown): string | undefined {
|
|
|
|
if (value == null) return undefined;
|
|
|
|
return String(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Declarative Pipeline Architecture
|
|
|
|
// ============================================================================
|
|
|
|
type Transform = 'boolean' | 'json' | 'base64' | 'number';
|
|
|
|
|
|
|
|
type MappingSpec = {
|
|
|
|
source:
|
|
|
|
| { type: 'env'; key: string }
|
|
|
|
| { type: 'hard'; value: string };
|
|
|
|
transforms: Transform[];
|
|
|
|
};
|
|
|
|
|
|
|
|
// Transform registry for extensibility
|
|
|
|
const transformRegistry: Record<string, (v: unknown) => unknown> = {
|
|
|
|
boolean: toBoolean,
|
|
|
|
json: toJson,
|
|
|
|
base64: fromBase64,
|
|
|
|
number: toNumber,
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a mapping string into a declarative spec
|
|
|
|
*/
|
|
|
|
function parseMappingSpec(input: string): MappingSpec {
|
|
|
|
const transforms: Transform[] = [];
|
|
|
|
let remaining = input;
|
|
|
|
|
|
|
|
// Check for hardcoded prefixes with type conversion
|
|
|
|
if (remaining.startsWith('hard_boolean:')) {
|
|
|
|
return {
|
|
|
|
source: { type: 'hard', value: remaining.slice(13) },
|
|
|
|
transforms: ['boolean']
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (remaining.startsWith('hard_json:')) {
|
|
|
|
return {
|
|
|
|
source: { type: 'hard', value: remaining.slice(10) },
|
|
|
|
transforms: ['json']
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (remaining.startsWith('hard_base64:')) {
|
|
|
|
return {
|
|
|
|
source: { type: 'hard', value: remaining.slice(12) },
|
|
|
|
transforms: ['base64']
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for generic hard: prefix
|
|
|
|
if (remaining.startsWith('hard:')) {
|
|
|
|
remaining = remaining.slice(5);
|
|
|
|
// Check for legacy suffixes on hardcoded values
|
|
|
|
if (remaining.endsWith('_JSON')) {
|
|
|
|
transforms.push('json');
|
|
|
|
remaining = remaining.slice(0, -5);
|
|
|
|
} else if (remaining.endsWith('_BASE64')) {
|
|
|
|
transforms.push('base64');
|
|
|
|
remaining = remaining.slice(0, -7);
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
source: { type: 'hard', value: remaining },
|
|
|
|
transforms
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for env var prefixes
|
|
|
|
if (remaining.startsWith('boolean:')) {
|
|
|
|
transforms.push('boolean');
|
|
|
|
remaining = remaining.slice(8);
|
|
|
|
} else if (remaining.startsWith('json:')) {
|
|
|
|
transforms.push('json');
|
|
|
|
remaining = remaining.slice(5);
|
|
|
|
} else if (remaining.startsWith('base64:')) {
|
|
|
|
transforms.push('base64');
|
|
|
|
remaining = remaining.slice(7);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for legacy suffixes on env vars
|
|
|
|
if (remaining.endsWith('_JSON')) {
|
|
|
|
transforms.push('json');
|
|
|
|
remaining = remaining.slice(0, -5);
|
|
|
|
} else if (remaining.endsWith('_BASE64')) {
|
|
|
|
transforms.push('base64');
|
|
|
|
remaining = remaining.slice(0, -7);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
source: { type: 'env', key: remaining },
|
|
|
|
transforms
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve the source value (env var or hardcoded)
|
|
|
|
*/
|
|
|
|
async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
|
|
|
|
if (source.type === 'hard') {
|
|
|
|
return source.value;
|
|
|
|
}
|
|
|
|
// source.type === 'env'
|
2025-08-15 18:14:57 +00:00
|
|
|
// Workaround for Qenv bug where empty strings are treated as undefined
|
|
|
|
// Check process.env directly first to preserve empty strings
|
|
|
|
if (Object.prototype.hasOwnProperty.call(process.env, source.key)) {
|
|
|
|
return process.env[source.key];
|
|
|
|
}
|
|
|
|
// Fall back to Qenv for other sources (env.json, docker secrets, etc.)
|
2025-08-15 13:17:18 +00:00
|
|
|
return await getQenv().getEnvVarOnDemand(source.key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Apply transformations in sequence
|
|
|
|
*/
|
|
|
|
function applyTransforms(value: unknown, transforms: Transform[]): unknown {
|
|
|
|
return transforms.reduce((acc, transform) => {
|
|
|
|
const fn = transformRegistry[transform];
|
|
|
|
return fn ? fn(acc) : acc;
|
|
|
|
}, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process a mapping value through the complete pipeline
|
|
|
|
*/
|
|
|
|
async function processMappingValue(mappingString: string): Promise<unknown> {
|
|
|
|
const spec = parseMappingSpec(mappingString);
|
2025-08-15 18:14:57 +00:00
|
|
|
console.log(` 🔍 Processing mapping: "${mappingString}"`);
|
|
|
|
console.log(` Source: ${spec.source.type === 'env' ? `env:${spec.source.key}` : `hard:${spec.source.value}`}`);
|
|
|
|
console.log(` Transforms: ${spec.transforms.length > 0 ? spec.transforms.join(', ') : 'none'}`);
|
|
|
|
|
2025-08-15 13:17:18 +00:00
|
|
|
const rawValue = await resolveSource(spec.source);
|
2025-08-15 18:14:57 +00:00
|
|
|
console.log(` Raw value: ${JSON.stringify(rawValue)} (type: ${typeof rawValue})`);
|
2025-08-15 13:17:18 +00:00
|
|
|
|
|
|
|
if (rawValue === undefined || rawValue === null) {
|
2025-08-15 18:14:57 +00:00
|
|
|
console.log(` ⚠️ Raw value is undefined/null, returning undefined`);
|
2025-08-15 13:17:18 +00:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2025-08-15 18:14:57 +00:00
|
|
|
const result = applyTransforms(rawValue, spec.transforms);
|
|
|
|
console.log(` Final value: ${JSON.stringify(result)} (type: ${typeof result})`);
|
|
|
|
return result;
|
2025-08-15 13:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively evaluate mapping values (strings or nested objects)
|
|
|
|
*/
|
|
|
|
async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
2025-08-15 18:14:57 +00:00
|
|
|
// Handle null explicitly - it should return null, not be treated as object
|
|
|
|
if (mappingValue === null) {
|
|
|
|
console.log(` 📌 Value is null, returning null`);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle strings (mapping specs)
|
2025-08-15 13:17:18 +00:00
|
|
|
if (typeof mappingValue === 'string') {
|
|
|
|
return processMappingValue(mappingValue);
|
|
|
|
}
|
|
|
|
|
2025-08-15 18:14:57 +00:00
|
|
|
// Handle objects (but not arrays or null)
|
2025-08-15 13:17:18 +00:00
|
|
|
if (mappingValue && typeof mappingValue === 'object' && !Array.isArray(mappingValue)) {
|
2025-08-15 18:14:57 +00:00
|
|
|
console.log(` 📂 Processing nested object with ${Object.keys(mappingValue).length} keys`);
|
2025-08-15 13:17:18 +00:00
|
|
|
const result: any = {};
|
|
|
|
for (const [key, value] of Object.entries(mappingValue)) {
|
2025-08-15 18:14:57 +00:00
|
|
|
console.log(` → Processing nested key "${key}"`);
|
|
|
|
const evaluated = await evaluateMappingValue(value);
|
|
|
|
// Important: Don't filter out false or other falsy values!
|
|
|
|
// Only skip if explicitly undefined
|
|
|
|
if (evaluated !== undefined) {
|
|
|
|
result[key] = evaluated;
|
|
|
|
console.log(` ✓ Nested key "${key}" = ${JSON.stringify(evaluated)} (type: ${typeof evaluated})`);
|
|
|
|
} else {
|
|
|
|
console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`);
|
|
|
|
}
|
2025-08-15 13:17:18 +00:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2025-08-15 18:14:57 +00:00
|
|
|
// For any other type (numbers, booleans, etc.), return as-is
|
|
|
|
console.log(` 📎 Returning value as-is: ${JSON.stringify(mappingValue)} (type: ${typeof mappingValue})`);
|
|
|
|
return mappingValue;
|
2025-08-15 13:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// AppData Interface and Class
|
|
|
|
// ============================================================================
|
2024-06-12 20:04:04 +02:00
|
|
|
export interface IAppDataOptions<T = any> {
|
2024-02-07 21:44:00 +01:00
|
|
|
dirPath?: string;
|
2024-06-12 20:04:04 +02:00
|
|
|
requiredKeys?: Array<keyof T>;
|
2024-02-09 15:57:32 +01:00
|
|
|
|
2024-06-19 15:07:49 +02:00
|
|
|
/**
|
2025-08-15 13:17:18 +00:00
|
|
|
* Whether keys should be persisted on disk or not
|
|
|
|
*/
|
|
|
|
ephemeral?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @deprecated Use 'ephemeral' instead
|
2024-06-19 15:07:49 +02:00
|
|
|
*/
|
|
|
|
ephermal?: boolean;
|
|
|
|
|
2024-02-09 15:57:32 +01:00
|
|
|
/**
|
|
|
|
* kvStoreKey: 'MY_ENV_VAR'
|
|
|
|
*/
|
2024-06-19 19:03:26 +02:00
|
|
|
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>;
|
2024-11-05 15:11:01 +01:00
|
|
|
overwriteObject?: plugins.tsclass.typeFest.PartialDeep<T>;
|
2024-02-07 21:44:00 +01:00
|
|
|
}
|
|
|
|
|
2024-02-13 00:48:44 +01:00
|
|
|
export class AppData<T = any> {
|
2024-01-25 13:57:55 +01:00
|
|
|
/**
|
|
|
|
* creates appdata. If no pathArg is given, data will be stored here:
|
|
|
|
* ${PWD}/.nogit/appdata
|
|
|
|
* @param pathArg
|
2024-02-09 15:57:32 +01:00
|
|
|
* @returns
|
2024-01-25 13:57:55 +01:00
|
|
|
*/
|
2024-06-19 19:03:26 +02:00
|
|
|
public static async createAndInit<T = any>(
|
2025-08-15 12:12:26 +00:00
|
|
|
optionsArg: IAppDataOptions<T> = {},
|
2024-06-19 19:03:26 +02:00
|
|
|
): Promise<AppData<T>> {
|
2024-02-13 00:48:44 +01:00
|
|
|
const appData = new AppData<T>(optionsArg);
|
2024-01-25 13:57:55 +01:00
|
|
|
await appData.readyDeferred.promise;
|
2023-08-26 09:56:20 +02:00
|
|
|
return appData;
|
|
|
|
}
|
|
|
|
|
2025-08-15 13:17:18 +00:00
|
|
|
/**
|
|
|
|
* Static helper to get an environment variable as a boolean
|
|
|
|
* @param envVarName The name of the environment variable
|
|
|
|
* @returns boolean value (true if env var is "true", false otherwise)
|
|
|
|
*/
|
|
|
|
public static async valueAsBoolean(envVarName: string): Promise<boolean> {
|
|
|
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
|
|
return toBoolean(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static helper to get an environment variable as parsed JSON
|
|
|
|
* @param envVarName The name of the environment variable
|
|
|
|
* @returns Parsed JSON object/array
|
|
|
|
*/
|
|
|
|
public static async valueAsJson<R = any>(envVarName: string): Promise<R | undefined> {
|
|
|
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
|
|
return toJson<R>(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static helper to get an environment variable as base64 decoded string
|
|
|
|
* @param envVarName The name of the environment variable
|
|
|
|
* @returns Decoded string
|
|
|
|
*/
|
|
|
|
public static async valueAsBase64(envVarName: string): Promise<string | undefined> {
|
|
|
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
|
|
return fromBase64(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static helper to get an environment variable as a string
|
|
|
|
* @param envVarName The name of the environment variable
|
|
|
|
* @returns String value
|
|
|
|
*/
|
|
|
|
public static async valueAsString(envVarName: string): Promise<string | undefined> {
|
|
|
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
|
|
return toString(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static helper to get an environment variable as a number
|
|
|
|
* @param envVarName The name of the environment variable
|
|
|
|
* @returns Number value
|
|
|
|
*/
|
|
|
|
public static async valueAsNumber(envVarName: string): Promise<number | undefined> {
|
|
|
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
|
|
return toNumber(value);
|
|
|
|
}
|
|
|
|
|
2023-08-26 09:56:20 +02:00
|
|
|
// instance
|
2024-06-12 20:04:04 +02:00
|
|
|
public readyDeferred = plugins.smartpromise.defer<void>();
|
|
|
|
public options: IAppDataOptions<T>;
|
2024-02-13 00:50:50 +01:00
|
|
|
private kvStore: KeyValueStore<T>;
|
2024-06-12 20:04:04 +02:00
|
|
|
|
|
|
|
constructor(optionsArg: IAppDataOptions<T> = {}) {
|
2024-02-07 21:44:00 +01:00
|
|
|
this.options = optionsArg;
|
2024-01-27 19:03:06 +01:00
|
|
|
this.init();
|
2023-08-24 10:39:47 +02:00
|
|
|
}
|
|
|
|
|
2024-01-25 13:57:55 +01:00
|
|
|
/**
|
|
|
|
* inits app data
|
|
|
|
*/
|
2025-08-15 13:17:18 +00:00
|
|
|
private async init() {
|
|
|
|
console.log('🚀 Initializing AppData...');
|
|
|
|
|
|
|
|
// Handle backward compatibility for typo
|
|
|
|
const isEphemeral = this.options.ephemeral ?? this.options.ephermal ?? false;
|
|
|
|
if (this.options.ephermal && !this.options.ephemeral) {
|
|
|
|
console.warn('⚠️ Option "ephermal" is deprecated, use "ephemeral" instead.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.options.dirPath) {
|
|
|
|
console.log(` 📁 Using custom directory: ${this.options.dirPath}`);
|
|
|
|
} else if (isEphemeral) {
|
|
|
|
console.log(` 💨 Using ephemeral storage (in-memory only)`);
|
2023-08-24 10:39:47 +02:00
|
|
|
} else {
|
|
|
|
const appDataDir = '/app/data';
|
|
|
|
const dataDir = '/data';
|
2023-08-24 22:59:43 +02:00
|
|
|
const nogitAppData = '.nogit/appdata';
|
2023-08-24 10:39:47 +02:00
|
|
|
const appDataExists = plugins.smartfile.fs.isDirectory(appDataDir);
|
|
|
|
const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
|
|
|
|
if (appDataExists) {
|
2024-02-07 21:44:00 +01:00
|
|
|
this.options.dirPath = appDataDir;
|
2025-08-15 13:17:18 +00:00
|
|
|
console.log(` 📁 Auto-selected container directory: ${appDataDir}`);
|
2023-08-24 22:59:43 +02:00
|
|
|
} else if (dataExists) {
|
2024-02-07 21:44:00 +01:00
|
|
|
this.options.dirPath = dataDir;
|
2025-08-15 13:17:18 +00:00
|
|
|
console.log(` 📁 Auto-selected data directory: ${dataDir}`);
|
2023-08-24 22:59:43 +02:00
|
|
|
} else {
|
|
|
|
await plugins.smartfile.fs.ensureDir(nogitAppData);
|
2024-02-07 21:44:00 +01:00
|
|
|
this.options.dirPath = nogitAppData;
|
2025-08-15 13:17:18 +00:00
|
|
|
console.log(` 📁 Auto-selected local directory: ${nogitAppData}`);
|
2023-08-24 10:39:47 +02:00
|
|
|
}
|
|
|
|
}
|
2024-06-12 20:04:04 +02:00
|
|
|
|
|
|
|
this.kvStore = new KeyValueStore<T>({
|
2025-08-15 13:17:18 +00:00
|
|
|
typeArg: isEphemeral ? 'ephemeral' : 'custom',
|
2024-02-12 19:16:43 +01:00
|
|
|
identityArg: 'appkv',
|
|
|
|
customPath: this.options.dirPath,
|
2024-06-19 19:03:26 +02:00
|
|
|
mandatoryKeys: this.options.requiredKeys as Array<keyof T>,
|
2024-02-12 19:16:43 +01:00
|
|
|
});
|
2024-02-09 15:57:32 +01:00
|
|
|
|
|
|
|
if (this.options.envMapping) {
|
2025-08-15 13:17:18 +00:00
|
|
|
console.log(`📦 Processing envMapping for AppData...`);
|
|
|
|
const totalKeys = Object.keys(this.options.envMapping).length;
|
|
|
|
let processedCount = 0;
|
|
|
|
|
|
|
|
// Process each top-level key in envMapping
|
|
|
|
for (const key in this.options.envMapping) {
|
|
|
|
try {
|
|
|
|
const mappingSpec = this.options.envMapping[key];
|
2025-08-15 18:14:57 +00:00
|
|
|
const specType = mappingSpec === null ? 'null' :
|
|
|
|
typeof mappingSpec === 'string' ? mappingSpec :
|
|
|
|
typeof mappingSpec === 'object' ? 'nested object' :
|
|
|
|
typeof mappingSpec;
|
|
|
|
console.log(` → Processing key "${key}" with spec: ${specType}`);
|
2025-08-15 13:17:18 +00:00
|
|
|
|
|
|
|
const evaluated = await evaluateMappingValue(mappingSpec);
|
2025-08-15 18:14:57 +00:00
|
|
|
// Important: Don't skip false, 0, empty string, or null values!
|
|
|
|
// Only skip if explicitly undefined
|
2025-08-15 13:17:18 +00:00
|
|
|
if (evaluated !== undefined) {
|
|
|
|
await this.kvStore.writeKey(key as keyof T, evaluated);
|
|
|
|
processedCount++;
|
2025-08-15 18:14:57 +00:00
|
|
|
const valueType = evaluated === null ? 'null' :
|
|
|
|
Array.isArray(evaluated) ? 'array' :
|
|
|
|
typeof evaluated;
|
|
|
|
const valuePreview = evaluated === null ? 'null' :
|
|
|
|
typeof evaluated === 'object' ?
|
|
|
|
(Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) :
|
|
|
|
JSON.stringify(evaluated);
|
|
|
|
console.log(` ✅ Successfully processed key "${key}" = ${valuePreview} (type: ${valueType})`);
|
2024-02-12 20:09:26 +01:00
|
|
|
} else {
|
2025-08-15 13:17:18 +00:00
|
|
|
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);
|
2024-02-12 18:40:01 +01:00
|
|
|
}
|
2025-08-15 13:17:18 +00:00
|
|
|
} catch (err) {
|
|
|
|
console.error(` ❌ Failed to evaluate envMapping for key "${key}":`, err);
|
2024-02-09 15:57:32 +01:00
|
|
|
}
|
|
|
|
}
|
2025-08-15 13:17:18 +00:00
|
|
|
|
|
|
|
console.log(`📊 EnvMapping complete: ${processedCount}/${totalKeys} keys successfully processed`);
|
|
|
|
}
|
2024-11-05 15:11:01 +01:00
|
|
|
|
2025-08-15 13:17:18 +00:00
|
|
|
// Apply overwrite object after env mapping
|
|
|
|
if (this.options.overwriteObject) {
|
|
|
|
const overwriteKeys = Object.keys(this.options.overwriteObject);
|
|
|
|
console.log(`🔄 Applying overwriteObject with ${overwriteKeys.length} key(s)...`);
|
|
|
|
|
|
|
|
for (const key of overwriteKeys) {
|
|
|
|
const value = this.options.overwriteObject[key];
|
|
|
|
const valueType = Array.isArray(value) ? 'array' : typeof value;
|
|
|
|
console.log(` 🔧 Overwriting key "${key}" with ${valueType} value`);
|
|
|
|
|
|
|
|
await this.kvStore.writeKey(
|
|
|
|
key as keyof T,
|
|
|
|
value,
|
|
|
|
);
|
2024-11-05 15:11:01 +01:00
|
|
|
}
|
2025-08-15 13:17:18 +00:00
|
|
|
|
|
|
|
console.log(`✅ OverwriteObject complete: ${overwriteKeys.length} key(s) overwritten`);
|
2024-02-09 15:57:32 +01:00
|
|
|
}
|
|
|
|
|
2023-08-24 10:39:47 +02:00
|
|
|
this.readyDeferred.resolve();
|
2025-08-15 13:17:18 +00:00
|
|
|
console.log('✨ AppData initialization complete!');
|
2023-08-24 10:39:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-12 20:04:04 +02:00
|
|
|
* returns a kvstore that resides in appdata
|
2023-08-24 10:39:47 +02:00
|
|
|
*/
|
2024-06-12 20:04:04 +02:00
|
|
|
public async getKvStore(): Promise<KeyValueStore<T>> {
|
2023-08-24 10:39:47 +02:00
|
|
|
await this.readyDeferred.promise;
|
|
|
|
return this.kvStore;
|
|
|
|
}
|
2024-02-09 11:52:30 +01:00
|
|
|
|
2024-06-12 20:04:04 +02:00
|
|
|
public async logMissingKeys(): Promise<Array<keyof T>> {
|
2024-02-09 11:52:30 +01:00
|
|
|
const kvStore = await this.getKvStore();
|
2024-04-14 02:10:29 +02:00
|
|
|
const missingMandatoryKeys = await kvStore.getMissingMandatoryKeys();
|
2024-02-09 11:52:30 +01:00
|
|
|
if (missingMandatoryKeys.length > 0) {
|
2024-02-09 15:57:32 +01:00
|
|
|
console.log(
|
|
|
|
`The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join(
|
2025-08-15 12:12:26 +00:00
|
|
|
',\n -> ',
|
|
|
|
)}`,
|
2024-02-09 15:57:32 +01:00
|
|
|
);
|
2024-02-09 11:52:30 +01:00
|
|
|
} else {
|
|
|
|
console.log('All mandatory keys are present in the appdata');
|
|
|
|
}
|
2024-02-13 02:04:04 +01:00
|
|
|
return missingMandatoryKeys;
|
2024-02-09 11:52:30 +01:00
|
|
|
}
|
2024-02-10 04:54:00 +01:00
|
|
|
|
2025-08-15 12:12:26 +00:00
|
|
|
public async waitForAndGetKey<K extends keyof T>(
|
|
|
|
keyArg: K,
|
|
|
|
): Promise<T[K] | undefined> {
|
2024-02-10 04:54:00 +01:00
|
|
|
await this.readyDeferred.promise;
|
2024-02-10 04:55:50 +01:00
|
|
|
await this.kvStore.waitForKeysPresent([keyArg]);
|
2024-06-12 20:04:04 +02:00
|
|
|
return this.kvStore.readKey(keyArg);
|
2024-02-10 04:54:00 +01:00
|
|
|
}
|
2024-06-19 19:03:26 +02:00
|
|
|
}
|