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
@@ -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,9 +8,9 @@ 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',
|
||||
THUMBNAIL = 'thumbnail',
|
||||
@@ -41,9 +41,9 @@ 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()
|
||||
.optional()
|
||||
|
||||
@@ -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,
|
||||
|
||||
+8
-12
@@ -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,9 +360,9 @@ 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',
|
||||
}
|
||||
|
||||
+7
-15
@@ -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,9 +84,9 @@ 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,12 +227,12 @@ 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 })
|
||||
async getAssetIds(albumId: string, assetIds: string[]): Promise<Set<string>> {
|
||||
@@ -348,13 +348,13 @@ 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,9 +399,9 @@ 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) {
|
||||
return this.db
|
||||
|
||||
@@ -73,10 +73,10 @@ 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 [
|
||||
{ tag: 'JpgFromRaw2', format: RawExtractedFormat.Jpeg },
|
||||
@@ -305,9 +305,9 @@ 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(
|
||||
'ffprobe',
|
||||
|
||||
@@ -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,19 +360,19 @@ 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}`, '');
|
||||
const hasDuplicationAnnotation = /^\+\d+$/.test(diff);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user