mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/mobile-ocr
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import {
|
||||
ExpressionBuilder,
|
||||
Insertable,
|
||||
Kysely,
|
||||
NotNull,
|
||||
Selectable,
|
||||
ShallowDehydrateObject,
|
||||
sql,
|
||||
Updateable,
|
||||
} from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns, Exif } from 'src/database';
|
||||
import { columns } from 'src/database';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { DB } from 'src/schema';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
@@ -56,7 +66,9 @@ const withAssets = (eb: ExpressionBuilder<DB, 'album'>) => {
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.select((eb) => eb.table('asset_exif').$castTo<Exif>().as('exifInfo'))
|
||||
.select((eb) =>
|
||||
eb.table('asset_exif').$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>().as('exifInfo'),
|
||||
)
|
||||
.innerJoin('album_asset', 'album_asset.assetId', 'asset.id')
|
||||
.whereRef('album_asset.albumId', '=', 'album.id')
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
|
||||
@@ -9,7 +9,6 @@ import { DB } from 'src/schema';
|
||||
import {
|
||||
anyUuid,
|
||||
asUuid,
|
||||
toJson,
|
||||
withDefaultVisibility,
|
||||
withEdits,
|
||||
withExif,
|
||||
@@ -105,7 +104,7 @@ export class AssetJobRepository {
|
||||
getForMigrationJob(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select(['asset.id', 'asset.ownerId', 'asset.encodedVideoPath'])
|
||||
.select(['asset.id', 'asset.ownerId'])
|
||||
.select(withFiles)
|
||||
.where('asset.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
@@ -269,7 +268,6 @@ export class AssetJobRepository {
|
||||
'asset.libraryId',
|
||||
'asset.ownerId',
|
||||
'asset.livePhotoVideoId',
|
||||
'asset.encodedVideoPath',
|
||||
'asset.originalPath',
|
||||
'asset.isOffline',
|
||||
])
|
||||
@@ -296,7 +294,12 @@ export class AssetJobRepository {
|
||||
.as('stack_result'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => toJson(eb, 'stack_result').as('stack'))
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.toJson(eb.table('stack_result'))
|
||||
.$castTo<{ id: string; primaryAssetId: string; assets: { id: string }[] } | null>()
|
||||
.as('stack'),
|
||||
)
|
||||
.where('asset.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
@@ -306,11 +309,21 @@ export class AssetJobRepository {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select(['asset.id'])
|
||||
.where('asset.type', '=', AssetType.Video)
|
||||
.where('asset.type', '=', sql.lit(AssetType.Video))
|
||||
.$if(!force, (qb) =>
|
||||
qb
|
||||
.where((eb) => eb.or([eb('asset.encodedVideoPath', 'is', null), eb('asset.encodedVideoPath', '=', '')]))
|
||||
.where('asset.visibility', '!=', AssetVisibility.Hidden),
|
||||
.where((eb) =>
|
||||
eb.not(
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('asset_file')
|
||||
.select('asset_file.id')
|
||||
.whereRef('asset_file.assetId', '=', 'asset.id')
|
||||
.where('asset_file.type', '=', sql.lit(AssetFileType.EncodedVideo)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)),
|
||||
)
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.stream();
|
||||
@@ -320,9 +333,10 @@ export class AssetJobRepository {
|
||||
getForVideoConversion(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath'])
|
||||
.select(['asset.id', 'asset.ownerId', 'asset.originalPath'])
|
||||
.select(withFiles)
|
||||
.where('asset.id', '=', id)
|
||||
.where('asset.type', '=', AssetType.Video)
|
||||
.where('asset.type', '=', sql.lit(AssetType.Video))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@@ -353,6 +367,7 @@ export class AssetJobRepository {
|
||||
'asset.checksum',
|
||||
'asset.originalPath',
|
||||
'asset.isExternal',
|
||||
'asset.visibility',
|
||||
'asset.originalFileName',
|
||||
'asset.livePhotoVideoId',
|
||||
'asset.fileCreatedAt',
|
||||
@@ -367,13 +382,16 @@ export class AssetJobRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getForStorageTemplateJob(id: string) {
|
||||
return this.storageTemplateAssetQuery().where('asset.id', '=', id).executeTakeFirst();
|
||||
getForStorageTemplateJob(id: string, options?: { includeHidden?: boolean }) {
|
||||
return this.storageTemplateAssetQuery()
|
||||
.where('asset.id', '=', id)
|
||||
.$if(!options?.includeHidden, (qb) => qb.where('asset.visibility', '!=', AssetVisibility.Hidden))
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
streamForStorageTemplateJob() {
|
||||
return this.storageTemplateAssetQuery().stream();
|
||||
return this.storageTemplateAssetQuery().where('asset.visibility', '!=', AssetVisibility.Hidden).stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.DATE], stream: true })
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely';
|
||||
import {
|
||||
ExpressionBuilder,
|
||||
Insertable,
|
||||
Kysely,
|
||||
NotNull,
|
||||
Selectable,
|
||||
SelectQueryBuilder,
|
||||
ShallowDehydrateObject,
|
||||
sql,
|
||||
Updateable,
|
||||
UpdateResult,
|
||||
} from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
@@ -25,6 +36,7 @@ import {
|
||||
withExif,
|
||||
withFaces,
|
||||
withFacesAndPeople,
|
||||
withFilePath,
|
||||
withFiles,
|
||||
withLibrary,
|
||||
withOwner,
|
||||
@@ -36,6 +48,13 @@ import { globToSqlPattern } from 'src/utils/misc';
|
||||
|
||||
export type AssetStats = Record<AssetType, number>;
|
||||
|
||||
export interface BoundingBox {
|
||||
west: number;
|
||||
south: number;
|
||||
east: number;
|
||||
north: number;
|
||||
}
|
||||
|
||||
interface AssetStatsOptions {
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
@@ -64,6 +83,7 @@ interface AssetBuilderOptions {
|
||||
assetType?: AssetType;
|
||||
visibility?: AssetVisibility;
|
||||
withCoordinates?: boolean;
|
||||
bbox?: BoundingBox;
|
||||
}
|
||||
|
||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||
@@ -120,6 +140,34 @@ interface GetByIdsRelations {
|
||||
const distinctLocked = <T extends LockableProperty[] | null>(eb: ExpressionBuilder<DB, 'asset_exif'>, columns: T) =>
|
||||
sql<T>`nullif(array(select distinct unnest(${eb.ref('asset_exif.lockedProperties')} || ${columns})), '{}')`;
|
||||
|
||||
const getBoundingCircle = (bbox: BoundingBox) => {
|
||||
const { west, south, east, north } = bbox;
|
||||
const eastUnwrapped = west <= east ? east : east + 360;
|
||||
const centerLongitude = (((west + eastUnwrapped) / 2 + 540) % 360) - 180;
|
||||
const centerLatitude = (south + north) / 2;
|
||||
const radius = sql<number>`greatest(
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${south}, ${west})),
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${south}, ${east})),
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${north}, ${west})),
|
||||
earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${north}, ${east}))
|
||||
)`;
|
||||
|
||||
return { centerLatitude, centerLongitude, radius };
|
||||
};
|
||||
|
||||
const withBoundingBox = <T>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_exif', T>, bbox: BoundingBox) => {
|
||||
const { west, south, east, north } = bbox;
|
||||
const withLatitude = qb.where('asset_exif.latitude', '>=', south).where('asset_exif.latitude', '<=', north);
|
||||
|
||||
if (west <= east) {
|
||||
return withLatitude.where('asset_exif.longitude', '>=', west).where('asset_exif.longitude', '<=', east);
|
||||
}
|
||||
|
||||
return withLatitude.where((eb) =>
|
||||
eb.or([eb('asset_exif.longitude', '>=', west), eb('asset_exif.longitude', '<=', east)]),
|
||||
);
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AssetRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
@@ -358,7 +406,7 @@ export class AssetRepository {
|
||||
(qb) =>
|
||||
qb
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
.select(['asset.id', 'asset.localDateTime'])
|
||||
.innerJoin('asset_job_status', 'asset.id', 'asset_job_status.assetId')
|
||||
.where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
|
||||
.where('asset.ownerId', '=', anyUuid(ownerIds))
|
||||
@@ -377,9 +425,7 @@ export class AssetRepository {
|
||||
.as('a'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.innerJoin('asset_exif', 'a.id', 'asset_exif.assetId')
|
||||
.selectAll('a')
|
||||
.select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo')),
|
||||
.selectAll('a'),
|
||||
)
|
||||
.selectFrom('res')
|
||||
.select(sql<number>`date_part('year', ("localDateTime" at time zone 'UTC')::date)::int`.as('year'))
|
||||
@@ -510,7 +556,11 @@ export class AssetRepository {
|
||||
eb
|
||||
.selectFrom('asset as stacked')
|
||||
.selectAll('stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.select((eb) =>
|
||||
eb
|
||||
.fn<ShallowDehydrateObject<Selectable<AssetTable>>>('array_agg', [eb.table('stacked')])
|
||||
.as('assets'),
|
||||
)
|
||||
.whereRef('stacked.stackId', '=', 'stack.id')
|
||||
.whereRef('stacked.id', '!=', 'stack.primaryAssetId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
@@ -519,7 +569,7 @@ export class AssetRepository {
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack')),
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
@@ -651,6 +701,20 @@ export class AssetRepository {
|
||||
.select(truncatedDate<Date>().as('timeBucket'))
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
|
||||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(!!options.bbox, (qb) => {
|
||||
const bbox = options.bbox!;
|
||||
const circle = getBoundingCircle(bbox);
|
||||
|
||||
const withBoundingCircle = qb
|
||||
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.where(
|
||||
sql`earth_box(ll_to_earth_public(${circle.centerLatitude}, ${circle.centerLongitude}), ${circle.radius})`,
|
||||
'@>',
|
||||
sql`ll_to_earth_public(asset_exif.latitude, asset_exif.longitude)`,
|
||||
);
|
||||
|
||||
return withBoundingBox(withBoundingCircle, bbox);
|
||||
})
|
||||
.$if(options.visibility === undefined, withDefaultVisibility)
|
||||
.$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!))
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
@@ -686,6 +750,7 @@ export class AssetRepository {
|
||||
params: [DummyValue.TIME_BUCKET, { withStacked: true }, { user: { id: DummyValue.UUID } }],
|
||||
})
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions, auth: AuthDto) {
|
||||
const order = options.order ?? 'desc';
|
||||
const query = this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
@@ -725,6 +790,18 @@ export class AssetRepository {
|
||||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(options.visibility == undefined, withDefaultVisibility)
|
||||
.$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!))
|
||||
.$if(!!options.bbox, (qb) => {
|
||||
const bbox = options.bbox!;
|
||||
const circle = getBoundingCircle(bbox);
|
||||
|
||||
const withBoundingCircle = qb.where(
|
||||
sql`earth_box(ll_to_earth_public(${circle.centerLatitude}, ${circle.centerLongitude}), ${circle.radius})`,
|
||||
'@>',
|
||||
sql`ll_to_earth_public(asset_exif.latitude, asset_exif.longitude)`,
|
||||
);
|
||||
|
||||
return withBoundingBox(withBoundingCircle, bbox);
|
||||
})
|
||||
.where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
@@ -771,7 +848,8 @@ export class AssetRepository {
|
||||
)
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||
.orderBy('asset.fileCreatedAt', options.order ?? 'desc'),
|
||||
.orderBy(sql`(asset."localDateTime" AT TIME ZONE 'UTC')::date`, order)
|
||||
.orderBy('asset.fileCreatedAt', order),
|
||||
)
|
||||
.with('agg', (qb) =>
|
||||
qb
|
||||
@@ -942,8 +1020,21 @@ export class AssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteFile({ assetId, type }: { assetId: string; type: AssetFileType }): Promise<void> {
|
||||
await this.db.deleteFrom('asset_file').where('assetId', '=', asUuid(assetId)).where('type', '=', type).execute();
|
||||
async deleteFile({
|
||||
assetId,
|
||||
type,
|
||||
edited,
|
||||
}: {
|
||||
assetId: string;
|
||||
type: AssetFileType;
|
||||
edited?: boolean;
|
||||
}): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('asset_file')
|
||||
.where('assetId', '=', asUuid(assetId))
|
||||
.where('type', '=', type)
|
||||
.$if(edited !== undefined, (qb) => qb.where('isEdited', '=', edited!))
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteFiles(files: Pick<Selectable<AssetFileTable>, 'id'>[]): Promise<void> {
|
||||
@@ -1062,7 +1153,8 @@ export class AssetRepository {
|
||||
async getForVideo(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select(['asset.encodedVideoPath', 'asset.originalPath'])
|
||||
.select(['asset.originalPath'])
|
||||
.select((eb) => withFilePath(eb, AssetFileType.EncodedVideo).as('encodedVideoPath'))
|
||||
.where('asset.id', '=', id)
|
||||
.where('asset.type', '=', AssetType.Video)
|
||||
.executeTakeFirst();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import 'src/schema'; // make sure all schema definitions are imported for schemaFromCode
|
||||
import { DB } from 'src/schema';
|
||||
import { immich_uuid_v7 } from 'src/schema/functions';
|
||||
import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types';
|
||||
import { vectorIndexQuery } from 'src/utils/database';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
@@ -288,7 +289,11 @@ export class DatabaseRepository {
|
||||
}
|
||||
|
||||
async getSchemaDrift() {
|
||||
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
|
||||
const source = schemaFromCode({
|
||||
overrides: true,
|
||||
namingStrategy: 'default',
|
||||
uuidFunction: (version) => (version === 7 ? `${immich_uuid_v7.name}()` : 'uuid_generate_v4()'),
|
||||
});
|
||||
const { database } = this.configRepository.getEnv();
|
||||
const target = await schemaFromDatabase({ connection: database.config });
|
||||
|
||||
@@ -426,7 +431,6 @@ export class DatabaseRepository {
|
||||
.updateTable('asset')
|
||||
.set((eb) => ({
|
||||
originalPath: eb.fn('REGEXP_REPLACE', ['originalPath', source, target]),
|
||||
encodedVideoPath: eb.fn('REGEXP_REPLACE', ['encodedVideoPath', source, target]),
|
||||
}))
|
||||
.execute();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, NotNull, sql } from 'kysely';
|
||||
import { Kysely, NotNull, Selectable, ShallowDehydrateObject, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetType, VectorIndex } from 'src/enum';
|
||||
import { probes } from 'src/repositories/database.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { anyUuid, asUuid, withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
interface DuplicateSearch {
|
||||
@@ -39,15 +39,15 @@ export class DuplicateRepository {
|
||||
qb
|
||||
.selectFrom('asset_exif')
|
||||
.selectAll('asset')
|
||||
.select((eb) => eb.table('asset_exif').as('exifInfo'))
|
||||
.select((eb) =>
|
||||
eb.table('asset_exif').$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>().as('exifInfo'),
|
||||
)
|
||||
.whereRef('asset_exif.assetId', '=', 'asset.id')
|
||||
.as('asset2'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select('asset.duplicateId')
|
||||
.select((eb) =>
|
||||
eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').$castTo<MapAsset[]>().as('assets'),
|
||||
)
|
||||
.select((eb) => eb.fn.jsonAgg('asset2').orderBy('asset.localDateTime', 'asc').as('assets'))
|
||||
.where('asset.ownerId', '=', asUuid(userId))
|
||||
.where('asset.duplicateId', 'is not', null)
|
||||
.$narrowType<{ duplicateId: NotNull }>()
|
||||
|
||||
@@ -162,6 +162,7 @@ export class EmailRepository {
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
tls: { rejectUnauthorized: !options.ignoreCert },
|
||||
secure: options.secure,
|
||||
auth:
|
||||
options.username || options.password
|
||||
? {
|
||||
|
||||
@@ -107,7 +107,7 @@ export class MediaRepository {
|
||||
ExposureTime: tags.exposureTime,
|
||||
ProfileDescription: tags.profileDescription,
|
||||
ColorSpace: tags.colorspace,
|
||||
Rating: tags.rating,
|
||||
Rating: tags.rating === null ? 0 : tags.rating,
|
||||
// specially convert Orientation to numeric Orientation# for exiftool
|
||||
'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
||||
|
||||
AndroidMake?: string;
|
||||
AndroidModel?: string;
|
||||
DeviceManufacturer?: string;
|
||||
DeviceModelName?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -70,7 +70,16 @@ export class OAuthRepository {
|
||||
|
||||
try {
|
||||
const tokens = await authorizationCodeGrant(client, new URL(url), { expectedState, pkceCodeVerifier });
|
||||
const profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck);
|
||||
|
||||
let profile: OAuthProfile;
|
||||
const tokenClaims = tokens.claims();
|
||||
if (tokenClaims && 'email' in tokenClaims) {
|
||||
this.logger.debug('Using ID token claims instead of userinfo endpoint');
|
||||
profile = tokenClaims as OAuthProfile;
|
||||
} else {
|
||||
profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck);
|
||||
}
|
||||
|
||||
if (!profile.sub) {
|
||||
throw new Error('Unexpected profile response, no `sub`');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, OrderByDirection, Selectable, sql } from 'kysely';
|
||||
import { Kysely, OrderByDirection, Selectable, ShallowDehydrateObject, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
@@ -433,7 +433,7 @@ export class SearchRepository {
|
||||
.select((eb) =>
|
||||
eb
|
||||
.fn('to_jsonb', [eb.table('asset_exif')])
|
||||
.$castTo<Selectable<AssetExifTable>>()
|
||||
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>>>()
|
||||
.as('exifInfo'),
|
||||
)
|
||||
.orderBy('asset_exif.city')
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Insertable, Kysely, Selectable, ShallowDehydrateObject, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Album, columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
|
||||
export type SharedLinkSearchOptions = {
|
||||
@@ -106,11 +107,15 @@ export class SharedLinkRepository {
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('a').filterWhere('a.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.$castTo<
|
||||
(ShallowDehydrateObject<Selectable<AssetTable>> & {
|
||||
exifInfo: ShallowDehydrateObject<Selectable<AssetExifTable>>;
|
||||
})[]
|
||||
>()
|
||||
.as('assets'),
|
||||
)
|
||||
.groupBy(['shared_link.id', sql`"album".*`])
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||
.select((eb) => eb.fn.toJson(eb.table('album')).$castTo<ShallowDehydrateObject<Album> | null>().as('album'))
|
||||
.where('shared_link.id', '=', id)
|
||||
.where('shared_link.userId', '=', userId)
|
||||
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
|
||||
@@ -124,19 +129,18 @@ export class SharedLinkRepository {
|
||||
.selectFrom('shared_link')
|
||||
.selectAll('shared_link')
|
||||
.where('shared_link.userId', '=', userId)
|
||||
.leftJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'shared_link.id')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('asset')
|
||||
.select((eb) => eb.fn.jsonAgg('asset').as('assets'))
|
||||
.whereRef('asset.id', '=', 'shared_link_asset.assetId')
|
||||
.selectFrom('shared_link_asset')
|
||||
.whereRef('shared_link.id', '=', 'shared_link_asset.sharedLinkId')
|
||||
.innerJoin('asset', 'asset.id', 'shared_link_asset.assetId')
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.as('assets'),
|
||||
(join) => join.onTrue(),
|
||||
.selectAll('asset')
|
||||
.orderBy('asset.fileCreatedAt', 'asc')
|
||||
.limit(1),
|
||||
).as('assets'),
|
||||
)
|
||||
.select('assets.assets')
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
@@ -174,12 +178,11 @@ export class SharedLinkRepository {
|
||||
.as('album'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
|
||||
.select((eb) => eb.fn.toJson('album').$castTo<ShallowDehydrateObject<Album> | null>().as('album'))
|
||||
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
|
||||
.$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!))
|
||||
.$if(!!id, (eb) => eb.where('shared_link.id', '=', id!))
|
||||
.orderBy('shared_link.createdAt', 'desc')
|
||||
.distinctOn(['shared_link.createdAt'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -246,6 +249,21 @@ export class SharedLinkRepository {
|
||||
await this.db.deleteFrom('shared_link').where('shared_link.id', '=', id).execute();
|
||||
}
|
||||
|
||||
@ChunkedArray({ paramIndex: 1 })
|
||||
async addAssets(id: string, assetIds: string[]) {
|
||||
if (assetIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.db
|
||||
.insertInto('shared_link_asset')
|
||||
.values(assetIds.map((assetId) => ({ assetId, sharedLinkId: id })))
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.returning(['shared_link_asset.assetId'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
private getSharedLinks(id: string) {
|
||||
return this.db
|
||||
.selectFrom('shared_link')
|
||||
@@ -269,7 +287,11 @@ export class SharedLinkRepository {
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`)
|
||||
.$castTo<MapAsset[]>()
|
||||
.$castTo<
|
||||
(ShallowDehydrateObject<Selectable<AssetTable>> & {
|
||||
exifInfo: ShallowDehydrateObject<Selectable<AssetExifTable>>;
|
||||
})[]
|
||||
>()
|
||||
.as('assets'),
|
||||
)
|
||||
.groupBy('shared_link.id')
|
||||
|
||||
Reference in New Issue
Block a user