mirror of
https://github.com/lanedirt/AliasVault.git
synced 2025-12-05 19:07:26 -06:00
Add field history scaffolding with implementation for password field (#1404)
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
672
apps/server/Databases/AliasClientDb/Migrations/20251204142538_1.7.1-MakeFieldHistoryFlexible.Designer.cs
generated
Normal file
672
apps/server/Databases/AliasClientDb/Migrations/20251204142538_1.7.1-MakeFieldHistoryFlexible.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
26
shared/models/src/vault/FieldHistory.ts
Normal file
26
shared/models/src/vault/FieldHistory.ts
Normal 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;
|
||||
@@ -14,3 +14,4 @@ export * from './Item';
|
||||
export * from './CredentialCompat';
|
||||
export * from './ItemMethods';
|
||||
export * from './SystemFieldRegistry';
|
||||
export * from './FieldHistory';
|
||||
|
||||
@@ -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');`,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user