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:
renovate[bot]
2026-10-28 20:41:28 +03:00
committed by Codex
parent 3127dbec71
commit 7fb7699c06
141 changed files with 654 additions and 761 deletions
+1 -1
View File
@@ -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);
+30 -34
View File
@@ -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
+19 -30
View File
@@ -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
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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();
+3 -3
View File
@@ -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
View File
@@ -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",
+4 -6
View File
@@ -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
View File
@@ -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 {
+4 -4
View File
@@ -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()
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+13 -13
View File
@@ -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) {
+5 -5
View File
@@ -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(
+22 -22
View File
@@ -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 {
+1 -3
View File
@@ -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: {
+1 -3
View File
@@ -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> {
+3 -4
View File
@@ -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);
+14 -14
View File
@@ -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 {
+4 -4
View File
@@ -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
+3 -9
View File
@@ -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;
};
+22 -22
View File
@@ -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] : [];
+1
View File
@@ -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 },
) {}
+4 -4
View File
@@ -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'],
+1 -3
View File
@@ -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 [];
+2 -6
View File
@@ -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));
}
+4 -6
View File
@@ -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;
+2 -2
View File
@@ -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] }) });
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+5 -5
View File
@@ -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;
+7 -21
View File
@@ -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;
}
+4 -4
View File
@@ -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;
+1 -3
View File
@@ -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