fix(AppData/Conversion): Improve boolean conversion and mapping evaluation in AppData, ensuring falsy values (like false, 0, and empty strings) are correctly handled and logged. Also, reduce test timeout and add local permissions settings for development.
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/npmextra',
|
||||
version: '5.3.0',
|
||||
version: '5.3.1',
|
||||
description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.'
|
||||
}
|
||||
|
@@ -20,10 +20,37 @@ function getQenv(): plugins.qenv.Qenv {
|
||||
// Type Converters - Centralized conversion logic
|
||||
// ============================================================================
|
||||
function toBoolean(value: unknown): boolean {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (value == null) return false;
|
||||
const s = String(value).toLowerCase();
|
||||
return s === 'true';
|
||||
// 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;
|
||||
}
|
||||
|
||||
function toJson<T = any>(value: unknown): T | undefined {
|
||||
@@ -159,6 +186,12 @@ async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
|
||||
return source.value;
|
||||
}
|
||||
// source.type === 'env'
|
||||
// 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.)
|
||||
return await getQenv().getEnvVarOnDemand(source.key);
|
||||
}
|
||||
|
||||
@@ -177,32 +210,60 @@ function applyTransforms(value: unknown, transforms: Transform[]): unknown {
|
||||
*/
|
||||
async function processMappingValue(mappingString: string): Promise<unknown> {
|
||||
const spec = parseMappingSpec(mappingString);
|
||||
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'}`);
|
||||
|
||||
const rawValue = await resolveSource(spec.source);
|
||||
console.log(` Raw value: ${JSON.stringify(rawValue)} (type: ${typeof rawValue})`);
|
||||
|
||||
if (rawValue === undefined || rawValue === null) {
|
||||
console.log(` ⚠️ Raw value is undefined/null, returning undefined`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return applyTransforms(rawValue, spec.transforms);
|
||||
const result = applyTransforms(rawValue, spec.transforms);
|
||||
console.log(` Final value: ${JSON.stringify(result)} (type: ${typeof result})`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively evaluate mapping values (strings or nested objects)
|
||||
*/
|
||||
async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
||||
// 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)
|
||||
if (typeof mappingValue === 'string') {
|
||||
return processMappingValue(mappingValue);
|
||||
}
|
||||
|
||||
// Handle objects (but not arrays or null)
|
||||
if (mappingValue && typeof mappingValue === 'object' && !Array.isArray(mappingValue)) {
|
||||
console.log(` 📂 Processing nested object with ${Object.keys(mappingValue).length} keys`);
|
||||
const result: any = {};
|
||||
for (const [key, value] of Object.entries(mappingValue)) {
|
||||
result[key] = await evaluateMappingValue(value);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// For any other type (numbers, booleans, etc.), return as-is
|
||||
console.log(` 📎 Returning value as-is: ${JSON.stringify(mappingValue)} (type: ${typeof mappingValue})`);
|
||||
return mappingValue;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -355,14 +416,26 @@ export class AppData<T = any> {
|
||||
for (const key in this.options.envMapping) {
|
||||
try {
|
||||
const mappingSpec = this.options.envMapping[key];
|
||||
console.log(` → Processing key "${key}" with spec:`, typeof mappingSpec === 'string' ? mappingSpec : 'nested object');
|
||||
const specType = mappingSpec === null ? 'null' :
|
||||
typeof mappingSpec === 'string' ? mappingSpec :
|
||||
typeof mappingSpec === 'object' ? 'nested object' :
|
||||
typeof mappingSpec;
|
||||
console.log(` → Processing key "${key}" with spec: ${specType}`);
|
||||
|
||||
const evaluated = await evaluateMappingValue(mappingSpec);
|
||||
// Important: Don't skip false, 0, empty string, or null values!
|
||||
// Only skip if explicitly undefined
|
||||
if (evaluated !== undefined) {
|
||||
await this.kvStore.writeKey(key as keyof T, evaluated);
|
||||
processedCount++;
|
||||
const valueType = Array.isArray(evaluated) ? 'array' : typeof evaluated;
|
||||
console.log(` ✅ Successfully processed key "${key}" (type: ${valueType})`);
|
||||
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})`);
|
||||
} else {
|
||||
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);
|
||||
}
|
||||
|
Reference in New Issue
Block a user