Add field history scaffolding with implementation for password field (#1404)

This commit is contained in:
Leendert de Borst
2025-12-04 15:35:54 +01:00
parent 044f7dd2c5
commit 14591f55a1
31 changed files with 2062 additions and 43 deletions

View File

@@ -1,11 +1,16 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
import PasswordHistoryModal from './PasswordHistoryModal';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import type { ItemField } from '@/utils/dist/shared/models/vault';
import { getSystemField } from '@/utils/dist/shared/models/vault';
interface FieldBlockProps {
field: ItemField;
itemId?: string;
}
/**
@@ -24,7 +29,30 @@ const convertUrlsToLinks = (text: string): string => {
* Dynamic field block component that renders based on field type.
* Uses the same FormInputCopyToClipboard component as existing credential blocks.
*/
const FieldBlock: React.FC<FieldBlockProps> = ({ field }) => {
const FieldBlock: React.FC<FieldBlockProps> = ({ field, itemId }) => {
const { t } = useTranslation();
const dbContext = useDb();
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [historyCount, setHistoryCount] = useState<number>(0);
// Check if this field supports history
const systemField = !field.FieldKey.startsWith('custom_') ? getSystemField(field.FieldKey) : null;
const hasHistoryEnabled = systemField?.EnableHistory === true;
// Check if there's actual history available
useEffect(() => {
console.log('[FieldBlock] useEffect triggered - hasHistoryEnabled:', hasHistoryEnabled, 'itemId:', itemId, 'fieldKey:', field.FieldKey);
if (hasHistoryEnabled && itemId && dbContext?.sqliteClient) {
try {
const history = dbContext.sqliteClient.getFieldHistory(itemId, field.FieldKey);
console.log('[FieldBlock] History retrieved:', history, 'count:', history.length);
setHistoryCount(history.length);
} catch (error) {
console.error('[FieldBlock] Error checking history:', error);
}
}
}, [hasHistoryEnabled, itemId, field.FieldKey, dbContext?.sqliteClient]);
// Skip rendering if no value
if (!field.Value || (typeof field.Value === 'string' && field.Value.trim() === '')) {
return null;
@@ -56,12 +84,62 @@ const FieldBlock: React.FC<FieldBlockProps> = ({ field }) => {
case 'Password':
case 'Hidden':
return (
<FormInputCopyToClipboard
id={field.FieldKey}
label={field.Label}
value={value}
type="password"
/>
<>
<div>
<label htmlFor={field.FieldKey} className="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{field.Label}
{historyCount > 0 && itemId && (
<button
type="button"
onClick={() => setShowHistoryModal(true)}
className="ml-2 inline-flex items-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 focus:outline-none"
title={t('credentials.viewHistory')}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</button>
)}
</label>
<div className="relative">
<input
type="password"
id={field.FieldKey}
readOnly
value={value}
className="w-full px-3 py-2.5 bg-white border border-gray-300 text-gray-900 text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(value);
} catch (err) {
console.error('Failed to copy:', err);
}
}}
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
title={t('common.copyToClipboard')}
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
</div>
</div>
</div>
{showHistoryModal && itemId && (
<PasswordHistoryModal
isOpen={showHistoryModal}
onClose={() => setShowHistoryModal(false)}
itemId={itemId}
fieldKey={field.FieldKey}
fieldLabel={field.Label}
/>
)}
</>
);
case 'TextArea':

View File

@@ -0,0 +1,198 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import type { FieldHistory } from '@/utils/dist/shared/models/vault';
interface PasswordHistoryModalProps {
isOpen: boolean;
onClose: () => void;
itemId: string;
fieldKey: string;
fieldLabel: string;
}
/**
* Modal component for displaying password history.
* Shows historical values with dates, passwords hidden by default.
*/
const PasswordHistoryModal: React.FC<PasswordHistoryModalProps> = ({
isOpen,
onClose,
itemId,
fieldKey,
fieldLabel
}) => {
const { t } = useTranslation();
const dbContext = useDb();
const [history, setHistory] = useState<FieldHistory[]>([]);
const [visiblePasswords, setVisiblePasswords] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!isOpen || !dbContext?.sqliteClient) {
return;
}
try {
setLoading(true);
const historyRecords = dbContext.sqliteClient.getFieldHistory(itemId, fieldKey);
setHistory(historyRecords);
} catch (error) {
console.error('Error loading field history:', error);
} finally {
setLoading(false);
}
}, [isOpen, dbContext?.sqliteClient, itemId, fieldKey]);
if (!isOpen) {
return null;
}
const togglePasswordVisibility = (historyId: string): void => {
setVisiblePasswords(prev => {
const newSet = new Set(prev);
if (newSet.has(historyId)) {
newSet.delete(historyId);
} else {
newSet.add(historyId);
}
return newSet;
});
};
const copyToClipboard = async (value: string): Promise<void> => {
try {
await navigator.clipboard.writeText(value);
} catch (error) {
console.error('Failed to copy to clipboard:', error);
}
};
const formatDate = (dateString: string): string => {
try {
const date = new Date(dateString);
return date.toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateString;
}
};
const parseValueSnapshot = (snapshot: string): string[] => {
try {
return JSON.parse(snapshot);
} catch {
return [snapshot];
}
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={onClose} />
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-2xl max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
{fieldLabel} {t('credentials.history')}
</h3>
<button
type="button"
className="text-gray-400 hover:text-gray-500 focus:outline-none"
onClick={onClose}
>
<span className="sr-only">{t('common.close')}</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="overflow-y-auto flex-1">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
</div>
) : history.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
{t('credentials.noHistoryAvailable')}
</div>
) : (
<div className="space-y-3">
{history.map((record) => {
const values = parseValueSnapshot(record.ValueSnapshot);
const isVisible = visiblePasswords.has(record.Id);
return (
<div
key={record.Id}
className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 border border-gray-200 dark:border-gray-600"
>
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-gray-600 dark:text-gray-400">
{formatDate(record.ChangedAt)}
</div>
<button
type="button"
onClick={() => togglePasswordVisibility(record.Id)}
className="text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 focus:outline-none"
>
{isVisible ? t('common.hide') : t('common.show')}
</button>
</div>
{values.map((value, idx) => (
<div
key={idx}
className="flex items-center gap-2 mt-2"
>
<div className="flex-1 font-mono text-sm bg-white dark:bg-gray-800 rounded px-3 py-2 border border-gray-200 dark:border-gray-600">
{isVisible ? value : '\u2022'.repeat(12)}
</div>
<button
type="button"
onClick={() => copyToClipboard(value)}
className="p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 focus:outline-none"
title={t('common.copyToClipboard')}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
</button>
</div>
))}
</div>
);
})}
</div>
)}
</div>
{/* Footer */}
<div className="mt-4 flex justify-end">
<button
type="button"
className="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none"
onClick={onClose}
>
{t('common.close')}
</button>
</div>
</div>
</div>
</div>
);
};
export default PasswordHistoryModal;

View File

@@ -184,7 +184,7 @@ const ItemDetails: React.FC = (): React.ReactElement => {
{t('credentials.loginCredentials')}
</h2>
{groupedFields.Login.map((field) => (
<FieldBlock key={field.FieldKey} field={field} />
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
))}
</div>
)}
@@ -195,7 +195,7 @@ const ItemDetails: React.FC = (): React.ReactElement => {
{t('credentials.alias')}
</h2>
{groupedFields.Alias.map((field) => (
<FieldBlock key={field.FieldKey} field={field} />
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
))}
</div>
)}
@@ -206,7 +206,7 @@ const ItemDetails: React.FC = (): React.ReactElement => {
{t('credentials.cardInformation')}
</h2>
{groupedFields.Card.map((field) => (
<FieldBlock key={field.FieldKey} field={field} />
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
))}
</div>
)}
@@ -217,7 +217,7 @@ const ItemDetails: React.FC = (): React.ReactElement => {
{t('credentials.identityInformation')}
</h2>
{groupedFields.Identity.map((field) => (
<FieldBlock key={field.FieldKey} field={field} />
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
))}
</div>
)}
@@ -228,7 +228,7 @@ const ItemDetails: React.FC = (): React.ReactElement => {
{t('common.customFields')}
</h2>
{groupedFields.Custom.map((field) => (
<FieldBlock key={field.FieldKey} field={field} />
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
))}
</div>
)}

View File

@@ -65,6 +65,8 @@
"openInNewWindow": "Open in new window",
"enabled": "Enabled",
"disabled": "Disabled",
"show": "Show",
"hide": "Hide",
"showPassword": "Show password",
"hidePassword": "Hide password",
"showDetails": "Show details",
@@ -244,7 +246,11 @@
"useDomainChooser": "Use domain chooser",
"enterCustomDomain": "Enter custom domain",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix"
"enterEmailPrefix": "Enter email prefix",
"viewHistory": "View history",
"history": "History",
"noHistoryAvailable": "No history available",
"tags": "Tags"
},
"itemTypes": {
"selectType": "Add New Item",

View File

@@ -1,9 +1,9 @@
import initSqlJs, { Database } from 'sql.js';
import * as dateFormatter from '@/utils/dateFormatter';
import type { Credential, EncryptionKey, PasswordSettings, TotpCode, Passkey, Item, ItemField, ItemTagRef, FieldType } from '@/utils/dist/shared/models/vault';
import type { Credential, EncryptionKey, PasswordSettings, TotpCode, Passkey, Item, ItemField, ItemTagRef, FieldType, FieldHistory } from '@/utils/dist/shared/models/vault';
import type { Attachment } from '@/utils/dist/shared/models/vault';
import { FieldKey, SystemFieldRegistry, getSystemField } from '@/utils/dist/shared/models/vault';
import { FieldKey, SystemFieldRegistry, getSystemField, MAX_FIELD_HISTORY_RECORDS } from '@/utils/dist/shared/models/vault';
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
import { VaultSqlGenerator, checkVersionCompatibility, extractVersionFromMigrationId } from '@/utils/dist/shared/vault-sql';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
@@ -2116,7 +2116,10 @@ export class SqliteClient {
item.Id
]);
// 2. Delete all existing FieldValues for this item
// 2. Track history for fields that have EnableHistory=true before deleting
await this.trackFieldHistory(item.Id, item.Fields, currentDateTime);
// 3. Delete all existing FieldValues for this item
const deleteFieldsQuery = `
UPDATE FieldValues
SET IsDeleted = 1,
@@ -2125,7 +2128,7 @@ export class SqliteClient {
this.executeUpdate(deleteFieldsQuery, [currentDateTime, item.Id]);
// 3. Insert new FieldValues
// 4. Insert new FieldValues
if (item.Fields && item.Fields.length > 0) {
for (const field of item.Fields) {
// Skip empty fields
@@ -2228,6 +2231,174 @@ export class SqliteClient {
}
}
/**
* Track field history for fields that have EnableHistory=true.
* This method should be called before updating/deleting field values.
* @param itemId - The ID of the item
* @param newFields - The new field values
* @param currentDateTime - The current timestamp
*/
private async trackFieldHistory(itemId: string, newFields: ItemField[], currentDateTime: string): Promise<void> {
// Get existing field values from database
const existingFieldsQuery = `
SELECT FieldKey, Value
FROM FieldValues
WHERE ItemId = ? AND IsDeleted = 0 AND FieldKey IS NOT NULL`;
const existingFields = this.executeQuery<{FieldKey: string, Value: string}>(existingFieldsQuery, [itemId]);
console.log('[History] Existing fields from DB:', existingFields);
// Create a map of existing values by FieldKey
const existingValuesMap: {[key: string]: string[]} = {};
existingFields.forEach(field => {
if (!existingValuesMap[field.FieldKey]) {
existingValuesMap[field.FieldKey] = [];
}
existingValuesMap[field.FieldKey].push(field.Value);
});
console.log('[History] Existing values map:', existingValuesMap);
// Check each new field to see if it has EnableHistory and if the value changed
for (const newField of newFields) {
console.log('[History] Checking field:', newField.FieldKey, 'Value:', newField.Value);
// Skip custom fields (only track system fields for now)
if (newField.FieldKey.startsWith('custom_')) {
console.log('[History] Skipping custom field:', newField.FieldKey);
continue;
}
// Get system field definition
const systemField = getSystemField(newField.FieldKey);
console.log('[History] System field definition:', systemField);
if (!systemField || !systemField.EnableHistory) {
console.log('[History] Field does not have EnableHistory:', newField.FieldKey);
continue;
}
// Get old and new values
const oldValues = existingValuesMap[newField.FieldKey] || [];
const newValues = Array.isArray(newField.Value) ? newField.Value : [newField.Value];
console.log('[History] Old values:', oldValues, 'New values:', newValues);
// Check if values changed
const valuesChanged = oldValues.length !== newValues.length ||
!oldValues.every((val, idx) => val === newValues[idx]);
console.log('[History] Values changed?', valuesChanged, 'Has old values?', oldValues.length > 0);
if (valuesChanged && oldValues.length > 0) {
console.log('[History] Creating history record for:', newField.FieldKey);
// Create history record for the old value
const historyId = crypto.randomUUID().toUpperCase();
// Store just the values as JSON array
const valueSnapshot = JSON.stringify(oldValues);
const historyQuery = `
INSERT INTO FieldHistories (Id, ItemId, FieldDefinitionId, FieldKey, ValueSnapshot, ChangedAt, CreatedAt, UpdatedAt, IsDeleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
this.executeUpdate(historyQuery, [
historyId,
itemId,
null, // FieldDefinitionId is NULL for system fields
newField.FieldKey, // FieldKey for system fields
valueSnapshot,
currentDateTime,
currentDateTime,
currentDateTime,
0
]);
// Prune old history records if we exceed the limit
await this.pruneFieldHistory(itemId, newField.FieldKey, currentDateTime);
}
}
}
/**
* Prune old field history records, keeping only the most recent MAX_FIELD_HISTORY_RECORDS.
* @param itemId - The ID of the item
* @param fieldKey - The field key to prune history for
* @param currentDateTime - The current timestamp
*/
private async pruneFieldHistory(itemId: string, fieldKey: string, currentDateTime: string): Promise<void> {
// Get all history records for this field
const historyQuery = `
SELECT Id, ChangedAt
FROM FieldHistories
WHERE ItemId = ? AND FieldKey = ? AND IsDeleted = 0
ORDER BY ChangedAt DESC`;
const matchingHistory = this.executeQuery<{Id: string, ChangedAt: string}>(historyQuery, [itemId, fieldKey]);
if (matchingHistory.length > MAX_FIELD_HISTORY_RECORDS) {
// Soft delete the oldest records beyond the limit
const recordsToDelete = matchingHistory.slice(MAX_FIELD_HISTORY_RECORDS);
const idsToDelete = recordsToDelete.map(r => r.Id);
if (idsToDelete.length > 0) {
const placeholders = idsToDelete.map(() => '?').join(',');
const deleteQuery = `
UPDATE FieldHistories
SET IsDeleted = 1, UpdatedAt = ?
WHERE Id IN (${placeholders})`;
this.executeUpdate(deleteQuery, [currentDateTime, ...idsToDelete]);
}
}
}
/**
* Get field history for a specific field.
* Returns history records ordered by ChangedAt descending (most recent first).
* @param itemId - The ID of the item
* @param fieldKey - The field key to get history for
* @returns Array of field history records
*/
public getFieldHistory(itemId: string, fieldKey: string): FieldHistory[] {
if (!this.db) {
throw new Error('Database not initialized');
}
console.log('[History] Getting history for itemId:', itemId, 'fieldKey:', fieldKey);
const query = `
SELECT
Id,
ItemId,
FieldKey,
ValueSnapshot,
ChangedAt,
CreatedAt,
UpdatedAt
FROM FieldHistories
WHERE ItemId = ? AND FieldKey = ? AND IsDeleted = 0
ORDER BY ChangedAt DESC
LIMIT ?`;
const results = this.executeQuery<{
Id: string;
ItemId: string;
FieldKey: string;
ValueSnapshot: string;
ChangedAt: string;
CreatedAt: string;
UpdatedAt: string;
}>(query, [itemId, fieldKey, MAX_FIELD_HISTORY_RECORDS]);
console.log('[History] History records found:', results);
return results.map(row => ({
Id: row.Id,
ItemId: row.ItemId,
FieldKey: row.FieldKey,
ValueSnapshot: row.ValueSnapshot,
ChangedAt: row.ChangedAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt
}));
}
/**
* Delete an item by ID (soft delete)
* @param itemId - The ID of the item to delete

View File

@@ -452,4 +452,30 @@ declare function getAllSystemFieldKeys(): string[];
*/
declare function isSystemFieldPrefix(fieldKey: string): boolean;
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldKey, type FieldKeyValue, type FieldType, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };
/**
* Field history record tracking changes to field values over time.
* Used for fields that have EnableHistory=true (e.g., passwords).
*/
type FieldHistory = {
/** Unique identifier for this history record */
Id: string;
/** ID of the item this history belongs to */
ItemId: string;
/** Field key (e.g., 'login.password') */
FieldKey: string;
/** Snapshot of the field value(s) at this point in time */
ValueSnapshot: string;
/** When this change occurred */
ChangedAt: string;
/** When this history record was created */
CreatedAt: string;
/** When this history record was last updated */
UpdatedAt: string;
};
/**
* Maximum number of history records to keep per field.
* Older records beyond this limit should be automatically pruned.
*/
declare const MAX_FIELD_HISTORY_RECORDS = 10;
export { type Alias, type Attachment, type Credential, type EncryptionKey, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };

View File

@@ -361,6 +361,9 @@ function isSystemFieldPrefix(fieldKey) {
return fieldKey.startsWith("login.") || fieldKey.startsWith("alias.") || fieldKey.startsWith("card.") || fieldKey.startsWith("identity.") || fieldKey.startsWith("api.") || fieldKey.startsWith("note.");
}
export { FieldKey, SystemFieldRegistry, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };
// src/vault/FieldHistory.ts
var MAX_FIELD_HISTORY_RECORDS = 10;
export { FieldKey, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1165,6 +1165,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
var MIGRATION_SCRIPTS = {
1: `\uFEFFBEGIN TRANSACTION;
@@ -2230,7 +2274,50 @@ CREATE INDEX "IX_TotpCodes_ItemId" ON "TotpCodes" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `\uFEFFBEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`
};
// src/sql/VaultVersions.ts
@@ -2318,6 +2405,13 @@ var VAULT_VERSIONS = [
description: "Update to Field-Based Data Model",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
},
{
revision: 13,
version: "1.7.1",
description: "Make FieldHistory Flexible",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
}
];

View File

@@ -1133,6 +1133,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
var MIGRATION_SCRIPTS = {
1: `\uFEFFBEGIN TRANSACTION;
@@ -2198,7 +2242,50 @@ CREATE INDEX "IX_TotpCodes_ItemId" ON "TotpCodes" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `\uFEFFBEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`
};
// src/sql/VaultVersions.ts
@@ -2286,6 +2373,13 @@ var VAULT_VERSIONS = [
description: "Update to Field-Based Data Model",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
},
{
revision: 13,
version: "1.7.1",
description: "Make FieldHistory Flexible",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
}
];

View File

@@ -452,4 +452,30 @@ declare function getAllSystemFieldKeys(): string[];
*/
declare function isSystemFieldPrefix(fieldKey: string): boolean;
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldKey, type FieldKeyValue, type FieldType, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };
/**
* Field history record tracking changes to field values over time.
* Used for fields that have EnableHistory=true (e.g., passwords).
*/
type FieldHistory = {
/** Unique identifier for this history record */
Id: string;
/** ID of the item this history belongs to */
ItemId: string;
/** Field key (e.g., 'login.password') */
FieldKey: string;
/** Snapshot of the field value(s) at this point in time */
ValueSnapshot: string;
/** When this change occurred */
ChangedAt: string;
/** When this history record was created */
CreatedAt: string;
/** When this history record was last updated */
UpdatedAt: string;
};
/**
* Maximum number of history records to keep per field.
* Older records beyond this limit should be automatically pruned.
*/
declare const MAX_FIELD_HISTORY_RECORDS = 10;
export { type Alias, type Attachment, type Credential, type EncryptionKey, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };

View File

@@ -361,6 +361,9 @@ function isSystemFieldPrefix(fieldKey) {
return fieldKey.startsWith("login.") || fieldKey.startsWith("alias.") || fieldKey.startsWith("card.") || fieldKey.startsWith("identity.") || fieldKey.startsWith("api.") || fieldKey.startsWith("note.");
}
export { FieldKey, SystemFieldRegistry, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };
// src/vault/FieldHistory.ts
var MAX_FIELD_HISTORY_RECORDS = 10;
export { FieldKey, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, getAllSystemFieldKeys, getFieldValue, getFieldValues, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isSystemField, isSystemFieldPrefix, itemToCredential };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1165,6 +1165,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
var MIGRATION_SCRIPTS = {
1: `\uFEFFBEGIN TRANSACTION;
@@ -2230,7 +2274,50 @@ CREATE INDEX "IX_TotpCodes_ItemId" ON "TotpCodes" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `\uFEFFBEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`
};
// src/sql/VaultVersions.ts
@@ -2318,6 +2405,13 @@ var VAULT_VERSIONS = [
description: "Update to Field-Based Data Model",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
},
{
revision: 13,
version: "1.7.1",
description: "Make FieldHistory Flexible",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
}
];

View File

@@ -1133,6 +1133,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
var MIGRATION_SCRIPTS = {
1: `\uFEFFBEGIN TRANSACTION;
@@ -2198,7 +2242,50 @@ CREATE INDEX "IX_TotpCodes_ItemId" ON "TotpCodes" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `\uFEFFBEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`
};
// src/sql/VaultVersions.ts
@@ -2286,6 +2373,13 @@ var VAULT_VERSIONS = [
description: "Update to Field-Based Data Model",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
},
{
revision: 13,
version: "1.7.1",
description: "Make FieldHistory Flexible",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
}
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1165,6 +1165,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
var MIGRATION_SCRIPTS = {
1: `\uFEFFBEGIN TRANSACTION;
@@ -2230,7 +2274,50 @@ CREATE INDEX "IX_TotpCodes_ItemId" ON "TotpCodes" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `\uFEFFBEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`
};
// src/sql/VaultVersions.ts
@@ -2318,6 +2405,13 @@ var VAULT_VERSIONS = [
description: "Update to Field-Based Data Model",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
},
{
revision: 13,
version: "1.7.1",
description: "Make FieldHistory Flexible",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
}
];

View File

@@ -1133,6 +1133,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
var MIGRATION_SCRIPTS = {
1: `\uFEFFBEGIN TRANSACTION;
@@ -2198,7 +2242,50 @@ CREATE INDEX "IX_TotpCodes_ItemId" ON "TotpCodes" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `\uFEFFBEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`
};
// src/sql/VaultVersions.ts
@@ -2286,6 +2373,13 @@ var VAULT_VERSIONS = [
description: "Update to Field-Based Data Model",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
},
{
revision: 13,
version: "1.7.1",
description: "Make FieldHistory Flexible",
releaseVersion: "0.26.0",
compatibleUpToVersion: "0.26.0"
}
];

View File

@@ -35,19 +35,30 @@ public class FieldHistory : SyncableEntity
public virtual Item Item { get; set; } = null!;
/// <summary>
/// Gets or sets the field definition ID.
/// Gets or sets the field definition ID for custom (user-defined) fields.
/// NULL for system fields (which use FieldKey instead).
/// </summary>
[Required]
public Guid FieldDefinitionId { get; set; }
public Guid? FieldDefinitionId { get; set; }
/// <summary>
/// Gets or sets the field definition object.
/// Gets or sets the field definition object for custom fields.
/// NULL for system fields.
/// </summary>
[ForeignKey("FieldDefinitionId")]
public virtual FieldDefinition FieldDefinition { get; set; } = null!;
public virtual FieldDefinition? FieldDefinition { get; set; }
/// <summary>
/// Gets or sets the value snapshot as JSON (e.g., '{"values":["hunter2"]}').
/// Gets or sets the system field key for predefined fields (e.g., 'login.password').
/// NULL for custom (user-defined) fields (which use FieldDefinitionId instead).
/// System field metadata is defined in code (SystemFieldRegistry), not in the database.
/// Note: Exactly one of FieldKey or FieldDefinitionId must be non-null.
/// </summary>
[StringLength(100)]
public string? FieldKey { get; set; }
/// <summary>
/// Gets or sets the value snapshot as JSON (e.g., '["oldpassword"]').
/// For multi-value fields, this stores an array of values.
/// </summary>
[Required]
public string ValueSnapshot { get; set; } = string.Empty;

View File

@@ -0,0 +1,672 @@
// <auto-generated />
using System;
using AliasClientDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AliasClientDb.Migrations
{
[DbContext(typeof(AliasClientDbContext))]
[Migration("20251204142538_1.7.1-MakeFieldHistoryFlexible")]
partial class _171MakeFieldHistoryFlexible
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasClientDb.Attachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Filename")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ItemId");
b.ToTable("Attachments");
});
modelBuilder.Entity("AliasClientDb.EncryptionKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<bool>("IsPrimary")
.HasColumnType("INTEGER");
b.Property<string>("PrivateKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("PublicKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("EncryptionKeys");
});
modelBuilder.Entity("AliasClientDb.FieldDefinition", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicableToTypes")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("EnableHistory")
.HasColumnType("INTEGER");
b.Property<string>("FieldType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<bool>("IsHidden")
.HasColumnType("INTEGER");
b.Property<bool>("IsMultiValue")
.HasColumnType("INTEGER");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<int>("Weight")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("FieldDefinitions");
});
modelBuilder.Entity("AliasClientDb.FieldHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("ChangedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid?>("FieldDefinitionId")
.HasColumnType("TEXT");
b.Property<string>("FieldKey")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("ValueSnapshot")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FieldDefinitionId");
b.HasIndex("ItemId");
b.ToTable("FieldHistories");
});
modelBuilder.Entity("AliasClientDb.FieldValue", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid?>("FieldDefinitionId")
.HasColumnType("TEXT");
b.Property<string>("FieldKey")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.Property<int>("Weight")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("FieldDefinitionId");
b.HasIndex("FieldKey");
b.HasIndex("ItemId");
b.HasIndex("ItemId", "FieldKey");
b.HasIndex("ItemId", "FieldDefinitionId", "Weight");
b.ToTable("FieldValues");
});
modelBuilder.Entity("AliasClientDb.Folder", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<Guid?>("ParentFolderId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<int>("Weight")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ParentFolderId");
b.ToTable("Folders");
});
modelBuilder.Entity("AliasClientDb.Item", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid?>("FolderId")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<string>("ItemType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<Guid?>("LogoId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("FolderId");
b.HasIndex("LogoId");
b.ToTable("Items");
});
modelBuilder.Entity("AliasClientDb.ItemTag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<Guid>("TagId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ItemId");
b.HasIndex("TagId");
b.HasIndex("ItemId", "TagId")
.IsUnique();
b.ToTable("ItemTags");
});
modelBuilder.Entity("AliasClientDb.Logo", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime?>("FetchedAt")
.HasColumnType("TEXT");
b.Property<byte[]>("FileData")
.HasColumnType("BLOB");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<string>("MimeType")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Source")
.IsUnique();
b.ToTable("Logos");
});
modelBuilder.Entity("AliasClientDb.Passkey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<byte[]>("AdditionalData")
.HasColumnType("BLOB");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<byte[]>("PrfKey")
.HasMaxLength(64)
.HasColumnType("BLOB");
b.Property<string>("PrivateKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PublicKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("RpId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<byte[]>("UserHandle")
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Id");
b.HasIndex("ItemId");
b.HasIndex("RpId");
b.ToTable("Passkeys");
});
modelBuilder.Entity("AliasClientDb.Setting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("Settings");
});
modelBuilder.Entity("AliasClientDb.Tag", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Color")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("DisplayOrder")
.HasColumnType("INTEGER");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name");
b.ToTable("Tags");
});
modelBuilder.Entity("AliasClientDb.TotpCode", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("SecretKey")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ItemId");
b.ToTable("TotpCodes");
});
modelBuilder.Entity("AliasClientDb.Attachment", b =>
{
b.HasOne("AliasClientDb.Item", "Item")
.WithMany("Attachments")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("AliasClientDb.FieldHistory", b =>
{
b.HasOne("AliasClientDb.FieldDefinition", "FieldDefinition")
.WithMany("FieldHistories")
.HasForeignKey("FieldDefinitionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("AliasClientDb.Item", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("FieldDefinition");
b.Navigation("Item");
});
modelBuilder.Entity("AliasClientDb.FieldValue", b =>
{
b.HasOne("AliasClientDb.FieldDefinition", "FieldDefinition")
.WithMany("FieldValues")
.HasForeignKey("FieldDefinitionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("AliasClientDb.Item", "Item")
.WithMany("FieldValues")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("FieldDefinition");
b.Navigation("Item");
});
modelBuilder.Entity("AliasClientDb.Folder", b =>
{
b.HasOne("AliasClientDb.Folder", "ParentFolder")
.WithMany("ChildFolders")
.HasForeignKey("ParentFolderId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentFolder");
});
modelBuilder.Entity("AliasClientDb.Item", b =>
{
b.HasOne("AliasClientDb.Folder", "Folder")
.WithMany("Items")
.HasForeignKey("FolderId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("AliasClientDb.Logo", "Logo")
.WithMany("Items")
.HasForeignKey("LogoId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Folder");
b.Navigation("Logo");
});
modelBuilder.Entity("AliasClientDb.ItemTag", b =>
{
b.HasOne("AliasClientDb.Item", "Item")
.WithMany("ItemTags")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AliasClientDb.Tag", "Tag")
.WithMany("ItemTags")
.HasForeignKey("TagId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
b.Navigation("Tag");
});
modelBuilder.Entity("AliasClientDb.Passkey", b =>
{
b.HasOne("AliasClientDb.Item", "Item")
.WithMany("Passkeys")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("AliasClientDb.TotpCode", b =>
{
b.HasOne("AliasClientDb.Item", "Item")
.WithMany("TotpCodes")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("AliasClientDb.FieldDefinition", b =>
{
b.Navigation("FieldHistories");
b.Navigation("FieldValues");
});
modelBuilder.Entity("AliasClientDb.Folder", b =>
{
b.Navigation("ChildFolders");
b.Navigation("Items");
});
modelBuilder.Entity("AliasClientDb.Item", b =>
{
b.Navigation("Attachments");
b.Navigation("FieldValues");
b.Navigation("ItemTags");
b.Navigation("Passkeys");
b.Navigation("TotpCodes");
});
modelBuilder.Entity("AliasClientDb.Logo", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("AliasClientDb.Tag", b =>
{
b.Navigation("ItemTags");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,49 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasClientDb.Migrations
{
/// <inheritdoc />
public partial class _171MakeFieldHistoryFlexible : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<Guid>(
name: "FieldDefinitionId",
table: "FieldHistories",
type: "TEXT",
nullable: true,
oldClrType: typeof(Guid),
oldType: "TEXT");
migrationBuilder.AddColumn<string>(
name: "FieldKey",
table: "FieldHistories",
type: "TEXT",
maxLength: 100,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FieldKey",
table: "FieldHistories");
migrationBuilder.AlterColumn<Guid>(
name: "FieldDefinitionId",
table: "FieldHistories",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@@ -145,7 +145,11 @@ namespace AliasClientDb.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("FieldDefinitionId")
b.Property<Guid?>("FieldDefinitionId")
.HasColumnType("TEXT");
b.Property<string>("FieldKey")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
@@ -524,8 +528,7 @@ namespace AliasClientDb.Migrations
b.HasOne("AliasClientDb.FieldDefinition", "FieldDefinition")
.WithMany("FieldHistories")
.HasForeignKey("FieldDefinitionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("AliasClientDb.Item", "Item")
.WithMany()

View File

@@ -1128,3 +1128,47 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');

View File

@@ -0,0 +1,44 @@
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');

View File

@@ -0,0 +1,26 @@
/**
* Field history record tracking changes to field values over time.
* Used for fields that have EnableHistory=true (e.g., passwords).
*/
export type FieldHistory = {
/** Unique identifier for this history record */
Id: string;
/** ID of the item this history belongs to */
ItemId: string;
/** Field key (e.g., 'login.password') */
FieldKey: string;
/** Snapshot of the field value(s) at this point in time */
ValueSnapshot: string;
/** When this change occurred */
ChangedAt: string;
/** When this history record was created */
CreatedAt: string;
/** When this history record was last updated */
UpdatedAt: string;
}
/**
* Maximum number of history records to keep per field.
* Older records beyond this limit should be automatically pruned.
*/
export const MAX_FIELD_HISTORY_RECORDS = 10;

View File

@@ -14,3 +14,4 @@ export * from './Item';
export * from './CredentialCompat';
export * from './ItemMethods';
export * from './SystemFieldRegistry';
export * from './FieldHistory';

View File

@@ -1134,6 +1134,50 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');
BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');
`;
/**
* Individual migration SQL scripts
@@ -2204,4 +2248,47 @@ COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251203162345_1.7.0-FieldBasedDataModelUpdate', '9.0.4');`,
12: `BEGIN TRANSACTION;
ALTER TABLE "FieldHistories" ADD "FieldKey" TEXT NULL;
CREATE TABLE "ef_temp_FieldHistories" (
"Id" TEXT NOT NULL CONSTRAINT "PK_FieldHistories" PRIMARY KEY,
"ChangedAt" TEXT NOT NULL,
"CreatedAt" TEXT NOT NULL,
"FieldDefinitionId" TEXT NULL,
"FieldKey" TEXT NULL,
"IsDeleted" INTEGER NOT NULL,
"ItemId" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
"ValueSnapshot" TEXT NOT NULL,
CONSTRAINT "FK_FieldHistories_FieldDefinitions_FieldDefinitionId" FOREIGN KEY ("FieldDefinitionId") REFERENCES "FieldDefinitions" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_FieldHistories_Items_ItemId" FOREIGN KEY ("ItemId") REFERENCES "Items" ("Id") ON DELETE CASCADE
);
INSERT INTO "ef_temp_FieldHistories" ("Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot")
SELECT "Id", "ChangedAt", "CreatedAt", "FieldDefinitionId", "FieldKey", "IsDeleted", "ItemId", "UpdatedAt", "ValueSnapshot"
FROM "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "FieldHistories";
ALTER TABLE "ef_temp_FieldHistories" RENAME TO "FieldHistories";
COMMIT;
PRAGMA foreign_keys = 1;
BEGIN TRANSACTION;
CREATE INDEX "IX_FieldHistories_FieldDefinitionId" ON "FieldHistories" ("FieldDefinitionId");
CREATE INDEX "IX_FieldHistories_ItemId" ON "FieldHistories" ("ItemId");
COMMIT;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251204142538_1.7.1-MakeFieldHistoryFlexible', '9.0.4');`,
};

View File

@@ -95,4 +95,11 @@ export const VAULT_VERSIONS: VaultVersion[] = [
releaseVersion: '0.26.0',
compatibleUpToVersion: '0.26.0',
},
{
revision: 13,
version: '1.7.1',
description: 'Make FieldHistory Flexible',
releaseVersion: '0.26.0',
compatibleUpToVersion: '0.26.0',
},
];