fix: migrate to Immich v3.0 API and DB schema
- Updated albumColumns and AlbumThumbnail components to display placeholder when no thumbnail is available. - added album_user table (album ownership moved here, role='owner') - drop asset.deviceId/deviceAssetId and albums.ownerId - asset.duration varchar -> integer (milliseconds) - removed isArchived instead use visibility
This commit is contained in:
@@ -2,7 +2,7 @@ import { ColumnDef } from "@tanstack/react-table"
|
||||
import { IAlbum } from "@/types/album"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { ArrowUpDown, MoreHorizontal, Calendar, Camera, Users, HardDrive } from "lucide-react"
|
||||
import { ArrowUpDown, MoreHorizontal, Calendar, Camera, Users, HardDrive, ImageOff } from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -48,15 +48,21 @@ export const albumColumns: ColumnDef<IAlbum>[] = [
|
||||
const album = row.original
|
||||
return (
|
||||
<div className="w-16 h-16 rounded-md overflow-hidden">
|
||||
<LazyImage
|
||||
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
|
||||
alt={album.albumName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
{album.albumThumbnailAssetId ? (
|
||||
<LazyImage
|
||||
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
|
||||
alt={album.albumName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gray-100 dark:bg-gray-800 text-gray-400">
|
||||
<ImageOff size={20} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -164,16 +170,20 @@ export const albumColumns: ColumnDef<IAlbum>[] = [
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const album = row.original
|
||||
const numberOfDays = differenceInDays(album.lastPhotoDate, album.firstPhotoDate)
|
||||
|
||||
const numberOfDays = (album.firstPhotoDate && album.lastPhotoDate)
|
||||
? differenceInDays(album.lastPhotoDate, album.firstPhotoDate)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm">
|
||||
{album.firstPhotoDate ? formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy') : ''} - {album.lastPhotoDate ? formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy') : ''}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString()} days`}
|
||||
</div>
|
||||
{numberOfDays !== null && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString()} days`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IAlbum } from '@/types/album';
|
||||
import { formatDate } from '@/helpers/date.helper';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { Calendar, Camera } from 'lucide-react';
|
||||
import { Calendar, Camera, ImageOff } from 'lucide-react';
|
||||
|
||||
interface IAlbumThumbnailProps {
|
||||
album: IAlbum;
|
||||
@@ -18,6 +18,9 @@ interface IAlbumThumbnailProps {
|
||||
|
||||
export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThumbnailProps) {
|
||||
const numberOfDays = useMemo(() => {
|
||||
if (!album.firstPhotoDate || !album.lastPhotoDate) {
|
||||
return null;
|
||||
}
|
||||
return differenceInDays(album.lastPhotoDate, album.firstPhotoDate);
|
||||
}, [album.firstPhotoDate, album.lastPhotoDate]);
|
||||
|
||||
@@ -29,16 +32,27 @@ export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThum
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden shadow-lg relative group">
|
||||
<label className="block relative ">
|
||||
<LazyImage
|
||||
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
|
||||
alt={album.albumName}
|
||||
title={album.albumName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
{album.albumThumbnailAssetId ? (
|
||||
<LazyImage
|
||||
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
|
||||
alt={album.albumName}
|
||||
title={album.albumName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center gap-2 bg-gray-100 dark:bg-gray-800 text-gray-400"
|
||||
style={{ width: '100%', height: '200px' }}
|
||||
title={album.albumName}
|
||||
>
|
||||
<ImageOff size={32} />
|
||||
<span className="text-xs">No photos</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 w-full bg-gray-800/70 text-white text-center text-xs font-bold py-1 group-hover:hidden">
|
||||
{album.firstPhotoDate ? formatDate(album.firstPhotoDate?.toString(), 'MMM d, yyyy') : ''} - {album.lastPhotoDate ? formatDate(album.lastPhotoDate?.toString(), 'MMM d, yyyy') : ''}
|
||||
</div>
|
||||
@@ -54,9 +68,11 @@ export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThum
|
||||
<Camera size={12} />
|
||||
{humanizeNumber(album.assetCount)}
|
||||
</div>
|
||||
<p className="bg-gray-800 flex items-center gap-1 text-white text-xs py-1 px-2 rounded">
|
||||
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`} <Calendar size={12} />
|
||||
</p>
|
||||
{numberOfDays !== null && (
|
||||
<p className="bg-gray-800 flex items-center gap-1 text-white text-xs py-1 px-2 rounded">
|
||||
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`} <Calendar size={12} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`p-4 ${selected ? 'bg-blue-500' : ''}`}>
|
||||
<Link href={`/albums/${album.id}`}>
|
||||
|
||||
@@ -7,6 +7,9 @@ export const pluralize = (number: number, word: string, pluralWord: string) => {
|
||||
}
|
||||
|
||||
export const humanizeBytes = (bytes: number) => {
|
||||
if (!bytes || isNaN(bytes)) {
|
||||
return `0 bytes`
|
||||
}
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} bytes`
|
||||
}
|
||||
@@ -19,22 +22,23 @@ export const humanizeBytes = (bytes: number) => {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
export const humanizeDuration = (duration: string) => {
|
||||
if (!duration) {
|
||||
export const humanizeDuration = (duration: string | number | null | undefined) => {
|
||||
if (duration === null || duration === undefined || duration === '') {
|
||||
return null;
|
||||
}
|
||||
// Example input : 00:00:04.350
|
||||
const [hours, minutes, seconds] = duration.split(':').map(Number);
|
||||
const totalSeconds = Math.round(hours * 3600 + minutes * 60 + seconds);
|
||||
const ms = Number(duration);
|
||||
if (isNaN(ms)) {
|
||||
return null;
|
||||
}
|
||||
const totalSeconds = Math.round(ms / 1000);
|
||||
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds}s`;
|
||||
}
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
if (totalSeconds < 3600) {
|
||||
// Show only minutes
|
||||
return `${Math.round(minutes)}m`;
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
|
||||
return `${Math.round(hours)}h ${Math.round(minutes)}m`;
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
@@ -163,16 +163,15 @@ export async function executeAction(
|
||||
}
|
||||
|
||||
case "archive": {
|
||||
await immichFetch("/assets", "PUT", { ids: assetIds, isArchived: true }, user);
|
||||
await immichFetch("/assets", "PUT", { ids: assetIds, visibility: "archive" }, user);
|
||||
return { action: "archive", assetsProcessed: assetIds.length };
|
||||
}
|
||||
|
||||
case "tag": {
|
||||
if (!config.tagName) throw new Error("Tag name is required");
|
||||
// Tags API may vary by Immich version — try the standard approach
|
||||
try {
|
||||
// Create or get tag
|
||||
const tag = await immichFetch("/tags", "POST", { name: config.tagName, type: "OBJECT" }, user);
|
||||
const tag = await immichFetch("/tags", "POST", { name: config.tagName }, user);
|
||||
// Tag assets
|
||||
await immichFetch(`/tags/${tag.id}/assets`, "PUT", { ids: assetIds }, user);
|
||||
return { action: "tag", assetsProcessed: assetIds.length };
|
||||
|
||||
@@ -29,7 +29,6 @@ export default async function handler(
|
||||
}
|
||||
const dbAssets = await db.selectDistinctOn([assets.id], {
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { albums } from "@/schema/albums.schema";
|
||||
import { count, eq, min, max, sql, and } from "drizzle-orm";
|
||||
import { assets } from "@/schema/assets.schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albumUsers } from "@/schema/albumUsers.schema";
|
||||
import { assetFaces, exif, person } from "@/schema";
|
||||
|
||||
export default async function handler(
|
||||
@@ -31,12 +32,13 @@ export default async function handler(
|
||||
faceCount: count(sql<string>`DISTINCT ${person.id}`), // Ensure unique personId
|
||||
})
|
||||
.from(albums)
|
||||
.innerJoin(albumUsers, and(eq(albums.id, albumUsers.albumId), eq(albumUsers.role, "owner")))
|
||||
.leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId))
|
||||
.leftJoin(assets, eq(albumsAssetsAssets.assetId, assets.id))
|
||||
.leftJoin(exif, and(eq(assets.id, exif.assetId), eq(assets.visibility, "timeline")))
|
||||
.leftJoin(assetFaces, eq(assets.id, assetFaces.assetId))
|
||||
.leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false)))
|
||||
.where(and(eq(albums.ownerId, currentUser.id), eq(albums.id, id)))
|
||||
.where(and(eq(albumUsers.userId, currentUser.id), eq(albums.id, id)))
|
||||
.groupBy(albums.id)
|
||||
.limit(1);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { albums } from "@/schema/albums.schema";
|
||||
import { count, desc, eq, and, isNotNull } from "drizzle-orm";
|
||||
import { assets } from "@/schema/assets.schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albumUsers } from "@/schema/albumUsers.schema";
|
||||
import { assetFaces, person } from "@/schema";
|
||||
|
||||
export default async function handler(
|
||||
@@ -26,11 +27,12 @@ export default async function handler(
|
||||
numberOfPhotos: count(assets.id),
|
||||
})
|
||||
.from(albums)
|
||||
.innerJoin(albumUsers, and(eq(albums.id, albumUsers.albumId), eq(albumUsers.role, "owner")))
|
||||
.leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId))
|
||||
.leftJoin(assets, eq(albumsAssetsAssets.assetId, assets.id))
|
||||
.leftJoin(assetFaces, eq(assets.id, assetFaces.assetId))
|
||||
.leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false)))
|
||||
.where(and(eq(albums.ownerId, currentUser.id), eq(albums.id, id), isNotNull(person.id)))
|
||||
.where(and(eq(albumUsers.userId, currentUser.id), eq(albums.id, id), isNotNull(person.id)))
|
||||
.orderBy(desc(person.name))
|
||||
.groupBy(person.id);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { albums } from "@/schema/albums.schema";
|
||||
import { count, desc, eq, min, max, sql, and, sum, isNotNull } from "drizzle-orm";
|
||||
import { assets } from "@/schema/assets.schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albumUsers } from "@/schema/albumUsers.schema";
|
||||
import { assetFaces, exif, person } from "@/schema";
|
||||
import { IAlbum } from "@/types/album";
|
||||
|
||||
@@ -63,12 +64,13 @@ export default async function handler(
|
||||
size: sum(exif.fileSizeInByte),
|
||||
order: albums.order,
|
||||
isActivityEnabled: albums.isActivityEnabled,
|
||||
ownerId: albums.ownerId,
|
||||
ownerId: albumUsers.userId,
|
||||
description: albums.description,
|
||||
lastModifiedAssetTimestamp: max(exif.dateTimeOriginal),
|
||||
faceCount: count(sql<string>`DISTINCT ${assetFaces.personId}`), // Ensure unique personId
|
||||
})
|
||||
.from(albums)
|
||||
.innerJoin(albumUsers, and(eq(albums.id, albumUsers.albumId), eq(albumUsers.role, "owner")))
|
||||
.leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId))
|
||||
.leftJoin(assets, and(
|
||||
eq(albumsAssetsAssets.assetId, assets.id),
|
||||
@@ -78,8 +80,8 @@ export default async function handler(
|
||||
.leftJoin(exif, eq(assets.id, exif.assetId))
|
||||
.leftJoin(assetFaces, eq(assets.id, assetFaces.assetId))
|
||||
.leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false)))
|
||||
.where(eq(albums.ownerId, currentUser.id))
|
||||
.groupBy(albums.id)
|
||||
.where(eq(albumUsers.userId, currentUser.id))
|
||||
.groupBy(albums.id, albumUsers.userId)
|
||||
.orderBy(desc(albums.createdAt));
|
||||
|
||||
const sortedAlbums = sortAlbums(dbAlbums as IAlbum[], sortBy, sortOrder);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import API from "@/lib/api";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albums } from "@/schema/albums.schema";
|
||||
import { albumUsers } from "@/schema/albumUsers.schema";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
@@ -35,11 +36,16 @@ export default async function handler(
|
||||
});
|
||||
}
|
||||
|
||||
const ownedAlbumIds = db
|
||||
.select({ albumId: albumUsers.albumId })
|
||||
.from(albumUsers)
|
||||
.where(and(eq(albumUsers.userId, currentUser.id), eq(albumUsers.role, "owner")));
|
||||
|
||||
const primaryAlbum = await db
|
||||
.select()
|
||||
.from(albums)
|
||||
.where(
|
||||
and(eq(albums.id, primaryAlbumId), eq(albums.ownerId, currentUser.id))
|
||||
and(eq(albums.id, primaryAlbumId), inArray(albums.id, ownedAlbumIds))
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
@@ -51,7 +57,7 @@ export default async function handler(
|
||||
const secondaryAlbums = await db
|
||||
.select()
|
||||
.from(albums)
|
||||
.where(and(inArray(albums.id, secondaryAlbumIds), eq(albums.ownerId, currentUser.id)));
|
||||
.where(and(inArray(albums.id, secondaryAlbumIds), inArray(albums.id, ownedAlbumIds)));
|
||||
|
||||
|
||||
if (secondaryAlbums.length === 0) {
|
||||
|
||||
@@ -9,7 +9,6 @@ const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) =>
|
||||
SELECT
|
||||
a."id",
|
||||
a."ownerId",
|
||||
a."deviceId",
|
||||
a."type",
|
||||
a."originalPath",
|
||||
a."isFavorite",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { albums } from "@/schema/albums.schema";
|
||||
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
|
||||
import { albumUsers } from "@/schema/albumUsers.schema";
|
||||
import { eq, inArray, and } from "drizzle-orm";
|
||||
|
||||
export const config = {
|
||||
@@ -39,10 +40,11 @@ export default async function handler(
|
||||
})
|
||||
.from(albumsAssetsAssets)
|
||||
.innerJoin(albums, eq(albumsAssetsAssets.albumId, albums.id))
|
||||
.innerJoin(albumUsers, and(eq(albumUsers.albumId, albums.id), eq(albumUsers.role, "owner")))
|
||||
.where(
|
||||
and(
|
||||
inArray(albumsAssetsAssets.assetId, assetIds),
|
||||
eq(albums.ownerId, currentUser.id)
|
||||
eq(albumUsers.userId, currentUser.id)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -39,19 +39,11 @@ export default async function handler(
|
||||
}
|
||||
|
||||
|
||||
const durationExpr = sql<number>`
|
||||
(
|
||||
CAST(SUBSTRING(${assets.duration}, 1, 2) AS DECIMAL) * 3600 +
|
||||
CAST(SUBSTRING(${assets.duration}, 4, 2) AS DECIMAL) * 60 +
|
||||
CAST(SUBSTRING(${assets.duration}, 7, 2) AS DECIMAL) +
|
||||
CAST(SUBSTRING(${assets.duration}, 10, 3) AS DECIMAL) / 1000
|
||||
)
|
||||
`;
|
||||
const durationExpr = sql<number>`(${assets.duration}::decimal / 1000)`;
|
||||
|
||||
const dbAssets = await db
|
||||
.selectDistinctOn([assets.id], {
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
|
||||
@@ -15,7 +15,6 @@ const getRowsByDates = async (startDateDate: Date, endDateDate: Date, currentUse
|
||||
return db
|
||||
.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
@@ -46,7 +45,6 @@ const getRowsByDates = async (startDateDate: Date, endDateDate: Date, currentUse
|
||||
const getRowsByAlbums = async (currentUser: IUser, albumId: string) => {
|
||||
return db.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
|
||||
@@ -67,7 +67,6 @@ export default async function handler(
|
||||
const dbAssets = await db
|
||||
.selectDistinctOn([assets.id], {
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
|
||||
@@ -56,7 +56,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const dbAssets = await db.select({
|
||||
id: assets.id,
|
||||
deviceId: assets.deviceId,
|
||||
type: assets.type,
|
||||
originalPath: assets.originalPath,
|
||||
isFavorite: assets.isFavorite,
|
||||
@@ -92,7 +91,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return {
|
||||
...asset,
|
||||
localDateTime: new Date(asset.localDateTime),
|
||||
duration: asset.duration ? parseInt(asset.duration) : 0,
|
||||
duration: asset.duration ?? 0,
|
||||
exifImageWidth: (isFlipped(asset.orientation) ? asset.exifImageHeight : asset.exifImageWidth) ?? 0,
|
||||
exifImageHeight: (isFlipped(asset.orientation) ? asset.exifImageWidth : asset.exifImageHeight) ?? 0,
|
||||
dateTimeOriginal: new Date(asset.dateTimeOriginal || new Date()).toISOString(),
|
||||
|
||||
@@ -19,10 +19,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
// Test asset upload permission by calling a no-op endpoint
|
||||
try {
|
||||
const response = await fetch(`${ENV.IMMICH_URL}/api/assets/exist`, {
|
||||
const response = await fetch(`${ENV.IMMICH_URL}/api/assets/bulk-upload-check`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ deviceAssetIds: ["permission-check"], deviceId: "immich-power-tools-check" }),
|
||||
body: JSON.stringify({
|
||||
assets: [{ id: "permission-check", checksum: "0000000000000000000000000000000000000000" }],
|
||||
}),
|
||||
});
|
||||
result.canUpload = response.ok;
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const albumUsers = pgTable('album_user', {
|
||||
albumId: uuid('albumId').notNull(),
|
||||
userId: uuid('userId').notNull(),
|
||||
role: varchar('role').notNull().default('editor'),
|
||||
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { pgTable, uuid, varchar, timestamp, boolean, text } from 'drizzle-orm/pg
|
||||
|
||||
export const albums = pgTable('album', {
|
||||
id: uuid('id').defaultRandom().primaryKey().notNull(),
|
||||
ownerId: uuid('ownerId').notNull(),
|
||||
albumName: varchar('albumName').notNull().default('Untitled Album'),
|
||||
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
|
||||
albumThumbnailAssetId: uuid('albumThumbnailAssetId'),
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, varchar, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const assets = pgTable('asset', {
|
||||
id: uuid('id').defaultRandom().primaryKey().notNull(),
|
||||
deviceAssetId: varchar('deviceAssetId').notNull(),
|
||||
ownerId: uuid('ownerId').notNull(),
|
||||
deviceId: varchar('deviceId').notNull(),
|
||||
type: varchar('type').notNull(),
|
||||
originalPath: varchar('originalPath').notNull(),
|
||||
fileCreatedAt: timestamp('fileCreatedAt', { withTimezone: true }).notNull(),
|
||||
fileModifiedAt: timestamp('fileModifiedAt', { withTimezone: true }).notNull(),
|
||||
isFavorite: boolean('isFavorite').default(false).notNull(),
|
||||
duration: varchar('duration'),
|
||||
duration: integer('duration'),
|
||||
// checksum: bytea('checksum').notNull(),
|
||||
visibility: varchar('visibility').default('timeline').notNull(),
|
||||
livePhotoVideoId: uuid('livePhotoVideoId').references((): any => assets.id, { onDelete: 'set null', onUpdate: 'cascade' }),
|
||||
|
||||
Vendored
+1
-6
@@ -1,7 +1,6 @@
|
||||
export interface IAsset {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
deviceId: string;
|
||||
type: string;
|
||||
originalPath: string;
|
||||
isFavorite: boolean;
|
||||
@@ -32,9 +31,7 @@ export interface IDuplicateAssetRecord {
|
||||
|
||||
export interface IDuplicateAsset {
|
||||
id: string;
|
||||
deviceAssetId: string;
|
||||
ownerId: string;
|
||||
deviceId: string;
|
||||
libraryId: null;
|
||||
type: string;
|
||||
originalPath: string;
|
||||
@@ -46,10 +43,8 @@ export interface IDuplicateAsset {
|
||||
localDateTime: Date;
|
||||
updatedAt: Date;
|
||||
isFavorite: boolean;
|
||||
isArchived: boolean;
|
||||
isTrashed: boolean;
|
||||
visibility: string;
|
||||
duration: string;
|
||||
duration: number | null;
|
||||
exifInfo: IDuplicateAssetExifInfo;
|
||||
livePhotoVideoId: null;
|
||||
people: any[];
|
||||
|
||||
Reference in New Issue
Block a user