fixing sum' file logic flaws & device UI stuff stuff
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
<MaxWidthWrapper cls="space-y-4">
|
<MaxWidthWrapper cls="space-y-4">
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon={Smartphone} cls="h-5 w-5 text-primary" />
|
<Icon icon={Smartphone} cls="h-5 w-5 text-primary" />
|
||||||
<Card.Title>Devices</Card.Title>
|
<Card.Title>Devices</Card.Title>
|
||||||
@@ -41,35 +41,37 @@
|
|||||||
{mobileVM.devicesTotal} total
|
{mobileVM.devicesTotal} total
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="grid grid-cols-2 gap-2 sm:flex sm:items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => void filesVM.cleanupDanglingFiles()}
|
onclick={() => void filesVM.cleanupDanglingFiles()}
|
||||||
disabled={filesVM.cleanupLoading}
|
disabled={filesVM.cleanupLoading}
|
||||||
|
class="col-span-1"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
cls={`h-4 w-4 mr-2 ${filesVM.cleanupLoading ? "animate-spin" : ""}`}
|
cls={`h-4 w-4 mr-2 ${filesVM.cleanupLoading ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
Cleanup Storage
|
<span class="truncate">Cleanup Storage</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => void mobileVM.refreshDevices()}
|
onclick={() => void mobileVM.refreshDevices()}
|
||||||
disabled={mobileVM.devicesLoading}
|
disabled={mobileVM.devicesLoading}
|
||||||
|
class="col-span-1"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={RefreshCw}
|
icon={RefreshCw}
|
||||||
cls={`h-4 w-4 mr-2 ${mobileVM.devicesLoading ? "animate-spin" : ""}`}
|
cls={`h-4 w-4 mr-2 ${mobileVM.devicesLoading ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
Refresh
|
<span class="hidden sm:inline">Refresh</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative mt-3 max-w-sm">
|
<div class="relative mt-2 w-full sm:max-w-sm">
|
||||||
<Icon
|
<Icon
|
||||||
icon={Search}
|
icon={Search}
|
||||||
cls="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
cls="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
||||||
@@ -92,88 +94,173 @@
|
|||||||
No devices registered yet.
|
No devices registered yet.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Table.Root>
|
<div class="space-y-3 md:hidden">
|
||||||
<Table.Header>
|
{#each mobileVM.devices as device (device.id)}
|
||||||
<Table.Row>
|
<div
|
||||||
<Table.Head>Device</Table.Head>
|
class="rounded-lg border bg-background p-3"
|
||||||
<Table.Head>Manufacturer / Model</Table.Head>
|
role="button"
|
||||||
<Table.Head>Android</Table.Head>
|
tabindex="0"
|
||||||
<Table.Head>Created</Table.Head>
|
onclick={() => goto(`/devices/${device.id}`)}
|
||||||
<Table.Head>Last Ping</Table.Head>
|
onkeydown={(e) => {
|
||||||
<Table.Head>Actions</Table.Head>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
</Table.Row>
|
e.preventDefault();
|
||||||
</Table.Header>
|
void goto(`/devices/${device.id}`);
|
||||||
<Table.Body>
|
}
|
||||||
{#each mobileVM.devices as device (device.id)}
|
}}
|
||||||
<Table.Row
|
>
|
||||||
class="cursor-pointer"
|
<div class="flex items-start justify-between gap-3">
|
||||||
onclick={() => goto(`/devices/${device.id}`)}
|
<div class="min-w-0">
|
||||||
>
|
<p class="truncate text-sm font-medium">{device.name}</p>
|
||||||
<Table.Cell>
|
<p class="text-muted-foreground truncate text-xs">
|
||||||
<div class="font-medium">{device.name}</div>
|
|
||||||
<div class="text-muted-foreground text-xs">
|
|
||||||
{device.externalDeviceId}
|
{device.externalDeviceId}
|
||||||
</div>
|
</p>
|
||||||
</Table.Cell>
|
</div>
|
||||||
<Table.Cell>
|
<AlertDialog.Root>
|
||||||
{device.manufacturer} / {device.model}
|
<AlertDialog.Trigger
|
||||||
</Table.Cell>
|
class={buttonVariants({
|
||||||
<Table.Cell>{device.androidVersion}</Table.Cell>
|
variant: "destructive",
|
||||||
<Table.Cell>
|
size: "sm",
|
||||||
{new Date(device.createdAt).toLocaleString()}
|
})}
|
||||||
</Table.Cell>
|
disabled={mobileVM.deletingDeviceId === device.id}
|
||||||
<Table.Cell>
|
onclick={(e) => e.stopPropagation()}
|
||||||
{mobileVM.formatLastPing(device.lastPingAt)}
|
>
|
||||||
</Table.Cell>
|
<Icon icon={Trash2} cls="h-4 w-4" />
|
||||||
<Table.Cell>
|
</AlertDialog.Trigger>
|
||||||
<AlertDialog.Root>
|
<AlertDialog.Content>
|
||||||
<AlertDialog.Trigger
|
<AlertDialog.Header>
|
||||||
class={buttonVariants({
|
<AlertDialog.Title>
|
||||||
variant: "destructive",
|
Delete device?
|
||||||
size: "sm",
|
</AlertDialog.Title>
|
||||||
})}
|
<AlertDialog.Description>
|
||||||
disabled={mobileVM.deletingDeviceId ===
|
This deletes the device and all related SMS/media data.
|
||||||
device.id}
|
Files in storage linked to this device are also removed.
|
||||||
onclick={(e) => e.stopPropagation()}
|
</AlertDialog.Description>
|
||||||
>
|
</AlertDialog.Header>
|
||||||
<Icon
|
<AlertDialog.Footer>
|
||||||
icon={Trash2}
|
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||||
cls="h-4 w-4"
|
<AlertDialog.Action
|
||||||
/>
|
onclick={async (e) => {
|
||||||
Delete
|
e.stopPropagation();
|
||||||
</AlertDialog.Trigger>
|
await mobileVM.deleteDevice(device.id);
|
||||||
<AlertDialog.Content>
|
}}
|
||||||
<AlertDialog.Header>
|
>
|
||||||
<AlertDialog.Title>
|
Delete
|
||||||
Delete device?
|
</AlertDialog.Action>
|
||||||
</AlertDialog.Title>
|
</AlertDialog.Footer>
|
||||||
<AlertDialog.Description>
|
</AlertDialog.Content>
|
||||||
This deletes the device and all related SMS/media data.
|
</AlertDialog.Root>
|
||||||
Files in storage linked to this device are also removed.
|
</div>
|
||||||
</AlertDialog.Description>
|
|
||||||
</AlertDialog.Header>
|
<div class="mt-3 grid grid-cols-2 gap-x-3 gap-y-2 text-xs">
|
||||||
<AlertDialog.Footer>
|
<div>
|
||||||
<AlertDialog.Cancel>
|
<p class="text-muted-foreground">Manufacturer / Model</p>
|
||||||
Cancel
|
<p class="truncate">{device.manufacturer} / {device.model}</p>
|
||||||
</AlertDialog.Cancel>
|
</div>
|
||||||
<AlertDialog.Action
|
<div>
|
||||||
onclick={async (e) => {
|
<p class="text-muted-foreground">Android</p>
|
||||||
e.stopPropagation();
|
<p>{device.androidVersion}</p>
|
||||||
await mobileVM.deleteDevice(
|
</div>
|
||||||
device.id,
|
<div>
|
||||||
);
|
<p class="text-muted-foreground">Created</p>
|
||||||
}}
|
<p class="truncate">
|
||||||
>
|
{new Date(device.createdAt).toLocaleString()}
|
||||||
Delete
|
</p>
|
||||||
</AlertDialog.Action>
|
</div>
|
||||||
</AlertDialog.Footer>
|
<div>
|
||||||
</AlertDialog.Content>
|
<p class="text-muted-foreground">Last Ping</p>
|
||||||
</AlertDialog.Root>
|
<p class="truncate">
|
||||||
</Table.Cell>
|
{mobileVM.formatLastPing(device.lastPingAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Head>Device</Table.Head>
|
||||||
|
<Table.Head>Manufacturer / Model</Table.Head>
|
||||||
|
<Table.Head>Android</Table.Head>
|
||||||
|
<Table.Head>Created</Table.Head>
|
||||||
|
<Table.Head>Last Ping</Table.Head>
|
||||||
|
<Table.Head>Actions</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
</Table.Header>
|
||||||
</Table.Body>
|
<Table.Body>
|
||||||
</Table.Root>
|
{#each mobileVM.devices as device (device.id)}
|
||||||
|
<Table.Row
|
||||||
|
class="cursor-pointer"
|
||||||
|
onclick={() => goto(`/devices/${device.id}`)}
|
||||||
|
>
|
||||||
|
<Table.Cell>
|
||||||
|
<div class="font-medium">{device.name}</div>
|
||||||
|
<div class="text-muted-foreground text-xs">
|
||||||
|
{device.externalDeviceId}
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{device.manufacturer} / {device.model}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{device.androidVersion}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{new Date(device.createdAt).toLocaleString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{mobileVM.formatLastPing(device.lastPingAt)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger
|
||||||
|
class={buttonVariants({
|
||||||
|
variant: "destructive",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
disabled={mobileVM.deletingDeviceId ===
|
||||||
|
device.id}
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={Trash2}
|
||||||
|
cls="h-4 w-4"
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Delete device?
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
This deletes the device and all related SMS/media data.
|
||||||
|
Files in storage linked to this device are also removed.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
Cancel
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action
|
||||||
|
onclick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await mobileVM.deleteDevice(
|
||||||
|
device.id,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -372,7 +372,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog.Root bind:open={mediaPreviewOpen}>
|
<Dialog.Root bind:open={mediaPreviewOpen}>
|
||||||
<Dialog.Content class="max-w-6xl p-8">
|
<Dialog.Content
|
||||||
|
class="h-[92vh] w-[96vw] overflow-hidden p-4 sm:max-w-[96vw] md:max-w-[94vw] lg:max-w-[90vw] xl:max-w-[86vw] 2xl:max-w-[82vw] md:p-6"
|
||||||
|
>
|
||||||
{#if selectedMedia}
|
{#if selectedMedia}
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
@@ -382,22 +384,30 @@
|
|||||||
>
|
>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="mt-2">
|
<div
|
||||||
|
class="bg-muted/30 mt-1 flex h-[calc(92vh-10.5rem)] items-center justify-center overflow-hidden rounded-md"
|
||||||
|
>
|
||||||
{#if isImageAsset(selectedMedia) && selectedMedia.r2Url}
|
{#if isImageAsset(selectedMedia) && selectedMedia.r2Url}
|
||||||
<img
|
<img
|
||||||
src={selectedMedia.r2Url}
|
src={selectedMedia.r2Url}
|
||||||
alt={selectedMedia.filename || selectedMedia.fileId}
|
alt={selectedMedia.filename || selectedMedia.fileId}
|
||||||
class="max-h-[80vh] w-full rounded-md object-contain"
|
class="h-full w-full object-contain"
|
||||||
|
/>
|
||||||
|
{:else if isVideoAsset(selectedMedia) && selectedMedia.r2Url}
|
||||||
|
<video
|
||||||
|
src={selectedMedia.r2Url}
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
preload="metadata"
|
||||||
|
class="h-full w-full bg-black object-contain"
|
||||||
/>
|
/>
|
||||||
{:else if isVideoAsset(selectedMedia)}
|
{:else if isVideoAsset(selectedMedia)}
|
||||||
<div
|
<div
|
||||||
class="bg-muted flex h-[60vh] items-center justify-center rounded-md text-center text-sm text-muted-foreground"
|
class="flex h-full w-full items-center justify-center text-center text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Icon icon={VideoIcon} cls="mx-auto h-10 w-10" />
|
<Icon icon={VideoIcon} cls="mx-auto h-10 w-10" />
|
||||||
<p class="mt-2">
|
<p class="mt-2">Video URL unavailable for preview.</p>
|
||||||
Video preview opens in a new tab.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -414,7 +424,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog.Footer class="mt-3">
|
<Dialog.Footer class="mt-2 justify-end">
|
||||||
{#if selectedMedia.r2Url}
|
{#if selectedMedia.r2Url}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -214,46 +214,59 @@ export class FileController {
|
|||||||
attributes: { "app.user.id": userId },
|
attributes: { "app.user.id": userId },
|
||||||
fn: () =>
|
fn: () =>
|
||||||
this.fileRepo
|
this.fileRepo
|
||||||
.listReferencedObjectKeysForUser(fctx, userId)
|
.listMobileMediaReferencedObjectKeysForUser(fctx, userId)
|
||||||
.andThen((referencedKeys) => {
|
.andThen((referencedKeys) =>
|
||||||
const referencedSet = new Set(referencedKeys);
|
this.fileRepo
|
||||||
return ResultAsync.combine([
|
.listMobileMediaDanglingFileIdsForUser(fctx, userId)
|
||||||
this.storageRepo.listObjectKeys(
|
.andThen((danglingFileIds) => {
|
||||||
fctx,
|
const referencedSet = new Set(referencedKeys);
|
||||||
`uploads/${userId}/`,
|
return ResultAsync.combine([
|
||||||
),
|
this.storageRepo.listObjectKeys(
|
||||||
this.storageRepo.listObjectKeys(
|
fctx,
|
||||||
fctx,
|
`uploads/${userId}/`,
|
||||||
`thumbnails/${userId}/`,
|
),
|
||||||
),
|
this.storageRepo.listObjectKeys(
|
||||||
]).andThen(([uploadKeys, thumbnailKeys]) => {
|
fctx,
|
||||||
const existingStorageKeys = [
|
`thumbnails/${userId}/`,
|
||||||
...new Set([...uploadKeys, ...thumbnailKeys]),
|
),
|
||||||
];
|
]).andThen(([uploadKeys, thumbnailKeys]) => {
|
||||||
|
const existingStorageKeys = [
|
||||||
|
...new Set([...uploadKeys, ...thumbnailKeys]),
|
||||||
|
];
|
||||||
|
|
||||||
const danglingKeys = existingStorageKeys.filter(
|
const danglingKeys = existingStorageKeys.filter(
|
||||||
(key) => !referencedSet.has(key),
|
(key) => !referencedSet.has(key),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (danglingKeys.length === 0) {
|
const deleteStorage =
|
||||||
return okAsync({
|
danglingKeys.length > 0
|
||||||
scanned: existingStorageKeys.length,
|
? this.storageRepo.deleteFiles(
|
||||||
referenced: referencedKeys.length,
|
fctx,
|
||||||
dangling: 0,
|
danglingKeys,
|
||||||
deleted: 0,
|
)
|
||||||
|
: okAsync(true);
|
||||||
|
|
||||||
|
return deleteStorage.andThen(() => {
|
||||||
|
const deleteRows =
|
||||||
|
danglingFileIds.length > 0
|
||||||
|
? this.fileRepo.deleteFiles(
|
||||||
|
fctx,
|
||||||
|
danglingFileIds,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
: okAsync(true);
|
||||||
|
|
||||||
|
return deleteRows.map(() => ({
|
||||||
|
scanned: existingStorageKeys.length,
|
||||||
|
referenced: referencedKeys.length,
|
||||||
|
dangling: danglingKeys.length,
|
||||||
|
deleted: danglingKeys.length,
|
||||||
|
deletedRows: danglingFileIds.length,
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}),
|
||||||
|
),
|
||||||
return this.storageRepo
|
|
||||||
.deleteFiles(fctx, danglingKeys)
|
|
||||||
.map(() => ({
|
|
||||||
scanned: existingStorageKeys.length,
|
|
||||||
referenced: referencedKeys.length,
|
|
||||||
dangling: danglingKeys.length,
|
|
||||||
deleted: danglingKeys.length,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "@pkg/db";
|
} from "@pkg/db";
|
||||||
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||||
import { FlowExecCtx } from "@core/flow.execution.context";
|
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||||
import { file, fileAccess } from "@pkg/db/schema";
|
import { file, fileAccess, mobileDevice, mobileMediaAsset } from "@pkg/db/schema";
|
||||||
import { type Err } from "@pkg/result";
|
import { type Err } from "@pkg/result";
|
||||||
import { fileErrors } from "./errors";
|
import { fileErrors } from "./errors";
|
||||||
import { logDomainEvent } from "@pkg/logger";
|
import { logDomainEvent } from "@pkg/logger";
|
||||||
@@ -381,7 +381,7 @@ export class FileRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
listReferencedObjectKeysForUser(
|
listMobileMediaReferencedObjectKeysForUser(
|
||||||
fctx: FlowExecCtx,
|
fctx: FlowExecCtx,
|
||||||
userId: string,
|
userId: string,
|
||||||
): ResultAsync<string[], Err> {
|
): ResultAsync<string[], Err> {
|
||||||
@@ -391,8 +391,13 @@ export class FileRepository {
|
|||||||
objectKey: file.objectKey,
|
objectKey: file.objectKey,
|
||||||
metadata: file.metadata,
|
metadata: file.metadata,
|
||||||
})
|
})
|
||||||
.from(file)
|
.from(mobileMediaAsset)
|
||||||
.where(eq(file.userId, userId)),
|
.innerJoin(file, eq(mobileMediaAsset.fileId, file.id))
|
||||||
|
.innerJoin(
|
||||||
|
mobileDevice,
|
||||||
|
eq(mobileMediaAsset.deviceId, mobileDevice.id),
|
||||||
|
)
|
||||||
|
.where(eq(mobileDevice.ownerUserId, userId)),
|
||||||
(error) =>
|
(error) =>
|
||||||
fileErrors.getFilesFailed(
|
fileErrors.getFilesFailed(
|
||||||
fctx,
|
fctx,
|
||||||
@@ -422,6 +427,35 @@ export class FileRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listMobileMediaDanglingFileIdsForUser(
|
||||||
|
fctx: FlowExecCtx,
|
||||||
|
userId: string,
|
||||||
|
): ResultAsync<string[], Err> {
|
||||||
|
return ResultAsync.fromPromise(
|
||||||
|
this.db
|
||||||
|
.select({ id: file.id })
|
||||||
|
.from(file)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(file.userId, userId),
|
||||||
|
sql`NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ${mobileMediaAsset}
|
||||||
|
INNER JOIN ${mobileDevice}
|
||||||
|
ON ${mobileMediaAsset.deviceId} = ${mobileDevice.id}
|
||||||
|
WHERE ${mobileMediaAsset.fileId} = ${file.id}
|
||||||
|
AND ${mobileDevice.ownerUserId} = ${userId}
|
||||||
|
)`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(error) =>
|
||||||
|
fileErrors.getFilesFailed(
|
||||||
|
fctx,
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
),
|
||||||
|
).map((rows) => rows.map((row) => row.id));
|
||||||
|
}
|
||||||
|
|
||||||
updateFileStatus(
|
updateFileStatus(
|
||||||
fctx: FlowExecCtx,
|
fctx: FlowExecCtx,
|
||||||
fileId: string,
|
fileId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user