mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-03 23:56:53 +00:00
파일, 카테고리, 디렉터리 정보를 불러올 때 특정 조건에서 네트워크 요청이 여러 번 발생할 수 있는 버그 수정
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/kit": "^2.49.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tanstack/svelte-virtual": "^3.13.15",
|
||||
"@tanstack/svelte-virtual": "^3.13.16",
|
||||
"@trpc/client": "^11.8.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/ms": "^0.7.34",
|
||||
@@ -64,7 +64,7 @@
|
||||
"pg": "^8.16.3",
|
||||
"superjson": "^2.2.6",
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^4.3.4"
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.0.0",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -36,8 +36,8 @@ importers:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
zod:
|
||||
specifier: ^4.3.4
|
||||
version: 4.3.4
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5
|
||||
devDependencies:
|
||||
'@eslint/compat':
|
||||
specifier: ^2.0.0
|
||||
@@ -58,8 +58,8 @@ importers:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0))
|
||||
'@tanstack/svelte-virtual':
|
||||
specifier: ^3.13.15
|
||||
version: 3.13.15(svelte@5.46.1)
|
||||
specifier: ^3.13.16
|
||||
version: 3.13.16(svelte@5.46.1)
|
||||
'@trpc/client':
|
||||
specifier: ^11.8.1
|
||||
version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)
|
||||
@@ -620,13 +620,13 @@ packages:
|
||||
svelte: ^5.0.0
|
||||
vite: ^6.3.0 || ^7.0.0
|
||||
|
||||
'@tanstack/svelte-virtual@3.13.15':
|
||||
resolution: {integrity: sha512-3PPLI3hsyT70zSZhBkSIZXIarlN+GjFNKeKr2Wk1UR7EuEVtXgNlB/Zk0sYtaeJ4CvGvldQNakOvbdETnWAgeA==}
|
||||
'@tanstack/svelte-virtual@3.13.16':
|
||||
resolution: {integrity: sha512-LRDPRzAPTIiDjiCA9lhNlFnZRLj/XsNhzNRsT5JEA8hzcBmZw8avdYYVjydPAy0ObFJgG1zBAm9Dtvwqju36sg==}
|
||||
peerDependencies:
|
||||
svelte: ^3.48.0 || ^4.0.0 || ^5.0.0
|
||||
|
||||
'@tanstack/virtual-core@3.13.15':
|
||||
resolution: {integrity: sha512-8cG3acM2cSIm3h8WxboHARAhQAJbYUhvmadvnN8uz8aziDwrbYb9KiARni+uY2qrLh49ycn+poGoxvtIAKhjog==}
|
||||
'@tanstack/virtual-core@3.13.16':
|
||||
resolution: {integrity: sha512-njazUC8mDkrxWmyZmn/3eXrDcP8Msb3chSr4q6a65RmwdSbMlMCdnOphv6/8mLO7O3Fuza5s4M4DclmvAO5w0w==}
|
||||
|
||||
'@trpc/client@11.8.1':
|
||||
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
||||
@@ -2025,8 +2025,8 @@ packages:
|
||||
zimmerframe@1.1.4:
|
||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||
|
||||
zod@4.3.4:
|
||||
resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==}
|
||||
zod@4.3.5:
|
||||
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
|
||||
|
||||
snapshots:
|
||||
|
||||
@@ -2389,12 +2389,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tanstack/svelte-virtual@3.13.15(svelte@5.46.1)':
|
||||
'@tanstack/svelte-virtual@3.13.16(svelte@5.46.1)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.15
|
||||
'@tanstack/virtual-core': 3.13.16
|
||||
svelte: 5.46.1
|
||||
|
||||
'@tanstack/virtual-core@3.13.15': {}
|
||||
'@tanstack/virtual-core@3.13.16': {}
|
||||
|
||||
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
@@ -3707,4 +3707,4 @@ snapshots:
|
||||
|
||||
zimmerframe@1.1.4: {}
|
||||
|
||||
zod@4.3.4: {}
|
||||
zod@4.3.5: {}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
let { categories, categoryMenuIcon, onCategoryClick, onCategoryMenuClick }: Props = $props();
|
||||
|
||||
let categoriesWithName = $derived(sortEntries($state.snapshot(categories)));
|
||||
let categoriesWithName = $derived(sortEntries([...categories]));
|
||||
</script>
|
||||
|
||||
{#if categoriesWithName.length > 0}
|
||||
|
||||
@@ -13,15 +13,15 @@ interface FileInfo {
|
||||
contentType: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
categoryIds: number[];
|
||||
categoryIds?: number[];
|
||||
}
|
||||
|
||||
interface CategoryInfo {
|
||||
id: number;
|
||||
parentId: CategoryId;
|
||||
name: string;
|
||||
files: { id: number; isRecursive: boolean }[];
|
||||
isFileRecursive: boolean;
|
||||
files?: { id: number; isRecursive: boolean }[];
|
||||
isFileRecursive?: boolean;
|
||||
}
|
||||
|
||||
const filesystem = new Dexie("filesystem") as Dexie & {
|
||||
@@ -55,7 +55,7 @@ export const getDirectoryInfo = async (id: number) => {
|
||||
};
|
||||
|
||||
export const storeDirectoryInfo = async (directoryInfo: DirectoryInfo) => {
|
||||
await filesystem.directory.put(directoryInfo);
|
||||
await filesystem.directory.upsert(directoryInfo.id, { ...directoryInfo });
|
||||
};
|
||||
|
||||
export const deleteDirectoryInfo = async (id: number) => {
|
||||
@@ -89,7 +89,7 @@ export const bulkGetFileInfos = async (ids: number[]) => {
|
||||
};
|
||||
|
||||
export const storeFileInfo = async (fileInfo: FileInfo) => {
|
||||
await filesystem.file.put(fileInfo);
|
||||
await filesystem.file.upsert(fileInfo.id, { ...fileInfo });
|
||||
};
|
||||
|
||||
export const deleteFileInfo = async (id: number) => {
|
||||
@@ -112,7 +112,7 @@ export const getCategoryInfo = async (id: number) => {
|
||||
};
|
||||
|
||||
export const storeCategoryInfo = async (categoryInfo: CategoryInfo) => {
|
||||
await filesystem.category.put(categoryInfo);
|
||||
await filesystem.category.upsert(categoryInfo.id, { ...categoryInfo });
|
||||
};
|
||||
|
||||
export const updateCategoryInfo = async (id: number, changes: { isFileRecursive?: boolean }) => {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||
import type { MaybeCategoryInfo } from "./types";
|
||||
import type { CategoryInfo, MaybeCategoryInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo, Partial<MaybeCategoryInfo>>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: CategoryId) => {
|
||||
const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({
|
||||
async fetchFromIndexedDB(id) {
|
||||
const [category, subCategories] = await Promise.all([
|
||||
id !== "root" ? IndexedDB.getCategoryInfo(id) : undefined,
|
||||
IndexedDB.getCategoryInfos(id),
|
||||
]);
|
||||
const files = category
|
||||
const files = category?.files
|
||||
? await Promise.all(
|
||||
category.files.map(async (file) => {
|
||||
const fileInfo = await IndexedDB.getFileInfo(file.id);
|
||||
return fileInfo
|
||||
? {
|
||||
id: file.id,
|
||||
parentId: fileInfo.parentId,
|
||||
contentType: fileInfo.contentType,
|
||||
name: fileInfo.name,
|
||||
createdAt: fileInfo.createdAt,
|
||||
@@ -31,137 +31,91 @@ const fetchFromIndexedDB = async (id: CategoryId) => {
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
subCategories,
|
||||
};
|
||||
} else if (category) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
parentId: category.parentId,
|
||||
name: category.name,
|
||||
subCategories,
|
||||
files: files!.filter((file) => !!file),
|
||||
isFileRecursive: category.isFileRecursive,
|
||||
files: files?.filter((file) => !!file) ?? [],
|
||||
isFileRecursive: category.isFileRecursive ?? false,
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
async fetchFromServer(id, cachedInfo, masterKey) {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subCategories: subCategoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().category.get.query({ id, recurse: true });
|
||||
|
||||
void IndexedDB.deleteDanglingCategoryInfos(id, new Set(subCategoriesRaw.map(({ id }) => id)));
|
||||
|
||||
const subCategories = await Promise.all(
|
||||
subCategoriesRaw.map(async (category) => {
|
||||
const decrypted = await decryptCategoryMetadata(category, masterKey);
|
||||
const existing = await IndexedDB.getCategoryInfo(category.id);
|
||||
await IndexedDB.storeCategoryInfo({
|
||||
const category = await trpc().category.get.query({ id, recurse: true });
|
||||
const [subCategories, files, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
category.subCategories.map(async (category) => ({
|
||||
id: category.id,
|
||||
parentId: id,
|
||||
name: decrypted.name,
|
||||
files: existing?.files ?? [],
|
||||
isFileRecursive: existing?.isFileRecursive ?? false,
|
||||
});
|
||||
return {
|
||||
id: category.id,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
category.files &&
|
||||
Promise.all(
|
||||
category.files.map(async (file) => ({
|
||||
id: file.id,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
),
|
||||
category.metadata && decryptCategoryMetadata(category.metadata, masterKey),
|
||||
]);
|
||||
|
||||
return storeToIndexedDB(
|
||||
id !== "root"
|
||||
? {
|
||||
id,
|
||||
parentId: category.metadata!.parent,
|
||||
subCategories,
|
||||
files: files!,
|
||||
isFileRecursive: cachedInfo?.isFileRecursive ?? false,
|
||||
...metadata!,
|
||||
}
|
||||
: { id, subCategories },
|
||||
);
|
||||
|
||||
const existingFiles = filesRaw
|
||||
? await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id))
|
||||
: [];
|
||||
const files = filesRaw
|
||||
? await Promise.all(
|
||||
filesRaw.map(async (file, index) => {
|
||||
const decrypted = await decryptFileMetadata(file, masterKey);
|
||||
const existing = existingFiles[index];
|
||||
if (existing) {
|
||||
const categoryIds = file.isRecursive
|
||||
? existing.categoryIds
|
||||
: Array.from(new Set([...existing.categoryIds, id as number]));
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: existing.parentId,
|
||||
contentType: file.contentType,
|
||||
name: decrypted.name,
|
||||
createdAt: decrypted.createdAt,
|
||||
lastModifiedAt: decrypted.lastModifiedAt,
|
||||
categoryIds,
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const decryptedMetadata = metadata
|
||||
? await decryptCategoryMetadata(metadata, masterKey)
|
||||
: undefined;
|
||||
if (id !== "root" && metadata && decryptedMetadata) {
|
||||
const existingCategory = await IndexedDB.getCategoryInfo(id);
|
||||
await IndexedDB.storeCategoryInfo({
|
||||
id: id as number,
|
||||
parentId: metadata.parent,
|
||||
name: decryptedMetadata.name,
|
||||
files:
|
||||
files?.map((file) => ({
|
||||
id: file.id,
|
||||
isRecursive: file.isRecursive,
|
||||
})) ??
|
||||
existingCategory?.files ??
|
||||
[],
|
||||
isFileRecursive: existingCategory?.isFileRecursive ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
subCategories,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
subCategories,
|
||||
files,
|
||||
...decryptedMetadata!,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteCategoryInfo(id as number);
|
||||
return { id, exists: false as const };
|
||||
return { id, exists: false };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const storeToIndexedDB = (info: CategoryInfo) => {
|
||||
if (info.id !== "root") {
|
||||
void IndexedDB.storeCategoryInfo(info);
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
new Map(info.files.map((file) => [file.id, file])).forEach((file) => {
|
||||
void IndexedDB.storeFileInfo(file);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
info.subCategories.forEach((category) => {
|
||||
void IndexedDB.storeCategoryInfo(category);
|
||||
});
|
||||
|
||||
void IndexedDB.deleteDanglingCategoryInfos(
|
||||
info.id,
|
||||
new Set(info.subCategories.map(({ id }) => id)),
|
||||
);
|
||||
|
||||
return { ...info, exists: true as const };
|
||||
};
|
||||
|
||||
export const getCategoryInfo = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, async (isInitial, resolve) => {
|
||||
if (isInitial) {
|
||||
const info = await fetchFromIndexedDB(id);
|
||||
if (info) {
|
||||
resolve(info);
|
||||
}
|
||||
}
|
||||
|
||||
const info = await fetchFromServer(id, masterKey);
|
||||
if (info) {
|
||||
resolve(info);
|
||||
}
|
||||
});
|
||||
return await cache.get(id, masterKey);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { monotonicResolve } from "$lib/utils";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte";
|
||||
import type { MaybeDirectoryInfo } from "./types";
|
||||
import type { DirectoryInfo, MaybeDirectoryInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: DirectoryId) => {
|
||||
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({
|
||||
async fetchFromIndexedDB(id) {
|
||||
const [directory, subDirectories, files] = await Promise.all([
|
||||
id !== "root" ? IndexedDB.getDirectoryInfo(id) : undefined,
|
||||
IndexedDB.getDirectoryInfos(id),
|
||||
@@ -16,96 +14,55 @@ const fetchFromIndexedDB = async (id: DirectoryId) => {
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
subDirectories,
|
||||
files,
|
||||
};
|
||||
} else if (directory) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
parentId: directory.parentId,
|
||||
name: directory.name,
|
||||
subDirectories,
|
||||
files,
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
async fetchFromServer(id, _cachedInfo, masterKey) {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subDirectories: subDirectoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().directory.get.query({ id });
|
||||
|
||||
void IndexedDB.deleteDanglingDirectoryInfos(id, new Set(subDirectoriesRaw.map(({ id }) => id)));
|
||||
void IndexedDB.deleteDanglingFileInfos(id, new Set(filesRaw.map(({ id }) => id)));
|
||||
|
||||
const existingFiles = await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id));
|
||||
const [subDirectories, files, decryptedMetadata] = await Promise.all([
|
||||
const directory = await trpc().directory.get.query({ id });
|
||||
const [subDirectories, files, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
subDirectoriesRaw.map(async (directory) => {
|
||||
const decrypted = await decryptDirectoryMetadata(directory, masterKey);
|
||||
await IndexedDB.storeDirectoryInfo({
|
||||
directory.subDirectories.map(async (directory) => ({
|
||||
id: directory.id,
|
||||
parentId: id,
|
||||
name: decrypted.name,
|
||||
});
|
||||
return {
|
||||
id: directory.id,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
...(await decryptDirectoryMetadata(directory, masterKey)),
|
||||
})),
|
||||
),
|
||||
Promise.all(
|
||||
filesRaw.map(async (file, index) => {
|
||||
const decrypted = await decryptFileMetadata(file, masterKey);
|
||||
await IndexedDB.storeFileInfo({
|
||||
directory.files.map(async (file) => ({
|
||||
id: file.id,
|
||||
parentId: id,
|
||||
contentType: file.contentType,
|
||||
name: decrypted.name,
|
||||
createdAt: decrypted.createdAt,
|
||||
lastModifiedAt: decrypted.lastModifiedAt,
|
||||
categoryIds: existingFiles[index]?.categoryIds ?? [],
|
||||
});
|
||||
return {
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
),
|
||||
metadata ? decryptDirectoryMetadata(metadata, masterKey) : undefined,
|
||||
directory.metadata && decryptDirectoryMetadata(directory.metadata, masterKey),
|
||||
]);
|
||||
|
||||
if (id !== "root" && metadata && decryptedMetadata) {
|
||||
await IndexedDB.storeDirectoryInfo({
|
||||
return storeToIndexedDB(
|
||||
id !== "root"
|
||||
? {
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
name: decryptedMetadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: directory.metadata!.parent,
|
||||
subDirectories,
|
||||
files,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: metadata!.parent,
|
||||
subDirectories,
|
||||
files,
|
||||
...decryptedMetadata!,
|
||||
};
|
||||
...metadata!,
|
||||
}
|
||||
: { id, subDirectories, files },
|
||||
);
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteDirectoryInfo(id as number);
|
||||
@@ -113,13 +70,33 @@ const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const storeToIndexedDB = (info: DirectoryInfo) => {
|
||||
if (info.id !== "root") {
|
||||
void IndexedDB.storeDirectoryInfo(info);
|
||||
}
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
info.subDirectories.forEach((subDirectory) => {
|
||||
void IndexedDB.storeDirectoryInfo(subDirectory);
|
||||
});
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
info.files.forEach((file) => {
|
||||
void IndexedDB.storeFileInfo(file);
|
||||
});
|
||||
|
||||
void IndexedDB.deleteDanglingDirectoryInfos(
|
||||
info.id,
|
||||
new Set(info.subDirectories.map(({ id }) => id)),
|
||||
);
|
||||
void IndexedDB.deleteDanglingFileInfos(info.id, new Set(info.files.map(({ id }) => id)));
|
||||
|
||||
return { ...info, exists: true as const };
|
||||
};
|
||||
|
||||
export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, (isInitial, resolve) =>
|
||||
monotonicResolve(
|
||||
[isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
return await cache.get(id, masterKey);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { monotonicResolve } from "$lib/utils";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||
import type { MaybeFileInfo } from "./types";
|
||||
import type { FileInfo, MaybeFileInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<number, MaybeFileInfo>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: number) => {
|
||||
const cache = new FilesystemCache<number, MaybeFileInfo>({
|
||||
async fetchFromIndexedDB(id) {
|
||||
const file = await IndexedDB.getFileInfo(id);
|
||||
const categories = file
|
||||
const categories = file?.categoryIds
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category ? { id: category.id, name: category.name } : undefined;
|
||||
return category
|
||||
? { id: category.id, parentId: category.parentId, name: category.name }
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
@@ -20,81 +20,42 @@ const fetchFromIndexedDB = async (id: number) => {
|
||||
if (file) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
parentId: file.parentId,
|
||||
contentType: file.contentType,
|
||||
name: file.name,
|
||||
createdAt: file.createdAt,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
categories: categories!.filter((category) => !!category),
|
||||
categories: categories?.filter((category) => !!category) ?? [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const bulkFetchFromIndexedDB = async (ids: number[]) => {
|
||||
const files = await IndexedDB.bulkGetFileInfos(ids);
|
||||
const categories = await Promise.all(
|
||||
files.map(async (file) =>
|
||||
file
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category ? { id: category.id, name: category.name } : undefined;
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
return new Map(
|
||||
files
|
||||
.map((file, index) =>
|
||||
file
|
||||
? ([
|
||||
file.id,
|
||||
{
|
||||
...file,
|
||||
exists: true,
|
||||
categories: categories[index]!.filter((category) => !!category),
|
||||
},
|
||||
] as const)
|
||||
: undefined,
|
||||
)
|
||||
.filter((file) => !!file),
|
||||
);
|
||||
};
|
||||
|
||||
const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
|
||||
async fetchFromServer(id, _cachedInfo, masterKey) {
|
||||
try {
|
||||
const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id });
|
||||
const [categories, decryptedMetadata] = await Promise.all([
|
||||
const file = await trpc().file.get.query({ id });
|
||||
const [categories, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
categoriesRaw.map(async (category) => ({
|
||||
file.categories.map(async (category) => ({
|
||||
id: category.id,
|
||||
parentId: category.parent,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(metadata, masterKey),
|
||||
decryptFileMetadata(file, masterKey),
|
||||
]);
|
||||
|
||||
await IndexedDB.storeFileInfo({
|
||||
return storeToIndexedDB({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
contentType: metadata.contentType,
|
||||
name: decryptedMetadata.name,
|
||||
createdAt: decryptedMetadata.createdAt,
|
||||
lastModifiedAt: decryptedMetadata.lastModifiedAt,
|
||||
categoryIds: categories.map((category) => category.id),
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: metadata.parent,
|
||||
contentType: metadata.contentType,
|
||||
contentIv: metadata.contentIv,
|
||||
parentId: file.parent,
|
||||
dataKey: metadata.dataKey,
|
||||
contentType: file.contentType,
|
||||
contentIv: file.contentIv,
|
||||
name: metadata.name,
|
||||
createdAt: metadata.createdAt,
|
||||
lastModifiedAt: metadata.lastModifiedAt,
|
||||
categories,
|
||||
...decryptedMetadata,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteFileInfo(id);
|
||||
@@ -102,74 +63,115 @@ const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
const bulkFetchFromServer = async (ids: number[], masterKey: CryptoKey) => {
|
||||
const filesRaw = await trpc().file.bulkGet.query({ ids });
|
||||
async bulkFetchFromIndexedDB(ids) {
|
||||
const files = await IndexedDB.bulkGetFileInfos([...ids]);
|
||||
const categories = await Promise.all(
|
||||
files.map(async (file) =>
|
||||
file?.categoryIds
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category
|
||||
? { id: category.id, parentId: category.parentId, name: category.name }
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
return new Map(
|
||||
files
|
||||
.filter((file) => !!file)
|
||||
.map((file, index) => [
|
||||
file.id,
|
||||
{
|
||||
...file,
|
||||
exists: true,
|
||||
categories: categories[index]?.filter((category) => !!category) ?? [],
|
||||
},
|
||||
]),
|
||||
);
|
||||
},
|
||||
|
||||
async bulkFetchFromServer(ids, masterKey) {
|
||||
const idsArray = [...ids.keys()];
|
||||
|
||||
const filesRaw = await trpc().file.bulkGet.query({ ids: idsArray });
|
||||
const files = await Promise.all(
|
||||
filesRaw.map(async (file) => {
|
||||
const [categories, decryptedMetadata] = await Promise.all([
|
||||
filesRaw.map(async ({ id, categories: categoriesRaw, ...metadataRaw }) => {
|
||||
const [categories, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
file.categories.map(async (category) => ({
|
||||
categoriesRaw.map(async (category) => ({
|
||||
id: category.id,
|
||||
parentId: category.parent,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(file, masterKey),
|
||||
decryptFileMetadata(metadataRaw, masterKey),
|
||||
]);
|
||||
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
name: decryptedMetadata.name,
|
||||
createdAt: decryptedMetadata.createdAt,
|
||||
lastModifiedAt: decryptedMetadata.lastModifiedAt,
|
||||
categoryIds: categories.map((category) => category.id),
|
||||
});
|
||||
return {
|
||||
id: file.id,
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
contentIv: file.contentIv,
|
||||
parentId: metadataRaw.parent,
|
||||
contentType: metadataRaw.contentType,
|
||||
contentIv: metadataRaw.contentIv,
|
||||
categories,
|
||||
...decryptedMetadata,
|
||||
...metadata,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const existingIds = new Set(filesRaw.map(({ id }) => id));
|
||||
|
||||
return new Map<number, MaybeFileInfo>([
|
||||
...files.map((file) => [file.id, file] as const),
|
||||
...ids.filter((id) => !existingIds.has(id)).map((id) => [id, { id, exists: false }] as const),
|
||||
...bulkStoreToIndexedDB(files),
|
||||
...idsArray
|
||||
.filter((id) => !existingIds.has(id))
|
||||
.map((id) => [id, { id, exists: false }] as const),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const storeToIndexedDB = (info: FileInfo) => {
|
||||
void IndexedDB.storeFileInfo({
|
||||
...info,
|
||||
categoryIds: info.categories.map(({ id }) => id),
|
||||
});
|
||||
|
||||
info.categories.forEach((category) => {
|
||||
void IndexedDB.storeCategoryInfo(category);
|
||||
});
|
||||
|
||||
return { ...info, exists: true as const };
|
||||
};
|
||||
|
||||
const bulkStoreToIndexedDB = (infos: FileInfo[]) => {
|
||||
// TODO: Bulk Upsert
|
||||
infos.forEach((info) => {
|
||||
void IndexedDB.storeFileInfo({
|
||||
...info,
|
||||
categoryIds: info.categories.map(({ id }) => id),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
new Map(
|
||||
infos.flatMap(({ categories }) => categories).map((category) => [category.id, category]),
|
||||
).forEach((category) => {
|
||||
void IndexedDB.storeCategoryInfo(category);
|
||||
});
|
||||
|
||||
return infos.map((info) => [info.id, { ...info, exists: true }] as const);
|
||||
};
|
||||
|
||||
export const getFileInfo = async (id: number, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, (isInitial, resolve) =>
|
||||
monotonicResolve(
|
||||
[isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
return await cache.get(id, masterKey);
|
||||
};
|
||||
|
||||
export const bulkGetFileInfo = async (ids: number[], masterKey: CryptoKey) => {
|
||||
return await cache.bulkGet(new Set(ids), (keys, resolve) =>
|
||||
monotonicResolve(
|
||||
[
|
||||
bulkFetchFromIndexedDB(
|
||||
Array.from(
|
||||
keys
|
||||
.entries()
|
||||
.filter(([, isInitial]) => isInitial)
|
||||
.map(([key]) => key),
|
||||
),
|
||||
),
|
||||
bulkFetchFromServer(Array.from(keys.keys()), masterKey),
|
||||
],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
return await cache.bulkGet(new Set(ids), masterKey);
|
||||
};
|
||||
|
||||
@@ -1,82 +1,120 @@
|
||||
import { untrack } from "svelte";
|
||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||
|
||||
export class FilesystemCache<K, V extends RV, RV = V> {
|
||||
private map = new Map<K, V | Promise<V>>();
|
||||
|
||||
get(key: K, loader: (isInitial: boolean, resolve: (value: RV | undefined) => void) => void) {
|
||||
const info = this.map.get(key);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
interface FilesystemCacheOptions<K, V> {
|
||||
fetchFromIndexedDB: (key: K) => Promise<V | undefined>;
|
||||
fetchFromServer: (key: K, cachedValue: V | undefined, masterKey: CryptoKey) => Promise<V>;
|
||||
bulkFetchFromIndexedDB?: (keys: Set<K>) => Promise<Map<K, V>>;
|
||||
bulkFetchFromServer?: (
|
||||
keys: Map<K, { cachedValue: V | undefined }>,
|
||||
masterKey: CryptoKey,
|
||||
) => Promise<Map<K, V>>;
|
||||
}
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<V>();
|
||||
if (!info) {
|
||||
this.map.set(key, promise);
|
||||
export class FilesystemCache<K, V extends object> {
|
||||
private map = new Map<K, { value?: V; promise?: Promise<V> }>();
|
||||
|
||||
constructor(private readonly options: FilesystemCacheOptions<K, V>) {}
|
||||
|
||||
get(key: K, masterKey: CryptoKey) {
|
||||
return untrack(() => {
|
||||
let state = this.map.get(key);
|
||||
if (state?.promise) return state.value ?? state.promise;
|
||||
|
||||
const { promise: newPromise, resolve } = Promise.withResolvers<V>();
|
||||
|
||||
if (!state) {
|
||||
const newState = $state({});
|
||||
state = newState;
|
||||
this.map.set(key, newState);
|
||||
}
|
||||
|
||||
loader(!info, (loadedInfo) => {
|
||||
if (!loadedInfo) return;
|
||||
state.promise = newPromise;
|
||||
|
||||
const info = this.map.get(key)!;
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(loadedInfo);
|
||||
this.map.set(key, state as V);
|
||||
resolve(state as V);
|
||||
(state.value
|
||||
? Promise.resolve(state.value)
|
||||
: this.options.fetchFromIndexedDB(key).then((loadedInfo) => {
|
||||
if (loadedInfo) {
|
||||
state.value = loadedInfo;
|
||||
resolve(state.value);
|
||||
}
|
||||
return loadedInfo;
|
||||
})
|
||||
)
|
||||
.then((cachedInfo) => this.options.fetchFromServer(key, cachedInfo, masterKey))
|
||||
.then((loadedInfo) => {
|
||||
if (state.value) {
|
||||
Object.assign(state.value, loadedInfo);
|
||||
} else {
|
||||
Object.assign(info, loadedInfo);
|
||||
resolve(info);
|
||||
state.value = loadedInfo;
|
||||
}
|
||||
resolve(state.value);
|
||||
})
|
||||
.finally(() => {
|
||||
state.promise = undefined;
|
||||
});
|
||||
|
||||
return newPromise;
|
||||
});
|
||||
}
|
||||
|
||||
bulkGet(keys: Set<K>, masterKey: CryptoKey) {
|
||||
return untrack(() => {
|
||||
const newPromises = new Map(
|
||||
keys
|
||||
.keys()
|
||||
.filter((key) => this.map.get(key)?.promise === undefined)
|
||||
.map((key) => [key, Promise.withResolvers<V>()]),
|
||||
);
|
||||
newPromises.forEach(({ promise }, key) => {
|
||||
const state = this.map.get(key);
|
||||
if (state) {
|
||||
state.promise = promise;
|
||||
} else {
|
||||
const newState = $state({ promise });
|
||||
this.map.set(key, newState);
|
||||
}
|
||||
});
|
||||
|
||||
return info ?? promise;
|
||||
}
|
||||
|
||||
async bulkGet(
|
||||
keys: Set<K>,
|
||||
loader: (keys: Map<K, boolean>, resolve: (values: Map<K, RV>) => void) => void,
|
||||
) {
|
||||
const states = new Map<K, V>();
|
||||
const promises = new Map<K, Promise<V>>();
|
||||
const resolvers = new Map<K, (value: V) => void>();
|
||||
|
||||
keys.forEach((key) => {
|
||||
const info = this.map.get(key);
|
||||
if (info instanceof Promise) {
|
||||
promises.set(key, info);
|
||||
} else if (info) {
|
||||
states.set(key, info);
|
||||
} else {
|
||||
const { promise, resolve } = Promise.withResolvers<V>();
|
||||
this.map.set(key, promise);
|
||||
promises.set(key, promise);
|
||||
resolvers.set(key, resolve);
|
||||
}
|
||||
});
|
||||
|
||||
loader(
|
||||
new Map([
|
||||
...states.keys().map((key) => [key, false] as const),
|
||||
...resolvers.keys().map((key) => [key, true] as const),
|
||||
]),
|
||||
(loadedInfos) =>
|
||||
const resolve = (loadedInfos: Map<K, V>) => {
|
||||
loadedInfos.forEach((loadedInfo, key) => {
|
||||
const info = this.map.get(key)!;
|
||||
const resolve = resolvers.get(key);
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(loadedInfo);
|
||||
this.map.set(key, state as V);
|
||||
resolve?.(state as V);
|
||||
const state = this.map.get(key)!;
|
||||
if (state.value) {
|
||||
Object.assign(state.value, loadedInfo);
|
||||
} else {
|
||||
Object.assign(info, loadedInfo);
|
||||
resolve?.(info);
|
||||
state.value = loadedInfo;
|
||||
}
|
||||
}),
|
||||
);
|
||||
newPromises.get(key)!.resolve(state.value);
|
||||
});
|
||||
return loadedInfos;
|
||||
};
|
||||
|
||||
const newStates = await Promise.all(
|
||||
promises.entries().map(async ([key, promise]) => [key, await promise] as const),
|
||||
);
|
||||
return new Map([...states, ...newStates]);
|
||||
this.options.bulkFetchFromIndexedDB!(
|
||||
new Set(newPromises.keys().filter((key) => this.map.get(key)!.value === undefined)),
|
||||
)
|
||||
.then(resolve)
|
||||
.then(() =>
|
||||
this.options.bulkFetchFromServer!(
|
||||
new Map(
|
||||
newPromises.keys().map((key) => [key, { cachedValue: this.map.get(key)!.value }]),
|
||||
),
|
||||
masterKey,
|
||||
),
|
||||
)
|
||||
.then(resolve)
|
||||
.finally(() => {
|
||||
newPromises.forEach((_, key) => {
|
||||
this.map.get(key)!.promise = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
keys
|
||||
.keys()
|
||||
.filter((key) => this.map.get(key)!.value === undefined)
|
||||
.map((key) => this.map.get(key)!.promise!),
|
||||
).then(() => new Map(keys.keys().map((key) => [key, this.map.get(key)!.value!] as const)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@ interface RootDirectoryInfo {
|
||||
}
|
||||
|
||||
export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo;
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "parentId" | "subDirectories" | "files">;
|
||||
export type MaybeDirectoryInfo =
|
||||
| (DirectoryInfo & { exists: true })
|
||||
| ({ id: DirectoryId; exists: false } & AllUndefined<Omit<DirectoryInfo, "id">>);
|
||||
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "subDirectories" | "files">;
|
||||
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
@@ -34,17 +35,19 @@ export interface FileInfo {
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
categories: { id: number; name: string }[];
|
||||
categories: FileCategoryInfo[];
|
||||
}
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "parentId" | "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
export type MaybeFileInfo =
|
||||
| (FileInfo & { exists: true })
|
||||
| ({ id: number; exists: false } & AllUndefined<Omit<FileInfo, "id">>);
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
|
||||
interface LocalCategoryInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
name: string;
|
||||
subCategories: SubCategoryInfo[];
|
||||
@@ -54,6 +57,7 @@ interface LocalCategoryInfo {
|
||||
|
||||
interface RootCategoryInfo {
|
||||
id: "root";
|
||||
parentId?: undefined;
|
||||
dataKey?: undefined;
|
||||
name?: undefined;
|
||||
subCategories: SubCategoryInfo[];
|
||||
@@ -62,10 +66,12 @@ interface RootCategoryInfo {
|
||||
}
|
||||
|
||||
export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo;
|
||||
export type MaybeCategoryInfo =
|
||||
| (CategoryInfo & { exists: true })
|
||||
| ({ id: CategoryId; exists: false } & AllUndefined<Omit<CategoryInfo, "id">>);
|
||||
|
||||
export type SubCategoryInfo = Omit<
|
||||
LocalCategoryInfo,
|
||||
"subCategories" | "files" | "isFileRecursive"
|
||||
>;
|
||||
export type MaybeCategoryInfo =
|
||||
| (CategoryInfo & { exists: true })
|
||||
| ({ id: CategoryId; exists: false } & AllUndefined<Omit<CategoryInfo, "id">>);
|
||||
export type FileCategoryInfo = Omit<SubCategoryInfo, "dataKey">;
|
||||
|
||||
@@ -39,6 +39,7 @@ export type NewFile = Omit<File, "id">;
|
||||
|
||||
interface FileCategory {
|
||||
id: number;
|
||||
parentId: CategoryId;
|
||||
mekVersion: number;
|
||||
encDek: string;
|
||||
dekVersion: Date;
|
||||
@@ -445,6 +446,7 @@ export const getFilesWithCategories = async (userId: number, fileIds: number[])
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
categories: file.categories.map((category) => ({
|
||||
id: category.id,
|
||||
parentId: category.parent_id ?? "root",
|
||||
mekVersion: category.master_encryption_key_version,
|
||||
encDek: category.encrypted_data_encryption_key,
|
||||
dekVersion: new Date(category.data_encryption_key_version),
|
||||
@@ -548,6 +550,7 @@ export const getAllFileCategories = async (fileId: number) => {
|
||||
(category) =>
|
||||
({
|
||||
id: category.id,
|
||||
parentId: category.parent_id ?? "root",
|
||||
mekVersion: category.master_encryption_key_version,
|
||||
encDek: category.encrypted_data_encryption_key,
|
||||
dekVersion: category.data_encryption_key_version,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./format";
|
||||
export * from "./gotoStateful";
|
||||
export * from "./promise";
|
||||
export * from "./sort";
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export const monotonicResolve = <T>(
|
||||
promises: (Promise<T> | false)[],
|
||||
callback: (value: T) => void,
|
||||
) => {
|
||||
let latestResolvedIndex = -1;
|
||||
promises
|
||||
.filter((promise) => !!promise)
|
||||
.forEach((promise, index) => {
|
||||
promise.then((value) => {
|
||||
if (index > latestResolvedIndex) {
|
||||
latestResolvedIndex = index;
|
||||
callback(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -46,6 +46,7 @@ const categoryRouter = router({
|
||||
})),
|
||||
files: files?.map((file) => ({
|
||||
id: file.id,
|
||||
parent: file.parentId,
|
||||
mekVersion: file.mekVersion,
|
||||
dek: file.encDek,
|
||||
dekVersion: file.dekVersion,
|
||||
|
||||
@@ -33,6 +33,7 @@ const fileRouter = router({
|
||||
lastModifiedAtIv: file.encLastModifiedAt.iv,
|
||||
categories: categories.map((category) => ({
|
||||
id: category.id,
|
||||
parent: category.parentId,
|
||||
mekVersion: category.mekVersion,
|
||||
dek: category.encDek,
|
||||
dekVersion: category.dekVersion,
|
||||
@@ -66,6 +67,7 @@ const fileRouter = router({
|
||||
lastModifiedAtIv: file.encLastModifiedAt.iv,
|
||||
categories: file.categories.map((category) => ({
|
||||
id: category.id,
|
||||
parent: category.parentId,
|
||||
mekVersion: category.mekVersion,
|
||||
dek: category.encDek,
|
||||
dekVersion: category.dekVersion,
|
||||
|
||||
Reference in New Issue
Block a user