chore(deps): update dependency eslint-plugin-unicorn to v78 (#32588)
Co-authored-by: Jason Rasmussen <jason@rasm.me> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> (cherry picked from commit 72e2bc8b0bc4a7e0a84ac16c0bde7f5e538729fb)
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^75.0.0",
|
||||
"eslint-plugin-unicorn": "^78.0.0",
|
||||
"exiftool-vendored": "^39.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
|
||||
@@ -14,9 +14,7 @@ type DayDistributionStrategy = (
|
||||
rng: SeededRandom,
|
||||
) => MockTimelineAsset[];
|
||||
|
||||
/**
|
||||
* Strategies for determining total asset count per month
|
||||
*/
|
||||
/** Strategies for determining total asset count per month */
|
||||
export const ASSET_DISTRIBUTION: Record<MonthDistribution, AssetDistributionStrategy | null> = {
|
||||
empty: null, // Special case - handled separately
|
||||
sparse: (rng) => rng.nextInt(3, 9), // 3-8 assets
|
||||
@@ -25,9 +23,7 @@ export const ASSET_DISTRIBUTION: Record<MonthDistribution, AssetDistributionStra
|
||||
'very-dense': (rng) => rng.nextInt(80, 151), // 80-150 assets
|
||||
};
|
||||
|
||||
/**
|
||||
* Strategies for distributing assets across days within a month
|
||||
*/
|
||||
/** Strategies for distributing assets across days within a month */
|
||||
export const DAY_DISTRIBUTION: Record<DayPattern, DayDistributionStrategy> = {
|
||||
'single-day': (year, month, daysInMonth, totalAssets, ownerId, rng) => {
|
||||
// All assets on one day in the middle of the month
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* Generator functions for timeline model objects
|
||||
*/
|
||||
/** Generator functions for timeline model objects */
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
@@ -12,9 +10,7 @@ import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns';
|
||||
import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config';
|
||||
import { ASPECT_RATIO_WEIGHTS, GENERATION_CONSTANTS, validateTimelineConfig } from './timeline-config';
|
||||
|
||||
/**
|
||||
* Generate a random aspect ratio based on weighted probabilities
|
||||
*/
|
||||
/** Generate a random aspect ratio based on weighted probabilities */
|
||||
export function generateAspectRatio(rng: SeededRandom): string {
|
||||
const random = rng.next();
|
||||
let cumulative = 0;
|
||||
@@ -89,9 +85,7 @@ export function generateAsset(
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate assets for a specific day
|
||||
*/
|
||||
/** Generate assets for a specific day */
|
||||
export function generateDayAssets(
|
||||
year: number,
|
||||
month: number,
|
||||
@@ -104,10 +98,10 @@ export function generateDayAssets(
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute assets evenly across consecutive days
|
||||
*
|
||||
* @returns Array of generated timeline assets
|
||||
*/
|
||||
Distribute assets evenly across consecutive days
|
||||
|
||||
@returns Array of generated timeline assets
|
||||
*/
|
||||
export function generateConsecutiveDays(
|
||||
year: number,
|
||||
month: number,
|
||||
@@ -133,9 +127,7 @@ export function generateConsecutiveDays(
|
||||
return assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate assets for a month with specified distribution pattern
|
||||
*/
|
||||
/** Generate assets for a month with specified distribution pattern */
|
||||
export function generateMonthAssets(
|
||||
year: number,
|
||||
month: number,
|
||||
@@ -173,9 +165,7 @@ export function generateMonthAssets(
|
||||
return assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main generator function for timeline data
|
||||
*/
|
||||
/** Main generator function for timeline data */
|
||||
export function generateTimelineData(config: TimelineConfig): MockTimelineData {
|
||||
validateTimelineConfig(config);
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* REST API output functions for converting timeline data to API response formats
|
||||
*/
|
||||
/** REST API output functions for converting timeline data to API response formats */
|
||||
|
||||
import {
|
||||
AlbumUserRole,
|
||||
@@ -19,9 +17,7 @@ import { signupDto } from 'src/fixtures.js';
|
||||
import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils.js';
|
||||
import type { MockTimelineAsset, MockTimelineData } from './timeline-config';
|
||||
|
||||
/**
|
||||
* Convert timeline/asset models to columnar format (parallel arrays)
|
||||
*/
|
||||
/** Convert timeline/asset models to columnar format (parallel arrays) */
|
||||
export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetResponseDto {
|
||||
const result: TimeBucketAssetResponseDto = {
|
||||
id: [],
|
||||
@@ -70,11 +66,11 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a single bucket from timeline data (mimics getTimeBucket API)
|
||||
* Automatically handles both ISO timestamp and simple month formats
|
||||
* Returns data in columnar format matching the actual API
|
||||
* When albumId is provided, only returns assets from that album
|
||||
*/
|
||||
Extract a single bucket from timeline data (mimics getTimeBucket API)
|
||||
Automatically handles both ISO timestamp and simple month formats
|
||||
Returns data in columnar format matching the actual API
|
||||
When albumId is provided, only returns assets from that album
|
||||
*/
|
||||
export function getTimeBucket(
|
||||
timelineData: MockTimelineData,
|
||||
timeBucket: string,
|
||||
@@ -141,16 +137,16 @@ export type Changes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to determine if an asset should be included based on filter criteria
|
||||
* @param asset - The asset to check
|
||||
* @param isTrashed - Filter for trashed status (undefined means no filter)
|
||||
* @param isArchived - Filter for archived status (undefined means no filter)
|
||||
* @param isFavorite - Filter for favorite status (undefined means no filter)
|
||||
* @param deletedAssetIds - Set of IDs for assets that have been deleted
|
||||
* @param archivedAssetIds - Set of IDs for assets that have been archived
|
||||
* @param favoritedAssetIds - Set of IDs for assets that have been favorited
|
||||
* @returns true if the asset matches all filter criteria
|
||||
*/
|
||||
Helper function to determine if an asset should be included based on filter criteria
|
||||
@param asset - The asset to check
|
||||
@param isTrashed - Filter for trashed status (undefined means no filter)
|
||||
@param isArchived - Filter for archived status (undefined means no filter)
|
||||
@param isFavorite - Filter for favorite status (undefined means no filter)
|
||||
@param deletedAssetIds - Set of IDs for assets that have been deleted
|
||||
@param archivedAssetIds - Set of IDs for assets that have been archived
|
||||
@param favoritedAssetIds - Set of IDs for assets that have been favorited
|
||||
@returns true if the asset matches all filter criteria
|
||||
*/
|
||||
function shouldIncludeAsset(
|
||||
asset: MockTimelineAsset,
|
||||
isTrashed: boolean | undefined,
|
||||
@@ -175,9 +171,9 @@ function shouldIncludeAsset(
|
||||
return isFavorite === undefined || actuallyFavorited === isFavorite;
|
||||
}
|
||||
/**
|
||||
* Get summary for all buckets (mimics getTimeBuckets API)
|
||||
* When albumId is provided, only includes buckets that contain assets from that album
|
||||
*/
|
||||
Get summary for all buckets (mimics getTimeBuckets API)
|
||||
When albumId is provided, only includes buckets that contain assets from that album
|
||||
*/
|
||||
export function getTimeBuckets(
|
||||
timelineData: MockTimelineData,
|
||||
isTrashed: boolean | undefined,
|
||||
@@ -279,9 +275,9 @@ const createDefaultOwner = (ownerId: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a TimelineAssetConfig to a full AssetResponseDto
|
||||
* This matches the response from GET /api/assets/:id
|
||||
*/
|
||||
Convert a TimelineAssetConfig to a full AssetResponseDto
|
||||
This matches the response from GET /api/assets/:id
|
||||
*/
|
||||
export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserResponseDto): AssetResponseDto {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -292,7 +288,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
||||
make: null,
|
||||
model: null,
|
||||
exifImageWidth: asset.ratio > 1 ? 4000 : 3000,
|
||||
exifImageHeight: asset.ratio > 1 ? Math.round(4000 / asset.ratio) : Math.round(3000 * asset.ratio),
|
||||
exifImageHeight: Math.round(asset.ratio > 1 ? 4000 : 3000 / asset.ratio),
|
||||
fileSizeInByte: asset.fileSizeInByte,
|
||||
orientation: '1',
|
||||
dateTimeOriginal: asset.fileCreatedAt,
|
||||
@@ -348,9 +344,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single asset by ID from timeline data
|
||||
* This matches the response from GET /api/assets/:id
|
||||
*/
|
||||
Get a single asset by ID from timeline data
|
||||
This matches the response from GET /api/assets/:id
|
||||
*/
|
||||
export function getAsset(
|
||||
timelineData: MockTimelineData,
|
||||
assetId: string,
|
||||
@@ -368,9 +364,9 @@ export function getAsset(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a mock album from timeline data
|
||||
* This matches the response from GET /api/albums/:id
|
||||
*/
|
||||
Get a mock album from timeline data
|
||||
This matches the response from GET /api/albums/:id
|
||||
*/
|
||||
export function getAlbum(
|
||||
timelineData: MockTimelineData,
|
||||
ownerId: string,
|
||||
|
||||
@@ -57,9 +57,7 @@ export type MonthSpec = {
|
||||
pattern: DayPattern;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for timeline data generation
|
||||
*/
|
||||
/** Configuration for timeline data generation */
|
||||
export type TimelineConfig = {
|
||||
ownerId?: string;
|
||||
months: MonthSpec[];
|
||||
@@ -88,9 +86,7 @@ export type SerializedTimelineData = {
|
||||
album: MockAlbum;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a TimelineConfig object to ensure all values are within expected ranges
|
||||
*/
|
||||
/** Validates a TimelineConfig object to ensure all values are within expected ranges */
|
||||
export function validateTimelineConfig(config: TimelineConfig): void {
|
||||
if (!config.months || config.months.length === 0) {
|
||||
throw new Error('TimelineConfig must contain at least one month');
|
||||
@@ -146,9 +142,7 @@ export function validateTimelineConfig(config: TimelineConfig): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default timeline configuration
|
||||
*/
|
||||
/** Create a default timeline configuration */
|
||||
export function createDefaultTimelineConfig(): TimelineConfig {
|
||||
const months: MonthSpec[] = [
|
||||
// 2024 - Mix of patterns
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config.js';
|
||||
|
||||
/**
|
||||
* Linear Congruential Generator for deterministic pseudo-random numbers
|
||||
*/
|
||||
/** Linear Congruential Generator for deterministic pseudo-random numbers */
|
||||
export class SeededRandom {
|
||||
private seed: number;
|
||||
|
||||
@@ -11,38 +9,32 @@ export class SeededRandom {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate next random number in range [0, 1)
|
||||
*/
|
||||
/** Generate next random number in range [0, 1) */
|
||||
next(): number {
|
||||
// LCG parameters from Numerical Recipes
|
||||
this.seed = (this.seed * 1_664_525 + 1_013_904_223) % 2_147_483_647;
|
||||
return this.seed / 2_147_483_647;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random integer in range [min, max)
|
||||
*/
|
||||
/** Generate random integer in range [min, max) */
|
||||
nextInt(min: number, max: number): number {
|
||||
return Math.floor(this.next() * (max - min)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random boolean with given probability
|
||||
*/
|
||||
/** Generate random boolean with given probability */
|
||||
nextBoolean(probability = 0.5): boolean {
|
||||
return this.next() < probability;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select random days using seed variation to avoid collisions.
|
||||
*
|
||||
* @param daysInMonth - Total number of days in the month
|
||||
* @param numDays - Number of days to select
|
||||
* @param rng - Random number generator instance
|
||||
* @returns Array of selected day numbers, sorted in descending order
|
||||
*/
|
||||
Select random days using seed variation to avoid collisions.
|
||||
|
||||
@param daysInMonth - Total number of days in the month
|
||||
@param numDays - Number of days to select
|
||||
@param rng - Random number generator instance
|
||||
@returns Array of selected day numbers, sorted in descending order
|
||||
*/
|
||||
export function selectRandomDays(daysInMonth: number, numDays: number, rng: SeededRandom): number[] {
|
||||
const selectedDays = new Set<number>();
|
||||
const maxAttempts = numDays * GENERATION_CONSTANTS.MAX_SELECT_ATTEMPTS; // Safety limit
|
||||
@@ -64,9 +56,7 @@ export function selectRandomDays(daysInMonth: number, numDays: number, rng: Seed
|
||||
return [...selectedDays].toSorted((a, b) => b - a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select item from array using seeded random
|
||||
*/
|
||||
/** Select item from array using seeded random */
|
||||
export function selectRandom<T>(arr: T[], rng: SeededRandom): T {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('Cannot select from empty array');
|
||||
@@ -75,9 +65,7 @@ export function selectRandom<T>(arr: T[], rng: SeededRandom): T {
|
||||
return arr[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select multiple random items from array using seeded random without duplicates
|
||||
*/
|
||||
/** Select multiple random items from array using seeded random without duplicates */
|
||||
export function selectRandomMultiple<T>(arr: T[], count: number, rng: SeededRandom): T[] {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('Cannot select from empty array');
|
||||
@@ -104,11 +92,11 @@ export function selectRandomMultiple<T>(arr: T[], count: number, rng: SeededRand
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse timeBucket parameter to extract year-month key
|
||||
* Handles both formats:
|
||||
* - ISO timestamp: "2024-12-01T00:00:00.000Z" -> "2024-12-01"
|
||||
* - Simple format: "2024-12-01" -> "2024-12-01"
|
||||
*/
|
||||
Parse timeBucket parameter to extract year-month key
|
||||
Handles both formats:
|
||||
- ISO timestamp: "2024-12-01T00:00:00.000Z" -> "2024-12-01"
|
||||
- Simple format: "2024-12-01" -> "2024-12-01"
|
||||
*/
|
||||
export function parseTimeBucketKey(timeBucket: string): string {
|
||||
if (!timeBucket) {
|
||||
throw new Error('timeBucket parameter cannot be empty');
|
||||
@@ -150,6 +138,7 @@ export function getMockAsset(
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
|
||||
if (unit === 'day') {
|
||||
return !date1.startOf('day').equals(date2.startOf('day'));
|
||||
|
||||
@@ -242,6 +242,7 @@ test.describe('OCR text interaction', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const createTouch = (id: number, x: number, y: number) => {
|
||||
return new Touch({
|
||||
identifier: id,
|
||||
|
||||
@@ -150,9 +150,9 @@ test.describe('Timeline', () => {
|
||||
});
|
||||
test.describe('keyboard', () => {
|
||||
/**
|
||||
* This text tests keyboard nativation, and also ensures that the scroll-to-asset behavior
|
||||
* scrolls the minimum amount. That is, if you are navigating using right arrow (auto scrolling
|
||||
* as necessary downwards), then the asset should always be at the lowest row of the grid.
|
||||
This text tests keyboard nativation, and also ensures that the scroll-to-asset behavior
|
||||
scrolls the minimum amount. That is, if you are navigating using right arrow (auto scrolling
|
||||
as necessary downwards), then the asset should always be at the lowest row of the grid.
|
||||
*/
|
||||
test('Next/previous asset - ArrowRight/ArrowLeft', async ({ page }) => {
|
||||
await pageUtils.openPhotosPage(page);
|
||||
|
||||
+5
-5
@@ -270,13 +270,13 @@ export const utils = {
|
||||
}
|
||||
},
|
||||
|
||||
waitForWebsocketEvent: ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
|
||||
waitForWebsocketEvent: ({ event, id, total: count, timeout: ms = 10_000 }: WaitOptions): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!id && !count) {
|
||||
reject(new Error('id or count must be provided for waitForWebsocketEvent'));
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
|
||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms);
|
||||
const type = id ? `id=${id}` : `count=${count}`;
|
||||
console.log(`Waiting for ${event} [${type}]`);
|
||||
const set = events[event];
|
||||
@@ -550,7 +550,7 @@ export const utils = {
|
||||
{
|
||||
headers: asBearerAuth(accessToken),
|
||||
fetch: (...args: Parameters<typeof fetch>) =>
|
||||
// eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await
|
||||
// eslint-disable-next-line unicorn/prefer-await
|
||||
fetch(...args).then((response) => {
|
||||
setCookie = response.headers.getSetCookie();
|
||||
return response;
|
||||
@@ -660,10 +660,10 @@ export const utils = {
|
||||
return !jobCounts.active && (!jobCounts.waiting || queues[queue].queueStatus.isPaused);
|
||||
},
|
||||
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms?: number) => {
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms: number = 15_000) => {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 15_000);
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms);
|
||||
|
||||
while (true) {
|
||||
const done = await utils.isQueueEmpty(accessToken, queue);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^75.0.0",
|
||||
"eslint-plugin-unicorn": "^78.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
|
||||
+16
-16
@@ -28,11 +28,11 @@ export const defaultQueueOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* An in-memory queue that processes tasks in parallel with a given concurrency.
|
||||
* @see {@link https://www.npmjs.com/package/fastq}
|
||||
* @template T - The type of the worker task data.
|
||||
* @template R - The type of the worker output data.
|
||||
*/
|
||||
An in-memory queue that processes tasks in parallel with a given concurrency.
|
||||
@see {@link https://www.npmjs.com/package/fastq}
|
||||
@template T - The type of the worker task data.
|
||||
@template R - The type of the worker output data.
|
||||
*/
|
||||
export class Queue<T, R> {
|
||||
private readonly queue: fastq.queueAsPromised<string, Task<T, R>>;
|
||||
private readonly store = new Map<string, Task<T, R>>();
|
||||
@@ -40,9 +40,9 @@ export class Queue<T, R> {
|
||||
readonly worker: (data: T) => Promise<R>;
|
||||
|
||||
/**
|
||||
* Create a new queue.
|
||||
* @param worker - The worker function that processes the task.
|
||||
* @param options - The queue options.
|
||||
Create a new queue.
|
||||
@param worker - The worker function that processes the task.
|
||||
@param options - The queue options.
|
||||
*/
|
||||
constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) {
|
||||
this.options = { ...defaultQueueOptions, ...options };
|
||||
@@ -65,20 +65,20 @@ export class Queue<T, R> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the queue to be empty.
|
||||
* @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
|
||||
* This promise could be ignored as it will not lead to a `unhandledRejection`.
|
||||
Wait for the queue to be empty.
|
||||
@returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
|
||||
This promise could be ignored as it will not lead to a `unhandledRejection`.
|
||||
*/
|
||||
drained(): Promise<void> {
|
||||
return this.queue.drained();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task at the end of the queue.
|
||||
* @see {@link https://www.npmjs.com/package/fastq}
|
||||
* @param data
|
||||
* @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
|
||||
* This promise could be ignored as it will not lead to a `unhandledRejection`.
|
||||
Add a task at the end of the queue.
|
||||
@see {@link https://www.npmjs.com/package/fastq}
|
||||
@param data
|
||||
@returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
|
||||
This promise could be ignored as it will not lead to a `unhandledRejection`.
|
||||
*/
|
||||
async push(data: T): Promise<Task<T, R>> {
|
||||
const id = uniqueId();
|
||||
|
||||
@@ -218,9 +218,9 @@ export const sha1 = (filepath: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Batches items and calls onBatch to process them
|
||||
* when the batch size is reached or the debounce time has passed.
|
||||
*/
|
||||
Batches items and calls onBatch to process them
|
||||
when the batch size is reached or the debounce time has passed.
|
||||
*/
|
||||
export class Batcher<T = unknown> {
|
||||
private items: T[] = [];
|
||||
private readonly batchSize: number;
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^75.0.0",
|
||||
"eslint-plugin-unicorn": "^78.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"pngjs": "^7.0.0",
|
||||
|
||||
@@ -28,14 +28,12 @@ export async function configureExpress(
|
||||
ssr,
|
||||
}: {
|
||||
/**
|
||||
* Whether to allow swagger module to write to the specs.json
|
||||
* This is not desirable when the API is not available
|
||||
* @default true
|
||||
Whether to allow swagger module to write to the specs.json
|
||||
This is not desirable when the API is not available
|
||||
@default true
|
||||
*/
|
||||
permitSwaggerWrite?: boolean;
|
||||
/**
|
||||
* Service to use for server-side rendering
|
||||
*/
|
||||
/** Service to use for server-side rendering */
|
||||
ssr: typeof ApiService | typeof MaintenanceWorkerService;
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -102,7 +102,7 @@ export class PromptConfirmMoveQuestions {
|
||||
message: 'Do you want to proceed? [Y/n]',
|
||||
name: 'value',
|
||||
})
|
||||
value(value: string): boolean {
|
||||
return ['yes', 'y'].includes((value || 'y').toLowerCase());
|
||||
value(value: string = 'y'): boolean {
|
||||
return ['yes', 'y'].includes(value.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -32,11 +32,11 @@ export const UpdatedAtTrigger = (name: string) =>
|
||||
export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500;
|
||||
|
||||
/**
|
||||
* Chunks an array or set into smaller collections of the same type and specified size.
|
||||
*
|
||||
* @param collection The collection to chunk.
|
||||
* @param size The size of each chunk.
|
||||
*/
|
||||
Chunks an array or set into smaller collections of the same type and specified size.
|
||||
|
||||
@param collection The collection to chunk.
|
||||
@param size The size of each chunk.
|
||||
*/
|
||||
function chunks<T>(collection: Array<T>, size: number): Array<Array<T>>;
|
||||
function chunks<T>(collection: Set<T>, size: number): Array<Set<T>>;
|
||||
function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>> | Array<Set<T>> {
|
||||
@@ -60,12 +60,12 @@ function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection,
|
||||
* to overcome the maximum number of parameters allowed by the database driver.
|
||||
*
|
||||
* @param options.paramIndex The index of the function parameter to chunk. Defaults to 0.
|
||||
* @param options.flatten Whether to flatten the results. Defaults to false.
|
||||
*/
|
||||
Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection,
|
||||
to overcome the maximum number of parameters allowed by the database driver.
|
||||
|
||||
@param options.paramIndex The index of the function parameter to chunk. Defaults to 0.
|
||||
@param options.flatten Whether to flatten the results. Defaults to false.
|
||||
*/
|
||||
export function Chunked(
|
||||
options: { paramIndex?: number; chunkSize?: number; mergeFn?: (results: any) => any } = {},
|
||||
): MethodDecorator {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { JsonParsed, isoDatetimeToDate, stringToBool } from 'src/validation.js';
|
||||
export enum AssetMediaSize {
|
||||
Original = 'original',
|
||||
/**
|
||||
* An full-sized image extracted/converted from non-web-friendly formats like RAW/HIF.
|
||||
* or otherwise the original image itself.
|
||||
An full-sized image extracted/converted from non-web-friendly formats like RAW/HIF.
|
||||
or otherwise the original image itself.
|
||||
*/
|
||||
FULLSIZE = 'fullsize',
|
||||
PREVIEW = 'preview',
|
||||
@@ -41,8 +41,8 @@ const AssetMediaBaseSchema = z.object({
|
||||
duration: z.coerce.number().int().min(0).optional().describe('Duration in milliseconds (for videos)'),
|
||||
filename: z.string().optional().describe('Filename'),
|
||||
/**
|
||||
* The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled
|
||||
* in the controller. File parts never reach the request body, so they must be optional here
|
||||
The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled
|
||||
in the controller. File parts never reach the request body, so they must be optional here
|
||||
*/
|
||||
[UploadFieldName.ASSET_DATA]: z
|
||||
.any()
|
||||
|
||||
@@ -14,10 +14,10 @@ enum DatabaseSslMode {
|
||||
const DatabaseSslModeSchema = z.enum(DatabaseSslMode).describe('Database SSL mode').meta({ id: 'DatabaseSslMode' });
|
||||
const absolutePath = z.string().regex(/^\//, 'Must be an absolute path').optional();
|
||||
/**
|
||||
* Treat certain strings as booleans and coerce them to boolean
|
||||
* Ideal for environment variables that are strings but should be treated as booleans
|
||||
* @docs https://zod.dev/api?id=stringbool
|
||||
*/
|
||||
Treat certain strings as booleans and coerce them to boolean
|
||||
Ideal for environment variables that are strings but should be treated as booleans
|
||||
@docs https://zod.dev/api?id=stringbool
|
||||
*/
|
||||
const stringBool = z.stringbool();
|
||||
|
||||
const trustedProxiesSchema = z
|
||||
|
||||
@@ -323,9 +323,9 @@ export const isFullyAlbumConfined = (filter: SearchFilter): boolean =>
|
||||
isAlbumConfined(filter) || (!!filter.or?.length && filter.or.every((branch) => isAlbumConfined(branch)));
|
||||
|
||||
/**
|
||||
* The structured shape and the deprecated flat search fields are mutually exclusive
|
||||
* TODO(v4): remove together with the deprecated flat fields.
|
||||
*/
|
||||
The structured shape and the deprecated flat search fields are mutually exclusive
|
||||
TODO(v4): remove together with the deprecated flat fields.
|
||||
*/
|
||||
const withShapeExclusivity = <T extends z.ZodObject<z.ZodRawShape>>(schema: T) => {
|
||||
const deprecatedFields = Object.keys(schema.shape).filter(
|
||||
(field) => (schema.shape[field] as z.ZodType).meta()?.deprecated,
|
||||
|
||||
+7
-11
@@ -59,9 +59,7 @@ export enum ChecksumAlgorithm {
|
||||
}
|
||||
|
||||
export enum AssetFileType {
|
||||
/**
|
||||
* An full/large-size image extracted/converted from RAW photos
|
||||
*/
|
||||
/** An full/large-size image extracted/converted from RAW photos */
|
||||
FullSize = 'fullsize',
|
||||
Preview = 'preview',
|
||||
Thumbnail = 'thumbnail',
|
||||
@@ -362,8 +360,8 @@ export enum SharedLinkType {
|
||||
Album = 'ALBUM',
|
||||
|
||||
/**
|
||||
* Individual asset
|
||||
* or group of assets that are not in an album
|
||||
Individual asset
|
||||
or group of assets that are not in an album
|
||||
*/
|
||||
Individual = 'INDIVIDUAL',
|
||||
}
|
||||
@@ -818,9 +816,9 @@ export enum DvProfile {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dolby Vision base-layer signal-compatibility ID from the DOVI configuration record.
|
||||
* Identifies what the base HEVC/AVC layer renders as on a non-DV decoder.
|
||||
*/
|
||||
Dolby Vision base-layer signal-compatibility ID from the DOVI configuration record.
|
||||
Identifies what the base HEVC/AVC layer renders as on a non-DV decoder.
|
||||
*/
|
||||
export enum DvSignalCompatibility {
|
||||
None = 0,
|
||||
Hdr10 = 1,
|
||||
@@ -1227,9 +1225,7 @@ export enum AssetVisibility {
|
||||
Archive = 'archive',
|
||||
Timeline = 'timeline',
|
||||
|
||||
/**
|
||||
* Video part of the LivePhotos and MotionPhotos
|
||||
*/
|
||||
/** Video part of the LivePhotos and MotionPhotos */
|
||||
Hidden = 'hidden',
|
||||
Locked = 'locked',
|
||||
}
|
||||
|
||||
+6
-14
@@ -10,23 +10,15 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
|
||||
import { type DB } from 'src/schema/index.js';
|
||||
import { getKyselyConfig } from 'src/utils/database.js';
|
||||
|
||||
/**
|
||||
* Manages worker lifecycle
|
||||
*/
|
||||
/** Manages worker lifecycle */
|
||||
class Workers {
|
||||
/**
|
||||
* Currently running workers
|
||||
*/
|
||||
/** Currently running workers */
|
||||
workers: Partial<Record<ImmichWorker, { kill: (signal: NodeJS.Signals) => Promise<void> | void }>> = {};
|
||||
|
||||
/**
|
||||
* Fail-safe in case anything dies during restart
|
||||
*/
|
||||
/** Fail-safe in case anything dies during restart */
|
||||
restarting = false;
|
||||
|
||||
/**
|
||||
* Boot all enabled workers
|
||||
*/
|
||||
/** Boot all enabled workers */
|
||||
async bootstrap() {
|
||||
const isMaintenanceMode = await this.isMaintenanceMode();
|
||||
const { workers } = new ConfigRepository().getEnv();
|
||||
@@ -92,8 +84,8 @@ class Workers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an individual worker
|
||||
* @param name Worker
|
||||
Start an individual worker
|
||||
@param name Worker
|
||||
*/
|
||||
private startWorker(name: ImmichWorker) {
|
||||
console.log(`Starting ${name} worker`);
|
||||
|
||||
@@ -43,9 +43,7 @@ export class MaintenanceWorkerController {
|
||||
private databaseBackupService: DatabaseBackupService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@link _ServerController.getServerConfig }
|
||||
*/
|
||||
/** {@link _ServerController.getServerConfig } */
|
||||
@Get('server/config')
|
||||
getServerConfig(): ServerConfigDto {
|
||||
return this.service.getSystemConfig();
|
||||
@@ -61,18 +59,14 @@ export class MaintenanceWorkerController {
|
||||
return this.service.getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _DatabaseBackupController.listDatabaseBackups}
|
||||
*/
|
||||
/** {@link _DatabaseBackupController.listDatabaseBackups} */
|
||||
@Get('admin/database-backups')
|
||||
@MaintenanceRoute()
|
||||
listDatabaseBackups(): Promise<DatabaseBackupListResponseDto> {
|
||||
return this.databaseBackupService.listBackups();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _DatabaseBackupController.downloadDatabaseBackup}
|
||||
*/
|
||||
/** {@link _DatabaseBackupController.downloadDatabaseBackup} */
|
||||
@Get('admin/database-backups/:filename')
|
||||
@MaintenanceRoute()
|
||||
async downloadDatabaseBackup(
|
||||
@@ -83,18 +77,14 @@ export class MaintenanceWorkerController {
|
||||
await sendFile(res, next, () => this.databaseBackupService.downloadBackup(filename), this.logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _DatabaseBackupController.deleteDatabaseBackup}
|
||||
*/
|
||||
/** {@link _DatabaseBackupController.deleteDatabaseBackup} */
|
||||
@Delete('admin/database-backups')
|
||||
@MaintenanceRoute()
|
||||
async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise<void> {
|
||||
return this.databaseBackupService.deleteBackup(dto.backups);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _DatabaseBackupController.uploadDatabaseBackup}
|
||||
*/
|
||||
/** {@link _DatabaseBackupController.uploadDatabaseBackup} */
|
||||
@Post('admin/database-backups/upload')
|
||||
@MaintenanceRoute()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
|
||||
@@ -282,9 +282,7 @@ describe(MaintenanceWorkerService.name, () => {
|
||||
|
||||
describe.skip('setAction'); // just calls setStatus+runAction
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
/** Actions */
|
||||
|
||||
describe('action: start', () => {
|
||||
it('should not do anything', async () => {
|
||||
|
||||
@@ -33,9 +33,7 @@ import { getConfig } from 'src/utils/config.js';
|
||||
import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance.js';
|
||||
import { getExternalDomain } from 'src/utils/misc.js';
|
||||
|
||||
/**
|
||||
* This service is available inside of maintenance mode to manage maintenance mode
|
||||
*/
|
||||
/** This service is available inside of maintenance mode to manage maintenance mode */
|
||||
@Injectable()
|
||||
export class MaintenanceWorkerService {
|
||||
#secret: string | null = null;
|
||||
@@ -87,9 +85,7 @@ export class MaintenanceWorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _BaseService.configRepos}
|
||||
*/
|
||||
/** {@link _BaseService.configRepos} */
|
||||
private get configRepos() {
|
||||
return {
|
||||
configRepo: this.configRepository,
|
||||
@@ -98,25 +94,19 @@ export class MaintenanceWorkerService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _BaseService.prototype.getConfig}
|
||||
*/
|
||||
/** {@link _BaseService.prototype.getConfig} */
|
||||
private getConfig(options: { withCache: boolean }) {
|
||||
return getConfig(this.configRepos, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _ServerService.getSystemConfig}
|
||||
*/
|
||||
/** {@link _ServerService.getSystemConfig} */
|
||||
getSystemConfig() {
|
||||
return {
|
||||
maintenanceMode: true,
|
||||
} as ServerConfigDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _VersionService.getVersion}
|
||||
*/
|
||||
/** {@link _VersionService.getVersion} */
|
||||
getVersion() {
|
||||
return ServerVersionResponseDto.fromSemVer(serverVersion);
|
||||
}
|
||||
@@ -125,9 +115,7 @@ export class MaintenanceWorkerService {
|
||||
return { res: 'pong' };
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _ApiService.ssr}
|
||||
*/
|
||||
/** {@link _ApiService.ssr} */
|
||||
ssr(excludePaths: string[]) {
|
||||
const { resourcePaths } = this.configRepository.getEnv();
|
||||
|
||||
@@ -158,9 +146,7 @@ export class MaintenanceWorkerService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link _StorageService.detectMediaLocation}
|
||||
*/
|
||||
/** {@link _StorageService.detectMediaLocation} */
|
||||
detectMediaLocation(): string {
|
||||
const envData = this.configRepository.getEnv();
|
||||
if (envData.storage.mediaLocation) {
|
||||
|
||||
@@ -227,11 +227,11 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset IDs for the given album ID.
|
||||
*
|
||||
* @param albumId Album ID to get asset IDs for.
|
||||
* @param assetIds Optional list of asset IDs to filter on.
|
||||
* @returns Set of Asset IDs for the given album ID.
|
||||
Get asset IDs for the given album ID.
|
||||
|
||||
@param albumId Album ID to get asset IDs for.
|
||||
@param assetIds Optional list of asset IDs to filter on.
|
||||
@returns Set of Asset IDs for the given album ID.
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
@@ -348,12 +348,12 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure all thumbnails for albums are updated by:
|
||||
* - Removing thumbnails from albums without assets
|
||||
* - Removing references of thumbnails to assets outside the album
|
||||
* - Setting a thumbnail when none is set and the album contains assets
|
||||
*
|
||||
* @returns Amount of updated album thumbnails or undefined when unknown
|
||||
Makes sure all thumbnails for albums are updated by:
|
||||
- Removing thumbnails from albums without assets
|
||||
- Removing references of thumbnails to assets outside the album
|
||||
- Setting a thumbnail when none is set and the album contains assets
|
||||
|
||||
@returns Amount of updated album thumbnails or undefined when unknown
|
||||
*/
|
||||
async updateThumbnails(): Promise<number | undefined> {
|
||||
// Subquery for getting a new thumbnail.
|
||||
@@ -399,8 +399,8 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get per-user asset contribution counts for a single album.
|
||||
* Excludes deleted assets, orders by count desc.
|
||||
Get per-user asset contribution counts for a single album.
|
||||
Excludes deleted assets, orders by count desc.
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getContributorCounts(id: string) {
|
||||
|
||||
@@ -73,9 +73,9 @@ export class MediaRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input file path to the input image
|
||||
* @returns ExtractResult if succeeded, or null if failed
|
||||
|
||||
@param input file path to the input image
|
||||
@returns ExtractResult if succeeded, or null if failed
|
||||
*/
|
||||
async extract(input: string): Promise<ExtractResult | null> {
|
||||
for (const { tag, format } of [
|
||||
@@ -305,8 +305,8 @@ export class MediaRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed for accurate segments, especially when remuxing, seeking and/or VFR is involved.
|
||||
* Scanning packets for keyframes in JS is much faster than -skip_frame nokey since it avoids decoding the video.
|
||||
Needed for accurate segments, especially when remuxing, seeking and/or VFR is involved.
|
||||
Scanning packets for keyframes in JS is much faster than -skip_frame nokey since it avoids decoding the video.
|
||||
*/
|
||||
probePackets(input: string, streamIndex: number): Promise<VideoPacketInfo | null> {
|
||||
const ffprobe = spawn(
|
||||
|
||||
@@ -38,28 +38,28 @@ import { ImmichFileResponse } from 'src/utils/file.js';
|
||||
import { batched, handlePromiseError } from 'src/utils/misc.js';
|
||||
|
||||
/**
|
||||
* Untracked Files:
|
||||
* Files are detected in /data/encoded-video, /data/library, /data/upload
|
||||
* Checked against the asset table
|
||||
* Files are detected in /data/thumbs
|
||||
* Checked against the asset_file table
|
||||
*
|
||||
* * Can perform download or delete of files
|
||||
*
|
||||
* Missing Files:
|
||||
* Paths are queried from asset(originalPath, encodedVideoPath), asset_file(path)
|
||||
* Check whether files exist on disk
|
||||
*
|
||||
* * Reports must include origin (asset or asset_file) & ID for further action
|
||||
* * Can perform trash (asset) or delete (asset_file)
|
||||
*
|
||||
* Checksum Mismatch:
|
||||
* Paths & checksums are queried from asset(originalPath, checksum)
|
||||
* Check whether files match checksum, missing files ignored
|
||||
*
|
||||
* * Reports must include origin (as above) for further action
|
||||
* * Can perform download or trash (asset)
|
||||
*/
|
||||
Untracked Files:
|
||||
Files are detected in /data/encoded-video, /data/library, /data/upload
|
||||
Checked against the asset table
|
||||
Files are detected in /data/thumbs
|
||||
Checked against the asset_file table
|
||||
|
||||
Can perform download or delete of files
|
||||
|
||||
Missing Files:
|
||||
Paths are queried from asset(originalPath, encodedVideoPath), asset_file(path)
|
||||
Check whether files exist on disk
|
||||
|
||||
Reports must include origin (asset or asset_file) & ID for further action
|
||||
Can perform trash (asset) or delete (asset_file)
|
||||
|
||||
Checksum Mismatch:
|
||||
Paths & checksums are queried from asset(originalPath, checksum)
|
||||
Check whether files match checksum, missing files ignored
|
||||
|
||||
Reports must include origin (as above) for further action
|
||||
Can perform download or trash (asset)
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class IntegrityService extends BaseService {
|
||||
|
||||
@@ -98,9 +98,7 @@ export class JobService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue follow up jobs
|
||||
*/
|
||||
/** Queue follow up jobs */
|
||||
private async onDone(item: JobItem) {
|
||||
switch (item.name) {
|
||||
case JobName.SidecarCheck: {
|
||||
|
||||
@@ -18,9 +18,7 @@ import {
|
||||
} from 'src/utils/maintenance.js';
|
||||
import { getExternalDomain } from 'src/utils/misc.js';
|
||||
|
||||
/**
|
||||
* This service is available outside of maintenance mode to manage maintenance mode
|
||||
*/
|
||||
/** This service is available outside of maintenance mode to manage maintenance mode */
|
||||
@Injectable()
|
||||
export class MaintenanceService extends BaseService {
|
||||
getMaintenanceMode(): Promise<MaintenanceModeState> {
|
||||
|
||||
@@ -169,8 +169,7 @@ export class MediaService extends BaseService {
|
||||
|
||||
let thumbhash: Buffer | undefined = generated?.thumbhash;
|
||||
if (!thumbhash) {
|
||||
const extractedImage = await this.extractOriginalImage(asset, config.image);
|
||||
const { info, data, colorspace } = extractedImage;
|
||||
const { info, data, colorspace } = await this.extractOriginalImage(asset, config.image);
|
||||
|
||||
thumbhash = await this.mediaRepository.generateThumbhash(
|
||||
{ data, info },
|
||||
@@ -290,8 +289,8 @@ export class MediaService extends BaseService {
|
||||
|
||||
private async generateImageThumbnails(asset: ThumbnailAsset, { image }: SystemConfig, useEdits: boolean = false) {
|
||||
// Handle embedded preview extraction for RAW files
|
||||
const extractedImage = await this.extractOriginalImage(asset, image, useEdits);
|
||||
const { info, data, colorspace, generateFullsize, convertFullsize, extracted, isTransparent } = extractedImage;
|
||||
const { info, data, colorspace, generateFullsize, convertFullsize, extracted, isTransparent } =
|
||||
await this.extractOriginalImage(asset, image, useEdits);
|
||||
|
||||
const previewFormat = image.preview.format;
|
||||
this.warnOnTransparencyLoss(isTransparent, previewFormat, asset.id);
|
||||
|
||||
@@ -268,7 +268,7 @@ export class StorageTemplateService extends BaseService {
|
||||
private async getTemplatePath(
|
||||
asset: StorageAsset,
|
||||
metadata: MoveAssetMetadata,
|
||||
stillPhoto?: StorageAsset,
|
||||
stillPhoto: StorageAsset = asset,
|
||||
): Promise<string> {
|
||||
const { storageLabel, filename } = metadata;
|
||||
|
||||
@@ -310,7 +310,7 @@ export class StorageTemplateService extends BaseService {
|
||||
let albumName = null;
|
||||
let albumStartDate = null;
|
||||
let albumEndDate = null;
|
||||
const assetForMetadata = stillPhoto || asset;
|
||||
const assetForMetadata = stillPhoto;
|
||||
|
||||
if (this.template.needsAlbum) {
|
||||
// For motion videos, use the still photo's album information since motion videos
|
||||
@@ -360,18 +360,18 @@ export class StorageTemplateService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of migrating duplicate filename to a new path, we need to check if it is already migrated
|
||||
* Due to the mechanism of appending +1, +2, +3, etc to the filename
|
||||
*
|
||||
* Example:
|
||||
* Source = upload/abc/def/FullSizeRender+7.heic
|
||||
* Expected Destination = upload/abc/def/FullSizeRender.heic
|
||||
*
|
||||
* The file is already at the correct location, but since there are other FullSizeRender.heic files in the
|
||||
* destination, it was renamed to FullSizeRender+7.heic.
|
||||
*
|
||||
* The lines below will be used to check if the differences between the source and destination is only the
|
||||
* +7 suffix, and if so, it will be considered as already migrated.
|
||||
In case of migrating duplicate filename to a new path, we need to check if it is already migrated
|
||||
Due to the mechanism of appending +1, +2, +3, etc to the filename
|
||||
|
||||
Example:
|
||||
Source = upload/abc/def/FullSizeRender+7.heic
|
||||
Expected Destination = upload/abc/def/FullSizeRender.heic
|
||||
|
||||
The file is already at the correct location, but since there are other FullSizeRender.heic files in the
|
||||
destination, it was renamed to FullSizeRender+7.heic.
|
||||
|
||||
The lines below will be used to check if the differences between the source and destination is only the
|
||||
+7 suffix, and if so, it will be considered as already migrated.
|
||||
*/
|
||||
if (source.startsWith(fullPath) && source.endsWith(`.${extension}`)) {
|
||||
const diff = source.replace(fullPath, '').replace(`.${extension}`, '');
|
||||
|
||||
@@ -420,14 +420,14 @@ describe(SystemConfigService.name, () => {
|
||||
},
|
||||
];
|
||||
|
||||
for (const { should, externalDomain, result } of externalDomainTests) {
|
||||
for (const { should, externalDomain, result = 'https://demo.immich.app' } of externalDomainTests) {
|
||||
it(`should normalize an external domain ${should}`, async () => {
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
const partialConfig = { server: { externalDomain } };
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
const config = await sut.getAdminConfig();
|
||||
expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
|
||||
expect(config.server.externalDomain).toEqual(result);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ export class WorkflowExecutionService extends BaseService {
|
||||
throw new Error('Hostname did not match any listed in methods[].allowedHosts in the plugin manifest');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-invalid-argument-count
|
||||
const res = await fetch(...args);
|
||||
|
||||
return {
|
||||
|
||||
@@ -355,8 +355,8 @@ export function withTags(eb: ExpressionBuilder<DB, 'asset'>) {
|
||||
).as('tags');
|
||||
}
|
||||
|
||||
export function truncatedDate<O>(order: AssetOrderBy = AssetOrderBy.TakenAt, size?: 'DAY' | 'MONTH') {
|
||||
return sql<O>`date_trunc(${sql.lit(size ?? 'MONTH')}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
||||
export function truncatedDate<O>(order: AssetOrderBy = AssetOrderBy.TakenAt, size: 'DAY' | 'MONTH' = 'MONTH') {
|
||||
return sql<O>`date_trunc(${sql.lit(size)}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
||||
}
|
||||
|
||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
||||
@@ -1106,14 +1106,14 @@ export type ReindexVectorIndexOptions = { indexName: string; lists?: number };
|
||||
|
||||
type VectorIndexQueryOptions = { table: string; vectorExtension: VectorExtension } & ReindexVectorIndexOptions;
|
||||
|
||||
export function vectorIndexQuery({ vectorExtension, table, indexName, lists }: VectorIndexQueryOptions): string {
|
||||
export function vectorIndexQuery({ vectorExtension, table, indexName, lists = 1 }: VectorIndexQueryOptions): string {
|
||||
switch (vectorExtension) {
|
||||
case DatabaseExtension.VectorChord: {
|
||||
return `
|
||||
CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} USING vchordrq (embedding vector_cosine_ops) WITH (options = $$
|
||||
residual_quantization = false
|
||||
[build.internal]
|
||||
lists = [${lists ?? 1}]
|
||||
lists = [${lists}]
|
||||
spherical_centroids = true
|
||||
build_threads = 4
|
||||
sampling_factor = 1024
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { isoDateToDate, isoDatetimeToDate } from 'src/validation.js';
|
||||
|
||||
/**
|
||||
* Convert a date to a ISO 8601 datetime string.
|
||||
*/
|
||||
/** Convert a date to a ISO 8601 datetime string. */
|
||||
export const asDateTimeString = <T extends Date | string | undefined | null>(x: T) => {
|
||||
return x instanceof Date ? isoDatetimeToDate.encode(x) : (x as Exclude<T, Date>);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a date to a date string (yyyy-mm-dd).
|
||||
*/
|
||||
/** Convert a date to a date string (yyyy-mm-dd). */
|
||||
export const asDateString = (x: Date | string | null): string | null => {
|
||||
return x instanceof Date ? isoDateToDate.encode(x) : x;
|
||||
};
|
||||
|
||||
/**
|
||||
* People born on February 29th are celebrated on February 28th in non-leap years.
|
||||
*/
|
||||
/** People born on February 29th are celebrated on February 28th in non-leap years. */
|
||||
export const isLeapDayObserved = ({ year, month, day }: { year: number; month: number; day: number }) => {
|
||||
return month === 2 && day === 28 && !DateTime.local(year).isInLeapYear;
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto.js';
|
||||
|
||||
/**
|
||||
* Counts all truthy values in the exifInfo object.
|
||||
* This matches the client implementation in web/src/lib/utils/exif-utils.ts
|
||||
*
|
||||
* @param asset Asset with optional exifInfo
|
||||
* @returns Count of truthy EXIF values
|
||||
*/
|
||||
Counts all truthy values in the exifInfo object.
|
||||
This matches the client implementation in web/src/lib/utils/exif-utils.ts
|
||||
|
||||
@param asset Asset with optional exifInfo
|
||||
@returns Count of truthy EXIF values
|
||||
*/
|
||||
export const getExifCount = (asset: AssetResponseDto): number => {
|
||||
return Object.values(asset.exifInfo ?? {}).filter(Boolean).length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Suggests the best duplicate asset to keep from a list of duplicates.
|
||||
* This is a direct port of the client logic from web/src/lib/utils/duplicate-utils.ts
|
||||
*
|
||||
* The best asset is determined by the following criteria:
|
||||
* 1. Largest image file size in bytes
|
||||
* 2. Largest count of EXIF data (as tie-breaker)
|
||||
*
|
||||
* @param assets List of duplicate assets
|
||||
* @returns The best asset to keep, or undefined if empty list
|
||||
*/
|
||||
Suggests the best duplicate asset to keep from a list of duplicates.
|
||||
This is a direct port of the client logic from web/src/lib/utils/duplicate-utils.ts
|
||||
|
||||
The best asset is determined by the following criteria:
|
||||
1. Largest image file size in bytes
|
||||
2. Largest count of EXIF data (as tie-breaker)
|
||||
|
||||
@param assets List of duplicate assets
|
||||
@returns The best asset to keep, or undefined if empty list
|
||||
*/
|
||||
export const suggestDuplicate = (assets: AssetResponseDto[]): AssetResponseDto | undefined => {
|
||||
if (assets.length === 0) {
|
||||
return undefined;
|
||||
@@ -48,12 +48,12 @@ export const suggestDuplicate = (assets: AssetResponseDto[]): AssetResponseDto |
|
||||
};
|
||||
|
||||
/**
|
||||
* Suggests the best duplicate asset IDs to keep from a list of duplicates.
|
||||
* Returns an array with a single asset ID (the best candidate), or empty if no assets.
|
||||
*
|
||||
* @param assets List of duplicate assets
|
||||
* @returns Array of suggested asset IDs to keep (0 or 1 element)
|
||||
*/
|
||||
Suggests the best duplicate asset IDs to keep from a list of duplicates.
|
||||
Returns an array with a single asset ID (the best candidate), or empty if no assets.
|
||||
|
||||
@param assets List of duplicate assets
|
||||
@returns Array of suggested asset IDs to keep (0 or 1 element)
|
||||
*/
|
||||
export const suggestDuplicateKeepAssetIds = (assets: AssetResponseDto[]): string[] => {
|
||||
const suggested = suggestDuplicate(assets);
|
||||
return suggested ? [suggested.id] : [];
|
||||
|
||||
@@ -71,6 +71,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
|
||||
protected constructor(
|
||||
protected config: ConfigFFmpegDto,
|
||||
// eslint-disable-next-line unicorn/no-object-as-default-parameter
|
||||
protected tune: VideoTuning = { strictGop: false, lowLatency: false },
|
||||
) {}
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ const raw = {
|
||||
};
|
||||
|
||||
/**
|
||||
* list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
|
||||
* @TODO share with the client
|
||||
* @see {@link web/src/lib/utils/asset-utils.ts#L329}
|
||||
**/
|
||||
list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
|
||||
@TODO share with the client
|
||||
@see {@link web/src/lib/utils/asset-utils.ts#L329}
|
||||
*/
|
||||
const webSupportedImage = {
|
||||
'.avif': ['image/avif'],
|
||||
'.bmp': ['image/bmp'],
|
||||
|
||||
@@ -52,9 +52,7 @@ export const getMethodNames = (instance: any) => {
|
||||
export const getExternalDomain = (server: SystemConfig['server'], defaultDomain = 'https://my.immich.app') =>
|
||||
server.externalDomain || defaultDomain;
|
||||
|
||||
/**
|
||||
* @returns a list of strings representing the keys of the object in dot notation
|
||||
*/
|
||||
/** @returns a list of strings representing the keys of the object in dot notation */
|
||||
export const getKeysDeep = (target: unknown, path: string[] = []) => {
|
||||
if (!target || typeof target !== 'object') {
|
||||
return [];
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { isEqual, isPlainObject } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* Deeply clones and converts a class instance to a plain object.
|
||||
*/
|
||||
/** Deeply clones and converts a class instance to a plain object. */
|
||||
export function toPlainObject<T extends object>(obj: T): T {
|
||||
return isPlainObject(obj) ? obj : structuredClone(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a deep comparison between objects, converting them to plain objects first if needed.
|
||||
*/
|
||||
/** Performs a deep comparison between objects, converting them to plain objects first if needed. */
|
||||
export function isEqualObject(value: object, other: object): boolean {
|
||||
return isEqual(toPlainObject(value), toPlainObject(other));
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ const canMatch = <T>(condition: EnumCondition<T>, value: T): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The conditions that decide which `field` values the filter can return. A top-level condition decides alone, otherwise each branch itself.
|
||||
*/
|
||||
/** The conditions that decide which `field` values the filter can return. A top-level condition decides alone, otherwise each branch itself. */
|
||||
const decidingConditions = <F extends EnumField>(filter: SearchFilter, field: F) => {
|
||||
if (filter[field] !== undefined) {
|
||||
return [filter[field]];
|
||||
@@ -39,9 +37,9 @@ const decidingConditions = <F extends EnumField>(filter: SearchFilter, field: F)
|
||||
};
|
||||
|
||||
/**
|
||||
* Keeps locked assets out of search results unless the session is elevated: a filter that asks for
|
||||
* them is rejected with 401, and any other filter gets `visibility != locked` ANDed in.
|
||||
*/
|
||||
Keeps locked assets out of search results unless the session is elevated: a filter that asks for
|
||||
them is rejected with 401, and any other filter gets `visibility != locked` ANDed in.
|
||||
*/
|
||||
export const applyLockedVisibilityPolicy = (auth: AuthDto, filter: SearchFilter): SearchFilter => {
|
||||
if (auth.session?.hasElevatedPermission) {
|
||||
return filter;
|
||||
|
||||
@@ -29,6 +29,6 @@ export const serialize = <T extends keyof SyncItem, D extends SyncItem[T]>({
|
||||
type,
|
||||
data,
|
||||
ids,
|
||||
ackType,
|
||||
ackType = type,
|
||||
}: SerializeOptions<T, D>) =>
|
||||
mapJsonLine({ type, data, ack: toAck({ type: ackType ?? type, updateId: ids[0], extraId: ids[1] }) });
|
||||
mapJsonLine({ type, data, ack: toAck({ type: ackType, updateId: ids[0], extraId: ids[1] }) });
|
||||
|
||||
@@ -72,9 +72,9 @@ type TransformState = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms an array of points through a series of edit operations (crop, rotate, mirror).
|
||||
* Points should be in absolute pixel coordinates relative to the starting dimensions.
|
||||
*/
|
||||
Transforms an array of points through a series of edit operations (crop, rotate, mirror).
|
||||
Points should be in absolute pixel coordinates relative to the starting dimensions.
|
||||
*/
|
||||
export const transformPoints = (
|
||||
points: Point[],
|
||||
edits: AssetEditActionItem[],
|
||||
|
||||
+71
-73
@@ -18,14 +18,14 @@ function isIPOrRange(value: string, options?: IsIPRangeOptions): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zod schema that validates an array of strings as IP addresses or IP/CIDR ranges.
|
||||
* When requireCIDR is true (default), plain IPs are rejected; only CIDR ranges are allowed.
|
||||
*
|
||||
* @example
|
||||
* z.string().optional().transform(...).pipe(IsIPRange())
|
||||
* @example
|
||||
* z.string().optional().transform(...).pipe(IsIPRange({ requireCIDR: false }))
|
||||
*/
|
||||
Zod schema that validates an array of strings as IP addresses or IP/CIDR ranges.
|
||||
When requireCIDR is true (default), plain IPs are rejected; only CIDR ranges are allowed.
|
||||
|
||||
@example
|
||||
z.string().optional().transform(...).pipe(IsIPRange())
|
||||
@example
|
||||
z.string().optional().transform(...).pipe(IsIPRange({ requireCIDR: false }))
|
||||
*/
|
||||
export function IsIPRange(options?: IsIPRangeOptions) {
|
||||
return z
|
||||
.array(z.string())
|
||||
@@ -33,12 +33,12 @@ export function IsIPRange(options?: IsIPRangeOptions) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Like z.object().partial(), but rejects objects where every field is undefined.
|
||||
* Use for update/patch DTOs where at least one field must be provided.
|
||||
*
|
||||
* @example
|
||||
* nonEmptyPartial({ name: z.string(), bio: z.string() }).meta({ id: 'UpdateDto' });
|
||||
*/
|
||||
Like z.object().partial(), but rejects objects where every field is undefined.
|
||||
Use for update/patch DTOs where at least one field must be provided.
|
||||
|
||||
@example
|
||||
nonEmptyPartial({ name: z.string(), bio: z.string() }).meta({ id: 'UpdateDto' });
|
||||
*/
|
||||
export function nonEmptyPartial<T extends z.ZodRawShape>(shape: T) {
|
||||
return z
|
||||
.object(shape)
|
||||
@@ -49,14 +49,14 @@ export function nonEmptyPartial<T extends z.ZodRawShape>(shape: T) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zod schema that validates sibling-exclusion for object schemas.
|
||||
* Validation passes when the target property is missing, or when none of the sibling properties are present.
|
||||
* Use with .pipe() like IsIPRange.
|
||||
*
|
||||
* @example
|
||||
* const Schema = z.object({ a: z.string().optional(), b: z.string().optional() });
|
||||
* Schema.pipe(IsNotSiblingOf(Schema, 'a', ['b']));
|
||||
*/
|
||||
Zod schema that validates sibling-exclusion for object schemas.
|
||||
Validation passes when the target property is missing, or when none of the sibling properties are present.
|
||||
Use with .pipe() like IsIPRange.
|
||||
|
||||
@example
|
||||
const Schema = z.object({ a: z.string().optional(), b: z.string().optional() });
|
||||
Schema.pipe(IsNotSiblingOf(Schema, 'a', ['b']));
|
||||
*/
|
||||
export function IsNotSiblingOf<
|
||||
TSchema extends z.ZodObject<z.ZodRawShape>,
|
||||
TKey extends z.infer<ReturnType<TSchema['keyof']>> & keyof z.infer<TSchema>,
|
||||
@@ -122,18 +122,18 @@ const FilenameParamSchema = z.object({
|
||||
export class FilenameParamDto extends createZodDto(FilenameParamSchema) {}
|
||||
|
||||
/**
|
||||
* The HTML5 email regex, but with unicode support, so that international email addresses
|
||||
* (unicode local parts and internationalized domain names) are accepted.
|
||||
* @see {@link z.regexes.html5Email}
|
||||
*/
|
||||
The HTML5 email regex, but with unicode support, so that international email addresses
|
||||
(unicode local parts and internationalized domain names) are accepted.
|
||||
@see {@link z.regexes.html5Email}
|
||||
*/
|
||||
const unicodeEmail =
|
||||
/^[\p{L}\p{M}\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\p{L}\p{N}](?:[\p{L}\p{M}\p{N}-]{0,61}[\p{L}\p{M}\p{N}])?(?:\.[\p{L}\p{N}](?:[\p{L}\p{M}\p{N}-]{0,61}[\p{L}\p{M}\p{N}])?)*$/u;
|
||||
|
||||
/**
|
||||
* Unified email validation
|
||||
* Converts email strings to lowercase and validates against a unicode aware email regex
|
||||
* @docs https://zod.dev/api?id=email
|
||||
*/
|
||||
Unified email validation
|
||||
Converts email strings to lowercase and validates against a unicode aware email regex
|
||||
@docs https://zod.dev/api?id=email
|
||||
*/
|
||||
export const toEmail = z
|
||||
.email({
|
||||
pattern: unicodeEmail,
|
||||
@@ -142,9 +142,9 @@ export const toEmail = z
|
||||
.transform((val) => val.toLowerCase());
|
||||
|
||||
/**
|
||||
* Parse ISO 8601 datetime strings to Date objects
|
||||
* @docs https://zod.dev/api?id=codec
|
||||
*/
|
||||
Parse ISO 8601 datetime strings to Date objects
|
||||
@docs https://zod.dev/api?id=codec
|
||||
*/
|
||||
export const isoDatetimeToDate = z
|
||||
.codec(
|
||||
z.iso.datetime({
|
||||
@@ -160,9 +160,9 @@ export const isoDatetimeToDate = z
|
||||
.meta({ example: '2024-01-01T00:00:00.000Z' });
|
||||
|
||||
/**
|
||||
* Parse ISO date strings to Date objects
|
||||
* @docs https://zod.dev/api?id=codec
|
||||
*/
|
||||
Parse ISO date strings to Date objects
|
||||
@docs https://zod.dev/api?id=codec
|
||||
*/
|
||||
export const isoDateToDate = z
|
||||
.codec(
|
||||
z.iso.date({
|
||||
@@ -177,46 +177,44 @@ export const isoDateToDate = z
|
||||
.meta({ example: '2024-01-01' });
|
||||
|
||||
/**
|
||||
* Latitude in range [-90, 90]. Reuse for body or query params.
|
||||
*
|
||||
* @example
|
||||
* // Regular (body): optional coordinates
|
||||
* latitudeSchema.optional().describe('Latitude coordinate')
|
||||
*
|
||||
* @example
|
||||
* // Pipe (query): coerce string to number then validate range
|
||||
* z.coerce.number().pipe(latitudeSchema).describe('Latitude (-90 to 90)')
|
||||
*/
|
||||
Latitude in range [-90, 90]. Reuse for body or query params.
|
||||
|
||||
@example
|
||||
// Regular (body): optional coordinates
|
||||
latitudeSchema.optional().describe('Latitude coordinate')
|
||||
|
||||
@example
|
||||
// Pipe (query): coerce string to number then validate range
|
||||
z.coerce.number().pipe(latitudeSchema).describe('Latitude (-90 to 90)')
|
||||
*/
|
||||
export const latitudeSchema = z.number().meta({ format: 'double' }).min(-90).max(90);
|
||||
|
||||
/**
|
||||
* Longitude in range [-180, 180]. Reuse for body or query params.
|
||||
*
|
||||
* @example
|
||||
* // Regular (body): optional coordinates
|
||||
* longitudeSchema.optional().describe('Longitude coordinate')
|
||||
*
|
||||
* @example
|
||||
* // Pipe (query): coerce string to number then validate range
|
||||
* z.coerce.number().pipe(longitudeSchema).describe('Longitude (-180 to 180)')
|
||||
*/
|
||||
Longitude in range [-180, 180]. Reuse for body or query params.
|
||||
|
||||
@example
|
||||
// Regular (body): optional coordinates
|
||||
longitudeSchema.optional().describe('Longitude coordinate')
|
||||
|
||||
@example
|
||||
// Pipe (query): coerce string to number then validate range
|
||||
z.coerce.number().pipe(longitudeSchema).describe('Longitude (-180 to 180)')
|
||||
*/
|
||||
export const longitudeSchema = z.number().meta({ format: 'double' }).min(-180).max(180);
|
||||
|
||||
/**
|
||||
* Parse string to boolean
|
||||
* This should be used for boolean query parameters and path parameters, but not for boolean request body parameters, as the first are always string.
|
||||
* We don't use z.coerce.boolean() as any truthy value is considered true
|
||||
* z.stringbool() is a more robust way to parse strings to booleans as it lets you specify the truthy and falsy values and the case sensitivity.
|
||||
* @docs https://zod.dev/api?id=coercion
|
||||
* @docs https://zod.dev/api?id=stringbool
|
||||
*/
|
||||
Parse string to boolean
|
||||
This should be used for boolean query parameters and path parameters, but not for boolean request body parameters, as the first are always string.
|
||||
We don't use z.coerce.boolean() as any truthy value is considered true
|
||||
z.stringbool() is a more robust way to parse strings to booleans as it lets you specify the truthy and falsy values and the case sensitivity.
|
||||
@docs https://zod.dev/api?id=coercion
|
||||
@docs https://zod.dev/api?id=stringbool
|
||||
*/
|
||||
export const stringToBool = z
|
||||
.stringbool({ truthy: ['true'], falsy: ['false'], case: 'sensitive' })
|
||||
.meta({ type: 'boolean' });
|
||||
|
||||
/**
|
||||
* Parse JSON strings from multipart/form-data
|
||||
*/
|
||||
/** Parse JSON strings from multipart/form-data */
|
||||
export const JsonParsed = z.transform((val, ctx) => {
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
@@ -234,13 +232,13 @@ export const JsonParsed = z.transform((val, ctx) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Hex color validation and normalization.
|
||||
* Accepts formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA (with or without # prefix).
|
||||
* Normalizes output to always include the # prefix.
|
||||
*
|
||||
* @example
|
||||
* hexColor.optional()
|
||||
*/
|
||||
Hex color validation and normalization.
|
||||
Accepts formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA (with or without # prefix).
|
||||
Normalizes output to always include the # prefix.
|
||||
|
||||
@example
|
||||
hexColor.optional()
|
||||
*/
|
||||
const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
||||
export const hexColor = z
|
||||
.string()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -15,6 +15,7 @@ const consume = async <T>(generator: AsyncIterableIterator<T>) => {
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { ctx } = newMediumService(BaseService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(ActivityService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AlbumService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(ApiKeyService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AssetMediaService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AssetService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AuthAdminService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(AuthService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const { sut, ctx } = newMediumService(DuplicateService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getKyselyDB, makeStream } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(IntegrityService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -30,10 +30,10 @@ let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const fileModifiedAt = new Date(1_700_000_000_000);
|
||||
|
||||
const createFile = async (filePath: string, modifiedAt: Date = fileModifiedAt) => {
|
||||
const createFile = async (filePath: string) => {
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, 'test');
|
||||
await utimes(filePath, modifiedAt, modifiedAt);
|
||||
await utimes(filePath, fileModifiedAt, fileModifiedAt);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
@@ -103,6 +103,7 @@ class LibraryTestContext extends MediumTestContext<typeof LibraryService> {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
const ctx = new LibraryTestContext(db || defaultDatabase);
|
||||
return { sut: ctx.sut, ctx };
|
||||
|
||||
@@ -19,6 +19,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(MemoryService, {
|
||||
database: db || defaultDatabase,
|
||||
@@ -47,10 +48,14 @@ const create = async (ctx: ReturnType<typeof setup>['ctx']) => {
|
||||
|
||||
const newPersonAsset = async (
|
||||
ctx: ReturnType<typeof setup>['ctx'],
|
||||
{ ownerId, personGroupId, localDateTime }: { ownerId: string; personGroupId: string; localDateTime?: string },
|
||||
{
|
||||
ownerId,
|
||||
personGroupId,
|
||||
localDateTime = '2024-06-13T12:00:00.000Z',
|
||||
}: { ownerId: string; personGroupId: string; localDateTime?: string },
|
||||
) => {
|
||||
const assetRepo = ctx.get(AssetRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId, localDateTime: localDateTime ?? '2024-06-13T12:00:00.000Z' });
|
||||
const { asset } = await ctx.newAsset({ ownerId, localDateTime });
|
||||
await Promise.all([
|
||||
ctx.newExif({ assetId: asset.id, make: 'Canon' }),
|
||||
ctx.newJobStatus({ assetId: asset.id }),
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(OcrService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PartnerService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -49,6 +49,7 @@ const authenticationResponse = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PasskeyService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -13,6 +13,7 @@ let defaultDatabase: Kysely<DB>;
|
||||
const wasmBytes = Buffer.from('some-wasm-binary-data');
|
||||
const sha256hash = Buffer.from('some-manifest-hash');
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PluginService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -18,6 +18,7 @@ let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const unitVector = (index: number) => JSON.stringify(Array.from({ length: 512 }, (_, i) => (i === index ? 1 : 0)));
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(SearchService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(SharedLinkService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(SyncService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(TagService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(TimelineService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(VersionService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(WorkflowService, {
|
||||
database: db || defaultDatabase,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -22,6 +22,7 @@ class LineAutoClosingStream extends Writable {
|
||||
private count: number;
|
||||
lines: string[] = [];
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
constructor({ after }: { after: number }) {
|
||||
super({ highWaterMark: 1 });
|
||||
this.count = after;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getKyselyDB, wait } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unnecessary-parameters
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@
|
||||
"eslint-plugin-compat": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-svelte": "^3.23.1",
|
||||
"eslint-plugin-unicorn": "^75.0.0",
|
||||
"eslint-plugin-unicorn": "^78.0.0",
|
||||
"factory.ts": "^1.4.1",
|
||||
"globals": "^17.0.0",
|
||||
"happy-dom": "^21.0.0",
|
||||
|
||||
@@ -7,11 +7,11 @@ interface Options {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a function when a click occurs outside of the element, or when the escape key is pressed.
|
||||
* @param node
|
||||
* @param options Object containing onOutclick and onEscape functions
|
||||
* @returns
|
||||
*/
|
||||
Calls a function when a click occurs outside of the element, or when the escape key is pressed.
|
||||
@param node
|
||||
@param options Object containing onOutclick and onEscape functions
|
||||
@returns
|
||||
*/
|
||||
export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn<Options> {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const { onOutclick } = options;
|
||||
|
||||
@@ -3,33 +3,19 @@ import { tick } from 'svelte';
|
||||
import type { Action } from 'svelte/action';
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* A function that is called when the dropdown should be closed.
|
||||
*/
|
||||
/** A function that is called when the dropdown should be closed. */
|
||||
closeDropdown: () => void;
|
||||
/**
|
||||
* The container element that with direct children that should be navigated.
|
||||
*/
|
||||
/** The container element that with direct children that should be navigated. */
|
||||
container?: HTMLElement;
|
||||
/**
|
||||
* Indicates if the dropdown is open.
|
||||
*/
|
||||
/** Indicates if the dropdown is open. */
|
||||
isOpen: boolean;
|
||||
/**
|
||||
* Override the default behavior for the escape key.
|
||||
*/
|
||||
/** Override the default behavior for the escape key. */
|
||||
onEscape?: (event: KeyboardEvent) => void;
|
||||
/**
|
||||
* A function that is called when the dropdown should be opened.
|
||||
*/
|
||||
/** A function that is called when the dropdown should be opened. */
|
||||
openDropdown?: (event: KeyboardEvent) => void;
|
||||
/**
|
||||
* The id of the currently selected element.
|
||||
*/
|
||||
/** The id of the currently selected element. */
|
||||
selectedId: string | undefined;
|
||||
/**
|
||||
* A function that is called when the selection changes, to notify consumers of the new selected id.
|
||||
*/
|
||||
/** A function that is called when the selection changes, to notify consumers of the new selected id. */
|
||||
selectionChanged: (id: string | undefined) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ interface Options {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a function when focus leaves the element.
|
||||
* @param node
|
||||
* @param options Object containing onFocusOut function
|
||||
*/
|
||||
Calls a function when focus leaves the element.
|
||||
@param node
|
||||
@param options Object containing onFocusOut function
|
||||
*/
|
||||
export function focusOutside(node: HTMLElement, options: Options = {}) {
|
||||
const { onFocusOut } = options;
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ import { getTabbable } from '#lib/utils/focus-util.ts';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Set whether the trap is active or not.
|
||||
*/
|
||||
/** Set whether the trap is active or not. */
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user