Files

285 lines
12 KiB
Swift

import Foundation
import VaultModels
/// Raw field row from database query.
public struct FieldRow {
public let itemId: String
public let fieldKey: String?
public let fieldDefinitionId: String?
public let customLabel: String?
public let customFieldType: String?
public let customIsHidden: Int64?
public let customEnableHistory: Int64?
public let value: String
public let displayOrder: Int
public init(
itemId: String,
fieldKey: String?,
fieldDefinitionId: String?,
customLabel: String?,
customFieldType: String?,
customIsHidden: Int64?,
customEnableHistory: Int64?,
value: String,
displayOrder: Int
) {
self.itemId = itemId
self.fieldKey = fieldKey
self.fieldDefinitionId = fieldDefinitionId
self.customLabel = customLabel
self.customFieldType = customFieldType
self.customIsHidden = customIsHidden
self.customEnableHistory = customEnableHistory
self.value = value
self.displayOrder = displayOrder
}
/// Initialize from a database row dictionary.
public init?(from row: [String: Any]) {
guard let itemId = row["ItemId"] as? String else { return nil }
self.itemId = itemId
self.fieldKey = row["FieldKey"] as? String
self.fieldDefinitionId = row["FieldDefinitionId"] as? String
self.customLabel = row["CustomLabel"] as? String
self.customFieldType = row["CustomFieldType"] as? String
self.customIsHidden = row["CustomIsHidden"] as? Int64
self.customEnableHistory = row["CustomEnableHistory"] as? Int64
self.value = row["Value"] as? String ?? ""
self.displayOrder = Int(row["DisplayOrder"] as? Int64 ?? 0)
}
}
/// Raw field row for single item queries (without ItemId).
public struct SingleItemFieldRow {
public let fieldKey: String?
public let fieldDefinitionId: String?
public let customLabel: String?
public let customFieldType: String?
public let customIsHidden: Int64?
public let customEnableHistory: Int64?
public let value: String
public let displayOrder: Int
/// Initialize from a database row dictionary.
public init?(from row: [String: Any]) {
self.fieldKey = row["FieldKey"] as? String
self.fieldDefinitionId = row["FieldDefinitionId"] as? String
self.customLabel = row["CustomLabel"] as? String
self.customFieldType = row["CustomFieldType"] as? String
self.customIsHidden = row["CustomIsHidden"] as? Int64
self.customEnableHistory = row["CustomEnableHistory"] as? Int64
self.value = row["Value"] as? String ?? ""
self.displayOrder = Int(row["DisplayOrder"] as? Int64 ?? 0)
}
}
/// Intermediate field representation before grouping.
public struct ProcessedField {
public let itemId: String
public let fieldKey: String
public let label: String
public let fieldType: String
public let isHidden: Bool
public let value: String
public let displayOrder: Int
public let isCustomField: Bool
public let enableHistory: Bool
}
/// Mapper class for processing database field rows into ItemField objects.
/// Handles both system fields (with FieldKey) and custom fields (with FieldDefinitionId).
public struct FieldMapper {
/// Process raw field rows from database into a map of ItemId -> [ItemField].
/// Handles system vs custom fields. Multi-value fields (like URLs) create separate ItemField entries
/// with the same fieldKey but different values, allowing Item.getFieldValues() to return all values.
/// - Parameter rows: Raw field rows from database
/// - Returns: Dictionary of ItemId to array of ItemField objects
public static func processFieldRows(_ rows: [FieldRow]) -> [String: [ItemField]] {
// First, convert rows to processed fields with proper metadata
let processedFields = rows.map { processFieldRow($0) }
// Group fields by ItemId, keeping separate entries for multi-value fields
var fieldsByItem: [String: [ItemField]] = [:]
for field in processedFields {
if fieldsByItem[field.itemId] == nil {
fieldsByItem[field.itemId] = []
}
// Create an ItemField for each row (including duplicates for multi-value fields)
let itemField = ItemField(
fieldKey: field.fieldKey,
label: field.label,
fieldType: field.fieldType,
value: field.value,
isHidden: field.isHidden,
displayOrder: field.displayOrder,
isCustomField: field.isCustomField,
enableHistory: field.enableHistory
)
fieldsByItem[field.itemId]!.append(itemField)
}
return fieldsByItem
}
/// Process a single field row to extract proper metadata.
/// System fields use FieldKey and get metadata from SystemFieldRegistry.
/// Custom fields use FieldDefinitionId and get metadata from the row.
/// - Parameter row: Raw field row
/// - Returns: Processed field with proper metadata
private static func processFieldRow(_ row: FieldRow) -> ProcessedField {
if let fieldKey = row.fieldKey, !fieldKey.isEmpty {
// System field: has FieldKey, get metadata from field metadata resolver
let metadata = resolveFieldMetadata(
fieldKey: fieldKey,
customLabel: nil,
customFieldType: nil,
customIsHidden: false,
customEnableHistory: false,
isCustomField: false
)
return ProcessedField(
itemId: row.itemId,
fieldKey: fieldKey,
label: metadata.label,
fieldType: metadata.fieldType,
isHidden: metadata.isHidden,
value: row.value,
displayOrder: row.displayOrder,
isCustomField: false,
enableHistory: metadata.enableHistory
)
} else {
// Custom field: has FieldDefinitionId, get metadata from FieldDefinitions
let fieldKey = row.fieldDefinitionId ?? ""
return ProcessedField(
itemId: row.itemId,
fieldKey: fieldKey,
label: row.customLabel ?? "",
fieldType: row.customFieldType ?? FieldType.text,
isHidden: row.customIsHidden == 1,
value: row.value,
displayOrder: row.displayOrder,
isCustomField: true,
enableHistory: row.customEnableHistory == 1
)
}
}
/// Process field rows for a single item (without ItemId in result).
/// Used when fetching a single item by ID.
/// Multi-value fields (like URLs) create separate ItemField entries with the same fieldKey.
/// - Parameter rows: Raw field rows for a single item
/// - Returns: Array of ItemField objects
public static func processFieldRowsForSingleItem(_ rows: [SingleItemFieldRow]) -> [ItemField] {
// Create an ItemField for each row (including duplicates for multi-value fields)
return rows.map { row in
let fieldKey = row.fieldKey ?? row.fieldDefinitionId ?? ""
let isCustomField = row.fieldKey == nil || row.fieldKey!.isEmpty
if !isCustomField, let rowFieldKey = row.fieldKey {
// System field
let metadata = resolveFieldMetadata(
fieldKey: rowFieldKey,
customLabel: nil,
customFieldType: nil,
customIsHidden: false,
customEnableHistory: false,
isCustomField: false
)
return ItemField(
fieldKey: rowFieldKey,
label: metadata.label,
fieldType: metadata.fieldType,
value: row.value,
isHidden: metadata.isHidden,
displayOrder: row.displayOrder,
isCustomField: false,
enableHistory: metadata.enableHistory
)
} else {
// Custom field
return ItemField(
fieldKey: fieldKey,
label: row.customLabel ?? "",
fieldType: row.customFieldType ?? FieldType.text,
value: row.value,
isHidden: row.customIsHidden == 1,
displayOrder: row.displayOrder,
isCustomField: true,
enableHistory: row.customEnableHistory == 1
)
}
}.sorted { $0.displayOrder < $1.displayOrder }
}
// MARK: - Field Metadata Resolution
/// Helper struct to hold resolved field metadata.
private struct FieldMetadata {
let label: String
let fieldType: String
let isHidden: Bool
let enableHistory: Bool
}
/// Resolve field metadata for system fields and custom fields.
private static func resolveFieldMetadata(
fieldKey: String,
customLabel: String?,
customFieldType: String?,
customIsHidden: Bool,
customEnableHistory: Bool,
isCustomField: Bool
) -> FieldMetadata {
if isCustomField {
return FieldMetadata(
label: customLabel ?? fieldKey,
fieldType: customFieldType ?? FieldType.text,
isHidden: customIsHidden,
enableHistory: customEnableHistory
)
}
// System field metadata based on FieldKey constants
switch fieldKey {
case FieldKey.loginUsername:
return FieldMetadata(label: "Username", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.loginPassword:
return FieldMetadata(label: "Password", fieldType: FieldType.password, isHidden: true, enableHistory: true)
case FieldKey.loginEmail:
return FieldMetadata(label: "Email", fieldType: FieldType.email, isHidden: false, enableHistory: false)
case FieldKey.loginUrl:
return FieldMetadata(label: "URL", fieldType: FieldType.uRL, isHidden: false, enableHistory: false)
case FieldKey.cardNumber:
return FieldMetadata(label: "Card Number", fieldType: FieldType.text, isHidden: true, enableHistory: false)
case FieldKey.cardCardholderName:
return FieldMetadata(label: "Cardholder Name", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.cardExpiryMonth:
return FieldMetadata(label: "Expiry Month", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.cardExpiryYear:
return FieldMetadata(label: "Expiry Year", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.cardCvv:
return FieldMetadata(label: "CVV", fieldType: FieldType.password, isHidden: true, enableHistory: false)
case FieldKey.cardPin:
return FieldMetadata(label: "PIN", fieldType: FieldType.password, isHidden: true, enableHistory: false)
case FieldKey.aliasFirstName:
return FieldMetadata(label: "First Name", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.aliasLastName:
return FieldMetadata(label: "Last Name", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.aliasGender:
return FieldMetadata(label: "Gender", fieldType: FieldType.text, isHidden: false, enableHistory: false)
case FieldKey.aliasBirthdate:
return FieldMetadata(label: "Birth Date", fieldType: FieldType.date, isHidden: false, enableHistory: false)
case FieldKey.notesContent:
return FieldMetadata(label: "Notes", fieldType: FieldType.textArea, isHidden: false, enableHistory: false)
default:
// Unknown system field - use field key as label
return FieldMetadata(label: fieldKey, fieldType: FieldType.text, isHidden: false, enableHistory: false)
}
}
}