supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
537 lines • 19.4 kB
JavaScript
;
/**
* Asset-to-Data Mapping System
* Phase 2, Checkpoint B3 - Bridge between asset selection and database seeding
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AssetDataMapper = void 0;
const logger_1 = require("../../../core/utils/logger");
class AssetDataMapper {
/**
* Map assets to database records using the provided configuration
*/
static mapAssetsToRecords(assets, config) {
const startTime = Date.now();
const records = [];
const errors = [];
const statistics = {
fieldUsageStats: {},
dataTypeStats: {},
fallbackUsageStats: {},
validationFailureStats: {}
};
logger_1.Logger.info(`🗂️ Mapping ${assets.length} assets to ${config.tableName} records`);
for (const asset of assets) {
try {
const mappedRecord = this.mapSingleAsset(asset, config, statistics);
records.push(mappedRecord);
}
catch (error) {
errors.push({ asset, error: error.message });
logger_1.Logger.warn(`Failed to map asset ${asset.id}: ${error.message}`);
}
}
const successful = records.filter(r => r.isValid).length;
const failed = records.length - successful;
const executionTime = Date.now() - startTime;
logger_1.Logger.info(`✅ Mapped ${successful}/${assets.length} assets successfully (${executionTime}ms)`);
return {
records,
successful,
failed,
totalAssets: assets.length,
executionTime,
errors,
statistics
};
}
/**
* Map a single asset to a database record
*/
static mapSingleAsset(asset, config, statistics) {
const mappedData = {};
const errors = [];
const warnings = [];
const usedFallbacks = [];
// Apply global defaults first
if (config.globalDefaults) {
Object.assign(mappedData, config.globalDefaults);
}
// Process each field mapping
for (const mapping of config.mappings) {
try {
const result = this.mapField(asset, mapping, statistics);
if (result.success) {
mappedData[mapping.targetField] = result.value;
if (result.usedFallback) {
usedFallbacks.push(mapping.targetField);
}
}
else {
const errorMsg = `Field '${mapping.targetField}': ${result.error}`;
if (mapping.required) {
if (config.onMappingError === 'throw') {
throw new Error(errorMsg);
}
else if (config.onMappingError === 'use_fallback' && mapping.fallbackGenerator) {
const fallbackValue = mapping.fallbackGenerator();
mappedData[mapping.targetField] = fallbackValue;
usedFallbacks.push(mapping.targetField);
warnings.push(`Used fallback for ${mapping.targetField}`);
}
else {
errors.push(errorMsg);
}
}
else {
// For optional fields, still add to errors if it's a conversion/validation error
if (config.onMappingError === 'skip') {
warnings.push(errorMsg);
}
else {
errors.push(errorMsg);
}
// Use default value for optional fields if available
if (mapping.defaultValue !== undefined) {
mappedData[mapping.targetField] = mapping.defaultValue;
}
}
}
}
catch (error) {
const errorMsg = `Field '${mapping.targetField}': ${error.message}`;
errors.push(errorMsg);
statistics.validationFailureStats[mapping.targetField] =
(statistics.validationFailureStats[mapping.targetField] || 0) + 1;
}
}
// Determine if record is valid
const hasRequiredFieldErrors = errors.some(error => config.mappings.some(m => m.required && error.includes(m.targetField)));
const isValid = !hasRequiredFieldErrors && (config.allowPartial || errors.length === 0);
return {
originalAsset: asset,
mappedData,
errors,
warnings,
isValid,
usedFallbacks
};
}
/**
* Map a single field from asset to target format
*/
static mapField(asset, mapping, statistics) {
// Update statistics
statistics.fieldUsageStats[mapping.sourceField] =
(statistics.fieldUsageStats[mapping.sourceField] || 0) + 1;
statistics.dataTypeStats[mapping.dataType] =
(statistics.dataTypeStats[mapping.dataType] || 0) + 1;
try {
// Extract raw value from asset
let rawValue = this.extractFieldValue(asset, mapping.sourceField);
// Handle missing required values
if ((rawValue === undefined || rawValue === null) && mapping.required) {
if (mapping.fallbackGenerator) {
rawValue = mapping.fallbackGenerator();
statistics.fallbackUsageStats[mapping.sourceField] =
(statistics.fallbackUsageStats[mapping.sourceField] || 0) + 1;
return { success: true, value: rawValue, usedFallback: true };
}
return { success: false, error: 'Required field is missing' };
}
// Use default value if available and value is missing
if ((rawValue === undefined || rawValue === null) && mapping.defaultValue !== undefined) {
rawValue = mapping.defaultValue;
}
// Apply transformer if provided
if (mapping.transformer && rawValue !== undefined && rawValue !== null) {
rawValue = mapping.transformer(rawValue, asset);
}
// Convert data type
const convertedValue = this.convertDataType(rawValue, mapping.dataType);
// Validate if validator provided
if (mapping.validator && !mapping.validator(convertedValue)) {
return { success: false, error: 'Validation failed' };
}
return { success: true, value: convertedValue };
}
catch (error) {
return { success: false, error: error.message };
}
}
/**
* Extract field value from asset
*/
static extractFieldValue(asset, sourceField) {
// Handle special fields
if (sourceField.startsWith('$')) {
const specialHandler = this.SPECIAL_FIELDS[sourceField];
if (specialHandler) {
return specialHandler(asset);
}
throw new Error(`Unknown special field: ${sourceField}`);
}
// Handle nested field access (e.g., "user.profile.displayName")
if (sourceField.includes('.')) {
const parts = sourceField.split('.');
let current = asset.metadata;
for (const part of parts) {
if (current && typeof current === 'object') {
current = current[part];
}
else {
return undefined;
}
}
return current;
}
// Direct field access
return asset.metadata[sourceField];
}
/**
* Convert value to target data type
*/
static convertDataType(value, targetType) {
if (value === null || value === undefined) {
return null;
}
switch (targetType) {
case 'string':
return String(value);
case 'number':
if (typeof value === 'number')
return value;
if (typeof value === 'string') {
const parsed = parseFloat(value);
if (isNaN(parsed)) {
throw new Error(`Cannot convert "${value}" to number`);
}
return parsed;
}
if (typeof value === 'boolean')
return value ? 1 : 0;
throw new Error(`Cannot convert ${typeof value} to number`);
case 'boolean':
if (typeof value === 'boolean')
return value;
if (typeof value === 'string') {
const lower = value.toLowerCase();
if (['true', '1', 'yes', 'on', 'enabled'].includes(lower))
return true;
if (['false', '0', 'no', 'off', 'disabled'].includes(lower))
return false;
}
if (typeof value === 'number')
return value !== 0;
throw new Error(`Cannot convert "${value}" to boolean`);
case 'date':
if (value instanceof Date)
return value;
if (typeof value === 'string' || typeof value === 'number') {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error(`Cannot convert "${value}" to date`);
}
return date;
}
throw new Error(`Cannot convert ${typeof value} to date`);
case 'json':
if (typeof value === 'object')
return value;
if (typeof value === 'string') {
try {
return JSON.parse(value);
}
catch {
throw new Error(`Cannot parse "${value}" as JSON`);
}
}
throw new Error(`Cannot convert ${typeof value} to JSON`);
case 'array':
if (Array.isArray(value))
return value;
if (typeof value === 'string') {
// Try to parse as JSON array first
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed))
return parsed;
}
catch {
// Fall back to comma-separated values
return value.split(',').map(item => item.trim());
}
}
// Wrap single values in array
return [value];
default:
throw new Error(`Unknown data type: ${targetType}`);
}
}
/**
* Validate mapping configuration
*/
static validateMappingConfig(config) {
const errors = [];
const warnings = [];
if (!config.tableName || config.tableName.trim() === '') {
errors.push('Table name is required');
}
if (!config.mappings || config.mappings.length === 0) {
errors.push('At least one field mapping is required');
}
const targetFields = new Set();
for (const mapping of config.mappings || []) {
// Check for duplicate target fields
if (targetFields.has(mapping.targetField)) {
errors.push(`Duplicate target field: ${mapping.targetField}`);
}
targetFields.add(mapping.targetField);
// Validate source field
if (!mapping.sourceField) {
errors.push(`Source field is required for target: ${mapping.targetField}`);
}
// Validate data type
if (!['string', 'number', 'boolean', 'date', 'json', 'array'].includes(mapping.dataType)) {
errors.push(`Invalid data type '${mapping.dataType}' for field: ${mapping.targetField}`);
}
// Warn about required fields without fallbacks
if (mapping.required && !mapping.fallbackGenerator && mapping.defaultValue === undefined) {
warnings.push(`Required field '${mapping.targetField}' has no fallback or default value`);
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
}
exports.AssetDataMapper = AssetDataMapper;
AssetDataMapper.SPECIAL_FIELDS = {
'$content': (asset) => asset.content,
'$filename': (asset) => asset.metadata.filename,
'$filepath': (asset) => asset.filePath,
'$filesize': (asset) => asset.fileSize,
'$lastmodified': (asset) => asset.lastModified,
'$type': (asset) => asset.type,
'$id': (asset) => asset.id,
'$isvalid': (asset) => asset.isValid,
'$extension': (asset) => asset.metadata.extension,
'$mimetype': (asset) => asset.metadata.mimeType
};
/**
* Create common field mapping configurations
*/
AssetDataMapper.createMappings = {
/**
* Create mapping for blog posts
*/
blogPost: () => [
{
sourceField: 'title',
targetField: 'title',
dataType: 'string',
required: true,
fallbackGenerator: () => `Generated Post ${Date.now()}`
},
{
sourceField: 'description',
targetField: 'description',
dataType: 'string',
required: false
},
{
sourceField: '$content',
targetField: 'content',
dataType: 'string',
required: true,
fallbackGenerator: () => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
},
{
sourceField: 'author',
targetField: 'author',
dataType: 'string',
required: false,
defaultValue: 'Anonymous'
},
{
sourceField: 'date',
targetField: 'published_at',
dataType: 'date',
required: false,
transformer: (value, asset) => value || asset.lastModified
},
{
sourceField: 'tags',
targetField: 'tags',
dataType: 'array',
required: false,
defaultValue: []
},
{
sourceField: 'featured',
targetField: 'is_featured',
dataType: 'boolean',
required: false,
defaultValue: false
}
],
/**
* Create mapping for users from CSV
*/
users: () => [
{
sourceField: 'name',
targetField: 'full_name',
dataType: 'string',
required: true,
fallbackGenerator: () => `User ${Math.floor(Math.random() * 10000)}`
},
{
sourceField: 'email',
targetField: 'email',
dataType: 'string',
required: true,
validator: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
fallbackGenerator: () => `user${Date.now()}@example.com`
},
{
sourceField: 'role',
targetField: 'role',
dataType: 'string',
required: false,
defaultValue: 'user'
},
{
sourceField: 'active',
targetField: 'is_active',
dataType: 'boolean',
required: false,
defaultValue: true
}
],
/**
* Create mapping for products
*/
products: () => [
{
sourceField: 'name',
targetField: 'name',
dataType: 'string',
required: true,
fallbackGenerator: () => `Product ${Date.now()}`
},
{
sourceField: 'description',
targetField: 'description',
dataType: 'string',
required: false
},
{
sourceField: 'price',
targetField: 'price',
dataType: 'number',
required: true,
validator: (value) => typeof value === 'number' && value >= 0,
fallbackGenerator: () => Math.floor(Math.random() * 100) + 1
},
{
sourceField: 'category',
targetField: 'category',
dataType: 'string',
required: false,
defaultValue: 'uncategorized'
},
{
sourceField: 'available',
targetField: 'is_available',
dataType: 'boolean',
required: false,
defaultValue: true
}
],
/**
* Create mapping for generic content
*/
content: () => [
{
sourceField: 'title',
targetField: 'title',
dataType: 'string',
required: true,
fallbackGenerator: () => `Content ${Date.now()}`
},
{
sourceField: '$content',
targetField: 'body',
dataType: 'string',
required: false
},
{
sourceField: '$type',
targetField: 'content_type',
dataType: 'string',
required: false
},
{
sourceField: '$lastmodified',
targetField: 'updated_at',
dataType: 'date',
required: false,
transformer: (value) => value || new Date()
}
]
};
/**
* Create common transformers for field values
*/
AssetDataMapper.createTransformers = {
/**
* Slugify a string for URL-safe identifiers
*/
slugify: (value) => {
return String(value)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
},
/**
* Truncate text to specified length
*/
truncate: (maxLength) => (value) => {
const str = String(value);
return str.length > maxLength ? str.substring(0, maxLength - 3) + '...' : str;
},
/**
* Extract first N words
*/
firstWords: (count) => (value) => {
return String(value).split(/\s+/).slice(0, count).join(' ');
},
/**
* Format date to ISO string
*/
toISOString: (value) => {
const date = new Date(value);
return isNaN(date.getTime()) ? null : date.toISOString();
},
/**
* Clean HTML tags from text
*/
stripHtml: (value) => {
return String(value).replace(/<[^>]*>/g, '');
},
/**
* Convert markdown to plain text (simple)
*/
markdownToText: (value) => {
return String(value)
.replace(/#+\s/g, '') // Remove headers
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold
.replace(/\*(.*?)\*/g, '$1') // Remove italic
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // Remove links
.replace(/`(.*?)`/g, '$1'); // Remove code
}
};
//# sourceMappingURL=asset-data-mapper.js.map