2023-06-13 19:06:16 +02:00
|
|
|
import { ModelType } from '../../BaseModel';
|
|
|
|
import { FolderEntity, NoteEntity, ResourceEntity, TagEntity, UserData, UserDataValue } from '../../services/database/types';
|
|
|
|
import Note from '../Note';
|
|
|
|
import Folder from '../Folder';
|
|
|
|
import Resource from '../Resource';
|
|
|
|
import Tag from '../Tag';
|
|
|
|
import BaseItem from '../BaseItem';
|
|
|
|
import { LoadOptions } from './types';
|
|
|
|
|
|
|
|
const maxKeyLength = 255;
|
|
|
|
|
|
|
|
type SupportedEntity = NoteEntity | ResourceEntity | FolderEntity | TagEntity;
|
|
|
|
|
|
|
|
const unserializeUserData = (s: string): UserData => {
|
|
|
|
if (!s) return {};
|
|
|
|
|
|
|
|
try {
|
|
|
|
const r = JSON.parse(s);
|
|
|
|
return r as UserData;
|
|
|
|
} catch (error) {
|
|
|
|
error.message = `Could not unserialize user data: ${error.message}: ${s}`;
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const serializeUserData = (d: UserData): string => {
|
|
|
|
if (!d) return '';
|
|
|
|
return JSON.stringify(d);
|
|
|
|
};
|
|
|
|
|
2023-06-30 10:11:26 +02:00
|
|
|
export const setUserData = <T>(userData: UserData, namespace: string, key: string, value: T, deleted = false): UserData => {
|
2023-06-13 19:06:16 +02:00
|
|
|
if (key.length > maxKeyLength) new Error(`Key must no be longer than ${maxKeyLength} characters`);
|
|
|
|
if (!(namespace in userData)) userData[namespace] = {};
|
|
|
|
if (key in userData[namespace] && userData[namespace][key].v === value) return userData;
|
|
|
|
|
|
|
|
const newUserDataValue: UserDataValue = {
|
|
|
|
v: value,
|
|
|
|
t: Date.now(),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (deleted) newUserDataValue.d = 1;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...userData,
|
|
|
|
[namespace]: {
|
|
|
|
...userData[namespace],
|
|
|
|
[key]: newUserDataValue,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getUserData = <T>(userData: UserData, namespace: string, key: string): T|undefined => {
|
|
|
|
if (!hasUserData(userData, namespace, key)) return undefined;
|
|
|
|
return userData[namespace][key].v as T;
|
|
|
|
};
|
|
|
|
|
|
|
|
const checkIsSupportedItemType = (itemType: ModelType) => {
|
|
|
|
if (![ModelType.Note, ModelType.Folder, ModelType.Tag, ModelType.Resource].includes(itemType)) {
|
|
|
|
throw new Error(`Unsupported item type: ${itemType}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-06-30 10:11:26 +02:00
|
|
|
export const setItemUserData = async <T>(itemType: ModelType, itemId: string, namespace: string, key: string, value: T, deleted = false): Promise<SupportedEntity> => {
|
2023-06-13 19:06:16 +02:00
|
|
|
checkIsSupportedItemType(itemType);
|
|
|
|
|
|
|
|
interface ItemSlice {
|
|
|
|
user_data: string;
|
|
|
|
updated_time?: number;
|
|
|
|
id?: string;
|
|
|
|
parent_id?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const options: LoadOptions = { fields: ['user_data'] };
|
|
|
|
if (itemType === ModelType.Note) (options.fields as string[]).push('parent_id');
|
|
|
|
|
|
|
|
const item = await BaseItem.loadItem(itemType, itemId, options) as ItemSlice;
|
|
|
|
|
|
|
|
const userData = unserializeUserData(item.user_data);
|
|
|
|
const newUserData = setUserData(userData, namespace, key, value, deleted);
|
|
|
|
|
|
|
|
const itemToSave: ItemSlice = {
|
|
|
|
id: itemId,
|
|
|
|
user_data: serializeUserData(newUserData),
|
|
|
|
updated_time: Date.now(),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (itemType === ModelType.Note) itemToSave.parent_id = item.parent_id;
|
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2023-06-13 19:06:16 +02:00
|
|
|
const saveOptions: any = { autoTimestamp: false };
|
|
|
|
|
|
|
|
if (itemType === ModelType.Note) return Note.save(itemToSave, saveOptions);
|
|
|
|
if (itemType === ModelType.Folder) return Folder.save(itemToSave, saveOptions);
|
|
|
|
if (itemType === ModelType.Resource) return Resource.save(itemToSave, saveOptions);
|
|
|
|
if (itemType === ModelType.Tag) return Tag.save(itemToSave, saveOptions);
|
|
|
|
|
|
|
|
throw new Error('Unreachable');
|
|
|
|
};
|
|
|
|
|
|
|
|
// Deprecated - don't use
|
2023-06-30 10:11:26 +02:00
|
|
|
export const setNoteUserData = async <T>(note: NoteEntity, namespace: string, key: string, value: T, deleted = false): Promise<NoteEntity> => {
|
2023-06-13 19:06:16 +02:00
|
|
|
return setItemUserData(ModelType.Note, note.id, namespace, key, value, deleted);
|
|
|
|
};
|
|
|
|
|
|
|
|
const hasUserData = (userData: UserData, namespace: string, key: string) => {
|
|
|
|
if (!(namespace in userData)) return false;
|
|
|
|
if (!(key in userData[namespace])) return false;
|
|
|
|
if (userData[namespace][key].d) return false;
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getItemUserData = async <T>(itemType: ModelType, itemId: string, namespace: string, key: string): Promise<T|undefined> => {
|
|
|
|
checkIsSupportedItemType(itemType);
|
|
|
|
|
|
|
|
interface ItemSlice {
|
|
|
|
user_data: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const item = await BaseItem.loadItem(itemType, itemId, { fields: ['user_data'] }) as ItemSlice;
|
|
|
|
const userData = unserializeUserData(item.user_data);
|
|
|
|
return getUserData(userData, namespace, key);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Deprecated - don't use
|
|
|
|
export const getNoteUserData = async <T>(note: NoteEntity, namespace: string, key: string): Promise<T|undefined> => {
|
|
|
|
return getItemUserData<T>(ModelType.Note, note.id, namespace, key);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const deleteItemUserData = async (itemType: ModelType, itemId: string, namespace: string, key: string): Promise<SupportedEntity> => {
|
|
|
|
return setItemUserData(itemType, itemId, namespace, key, 0, true);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Deprecated - don't use
|
|
|
|
export const deleteNoteUserData = async (note: NoteEntity, namespace: string, key: string): Promise<NoteEntity> => {
|
|
|
|
return setNoteUserData(note, namespace, key, 0, true);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const mergeUserData = (target: UserData, source: UserData): UserData => {
|
|
|
|
const output: UserData = { ...target };
|
|
|
|
|
|
|
|
for (const namespaceName of Object.keys(source)) {
|
|
|
|
if (!(namespaceName in output)) output[namespaceName] = source[namespaceName];
|
|
|
|
const namespace = source[namespaceName];
|
|
|
|
for (const [key, value] of Object.entries(namespace)) {
|
|
|
|
// Keep ours
|
|
|
|
if (output[namespaceName][key] && output[namespaceName][key].t >= value.t) continue;
|
|
|
|
|
|
|
|
// Use theirs
|
|
|
|
output[namespaceName][key] = {
|
|
|
|
...value,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
};
|