diff --git a/cli/package.json b/cli/package.json index 263a4ff26f..59d303eaa7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^24.10.4", + "@types/node": "^24.10.8", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 2e3fef07d6..ee5db45c9a 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -17,12 +17,17 @@ Hardware and software requirements for Immich: - Immich runs well in a virtualized environment when running in a full virtual machine. The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users. If you have issues, we recommend that you switch to a supported VM deployment. -- **RAM**: Minimum 4GB, recommended 6GB. +- **RAM**: Minimum 6GB, recommended 8GB. - **CPU**: Minimum 2 cores, recommended 4 cores. - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions. - The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average. -:::tip +:::note RAM requirements +For a smooth experience, especially during asset upload, Immich requires at least 6GB of RAM. +For systems with only 4GB of RAM, Immich can be run with machine learning features disabled. +::: + +:::tip Postgres setup Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience. The Postgres database files are typically between 1-3 GB in size. For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind. diff --git a/e2e/package.json b/e2e/package.json index c3d9a9f183..13138ca714 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -27,7 +27,7 @@ "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^24.10.4", + "@types/node": "^24.10.8", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 4ae542bacf..8b7f289921 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -8,7 +8,7 @@ dotenv.config({ path: resolve(import.meta.dirname, '.env') }); export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1'; export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1'; export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`; -export const playwriteSlowMo = parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0'); +export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0'); export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER; process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1'; @@ -39,13 +39,13 @@ const config: PlaywrightTestConfig = { testMatch: /.*\.e2e-spec\.ts/, workers: 1, }, - { - name: 'parallel tests', - use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.parallel-e2e-spec\.ts/, - fullyParallel: true, - workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1), - }, + // { + // name: 'parallel tests', + // use: { ...devices['Desktop Chrome'] }, + // testMatch: /.*\.parallel-e2e-spec\.ts/, + // fullyParallel: true, + // workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1), + // }, // { // name: 'firefox', diff --git a/e2e/src/api/specs/database-backups.e2e-spec.ts b/e2e/src/api/specs/database-backups.e2e-spec.ts new file mode 100644 index 0000000000..2b0f6ae61a --- /dev/null +++ b/e2e/src/api/specs/database-backups.e2e-spec.ts @@ -0,0 +1,350 @@ +import { LoginResponseDto, ManualJobName } from '@immich/sdk'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +describe('/admin/database-backups', () => { + let cookie: string | undefined; + let admin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup(); + await utils.resetBackups(admin.accessToken); + }); + + describe('GET /', async () => { + it('should succeed and be empty', async () => { + const { status, body } = await request(app) + .get('/admin/database-backups') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + backups: [], + }); + }); + + it('should contain a created backup', async () => { + await utils.createJob(admin.accessToken, { + name: ManualJobName.BackupDatabase, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'backupDatabase'); + + await expect + .poll( + async () => { + const { status, body } = await request(app) + .get('/admin/database-backups') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + return body; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toEqual( + expect.objectContaining({ + backups: [ + expect.objectContaining({ + filename: expect.stringMatching(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/), + filesize: expect.any(Number), + }), + ], + }), + ); + }); + }); + + describe('DELETE /', async () => { + it('should delete backup', async () => { + const filename = await utils.createBackup(admin.accessToken); + + const { status } = await request(app) + .delete(`/admin/database-backups`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ backups: [filename] }); + + expect(status).toBe(200); + + const { status: listStatus, body } = await request(app) + .get('/admin/database-backups') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(listStatus).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + backups: [], + }), + ); + }); + }); + + // => action: restore database flow + + describe.sequential('POST /start-restore', () => { + afterAll(async () => { + await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ action: 'end' }); + await utils.poll( + () => request(app).get('/server/config'), + ({ status, body }) => status === 200 && !body.maintenanceMode, + ); + + admin = await utils.adminSetup(); + }); + + it.sequential('should not work when the server is configured', async () => { + const { status, body } = await request(app).post('/admin/database-backups/start-restore').send(); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('The server already has an admin')); + }); + + it.sequential('should enter maintenance mode in "database restore mode"', async () => { + await utils.resetDatabase(); // reset database before running this test + + const { status, headers } = await request(app).post('/admin/database-backups/start-restore').send(); + + expect(status).toBe(201); + + cookie = headers['set-cookie'][0].split(';')[0]; + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status2).toBe(200); + expect(body).toEqual({ + active: true, + action: 'select_database_restore', + }); + }); + }); + + // => action: restore database + + describe.sequential('POST /backups/restore', () => { + beforeAll(async () => { + await utils.disconnectDatabase(); + }); + + afterAll(async () => { + await utils.connectDatabase(); + }); + + it.sequential('should restore a backup', { timeout: 60_000 }, async () => { + let filename = await utils.createBackup(admin.accessToken); + + // work-around until test is running on released version + await utils.move( + `/data/backups/${filename}`, + '/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz', + ); + filename = 'immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz'; + + const { status } = await request(app) + .post('/admin/maintenance') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + action: 'restore_database', + restoreBackupFilename: filename, + }); + + expect(status).toBe(201); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status2).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + }), + ); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 60_000, + }, + ) + .toBeFalsy(); + }); + + it.sequential('fail to restore a corrupted backup', { timeout: 60_000 }, async () => { + await utils.prepareTestBackup('corrupted'); + + const { status, headers } = await request(app) + .post('/admin/maintenance') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + action: 'restore_database', + restoreBackupFilename: 'development-corrupted.sql.gz', + }); + + expect(status).toBe(201); + cookie = headers['set-cookie'][0].split(';')[0]; + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + return body; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: 'Something went wrong, see logs!', + }), + ); + + const { status: status2, body: body2 } = await request(app) + .get('/admin/maintenance/status') + .set('cookie', cookie!) + .send({ token: 'token' }); + expect(status2).toBe(200); + expect(body2).toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: expect.stringContaining('IM CORRUPTED'), + }), + ); + + await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ + action: 'end', + }); + + await utils.poll( + () => request(app).get('/server/config'), + ({ status, body }) => status === 200 && !body.maintenanceMode, + ); + }); + + it.sequential('rollback to restore point if backup is missing admin', { timeout: 60_000 }, async () => { + await utils.prepareTestBackup('empty'); + + const { status, headers } = await request(app) + .post('/admin/maintenance') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + action: 'restore_database', + restoreBackupFilename: 'development-empty.sql.gz', + }); + + expect(status).toBe(201); + cookie = headers['set-cookie'][0].split(';')[0]; + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + return body; + }, + { + interval: 500, + timeout: 30_000, + }, + ) + .toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: 'Something went wrong, see logs!', + }), + ); + + const { status: status2, body: body2 } = await request(app) + .get('/admin/maintenance/status') + .set('cookie', cookie!) + .send({ token: 'token' }); + expect(status2).toBe(200); + expect(body2).toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: expect.stringContaining('Server health check failed, no admin exists.'), + }), + ); + + await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ + action: 'end', + }); + + await utils.poll( + () => request(app).get('/server/config'), + ({ status, body }) => status === 200 && !body.maintenanceMode, + ); + }); + }); +}); diff --git a/e2e/src/api/specs/maintenance.e2e-spec.ts b/e2e/src/api/specs/maintenance.e2e-spec.ts index 9e010e60d4..819a4e31f7 100644 --- a/e2e/src/api/specs/maintenance.e2e-spec.ts +++ b/e2e/src/api/specs/maintenance.e2e-spec.ts @@ -14,6 +14,7 @@ describe('/admin/maintenance', () => { await utils.resetDatabase(); admin = await utils.adminSetup(); nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + await utils.resetBackups(admin.accessToken); }); // => outside of maintenance mode @@ -26,6 +27,17 @@ describe('/admin/maintenance', () => { }); }); + describe('GET /status', async () => { + it('to always indicate we are not in maintenance mode', async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + expect(body).toEqual({ + active: false, + action: 'end', + }); + }); + }); + describe('POST /login', async () => { it('should not work out of maintenance mode', async () => { const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' }); @@ -39,6 +51,7 @@ describe('/admin/maintenance', () => { describe.sequential('POST /', () => { it('should require authentication', async () => { const { status, body } = await request(app).post('/admin/maintenance').send({ + active: false, action: 'end', }); expect(status).toBe(401); @@ -69,6 +82,7 @@ describe('/admin/maintenance', () => { .send({ action: 'start', }); + expect(status).toBe(201); cookie = headers['set-cookie'][0].split(';')[0]; @@ -79,7 +93,8 @@ describe('/admin/maintenance', () => { await expect .poll( async () => { - const { body } = await request(app).get('/server/config'); + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); return body.maintenanceMode; }, { @@ -102,6 +117,17 @@ describe('/admin/maintenance', () => { }); }); + describe('GET /status', async () => { + it('to indicate we are in maintenance mode', async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + expect(body).toEqual({ + active: true, + action: 'start', + }); + }); + }); + describe('POST /login', async () => { it('should fail without cookie or token in body', async () => { const { status, body } = await request(app).post('/admin/maintenance/login').send({}); @@ -158,7 +184,8 @@ describe('/admin/maintenance', () => { await expect .poll( async () => { - const { body } = await request(app).get('/server/config'); + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); return body.maintenanceMode; }, { diff --git a/e2e/src/generators/timeline/rest-response.ts b/e2e/src/generators/timeline/rest-response.ts index 21cf59e793..a193535cd3 100644 --- a/e2e/src/generators/timeline/rest-response.ts +++ b/e2e/src/generators/timeline/rest-response.ts @@ -348,6 +348,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons checksum: asset.checksum, width: exifInfo.exifImageWidth ?? 1, height: exifInfo.exifImageHeight ?? 1, + isEdited: false, }; } diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index e571acdf1d..85712f95b4 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -8,6 +8,7 @@ import { CreateLibraryDto, JobCreateDto, MaintenanceAction, + ManualJobName, MetadataSearchDto, Permission, PersonCreateDto, @@ -30,10 +31,12 @@ import { createStack, createUserAdmin, deleteAssets, + deleteDatabaseBackup, getAssetInfo, getConfig, getConfigDefaults, getQueuesLegacy, + listDatabaseBackups, login, runQueueCommandLegacy, scanLibrary, @@ -62,6 +65,7 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { setTimeout as setAsyncTimeout } from 'node:timers/promises'; import { promisify } from 'node:util'; +import { createGzip } from 'node:zlib'; import pg from 'pg'; import { io, type Socket } from 'socket.io-client'; import { loginDto, signupDto } from 'src/fixtures'; @@ -89,8 +93,9 @@ export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer $ export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); export const immichCli = (args: string[]) => executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise; -export const immichAdmin = (args: string[]) => - executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); +export const dockerExec = (args: string[]) => + executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]); +export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]); export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -154,12 +159,26 @@ const onEvent = ({ event, id }: { event: EventType; id: string }) => { }; export const utils = { + connectDatabase: async () => { + if (!client) { + client = new pg.Client(dbUrl); + client.on('end', () => (client = null)); + client.on('error', () => (client = null)); + await client.connect(); + } + + return client; + }, + + disconnectDatabase: async () => { + if (client) { + await client.end(); + } + }, + resetDatabase: async (tables?: string[]) => { try { - if (!client) { - client = new pg.Client(dbUrl); - await client.connect(); - } + client = await utils.connectDatabase(); tables = tables || [ // TODO e2e test for deleting a stack, since it is quite complex @@ -616,6 +635,36 @@ export const utils = { return executeCommand('docker', ['exec', 'immich-e2e-server', 'mkdir', '-p', path]).promise; }, + createBackup: async (accessToken: string) => { + await utils.createJob(accessToken, { + name: ManualJobName.BackupDatabase, + }); + + return utils.poll( + () => request(app).get('/admin/database-backups').set('Authorization', `Bearer ${accessToken}`), + ({ status, body }) => status === 200 && body.backups.length === 1, + ({ body }) => body.backups[0].filename, + ); + }, + + resetBackups: async (accessToken: string) => { + const { backups } = await listDatabaseBackups({ headers: asBearerAuth(accessToken) }); + await deleteDatabaseBackup({ databaseBackupDeleteDto: { backups } }, { headers: asBearerAuth(accessToken) }); + }, + + prepareTestBackup: async (generate: 'empty' | 'corrupted') => { + const dir = await mkdtemp(join(tmpdir(), 'test-')); + const fn = join(dir, 'file'); + + const sql = Readable.from(generate === 'corrupted' ? 'IM CORRUPTED;' : 'SELECT 1;'); + const gzip = createGzip(); + const writeStream = createWriteStream(fn); + await pipeline(sql, gzip, writeStream); + + await executeCommand('docker', ['cp', fn, `immich-e2e-server:/data/backups/development-${generate}.sql.gz`]) + .promise; + }, + resetAdminConfig: async (accessToken: string) => { const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) }); await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); @@ -658,6 +707,25 @@ export const utils = { await utils.waitForQueueFinish(accessToken, 'sidecar'); await utils.waitForQueueFinish(accessToken, 'metadataExtraction'); }, + + async poll(cb: () => Promise, validate: (value: T) => boolean, map?: (value: T) => any) { + let timeout = 0; + while (true) { + try { + const data = await cb(); + if (validate(data)) { + return map ? map(data) : data; + } + timeout++; + if (timeout >= 10) { + throw 'Could not clean up test.'; + } + await new Promise((resolve) => setTimeout(resolve, 5e2)); + } catch { + // no-op + } + } + }, }; utils.initSdk(); diff --git a/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts b/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts index 3d65b20c87..669f1b815c 100644 --- a/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts @@ -12,7 +12,7 @@ import { import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; import { utils } from 'src/utils'; -import { assetViewerUtils, cancelAllPollers } from 'src/web/specs/timeline/utils'; +import { assetViewerUtils } from 'src/web/specs/timeline/utils'; test.describe.configure({ mode: 'parallel' }); test.describe('asset-viewer', () => { @@ -49,7 +49,6 @@ test.describe('asset-viewer', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; diff --git a/e2e/src/web/specs/database-backups.e2e-spec.ts b/e2e/src/web/specs/database-backups.e2e-spec.ts new file mode 100644 index 0000000000..d101215ceb --- /dev/null +++ b/e2e/src/web/specs/database-backups.e2e-spec.ts @@ -0,0 +1,105 @@ +import { LoginResponseDto } from '@immich/sdk'; +import { expect, test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Database Backups', () => { + let admin: LoginResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test('restore a backup from settings', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + const filename = await utils.createBackup(admin.accessToken); + await utils.setAuthCookies(context, admin.accessToken); + + // work-around until test is running on released version + await utils.move( + `/data/backups/${filename}`, + '/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz', + ); + + await page.goto('/admin/maintenance?isOpen=backups'); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await page.waitForURL('/admin/maintenance**', { timeout: 60_000 }); + }); + + test('handle backup restore failure', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + await utils.prepareTestBackup('corrupted'); + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto('/admin/maintenance?isOpen=backups'); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await expect(page.getByText('IM CORRUPTED')).toBeVisible({ timeout: 60_000 }); + await page.getByRole('button', { name: 'End maintenance mode' }).click(); + await page.waitForURL('/admin/maintenance**'); + }); + + test('rollback to restore point if backup is missing admin', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + await utils.prepareTestBackup('empty'); + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto('/admin/maintenance?isOpen=backups'); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await expect(page.getByText('Server health check failed, no admin exists.')).toBeVisible({ timeout: 60_000 }); + await page.getByRole('button', { name: 'End maintenance mode' }).click(); + await page.waitForURL('/admin/maintenance**'); + }); + + test('restore a backup from onboarding', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + const filename = await utils.createBackup(admin.accessToken); + await utils.setAuthCookies(context, admin.accessToken); + + // work-around until test is running on released version + await utils.move( + `/data/backups/${filename}`, + '/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz', + ); + + await utils.resetDatabase(); + + await page.goto('/'); + await page.getByRole('button', { name: 'Restore from backup' }).click(); + + try { + await page.waitForURL('/maintenance**'); + } catch { + // when chained with the rest of the tests + // this navigation may fail..? not sure why... + await page.goto('/maintenance'); + await page.waitForURL('/maintenance**'); + } + + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await page.waitForURL('/photos', { timeout: 60_000 }); + }); +}); diff --git a/e2e/src/web/specs/maintenance.e2e-spec.ts b/e2e/src/web/specs/maintenance.e2e-spec.ts index 534c05f783..8b1631f0bf 100644 --- a/e2e/src/web/specs/maintenance.e2e-spec.ts +++ b/e2e/src/web/specs/maintenance.e2e-spec.ts @@ -16,12 +16,12 @@ test.describe('Maintenance', () => { test('enter and exit maintenance mode', async ({ context, page }) => { await utils.setAuthCookies(context, admin.accessToken); - await page.goto('/admin/system-settings?isOpen=maintenance'); - await page.getByRole('button', { name: 'Start maintenance mode' }).click(); + await page.goto('/admin/maintenance'); + await page.getByRole('button', { name: 'Switch to maintenance mode' }).click(); await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 }); await page.getByRole('button', { name: 'End maintenance mode' }).click(); - await page.waitForURL('**/admin/system-settings*', { timeout: 10_000 }); + await page.waitForURL('**/admin/maintenance*', { timeout: 10_000 }); }); test('maintenance shows no options to users until they authenticate', async ({ page }) => { diff --git a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts b/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts index 5faf8380d1..47026e2cd4 100644 --- a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts @@ -18,7 +18,6 @@ import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } fro import { utils } from 'src/utils'; import { assetViewerUtils, - cancelAllPollers, padYearMonth, pageUtils, poll, @@ -64,7 +63,6 @@ test.describe('Timeline', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/web/specs/timeline/utils.ts index 397a1656e8..0f04bf9361 100644 --- a/e2e/src/web/specs/timeline/utils.ts +++ b/e2e/src/web/specs/timeline/utils.ts @@ -23,13 +23,6 @@ export async function throttlePage(context: BrowserContext, page: Page) { await session.send('Emulation.setCPUThrottlingRate', { rate: 10 }); } -let activePollsAbortController = new AbortController(); - -export const cancelAllPollers = () => { - activePollsAbortController.abort(); - activePollsAbortController = new AbortController(); -}; - export const poll = async ( page: Page, query: () => Promise, @@ -37,21 +30,14 @@ export const poll = async ( ) => { let result; const timeout = Date.now() + 10_000; - const signal = activePollsAbortController.signal; const terminate = callback || ((result: Awaited | undefined) => !!result); while (!terminate(result) && Date.now() < timeout) { - if (signal.aborted) { - return; - } try { result = await query(); } catch { // ignore } - if (signal.aborted) { - return; - } if (page.isClosed()) { return; } diff --git a/i18n/en.json b/i18n/en.json index 2d2f270e44..59a39ded16 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -189,6 +189,9 @@ "machine_learning_smart_search_enabled": "Enable smart search", "machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.", "machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.", + "maintenance_delete_backup": "Delete Backup", + "maintenance_delete_backup_description": "This file will be irrevocably deleted.", + "maintenance_delete_error": "Failed to delete backup.", "maintenance_integrity_check_all": "Check All", "maintenance_integrity_checksum_mismatch": "Checksum Mismatch", "maintenance_integrity_checksum_mismatch_job": "Check for checksum mismatches", @@ -200,10 +203,18 @@ "maintenance_integrity_untracked_file": "Untracked Files", "maintenance_integrity_untracked_file_job": "Check for untracked files", "maintenance_integrity_untracked_file_refresh_job": "Refresh untracked file reports", + "maintenance_restore_backup": "Restore Backup", + "maintenance_restore_backup_description": "Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.", + "maintenance_restore_backup_different_version": "This backup was created with a different version of Immich!", + "maintenance_restore_backup_unknown_version": "Couldn't determine backup version.", + "maintenance_restore_database_backup": "Restore database backup", + "maintenance_restore_database_backup_description": "Rollback to an earlier database state using a backup file", "maintenance_settings": "Maintenance", "maintenance_settings_description": "Put Immich into maintenance mode.", - "maintenance_start": "Start maintenance mode", + "maintenance_start": "Switch to maintenance mode", "maintenance_start_error": "Failed to start maintenance mode.", + "maintenance_upload_backup": "Upload database backup file", + "maintenance_upload_backup_error": "Could not upload backup, is it an .sql/.sql.gz file?", "manage_concurrency": "Manage Concurrency", "manage_concurrency_description": "Navigate to the jobs page to manage job concurrency", "manage_log_settings": "Manage log settings", @@ -615,7 +626,7 @@ "backup_album_selection_page_select_albums": "Select albums", "backup_album_selection_page_selection_info": "Selection Info", "backup_album_selection_page_total_assets": "Total unique assets", - "backup_albums_sync": "Backup albums synchronization", + "backup_albums_sync": "Backup Albums Synchronization", "backup_all": "All", "backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…", "backup_background_service_complete_notification": "Asset backup complete", @@ -940,6 +951,7 @@ "download_include_embedded_motion_videos": "Embedded videos", "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", "download_notfound": "Download not found", + "download_original": "Download original", "download_paused": "Download paused", "download_settings": "Download", "download_settings_description": "Manage settings related to asset download", @@ -1418,10 +1430,28 @@ "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", "main_branch_warning": "You're using a development version; we strongly recommend using a release version!", "main_menu": "Main menu", + "maintenance_action_restore": "Restoring Database", "maintenance_description": "Immich has been put into maintenance mode.", "maintenance_end": "End maintenance mode", "maintenance_end_error": "Failed to end maintenance mode.", "maintenance_logged_in_as": "Currently logged in as {user}", + "maintenance_restore_from_backup": "Restore From Backup", + "maintenance_restore_library": "Restore Your Library", + "maintenance_restore_library_confirm": "If this looks correct, continue to restoring a backup!", + "maintenance_restore_library_description": "Restoring Database", + "maintenance_restore_library_folder_has_files": "{folder} has {count} folder(s)", + "maintenance_restore_library_folder_no_files": "{folder} is missing files!", + "maintenance_restore_library_folder_pass": "readable and writable", + "maintenance_restore_library_folder_read_fail": "not readable", + "maintenance_restore_library_folder_write_fail": "not writable", + "maintenance_restore_library_hint_missing_files": "You may be missing important files", + "maintenance_restore_library_hint_regenerate_later": "You can regenerate these later in settings", + "maintenance_restore_library_hint_storage_template_missing_files": "Using storage template? You may be missing files", + "maintenance_restore_library_loading": "Loading integrity checks and heuristics…", + "maintenance_task_backup": "Creating a backup of the existing database…", + "maintenance_task_migrations": "Running database migrations…", + "maintenance_task_restore": "Restoring the chosen backup…", + "maintenance_task_rollback": "Restore failed, rolling back to restore point…", "maintenance_title": "Temporarily Unavailable", "make": "Make", "manage_geolocation": "Manage location", @@ -2229,6 +2259,7 @@ "unhide_person": "Unhide person", "unknown": "Unknown", "unknown_country": "Unknown Country", + "unknown_date": "Unknown date", "unknown_year": "Unknown Year", "unlimited": "Unlimited", "unlink_motion_video": "Unlink motion video", @@ -2271,7 +2302,7 @@ "url": "URL", "usage": "Usage", "use_biometric": "Use biometric", - "use_current_connection": "use current connection", + "use_current_connection": "Use current connection", "use_custom_date_range": "Use custom date range instead", "user": "User", "user_has_been_deleted": "This user has been deleted.", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 32b2bc6db0..dfc217c118 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -92,14 +92,14 @@ FROM python:3.13-slim-trixie@sha256:0222b795db95bf7412cede36ab46a266cfb31f632e64 RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ - wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.24.8/intel-igc-core-2_2.24.8+20344_amd64.deb && \ - wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.24.8/intel-igc-opencl-2_2.24.8+20344_amd64.deb && \ - wget -nv https://github.com/intel/compute-runtime/releases/download/25.48.36300.8/intel-opencl-icd_25.48.36300.8-0_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-core-2_2.27.10+20617_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-opencl-2_2.27.10+20617_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/intel-opencl-icd_26.01.36711.4-0_amd64.deb && \ wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \ wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \ wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \ # TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file - wget -nv https://github.com/intel/compute-runtime/releases/download/25.48.36300.8/libigdgmm12_22.8.2_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \ dpkg -i *.deb && \ rm *.deb && \ apt-get remove wget -yqq && \ diff --git a/mise.toml b/mise.toml index f020a078d0..dd2bfdf079 100644 --- a/mise.toml +++ b/mise.toml @@ -3,7 +3,7 @@ experimental_monorepo_root = true [tools] node = "24.13.0" flutter = "3.35.7" -pnpm = "10.27.0" +pnpm = "10.28.0" terragrunt = "0.93.10" opentofu = "1.10.7" java = "25.0.1" diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index c6e04e5a10..0d4925077a 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -117,6 +117,9 @@ + diff --git a/mobile/drift_schemas/main/drift_schema_v17.json b/mobile/drift_schemas/main/drift_schema_v17.json new file mode 100644 index 0000000000..a26b7b57ad --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v17.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":13,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":14,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":15,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":16,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":17,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":18,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":19,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":21,"references":[1,20],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":22,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":23,"references":[1,22],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":24,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":25,"references":[],"type":"table","data":{"name":"trashed_local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":26,"references":[16],"type":"index","data":{"on":16,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":27,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":28,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 5774a13c90..310e30ea62 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -22,6 +22,7 @@ sealed class BaseAsset { final int? durationInSeconds; final bool isFavorite; final String? livePhotoVideoId; + final bool isEdited; const BaseAsset({ required this.name, @@ -34,6 +35,7 @@ sealed class BaseAsset { this.durationInSeconds, this.isFavorite = false, this.livePhotoVideoId, + required this.isEdited, }); bool get isImage => type == AssetType.image; @@ -71,6 +73,7 @@ sealed class BaseAsset { height: ${height ?? ""}, durationInSeconds: ${durationInSeconds ?? ""}, isFavorite: $isFavorite, + isEdited: $isEdited, }'''; } @@ -85,7 +88,8 @@ sealed class BaseAsset { width == other.width && height == other.height && durationInSeconds == other.durationInSeconds && - isFavorite == other.isFavorite; + isFavorite == other.isFavorite && + isEdited == other.isEdited; } return false; } @@ -99,6 +103,7 @@ sealed class BaseAsset { width.hashCode ^ height.hashCode ^ durationInSeconds.hashCode ^ - isFavorite.hashCode; + isFavorite.hashCode ^ + isEdited.hashCode; } } diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index b7ef635f2d..887dfd3834 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -28,6 +28,7 @@ class LocalAsset extends BaseAsset { this.adjustmentTime, this.latitude, this.longitude, + required super.isEdited, }) : remoteAssetId = remoteId; @override @@ -107,6 +108,7 @@ class LocalAsset extends BaseAsset { DateTime? adjustmentTime, double? latitude, double? longitude, + bool? isEdited, }) { return LocalAsset( id: id ?? this.id, @@ -125,6 +127,7 @@ class LocalAsset extends BaseAsset { adjustmentTime: adjustmentTime ?? this.adjustmentTime, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, + isEdited: isEdited ?? this.isEdited, ); } } diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 4974dc9118..43d49506e3 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -28,6 +28,7 @@ class RemoteAsset extends BaseAsset { this.visibility = AssetVisibility.timeline, super.livePhotoVideoId, this.stackId, + required super.isEdited, }) : localAssetId = localId; @override @@ -104,6 +105,7 @@ class RemoteAsset extends BaseAsset { AssetVisibility? visibility, String? livePhotoVideoId, String? stackId, + bool? isEdited, }) { return RemoteAsset( id: id ?? this.id, @@ -122,6 +124,7 @@ class RemoteAsset extends BaseAsset { visibility: visibility ?? this.visibility, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, stackId: stackId ?? this.stackId, + isEdited: isEdited ?? this.isEdited, ); } } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 8b324cf6cf..e4a129d322 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -436,5 +436,6 @@ extension PlatformToLocalAsset on PlatformAsset { adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true), latitude: latitude, longitude: longitude, + isEdited: false, ); } diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index 6ccc5a97bf..a3f935c492 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -77,6 +77,7 @@ extension on AssetResponseDto { thumbHash: thumbhash, localId: null, type: type.toAssetType(), + isEdited: isEdited, ); } } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index e14321a780..d5029abac8 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -247,6 +247,42 @@ class SyncStreamService { } } + Future handleWsAssetEditReadyV1Batch(List batchData) async { + if (batchData.isEmpty) return; + + _logger.info('Processing batch of ${batchData.length} AssetEditReadyV1 events'); + + final List assets = []; + + try { + for (final data in batchData) { + if (data is! Map) { + continue; + } + + final payload = data; + final assetData = payload['asset']; + + if (assetData == null) { + continue; + } + + final asset = SyncAssetV1.fromJson(assetData); + + if (asset != null) { + assets.add(asset); + } + } + + if (assets.isNotEmpty) { + await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-edit'); + _logger.info('Successfully processed ${assets.length} edited assets'); + } + } catch (error, stackTrace) { + _logger.severe("Error processing AssetEditReadyV1 websocket batch events", error, stackTrace); + } + } + Future _handleRemoteTrashed(Iterable checksums) async { if (checksums.isEmpty) { return Future.value(); diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index 637ae20cb8..6840bae595 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -196,6 +196,16 @@ class BackgroundSyncManager { }); } + Future syncWebsocketEditBatch(List batchData) { + if (_syncWebsocketTask != null) { + return _syncWebsocketTask!.future; + } + _syncWebsocketTask = _handleWsAssetEditReadyV1Batch(batchData); + return _syncWebsocketTask!.whenComplete(() { + _syncWebsocketTask = null; + }); + } + Future syncLinkedAlbum() { if (_linkedAlbumSyncTask != null) { return _linkedAlbumSyncTask!.future; @@ -231,3 +241,8 @@ Cancelable _handleWsAssetUploadReadyV1Batch(List batchData) => ru computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData), debugLabel: 'websocket-batch', ); + +Cancelable _handleWsAssetEditReadyV1Batch(List batchData) => runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1Batch(batchData), + debugLabel: 'websocket-edit', +); diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 6591f922ae..9d154a5013 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -47,5 +47,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { latitude: latitude, longitude: longitude, cloudId: iCloudId, + isEdited: false, ); } diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index 93d7f0c90d..1db22b558e 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -25,7 +25,8 @@ SELECT NULL as i_cloud_id, NULL as latitude, NULL as longitude, - NULL as adjustmentTime + NULL as adjustmentTime, + rae.is_edited FROM remote_asset_entity rae LEFT JOIN @@ -61,7 +62,8 @@ SELECT lae.i_cloud_id, lae.latitude, lae.longitude, - lae.adjustment_time + lae.adjustment_time, + 0 as is_edited FROM local_asset_entity lae WHERE NOT EXISTS ( diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart index 169004b45d..f71aa8eb54 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart +++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart @@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor { ); $arrayStartIndex += generatedlimit.amountOfVariables; return customSelect( - 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}', + 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}', variables: [ for (var $ in userIds) i0.Variable($), ...generatedlimit.introducedVariables, @@ -66,6 +66,7 @@ class MergedAssetDrift extends i1.ModularAccessor { latitude: row.readNullable('latitude'), longitude: row.readNullable('longitude'), adjustmentTime: row.readNullable('adjustmentTime'), + isEdited: row.read('is_edited'), ), ); } @@ -137,6 +138,7 @@ class MergedAssetResult { final double? latitude; final double? longitude; final DateTime? adjustmentTime; + final bool isEdited; MergedAssetResult({ this.remoteId, this.localId, @@ -158,6 +160,7 @@ class MergedAssetResult { this.latitude, this.longitude, this.adjustmentTime, + required this.isEdited, }); } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index dcc885a2a9..4dc0fa568f 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -44,6 +44,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin TextColumn get libraryId => text().nullable()(); + BoolColumn get isEdited => boolean().withDefault(const Constant(false))(); + @override Set get primaryKey => {id}; } @@ -66,5 +68,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { livePhotoVideoId: livePhotoVideoId, localId: localId, stackId: stackId, + isEdited: isEdited, ); } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index eab7f95f64..2d9e8b235e 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -31,6 +31,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder = required i2.AssetVisibility visibility, i0.Value stackId, i0.Value libraryId, + i0.Value isEdited, }); typedef $$RemoteAssetEntityTableUpdateCompanionBuilder = i1.RemoteAssetEntityCompanion Function({ @@ -52,6 +53,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder = i0.Value visibility, i0.Value stackId, i0.Value libraryId, + i0.Value isEdited, }); final class $$RemoteAssetEntityTableReferences @@ -196,6 +198,11 @@ class $$RemoteAssetEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); + i0.ColumnFilters get isEdited => $composableBuilder( + column: $table.isEdited, + builder: (column) => i0.ColumnFilters(column), + ); + i5.$$UserEntityTableFilterComposer get ownerId { final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, @@ -318,6 +325,11 @@ class $$RemoteAssetEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); + i0.ColumnOrderings get isEdited => $composableBuilder( + column: $table.isEdited, + builder: (column) => i0.ColumnOrderings(column), + ); + i5.$$UserEntityTableOrderingComposer get ownerId { final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, @@ -417,6 +429,9 @@ class $$RemoteAssetEntityTableAnnotationComposer i0.GeneratedColumn get libraryId => $composableBuilder(column: $table.libraryId, builder: (column) => column); + i0.GeneratedColumn get isEdited => + $composableBuilder(column: $table.isEdited, builder: (column) => column); + i5.$$UserEntityTableAnnotationComposer get ownerId { final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -497,6 +512,7 @@ class $$RemoteAssetEntityTableTableManager const i0.Value.absent(), i0.Value stackId = const i0.Value.absent(), i0.Value libraryId = const i0.Value.absent(), + i0.Value isEdited = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion( name: name, type: type, @@ -516,6 +532,7 @@ class $$RemoteAssetEntityTableTableManager visibility: visibility, stackId: stackId, libraryId: libraryId, + isEdited: isEdited, ), createCompanionCallback: ({ @@ -537,6 +554,7 @@ class $$RemoteAssetEntityTableTableManager required i2.AssetVisibility visibility, i0.Value stackId = const i0.Value.absent(), i0.Value libraryId = const i0.Value.absent(), + i0.Value isEdited = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion.insert( name: name, type: type, @@ -556,6 +574,7 @@ class $$RemoteAssetEntityTableTableManager visibility: visibility, stackId: stackId, libraryId: libraryId, + isEdited: isEdited, ), withReferenceMapper: (p0) => p0 .map( @@ -844,6 +863,21 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity type: i0.DriftSqlType.string, requiredDuringInsert: false, ); + static const i0.VerificationMeta _isEditedMeta = const i0.VerificationMeta( + 'isEdited', + ); + @override + late final i0.GeneratedColumn isEdited = i0.GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const i4.Constant(false), + ); @override List get $columns => [ name, @@ -864,6 +898,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity visibility, stackId, libraryId, + isEdited, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -987,6 +1022,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity libraryId.isAcceptableOrUnknown(data['library_id']!, _libraryIdMeta), ); } + if (data.containsKey('is_edited')) { + context.handle( + _isEditedMeta, + isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta), + ); + } return context; } @@ -1075,6 +1116,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity i0.DriftSqlType.string, data['${effectivePrefix}library_id'], ), + isEdited: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, ); } @@ -1115,6 +1160,7 @@ class RemoteAssetEntityData extends i0.DataClass final i2.AssetVisibility visibility; final String? stackId; final String? libraryId; + final bool isEdited; const RemoteAssetEntityData({ required this.name, required this.type, @@ -1134,6 +1180,7 @@ class RemoteAssetEntityData extends i0.DataClass required this.visibility, this.stackId, this.libraryId, + required this.isEdited, }); @override Map toColumns(bool nullToAbsent) { @@ -1182,6 +1229,7 @@ class RemoteAssetEntityData extends i0.DataClass if (!nullToAbsent || libraryId != null) { map['library_id'] = i0.Variable(libraryId); } + map['is_edited'] = i0.Variable(isEdited); return map; } @@ -1213,6 +1261,7 @@ class RemoteAssetEntityData extends i0.DataClass ), stackId: serializer.fromJson(json['stackId']), libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), ); } @override @@ -1241,6 +1290,7 @@ class RemoteAssetEntityData extends i0.DataClass ), 'stackId': serializer.toJson(stackId), 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), }; } @@ -1263,6 +1313,7 @@ class RemoteAssetEntityData extends i0.DataClass i2.AssetVisibility? visibility, i0.Value stackId = const i0.Value.absent(), i0.Value libraryId = const i0.Value.absent(), + bool? isEdited, }) => i1.RemoteAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -1288,6 +1339,7 @@ class RemoteAssetEntityData extends i0.DataClass visibility: visibility ?? this.visibility, stackId: stackId.present ? stackId.value : this.stackId, libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, ); RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { return RemoteAssetEntityData( @@ -1319,6 +1371,7 @@ class RemoteAssetEntityData extends i0.DataClass : this.visibility, stackId: data.stackId.present ? data.stackId.value : this.stackId, libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, ); } @@ -1342,7 +1395,8 @@ class RemoteAssetEntityData extends i0.DataClass ..write('livePhotoVideoId: $livePhotoVideoId, ') ..write('visibility: $visibility, ') ..write('stackId: $stackId, ') - ..write('libraryId: $libraryId') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') ..write(')')) .toString(); } @@ -1367,6 +1421,7 @@ class RemoteAssetEntityData extends i0.DataClass visibility, stackId, libraryId, + isEdited, ); @override bool operator ==(Object other) => @@ -1389,7 +1444,8 @@ class RemoteAssetEntityData extends i0.DataClass other.livePhotoVideoId == this.livePhotoVideoId && other.visibility == this.visibility && other.stackId == this.stackId && - other.libraryId == this.libraryId); + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); } class RemoteAssetEntityCompanion @@ -1412,6 +1468,7 @@ class RemoteAssetEntityCompanion final i0.Value visibility; final i0.Value stackId; final i0.Value libraryId; + final i0.Value isEdited; const RemoteAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -1431,6 +1488,7 @@ class RemoteAssetEntityCompanion this.visibility = const i0.Value.absent(), this.stackId = const i0.Value.absent(), this.libraryId = const i0.Value.absent(), + this.isEdited = const i0.Value.absent(), }); RemoteAssetEntityCompanion.insert({ required String name, @@ -1451,6 +1509,7 @@ class RemoteAssetEntityCompanion required i2.AssetVisibility visibility, this.stackId = const i0.Value.absent(), this.libraryId = const i0.Value.absent(), + this.isEdited = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id), @@ -1476,6 +1535,7 @@ class RemoteAssetEntityCompanion i0.Expression? visibility, i0.Expression? stackId, i0.Expression? libraryId, + i0.Expression? isEdited, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -1496,6 +1556,7 @@ class RemoteAssetEntityCompanion if (visibility != null) 'visibility': visibility, if (stackId != null) 'stack_id': stackId, if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, }); } @@ -1518,6 +1579,7 @@ class RemoteAssetEntityCompanion i0.Value? visibility, i0.Value? stackId, i0.Value? libraryId, + i0.Value? isEdited, }) { return i1.RemoteAssetEntityCompanion( name: name ?? this.name, @@ -1538,6 +1600,7 @@ class RemoteAssetEntityCompanion visibility: visibility ?? this.visibility, stackId: stackId ?? this.stackId, libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, ); } @@ -1602,6 +1665,9 @@ class RemoteAssetEntityCompanion if (libraryId.present) { map['library_id'] = i0.Variable(libraryId.value); } + if (isEdited.present) { + map['is_edited'] = i0.Variable(isEdited.value); + } return map; } @@ -1625,7 +1691,8 @@ class RemoteAssetEntityCompanion ..write('livePhotoVideoId: $livePhotoVideoId, ') ..write('visibility: $visibility, ') ..write('stackId: $stackId, ') - ..write('libraryId: $libraryId') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart index 2eaff5d5fb..d239588529 100644 --- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart @@ -45,5 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD height: height, width: width, orientation: orientation, + isEdited: false, ); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 482f7f04b2..ad15401bed 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository { } @override - int get schemaVersion => 16; + int get schemaVersion => 17; @override MigrationStrategy get migration => MigrationStrategy( @@ -201,6 +201,9 @@ class Drift extends $Drift implements IDatabaseRepository { await m.createIndex(v16.idxLocalAssetCloudId); await m.createTable(v16.remoteAssetCloudIdEntity); }, + from16To17: (m, v17) async { + await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 10cba0821a..fe7d1d4f0d 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -6911,6 +6911,503 @@ i1.GeneratedColumn _column_100(String aliasedName) => true, type: i1.DriftSqlType.dateTime, ); + +final class Schema17 extends i0.VersionedSchema { + Schema17({required super.database}) : super(version: 17); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 localAssetEntity = Shape26( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape28 extends i0.VersionedTable { + Shape28({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get updatedAt => + columnsByName['updated_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get width => + columnsByName['width']! as i1.GeneratedColumn; + i1.GeneratedColumn get height => + columnsByName['height']! as i1.GeneratedColumn; + i1.GeneratedColumn get durationInSeconds => + columnsByName['duration_in_seconds']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get checksum => + columnsByName['checksum']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => + columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get ownerId => + columnsByName['owner_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get localDateTime => + columnsByName['local_date_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get thumbHash => + columnsByName['thumb_hash']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get livePhotoVideoId => + columnsByName['live_photo_video_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get visibility => + columnsByName['visibility']! as i1.GeneratedColumn; + i1.GeneratedColumn get stackId => + columnsByName['stack_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get libraryId => + columnsByName['library_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get isEdited => + columnsByName['is_edited']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_101(String aliasedName) => + i1.GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -6927,6 +7424,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema14 schema) from13To14, required Future Function(i1.Migrator m, Schema15 schema) from14To15, required Future Function(i1.Migrator m, Schema16 schema) from15To16, + required Future Function(i1.Migrator m, Schema17 schema) from16To17, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -7005,6 +7503,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from15To16(migrator, schema); return 16; + case 16: + final schema = Schema17(database: database); + final migrator = i1.Migrator(database, schema); + await from16To17(migrator, schema); + return 17; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -7027,6 +7530,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema14 schema) from13To14, required Future Function(i1.Migrator m, Schema15 schema) from14To15, required Future Function(i1.Migrator m, Schema16 schema) from15To16, + required Future Function(i1.Migrator m, Schema17 schema) from16To17, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -7044,5 +7548,6 @@ i1.OnUpgrade stepByStep({ from13To14: from13To14, from14To15: from14To15, from15To16: from15To16, + from16To17: from16To17, ), ); diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 95239a469c..c92ce427d5 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -200,6 +200,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { libraryId: Value(asset.libraryId), width: Value(asset.width), height: Value(asset.height), + isEdited: Value(asset.isEdited), ); batch.insert( diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index e625b57c1a..f57ef04b07 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -70,6 +70,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { durationInSeconds: row.durationInSeconds, livePhotoVideoId: row.livePhotoVideoId, stackId: row.stackId, + isEdited: row.isEdited, ) : LocalAsset( id: row.localId!, @@ -88,6 +89,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { latitude: row.latitude, longitude: row.longitude, adjustmentTime: row.adjustmentTime, + isEdited: row.isEdited, ), ) .get(); diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index a1d7e55f32..22bc893cac 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -92,7 +92,7 @@ class _MobileLayout extends StatelessWidget { ], ) .toList(); - return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings]); + return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 60), children: [...settings]); } } @@ -142,7 +142,7 @@ class SettingsSubPage extends StatelessWidget { context.locale; return Scaffold( appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), - body: section.widget, + body: Padding(padding: const EdgeInsets.only(bottom: 60.0), child: section.widget), ); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index ac23e6ddce..bc407f5b79 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:logging/logging.dart'; @@ -49,6 +50,7 @@ class SplashScreenPageState extends ConsumerState { final accessToken = Store.tryGet(StoreKey.accessToken); if (accessToken != null && serverUrl != null && endpoint != null) { + final infoProvider = ref.read(serverInfoProvider.notifier); final wsProvider = ref.read(websocketProvider.notifier); final backgroundManager = ref.read(backgroundSyncProvider); final backupProvider = ref.read(driftBackupProvider.notifier); @@ -58,6 +60,7 @@ class SplashScreenPageState extends ConsumerState { (_) async { try { wsProvider.connect(); + unawaited(infoProvider.getServerInfo()); if (Store.isBetaTimelineEnabled) { bool syncSuccess = false; diff --git a/mobile/lib/pages/settings/sync_status.page.dart b/mobile/lib/pages/settings/sync_status.page.dart index d54ba89e5d..58750e9e30 100644 --- a/mobile/lib/pages/settings/sync_status.page.dart +++ b/mobile/lib/pages/settings/sync_status.page.dart @@ -18,6 +18,7 @@ class SyncStatusPage extends StatelessWidget { splashRadius: 24, icon: const Icon(Icons.arrow_back_ios_rounded), ), + centerTitle: false, ), body: const SyncStatusAndActions(), ); diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 579b4c1d58..9da21c72ee 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -118,6 +118,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection ), _PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()), _PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId), + _PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()), ]); } diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart index d042f52673..10b9ca7ae4 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -167,7 +167,7 @@ class _PlaceTile extends StatelessWidget { child: SizedBox( width: 80, height: 80, - child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover), + child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover, thumbhash: ""), ), ), ); diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index c42f49091f..318e9894f2 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -14,14 +14,15 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/album_filter.utils.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -310,18 +311,17 @@ class _SortButtonState extends ConsumerState<_SortButton> { : const Icon(Icons.abc, color: Colors.transparent), onPressed: () => onMenuTapped(sortMode), style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)), + padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(12, 12, 24, 12)), backgroundColor: WidgetStateProperty.all( albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent, ), shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), ), ), child: Text( sortMode.label.t(context: context), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, + style: context.textTheme.labelLarge?.copyWith( color: albumSortOption == sortMode ? context.colorScheme.onPrimary : context.colorScheme.onSurface.withAlpha(185), @@ -344,15 +344,12 @@ class _SortButtonState extends ConsumerState<_SortButton> { Padding( padding: const EdgeInsets.only(right: 5), child: albumSortIsReverse - ? const Icon(Icons.keyboard_arrow_down) - : const Icon(Icons.keyboard_arrow_up_rounded), + ? Icon(Icons.keyboard_arrow_down, color: context.colorScheme.onSurface) + : Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface), ), Text( albumSortOption.label.t(context: context), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(225), - ), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)), ), isSorting ? SizedBox( @@ -542,7 +539,11 @@ class _QuickSortAndViewMode extends StatelessWidget { initialIsReverse: currentIsReverse, ), IconButton( - icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), + icon: Icon( + isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, + size: 24, + color: context.colorScheme.onSurface, + ), onPressed: onToggleViewMode, ), ], @@ -662,6 +663,8 @@ class _GridAlbumCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); + return GestureDetector( onTap: () => onAlbumSelected(album), child: Card( @@ -680,12 +683,22 @@ class _GridAlbumCard extends ConsumerWidget { borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), child: SizedBox( width: double.infinity, - child: album.thumbnailAssetId != null - ? Thumbnail.remote(remoteId: album.thumbnailAssetId!) - : Container( - color: context.colorScheme.surfaceContainerHighest, - child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey), - ), + child: FutureBuilder( + future: albumThumbnailAsset, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data != null) { + return Thumbnail.remote( + remoteId: album.thumbnailAssetId!, + thumbhash: snapshot.data!.thumbHash ?? "", + ); + } + + return Container( + color: context.colorScheme.surfaceContainerHighest, + child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey), + ); + }, + ), ), ), ), diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 561b018ef8..1aeadf61bc 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -class AlbumTile extends StatelessWidget { +class AlbumTile extends ConsumerWidget { const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected}); final RemoteAlbum album; @@ -14,7 +16,9 @@ class AlbumTile extends StatelessWidget { final Function(RemoteAlbum)? onAlbumSelected; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); + return LargeLeadingTile( title: Text( album.name, @@ -29,23 +33,35 @@ class AlbumTile extends StatelessWidget { ), onTap: () => onAlbumSelected?.call(album), leadingPadding: const EdgeInsets.only(right: 16), - leading: album.thumbnailAssetId != null - ? ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(15)), - child: SizedBox(width: 80, height: 80, child: Thumbnail.remote(remoteId: album.thumbnailAssetId!)), - ) - : SizedBox( - width: 80, - height: 80, - child: Container( - decoration: BoxDecoration( - color: context.colorScheme.surfaceContainer, - borderRadius: const BorderRadius.all(Radius.circular(16)), - border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1), - ), - child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey), - ), - ), + leading: FutureBuilder( + future: albumThumbnailAsset, + builder: (context, snapshot) { + return snapshot.hasData && snapshot.data != null + ? ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(15)), + child: SizedBox( + width: 80, + height: 80, + child: Thumbnail.remote( + remoteId: album.thumbnailAssetId!, + thumbhash: snapshot.data!.thumbHash ?? "", + ), + ), + ) + : SizedBox( + width: 80, + height: 80, + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(16)), + border: Border.all(color: context.colorScheme.outline.withAlpha(50), width: 1), + ), + child: const Icon(Icons.photo_album_rounded, size: 24, color: Colors.grey), + ), + ); + }, + ), ); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index ed3873b510..771d518bba 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; @@ -164,11 +165,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget { children: [ if (albums.isNotEmpty) SheetTile( - title: 'appears_in'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + title: 'appears_in'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), Padding( padding: const EdgeInsets.only(left: 24), @@ -224,9 +222,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { color: context.textTheme.labelLarge?.color, ), subtitle: _getFileInfo(asset, exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ); }, ); @@ -241,9 +237,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { color: context.textTheme.labelLarge?.color, ), subtitle: _getFileInfo(asset, exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ); } } @@ -262,11 +256,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget { const SheetLocationDetails(), // Details header SheetTile( - title: 'details'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + title: 'details'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), // File info buildFileInfoTile(), @@ -278,9 +269,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { titleStyle: context.textTheme.labelLarge, leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color), subtitle: _getCameraInfoSubtitle(exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], // Lens info @@ -291,15 +280,13 @@ class _AssetDetailBottomSheet extends ConsumerWidget { titleStyle: context.textTheme.labelLarge, leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color), subtitle: _getLensInfoSubtitle(exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], // Appears in (Albums) Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)), // padding at the bottom to avoid cut-off - const SizedBox(height: 30), + const SizedBox(height: 60), ], ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart index c0343d03ca..ce561c4016 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -77,11 +78,8 @@ class _SheetLocationDetailsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ SheetTile( - title: 'location'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + title: 'location'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null, onTap: editLocation, ), @@ -105,9 +103,7 @@ class _SheetLocationDetailsState extends ConsumerState { ), Text( coordinates, - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart index 64f22eca92..d62a964401 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; @@ -53,11 +54,8 @@ class _SheetPeopleDetailsState extends ConsumerState { Padding( padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16), child: Text( - "people".t(context: context).toUpperCase(), - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + "people".t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), SizedBox( diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index e77803c206..ad7d53af13 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -112,14 +112,17 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type); } else { final String assetId; + final String thumbhash; if (asset is LocalAsset && asset.hasRemote) { assetId = asset.remoteId!; + thumbhash = ""; } else if (asset is RemoteAsset) { assetId = asset.id; + thumbhash = asset.thumbHash ?? ""; } else { throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); } - provider = RemoteFullImageProvider(assetId: assetId); + provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash); } return provider; @@ -132,8 +135,9 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai } final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; - return assetId != null ? RemoteThumbProvider(assetId: assetId) : null; + final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; + return assetId != null ? RemoteThumbProvider(assetId: assetId, thumbhash: thumbhash) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => - asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); + asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)) && !asset.isEdited; diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index d9a736861f..b550e53c21 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -16,8 +16,9 @@ class RemoteThumbProvider extends CancellableImageProvider with CancellableImageProviderMixin { static final cacheManager = RemoteThumbnailCacheManager(); final String assetId; + final String thumbhash; - RemoteThumbProvider({required this.assetId}); + RemoteThumbProvider({required this.assetId, required this.thumbhash}); @override Future obtainKey(ImageConfiguration configuration) { @@ -38,7 +39,7 @@ class RemoteThumbProvider extends CancellableImageProvider Stream _codec(RemoteThumbProvider key, ImageDecoderCallback decode) { final request = this.request = RemoteImageRequest( - uri: getThumbnailUrlForRemoteId(key.assetId), + uri: getThumbnailUrlForRemoteId(key.assetId, thumbhash: key.thumbhash), headers: ApiService.getRequestHeaders(), cacheManager: cacheManager, ); @@ -49,22 +50,23 @@ class RemoteThumbProvider extends CancellableImageProvider bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteThumbProvider) { - return assetId == other.assetId; + return assetId == other.assetId && thumbhash == other.thumbhash; } return false; } @override - int get hashCode => assetId.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { static final cacheManager = RemoteThumbnailCacheManager(); final String assetId; + final String thumbhash; - RemoteFullImageProvider({required this.assetId}); + RemoteFullImageProvider({required this.assetId, required this.thumbhash}); @override Future obtainKey(ImageConfiguration configuration) { @@ -75,7 +77,7 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -94,7 +96,7 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode; } diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 92b1bb2544..f878c214a9 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -21,9 +21,14 @@ class Thumbnail extends StatefulWidget { const Thumbnail({this.imageProvider, this.fit = BoxFit.cover, this.thumbhashProvider, super.key}); - Thumbnail.remote({required String remoteId, this.fit = BoxFit.cover, Size size = kThumbnailResolution, super.key}) - : imageProvider = RemoteThumbProvider(assetId: remoteId), - thumbhashProvider = null; + Thumbnail.remote({ + required String remoteId, + required String thumbhash, + this.fit = BoxFit.cover, + Size size = kThumbnailResolution, + super.key, + }) : imageProvider = RemoteThumbProvider(assetId: remoteId, thumbhash: thumbhash), + thumbhashProvider = null; Thumbnail.fromAsset({ required BaseAsset? asset, diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index bdaf67ab7e..d6485ae7b6 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; @@ -47,6 +48,7 @@ class _ThumbnailTileState extends ConsumerState { Widget build(BuildContext context) { final asset = widget.asset; final heroIndex = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + final isCurrentAsset = ref.watch(assetViewerProvider.select((current) => current.currentAsset == asset)); final assetContainerColor = context.isDarkTheme ? context.primaryColor.darken(amount: 0.4) @@ -59,6 +61,10 @@ class _ThumbnailTileState extends ConsumerState { final bool storageIndicator = ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator; + if (!isCurrentAsset) { + _hideIndicators = false; + } + if (isSelected) { _showSelectionContainer = true; } @@ -96,7 +102,11 @@ class _ThumbnailTileState extends ConsumerState { children: [ Positioned.fill( child: Hero( - tag: '${asset?.heroTag ?? ''}_$heroIndex', + // This key resets the hero animation when the asset is changed in the asset viewer. + // It doesn't seem like the best solution, and only works to reset the hero, not prime the hero of the new active asset for animation, + // but other solutions have failed thus far. + key: ValueKey(isCurrentAsset), + tag: '${asset?.heroTag}_$heroIndex', child: Thumbnail.fromAsset(asset: asset, size: widget.size), // Placeholderbuilder used to hide indicators on first hero animation, since flightShuttleBuilder isn't called until both source and destination hero exist in widget tree. placeholderBuilder: (context, heroSize, child) { diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index e85a6c05f8..62889b10cb 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -60,7 +60,11 @@ class DriftMemoryCard extends ConsumerWidget { child: SizedBox( width: 205, height: 200, - child: Thumbnail.remote(remoteId: memory.assets[0].id, fit: BoxFit.cover), + child: Thumbnail.remote( + remoteId: memory.assets[0].id, + thumbhash: memory.assets[0].thumbHash ?? "", + fit: BoxFit.cover, + ), ), ), Positioned( diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 604f1c8d0d..a6e8503d99 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -160,7 +160,7 @@ class AppLifeCycleNotifier extends StateNotifier { _resumeBackup(); }), _resumeBackup(), - backgroundManager.syncCloudIds(), + _safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"), ]); } else { await _safeRun(backgroundManager.hashAssets(), "hashAssets"); diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index 75a2a35fb6..1cd5ded487 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -69,6 +69,7 @@ class CastNotifier extends StateNotifier { : AssetType.other, createdAt: asset.fileCreatedAt, updatedAt: asset.updatedAt, + isEdited: false, ); _gCastService.loadMedia(remoteAsset, reload); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 6a1083bfcc..f9473ce440 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -144,6 +144,7 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_asset_hidden', _handleOnAssetHidden); } else { socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); } socket.on('on_config_update', _handleOnConfigUpdate); @@ -192,10 +193,12 @@ class WebsocketNotifier extends StateNotifier { void stopListeningToBetaEvents() { state.socket?.off('AssetUploadReadyV1'); + state.socket?.off('AssetEditReadyV1'); } void startListeningToBetaEvents() { state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); } void listenUploadEvent() { @@ -315,6 +318,10 @@ class WebsocketNotifier extends StateNotifier { _batchDebouncer.run(_processBatchedAssetUploadReady); } + void _handleSyncAssetEditReady(dynamic data) { + unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditBatch([data])); + } + void _processBatchedAssetUploadReady() { if (_batchedAssetUploadReady.isEmpty) { return; diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index 654be78fb4..3a3e50f370 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -25,6 +25,7 @@ class FileMediaRepository { type: AssetType.image, createdAt: entity.createDateTime, updatedAt: entity.modifiedDateTime, + isEdited: false, ); } diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index fe1e4ac13b..90c8d7f7d4 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -307,10 +307,10 @@ class BackgroundUploadService { priority: priority, isFavorite: asset.isFavorite, requiresWiFi: requiresWiFi, - cloudId: asset.cloudId, - adjustmentTime: asset.adjustmentTime?.toIso8601String(), - latitude: asset.latitude?.toString(), - longitude: asset.longitude?.toString(), + cloudId: entity.isLivePhoto ? null : asset.cloudId, + adjustmentTime: entity.isLivePhoto ? null : asset.adjustmentTime?.toIso8601String(), + latitude: entity.isLivePhoto ? null : asset.latitude?.toString(), + longitude: entity.isLivePhoto ? null : asset.longitude?.toString(), ); } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index b979096e1c..9cd562b5db 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -40,6 +41,7 @@ final foregroundUploadServiceProvider = Provider((ref) { ref.watch(backupRepositoryProvider), ref.watch(connectivityApiProvider), ref.watch(appSettingsServiceProvider), + ref.watch(assetMediaRepositoryProvider), ); }); @@ -55,6 +57,7 @@ class ForegroundUploadService { this._backupRepository, this._connectivityApi, this._appSettingsService, + this._assetMediaRepository, ); final UploadRepository _uploadRepository; @@ -62,6 +65,7 @@ class ForegroundUploadService { final DriftBackupRepository _backupRepository; final ConnectivityApi _connectivityApi; final AppSettingsService _appSettingsService; + final AssetMediaRepository _assetMediaRepository; final Logger _logger = Logger('ForegroundUploadService'); bool shouldAbortUpload = false; @@ -311,7 +315,8 @@ class ForegroundUploadService { return; } - final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name; + final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName; final deviceId = Store.get(StoreKey.deviceId); final headers = ApiService.getRequestHeaders(); @@ -322,19 +327,6 @@ class ForegroundUploadService { 'fileModifiedAt': asset.updatedAt.toUtc().toIso8601String(), 'isFavorite': asset.isFavorite.toString(), 'duration': asset.duration.toString(), - if (CurrentPlatform.isIOS && asset.cloudId != null) - 'metadata': jsonEncode([ - RemoteAssetMetadataItem( - key: RemoteAssetMetadataKey.mobileApp, - value: RemoteAssetMobileAppMetadata( - cloudId: asset.cloudId, - createdAt: asset.createdAt.toIso8601String(), - adjustmentTime: asset.adjustmentTime?.toIso8601String(), - latitude: asset.latitude?.toString(), - longitude: asset.longitude?.toString(), - ), - ), - ]), }; // Upload live photo video first if available @@ -363,6 +355,22 @@ class ForegroundUploadService { fields['livePhotoVideoId'] = livePhotoVideoId; } + // Add cloudId metadata only to the still image, not the motion video, becasue when the sync id happens, the motion video can get associated with the wrong still image. + if (CurrentPlatform.isIOS && asset.cloudId != null) { + fields['metadata'] = jsonEncode([ + RemoteAssetMetadataItem( + key: RemoteAssetMetadataKey.mobileApp, + value: RemoteAssetMobileAppMetadata( + cloudId: asset.cloudId, + createdAt: asset.createdAt.toIso8601String(), + adjustmentTime: asset.adjustmentTime?.toIso8601String(), + latitude: asset.latitude?.toString(), + longitude: asset.longitude?.toString(), + ), + ), + ]); + } + final result = await _uploadRepository.uploadFile( file: file, originalFileName: originalFileName, diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index 2a6ca3a8da..a633a04d7f 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -61,7 +61,12 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale ), ), chipTheme: const ChipThemeData(side: BorderSide.none), - sliderTheme: const SliderThemeData(thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), trackHeight: 2.0), + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, + // ignore: deprecated_member_use + year2023: false, + ), bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed), popupMenuTheme: const PopupMenuThemeData( shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 4059f5baa2..079f0e51fa 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -50,8 +50,10 @@ String getThumbnailUrlForRemoteId( final String id, { AssetMediaSize type = AssetMediaSize.thumbnail, bool edited = true, + String? thumbhash, }) { - return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited'; + final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited'; + return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url; } String getPlaybackUrlForRemoteId(final String id) { diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 0c1f03086f..090889ff32 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) { if (value is Map) { addDefault(value, 'visibility', 'timeline'); addDefault(value, 'createdAt', DateTime.now().toIso8601String()); + addDefault(value, 'isEdited', false); } break; case 'UserAdminResponseDto': @@ -46,6 +47,10 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); addDefault(value, 'hasProfileImage', false); } + case 'SyncAssetV1': + if (value is Map) { + addDefault(value, 'isEdited', false); + } case 'ServerFeaturesDto': if (value is Map) { addDefault(value, 'ocr', false); diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index d95d7c7483..3fa443121a 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -33,7 +33,7 @@ class PersonNameEditForm extends HookConsumerWidget { decoration: InputDecoration( hintText: 'name'.tr(), border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occured' : null, + errorText: isError.value ? 'Error occurred' : null, ), ), ), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 04786bf916..08e66df48d 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -1,14 +1,14 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; class GroupSettings extends HookConsumerWidget { const GroupSettings({super.key}); @@ -33,12 +33,24 @@ class GroupSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "asset_list_group_by_sub_title".tr()), + SettingGroupTitle( + title: "asset_list_group_by_sub_title".t(context: context), + icon: Icons.group_work_outlined, + ), SettingsRadioListTile( groups: [ - SettingsRadioGroup(title: 'asset_list_layout_settings_group_by_month_day'.tr(), value: GroupAssetsBy.day), - SettingsRadioGroup(title: 'month'.tr(), value: GroupAssetsBy.month), - SettingsRadioGroup(title: 'asset_list_layout_settings_group_automatically'.tr(), value: GroupAssetsBy.auto), + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_by_month_day'.t(context: context), + value: GroupAssetsBy.day, + ), + SettingsRadioGroup( + title: 'month'.t(context: context), + value: GroupAssetsBy.month, + ), + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_automatically'.t(context: context), + value: GroupAssetsBy.auto, + ), ], groupBy: groupBy, onRadioChanged: changeGroupValue, diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index bcb4a5ec9c..5d82630fc6 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,11 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { @@ -19,10 +20,13 @@ class LayoutSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "asset_list_layout_sub_title".tr()), + SettingGroupTitle( + title: "asset_list_layout_sub_title".t(context: context), + icon: Icons.view_module_outlined, + ), SettingsSwitchListTile( valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".tr(), + title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSliderListTile( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index aed88b90b0..e437b82dd4 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -1,10 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -19,21 +18,21 @@ class ImageViewerQualitySetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "setting_image_viewer_title".tr()), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - title: Text('setting_image_viewer_help', style: context.textTheme.bodyMedium).tr(), + SettingGroupTitle( + title: "photos".t(context: context), + icon: Icons.image_outlined, + subtitle: "setting_image_viewer_help".t(context: context), ), SettingsSwitchListTile( valueNotifier: isPreview, - title: "setting_image_viewer_preview_title".tr(), - subtitle: "setting_image_viewer_preview_subtitle".tr(), + title: "setting_image_viewer_preview_title".t(context: context), + subtitle: "setting_image_viewer_preview_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: isOriginal, - title: "setting_image_viewer_original_title".tr(), - subtitle: "setting_image_viewer_original_subtitle".tr(), + title: "setting_image_viewer_original_title".t(context: context), + subtitle: "setting_image_viewer_original_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 9a89b7e1e3..c03dcc51b4 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -1,9 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -19,23 +19,26 @@ class VideoViewerSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "videos".tr()), + SettingGroupTitle( + title: "videos".t(context: context), + icon: Icons.video_camera_back_outlined, + ), SettingsSwitchListTile( valueNotifier: useAutoPlayVideo, - title: "setting_video_viewer_auto_play_title".tr(), - subtitle: "setting_video_viewer_auto_play_subtitle".tr(), + title: "setting_video_viewer_auto_play_title".t(context: context), + subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: useLoopVideo, - title: "setting_video_viewer_looping_title".tr(), - subtitle: "loop_videos_description".tr(), + title: "setting_video_viewer_looping_title".t(context: context), + subtitle: "loop_videos_description".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: useOriginalVideo, - title: "setting_video_viewer_original_video_title".tr(), - subtitle: "setting_video_viewer_original_video_subtitle".tr(), + title: "setting_video_viewer_original_video_title".t(context: context), + subtitle: "setting_video_viewer_original_video_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 743d38fc48..b44971a135 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; class DriftBackupSettings extends ConsumerWidget { @@ -25,36 +27,25 @@ class DriftBackupSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return SettingsSubPageScaffold( settings: [ - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - "network_requirements".t(context: context).toUpperCase(), - style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)), - ), + SettingGroupTitle( + title: "network_requirements".t(context: context), + icon: Icons.cell_tower, ), const _UseWifiForUploadVideosButton(), const _UseWifiForUploadPhotosButton(), if (CurrentPlatform.isAndroid) ...[ const Divider(), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - "background_options".t(context: context).toUpperCase(), - style: context.textTheme.labelSmall?.copyWith( - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), + SettingGroupTitle( + title: "background_options".t(context: context), + icon: Icons.charging_station_rounded, ), const _BackupOnlyWhenChargingButton(), const _BackupDelaySlider(), ], const Divider(), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - "backup_albums_sync".t(context: context).toUpperCase(), - style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)), - ), + SettingGroupTitle( + title: "backup_albums_sync".t(context: context), + icon: Icons.sync, ), const _AlbumSyncActionButton(), ], @@ -105,81 +96,67 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> @override Widget build(BuildContext context) { - return ListView( - shrinkWrap: true, - children: [ - StreamBuilder( - stream: Store.watch(StoreKey.syncAlbums), - initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, - builder: (context, snapshot) { - final albumSyncEnable = snapshot.data ?? false; - return Column( - children: [ - ListTile( - title: Text( - "sync_albums".t(context: context), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ), - subtitle: Text( - "sync_upload_album_setting_subtitle".t(context: context), - style: context.textTheme.labelLarge, - ), - trailing: Switch( - value: albumSyncEnable, - onChanged: (bool newValue) async { - await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue); + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ListView( + shrinkWrap: true, + children: [ + StreamBuilder( + stream: Store.watch(StoreKey.syncAlbums), + initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, + builder: (context, snapshot) { + final albumSyncEnable = snapshot.data ?? false; + return Column( + children: [ + SettingListTile( + title: "sync_albums".t(context: context), + subtitle: "sync_upload_album_setting_subtitle".t(context: context), + trailing: Switch( + value: albumSyncEnable, + onChanged: (bool newValue) async { + await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue); - if (newValue == true) { - await _manageLinkedAlbums(); - } - }, + if (newValue == true) { + await _manageLinkedAlbums(); + } + }, + ), ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: albumSyncEnable ? 1.0 : 0.0, - child: albumSyncEnable - ? ListTile( - onTap: _manualSyncAlbums, - contentPadding: const EdgeInsets.only(left: 32, right: 16), - title: Text( - "organize_into_albums".t(context: context), - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.normal, - ), - ), - subtitle: Text( - "organize_into_albums_description".t(context: context), - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - trailing: isAlbumSyncInProgress - ? const SizedBox( - width: 32, - height: 32, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ) - : IconButton( - onPressed: _manualSyncAlbums, - icon: const Icon(Icons.sync_rounded), - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - iconSize: 20, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - ), - ) - : const SizedBox.shrink(), + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: albumSyncEnable ? 1.0 : 0.0, + child: albumSyncEnable + ? SettingListTile( + onTap: _manualSyncAlbums, + contentPadding: const EdgeInsets.only(left: 32, right: 16), + title: "organize_into_albums".t(context: context), + subtitle: "organize_into_albums_description".t(context: context), + trailing: isAlbumSyncInProgress + ? const SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ) + : IconButton( + onPressed: _manualSyncAlbums, + icon: const Icon(Icons.sync_rounded), + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + iconSize: 20, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + ) + : const SizedBox.shrink(), + ), ), - ), - ], - ); - }, - ), - ], + ], + ); + }, + ), + ], + ), ); } } @@ -222,24 +199,24 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> { @override Widget build(BuildContext context) { - return ListTile( - title: Text( - widget.titleKey.t(context: context), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ), - subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge), - trailing: StreamBuilder( - stream: valueStream, - initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, - builder: (context, snapshot) { - final value = snapshot.data ?? false; - return Switch( - value: value, - onChanged: (bool newValue) async { - await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue); - }, - ); - }, + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: SettingListTile( + title: widget.titleKey.t(context: context), + subtitle: widget.subtitleKey.t(context: context), + trailing: StreamBuilder( + stream: valueStream, + initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, + builder: (context, snapshot) { + final value = snapshot.data ?? false; + return Switch( + value: value, + onChanged: (bool newValue) async { + await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue); + }, + ); + }, + ), ), ); } @@ -354,7 +331,7 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> { 'backup_controller_page_background_delay'.tr( namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)}, ), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), ), ), Slider( diff --git a/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart b/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart index 6120524d5b..be28162b98 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart @@ -34,33 +34,36 @@ class EntityCountTile extends StatelessWidget { children: [ // Icon and Label Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, color: context.primaryColor), - const SizedBox(width: 8), + Icon(icon, color: context.primaryColor, size: 14), + const SizedBox(width: 4), Flexible( child: Text( label, - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16), + style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w500), ), ), ], ), // Number const Spacer(), - RichText( - text: TextSpan( - style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600), - children: [ - TextSpan( - text: zeroPadding(count, maxDigits), - style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)), - ), - TextSpan( - text: count.toString(), - style: TextStyle(color: context.primaryColor), - ), - ], + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: RichText( + text: TextSpan( + style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode'), + children: [ + TextSpan( + text: zeroPadding(count, maxDigits), + style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)), + ), + TextSpan( + text: count.toString(), + style: TextStyle(color: context.colorScheme.onSurface), + ), + ], + ), ), ), ], diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 1f3fa14c62..ada2aacd66 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart' import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -112,48 +114,39 @@ class SyncStatusAndActions extends HookConsumerWidget { padding: const EdgeInsets.only(top: 16, bottom: 96), children: [ const _SyncStatsCounts(), - const Divider(height: 1, indent: 16, endIndent: 16), - const SizedBox(height: 24), - _SectionHeaderText(text: "jobs".t(context: context)), - ListTile( - title: Text( - "sync_local".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("tap_to_run_job".t(context: context)), + const Divider(height: 10), + const SizedBox(height: 16), + SettingGroupTitle(title: "jobs".t(context: context)), + SettingListTile( + title: "sync_local".t(context: context), + subtitle: "tap_to_run_job".t(context: context), leading: const Icon(Icons.sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), onTap: () { ref.read(backgroundSyncProvider).syncLocal(full: true); }, ), - ListTile( - title: Text( - "sync_remote".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("tap_to_run_job".t(context: context)), + SettingListTile( + title: "sync_remote".t(context: context), + subtitle: "tap_to_run_job".t(context: context), leading: const Icon(Icons.cloud_sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), onTap: () { ref.read(backgroundSyncProvider).syncRemote(); }, ), - ListTile( - title: Text( - "hash_asset".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), + SettingListTile( + title: "hash_asset".t(context: context), leading: const Icon(Icons.tag), - subtitle: Text("tap_to_run_job".t(context: context)), + subtitle: "tap_to_run_job".t(context: context), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), onTap: () { ref.read(backgroundSyncProvider).hashAssets(); }, ), - const Divider(height: 1, indent: 16, endIndent: 16), - const SizedBox(height: 24), - _SectionHeaderText(text: "actions".t(context: context)), + const Divider(height: 1), + const SizedBox(height: 16), + SettingGroupTitle(title: "actions".t(context: context)), ListTile( title: Text( "clear_file_cache".t(context: context), @@ -202,26 +195,6 @@ class _SyncStatusIcon extends StatelessWidget { } } -class _SectionHeaderText extends StatelessWidget { - final String text; - - const _SectionHeaderText({required this.text}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - text.toUpperCase(), - style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(200), - ), - ), - ); - } -} - class _SyncStatsCounts extends ConsumerWidget { const _SyncStatsCounts(); @@ -279,9 +252,9 @@ class _SyncStatsCounts extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SectionHeaderText(text: "assets".t(context: context)), + SettingGroupTitle(title: "assets".t(context: context)), Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), // 1. Wrap in IntrinsicHeight child: IntrinsicHeight( child: Flex( @@ -309,9 +282,9 @@ class _SyncStatsCounts extends ConsumerWidget { ), ), ), - _SectionHeaderText(text: "albums".t(context: context)), + SettingGroupTitle(title: "albums".t(context: context)), Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( child: Flex( direction: Axis.horizontal, @@ -337,9 +310,9 @@ class _SyncStatsCounts extends ConsumerWidget { ), ), ), - _SectionHeaderText(text: "other".t(context: context)), + SettingGroupTitle(title: "other".t(context: context)), Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( child: Flex( direction: Axis.horizontal, @@ -368,7 +341,7 @@ class _SyncStatsCounts extends ConsumerWidget { // To be removed once the experimental feature is stable if (CurrentPlatform.isAndroid && appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) ...[ - _SectionHeaderText(text: "trash".t(context: context)), + SettingGroupTitle(title: "trash".t(context: context)), Consumer( builder: (context, ref, _) { final counts = ref.watch(trashedAssetsCountProvider); diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart index 480665e614..21e0edb34c 100644 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; class BetaTimelineListTile extends ConsumerWidget { const BetaTimelineListTile({super.key}); @@ -56,8 +57,8 @@ class BetaTimelineListTile extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 4.0), - child: ListTile( - title: Text("new_timeline".t(context: context)), + child: SettingListTile( + title: "new_timeline".t(context: context), trailing: Switch.adaptive( value: betaTimelineValue, onChanged: onSwitchChanged, diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 7acb04686b..e24a4d481a 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -142,7 +142,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState { final state = ref.watch(cleanupProvider); final hasDate = state.selectedDate != null; final hasAssets = _hasScanned && state.assetsToDelete.isNotEmpty; - + final subtitleStyle = context.textTheme.bodyMedium!.copyWith( + color: context.textTheme.bodyMedium!.color!.withAlpha(215), + ); StepStyle styleForState(StepState stepState, {bool isDestructive = false}) { switch (stepState) { case StepState.complete: @@ -214,10 +216,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { borderRadius: const BorderRadius.all(Radius.circular(12)), border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)), ), - child: Text( - 'free_up_space_description'.t(context: context), - style: context.textTheme.labelLarge?.copyWith(fontSize: 15), - ), + child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium), ), ), @@ -256,7 +255,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('cutoff_date_description'.t(context: context), style: context.textTheme.labelLarge), + Text('cutoff_date_description'.t(context: context), style: subtitleStyle), const SizedBox(height: 16), GridView.count( shrinkWrap: true, @@ -352,7 +351,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('cleanup_filter_description'.t(context: context), style: context.textTheme.labelLarge), + Text('cleanup_filter_description'.t(context: context), style: subtitleStyle), const SizedBox(height: 16), SegmentedButton( segments: [ @@ -381,10 +380,15 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const SizedBox(height: 16), SwitchListTile( contentPadding: EdgeInsets.zero, - title: Text('keep_favorites'.t(context: context), style: context.textTheme.titleSmall), + title: Text( + 'keep_favorites'.t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), + ), subtitle: Text( 'keep_favorites_description'.t(context: context), - style: context.textTheme.labelLarge, + style: context.textTheme.bodyMedium!.copyWith( + color: context.textTheme.bodyMedium!.color!.withAlpha(215), + ), ), value: state.keepFavorites, onChanged: (value) { @@ -435,10 +439,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : null, content: Column( children: [ - Text( - 'cleanup_step3_description'.t(context: context), - style: context.textTheme.labelLarge?.copyWith(fontSize: 15), - ), + Text('cleanup_step3_description'.t(context: context), style: subtitleStyle), if (CurrentPlatform.isIOS) ...[ const SizedBox(height: 12), Container( diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index 47e85fd7cc..735971e0c2 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState { autovalidateMode: AutovalidateMode.onUserInteraction, validator: validateUrl, keyboardType: TextInputType.url, - style: const TextStyle(fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600, fontSize: 14), + style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 14), decoration: InputDecoration( hintText: 'http(s)://immich.domain.com', contentPadding: const EdgeInsets.all(16), diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 8cc6079961..da5ecab684 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -1,12 +1,12 @@ import 'dart:convert'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart'; @@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), - child: Text("external_network_sheet_info".tr(), style: context.textTheme.bodyMedium), + child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium), ), const SizedBox(height: 4), Divider(color: context.colorScheme.surfaceContainerHighest), @@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.add), - label: Text('add_endpoint'.tr().toUpperCase()), + label: Text('add_endpoint'.t(context: context)), onPressed: enabled ? () { entries.value = [ diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index 3f47233eec..c89c8e149e 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; @@ -167,13 +168,12 @@ class LocalNetworkPreference extends HookConsumerWidget { enabled: enabled, contentPadding: const EdgeInsets.only(left: 24, right: 8), leading: const Icon(Icons.lan_rounded), - title: Text("server_endpoint".tr()), + title: Text("server_endpoint".t(context: context)), subtitle: localEndpointText.value.isEmpty ? const Text("http://local-ip:2283") : Text( localEndpointText.value, style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), fontFamily: 'GoogleSansCode', ), @@ -190,7 +190,7 @@ class LocalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.wifi_find_rounded), - label: Text('use_current_connection'.tr().toUpperCase()), + label: Text('use_current_connection'.t(context: context)), onPressed: enabled ? autofillCurrentNetwork : null, ), ), diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index 4acf80af84..981bec2c0c 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -10,6 +11,7 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class NetworkingSettings extends HookConsumerWidget { @@ -87,12 +89,10 @@ class NetworkingSettings extends HookConsumerWidget { return ListView( padding: const EdgeInsets.only(bottom: 96), children: [ - Padding( - padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8), - child: NetworkPreferenceTitle( - title: "current_server_address".tr().toUpperCase(), - icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, - ), + const SizedBox(height: 8), + SettingGroupTitle( + title: "current_server_address".t(context: context), + icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -108,12 +108,7 @@ class NetworkingSettings extends HookConsumerWidget { : const Icon(Icons.circle_outlined), title: Text( currentEndpoint ?? "--", - style: TextStyle( - fontSize: 16, - fontFamily: 'GoogleSansCode', - fontWeight: FontWeight.bold, - color: context.primaryColor, - ), + style: TextStyle(fontSize: 14, fontFamily: 'GoogleSansCode', color: context.primaryColor), ), ), ), @@ -128,14 +123,16 @@ class NetworkingSettings extends HookConsumerWidget { title: "automatic_endpoint_switching_title".tr(), subtitle: "automatic_endpoint_switching_subtitle".tr(), ), - Padding( - padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16), - child: NetworkPreferenceTitle(title: "local_network".tr().toUpperCase(), icon: Icons.home_outlined), + const SizedBox(height: 8), + SettingGroupTitle( + title: "local_network".t(context: context), + icon: Icons.home_outlined, ), LocalNetworkPreference(enabled: featureEnabled.value), - Padding( - padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16), - child: NetworkPreferenceTitle(title: "external_network".tr().toUpperCase(), icon: Icons.dns_outlined), + const SizedBox(height: 16), + SettingGroupTitle( + title: "external_network".t(context: context), + icon: Icons.dns_outlined, ), ExternalNetworkPreference(enabled: featureEnabled.value), ], @@ -143,30 +140,6 @@ class NetworkingSettings extends HookConsumerWidget { } } -class NetworkPreferenceTitle extends StatelessWidget { - const NetworkPreferenceTitle({super.key, required this.icon, required this.title}); - - final IconData icon; - final String title; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Icon(icon, color: context.colorScheme.onSurface.withAlpha(150)), - const SizedBox(width: 8), - Text( - title, - style: context.textTheme.displaySmall?.copyWith( - color: context.colorScheme.onSurface.withAlpha(200), - fontWeight: FontWeight.w500, - ), - ), - ], - ); - } -} - class NetworkStatusIcon extends StatelessWidget { const NetworkStatusIcon({super.key, required this.status, this.enabled = true}) : super(); @@ -175,10 +148,10 @@ class NetworkStatusIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: _buildIcon(context)); + return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: buildIcon(context)); } - Widget _buildIcon(BuildContext context) => switch (status) { + Widget buildIcon(BuildContext context) => switch (status) { AuxCheckStatus.loading => Padding( padding: const EdgeInsets.only(left: 4.0), child: SizedBox( diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index 49f57a5e94..5e745dd61d 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -1,9 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -22,10 +22,13 @@ class HapticSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "haptic_feedback_title".tr()), + SettingGroupTitle( + title: "haptic_feedback_title".t(context: context), + icon: Icons.vibration_outlined, + ), SettingsSwitchListTile( valueNotifier: isHapticFeedbackEnabled, - title: 'haptic_feedback_switch'.tr(), + title: 'enabled'.t(context: context), onChanged: onHapticFeedbackChange, ), ], diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 123f7c9921..fc20fb7bed 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -1,12 +1,12 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -74,23 +74,26 @@ class ThemeSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "theme".tr()), + SettingGroupTitle( + title: "theme".t(context: context), + icon: Icons.color_lens_outlined, + ), SettingsSwitchListTile( valueNotifier: isSystemTheme, - title: 'theme_setting_system_theme_switch'.tr(), + title: 'theme_setting_system_theme_switch'.t(context: context), onChanged: onSystemThemeChange, ), if (currentTheme.value != ThemeMode.system) SettingsSwitchListTile( valueNotifier: isDarkTheme, - title: 'map_settings_dark_mode'.tr(), + title: 'map_settings_dark_mode'.t(context: context), onChanged: onThemeChange, ), const PrimaryColorSetting(), SettingsSwitchListTile( valueNotifier: applyThemeToBackgroundProvider, - title: "theme_setting_colorful_interface_title".tr(), - subtitle: 'theme_setting_colorful_interface_subtitle'.tr(), + title: "theme_setting_colorful_interface_title".t(context: context), + subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), onChanged: onSurfaceColorSettingChange, ), ], diff --git a/mobile/lib/widgets/settings/setting_group_title.dart b/mobile/lib/widgets/settings/setting_group_title.dart new file mode 100644 index 0000000000..48b1a9bfba --- /dev/null +++ b/mobile/lib/widgets/settings/setting_group_title.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; + +class SettingGroupTitle extends StatelessWidget { + final String title; + final String? subtitle; + final IconData? icon; + final EdgeInsetsGeometry? contentPadding; + + const SettingGroupTitle({super.key, required this.title, this.icon, this.subtitle, this.contentPadding}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: contentPadding ?? const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 8.0), + child: Column( + children: [ + Row( + children: [ + if (icon != null) ...[ + Icon(icon, color: context.colorScheme.onSurfaceSecondary, size: 20), + const SizedBox(width: 8), + ], + Text(title, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary)), + ], + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + ), + ], + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/setting_list_tile.dart b/mobile/lib/widgets/settings/setting_list_tile.dart new file mode 100644 index 0000000000..17f44f8a85 --- /dev/null +++ b/mobile/lib/widgets/settings/setting_list_tile.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingListTile extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? leading; + final Widget? trailing; + final VoidCallback? onTap; + final EdgeInsetsGeometry? contentPadding; + + const SettingListTile({ + required this.title, + this.subtitle, + this.leading, + this.trailing, + this.onTap, + this.contentPadding, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5)), + subtitle: subtitle != null + ? Text( + subtitle!, + style: context.textTheme.bodyMedium!.copyWith(color: context.textTheme.bodyMedium!.color!.withAlpha(215)), + ) + : null, + leading: leading, + trailing: trailing, + onTap: onTap, + contentPadding: contentPadding, + ); + } +} diff --git a/mobile/lib/widgets/settings/settings_card.dart b/mobile/lib/widgets/settings/settings_card.dart index 36eff7bae1..b5dcaac1ca 100644 --- a/mobile/lib/widgets/settings/settings_card.dart +++ b/mobile/lib/widgets/settings/settings_card.dart @@ -36,11 +36,8 @@ class SettingsCard extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: Icon(icon, color: context.primaryColor), ), - title: Text( - title, - style: context.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ), - subtitle: Text(subtitle, style: context.textTheme.labelLarge), + title: Text(title, style: context.textTheme.titleMedium!.copyWith(color: context.primaryColor)), + subtitle: Text(subtitle, style: context.textTheme.bodyMedium), onTap: () => context.pushRoute(settingRoute), ), ), diff --git a/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart b/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart index b4cb67239e..78f483f0a9 100644 --- a/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart +++ b/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart @@ -9,13 +9,11 @@ class SettingsSubPageScaffold extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.symmetric(vertical: 16), itemCount: settings.length, itemBuilder: (ctx, index) => settings[index], separatorBuilder: (context, index) => showDivider - ? const Column( - children: [SizedBox(height: 5), Divider(height: 10, indent: 15, endIndent: 15), SizedBox(height: 15)], - ) + ? const Column(children: [SizedBox(height: 5), Divider(height: 10), SizedBox(height: 15)]) : const SizedBox(height: 10), ); } diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart new file mode 100644 index 0000000000..fbd485f86f --- /dev/null +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -0,0 +1,269 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class DatabaseBackupsAdminApi { + DatabaseBackupsAdminApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Delete database backup + /// + /// Delete a backup by its filename + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): + Future deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups'; + + // ignore: prefer_final_locals + Object? postBody = databaseBackupDeleteDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Delete database backup + /// + /// Delete a backup by its filename + /// + /// Parameters: + /// + /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): + Future deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { + final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Download database backup + /// + /// Downloads the database backup file + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] filename (required): + Future downloadDatabaseBackupWithHttpInfo(String filename,) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups/{filename}' + .replaceAll('{filename}', filename); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Download database backup + /// + /// Downloads the database backup file + /// + /// Parameters: + /// + /// * [String] filename (required): + Future downloadDatabaseBackup(String filename,) async { + final response = await downloadDatabaseBackupWithHttpInfo(filename,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile; + + } + return null; + } + + /// List database backups + /// + /// Get the list of the successful and failed backups + /// + /// Note: This method returns the HTTP [Response]. + Future listDatabaseBackupsWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// List database backups + /// + /// Get the list of the successful and failed backups + Future listDatabaseBackups() async { + final response = await listDatabaseBackupsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'DatabaseBackupListResponseDto',) as DatabaseBackupListResponseDto; + + } + return null; + } + + /// Start database backup restore flow + /// + /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) + /// + /// Note: This method returns the HTTP [Response]. + Future startDatabaseRestoreFlowWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups/start-restore'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Start database backup restore flow + /// + /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) + Future startDatabaseRestoreFlow() async { + final response = await startDatabaseRestoreFlowWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Upload database backup + /// + /// Uploads .sql/.sql.gz file to restore backup from + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [MultipartFile] file: + Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups/upload'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['multipart/form-data']; + + bool hasFields = false; + final mp = MultipartRequest('POST', Uri.parse(apiPath)); + if (file != null) { + hasFields = true; + mp.fields[r'file'] = file.field; + mp.files.add(file); + } + if (hasFields) { + postBody = mp; + } + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Upload database backup + /// + /// Uploads .sql/.sql.gz file to restore backup from + /// + /// Parameters: + /// + /// * [MultipartFile] file: + Future uploadDatabaseBackup({ MultipartFile? file, }) async { + final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index c9581b19dd..27aa3b98f3 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -26,6 +26,7 @@ class AssetResponseDto { required this.height, required this.id, required this.isArchived, + required this.isEdited, required this.isFavorite, required this.isOffline, required this.isTrashed, @@ -85,6 +86,8 @@ class AssetResponseDto { bool isArchived; + bool isEdited; + bool isFavorite; bool isOffline; @@ -162,6 +165,7 @@ class AssetResponseDto { other.height == height && other.id == id && other.isArchived == isArchived && + other.isEdited == isEdited && other.isFavorite == isFavorite && other.isOffline == isOffline && other.isTrashed == isTrashed && @@ -200,6 +204,7 @@ class AssetResponseDto { (height == null ? 0 : height!.hashCode) + (id.hashCode) + (isArchived.hashCode) + + (isEdited.hashCode) + (isFavorite.hashCode) + (isOffline.hashCode) + (isTrashed.hashCode) + @@ -223,7 +228,7 @@ class AssetResponseDto { (width == null ? 0 : width!.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; + String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; @@ -252,6 +257,7 @@ class AssetResponseDto { } json[r'id'] = this.id; json[r'isArchived'] = this.isArchived; + json[r'isEdited'] = this.isEdited; json[r'isFavorite'] = this.isFavorite; json[r'isOffline'] = this.isOffline; json[r'isTrashed'] = this.isTrashed; @@ -332,6 +338,7 @@ class AssetResponseDto { : num.parse('${json[r'height']}'), id: mapValueOfType(json, r'id')!, isArchived: mapValueOfType(json, r'isArchived')!, + isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, isOffline: mapValueOfType(json, r'isOffline')!, isTrashed: mapValueOfType(json, r'isTrashed')!, @@ -413,6 +420,7 @@ class AssetResponseDto { 'height', 'id', 'isArchived', + 'isEdited', 'isFavorite', 'isOffline', 'isTrashed', diff --git a/mobile/openapi/lib/model/database_backup_delete_dto.dart b/mobile/openapi/lib/model/database_backup_delete_dto.dart new file mode 100644 index 0000000000..8bc33a81dc --- /dev/null +++ b/mobile/openapi/lib/model/database_backup_delete_dto.dart @@ -0,0 +1,101 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class DatabaseBackupDeleteDto { + /// Returns a new [DatabaseBackupDeleteDto] instance. + DatabaseBackupDeleteDto({ + this.backups = const [], + }); + + List backups; + + @override + bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDeleteDto && + _deepEquality.equals(other.backups, backups); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (backups.hashCode); + + @override + String toString() => 'DatabaseBackupDeleteDto[backups=$backups]'; + + Map toJson() { + final json = {}; + json[r'backups'] = this.backups; + return json; + } + + /// Returns a new [DatabaseBackupDeleteDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DatabaseBackupDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "DatabaseBackupDeleteDto"); + if (value is Map) { + final json = value.cast(); + + return DatabaseBackupDeleteDto( + backups: json[r'backups'] is Iterable + ? (json[r'backups'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = DatabaseBackupDeleteDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = DatabaseBackupDeleteDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DatabaseBackupDeleteDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = DatabaseBackupDeleteDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'backups', + }; +} + diff --git a/mobile/openapi/lib/model/database_backup_dto.dart b/mobile/openapi/lib/model/database_backup_dto.dart new file mode 100644 index 0000000000..4bf231587b --- /dev/null +++ b/mobile/openapi/lib/model/database_backup_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class DatabaseBackupDto { + /// Returns a new [DatabaseBackupDto] instance. + DatabaseBackupDto({ + required this.filename, + required this.filesize, + }); + + String filename; + + num filesize; + + @override + bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDto && + other.filename == filename && + other.filesize == filesize; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (filename.hashCode) + + (filesize.hashCode); + + @override + String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize]'; + + Map toJson() { + final json = {}; + json[r'filename'] = this.filename; + json[r'filesize'] = this.filesize; + return json; + } + + /// Returns a new [DatabaseBackupDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DatabaseBackupDto? fromJson(dynamic value) { + upgradeDto(value, "DatabaseBackupDto"); + if (value is Map) { + final json = value.cast(); + + return DatabaseBackupDto( + filename: mapValueOfType(json, r'filename')!, + filesize: num.parse('${json[r'filesize']}'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = DatabaseBackupDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = DatabaseBackupDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DatabaseBackupDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = DatabaseBackupDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'filename', + 'filesize', + }; +} + diff --git a/mobile/openapi/lib/model/database_backup_list_response_dto.dart b/mobile/openapi/lib/model/database_backup_list_response_dto.dart new file mode 100644 index 0000000000..16985dd605 --- /dev/null +++ b/mobile/openapi/lib/model/database_backup_list_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class DatabaseBackupListResponseDto { + /// Returns a new [DatabaseBackupListResponseDto] instance. + DatabaseBackupListResponseDto({ + this.backups = const [], + }); + + List backups; + + @override + bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupListResponseDto && + _deepEquality.equals(other.backups, backups); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (backups.hashCode); + + @override + String toString() => 'DatabaseBackupListResponseDto[backups=$backups]'; + + Map toJson() { + final json = {}; + json[r'backups'] = this.backups; + return json; + } + + /// Returns a new [DatabaseBackupListResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DatabaseBackupListResponseDto? fromJson(dynamic value) { + upgradeDto(value, "DatabaseBackupListResponseDto"); + if (value is Map) { + final json = value.cast(); + + return DatabaseBackupListResponseDto( + backups: DatabaseBackupDto.listFromJson(json[r'backups']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = DatabaseBackupListResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = DatabaseBackupListResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DatabaseBackupListResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = DatabaseBackupListResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'backups', + }; +} + diff --git a/mobile/openapi/lib/model/maintenance_action.dart b/mobile/openapi/lib/model/maintenance_action.dart index 9be628961f..59cfd0b21f 100644 --- a/mobile/openapi/lib/model/maintenance_action.dart +++ b/mobile/openapi/lib/model/maintenance_action.dart @@ -25,11 +25,15 @@ class MaintenanceAction { static const start = MaintenanceAction._(r'start'); static const end = MaintenanceAction._(r'end'); + static const selectDatabaseRestore = MaintenanceAction._(r'select_database_restore'); + static const restoreDatabase = MaintenanceAction._(r'restore_database'); /// List of all possible values in this [enum][MaintenanceAction]. static const values = [ start, end, + selectDatabaseRestore, + restoreDatabase, ]; static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value); @@ -70,6 +74,8 @@ class MaintenanceActionTypeTransformer { switch (data) { case r'start': return MaintenanceAction.start; case r'end': return MaintenanceAction.end; + case r'select_database_restore': return MaintenanceAction.selectDatabaseRestore; + case r'restore_database': return MaintenanceAction.restoreDatabase; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart new file mode 100644 index 0000000000..1c364a6fdc --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MaintenanceDetectInstallResponseDto { + /// Returns a new [MaintenanceDetectInstallResponseDto] instance. + MaintenanceDetectInstallResponseDto({ + this.storage = const [], + }); + + List storage; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallResponseDto && + _deepEquality.equals(other.storage, storage); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (storage.hashCode); + + @override + String toString() => 'MaintenanceDetectInstallResponseDto[storage=$storage]'; + + Map toJson() { + final json = {}; + json[r'storage'] = this.storage; + return json; + } + + /// Returns a new [MaintenanceDetectInstallResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceDetectInstallResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceDetectInstallResponseDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceDetectInstallResponseDto( + storage: MaintenanceDetectInstallStorageFolderDto.listFromJson(json[r'storage']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MaintenanceDetectInstallResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MaintenanceDetectInstallResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceDetectInstallResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MaintenanceDetectInstallResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'storage', + }; +} + diff --git a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart new file mode 100644 index 0000000000..e2a1e35dea --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart @@ -0,0 +1,123 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MaintenanceDetectInstallStorageFolderDto { + /// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance. + MaintenanceDetectInstallStorageFolderDto({ + required this.files, + required this.folder, + required this.readable, + required this.writable, + }); + + num files; + + StorageFolder folder; + + bool readable; + + bool writable; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallStorageFolderDto && + other.files == files && + other.folder == folder && + other.readable == readable && + other.writable == writable; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (files.hashCode) + + (folder.hashCode) + + (readable.hashCode) + + (writable.hashCode); + + @override + String toString() => 'MaintenanceDetectInstallStorageFolderDto[files=$files, folder=$folder, readable=$readable, writable=$writable]'; + + Map toJson() { + final json = {}; + json[r'files'] = this.files; + json[r'folder'] = this.folder; + json[r'readable'] = this.readable; + json[r'writable'] = this.writable; + return json; + } + + /// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceDetectInstallStorageFolderDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceDetectInstallStorageFolderDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceDetectInstallStorageFolderDto( + files: num.parse('${json[r'files']}'), + folder: StorageFolder.fromJson(json[r'folder'])!, + readable: mapValueOfType(json, r'readable')!, + writable: mapValueOfType(json, r'writable')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MaintenanceDetectInstallStorageFolderDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MaintenanceDetectInstallStorageFolderDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceDetectInstallStorageFolderDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MaintenanceDetectInstallStorageFolderDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'files', + 'folder', + 'readable', + 'writable', + }; +} + diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart new file mode 100644 index 0000000000..124fa674fd --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart @@ -0,0 +1,158 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MaintenanceStatusResponseDto { + /// Returns a new [MaintenanceStatusResponseDto] instance. + MaintenanceStatusResponseDto({ + required this.action, + required this.active, + this.error, + this.progress, + this.task, + }); + + MaintenanceAction action; + + bool active; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? error; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? progress; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? task; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceStatusResponseDto && + other.action == action && + other.active == active && + other.error == error && + other.progress == progress && + other.task == task; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (active.hashCode) + + (error == null ? 0 : error!.hashCode) + + (progress == null ? 0 : progress!.hashCode) + + (task == null ? 0 : task!.hashCode); + + @override + String toString() => 'MaintenanceStatusResponseDto[action=$action, active=$active, error=$error, progress=$progress, task=$task]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'active'] = this.active; + if (this.error != null) { + json[r'error'] = this.error; + } else { + // json[r'error'] = null; + } + if (this.progress != null) { + json[r'progress'] = this.progress; + } else { + // json[r'progress'] = null; + } + if (this.task != null) { + json[r'task'] = this.task; + } else { + // json[r'task'] = null; + } + return json; + } + + /// Returns a new [MaintenanceStatusResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceStatusResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceStatusResponseDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceStatusResponseDto( + action: MaintenanceAction.fromJson(json[r'action'])!, + active: mapValueOfType(json, r'active')!, + error: mapValueOfType(json, r'error'), + progress: num.parse('${json[r'progress']}'), + task: mapValueOfType(json, r'task'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MaintenanceStatusResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MaintenanceStatusResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceStatusResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MaintenanceStatusResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'action', + 'active', + }; +} + diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index d762946c3f..d5b9bf5086 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -62,6 +62,10 @@ class Permission { static const authPeriodChangePassword = Permission._(r'auth.changePassword'); static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); static const archivePeriodRead = Permission._(r'archive.read'); + static const backupPeriodList = Permission._(r'backup.list'); + static const backupPeriodDownload = Permission._(r'backup.download'); + static const backupPeriodUpload = Permission._(r'backup.upload'); + static const backupPeriodDelete = Permission._(r'backup.delete'); static const duplicatePeriodRead = Permission._(r'duplicate.read'); static const duplicatePeriodDelete = Permission._(r'duplicate.delete'); static const facePeriodCreate = Permission._(r'face.create'); @@ -214,6 +218,10 @@ class Permission { authPeriodChangePassword, authDevicePeriodDelete, archivePeriodRead, + backupPeriodList, + backupPeriodDownload, + backupPeriodUpload, + backupPeriodDelete, duplicatePeriodRead, duplicatePeriodDelete, facePeriodCreate, @@ -401,6 +409,10 @@ class PermissionTypeTransformer { case r'auth.changePassword': return Permission.authPeriodChangePassword; case r'authDevice.delete': return Permission.authDevicePeriodDelete; case r'archive.read': return Permission.archivePeriodRead; + case r'backup.list': return Permission.backupPeriodList; + case r'backup.download': return Permission.backupPeriodDownload; + case r'backup.upload': return Permission.backupPeriodUpload; + case r'backup.delete': return Permission.backupPeriodDelete; case r'duplicate.read': return Permission.duplicatePeriodRead; case r'duplicate.delete': return Permission.duplicatePeriodDelete; case r'face.create': return Permission.facePeriodCreate; diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index c724337529..d2fe900d4f 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -14,25 +14,41 @@ class SetMaintenanceModeDto { /// Returns a new [SetMaintenanceModeDto] instance. SetMaintenanceModeDto({ required this.action, + this.restoreBackupFilename, }); MaintenanceAction action; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? restoreBackupFilename; + @override bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto && - other.action == action; + other.action == action && + other.restoreBackupFilename == restoreBackupFilename; @override int get hashCode => // ignore: unnecessary_parenthesis - (action.hashCode); + (action.hashCode) + + (restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode); @override - String toString() => 'SetMaintenanceModeDto[action=$action]'; + String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename]'; Map toJson() { final json = {}; json[r'action'] = this.action; + if (this.restoreBackupFilename != null) { + json[r'restoreBackupFilename'] = this.restoreBackupFilename; + } else { + // json[r'restoreBackupFilename'] = null; + } return json; } @@ -46,6 +62,7 @@ class SetMaintenanceModeDto { return SetMaintenanceModeDto( action: MaintenanceAction.fromJson(json[r'action'])!, + restoreBackupFilename: mapValueOfType(json, r'restoreBackupFilename'), ); } return null; diff --git a/mobile/openapi/lib/model/storage_folder.dart b/mobile/openapi/lib/model/storage_folder.dart new file mode 100644 index 0000000000..df66bc187a --- /dev/null +++ b/mobile/openapi/lib/model/storage_folder.dart @@ -0,0 +1,97 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class StorageFolder { + /// Instantiate a new enum with the provided [value]. + const StorageFolder._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const encodedVideo = StorageFolder._(r'encoded-video'); + static const library_ = StorageFolder._(r'library'); + static const upload = StorageFolder._(r'upload'); + static const profile = StorageFolder._(r'profile'); + static const thumbs = StorageFolder._(r'thumbs'); + static const backups = StorageFolder._(r'backups'); + + /// List of all possible values in this [enum][StorageFolder]. + static const values = [ + encodedVideo, + library_, + upload, + profile, + thumbs, + backups, + ]; + + static StorageFolder? fromJson(dynamic value) => StorageFolderTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StorageFolder.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [StorageFolder] to String, +/// and [decode] dynamic data back to [StorageFolder]. +class StorageFolderTypeTransformer { + factory StorageFolderTypeTransformer() => _instance ??= const StorageFolderTypeTransformer._(); + + const StorageFolderTypeTransformer._(); + + String encode(StorageFolder data) => data.value; + + /// Decodes a [dynamic value][data] to a StorageFolder. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + StorageFolder? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'encoded-video': return StorageFolder.encodedVideo; + case r'library': return StorageFolder.library_; + case r'upload': return StorageFolder.upload; + case r'profile': return StorageFolder.profile; + case r'thumbs': return StorageFolder.thumbs; + case r'backups': return StorageFolder.backups; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [StorageFolderTypeTransformer] instance. + static StorageFolderTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index a2c89eb5c1..1d0e735396 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -20,6 +20,7 @@ class SyncAssetV1 { required this.fileModifiedAt, required this.height, required this.id, + required this.isEdited, required this.isFavorite, required this.libraryId, required this.livePhotoVideoId, @@ -47,6 +48,8 @@ class SyncAssetV1 { String id; + bool isEdited; + bool isFavorite; String? libraryId; @@ -78,6 +81,7 @@ class SyncAssetV1 { other.fileModifiedAt == fileModifiedAt && other.height == height && other.id == id && + other.isEdited == isEdited && other.isFavorite == isFavorite && other.libraryId == libraryId && other.livePhotoVideoId == livePhotoVideoId && @@ -100,6 +104,7 @@ class SyncAssetV1 { (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + (height == null ? 0 : height!.hashCode) + (id.hashCode) + + (isEdited.hashCode) + (isFavorite.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + @@ -113,7 +118,7 @@ class SyncAssetV1 { (width == null ? 0 : width!.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; @@ -144,6 +149,7 @@ class SyncAssetV1 { // json[r'height'] = null; } json[r'id'] = this.id; + json[r'isEdited'] = this.isEdited; json[r'isFavorite'] = this.isFavorite; if (this.libraryId != null) { json[r'libraryId'] = this.libraryId; @@ -198,6 +204,7 @@ class SyncAssetV1 { fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), height: mapValueOfType(json, r'height'), id: mapValueOfType(json, r'id')!, + isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), @@ -263,6 +270,7 @@ class SyncAssetV1 { 'fileModifiedAt', 'height', 'id', + 'isEdited', 'isFavorite', 'libraryId', 'livePhotoVideoId', diff --git a/mobile/test/domain/repositories/sync_stream_repository_test.dart b/mobile/test/domain/repositories/sync_stream_repository_test.dart index d39446ada3..a26683213c 100644 --- a/mobile/test/domain/repositories/sync_stream_repository_test.dart +++ b/mobile/test/domain/repositories/sync_stream_repository_test.dart @@ -44,6 +44,7 @@ SyncAssetV1 _createAsset({ livePhotoVideoId: null, stackId: null, thumbhash: null, + isEdited: false, ); } diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 571d5c0284..89eef9c831 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -19,6 +19,7 @@ import 'schema_v13.dart' as v13; import 'schema_v14.dart' as v14; import 'schema_v15.dart' as v15; import 'schema_v16.dart' as v16; +import 'schema_v17.dart' as v17; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -56,6 +57,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v15.DatabaseAtV15(db); case 16: return v16.DatabaseAtV16(db); + case 17: + return v17.DatabaseAtV17(db); default: throw MissingSchemaException(version, versions); } @@ -78,5 +81,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 14, 15, 16, + 17, ]; } diff --git a/mobile/test/drift/main/generated/schema_v17.dart b/mobile/test/drift/main/generated/schema_v17.dart new file mode 100644 index 0000000000..042c069ecd --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v17.dart @@ -0,0 +1,8337 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV17 extends GeneratedDatabase { + DatabaseAtV17(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 17; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 8d92011999..f3d6ab42a8 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -64,6 +64,7 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2025), updatedAt: DateTime(2025, 2), + isEdited: false, ); static final image2 = LocalAsset( @@ -72,5 +73,6 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2000), updatedAt: DateTime(20021), + isEdited: false, ); } diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index 69f6c1753f..c2254c0a03 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -128,6 +128,7 @@ abstract final class SyncStreamStub { visibility: AssetVisibility.timeline, width: null, height: null, + isEdited: false, ), ack: ack, ); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index d0374c3987..41dc46823d 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -194,6 +194,7 @@ void main() { latitude: 37.7749, longitude: -122.4194, adjustmentTime: DateTime(2026, 1, 2), + isEdited: false, ); final mockEntity = MockAssetEntity(); @@ -242,6 +243,7 @@ void main() { cloudId: 'cloud-id-123', latitude: 37.7749, longitude: -122.4194, + isEdited: false, ); final mockEntity = MockAssetEntity(); @@ -279,6 +281,7 @@ void main() { createdAt: DateTime(2025, 1, 1), updatedAt: DateTime(2025, 1, 2), cloudId: null, // No cloudId + isEdited: false, ); final mockEntity = MockAssetEntity(); @@ -320,6 +323,7 @@ void main() { cloudId: 'cloud-id-livephoto', latitude: 37.7749, longitude: -122.4194, + isEdited: false, ); final mockEntity = MockAssetEntity(); diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 498607e3d2..9d94e71052 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -131,6 +131,7 @@ abstract final class TestUtils { isFavorite: false, width: width, height: height, + isEdited: false, ); } @@ -154,6 +155,7 @@ abstract final class TestUtils { width: width, height: height, orientation: orientation, + isEdited: false, ); } } diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 19ad7166c6..b6f39ac3bd 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -27,6 +27,7 @@ class MediumFactory { type: type ?? AssetType.image, createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + isEdited: false, ); } diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index d93d59d3c7..4152155d24 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -23,6 +23,7 @@ LocalAsset createLocalAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: false, ); } @@ -45,6 +46,7 @@ RemoteAsset createRemoteAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: false, ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 67ac547fe3..804677c663 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -322,16 +322,27 @@ "x-immich-state": "Stable" } }, +<<<<<<< HEAD "/admin/integrity/report": { "post": { "description": "Get all flagged items by integrity report type", "operationId": "getIntegrityReport", +======= + "/admin/database-backups": { + "delete": { + "description": "Delete a backup by its filename", + "operationId": "deleteDatabaseBackup", +>>>>>>> origin/main "parameters": [], "requestBody": { "content": { "application/json": { "schema": { +<<<<<<< HEAD "$ref": "#/components/schemas/IntegrityGetReportDto" +======= + "$ref": "#/components/schemas/DatabaseBackupDeleteDto" +>>>>>>> origin/main } } }, @@ -339,6 +350,7 @@ }, "responses": { "200": { +<<<<<<< HEAD "content": { "application/json": { "schema": { @@ -346,6 +358,8 @@ } } }, +======= +>>>>>>> origin/main "description": "" } }, @@ -360,13 +374,20 @@ "api_key": [] } ], +<<<<<<< HEAD "summary": "Get integrity report by type", "tags": [ "Maintenance (admin)" +======= + "summary": "Delete database backup", + "tags": [ + "Database Backups (admin)" +>>>>>>> origin/main ], "x-immich-admin-only": true, "x-immich-history": [ { +<<<<<<< HEAD "version": "v2.6.0", "state": "Added" }, @@ -548,13 +569,33 @@ "get": { "description": "Get a count of the items flagged in each integrity report", "operationId": "getIntegrityReportSummary", +======= + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "backup.delete", + "x-immich-state": "Alpha" + }, + "get": { + "description": "Get the list of the successful and failed backups", + "operationId": "listDatabaseBackups", +>>>>>>> origin/main "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { +<<<<<<< HEAD "$ref": "#/components/schemas/IntegrityReportSummaryResponseDto" +======= + "$ref": "#/components/schemas/DatabaseBackupListResponseDto" +>>>>>>> origin/main } } }, @@ -572,18 +613,32 @@ "api_key": [] } ], +<<<<<<< HEAD "summary": "Get integrity report summary", "tags": [ "Maintenance (admin)" +======= + "summary": "List database backups", + "tags": [ + "Database Backups (admin)" +>>>>>>> origin/main ], "x-immich-admin-only": true, "x-immich-history": [ { +<<<<<<< HEAD "version": "v2.6.0", "state": "Added" }, { "version": "v2.6.0", +======= + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", +>>>>>>> origin/main "state": "Alpha" } ], @@ -591,6 +646,145 @@ "x-immich-state": "Alpha" } }, +<<<<<<< HEAD +======= + "/admin/database-backups/start-restore": { + "post": { + "description": "Put Immich into maintenance mode to restore a backup (Immich must not be configured)", + "operationId": "startDatabaseRestoreFlow", + "parameters": [], + "responses": { + "201": { + "description": "" + } + }, + "summary": "Start database backup restore flow", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, + "/admin/database-backups/upload": { + "post": { + "description": "Uploads .sql/.sql.gz file to restore backup from", + "operationId": "uploadDatabaseBackup", + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DatabaseBackupUploadDto" + } + } + }, + "description": "Backup Upload", + "required": true + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Upload database backup", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "backup.upload", + "x-immich-state": "Alpha" + } + }, + "/admin/database-backups/{filename}": { + "get": { + "description": "Downloads the database backup file", + "operationId": "downloadDatabaseBackup", + "parameters": [ + { + "name": "filename", + "required": true, + "in": "path", + "schema": { + "format": "string", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Download database backup", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "backup.download", + "x-immich-state": "Alpha" + } + }, +>>>>>>> origin/main "/admin/maintenance": { "post": { "description": "Put Immich into or take it out of maintenance mode", @@ -641,6 +835,53 @@ "x-immich-state": "Alpha" } }, + "/admin/maintenance/detect-install": { + "get": { + "description": "Collect integrity checks and other heuristics about local data.", + "operationId": "detectPriorInstall", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaintenanceDetectInstallResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Detect existing install", + "tags": [ + "Maintenance (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "maintenance", + "x-immich-state": "Alpha" + } + }, "/admin/maintenance/login": { "post": { "description": "Login with maintenance token or cookie to receive current information and perform further actions.", @@ -685,6 +926,40 @@ "x-immich-state": "Alpha" } }, + "/admin/maintenance/status": { + "get": { + "description": "Fetch information about the currently running maintenance action.", + "operationId": "getMaintenanceStatus", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaintenanceStatusResponseDto" + } + } + }, + "description": "" + } + }, + "summary": "Get maintenance mode status", + "tags": [ + "Maintenance (admin)" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, "/admin/notifications": { "post": { "description": "Create a new notification for a specific user.", @@ -14930,6 +15205,10 @@ "name": "Authentication (admin)", "description": "Administrative endpoints related to authentication." }, + { + "name": "Database Backups (admin)", + "description": "Manage backups of the Immich database." + }, { "name": "Deprecated", "description": "Deprecated endpoints that are planned for removal in the next major release." @@ -16549,6 +16828,20 @@ "isArchived": { "type": "boolean" }, + "isEdited": { + "type": "boolean", + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-state": "Beta" + }, "isFavorite": { "type": "boolean" }, @@ -16681,6 +16974,7 @@ "height", "id", "isArchived", + "isEdited", "isFavorite", "isOffline", "isTrashed", @@ -17107,6 +17401,58 @@ ], "type": "object" }, + "DatabaseBackupDeleteDto": { + "properties": { + "backups": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "backups" + ], + "type": "object" + }, + "DatabaseBackupDto": { + "properties": { + "filename": { + "type": "string" + }, + "filesize": { + "type": "number" + } + }, + "required": [ + "filename", + "filesize" + ], + "type": "object" + }, + "DatabaseBackupListResponseDto": { + "properties": { + "backups": { + "items": { + "$ref": "#/components/schemas/DatabaseBackupDto" + }, + "type": "array" + } + }, + "required": [ + "backups" + ], + "type": "object" + }, + "DatabaseBackupUploadDto": { + "properties": { + "file": { + "format": "binary", + "type": "string" + } + }, + "type": "object" + }, "DownloadArchiveInfo": { "properties": { "assetIds": { @@ -17874,7 +18220,9 @@ "MaintenanceAction": { "enum": [ "start", - "end" + "end", + "select_database_restore", + "restore_database" ], "type": "string" }, @@ -17889,6 +18237,47 @@ ], "type": "object" }, + "MaintenanceDetectInstallResponseDto": { + "properties": { + "storage": { + "items": { + "$ref": "#/components/schemas/MaintenanceDetectInstallStorageFolderDto" + }, + "type": "array" + } + }, + "required": [ + "storage" + ], + "type": "object" + }, + "MaintenanceDetectInstallStorageFolderDto": { + "properties": { + "files": { + "type": "number" + }, + "folder": { + "allOf": [ + { + "$ref": "#/components/schemas/StorageFolder" + } + ] + }, + "readable": { + "type": "boolean" + }, + "writable": { + "type": "boolean" + } + }, + "required": [ + "files", + "folder", + "readable", + "writable" + ], + "type": "object" + }, "MaintenanceLoginDto": { "properties": { "token": { @@ -17897,6 +18286,34 @@ }, "type": "object" }, + "MaintenanceStatusResponseDto": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/MaintenanceAction" + } + ] + }, + "active": { + "type": "boolean" + }, + "error": { + "type": "string" + }, + "progress": { + "type": "number" + }, + "task": { + "type": "string" + } + }, + "required": [ + "action", + "active" + ], + "type": "object" + }, "ManualJobName": { "enum": [ "person-cleanup", @@ -18874,6 +19291,10 @@ "auth.changePassword", "authDevice.delete", "archive.read", + "backup.list", + "backup.download", + "backup.upload", + "backup.delete", "duplicate.read", "duplicate.delete", "face.create", @@ -20657,6 +21078,9 @@ "$ref": "#/components/schemas/MaintenanceAction" } ] + }, + "restoreBackupFilename": { + "type": "string" } }, "required": [ @@ -21229,6 +21653,17 @@ }, "type": "object" }, + "StorageFolder": { + "enum": [ + "encoded-video", + "library", + "upload", + "profile", + "thumbs", + "backups" + ], + "type": "string" + }, "SyncAckDeleteDto": { "properties": { "types": { @@ -21680,6 +22115,9 @@ "id": { "type": "string" }, + "isEdited": { + "type": "boolean" + }, "isFavorite": { "type": "boolean" }, @@ -21737,6 +22175,7 @@ "fileModifiedAt", "height", "id", + "isEdited", "isFavorite", "libraryId", "livePhotoVideoId", diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 69e422cbbe..499539f5ee 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^24.10.4", + "@types/node": "^24.10.8", "typescript": "^5.3.3" }, "repository": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index abf7236e37..8d6b90c013 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -40,6 +40,7 @@ export type ActivityStatisticsResponseDto = { comments: number; likes: number; }; +<<<<<<< HEAD export type IntegrityGetReportDto = { cursor?: string; limit?: number; @@ -58,9 +59,33 @@ export type IntegrityReportSummaryResponseDto = { checksum_mismatch: number; missing_file: number; untracked_file: number; +======= +export type DatabaseBackupDeleteDto = { + backups: string[]; +}; +export type DatabaseBackupDto = { + filename: string; + filesize: number; +}; +export type DatabaseBackupListResponseDto = { + backups: DatabaseBackupDto[]; +}; +export type DatabaseBackupUploadDto = { + file?: Blob; +>>>>>>> origin/main }; export type SetMaintenanceModeDto = { action: MaintenanceAction; + restoreBackupFilename?: string; +}; +export type MaintenanceDetectInstallStorageFolderDto = { + files: number; + folder: StorageFolder; + readable: boolean; + writable: boolean; +}; +export type MaintenanceDetectInstallResponseDto = { + storage: MaintenanceDetectInstallStorageFolderDto[]; }; export type MaintenanceLoginDto = { token?: string; @@ -68,6 +93,13 @@ export type MaintenanceLoginDto = { export type MaintenanceAuthDto = { username: string; }; +export type MaintenanceStatusResponseDto = { + action: MaintenanceAction; + active: boolean; + error?: string; + progress?: number; + task?: string; +}; export type NotificationCreateDto = { data?: object; description?: string | null; @@ -371,6 +403,7 @@ export type AssetResponseDto = { height: number | null; id: string; isArchived: boolean; + isEdited: boolean; isFavorite: boolean; isOffline: boolean; isTrashed: boolean; @@ -1957,6 +1990,7 @@ export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) { })); } /** +<<<<<<< HEAD * Get integrity report by type */ export function getIntegrityReport({ integrityGetReportDto }: { @@ -2016,6 +2050,61 @@ export function getIntegrityReportSummary(opts?: Oazapfts.RequestOpts) { status: 200; data: IntegrityReportSummaryResponseDto; }>("/admin/integrity/summary", { +======= + * Delete database backup + */ +export function deleteDatabaseBackup({ databaseBackupDeleteDto }: { + databaseBackupDeleteDto: DatabaseBackupDeleteDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/database-backups", oazapfts.json({ + ...opts, + method: "DELETE", + body: databaseBackupDeleteDto + }))); +} +/** + * List database backups + */ +export function listDatabaseBackups(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: DatabaseBackupListResponseDto; + }>("/admin/database-backups", { + ...opts + })); +} +/** + * Start database backup restore flow + */ +export function startDatabaseRestoreFlow(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/database-backups/start-restore", { + ...opts, + method: "POST" + })); +} +/** + * Upload database backup + */ +export function uploadDatabaseBackup({ databaseBackupUploadDto }: { + databaseBackupUploadDto: DatabaseBackupUploadDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/database-backups/upload", oazapfts.multipart({ + ...opts, + method: "POST", + body: databaseBackupUploadDto + }))); +} +/** + * Download database backup + */ +export function downloadDatabaseBackup({ filename }: { + filename: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/admin/database-backups/${encodeURIComponent(filename)}`, { +>>>>>>> origin/main ...opts })); } @@ -2031,6 +2120,17 @@ export function setMaintenanceMode({ setMaintenanceModeDto }: { body: setMaintenanceModeDto }))); } +/** + * Detect existing install + */ +export function detectPriorInstall(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MaintenanceDetectInstallResponseDto; + }>("/admin/maintenance/detect-install", { + ...opts + })); +} /** * Log into maintenance mode */ @@ -2046,6 +2146,17 @@ export function maintenanceLogin({ maintenanceLoginDto }: { body: maintenanceLoginDto }))); } +/** + * Get maintenance mode status + */ +export function getMaintenanceStatus(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MaintenanceStatusResponseDto; + }>("/admin/maintenance/status", { + ...opts + })); +} /** * Create a notification */ @@ -5401,7 +5512,17 @@ export enum IntegrityReportType { } export enum MaintenanceAction { Start = "start", - End = "end" + End = "end", + SelectDatabaseRestore = "select_database_restore", + RestoreDatabase = "restore_database" +} +export enum StorageFolder { + EncodedVideo = "encoded-video", + Library = "library", + Upload = "upload", + Profile = "profile", + Thumbs = "thumbs", + Backups = "backups" } export enum NotificationLevel { Success = "success", @@ -5499,6 +5620,10 @@ export enum Permission { AuthChangePassword = "auth.changePassword", AuthDeviceDelete = "authDevice.delete", ArchiveRead = "archive.read", + BackupList = "backup.list", + BackupDownload = "backup.download", + BackupUpload = "backup.upload", + BackupDelete = "backup.delete", DuplicateRead = "duplicate.read", DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", diff --git a/package.json b/package.json index 9ebfc6dd04..ad325ac493 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a", + "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48", "engines": { "pnpm": ">=10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e74dc09990..59b4846bff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 cli: dependencies: @@ -63,11 +63,11 @@ importers: specifier: ^4.13.1 version: 4.13.4 '@types/node': - specifier: ^24.10.4 + specifier: ^24.10.8 version: 24.10.9 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -85,7 +85,7 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -97,28 +97,28 @@ importers: version: 5.5.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) yaml: specifier: ^2.3.1 version: 2.8.2 @@ -127,16 +127,16 @@ importers: dependencies: '@docusaurus/core': specifier: ~3.9.0 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/preset-classic': specifier: ~3.9.0 - version: 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + version: 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/theme-common': specifier: ~3.9.0 - version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-mermaid': specifier: ~3.9.0 - version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -145,13 +145,13 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.7)(react@18.3.1) + version: 3.1.1(@types/react@19.2.8)(react@18.3.1) autoprefixer: specifier: ^10.4.17 version: 10.4.23(postcss@8.5.6) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lunr: specifier: ^2.3.9 version: 2.3.9 @@ -163,7 +163,7 @@ importers: version: 2.4.1(react@18.3.1) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.103.0) + version: 4.0.2(webpack@5.104.1) react: specifier: ^18.0.0 version: 18.3.1 @@ -188,7 +188,7 @@ importers: version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 typescript: specifier: ^5.1.6 version: 5.9.3 @@ -220,7 +220,7 @@ importers: specifier: ^3.4.2 version: 3.7.1 '@types/node': - specifier: ^24.10.4 + specifier: ^24.10.8 version: 24.10.9 '@types/pg': specifier: ^8.15.1 @@ -242,7 +242,7 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -257,16 +257,16 @@ importers: version: 3.7.2 pg: specifier: ^8.11.3 - version: 8.16.3 + version: 8.17.1 pngjs: specifier: ^7.0.0 version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -281,13 +281,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) utimes: specifier: ^5.2.1 version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) e2e-auth-server: devDependencies: @@ -308,10 +308,10 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.1.1(prettier@3.7.4) + version: 4.2.0(prettier@3.8.0) open-api/typescript-sdk: dependencies: @@ -320,7 +320,7 @@ importers: version: 1.1.0 devDependencies: '@types/node': - specifier: ^24.10.4 + specifier: ^24.10.8 version: 24.10.9 typescript: specifier: ^5.3.3 @@ -345,58 +345,58 @@ importers: version: 2.0.0-rc13 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4) + version: 11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(bullmq@5.66.5) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + version: 6.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.2.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 11.2.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.210.0 + version: 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.210.0 + version: 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.57.0 - version: 0.57.0(@opentelemetry/api@1.9.0) + specifier: ^0.58.0 + version: 0.58.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.55.0 - version: 0.55.0(@opentelemetry/api@1.9.0) + specifier: ^0.56.0 + version: 0.56.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.61.0 - version: 0.61.2(@opentelemetry/api@1.9.0) + specifier: ^0.62.0 + version: 0.62.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.210.0 + version: 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.38.0 @@ -426,7 +426,7 @@ importers: version: 2.2.2 bullmq: specifier: ^5.51.0 - version: 5.66.4 + version: 5.66.5 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -462,7 +462,7 @@ importers: version: 2.1.3 geo-tz: specifier: ^8.0.0 - version: 8.1.4 + version: 8.1.5 handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -471,7 +471,7 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.9.0 + version: 5.9.1 jose: specifier: ^5.10.0 version: 5.10.0 @@ -501,16 +501,16 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3) nestjs-cls: specifier: ^5.0.0 - version: 5.4.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 5.4.3(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(kysely@0.28.2)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(kysely@0.28.2)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^7.0.0 - version: 7.0.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + version: 7.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) nodemailer: specifier: ^7.0.0 version: 7.0.12 @@ -519,10 +519,10 @@ importers: version: 6.8.1 pg: specifier: ^8.11.3 - version: 8.16.3 + version: 8.17.1 pg-connection-string: specifier: ^2.9.1 - version: 2.9.1 + version: 2.10.0 picomatch: specifier: ^4.0.2 version: 4.0.3 @@ -573,7 +573,7 @@ importers: version: 3.1.0 ua-parser-js: specifier: ^2.0.0 - version: 2.0.7 + version: 2.0.8 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -586,13 +586,13 @@ importers: version: 9.39.2 '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.14(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9) + version: 11.0.15(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12) '@swc/core': specifier: ^1.4.14 version: 1.15.8(@swc/helpers@0.5.17) @@ -628,7 +628,7 @@ importers: version: 9.0.10 '@types/lodash': specifier: ^4.14.197 - version: 4.17.21 + version: 4.17.23 '@types/luxon': specifier: ^3.6.2 version: 3.7.1 @@ -639,11 +639,11 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@types/node': - specifier: ^24.10.4 + specifier: ^24.10.8 version: 24.10.9 '@types/nodemailer': specifier: ^7.0.0 - version: 7.0.4 + version: 7.0.5 '@types/picomatch': specifier: ^4.0.0 version: 4.0.2 @@ -652,7 +652,7 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.8 '@types/sanitize-html': specifier: ^2.13.0 version: 2.16.0 @@ -670,7 +670,7 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) eslint: specifier: ^9.14.0 version: 9.39.2(jiti@2.6.1) @@ -679,7 +679,7 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -697,13 +697,13 @@ importers: version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 - version: 15.6.12 + version: 15.7.0 supertest: specifier: ^7.1.0 version: 7.2.2 @@ -718,16 +718,16 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web: dependencies: @@ -741,8 +741,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.58.4 - version: 0.58.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) + specifier: ^0.59.0 + version: 0.59.0(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -751,22 +751,22 @@ importers: version: 7.4.47 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.14.0 + version: 5.14.1 '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)) '@photo-sphere-viewer/markers-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/settings-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@photo-sphere-viewer/video-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -793,7 +793,7 @@ importers: version: 4.7.8 happy-dom: specifier: ^20.0.0 - version: 20.1.0 + version: 20.3.0 intl-messageformat: specifier: ^11.0.0 version: 11.0.9 @@ -808,7 +808,7 @@ importers: version: 3.7.2 maplibre-gl: specifier: ^5.6.2 - version: 5.15.0 + version: 5.16.0 pmtiles: specifier: ^4.3.0 version: 4.3.2 @@ -860,25 +860,25 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) + version: 3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) '@sveltejs/enhanced-img': specifier: ^0.9.0 - version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': - specifier: 6.2.3 - version: 6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: 6.2.4 + version: 6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.18(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.3.1(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -902,7 +902,7 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 version: 17.2.3 @@ -929,16 +929,16 @@ importers: version: 16.5.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.1.1(prettier@3.7.4) + version: 4.2.0(prettier@3.8.0) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.1(prettier@3.7.4)(svelte@5.46.4) + version: 3.4.1(prettier@3.8.0)(svelte@5.46.4) rollup-plugin-visualizer: specifier: ^6.0.0 version: 6.0.5(rollup@4.55.1) @@ -959,13 +959,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.45.0 - version: 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -1129,124 +1129,124 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-sesv2@3.952.0': - resolution: {integrity: sha512-0avirspZ7/RkHqp9It12xx6UJ2rkO6B6EeNScIgDkgyELl4tGsmF8bhBSPDqeJMZ1HQGYglanzkDRrYFgTN6iA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-sesv2@3.971.0': + resolution: {integrity: sha512-NP/lbf3mfY10Txzl0ml2YnTjnZwflp1+faOotMCrXi4fb6kInosdW0ZSHXNlNulFo9cW+llq07lD59Sw3nny+A==} + engines: {node: '>=20.0.0'} - '@aws-sdk/client-sso@3.948.0': - resolution: {integrity: sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.971.0': + resolution: {integrity: sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.947.0': - resolution: {integrity: sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.970.0': + resolution: {integrity: sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.947.0': - resolution: {integrity: sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.970.0': + resolution: {integrity: sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.947.0': - resolution: {integrity: sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.970.0': + resolution: {integrity: sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.952.0': - resolution: {integrity: sha512-N5B15SwzMkZ8/LLopNksTlPEWWZn5tbafZAUfMY5Xde4rSHGWmv5H/ws2M3P8L0X77E2wKnOJsNmu+GsArBreQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.971.0': + resolution: {integrity: sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.952.0': - resolution: {integrity: sha512-jL9zc+e+7sZeJrHzYKK9GOjl1Ktinh0ORU3cM2uRBi7fuH/0zV9pdMN8PQnGXz0i4tJaKcZ1lrE4V0V6LB9NQg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-login@3.971.0': + resolution: {integrity: sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.952.0': - resolution: {integrity: sha512-pj7nidLrb3Dz9llcUPh6N0Yv1dBYTS9xJqi8u0kI8D5sn72HJMB+fIOhcDQVXXAw/dpVolOAH9FOAbog5JDAMg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.971.0': + resolution: {integrity: sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.947.0': - resolution: {integrity: sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.970.0': + resolution: {integrity: sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.952.0': - resolution: {integrity: sha512-1CQdP5RzxeXuEfytbAD5TgreY1c9OacjtCdO8+n9m05tpzBABoNBof0hcjzw1dtrWFH7deyUgfwCl1TAN3yBWQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.971.0': + resolution: {integrity: sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.952.0': - resolution: {integrity: sha512-5hJbfaZdHDAP8JlwplNbXJAat9Vv7L0AbTZzkbPIgjHhC3vrMf5r3a6I1HWFp5i5pXo7J45xyuf5uQGZJxJlCg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.971.0': + resolution: {integrity: sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.936.0': - resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.969.0': + resolution: {integrity: sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.936.0': - resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.969.0': + resolution: {integrity: sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.948.0': - resolution: {integrity: sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.969.0': + resolution: {integrity: sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.947.0': - resolution: {integrity: sha512-DS2tm5YBKhPW2PthrRBDr6eufChbwXe0NjtTZcYDfUCXf0OR+W6cIqyKguwHMJ+IyYdey30AfVw9/Lb5KB8U8A==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-sdk-s3@3.970.0': + resolution: {integrity: sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.947.0': - resolution: {integrity: sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-user-agent@3.970.0': + resolution: {integrity: sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.952.0': - resolution: {integrity: sha512-OtuirjxuOqZyDcI0q4WtoyWfkq3nSnbH41JwJQsXJefduWcww1FQe5TL1JfYCU7seUxHzK8rg2nFxUBuqUlZtg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.971.0': + resolution: {integrity: sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.936.0': - resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.969.0': + resolution: {integrity: sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.947.0': - resolution: {integrity: sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/signature-v4-multi-region@3.970.0': + resolution: {integrity: sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.952.0': - resolution: {integrity: sha512-IpQVC9WOeXQlCEcFVNXWDIKy92CH1Az37u9K0H3DF/HT56AjhyDVKQQfHUy00nt7bHFe3u0K5+zlwErBeKy5ZA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.971.0': + resolution: {integrity: sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.936.0': - resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.969.0': + resolution: {integrity: sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-arn-parser@3.893.0': - resolution: {integrity: sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-arn-parser@3.968.0': + resolution: {integrity: sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.936.0': - resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.970.0': + resolution: {integrity: sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.893.0': - resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-locate-window@3.965.2': + resolution: {integrity: sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.936.0': - resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} + '@aws-sdk/util-user-agent-browser@3.969.0': + resolution: {integrity: sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==} - '@aws-sdk/util-user-agent-node@3.947.0': - resolution: {integrity: sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-user-agent-node@3.971.0': + resolution: {integrity: sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==} + engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' peerDependenciesMeta: aws-crt: optional: true - '@aws-sdk/xml-builder@3.930.0': - resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} + '@aws-sdk/xml-builder@3.969.0': + resolution: {integrity: sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} - '@aws/lambda-invoke-store@0.2.2': - resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} - engines: {node: '>=18.0.0'} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.28.5': @@ -3128,8 +3128,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.58.4': - resolution: {integrity: sha512-/Y+TRA9E8VQ+yH0aqrkEnQTQi4j02dNgahil9NbJe3hSnakfDHZUgJR5xevGZbKqlnBV4O3mjbwmzr6j9wlP7w==} + '@immich/ui@0.59.0': + resolution: {integrity: sha512-7yxvyhhd99T0AHhjMakp7c/U4n0jGAmRO5xpncsRASRvqZve/LAibjr6N5FJc5IAd222DROTMLn6imsxVfqfvg==} peerDependencies: svelte: ^5.0.0 @@ -3137,6 +3137,10 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} + '@inquirer/ansi@2.0.3': + resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/checkbox@4.3.2': resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} engines: {node: '>=18'} @@ -3146,6 +3150,15 @@ packages: '@types/node': optional: true + '@inquirer/checkbox@5.0.4': + resolution: {integrity: sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@5.1.21': resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} @@ -3155,6 +3168,15 @@ packages: '@types/node': optional: true + '@inquirer/confirm@6.0.4': + resolution: {integrity: sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@10.3.2': resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} @@ -3164,6 +3186,15 @@ packages: '@types/node': optional: true + '@inquirer/core@11.1.1': + resolution: {integrity: sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/editor@4.2.23': resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} engines: {node: '>=18'} @@ -3173,6 +3204,15 @@ packages: '@types/node': optional: true + '@inquirer/editor@5.0.4': + resolution: {integrity: sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/expand@4.0.23': resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} engines: {node: '>=18'} @@ -3182,6 +3222,15 @@ packages: '@types/node': optional: true + '@inquirer/expand@5.0.4': + resolution: {integrity: sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -3191,10 +3240,23 @@ packages: '@types/node': optional: true + '@inquirer/external-editor@2.0.3': + resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} + '@inquirer/figures@2.0.3': + resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/input@4.3.1': resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} @@ -3204,6 +3266,15 @@ packages: '@types/node': optional: true + '@inquirer/input@5.0.4': + resolution: {integrity: sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/number@3.0.23': resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} engines: {node: '>=18'} @@ -3213,6 +3284,15 @@ packages: '@types/node': optional: true + '@inquirer/number@4.0.4': + resolution: {integrity: sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/password@4.0.23': resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} @@ -3222,9 +3302,9 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.10.1': - resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} - engines: {node: '>=18'} + '@inquirer/password@5.0.4': + resolution: {integrity: sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -3240,6 +3320,15 @@ packages: '@types/node': optional: true + '@inquirer/prompts@8.2.0': + resolution: {integrity: sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/rawlist@4.1.11': resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} engines: {node: '>=18'} @@ -3249,6 +3338,15 @@ packages: '@types/node': optional: true + '@inquirer/rawlist@5.2.0': + resolution: {integrity: sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/search@3.2.2': resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} engines: {node: '>=18'} @@ -3258,6 +3356,15 @@ packages: '@types/node': optional: true + '@inquirer/search@4.1.0': + resolution: {integrity: sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/select@4.4.2': resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} engines: {node: '>=18'} @@ -3267,6 +3374,15 @@ packages: '@types/node': optional: true + '@inquirer/select@5.0.4': + resolution: {integrity: sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@3.0.10': resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} @@ -3276,12 +3392,18 @@ packages: '@types/node': optional: true + '@inquirer/type@4.0.3': + resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} - '@ioredis/commands@1.4.0': - resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} - '@ioredis/commands@1.5.0': resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} @@ -3558,8 +3680,8 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 - '@nestjs/cli@11.0.14': - resolution: {integrity: sha512-YwP03zb5VETTwelXU+AIzMVbEZKk/uxJL+z9pw0mdG9ogAtqZ6/mpmIM4nEq/NU8D0a7CBRLcMYUmWW/55pfqw==} + '@nestjs/cli@11.0.15': + resolution: {integrity: sha512-4Sw4i+PRI1CGVnl3F15GWytFYD+QHs6vsayVeqDhhWwL1a7ZhQyUYvmlCMoWi77rZA0+m3ObUO1WujtkXsYBDQ==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -3571,8 +3693,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.11': - resolution: {integrity: sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==} + '@nestjs/common@11.1.12': + resolution: {integrity: sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3584,8 +3706,8 @@ packages: class-validator: optional: true - '@nestjs/core@11.1.11': - resolution: {integrity: sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==} + '@nestjs/core@11.1.12': + resolution: {integrity: sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3615,14 +3737,14 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.11': - resolution: {integrity: sha512-kyABSskdMRIAMWL0SlbwtDy4yn59RL4HDdwHDz/fxWuv7/53YP8Y2DtV3/sHqY5Er0msMVTZrM38MjqXhYL7gw==} + '@nestjs/platform-express@11.1.12': + resolution: {integrity: sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io@11.1.11': - resolution: {integrity: sha512-0z6pLg9CuTXtz7q2lRZoPOU94DN28OTa39f4cQrlZysKA6QrKM7w7z6xqb4g32qjF+LQHFNRmMJtE/pLrxBaig==} + '@nestjs/platform-socket.io@11.1.12': + resolution: {integrity: sha512-1itTTYsAZecrq2NbJOkch32y8buLwN7UpcNRdJrhlS+ovJ5GxLx3RyJ3KylwBhbYnO5AeYyL1U/i4W5mg/4qDA==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -3639,8 +3761,8 @@ packages: peerDependencies: typescript: '>=4.8.2' - '@nestjs/swagger@11.2.4': - resolution: {integrity: sha512-7MLqtHfD2qfhZUyg13FyX6liwigtXUL8gHXq7PaBcGo9cu8QWDDT//Fn3qzJx59+Wh+Ly/Zn+prCMpskPI5nrQ==} + '@nestjs/swagger@11.2.5': + resolution: {integrity: sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA==} peerDependencies: '@fastify/static': ^8.0.0 || ^9.0.0 '@nestjs/common': ^11.0.1 @@ -3656,8 +3778,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.1.11': - resolution: {integrity: sha512-Po2aZKXlxuySDEh3Gi05LJ7/BtfTAPRZ3KPTrbpNrTmgGr3rFgEGYpQwN50wXYw0pywoICiFLZSZ/qXsplf6NA==} + '@nestjs/testing@11.1.12': + resolution: {integrity: sha512-W0M/i5nb9qRQpTQfJm+1mGT/+y4YezwwdcD7mxFG8JEZ5fz/ZEAk1Ayri2VBJKJUdo20B1ggnvqew4dlTMrSNg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3669,8 +3791,8 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/websockets@11.1.11': - resolution: {integrity: sha512-apuP7C/gtMBIYNgA8IWt75GTZeWya5JQCnrLZFcOu+IZt00j9Xd/Bm7hbj/Qr/JVoM/7q6c/4p4oOZtBGx4aeA==} + '@nestjs/websockets@11.1.12': + resolution: {integrity: sha512-ulSOYcgosx1TqY425cRC5oXtAu1R10+OSmVfgyR9ueR25k4luekURt8dzAZxhxSCI0OsDj9WKCFLTkEuAwg0wg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3713,88 +3835,94 @@ packages: '@oazapfts/runtime@1.1.0': resolution: {integrity: sha512-PwCn69pexqg/uhc0bpEHSlRFdfTtSnq3icXHd0wf4BQwZSMKsCerTnydzegVScEegYkokzIxMcl9li7on86A2w==} - '@opentelemetry/api-logs@0.208.0': - resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + '@opentelemetry/api-logs@0.210.0': + resolution: {integrity: sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.2.0': - resolution: {integrity: sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==} + '@opentelemetry/configuration@0.210.0': + resolution: {integrity: sha512-tM0ROS/hZM72kB55cSjDcghVcUXBJdGkGzpkhD7M1B/gpcvZPSGfjFgKN3dgmxNgF76NxtbUwv3ik0wS+Kz52g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.4.0': + resolution: {integrity: sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.2.0': - resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + '@opentelemetry/core@2.4.0': + resolution: {integrity: sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.208.0': - resolution: {integrity: sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg==} + '@opentelemetry/exporter-logs-otlp-grpc@0.210.0': + resolution: {integrity: sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.208.0': - resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + '@opentelemetry/exporter-logs-otlp-http@0.210.0': + resolution: {integrity: sha512-Q8/SEQtgrErbVVRg9M9iaG8m5wdPNdU0UOF7U43sAhwfmPG92ZOk/aenKhg0DXSNJHhkCDNCgS1kSoErAB3z0A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.208.0': - resolution: {integrity: sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ==} + '@opentelemetry/exporter-logs-otlp-proto@0.210.0': + resolution: {integrity: sha512-Y/yPc+gDhsWB7AsNzQWxblw4ULbvhCycMaQ2aAn+HSAVbgbMiZa0SbclPVHSnpnNzKSLVavFjweAr0pQA1KKLg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0': - resolution: {integrity: sha512-YbEnk7jjYmvhIwp2xJGkEvdgnayrA2QSr28R1LR1klDPvCxsoQPxE6TokDbQpoCEhD3+KmJVEXfb4EeEQxjymg==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.210.0': + resolution: {integrity: sha512-pWZ/Tjrqev9rdkqe8F6A9FGddLZrjl6iRAU5LBvvRL6I3PSgG8z1xM0cESAy1jzAF4wGohnAh8rB7hHzpUOYEA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.208.0': - resolution: {integrity: sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA==} + '@opentelemetry/exporter-metrics-otlp-http@0.210.0': + resolution: {integrity: sha512-JpLThG8Hh8A/Jzdzw9i4Ftu+EzvLaX/LouN+mOOHmadL0iror0Qsi3QWzucXeiUsDDsiYgjfKyi09e6sltytgA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.208.0': - resolution: {integrity: sha512-CvvVD5kRDmRB/uSMalvEF6kiamY02pB46YAqclHtfjJccNZFxbkkXkMMmcJ7NgBFa5THmQBNVQ2AHyX29nRxOw==} + '@opentelemetry/exporter-metrics-otlp-proto@0.210.0': + resolution: {integrity: sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.208.0': - resolution: {integrity: sha512-Rgws8GfIfq2iNWCD3G1dTD9xwYsCof1+tc5S5X0Ahdb5CrAPE+k5P70XCWHqrFFurVCcKaHLJ/6DjIBHWVfLiw==} + '@opentelemetry/exporter-prometheus@0.210.0': + resolution: {integrity: sha512-8i+7d70Hho6pcheTtbqIuS+bo+AIX/oNUTMwIEZoehUE4ZdbGmeVaE+hJS2LAErFeFaU71w164lAgYyMUEQ8zw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.208.0': - resolution: {integrity: sha512-E/eNdcqVUTAT7BC+e8VOw/krqb+5rjzYkztMZ/o+eyJl+iEY6PfczPXpwWuICwvsm0SIhBoh9hmYED5Vh5RwIw==} + '@opentelemetry/exporter-trace-otlp-grpc@0.210.0': + resolution: {integrity: sha512-1GPLOyxIfUX24WM8Oea+vx9d9TlewposUnsQXTjusxVMQ/dWvt5JIDJyTsfNDS412XRUOORgF97PwsfDY5QKGA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.208.0': - resolution: {integrity: sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==} + '@opentelemetry/exporter-trace-otlp-http@0.210.0': + resolution: {integrity: sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.208.0': - resolution: {integrity: sha512-q844Jc3ApkZVdWYd5OAl+an3n1XXf3RWHa3Zgmnhw3HpsM3VluEKHckUUEqHPzbwDUx2lhPRVkqK7LsJ/CbDzA==} + '@opentelemetry/exporter-trace-otlp-proto@0.210.0': + resolution: {integrity: sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.2.0': - resolution: {integrity: sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg==} + '@opentelemetry/exporter-zipkin@2.4.0': + resolution: {integrity: sha512-qpiXY0TUEFjBBp9b1na9LfuVQw6W8LH+te7uv+CC+0Up78ZDtZZwOjK2M7CL7Nspnw+yS4JdgEA7oxsBu0Ctsg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3805,62 +3933,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.208.0': - resolution: {integrity: sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==} + '@opentelemetry/instrumentation-http@0.210.0': + resolution: {integrity: sha512-dICO+0D0VBnrDOmDXOvpmaP0gvai6hNhJ5y6+HFutV0UoXc7pMgJlJY3O7AzT725cW/jP38ylmfHhQa7M0Nhww==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.57.0': - resolution: {integrity: sha512-o/PYGPbfFbS0Sq8EEQC8YUgDMiTGvwoMejPjV2d466yJoii+BUpffGejVQN0hC5V5/GT29m1B1jL+3yruNxwDw==} + '@opentelemetry/instrumentation-ioredis@0.58.0': + resolution: {integrity: sha512-2tEJFeoM465A0FwPB0+gNvdM/xPBRIqNtC4mW+mBKy+ZKF9CWa7rEqv87OODGrigkEDpkH8Bs1FKZYbuHKCQNQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.55.0': - resolution: {integrity: sha512-JFLNhbbEGnnQrMKOYoXx0nNk5N9cPeghu4xP/oup40a7VaSeYruyOiFbg9nkbS4ZQiI8aMuRqUT3Mo4lQjKEKg==} + '@opentelemetry/instrumentation-nestjs-core@0.56.0': + resolution: {integrity: sha512-2wKd6+/nKyZVTkElTHRZAAEQ7moGqGmTIXlZvfAeV/dNA+6zbbl85JBcyeUFIYt+I42Naq5RgKtUY8fK6/GE1g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.61.2': - resolution: {integrity: sha512-l1tN4dX8Ig1bKzMu81Q1EBXWFRy9wqchXbeHDRniJsXYND5dC8u1Uhah7wz1zZta3fbBWflP2mJZcDPWNsAMRg==} + '@opentelemetry/instrumentation-pg@0.62.0': + resolution: {integrity: sha512-/ZSMRCyFRMjQVx7Wf+BIAOMEdN/XWBbAGTNLKfQgGYs1GlmdiIFkUy8Z8XGkToMpKrgZju0drlTQpqt4Ul7R6w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.208.0': - resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} + '@opentelemetry/instrumentation@0.210.0': + resolution: {integrity: sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.208.0': - resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + '@opentelemetry/otlp-exporter-base@0.210.0': + resolution: {integrity: sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.208.0': - resolution: {integrity: sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ==} + '@opentelemetry/otlp-grpc-exporter-base@0.210.0': + resolution: {integrity: sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.208.0': - resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + '@opentelemetry/otlp-transformer@0.210.0': + resolution: {integrity: sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.2.0': - resolution: {integrity: sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw==} + '@opentelemetry/propagator-b3@2.4.0': + resolution: {integrity: sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.2.0': - resolution: {integrity: sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA==} + '@opentelemetry/propagator-jaeger@2.4.0': + resolution: {integrity: sha512-t6muBL/3AMD++1EMF658C/KIpj3gfmTmftX3mEQql4KIxNGFvacCmmTtrQt9IZAJmQRfjQRCkv+vsGbQugeJIw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3869,38 +3997,38 @@ packages: resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.2.0': - resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + '@opentelemetry/resources@2.4.0': + resolution: {integrity: sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.208.0': - resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + '@opentelemetry/sdk-logs@0.210.0': + resolution: {integrity: sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.2.0': - resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + '@opentelemetry/sdk-metrics@2.4.0': + resolution: {integrity: sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.208.0': - resolution: {integrity: sha512-pbAqpZ7zTMFuTf3YecYsecsto/mheuvnK2a/jgstsE5ynWotBjgF5bnz5500W9Xl2LeUfg04WMt63TWtAgzRMw==} + '@opentelemetry/sdk-node@0.210.0': + resolution: {integrity: sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.2.0': - resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + '@opentelemetry/sdk-trace-base@2.4.0': + resolution: {integrity: sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.2.0': - resolution: {integrity: sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==} + '@opentelemetry/sdk-trace-node@2.4.0': + resolution: {integrity: sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -4000,35 +4128,35 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@photo-sphere-viewer/core@5.14.0': - resolution: {integrity: sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==} + '@photo-sphere-viewer/core@5.14.1': + resolution: {integrity: sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==} - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.0': - resolution: {integrity: sha512-Ez88sZ4sj3fONpZSortnN3gLXlvV/hn5U/88LsWtxI73YwhkZ06ZtXFYLXU4MBaJvqCbMGaR6j39uVXTWFo5rw==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.14.1': + resolution: {integrity: sha512-rZ6igEy1TEfgHB8Ak/8N0rZNYQLbNEGLVmhwNxDMWESCJ9nrNx3tJHFn7k6eZYjj9zJA73xF5YdY6XWUCpZDzg==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/video-plugin': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/video-plugin': 5.14.1 - '@photo-sphere-viewer/markers-plugin@5.14.0': - resolution: {integrity: sha512-w7txVHtLxXMS61m0EbNjgvdNXQYRh6Aa0oatft5oruKgoXLg/UlCu1mG6Btg+zrNsG05W2zl4gRM3fcWoVdneA==} + '@photo-sphere-viewer/markers-plugin@5.14.1': + resolution: {integrity: sha512-tKMrVem19sZFVQwH6IlubEIucDD2EtwxzmWClHCEojM/+ajucuTDvO2N+I6HEqJClBcNsdHAUwA/zyY6MGOu2Q==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/resolution-plugin@5.14.0': - resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==} + '@photo-sphere-viewer/resolution-plugin@5.14.1': + resolution: {integrity: sha512-OiNie5psqEFSQYCSe8wIlE8slnoh2Lk7oBGEQxJXtj/j08J5E5xg46uTmKgN+lWxQd0+LM3pgY7U7tTUqeH6ZQ==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/settings-plugin': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/settings-plugin': 5.14.1 - '@photo-sphere-viewer/settings-plugin@5.14.0': - resolution: {integrity: sha512-sMLX4hFSE2PjiP2iUmH9qUAz6GV+UN2WX1zu/D58BBWzF3+8mV+FC9l50qxruO8qvWqqLwYysHUElHnmPPtpTg==} + '@photo-sphere-viewer/settings-plugin@5.14.1': + resolution: {integrity: sha512-urVNMe/E+uffoe1Z8oMIt0e/6Wpf5mTnSJVJ65405trQKEGAIqJ57FlpxVp4UKPulstwpa1fRw5u1C1lzdanJA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/video-plugin@5.14.0': - resolution: {integrity: sha512-jWMZBNlfwYq8Lgc8ncs3ptwHR6Yk7Wl8o1BCFYhmhoRkGZFHEjoOQj7gMPXCET+3iYXQ1TsjTh4ZCW8UUOi+pg==} + '@photo-sphere-viewer/video-plugin@5.14.1': + resolution: {integrity: sha512-7yItXiD+eS/+9lgtaE9+wXSIpdYVU0kBsBN4vNtChaoJZF3JB8WUXjYLbszKp1yhwsvZ6eNxDcMBUgYdK6CrQA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 '@photostructure/tz-lookup@11.3.0': resolution: {integrity: sha512-rYGy7ETBHTnXrwbzm47e3LJPKJmzpY7zXnbZhdosNU0lTGWVqzxptSjK4qZkJ1G+Kwy4F6XStNR9ZqMsXAoASQ==} @@ -4392,32 +4520,32 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} - '@smithy/abort-controller@4.2.6': - resolution: {integrity: sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg==} + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} engines: {node: '>=18.0.0'} - '@smithy/config-resolver@4.4.4': - resolution: {integrity: sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA==} + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.19.0': - resolution: {integrity: sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w==} + '@smithy/core@3.20.7': + resolution: {integrity: sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.6': - resolution: {integrity: sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA==} + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.7': - resolution: {integrity: sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ==} + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.2.6': - resolution: {integrity: sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg==} + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.2.6': - resolution: {integrity: sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ==} + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': @@ -4428,72 +4556,72 @@ packages: resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.6': - resolution: {integrity: sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w==} + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.0': - resolution: {integrity: sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A==} + '@smithy/middleware-endpoint@4.4.8': + resolution: {integrity: sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.16': - resolution: {integrity: sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ==} + '@smithy/middleware-retry@4.4.24': + resolution: {integrity: sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.7': - resolution: {integrity: sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA==} + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.6': - resolution: {integrity: sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA==} + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.6': - resolution: {integrity: sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA==} + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.4.6': - resolution: {integrity: sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ==} + '@smithy/node-http-handler@4.4.8': + resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.6': - resolution: {integrity: sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA==} + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.6': - resolution: {integrity: sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ==} + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.6': - resolution: {integrity: sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg==} + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.6': - resolution: {integrity: sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA==} + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.6': - resolution: {integrity: sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg==} + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.4.1': - resolution: {integrity: sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA==} + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.6': - resolution: {integrity: sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA==} + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.10.1': - resolution: {integrity: sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA==} + '@smithy/smithy-client@4.10.9': + resolution: {integrity: sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==} engines: {node: '>=18.0.0'} - '@smithy/types@4.10.0': - resolution: {integrity: sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ==} + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.6': - resolution: {integrity: sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg==} + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} engines: {node: '>=18.0.0'} '@smithy/util-base64@4.3.0': @@ -4520,32 +4648,32 @@ packages: resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.15': - resolution: {integrity: sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A==} + '@smithy/util-defaults-mode-browser@4.3.23': + resolution: {integrity: sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.18': - resolution: {integrity: sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q==} + '@smithy/util-defaults-mode-node@4.2.26': + resolution: {integrity: sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.2.6': - resolution: {integrity: sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg==} + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} engines: {node: '>=18.0.0'} '@smithy/util-hex-encoding@4.2.0': resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.2.6': - resolution: {integrity: sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg==} + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.6': - resolution: {integrity: sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA==} + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.7': - resolution: {integrity: sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A==} + '@smithy/util-stream@4.5.10': + resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@4.2.0': @@ -4620,8 +4748,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@sveltejs/vite-plugin-svelte@6.2.3': - resolution: {integrity: sha512-a+uxqQ9j6Lxmq4plbGaNdM9hgDCZyxAv/yvuyF5iWoA2H5icZkqD3rdK155ZQgFLX2lc3NvahHG4OgKpYqYPiQ==} + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.0.0 @@ -4925,14 +5053,14 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@turf/boolean-point-in-polygon@7.3.1': - resolution: {integrity: sha512-BUPW63vE43LctwkgannjmEFTX1KFR/18SS7WzFahJWK1ZoP0s1jrfxGX+pi0BH/3Dd9mA71hkGKDDnj1Ndcz0g==} + '@turf/boolean-point-in-polygon@7.3.2': + resolution: {integrity: sha512-PAfPDQ0TW1+VLgZ7tReTSyZ/X41AW7/nMRQxVpY+h/aG7JomZJ779lojnODT4dWCn3IMTA3xD2dDDfVYBAQMYg==} - '@turf/helpers@7.3.1': - resolution: {integrity: sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg==} + '@turf/helpers@7.3.2': + resolution: {integrity: sha512-5HFN42rgWjSobdTMxbuq+ZdXPcqp1IbMgFYULTLCplEQM3dXhsyRFe7DCss4Eiw12iW3q6Z5UeTNVfITsE5lgA==} - '@turf/invariant@7.3.1': - resolution: {integrity: sha512-IdZJfDjIDCLH+Gu2yLFoSM7H23sdetIo5t4ET1/25X8gi3GE2XSqbZwaGjuZgNh02nisBewLqNiJs2bo+hrqZA==} + '@turf/invariant@7.3.2': + resolution: {integrity: sha512-brGmL1EFhZH/YNXhq6S+8sPWBEnmvEyxMWJO8bUNOFZyWHYiRTwxQHZM+An1blkbQ77PiEzsdNAspZqE1j7YKA==} '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -5218,8 +5346,8 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.21': - resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} '@types/luxon@3.7.1': resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} @@ -5257,17 +5385,17 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.28': - resolution: {integrity: sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==} + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} '@types/node@24.10.9': resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} - '@types/node@25.0.7': - resolution: {integrity: sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==} + '@types/node@25.0.9': + resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} - '@types/nodemailer@7.0.4': - resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==} + '@types/nodemailer@7.0.5': + resolution: {integrity: sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==} '@types/oidc-provider@9.5.0': resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} @@ -5275,8 +5403,8 @@ packages: '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} - '@types/pg-pool@2.0.6': - resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} @@ -5311,8 +5439,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/react@19.2.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -5395,63 +5523,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.52.0': - resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} + '@typescript-eslint/eslint-plugin@8.53.0': + resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.52.0 + '@typescript-eslint/parser': ^8.53.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.52.0': - resolution: {integrity: sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==} + '@typescript-eslint/parser@8.53.0': + resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.52.0': - resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} + '@typescript-eslint/project-service@8.53.0': + resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.52.0': - resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} + '@typescript-eslint/scope-manager@8.53.0': + resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.52.0': - resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} + '@typescript-eslint/tsconfig-utils@8.53.0': + resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.52.0': - resolution: {integrity: sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==} + '@typescript-eslint/type-utils@8.53.0': + resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.52.0': - resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} + '@typescript-eslint/types@8.53.0': + resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.52.0': - resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} + '@typescript-eslint/typescript-estree@8.53.0': + resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.52.0': - resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} + '@typescript-eslint/utils@8.53.0': + resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.52.0': - resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} + '@typescript-eslint/visitor-keys@8.53.0': + resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -6002,8 +6130,8 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.66.4: - resolution: {integrity: sha512-y2VRk2z7d1YNI2JQDD7iThoD0X/0iZZ3VEp8lqT5s5U0XDl9CIjXp1LQgmE9EKy6ReHtzmYXS1f328PnUbZGtQ==} + bullmq@5.66.5: + resolution: {integrity: sha512-DC1E7P03L+TfNHv+2SGxwNYvtb0oJPODWSKkWdfis0heU5zFW16vjM7fCjwlxMdGWw2w28EI3mTRfYLEHeQQSw==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -7192,6 +7320,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -7275,8 +7406,8 @@ packages: peerDependencies: eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-plugin-prettier@5.5.4: - resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -7562,8 +7693,8 @@ packages: file-source@0.6.1: resolution: {integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==} - file-type@21.2.0: - resolution: {integrity: sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg==} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} fill-range@7.1.1: @@ -7718,8 +7849,8 @@ packages: geo-coordinates-parser@1.7.4: resolution: {integrity: sha512-gVGxBW+s1csexXVMf5bIwz3TH9n4sCEglOOOqmrPk8YazUI5f79jCowKjTw05m/0h1//3+Z2m/nv8IIozgZyUw==} - geo-tz@8.1.4: - resolution: {integrity: sha512-xayeOC05wgy6JATU/k7GFHTMfSimzL1Fi3KSzt2GqvEnP1ZFXyQ9V4VAiTrTYhZSmRr0dbchZkximSegHZNUfA==} + geo-tz@8.1.5: + resolution: {integrity: sha512-C0g6Zyo/4/wtaONcprVq6gHq4LnbheC7HXXi0nZMG8lbxqvOj8IZcTolCd0MeOmBekXnyXKKeDlh6g2o4Yy3qw==} engines: {node: '>=16'} geobuf@3.0.2: @@ -7872,8 +8003,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.1.0: - resolution: {integrity: sha512-ebvqjBqzenBk2LjzNEAzoj7yhw7rW/R2/wVevMu6Mrq3MXtcI/RUz4+ozpcOcqVLEWPqLfg2v9EAU7fFXZUUJw==} + happy-dom@20.3.0: + resolution: {integrity: sha512-5qJbkqcvR8j/a4av5IWqqIWmEGf9dt6OhGMS6qxCgjSOBGzGa5XLoqg40OyD8XNzQ+g1g2zsXi10kjfpzYH55Q==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -8208,12 +8339,8 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.8.2: - resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} - engines: {node: '>=12.22.0'} - - ioredis@5.9.0: - resolution: {integrity: sha512-T3VieIilNumOJCXI9SDgo4NnF6sZkd6XcmPi6qWtw4xqbt8nNz/ZVNiIH1L9puMTSHZh1mUWA4xKa2nWPF4NwQ==} + ioredis@5.9.1: + resolution: {integrity: sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ==} engines: {node: '>=12.22.0'} ip-address@10.1.0: @@ -8921,8 +9048,8 @@ packages: resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} engines: {node: '>=6.4.0'} - maplibre-gl@5.15.0: - resolution: {integrity: sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==} + maplibre-gl@5.16.0: + resolution: {integrity: sha512-/VDY89nr4jgLJyzmhy325cG6VUI02WkZ/UfVuDbG/piXzo6ODnM+omDFIwWY8tsEsBG26DNDmNMn3Y2ikHsBiA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} mark.js@8.11.1: @@ -9378,6 +9505,10 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -9821,30 +9952,30 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - pg-cloudflare@1.2.7: - resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.10.0: + resolution: {integrity: sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.10.1: - resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} peerDependencies: pg: '>=8.0' - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.16.3: - resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + pg@8.17.1: + resolution: {integrity: sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -10380,8 +10511,8 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} engines: {node: '>=0.10.0'} postgres-date@1.0.7: @@ -10406,8 +10537,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} prettier-plugin-organize-imports@4.3.0: @@ -10420,8 +10551,8 @@ packages: vue-tsc: optional: true - prettier-plugin-sort-json@4.1.1: - resolution: {integrity: sha512-uJ49wCzwJ/foKKV4tIPxqi4jFFvwUzw4oACMRG2dcmDhBKrxBv0L2wSKkAqHCmxKCvj0xcCZS4jO2kSJO/tRJw==} + prettier-plugin-sort-json@4.2.0: + resolution: {integrity: sha512-jK1w3/7otTvHtv1eoLji2U9mEoOGeyl7QQQ/afLnjht1YtRLSUUk8o0rIIC/HUVXhoGPCFe4SVZbRGYjjUVgvA==} engines: {node: '>=18.0.0'} peerDependencies: prettier: ^3.0.0 @@ -10432,8 +10563,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.0: + resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} engines: {node: '>=14'} hasBin: true @@ -10499,6 +10630,10 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} + engines: {node: '>=12.0.0'} + protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -11222,8 +11357,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-formatter@15.6.12: - resolution: {integrity: sha512-mkpF+RG402P66VMsnQkWewTRzDBWfu9iLbOfxaW/nAKOS/2A9MheQmcU5cmX0D0At9azrorZwpvcBRNNBozACQ==} + sql-formatter@15.7.0: + resolution: {integrity: sha512-o2yiy7fYXK1HvzA8P6wwj8QSuwG3e/XcpWht/jIxkQX99c0SVPw0OXdLSV9fHASPiYB09HLA0uq8hokGydi/QA==} hasBin: true srcset@4.0.0: @@ -11497,8 +11632,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.11: - resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} systeminformation@5.23.8: @@ -11568,10 +11703,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} @@ -11834,8 +11971,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.52.0: - resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==} + typescript-eslint@8.53.0: + resolution: {integrity: sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -11849,8 +11986,8 @@ packages: ua-is-frozen@0.1.2: resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - ua-parser-js@2.0.7: - resolution: {integrity: sha512-CFdHVHr+6YfbktNZegH3qbYvYgC7nRNEUm2tk7nSFXSODUu4tDBpaFpP1jdXBUOKKwapVlWRfTtS8bCPzsQ47w==} + ua-parser-js@2.0.8: + resolution: {integrity: sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==} hasBin: true ufo@1.6.2: @@ -12093,8 +12230,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-tsconfig-paths@6.0.3: - resolution: {integrity: sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg==} + vite-tsconfig-paths@6.0.4: + resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} peerDependencies: vite: '*' peerDependenciesMeta: @@ -12217,8 +12354,8 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} wbuf@1.7.3: @@ -12286,8 +12423,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.103.0: - resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -12390,6 +12527,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -12757,15 +12898,15 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-locate-window': 3.893.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-locate-window': 3.965.2 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.969.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -12774,383 +12915,383 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.969.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-sesv2@3.952.0': + '@aws-sdk/client-sesv2@3.971.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/credential-provider-node': 3.952.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/signature-v4-multi-region': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/core': 3.19.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/hash-node': 4.2.6 - '@smithy/invalid-dependency': 4.2.6 - '@smithy/middleware-content-length': 4.2.6 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-retry': 4.4.16 - '@smithy/middleware-serde': 4.2.7 - '@smithy/middleware-stack': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/node-http-handler': 4.4.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/credential-provider-node': 3.971.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/signature-v4-multi-region': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.7 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.15 - '@smithy/util-defaults-mode-node': 4.2.18 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 + '@smithy/util-defaults-mode-browser': 4.3.23 + '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.948.0': + '@aws-sdk/client-sso@3.971.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/core': 3.19.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/hash-node': 4.2.6 - '@smithy/invalid-dependency': 4.2.6 - '@smithy/middleware-content-length': 4.2.6 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-retry': 4.4.16 - '@smithy/middleware-serde': 4.2.7 - '@smithy/middleware-stack': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/node-http-handler': 4.4.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.7 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.15 - '@smithy/util-defaults-mode-node': 4.2.18 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 + '@smithy/util-defaults-mode-browser': 4.3.23 + '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.947.0': + '@aws-sdk/core@3.970.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@aws-sdk/xml-builder': 3.930.0 - '@smithy/core': 3.19.0 - '@smithy/node-config-provider': 4.3.6 - '@smithy/property-provider': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/signature-v4': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/xml-builder': 3.969.0 + '@smithy/core': 3.20.7 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 - '@smithy/util-middleware': 4.2.6 + '@smithy/util-middleware': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.947.0': + '@aws-sdk/credential-provider-env@3.970.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.947.0': + '@aws-sdk/credential-provider-http@3.970.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/node-http-handler': 4.4.6 - '@smithy/property-provider': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/util-stream': 4.5.7 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.952.0': + '@aws-sdk/credential-provider-ini@3.971.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/credential-provider-env': 3.947.0 - '@aws-sdk/credential-provider-http': 3.947.0 - '@aws-sdk/credential-provider-login': 3.952.0 - '@aws-sdk/credential-provider-process': 3.947.0 - '@aws-sdk/credential-provider-sso': 3.952.0 - '@aws-sdk/credential-provider-web-identity': 3.952.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/credential-provider-imds': 4.2.6 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/credential-provider-env': 3.970.0 + '@aws-sdk/credential-provider-http': 3.970.0 + '@aws-sdk/credential-provider-login': 3.971.0 + '@aws-sdk/credential-provider-process': 3.970.0 + '@aws-sdk/credential-provider-sso': 3.971.0 + '@aws-sdk/credential-provider-web-identity': 3.971.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.952.0': + '@aws-sdk/credential-provider-login@3.971.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.952.0': + '@aws-sdk/credential-provider-node@3.971.0': dependencies: - '@aws-sdk/credential-provider-env': 3.947.0 - '@aws-sdk/credential-provider-http': 3.947.0 - '@aws-sdk/credential-provider-ini': 3.952.0 - '@aws-sdk/credential-provider-process': 3.947.0 - '@aws-sdk/credential-provider-sso': 3.952.0 - '@aws-sdk/credential-provider-web-identity': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/credential-provider-imds': 4.2.6 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/credential-provider-env': 3.970.0 + '@aws-sdk/credential-provider-http': 3.970.0 + '@aws-sdk/credential-provider-ini': 3.971.0 + '@aws-sdk/credential-provider-process': 3.970.0 + '@aws-sdk/credential-provider-sso': 3.971.0 + '@aws-sdk/credential-provider-web-identity': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.947.0': + '@aws-sdk/credential-provider-process@3.970.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.952.0': + '@aws-sdk/credential-provider-sso@3.971.0': dependencies: - '@aws-sdk/client-sso': 3.948.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/token-providers': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/client-sso': 3.971.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/token-providers': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.952.0': + '@aws-sdk/credential-provider-web-identity@3.971.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/middleware-host-header@3.936.0': + '@aws-sdk/middleware-host-header@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.936.0': + '@aws-sdk/middleware-logger@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.948.0': + '@aws-sdk/middleware-recursion-detection@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@aws/lambda-invoke-store': 0.2.2 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.947.0': + '@aws-sdk/middleware-sdk-s3@3.970.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-arn-parser': 3.893.0 - '@smithy/core': 3.19.0 - '@smithy/node-config-provider': 4.3.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/signature-v4': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-arn-parser': 3.968.0 + '@smithy/core': 3.20.7 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 '@smithy/util-config-provider': 4.2.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-stream': 4.5.7 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.947.0': + '@aws-sdk/middleware-user-agent@3.970.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@smithy/core': 3.19.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@smithy/core': 3.20.7 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.952.0': + '@aws-sdk/nested-clients@3.971.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/core': 3.19.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/hash-node': 4.2.6 - '@smithy/invalid-dependency': 4.2.6 - '@smithy/middleware-content-length': 4.2.6 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-retry': 4.4.16 - '@smithy/middleware-serde': 4.2.7 - '@smithy/middleware-stack': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/node-http-handler': 4.4.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.7 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.15 - '@smithy/util-defaults-mode-node': 4.2.18 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 + '@smithy/util-defaults-mode-browser': 4.3.23 + '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.936.0': + '@aws-sdk/region-config-resolver@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.947.0': + '@aws-sdk/signature-v4-multi-region@3.970.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/signature-v4': 5.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/middleware-sdk-s3': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.952.0': + '@aws-sdk/token-providers@3.971.0': dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.936.0': + '@aws-sdk/types@3.969.0': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.893.0': + '@aws-sdk/util-arn-parser@3.968.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.936.0': + '@aws-sdk/util-endpoints@3.970.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-endpoints': 3.2.6 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.893.0': + '@aws-sdk/util-locate-window@3.965.2': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.936.0': + '@aws-sdk/util-user-agent-browser@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 bowser: 2.13.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.947.0': + '@aws-sdk/util-user-agent-node@3.971.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.930.0': + '@aws-sdk/xml-builder@3.969.0': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws/lambda-invoke-store@0.2.2': {} + '@aws/lambda-invoke-store@0.2.3': {} - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.28.6': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 @@ -13160,7 +13301,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -13882,13 +14023,13 @@ snapshots: '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@babel/traverse@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -14282,26 +14423,26 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.3.1(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docsearch/core@4.3.1(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@docsearch/css@4.3.2': {} - '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': + '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: '@ai-sdk/react': 2.0.115(react@18.3.1)(zod@4.2.1) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.0)(algoliasearch@5.46.0)(search-insights@2.17.3) - '@docsearch/core': 4.3.1(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docsearch/core': 4.3.1(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docsearch/css': 4.3.2 ai: 5.0.113(zod@4.2.1) algoliasearch: 5.46.0 marked: 16.4.2 zod: 4.2.1 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 @@ -14342,24 +14483,24 @@ snapshots: '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.103.0) + babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.104.1) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.103.0) - css-loader: 6.11.0(webpack@5.103.0) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.103.0) + copy-webpack-plugin: 11.0.0(webpack@5.104.1) + css-loader: 6.11.0(webpack@5.104.1) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.104.1) cssnano: 6.1.2(postcss@8.5.6) - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.4(webpack@5.103.0) - null-loader: 4.0.1(webpack@5.103.0) + mini-css-extract-plugin: 2.9.4(webpack@5.104.1) + null-loader: 4.0.1(webpack@5.104.1) postcss: 8.5.6 - postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1) postcss-preset-env: 10.5.0(postcss@8.5.6) - terser-webpack-plugin: 5.3.16(webpack@5.103.0) + terser-webpack-plugin: 5.3.16(webpack@5.104.1) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) - webpack: 5.103.0 - webpackbar: 6.0.1(webpack@5.103.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + webpack: 5.104.1 + webpackbar: 6.0.1(webpack@5.104.1) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -14375,7 +14516,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: '@docusaurus/babel': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/bundler': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) @@ -14384,7 +14525,7 @@ snapshots: '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -14399,7 +14540,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.2 html-tags: 3.3.1 - html-webpack-plugin: 5.6.5(webpack@5.103.0) + html-webpack-plugin: 5.6.5(webpack@5.104.1) leven: 3.1.0 lodash: 4.17.21 open: 8.4.2 @@ -14409,7 +14550,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.103.0) + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.104.1) react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) @@ -14418,9 +14559,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.103.0 + webpack: 5.104.1 webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.2(webpack@5.103.0) + webpack-dev-server: 5.2.2(webpack@5.104.1) webpack-merge: 6.0.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14460,7 +14601,7 @@ snapshots: '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) fs-extra: 11.3.2 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -14476,9 +14617,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) vfile: 6.0.3 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@swc/core' - esbuild @@ -14490,7 +14631,7 @@ snapshots: dependencies: '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.8 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.3.1 @@ -14504,13 +14645,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14526,7 +14667,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14545,13 +14686,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14566,7 +14707,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14585,9 +14726,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14596,7 +14737,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14615,9 +14756,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14642,9 +14783,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 @@ -14670,9 +14811,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -14696,9 +14837,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.12 @@ -14723,9 +14864,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -14749,9 +14890,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14780,9 +14921,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14791,7 +14932,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14810,22 +14951,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14852,25 +14993,25 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 react: 18.3.1 - '@docusaurus/theme-classic@3.9.2(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/theme-classic@3.9.2(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@18.3.1) clsx: 2.1.1 infima: 0.2.0-alpha.45 lodash: 4.17.21 @@ -14902,15 +15043,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.8 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -14926,11 +15067,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/theme-mermaid@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) mermaid: 11.12.2 @@ -14956,13 +15097,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docsearch/react': 4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docsearch/react': 4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15009,14 +15150,14 @@ snapshots: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 '@types/mdast': 4.0.4 - '@types/react': 19.2.7 + '@types/react': 19.2.8 commander: 5.1.0 joi: 17.13.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -15064,7 +15205,7 @@ snapshots: '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) fs-extra: 11.3.2 github-slugger: 1.5.0 globby: 11.1.0 @@ -15077,9 +15218,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@swc/core' - esbuild @@ -15376,7 +15517,7 @@ snapshots: '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.7.4 + prettier: 3.8.0 '@floating-ui/core@1.7.3': dependencies: @@ -15452,10 +15593,10 @@ snapshots: dependencies: '@fortawesome/fontawesome-common-types': 7.1.0 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) lodash: 4.17.21 '@grpc/grpc-js@1.14.3': @@ -15604,12 +15745,12 @@ snapshots: dependencies: svelte: 5.46.4 - '@immich/ui@0.58.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)': + '@immich/ui@0.59.0(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.4) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 - bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) + bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) luxon: 3.7.2 simple-icons: 16.4.0 svelte: 5.46.4 @@ -15622,6 +15763,8 @@ snapshots: '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.3': {} + '@inquirer/checkbox@4.3.2(@types/node@24.10.9)': dependencies: '@inquirer/ansi': 1.0.2 @@ -15632,6 +15775,15 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/checkbox@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/confirm@5.1.21(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15639,6 +15791,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/confirm@6.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/core@10.3.2(@types/node@24.10.9)': dependencies: '@inquirer/ansi': 1.0.2 @@ -15652,6 +15811,18 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/core@11.1.1(@types/node@24.10.9)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) + cli-width: 4.1.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + wrap-ansi: 9.0.2 + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/editor@4.2.23(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15660,6 +15831,14 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/editor@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/external-editor': 2.0.3(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/expand@4.0.23(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15668,6 +15847,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/expand@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/external-editor@1.0.3(@types/node@24.10.9)': dependencies: chardet: 2.1.1 @@ -15675,8 +15861,17 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/external-editor@2.0.3(@types/node@24.10.9)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.3': {} + '@inquirer/input@4.3.1(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15684,6 +15879,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/input@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/number@3.0.23(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15691,6 +15893,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/number@4.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/password@4.0.23(@types/node@24.10.9)': dependencies: '@inquirer/ansi': 1.0.2 @@ -15699,18 +15908,11 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 - '@inquirer/prompts@7.10.1(@types/node@24.10.9)': + '@inquirer/password@5.0.4(@types/node@24.10.9)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@24.10.9) - '@inquirer/confirm': 5.1.21(@types/node@24.10.9) - '@inquirer/editor': 4.2.23(@types/node@24.10.9) - '@inquirer/expand': 4.0.23(@types/node@24.10.9) - '@inquirer/input': 4.3.1(@types/node@24.10.9) - '@inquirer/number': 3.0.23(@types/node@24.10.9) - '@inquirer/password': 4.0.23(@types/node@24.10.9) - '@inquirer/rawlist': 4.1.11(@types/node@24.10.9) - '@inquirer/search': 3.2.2(@types/node@24.10.9) - '@inquirer/select': 4.4.2(@types/node@24.10.9) + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) optionalDependencies: '@types/node': 24.10.9 @@ -15729,6 +15931,21 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/prompts@8.2.0(@types/node@24.10.9)': + dependencies: + '@inquirer/checkbox': 5.0.4(@types/node@24.10.9) + '@inquirer/confirm': 6.0.4(@types/node@24.10.9) + '@inquirer/editor': 5.0.4(@types/node@24.10.9) + '@inquirer/expand': 5.0.4(@types/node@24.10.9) + '@inquirer/input': 5.0.4(@types/node@24.10.9) + '@inquirer/number': 4.0.4(@types/node@24.10.9) + '@inquirer/password': 5.0.4(@types/node@24.10.9) + '@inquirer/rawlist': 5.2.0(@types/node@24.10.9) + '@inquirer/search': 4.1.0(@types/node@24.10.9) + '@inquirer/select': 5.0.4(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/rawlist@4.1.11(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15737,6 +15954,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/rawlist@5.2.0(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/search@3.2.2(@types/node@24.10.9)': dependencies: '@inquirer/core': 10.3.2(@types/node@24.10.9) @@ -15746,6 +15970,14 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/search@4.1.0(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/select@4.4.2(@types/node@24.10.9)': dependencies: '@inquirer/ansi': 1.0.2 @@ -15756,16 +15988,27 @@ snapshots: optionalDependencies: '@types/node': 24.10.9 + '@inquirer/select@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + '@inquirer/type@3.0.10(@types/node@24.10.9)': optionalDependencies: '@types/node': 24.10.9 + '@inquirer/type@4.0.3(@types/node@24.10.9)': + optionalDependencies: + '@types/node': 24.10.9 + '@internationalized/date@3.10.0': dependencies: '@swc/helpers': 0.5.17 - '@ioredis/commands@1.4.0': {} - '@ioredis/commands@1.5.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -15891,8 +16134,8 @@ snapshots: '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) browserslist: 4.28.1 transitivePeerDependencies: - eslint @@ -16057,10 +16300,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1)': + '@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.2.7 + '@types/react': 19.2.8 react: 18.3.1 '@mermaid-js/parser@0.6.3': @@ -16089,39 +16332,39 @@ snapshots: '@namnode/store@0.1.0': {} - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(bullmq@5.66.5)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.66.4 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.66.5 tslib: 2.8.1 - '@nestjs/cli@11.0.14(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9)': + '@nestjs/cli@11.0.15(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics-cli': 19.2.19(@types/node@24.10.9)(chokidar@4.0.3) - '@inquirer/prompts': 7.10.1(@types/node@24.10.9) + '@inquirer/prompts': 8.2.0(@types/node@24.10.9) '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17))) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))) glob: 13.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.15.8(@swc/helpers@0.5.17) @@ -16131,9 +16374,9 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.2.0 + file-type: 21.3.0 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -16146,9 +16389,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -16158,21 +16401,21 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) - '@nestjs/websockets': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/websockets': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/platform-express@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@nestjs/platform-express@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.2.1 multer: 2.0.2 @@ -16181,10 +16424,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -16193,10 +16436,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@nestjs/schedule@6.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.3.5 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': @@ -16210,12 +16453,12 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) js-yaml: 4.1.1 lodash: 4.17.21 path-to-regexp: 8.3.0 @@ -16225,25 +16468,25 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/testing@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)': + '@nestjs/testing@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/websockets@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -16279,124 +16522,130 @@ snapshots: '@oazapfts/runtime@1.1.0': {} - '@opentelemetry/api-logs@0.208.0': + '@opentelemetry/api-logs@0.210.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/configuration@0.210.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + yaml: 2.8.2 + + '@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/host-metrics@0.36.2(@opentelemetry/api@1.9.0)': @@ -16404,158 +16653,160 @@ snapshots: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.58.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.55.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.56.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.61.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.62.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 - '@types/pg-pool': 2.0.6 + '@types/pg-pool': 2.0.7 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/api-logs': 0.210.0 import-in-the-middle: 2.0.0 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - protobufjs: 7.5.4 + '@opentelemetry/api-logs': 0.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + protobufjs: 8.0.0 - '@opentelemetry/propagator-b3@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common@0.38.2': {} - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.210.0 + '@opentelemetry/configuration': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions@1.38.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2@2.3.1': dependencies: @@ -16622,32 +16873,32 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@photo-sphere-viewer/core@5.14.0': + '@photo-sphere-viewer/core@5.14.1': dependencies: three: 0.179.1 - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': + '@photo-sphere-viewer/equirectangular-video-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))': dependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/video-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/video-plugin': 5.14.1(@photo-sphere-viewer/core@5.14.1) three: 0.182.0 - '@photo-sphere-viewer/markers-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/markers-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/resolution-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': + '@photo-sphere-viewer/resolution-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/settings-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))': dependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/settings-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/settings-plugin': 5.14.1(@photo-sphere-viewer/core@5.14.1) - '@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/settings-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 three: 0.182.0 '@photostructure/tz-lookup@11.3.0': {} @@ -16789,7 +17040,7 @@ snapshots: '@react-email/render@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: html-to-text: 9.0.5 - prettier: 3.7.4 + prettier: 3.8.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-promise-suspense: 0.3.4 @@ -16936,59 +17187,59 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 - '@smithy/abort-controller@4.2.6': + '@smithy/abort-controller@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/config-resolver@4.4.4': + '@smithy/config-resolver@4.4.6': dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 '@smithy/util-config-provider': 4.2.0 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/core@3.19.0': + '@smithy/core@3.20.7': dependencies: - '@smithy/middleware-serde': 4.2.7 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-stream': 4.5.7 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 '@smithy/util-utf8': 4.2.0 '@smithy/uuid': 1.1.0 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.6': + '@smithy/credential-provider-imds@4.2.8': dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/property-provider': 4.2.6 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.7': + '@smithy/fetch-http-handler@5.3.9': dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/querystring-builder': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 tslib: 2.8.1 - '@smithy/hash-node@4.2.6': + '@smithy/hash-node@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 '@smithy/util-buffer-from': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/invalid-dependency@4.2.6': + '@smithy/invalid-dependency@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': @@ -16999,120 +17250,120 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/middleware-content-length@4.2.6': + '@smithy/middleware-content-length@4.2.8': dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.0': + '@smithy/middleware-endpoint@4.4.8': dependencies: - '@smithy/core': 3.19.0 - '@smithy/middleware-serde': 4.2.7 - '@smithy/node-config-provider': 4.3.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-middleware': 4.2.6 + '@smithy/core': 3.20.7 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.16': + '@smithy/middleware-retry@4.4.24': dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/service-error-classification': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/uuid': 1.1.0 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.7': + '@smithy/middleware-serde@4.2.9': dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/middleware-stack@4.2.6': + '@smithy/middleware-stack@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/node-config-provider@4.3.6': + '@smithy/node-config-provider@4.3.8': dependencies: - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.4.6': + '@smithy/node-http-handler@4.4.8': dependencies: - '@smithy/abort-controller': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/querystring-builder': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/property-provider@4.2.6': + '@smithy/property-provider@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/protocol-http@5.3.6': + '@smithy/protocol-http@5.3.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/querystring-builder@4.2.6': + '@smithy/querystring-builder@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 '@smithy/util-uri-escape': 4.2.0 tslib: 2.8.1 - '@smithy/querystring-parser@4.2.6': + '@smithy/querystring-parser@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/service-error-classification@4.2.6': + '@smithy/service-error-classification@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 - '@smithy/shared-ini-file-loader@4.4.1': + '@smithy/shared-ini-file-loader@4.4.3': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/signature-v4@5.3.6': + '@smithy/signature-v4@5.3.8': dependencies: '@smithy/is-array-buffer': 4.2.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-middleware': 4.2.6 + '@smithy/util-middleware': 4.2.8 '@smithy/util-uri-escape': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/smithy-client@4.10.1': + '@smithy/smithy-client@4.10.9': dependencies: - '@smithy/core': 3.19.0 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-stack': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - '@smithy/util-stream': 4.5.7 + '@smithy/core': 3.20.7 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@smithy/types@4.10.0': + '@smithy/types@4.12.0': dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.2.6': + '@smithy/url-parser@4.2.8': dependencies: - '@smithy/querystring-parser': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 '@smithy/util-base64@4.3.0': @@ -17143,49 +17394,49 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.15': + '@smithy/util-defaults-mode-browser@4.3.23': dependencies: - '@smithy/property-provider': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.18': + '@smithy/util-defaults-mode-node@4.2.26': dependencies: - '@smithy/config-resolver': 4.4.4 - '@smithy/credential-provider-imds': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/property-provider': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-endpoints@3.2.6': + '@smithy/util-endpoints@3.2.8': dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 '@smithy/util-hex-encoding@4.2.0': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.2.6': + '@smithy/util-middleware@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-retry@4.2.6': + '@smithy/util-retry@4.2.8': dependencies: - '@smithy/service-error-classification': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-stream@4.5.7': + '@smithy/util-stream@4.5.10': dependencies: - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/node-http-handler': 4.4.6 - '@smithy/types': 4.10.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 '@smithy/util-buffer-from': 4.2.0 '@smithy/util-hex-encoding': 4.2.0 @@ -17229,29 +17480,29 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) magic-string: 0.30.21 sharp: 0.34.5 svelte: 5.46.4 svelte-parse-markup: 0.1.5(svelte@5.46.4) - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-imagetools: 9.0.2(rollup@4.55.1) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -17264,29 +17515,29 @@ snapshots: set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 5.46.4 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@opentelemetry/api': 1.9.0 typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 svelte: 5.46.4 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 svelte: 5.46.4 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -17505,16 +17756,16 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -17536,14 +17787,14 @@ snapshots: dependencies: svelte: 5.46.4 - '@testing-library/svelte@5.3.1(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@testing-library/svelte@5.3.1(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/svelte-core': 1.0.0(svelte@5.46.4) svelte: 5.46.4 optionalDependencies: - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -17563,22 +17814,22 @@ snapshots: '@trysound/sax@0.2.0': {} - '@turf/boolean-point-in-polygon@7.3.1': + '@turf/boolean-point-in-polygon@7.3.2': dependencies: - '@turf/helpers': 7.3.1 - '@turf/invariant': 7.3.1 + '@turf/helpers': 7.3.2 + '@turf/invariant': 7.3.2 '@types/geojson': 7946.0.16 point-in-polygon-hao: 1.2.4 tslib: 2.8.1 - '@turf/helpers@7.3.1': + '@turf/helpers@7.3.2': dependencies: '@types/geojson': 7946.0.16 tslib: 2.8.1 - '@turf/invariant@7.3.1': + '@turf/invariant@7.3.2': dependencies: - '@turf/helpers': 7.3.1 + '@turf/helpers': 7.3.2 '@types/geojson': 7946.0.16 tslib: 2.8.1 @@ -17934,9 +18185,9 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.21 + '@types/lodash': 4.17.23 - '@types/lodash@4.17.21': {} + '@types/lodash@4.17.23': {} '@types/luxon@3.7.1': {} @@ -17974,7 +18225,7 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.28': + '@types/node@20.19.30': dependencies: undici-types: 6.21.0 @@ -17982,14 +18233,14 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/node@25.0.7': + '@types/node@25.0.9': dependencies: undici-types: 7.16.0 optional: true - '@types/nodemailer@7.0.4': + '@types/nodemailer@7.0.5': dependencies: - '@aws-sdk/client-sesv2': 3.952.0 + '@aws-sdk/client-sesv2': 3.971.0 '@types/node': 24.10.9 transitivePeerDependencies: - aws-crt @@ -18002,20 +18253,20 @@ snapshots: '@types/parse5@5.0.3': {} - '@types/pg-pool@2.0.6': + '@types/pg-pool@2.0.7': dependencies: '@types/pg': 8.16.0 '@types/pg@8.15.6': dependencies: '@types/node': 24.10.9 - pg-protocol: 1.10.3 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/pg@8.16.0': dependencies: '@types/node': 24.10.9 - pg-protocol: 1.10.3 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/picomatch@4.0.2': {} @@ -18037,21 +18288,21 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.8 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.8 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.8 - '@types/react@19.2.7': + '@types/react@19.2.8': dependencies: csstype: 3.2.3 @@ -18155,14 +18406,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.0 eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -18171,41 +18422,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) - '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.52.0': + '@typescript-eslint/scope-manager@8.53.0': dependencies: - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/visitor-keys': 8.53.0 - '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) @@ -18213,14 +18464,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.52.0': {} + '@typescript-eslint/types@8.53.0': {} - '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/visitor-keys': 8.52.0 + '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 @@ -18230,27 +18481,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.52.0': + '@typescript-eslint/visitor-keys@8.53.0': dependencies: - '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/types': 8.53.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.5': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -18265,11 +18516,11 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -18284,7 +18535,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -18304,13 +18555,13 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -18680,12 +18931,12 @@ snapshots: b4a@1.7.3: {} - babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.104.1): dependencies: '@babel/core': 7.28.5 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.103.0 + webpack: 5.104.1 babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -18786,15 +19037,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) + runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) svelte: 5.46.4 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -18909,10 +19160,10 @@ snapshots: builtin-modules@5.0.0: {} - bullmq@5.66.4: + bullmq@5.66.5: dependencies: cron-parser: 4.9.0 - ioredis: 5.8.2 + ioredis: 5.9.1 msgpackr: 1.11.5 node-abort-controller: 3.1.1 semver: 7.7.3 @@ -19352,7 +19603,7 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - copy-webpack-plugin@11.0.0(webpack@5.103.0): + copy-webpack-plugin@11.0.0(webpack@5.104.1): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -19360,7 +19611,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.103.0 + webpack: 5.104.1 core-js-compat@3.47.0: dependencies: @@ -19444,7 +19695,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.103.0): + css-loader@6.11.0(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -19455,9 +19706,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.3 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.103.0): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.6) @@ -19465,7 +19716,7 @@ snapshots: postcss: 8.5.6 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.103.0 + webpack: 5.104.1 optionalDependencies: clean-css: 5.3.3 @@ -19957,9 +20208,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -20141,6 +20392,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -20312,12 +20565,12 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.3 - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0): dependencies: eslint: 9.39.2(jiti@2.6.1) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 + prettier: 3.8.0 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) @@ -20707,17 +20960,17 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.103.0): + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 file-source@0.6.1: dependencies: stream-source: 0.3.5 - file-type@21.2.0: + file-type@21.3.0: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.4 @@ -20796,9 +21049,9 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 chalk: 4.1.2 chokidar: 4.0.3 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -20811,7 +21064,7 @@ snapshots: semver: 7.7.3 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) form-data-encoder@2.1.4: {} @@ -20891,10 +21144,10 @@ snapshots: geo-coordinates-parser@1.7.4: {} - geo-tz@8.1.4: + geo-tz@8.1.5: dependencies: - '@turf/boolean-point-in-polygon': 7.3.1 - '@turf/helpers': 7.3.1 + '@turf/boolean-point-in-polygon': 7.3.2 + '@turf/helpers': 7.3.2 geobuf: 3.0.2 pbf: 3.3.0 @@ -21070,9 +21323,9 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.1.0: + happy-dom@20.3.0: dependencies: - '@types/node': 20.19.28 + '@types/node': 20.19.30 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 whatwg-mimetype: 3.0.0 @@ -21318,7 +21571,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.5(webpack@5.103.0): + html-webpack-plugin@5.6.5(webpack@5.104.1): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -21326,7 +21579,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.0 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 htmlparser2@6.1.0: dependencies: @@ -21550,21 +21803,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@5.8.2: - dependencies: - '@ioredis/commands': 1.4.0 - cluster-key-slot: 1.1.2 - debug: 4.4.3 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - - ioredis@5.9.0: + ioredis@5.9.1: dependencies: '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 @@ -22283,7 +22522,7 @@ snapshots: tinyqueue: 2.0.3 vt-pbf: 3.1.3 - maplibre-gl@5.15.0: + maplibre-gl@5.16.0: dependencies: '@mapbox/geojson-rewind': 0.5.2 '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -22915,11 +23154,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.103.0): + mini-css-extract-plugin@2.9.4(webpack@5.104.1): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.103.0 + webpack: 5.104.1 minimalistic-assert@1.0.1: {} @@ -23054,6 +23293,8 @@ snapshots: mute-stream@2.0.0: {} + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -23086,12 +23327,12 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.12 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -23100,25 +23341,25 @@ snapshots: - '@types/node' - typescript - nestjs-cls@5.4.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@5.4.3(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(kysely@0.28.2)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(kysely@0.28.2)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.2 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@7.0.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11): + nestjs-otel@7.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12): dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': 1.9.0 '@opentelemetry/host-metrics': 0.36.2(@opentelemetry/api@1.9.0) response-time: 2.3.4 @@ -23227,11 +23468,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.103.0): + null-loader@4.0.1(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 nwsapi@2.2.23: optional: true @@ -23438,7 +23679,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -23526,36 +23767,36 @@ snapshots: peberminta@0.9.0: {} - pg-cloudflare@1.2.7: + pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.9.1: {} + pg-connection-string@2.10.0: {} pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.16.3): + pg-pool@3.11.0(pg@8.17.1): dependencies: - pg: 8.16.3 + pg: 8.17.1 - pg-protocol@1.10.3: {} + pg-protocol@1.11.0: {} pg-types@2.2.0: dependencies: pg-int8: 1.0.1 postgres-array: 2.0.0 - postgres-bytea: 1.0.0 + postgres-bytea: 1.0.1 postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.16.3: + pg@8.17.1: dependencies: - pg-connection-string: 2.9.1 - pg-pool: 3.10.1(pg@8.16.3) - pg-protocol: 1.10.3 + pg-connection-string: 2.10.0 + pg-pool: 3.11.0(pg@8.17.1) + pg-protocol: 1.11.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.2.7 + pg-cloudflare: 1.3.0 pgpass@1.0.5: dependencies: @@ -23793,13 +24034,13 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0): + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1): dependencies: cosmiconfig: 8.3.6(typescript@5.9.3) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.3 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - typescript @@ -24105,7 +24346,7 @@ snapshots: postgres-array@2.0.0: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.1: {} postgres-date@1.0.7: {} @@ -24121,25 +24362,25 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.8.0)(typescript@5.9.3): dependencies: - prettier: 3.7.4 + prettier: 3.8.0 typescript: 5.9.3 - prettier-plugin-sort-json@4.1.1(prettier@3.7.4): + prettier-plugin-sort-json@4.2.0(prettier@3.8.0): dependencies: - prettier: 3.7.4 + prettier: 3.8.0 - prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.4): + prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.46.4): dependencies: - prettier: 3.7.4 + prettier: 3.8.0 svelte: 5.46.4 - prettier@3.7.4: {} + prettier@3.8.0: {} pretty-error@4.0.0: dependencies: @@ -24217,6 +24458,21 @@ snapshots: '@types/node': 24.10.9 long: 5.3.2 + protobufjs@8.0.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.10.9 + long: 5.3.2 + protocol-buffers-schema@3.6.0: {} proxy-addr@2.0.7: @@ -24294,11 +24550,11 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.103.0): + raw-loader@4.0.2(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 rc@1.2.8: dependencies: @@ -24351,11 +24607,11 @@ snapshots: dependencies: react: 18.3.1 - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.103.0): + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.104.1): dependencies: '@babel/runtime': 7.28.6 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - webpack: 5.103.0 + webpack: 5.104.1 react-promise-suspense@0.3.4: dependencies: @@ -24749,14 +25005,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4): + runed@0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.46.4 optionalDependencies: - '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) rw@1.3.3: {} @@ -25198,7 +25454,7 @@ snapshots: sprintf-js@1.0.3: {} - sql-formatter@15.6.12: + sql-formatter@15.7.0: dependencies: argparse: 2.0.1 nearley: 2.20.1 @@ -25472,7 +25728,7 @@ snapshots: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 - maplibre-gl: 5.15.0 + maplibre-gl: 5.16.0 pmtiles: 3.2.1 svelte: 5.46.4 @@ -25488,10 +25744,10 @@ snapshots: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) + runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) style-to-object: 1.0.14 svelte: 5.46.4 transitivePeerDependencies: @@ -25542,7 +25798,7 @@ snapshots: symbol-tree@3.2.4: optional: true - synckit@0.11.11: + synckit@0.11.12: dependencies: '@pkgr/core': 0.2.9 @@ -25657,25 +25913,25 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17))): + terser-webpack-plugin@5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) optionalDependencies: '@swc/core': 1.15.8(@swc/helpers@0.5.17) - terser-webpack-plugin@5.3.16(webpack@5.103.0): + terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.103.0 + webpack: 5.104.1 terser@5.44.1: dependencies: @@ -25917,12 +26173,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -25932,7 +26188,7 @@ snapshots: ua-is-frozen@0.1.2: {} - ua-parser-js@2.0.7: + ua-parser-js@2.0.8: dependencies: detect-europe-js: 0.1.2 is-standalone-pwa: 0.1.1 @@ -26104,14 +26360,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 optionalDependencies: - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) url-parse@1.5.10: dependencies: @@ -26223,13 +26479,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -26244,7 +26500,7 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 @@ -26273,7 +26529,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -26282,7 +26538,7 @@ snapshots: rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 25.0.9 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -26291,15 +26547,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitefu@1.1.1(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -26327,7 +26583,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.9 - happy-dom: 20.1.0 + happy-dom: 20.3.0 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti @@ -26343,7 +26599,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -26371,7 +26627,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.9 - happy-dom: 20.1.0 + happy-dom: 20.3.0 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti @@ -26387,11 +26643,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -26409,13 +26665,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.0.7 - happy-dom: 20.1.0 + '@types/node': 25.0.9 + happy-dom: 20.3.0 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti @@ -26466,7 +26722,7 @@ snapshots: xml-name-validator: 5.0.0 optional: true - watchpack@2.4.4: + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -26506,7 +26762,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(webpack@5.103.0): + webpack-dev-middleware@7.4.5(webpack@5.104.1): dependencies: colorette: 2.0.20 memfs: 4.51.1 @@ -26515,9 +26771,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 - webpack-dev-server@5.2.2(webpack@5.103.0): + webpack-dev-server@5.2.2(webpack@5.104.1): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -26545,10 +26801,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.103.0) + webpack-dev-middleware: 7.4.5(webpack@5.104.1) ws: 8.19.0 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - bufferutil - debug @@ -26573,7 +26829,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.103.0: + webpack@5.104.1: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -26586,7 +26842,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.4 - es-module-lexer: 1.7.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -26597,15 +26853,15 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.103.0) - watchpack: 2.4.4 + terser-webpack-plugin: 5.3.16(webpack@5.104.1) + watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17)): + webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -26618,7 +26874,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.4 - es-module-lexer: 1.7.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -26629,15 +26885,15 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17))) - watchpack: 2.4.4 + terser-webpack-plugin: 5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))) + watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpackbar@6.0.1(webpack@5.103.0): + webpackbar@6.0.1(webpack@5.104.1): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -26646,7 +26902,7 @@ snapshots: markdown-table: 2.0.0 pretty-time: 1.1.0 std-env: 3.10.0 - webpack: 5.103.0 + webpack: 5.104.1 wrap-ansi: 7.0.0 websocket-driver@0.7.4: @@ -26740,6 +26996,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} write-file-atomic@3.0.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f7f22e6f44..be30451965 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,6 +11,7 @@ packages: - .github ignoredBuiltDependencies: - '@nestjs/core' + - '@parcel/watcher' - '@scarf/scarf' - '@swc/core' - canvas diff --git a/server/package.json b/server/package.json index 882c7e71ba..e84cc2f9eb 100644 --- a/server/package.json +++ b/server/package.json @@ -45,14 +45,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.208.0", - "@opentelemetry/instrumentation-http": "^0.208.0", - "@opentelemetry/instrumentation-ioredis": "^0.57.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.55.0", - "@opentelemetry/instrumentation-pg": "^0.61.0", + "@opentelemetry/exporter-prometheus": "^0.210.0", + "@opentelemetry/instrumentation-http": "^0.210.0", + "@opentelemetry/instrumentation-ioredis": "^0.58.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.56.0", + "@opentelemetry/instrumentation-pg": "^0.62.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.208.0", + "@opentelemetry/sdk-node": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", @@ -135,7 +135,7 @@ "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", "@types/multer": "^2.0.0", - "@types/node": "^24.10.4", + "@types/node": "^24.10.8", "@types/nodemailer": "^7.0.0", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index caa4ea4b6e..7d622ea23d 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -10,6 +10,7 @@ import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; import { ImmichWorker } from 'src/enum'; import { MaintenanceAuthGuard } from 'src/maintenance/maintenance-auth.guard'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { MaintenanceWorkerController } from 'src/maintenance/maintenance-worker.controller'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; @@ -21,8 +22,11 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { repositories } from 'src/repositories'; import { AppRepository } from 'src/repositories/app.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository'; import { WebsocketRepository } from 'src/repositories/websocket.repository'; @@ -103,8 +107,12 @@ export class ApiModule extends BaseModule {} providers: [ ConfigRepository, LoggingRepository, + StorageRepository, + ProcessRepository, + DatabaseRepository, SystemMetadataRepository, AppRepository, + MaintenanceHealthRepository, MaintenanceWebsocketRepository, MaintenanceWorkerService, ...commonMiddleware, @@ -116,9 +124,14 @@ export class MaintenanceModule { constructor( @Inject(IWorker) private worker: ImmichWorker, logger: LoggingRepository, + private maintenanceWorkerService: MaintenanceWorkerService, ) { logger.setAppName(this.worker); } + + async onModuleInit() { + await this.maintenanceWorkerService.init(); + } } @Module({ diff --git a/server/src/constants.ts b/server/src/constants.ts index fe0cb4a8f0..0b26ca94b7 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -141,6 +141,7 @@ export const endpointTags: Record = { [ApiTag.Assets]: 'An asset is an image or video that has been uploaded to Immich.', [ApiTag.Authentication]: 'Endpoints related to user authentication, including OAuth.', [ApiTag.AuthenticationAdmin]: 'Administrative endpoints related to authentication.', + [ApiTag.DatabaseBackups]: 'Manage backups of the Immich database.', [ApiTag.Deprecated]: 'Deprecated endpoints that are planned for removal in the next major release.', [ApiTag.Download]: 'Endpoints for downloading assets or collections of assets.', [ApiTag.Duplicates]: 'Endpoints for managing and identifying duplicate assets.', diff --git a/server/src/controllers/database-backup.controller.ts b/server/src/controllers/database-backup.controller.ts new file mode 100644 index 0000000000..737c8f3958 --- /dev/null +++ b/server/src/controllers/database-backup.controller.ts @@ -0,0 +1,101 @@ +import { Body, Controller, Delete, Get, Next, Param, Post, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { NextFunction, Response } from 'express'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { + DatabaseBackupDeleteDto, + DatabaseBackupListResponseDto, + DatabaseBackupUploadDto, +} from 'src/dtos/database-backup.dto'; +import { ApiTag, ImmichCookie, Permission } from 'src/enum'; +import { Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { LoginDetails } from 'src/services/auth.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import { sendFile } from 'src/utils/file'; +import { respondWithCookie } from 'src/utils/response'; +import { FilenameParamDto } from 'src/validation'; + +@ApiTags(ApiTag.DatabaseBackups) +@Controller('admin/database-backups') +export class DatabaseBackupController { + constructor( + private logger: LoggingRepository, + private service: DatabaseBackupService, + private maintenanceService: MaintenanceService, + ) {} + + @Get() + @Endpoint({ + summary: 'List database backups', + description: 'Get the list of the successful and failed backups', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.Maintenance, admin: true }) + listDatabaseBackups(): Promise { + return this.service.listBackups(); + } + + @Get(':filename') + @FileResponse() + @Endpoint({ + summary: 'Download database backup', + description: 'Downloads the database backup file', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.BackupDownload, admin: true }) + async downloadDatabaseBackup( + @Param() { filename }: FilenameParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ): Promise { + await sendFile(res, next, () => this.service.downloadBackup(filename), this.logger); + } + + @Delete() + @Endpoint({ + summary: 'Delete database backup', + description: 'Delete a backup by its filename', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.BackupDelete, admin: true }) + async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise { + return this.service.deleteBackup(dto.backups); + } + + @Post('start-restore') + @Endpoint({ + summary: 'Start database backup restore flow', + description: 'Put Immich into maintenance mode to restore a backup (Immich must not be configured)', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + async startDatabaseRestoreFlow( + @GetLoginDetails() loginDetails: LoginDetails, + @Res({ passthrough: true }) res: Response, + ): Promise { + const { jwt } = await this.maintenanceService.startRestoreFlow(); + return respondWithCookie(res, undefined, { + isSecure: loginDetails.isSecure, + values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }], + }); + } + + @Post('upload') + @Authenticated({ permission: Permission.BackupUpload, admin: true }) + @ApiConsumes('multipart/form-data') + @ApiBody({ description: 'Backup Upload', type: DatabaseBackupUploadDto }) + @Endpoint({ + summary: 'Upload database backup', + description: 'Uploads .sql/.sql.gz file to restore backup from', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @UseInterceptors(FileInterceptor('file')) + uploadDatabaseBackup( + @UploadedFile() + file: Express.Multer.File, + ): Promise { + return this.service.uploadBackup(file); + } +} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 5b407f0ac9..911452ffef 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -6,6 +6,7 @@ import { AssetMediaController } from 'src/controllers/asset-media.controller'; import { AssetController } from 'src/controllers/asset.controller'; import { AuthAdminController } from 'src/controllers/auth-admin.controller'; import { AuthController } from 'src/controllers/auth.controller'; +import { DatabaseBackupController } from 'src/controllers/database-backup.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { DuplicateController } from 'src/controllers/duplicate.controller'; import { FaceController } from 'src/controllers/face.controller'; @@ -47,6 +48,7 @@ export const controllers = [ AssetMediaController, AuthController, AuthAdminController, + DatabaseBackupController, DownloadController, DuplicateController, FaceController, diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index 7b2aa17582..169fec7890 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -1,9 +1,15 @@ -import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Get, Post, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceLoginDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { LoginDetails } from 'src/services/auth.service'; @@ -15,6 +21,27 @@ import { respondWithCookie } from 'src/utils/response'; export class MaintenanceController { constructor(private service: MaintenanceService) {} + @Get('status') + @Endpoint({ + summary: 'Get maintenance mode status', + description: 'Fetch information about the currently running maintenance action.', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + getMaintenanceStatus(): MaintenanceStatusResponseDto { + return this.service.getMaintenanceStatus(); + } + + @Get('detect-install') + @Endpoint({ + summary: 'Detect existing install', + description: 'Collect integrity checks and other heuristics about local data.', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.Maintenance, admin: true }) + detectPriorInstall(): Promise { + return this.service.detectPriorInstall(); + } + @Post('login') @Endpoint({ summary: 'Log into maintenance mode', @@ -38,8 +65,8 @@ export class MaintenanceController { @GetLoginDetails() loginDetails: LoginDetails, @Res({ passthrough: true }) res: Response, ): Promise { - if (dto.action === MaintenanceAction.Start) { - const { jwt } = await this.service.startMaintenance(auth.user.name); + if (dto.action !== MaintenanceAction.End) { + const { jwt } = await this.service.startMaintenance(dto, auth.user.name); return respondWithCookie(res, undefined, { isSecure: loginDetails.isSecure, values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }], diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index d688857de1..cb40d3eec4 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,7 +1,15 @@ import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { StorageAsset } from 'src/database'; -import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum'; +import { + AssetFileType, + AssetPathType, + ImageFormat, + PathType, + PersonPathType, + RawExtractedFormat, + StorageFolder, +} from 'src/enum'; import { AssetRepository } from 'src/repositories/asset.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -24,15 +32,6 @@ export interface MoveRequest { }; } -export type GeneratedImageType = - | AssetPathType.Preview - | AssetPathType.Thumbnail - | AssetPathType.FullSize - | AssetPathType.EditedPreview - | AssetPathType.EditedThumbnail - | AssetPathType.EditedFullSize; -export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo; - export type ThumbnailPathEntity = { id: string; ownerId: string }; let instance: StorageCore | null; @@ -111,8 +110,19 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`); } - static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') { - return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`); + static getImagePath( + asset: ThumbnailPathEntity, + { + fileType, + format, + isEdited, + }: { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat; isEdited: boolean }, + ) { + return StorageCore.getNestedPath( + StorageFolder.Thumbnails, + asset.ownerId, + `${asset.id}_${fileType}${isEdited ? '_edited' : ''}.${format}`, + ); } static getEncodedVideoPath(asset: ThumbnailPathEntity) { @@ -137,14 +147,14 @@ export class StorageCore { return normalizedPath.startsWith(normalizedAppMediaLocation); } - async moveAssetImage(asset: StorageAsset, pathType: GeneratedImageType, format: ImageFormat) { + async moveAssetImage(asset: StorageAsset, fileType: AssetFileType, format: ImageFormat) { const { id: entityId, files } = asset; - const oldFile = getAssetFile(files, pathType); + const oldFile = getAssetFile(files, fileType, { isEdited: false }); return this.moveFile({ entityId, - pathType, + pathType: fileType, oldPath: oldFile?.path || null, - newPath: StorageCore.getImagePath(asset, pathType, format), + newPath: StorageCore.getImagePath(asset, { fileType, format, isEdited: false }), }); } @@ -298,19 +308,19 @@ export class StorageCore { case AssetPathType.Original: { return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.FullSize: { + case AssetFileType.FullSize: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath }); } - case AssetPathType.Preview: { + case AssetFileType.Preview: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath }); } - case AssetPathType.Thumbnail: { + case AssetFileType.Thumbnail: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath }); } case AssetPathType.EncodedVideo: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); } - case AssetPathType.Sidecar: { + case AssetFileType.Sidecar: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath }); } case PersonPathType.Face: { diff --git a/server/src/database.ts b/server/src/database.ts index 95bc98bae4..60b6cc40aa 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -39,6 +39,7 @@ export type AssetFile = { id: string; type: AssetFileType; path: string; + isEdited: boolean; }; export type Library = { @@ -344,7 +345,7 @@ export const columns = { 'asset.width', 'asset.height', ], - assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], + assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type', 'asset_file.isEdited'], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authApiKey: ['api_key.id', 'api_key.permissions'], authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'], @@ -395,6 +396,7 @@ export const columns = { 'asset.libraryId', 'asset.width', 'asset.height', + 'asset.isEdited', ], syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'], syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], @@ -456,6 +458,7 @@ export const columns = { 'asset_exif.projectionType', 'asset_exif.rating', 'asset_exif.state', + 'asset_exif.tags', 'asset_exif.timeZone', ], plugin: [ @@ -479,4 +482,5 @@ export const lockableProperties = [ 'longitude', 'rating', 'timeZone', + 'tags', ] as const; diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 1607c15085..92ee3c5875 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -98,6 +98,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { @Property({ history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) resized?: boolean; + @Property({ history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') }) + isEdited!: boolean; } export type MapAsset = { @@ -137,6 +139,7 @@ export type MapAsset = { type: AssetType; width: number | null; height: number | null; + isEdited: boolean; }; export class AssetStackResponseDto { @@ -245,5 +248,6 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset resized: true, width: entity.width, height: entity.height, + isEdited: entity.isEdited, }; } diff --git a/server/src/dtos/database-backup.dto.ts b/server/src/dtos/database-backup.dto.ts new file mode 100644 index 0000000000..dc06cdc6ec --- /dev/null +++ b/server/src/dtos/database-backup.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class DatabaseBackupDto { + filename!: string; + filesize!: number; +} + +export class DatabaseBackupListResponseDto { + backups!: DatabaseBackupDto[]; +} + +export class DatabaseBackupUploadDto { + @ApiProperty({ type: 'string', format: 'binary', required: false }) + file?: any; +} + +export class DatabaseBackupDeleteDto { + @IsString({ each: true }) + backups!: string[]; +} diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index fe6960c0a4..4b8c39c55e 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,9 +1,14 @@ -import { MaintenanceAction } from 'src/enum'; +import { ValidateIf } from 'class-validator'; +import { MaintenanceAction, StorageFolder } from 'src/enum'; import { ValidateEnum, ValidateString } from 'src/validation'; export class SetMaintenanceModeDto { @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction' }) action!: MaintenanceAction; + + @ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase) + @ValidateString() + restoreBackupFilename?: string; } export class MaintenanceLoginDto { @@ -14,3 +19,26 @@ export class MaintenanceLoginDto { export class MaintenanceAuthDto { username!: string; } + +export class MaintenanceStatusResponseDto { + active!: boolean; + + @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction' }) + action!: MaintenanceAction; + + progress?: number; + task?: string; + error?: string; +} + +export class MaintenanceDetectInstallStorageFolderDto { + @ValidateEnum({ enum: StorageFolder, name: 'StorageFolder' }) + folder!: StorageFolder; + readable!: boolean; + writable!: boolean; + files!: number; +} + +export class MaintenanceDetectInstallResponseDto { + storage!: MaintenanceDetectInstallStorageFolderDto[]; +} diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 6baf3c8ac7..0d1ab0e74d 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -121,6 +121,8 @@ export class SyncAssetV1 { width!: number | null; @ApiProperty({ type: 'integer' }) height!: number | null; + @ApiProperty({ type: 'boolean' }) + isEdited!: boolean; } @ExtraModel() diff --git a/server/src/enum.ts b/server/src/enum.ts index 6f2f55f609..179ac20b80 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -45,9 +45,6 @@ export enum AssetFileType { Preview = 'preview', Thumbnail = 'thumbnail', Sidecar = 'sidecar', - FullSizeEdited = 'fullsize_edited', - PreviewEdited = 'preview_edited', - ThumbnailEdited = 'thumbnail_edited', } export enum AlbumUserRole { @@ -136,6 +133,11 @@ export enum Permission { ArchiveRead = 'archive.read', + BackupList = 'backup.list', + BackupDownload = 'backup.download', + BackupUpload = 'backup.upload', + BackupDelete = 'backup.delete', + DuplicateRead = 'duplicate.read', DuplicateDelete = 'duplicate.delete', @@ -380,14 +382,7 @@ export enum ManualJobName { export enum AssetPathType { Original = 'original', - FullSize = 'fullsize', - Preview = 'preview', - EditedFullSize = 'edited_fullsize', - EditedPreview = 'edited_preview', - EditedThumbnail = 'edited_thumbnail', - Thumbnail = 'thumbnail', EncodedVideo = 'encoded_video', - Sidecar = 'sidecar', } export enum PersonPathType { @@ -398,7 +393,7 @@ export enum UserPathType { Profile = 'profile', } -export type PathType = AssetPathType | PersonPathType | UserPathType; +export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType; export enum TranscodePolicy { All = 'all', @@ -726,6 +721,7 @@ export enum DatabaseLock { MediaLocation = 700, GetSystemConfig = 69, BackupDatabase = 42, + MaintenanceOperation = 621, MemoryCreation = 777, IntegrityCheck = 67, } @@ -733,6 +729,8 @@ export enum DatabaseLock { export enum MaintenanceAction { Start = 'start', End = 'end', + SelectDatabaseRestore = 'select_database_restore', + RestoreDatabase = 'restore_database', } export enum ExitCode { @@ -879,6 +877,7 @@ export enum ApiTag { Authentication = 'Authentication', AuthenticationAdmin = 'Authentication (admin)', Assets = 'Assets', + DatabaseBackups = 'Database Backups (admin)', Deprecated = 'Deprecated', Download = 'Download', Duplicates = 'Duplicates', diff --git a/server/src/main.ts b/server/src/main.ts index 47185e846f..a8e3178a43 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,11 +1,11 @@ -import { Kysely } from 'kysely'; +import { Kysely, sql } from 'kysely'; import { CommandFactory } from 'nest-commander'; import { ChildProcess, fork } from 'node:child_process'; import { dirname, join } from 'node:path'; import { Worker } from 'node:worker_threads'; import { PostgresError } from 'postgres'; import { ImmichAdminModule } from 'src/app.module'; -import { ExitCode, ImmichWorker, LogLevel, SystemMetadataKey } from 'src/enum'; +import { DatabaseLock, ExitCode, ImmichWorker, LogLevel, SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { type DB } from 'src/schema'; @@ -35,19 +35,18 @@ class Workers { if (isMaintenanceMode) { this.startWorker(ImmichWorker.Maintenance); } else { + await this.waitForFreeLock(); + for (const worker of workers) { this.startWorker(worker); } } } - /** - * Initialise a short-lived Nest application to build configuration - * @returns System configuration - */ private async isMaintenanceMode(): Promise { const { database } = new ConfigRepository().getEnv(); - const kysely = new Kysely(getKyselyConfig(database.config)); + const { log: _, ...kyselyConfig } = getKyselyConfig(database.config); + const kysely = new Kysely(kyselyConfig); const systemMetadataRepository = new SystemMetadataRepository(kysely); try { @@ -65,6 +64,32 @@ class Workers { } } + private async waitForFreeLock() { + const { database } = new ConfigRepository().getEnv(); + const kysely = new Kysely(getKyselyConfig(database.config)); + + let locked = false; + while (!locked) { + locked = await kysely.connection().execute(async (conn) => { + const { rows } = await sql<{ + pg_try_advisory_lock: boolean; + }>`SELECT pg_try_advisory_lock(${DatabaseLock.MaintenanceOperation})`.execute(conn); + + const isLocked = rows[0].pg_try_advisory_lock; + + if (isLocked) { + await sql`SELECT pg_advisory_unlock(${DatabaseLock.MaintenanceOperation})`.execute(conn); + } + + return isLocked; + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + await kysely.destroy(); + } + /** * Start an individual worker * @param name Worker diff --git a/server/src/maintenance/maintenance-health.repository.ts b/server/src/maintenance/maintenance-health.repository.ts new file mode 100644 index 0000000000..aeef93ec51 --- /dev/null +++ b/server/src/maintenance/maintenance-health.repository.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; +import { fork } from 'node:child_process'; +import { dirname, join } from 'node:path'; + +@Injectable() +export class MaintenanceHealthRepository { + checkApiHealth(): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line unicorn/prefer-module + const basePath = dirname(__filename); + const workerFile = join(basePath, '..', 'workers', `api.js`); + + const worker = fork(workerFile, [], { + execArgv: process.execArgv.filter((arg) => !arg.startsWith('--inspect')), + env: { + ...process.env, + IMMICH_HOST: '127.0.0.1', + IMMICH_PORT: '33001', + }, + stdio: ['ignore', 'pipe', 'ignore', 'ipc'], + }); + + async function checkHealth() { + try { + const response = await fetch('http://127.0.0.1:33001/api/server/config'); + const { isOnboarded } = await response.json(); + if (isOnboarded) { + resolve(); + } else { + reject(new Error('Server health check failed, no admin exists.')); + } + } catch (error) { + reject(error); + } finally { + if (worker.exitCode === null) { + worker.kill('SIGTERM'); + } + } + } + + let output = '', + alive = false; + + worker.stdout?.on('data', (data) => { + if (alive) { + return; + } + + output += data; + + if (output.includes('Immich Server is listening')) { + alive = true; + void checkHealth(); + } + }); + + worker.on('exit', reject); + worker.on('error', reject); + + setTimeout(() => { + if (worker.exitCode === null) { + worker.kill('SIGTERM'); + } + }, 20_000); + }); + } +} diff --git a/server/src/maintenance/maintenance-websocket.repository.ts b/server/src/maintenance/maintenance-websocket.repository.ts index cf04c0ad12..d13ceb083f 100644 --- a/server/src/maintenance/maintenance-websocket.repository.ts +++ b/server/src/maintenance/maintenance-websocket.repository.ts @@ -7,17 +7,24 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; +import { MaintenanceAuthDto, MaintenanceStatusResponseDto } from 'src/dtos/maintenance.dto'; import { AppRepository } from 'src/repositories/app.repository'; import { AppRestartEvent, ArgsOf } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -export const serverEvents = ['AppRestart'] as const; -export type ServerEvents = (typeof serverEvents)[number]; - -export interface ClientEventMap { - AppRestartV1: [AppRestartEvent]; +interface ServerEventMap { + AppRestart: [AppRestartEvent]; + MaintenanceStatus: [MaintenanceStatusResponseDto]; } +interface ClientEventMap { + AppRestartV1: [AppRestartEvent]; + MaintenanceStatusV1: [MaintenanceStatusResponseDto]; +} + +type AuthFn = (client: Socket) => Promise; +type StatusUpdateFn = (status: MaintenanceStatusResponseDto) => void; + @WebSocketGateway({ cors: true, path: '/api/socket.io', @@ -25,8 +32,11 @@ export interface ClientEventMap { }) @Injectable() export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { + private authFn?: AuthFn; + private statusUpdateFn?: StatusUpdateFn; + @WebSocketServer() - private websocketServer?: Server; + private server?: Server; constructor( private logger: LoggingRepository, @@ -35,10 +45,10 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa this.logger.setContext(MaintenanceWebsocketRepository.name); } - afterInit(websocketServer: Server) { + afterInit(server: Server) { this.logger.log('Initialized websocket server'); - - websocketServer.on('AppRestart', (event: ArgsOf<'AppRestart'>, ack?: (ok: 'ok') => void) => { + server.on('MaintenanceStatus', (status) => this.statusUpdateFn?.(status)); + server.on('AppRestart', (event: ArgsOf<'AppRestart'>, ack?: (ok: 'ok') => void) => { this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`); ack?.('ok'); @@ -46,20 +56,40 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa }); } + clientSend(event: T, room: string, ...data: ClientEventMap[T]) { + this.server?.to(room).emit(event, ...data); + } + clientBroadcast(event: T, ...data: ClientEventMap[T]) { - this.websocketServer?.emit(event, ...data); + this.server?.emit(event, ...data); } - serverSend(event: T, ...args: ArgsOf): void { + serverSend(event: T, ...args: ServerEventMap[T]): void { this.logger.debug(`Server event: ${event} (send)`); - this.websocketServer?.serverSideEmit(event, ...args); + this.server?.serverSideEmit(event, ...args); } - handleConnection(client: Socket) { - this.logger.log(`Websocket Connect: ${client.id}`); + async handleConnection(client: Socket) { + try { + await this.authFn!(client); + await client.join('private'); + this.logger.log(`Websocket Connect: ${client.id} (private)`); + } catch { + await client.join('public'); + this.logger.log(`Websocket Connect: ${client.id} (public)`); + } } - handleDisconnect(client: Socket) { + async handleDisconnect(client: Socket) { this.logger.log(`Websocket Disconnect: ${client.id}`); + await Promise.allSettled([client.leave('private'), client.leave('public')]); + } + + setAuthFn(fn: (client: Socket) => Promise) { + this.authFn = fn; + } + + setStatusUpdateFn(fn: (status: MaintenanceStatusResponseDto) => void) { + this.statusUpdateFn = fn; } } diff --git a/server/src/maintenance/maintenance-worker.controller.ts b/server/src/maintenance/maintenance-worker.controller.ts index e6143b771a..72527e27c0 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -1,23 +1,114 @@ -import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common'; -import { Request, Response } from 'express'; -import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; -import { ServerConfigDto } from 'src/dtos/server.dto'; -import { ImmichCookie, MaintenanceAction } from 'src/enum'; +import { + Body, + Controller, + Delete, + Get, + Next, + Param, + Post, + Req, + Res, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { NextFunction, Request, Response } from 'express'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceLoginDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; +import { ServerConfigDto, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ImmichCookie } from 'src/enum'; import { MaintenanceRoute } from 'src/maintenance/maintenance-auth.guard'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; import { GetLoginDetails } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoginDetails } from 'src/services/auth.service'; +import { sendFile } from 'src/utils/file'; import { respondWithCookie } from 'src/utils/response'; +import { FilenameParamDto } from 'src/validation'; + +import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller'; +import type { ServerController as _ServerController } from 'src/controllers/server.controller'; +import { DatabaseBackupDeleteDto, DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; @Controller() export class MaintenanceWorkerController { - constructor(private service: MaintenanceWorkerService) {} + constructor( + private logger: LoggingRepository, + private service: MaintenanceWorkerService, + ) {} + /** + * {@link _ServerController.getServerConfig } + */ @Get('server/config') - getServerConfig(): Promise { + getServerConfig(): ServerConfigDto { return this.service.getSystemConfig(); } + @Get('server/version') + getServerVersion(): ServerVersionResponseDto { + return this.service.getVersion(); + } + + /** + * {@link _DatabaseBackupController.listDatabaseBackups} + */ + @Get('admin/database-backups') + @MaintenanceRoute() + listDatabaseBackups(): Promise { + return this.service.listBackups(); + } + + /** + * {@link _DatabaseBackupController.downloadDatabaseBackup} + */ + @Get('admin/database-backups/:filename') + @MaintenanceRoute() + async downloadDatabaseBackup( + @Param() { filename }: FilenameParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ) { + await sendFile(res, next, () => this.service.downloadBackup(filename), this.logger); + } + + /** + * {@link _DatabaseBackupController.deleteDatabaseBackup} + */ + @Delete('admin/database-backups') + @MaintenanceRoute() + async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise { + return this.service.deleteBackup(dto.backups); + } + + /** + * {@link _DatabaseBackupController.uploadDatabaseBackup} + */ + @Post('admin/database-backups/upload') + @MaintenanceRoute() + @UseInterceptors(FileInterceptor('file')) + uploadDatabaseBackup( + @UploadedFile() + file: Express.Multer.File, + ): Promise { + return this.service.uploadBackup(file); + } + + @Get('admin/maintenance/status') + maintenanceStatus(@Req() request: Request): Promise { + return this.service.status(request.cookies[ImmichCookie.MaintenanceToken]); + } + + @Get('admin/maintenance/detect-install') + detectPriorInstall(): Promise { + return this.service.detectPriorInstall(); + } + @Post('admin/maintenance/login') async maintenanceLogin( @Req() request: Request, @@ -35,9 +126,7 @@ export class MaintenanceWorkerController { @Post('admin/maintenance') @MaintenanceRoute() - async setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): Promise { - if (dto.action === MaintenanceAction.End) { - await this.service.endMaintenance(); - } + setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): void { + void this.service.setAction(dto); } } diff --git a/server/src/maintenance/maintenance-worker.service.spec.ts b/server/src/maintenance/maintenance-worker.service.spec.ts index dd5b984214..9fd8f38fcb 100644 --- a/server/src/maintenance/maintenance-worker.service.spec.ts +++ b/server/src/maintenance/maintenance-worker.service.spec.ts @@ -1,25 +1,51 @@ -import { UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { SignJWT } from 'jose'; -import { SystemMetadataKey } from 'src/enum'; +import { DateTime } from 'luxon'; +import { PassThrough, Readable } from 'node:stream'; +import { StorageCore } from 'src/cores/storage.core'; +import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; -import { automock, getMocks, ServiceMocks } from 'test/utils'; +import { automock, AutoMocked, getMocks, mockDuplex, mockSpawn, ServiceMocks } from 'test/utils'; + +function* mockData() { + yield ''; +} describe(MaintenanceWorkerService.name, () => { let sut: MaintenanceWorkerService; let mocks: ServiceMocks; - let maintenanceWorkerRepositoryMock: MaintenanceWebsocketRepository; + let maintenanceWebsocketRepositoryMock: AutoMocked; + let maintenanceHealthRepositoryMock: AutoMocked; beforeEach(() => { mocks = getMocks(); - maintenanceWorkerRepositoryMock = automock(MaintenanceWebsocketRepository, { args: [mocks.logger], strict: false }); + maintenanceWebsocketRepositoryMock = automock(MaintenanceWebsocketRepository, { + args: [mocks.logger], + strict: false, + }); + maintenanceHealthRepositoryMock = automock(MaintenanceHealthRepository, { + args: [mocks.logger], + strict: false, + }); + sut = new MaintenanceWorkerService( mocks.logger as never, mocks.app, mocks.config, mocks.systemMetadata as never, - maintenanceWorkerRepositoryMock, + maintenanceWebsocketRepositoryMock, + maintenanceHealthRepositoryMock, + mocks.storage as never, + mocks.process, + mocks.database as never, ); + + sut.mock({ + active: true, + action: MaintenanceAction.Start, + }); }); it('should work', () => { @@ -27,14 +53,43 @@ describe(MaintenanceWorkerService.name, () => { }); describe('getSystemConfig', () => { - it('should respond the server is in maintenance mode', async () => { - await expect(sut.getSystemConfig()).resolves.toMatchObject( + it('should respond the server is in maintenance mode', () => { + expect(sut.getSystemConfig()).toMatchObject( expect.objectContaining({ maintenanceMode: true, }), ); - expect(mocks.systemMetadata.get).toHaveBeenCalled(); + expect(mocks.systemMetadata.get).toHaveBeenCalledTimes(0); + }); + }); + + describe.skip('ssr'); + describe.skip('detectMediaLocation'); + + describe('setStatus', () => { + it('should broadcast status', () => { + sut.setStatus({ + active: true, + action: MaintenanceAction.Start, + task: 'abc', + error: 'def', + }); + + expect(maintenanceWebsocketRepositoryMock.serverSend).toHaveBeenCalled(); + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledTimes(2); + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: 'start', + task: 'abc', + error: 'def', + }); + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'public', { + active: true, + action: 'start', + task: 'abc', + error: 'Something went wrong, see logs!', + }); }); }); @@ -42,7 +97,14 @@ describe(MaintenanceWorkerService.name, () => { const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/; it('should log a valid login URL', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); + await expect(sut.logSecret()).resolves.toBeUndefined(); expect(mocks.logger.log).toHaveBeenCalledWith(expect.stringMatching(RE_LOGIN_URL)); @@ -63,7 +125,13 @@ describe(MaintenanceWorkerService.name, () => { }); it('should parse cookie properly', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); await expect( sut.authenticate({ @@ -73,13 +141,102 @@ describe(MaintenanceWorkerService.name, () => { }); }); + describe('status', () => { + beforeEach(() => { + sut.mock({ + active: true, + action: MaintenanceAction.Start, + error: 'secret value!', + }); + }); + + it('generates private status', async () => { + const jwt = await new SignJWT({ _mockValue: true }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('4h') + .sign(new TextEncoder().encode('secret')); + + await expect(sut.status(jwt)).resolves.toEqual( + expect.objectContaining({ + error: 'secret value!', + }), + ); + }); + + it('generates public status', async () => { + await expect(sut.status()).resolves.toEqual( + expect.objectContaining({ + error: 'Something went wrong, see logs!', + }), + ); + }); + }); + + describe('detectPriorInstall', () => { + it('generate report about prior installation', async () => { + mocks.storage.readdir.mockResolvedValue(['.immich', 'file1', 'file2']); + mocks.storage.readFile.mockResolvedValue(undefined as never); + mocks.storage.overwriteFile.mockRejectedValue(undefined as never); + + await expect(sut.detectPriorInstall()).resolves.toMatchInlineSnapshot(` + { + "storage": [ + { + "files": 2, + "folder": "encoded-video", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "library", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "upload", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "profile", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "thumbs", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "backups", + "readable": true, + "writable": false, + }, + ], + } + `); + }); + }); + describe('login', () => { it('should fail without token', async () => { await expect(sut.login()).rejects.toThrowError(new UnauthorizedException('Missing JWT Token')); }); it('should fail with expired JWT', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); const jwt = await new SignJWT({}) .setProtectedHeader({ alg: 'HS256' }) @@ -91,7 +248,13 @@ describe(MaintenanceWorkerService.name, () => { }); it('should succeed with valid JWT', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); const jwt = await new SignJWT({ _mockValue: true }) .setProtectedHeader({ alg: 'HS256' }) @@ -107,22 +270,275 @@ describe(MaintenanceWorkerService.name, () => { }); }); - describe('endMaintenance', () => { + describe.skip('setAction'); // just calls setStatus+runAction + + /** + * Actions + */ + + describe('action: start', () => { + it('should not do anything', async () => { + await sut.runAction({ + action: MaintenanceAction.Start, + }); + + expect(mocks.logger.log).toHaveBeenCalledTimes(0); + }); + }); + + describe('action: end', () => { it('should set maintenance mode', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); - await expect(sut.endMaintenance()).resolves.toBeUndefined(); + await sut.runAction({ + action: MaintenanceAction.End, + }); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: false, }); - expect(maintenanceWorkerRepositoryMock.clientBroadcast).toHaveBeenCalledWith('AppRestartV1', { + expect(maintenanceWebsocketRepositoryMock.clientBroadcast).toHaveBeenCalledWith('AppRestartV1', { isMaintenanceMode: false, }); - expect(maintenanceWorkerRepositoryMock.serverSend).toHaveBeenCalledWith('AppRestart', { + expect(maintenanceWebsocketRepositoryMock.serverSend).toHaveBeenCalledWith('AppRestart', { isMaintenanceMode: false, }); }); }); + + describe('action: restore database', () => { + beforeEach(() => { + mocks.database.tryLock.mockResolvedValueOnce(true); + + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); + mocks.process.fork.mockImplementation(() => mockSpawn(0, 'Immich Server is listening', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.storage.createPlainReadStream.mockReturnValue(Readable.from(mockData())); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.storage.createGzip.mockReturnValue(new PassThrough()); + mocks.storage.createGunzip.mockReturnValue(new PassThrough()); + }); + + it('should update maintenance mode state', async () => { + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'filename', + }); + + expect(mocks.database.tryLock).toHaveBeenCalled(); + expect(mocks.logger.log).toHaveBeenCalledWith('Running maintenance action restore_database'); + + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: 'secret', + action: { + action: 'start', + }, + }); + }); + + it('should fail to restore invalid backup', async () => { + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'filename', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: Invalid backup file format!', + task: 'error', + }); + }); + + it('should successfully run a backup', async () => { + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + { + active: true, + action: MaintenanceAction.RestoreDatabase, + task: 'ready', + progress: expect.any(Number), + }, + ); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + { + active: true, + action: 'end', + }, + ); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); + }); + + it('should fail if backup creation fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: pg_dump non-zero exit code (1)\nerror', + task: 'error', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + expect.objectContaining({ + task: 'error', + }), + ); + }); + + it('should fail if restore itself fails', async () => { + mocks.process.spawnDuplexStream + .mockReturnValueOnce(mockDuplex('pg_dump', 0, 'data', '')) + .mockReturnValueOnce(mockDuplex('gzip', 0, 'data', '')) + .mockReturnValueOnce(mockDuplex('psql', 1, '', 'error')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: psql non-zero exit code (1)\nerror', + task: 'error', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + expect.objectContaining({ + task: 'error', + }), + ); + }); + + it('should rollback if database migrations fail', async () => { + mocks.database.runMigrations.mockRejectedValue(new Error('Migrations Error')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: Migrations Error', + task: 'error', + }); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalledTimes(0); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); + }); + + it('should rollback if API healthcheck fails', async () => { + maintenanceHealthRepositoryMock.checkApiHealth.mockRejectedValue(new Error('Health Error')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: Health Error', + task: 'error', + }); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); + }); + }); + + /** + * Backups + */ + + describe('listBackups', () => { + it('should give us all backups', async () => { + mocks.storage.readdir.mockResolvedValue([ + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + 'immich-db-backup-1753789649000.sql.gz', + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + ]); + mocks.storage.stat.mockResolvedValue({ size: 1024 } as any); + + await expect(sut.listBackups()).resolves.toMatchObject({ + backups: [ + { filename: 'immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, + { filename: 'immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, + { filename: 'immich-db-backup-1753789649000.sql.gz', filesize: 1024 }, + ], + }); + }); + }); + + describe('deleteBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.deleteBackup(['filename'])).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should unlink the target file', async () => { + await sut.deleteBackup(['filename.sql']); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename.sql`, + ); + }); + }); + + describe('uploadBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should write file', async () => { + await sut.uploadBackup({ originalname: 'path.sql.gz', buffer: 'buffer' } as never); + expect(mocks.storage.createOrOverwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', 'buffer'); + }); + }); + + describe('downloadBackup', () => { + it('should reject invalid file names', () => { + expect(() => sut.downloadBackup('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!')); + }); + + it('should get backup path', () => { + expect(sut.downloadBackup('hello.sql.gz')).toEqual( + expect.objectContaining({ + path: '/data/backups/hello.sql.gz', + }), + ); + }); + }); }); diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index c03231c274..8ad92799cd 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -4,19 +4,41 @@ import { NextFunction, Request, Response } from 'express'; import { jwtVerify } from 'jose'; import { readFileSync } from 'node:fs'; import { IncomingHttpHeaders } from 'node:http'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { ImmichCookie, SystemMetadataKey } from 'src/enum'; +import { serverVersion } from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; +import { ServerConfigDto, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { DatabaseLock, ImmichCookie, MaintenanceAction, SystemMetadataKey } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { AppRepository } from 'src/repositories/app.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { type ApiService as _ApiService } from 'src/services/api.service'; import { type BaseService as _BaseService } from 'src/services/base.service'; +import { type DatabaseBackupService as _DatabaseBackupService } from 'src/services/database-backup.service'; import { type ServerService as _ServerService } from 'src/services/server.service'; +import { type VersionService as _VersionService } from 'src/services/version.service'; import { MaintenanceModeState } from 'src/types'; import { getConfig } from 'src/utils/config'; -import { createMaintenanceLoginUrl } from 'src/utils/maintenance'; +import { + deleteDatabaseBackup, + downloadDatabaseBackup, + listDatabaseBackups, + restoreDatabaseBackup, + uploadDatabaseBackup, +} from 'src/utils/database-backups'; +import { ImmichFileResponse } from 'src/utils/file'; +import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; /** @@ -24,16 +46,51 @@ import { getExternalDomain } from 'src/utils/misc'; */ @Injectable() export class MaintenanceWorkerService { + #secret: string | null = null; + #status: MaintenanceStatusResponseDto = { + active: true, + action: MaintenanceAction.Start, + }; + constructor( protected logger: LoggingRepository, private appRepository: AppRepository, private configRepository: ConfigRepository, private systemMetadataRepository: SystemMetadataRepository, - private maintenanceWorkerRepository: MaintenanceWebsocketRepository, + private maintenanceWebsocketRepository: MaintenanceWebsocketRepository, + private maintenanceHealthRepository: MaintenanceHealthRepository, + private storageRepository: StorageRepository, + private processRepository: ProcessRepository, + private databaseRepository: DatabaseRepository, ) { this.logger.setContext(this.constructor.name); } + mock(status: MaintenanceStatusResponseDto) { + this.#secret = 'secret'; + this.#status = status; + } + + async init() { + const state = (await this.systemMetadataRepository.get( + SystemMetadataKey.MaintenanceMode, + )) as MaintenanceModeState & { isMaintenanceMode: true }; + + this.#secret = state.secret; + this.#status = { + active: true, + action: state.action.action, + }; + + StorageCore.setMediaLocation(this.detectMediaLocation()); + + this.maintenanceWebsocketRepository.setAuthFn(async (client) => this.authenticate(client.request.headers)); + this.maintenanceWebsocketRepository.setStatusUpdateFn((status) => (this.#status = status)); + + await this.logSecret(); + void this.runAction(state.action); + } + /** * {@link _BaseService.configRepos} */ @@ -55,22 +112,17 @@ export class MaintenanceWorkerService { /** * {@link _ServerService.getSystemConfig} */ - async getSystemConfig() { - const config = await this.getConfig({ withCache: false }); - + getSystemConfig() { return { - loginPageMessage: config.server.loginPageMessage, - trashDays: config.trash.days, - userDeleteDelay: config.user.deleteDelay, - oauthButtonText: config.oauth.buttonText, - isInitialized: true, - isOnboarded: true, - externalDomain: config.server.externalDomain, - publicUsers: config.server.publicUsers, - mapDarkStyleUrl: config.map.darkStyle, - mapLightStyleUrl: config.map.lightStyle, maintenanceMode: true, - }; + } as ServerConfigDto; + } + + /** + * {@link _VersionService.getVersion} + */ + getVersion() { + return ServerVersionResponseDto.fromSemVer(serverVersion); } /** @@ -106,12 +158,99 @@ export class MaintenanceWorkerService { }; } - private async secret(): Promise { - const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) as { - secret: string; - }; + /** + * {@link _StorageService.detectMediaLocation} + */ + detectMediaLocation(): string { + const envData = this.configRepository.getEnv(); + if (envData.storage.mediaLocation) { + return envData.storage.mediaLocation; + } - return state.secret; + const targets: string[] = []; + const candidates = ['/data', '/usr/src/app/upload']; + + for (const candidate of candidates) { + const exists = this.storageRepository.existsSync(candidate); + if (exists) { + targets.push(candidate); + } + } + + if (targets.length === 1) { + return targets[0]; + } + + return '/usr/src/app/upload'; + } + + /** + * {@link _DatabaseBackupService.listBackups} + */ + async listBackups(): Promise<{ backups: { filename: string; filesize: number }[] }> { + const backups = await listDatabaseBackups(this.backupRepos); + return { backups }; + } + + /** + * {@link _DatabaseBackupService.deleteBackup} + */ + async deleteBackup(files: string[]): Promise { + return deleteDatabaseBackup(this.backupRepos, files); + } + + /** + * {@link _DatabaseBackupService.uploadBackup} + */ + async uploadBackup(file: Express.Multer.File): Promise { + return uploadDatabaseBackup(this.backupRepos, file); + } + + /** + * {@link _DatabaseBackupService.downloadBackup} + */ + downloadBackup(fileName: string): ImmichFileResponse { + return downloadDatabaseBackup(fileName); + } + + private get secret() { + if (!this.#secret) { + throw new Error('Secret is not initialised yet.'); + } + + return this.#secret; + } + + private get backupRepos() { + return { + logger: this.logger, + storage: this.storageRepository, + config: this.configRepository, + process: this.processRepository, + database: this.databaseRepository, + health: this.maintenanceHealthRepository, + }; + } + + private getStatus(): MaintenanceStatusResponseDto { + return this.#status; + } + + private getPublicStatus(): MaintenanceStatusResponseDto { + const state = structuredClone(this.#status); + + if (state.error) { + state.error = 'Something went wrong, see logs!'; + } + + return state; + } + + setStatus(status: MaintenanceStatusResponseDto): void { + this.#status = status; + this.maintenanceWebsocketRepository.serverSend('MaintenanceStatus', status); + this.maintenanceWebsocketRepository.clientSend('MaintenanceStatusV1', 'private', status); + this.maintenanceWebsocketRepository.clientSend('MaintenanceStatusV1', 'public', this.getPublicStatus()); } async logSecret(): Promise { @@ -123,7 +262,7 @@ export class MaintenanceWorkerService { { username: 'immich-admin', }, - await this.secret(), + this.secret, ); this.logger.log(`\n\n🚧 Immich is in maintenance mode, you can log in using the following URL:\n${url}\n`); @@ -134,28 +273,115 @@ export class MaintenanceWorkerService { return this.login(jwtToken); } + async status(potentiallyJwt?: string): Promise { + try { + await this.login(potentiallyJwt); + return this.getStatus(); + } catch { + return this.getPublicStatus(); + } + } + + detectPriorInstall(): Promise { + return detectPriorInstall(this.storageRepository); + } + async login(jwt?: string): Promise { if (!jwt) { throw new UnauthorizedException('Missing JWT Token'); } - const secret = await this.secret(); - try { - const result = await jwtVerify(jwt, new TextEncoder().encode(secret)); + const result = await jwtVerify(jwt, new TextEncoder().encode(this.secret)); return result.payload; } catch { throw new UnauthorizedException('Invalid JWT Token'); } } - async endMaintenance(): Promise { + async setAction(action: SetMaintenanceModeDto) { + this.setStatus({ + active: true, + action: action.action, + }); + + await this.runAction(action); + } + + async runAction(action: SetMaintenanceModeDto) { + switch (action.action) { + case MaintenanceAction.Start: { + return; + } + case MaintenanceAction.End: { + return this.endMaintenance(); + } + case MaintenanceAction.SelectDatabaseRestore: { + return; + } + } + + const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation); + if (!lock) { + return; + } + + this.logger.log(`Running maintenance action ${action.action}`); + + await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: this.secret, + action: { + action: MaintenanceAction.Start, + }, + }); + + try { + if (!action.restoreBackupFilename) { + throw new Error("Expected restoreBackupFilename but it's missing!"); + } + + await this.restoreBackup(action.restoreBackupFilename); + } catch (error) { + this.logger.error(`Encountered error running action: ${error}`); + this.setStatus({ + active: true, + action: action.action, + task: 'error', + error: '' + error, + }); + } + } + + private async restoreBackup(filename: string): Promise { + this.setStatus({ + active: true, + action: MaintenanceAction.RestoreDatabase, + task: 'ready', + progress: 0, + }); + + await restoreDatabaseBackup(this.backupRepos, filename, (task, progress) => + this.setStatus({ + active: true, + action: MaintenanceAction.RestoreDatabase, + progress, + task, + }), + ); + + await this.setAction({ + action: MaintenanceAction.End, + }); + } + + private async endMaintenance(): Promise { const state: MaintenanceModeState = { isMaintenanceMode: false as const }; await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state); // => corresponds to notification.service.ts#onAppRestart - this.maintenanceWorkerRepository.clientBroadcast('AppRestartV1', state); - this.maintenanceWorkerRepository.serverSend('AppRestart', state); + this.maintenanceWebsocketRepository.clientBroadcast('AppRestartV1', state); + this.maintenanceWebsocketRepository.serverSend('AppRestart', state); this.appRepository.exitApp(); } } diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index ccd90680bb..bb9941a162 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -29,7 +29,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -37,20 +38,6 @@ select and "asset_file"."type" = $1 ) as agg ) as "files", - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "tag"."value" - from - "tag" - inner join "tag_asset" on "tag"."id" = "tag_asset"."tagId" - where - "asset"."id" = "tag_asset"."assetId" - ) as agg - ) as "tags", to_json("asset_exif") as "exifInfo" from "asset" @@ -72,7 +59,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -99,7 +87,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -145,7 +134,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -174,7 +164,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -244,7 +235,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -269,7 +261,8 @@ where select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -318,7 +311,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -357,7 +351,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -444,7 +439,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -536,7 +532,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -575,7 +572,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 666f41eb09..abaae84036 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -286,7 +286,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 64714e5665..b5f1dc7d18 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -43,6 +43,7 @@ select "asset_exif"."projectionType", "asset_exif"."rating", "asset_exif"."state", + "asset_exif"."tags", "asset_exif"."timeZone" from "asset_exif" @@ -127,6 +128,7 @@ select "asset_exif"."projectionType", "asset_exif"."rating", "asset_exif"."state", + "asset_exif"."tags", "asset_exif"."timeZone" from "asset_exif" diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index e7595b3d1e..f817ad57b3 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -71,6 +71,7 @@ select "asset"."libraryId", "asset"."width", "asset"."height", + "asset"."isEdited", "album_asset"."updateId" from "album_asset" as "album_asset" @@ -103,6 +104,7 @@ select "asset"."libraryId", "asset"."width", "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -140,7 +142,8 @@ select "asset"."stackId", "asset"."libraryId", "asset"."width", - "asset"."height" + "asset"."height", + "asset"."isEdited" from "album_asset" as "album_asset" inner join "asset" on "asset"."id" = "album_asset"."assetId" @@ -456,6 +459,7 @@ select "asset"."libraryId", "asset"."width", "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -751,6 +755,7 @@ select "asset"."libraryId", "asset"."width", "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -802,6 +807,7 @@ select "asset"."libraryId", "asset"."width", "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 39e658a5a8..b5cb4a537b 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { Kysely } from 'kysely'; -import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { Asset, columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -42,15 +41,6 @@ export class AssetJobRepository { .where('asset.id', '=', asUuid(id)) .select(['id', 'originalPath']) .select((eb) => withFiles(eb, AssetFileType.Sidecar)) - .select((eb) => - jsonArrayFrom( - eb - .selectFrom('tag') - .select(['tag.value']) - .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagId') - .whereRef('asset.id', '=', 'tag_asset.assetId'), - ).as('tags'), - ) .$call(withExifInner) .limit(1) .executeTakeFirst(); diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 325835b965..944417c0ad 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -178,6 +178,7 @@ export class AssetRepository { bitsPerSample: ref('bitsPerSample'), rating: ref('rating'), fps: ref('fps'), + tags: ref('tags'), lockedProperties: lockedPropertiesBehavior === 'append' ? distinctLocked(eb, exif.lockedProperties ?? null) @@ -903,20 +904,22 @@ export class AssetRepository { .execute(); } - async upsertFile(file: Pick, 'assetId' | 'path' | 'type'>): Promise { + async upsertFile(file: Pick, 'assetId' | 'path' | 'type' | 'isEdited'>): Promise { const value = { ...file, assetId: asUuid(file.assetId) }; await this.db .insertInto('asset_file') .values(value) .onConflict((oc) => - oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({ + oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({ path: eb.ref('excluded.path'), })), ) .execute(); } - async upsertFiles(files: Pick, 'assetId' | 'path' | 'type'>[]): Promise { + async upsertFiles( + files: Pick, 'assetId' | 'path' | 'type' | 'isEdited'>[], + ): Promise { if (files.length === 0) { return; } @@ -926,7 +929,7 @@ export class AssetRepository { .insertInto('asset_file') .values(values) .onConflict((oc) => - oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({ + oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({ path: eb.ref('excluded.path'), })), ) diff --git a/server/src/repositories/process.repository.spec.ts b/server/src/repositories/process.repository.spec.ts new file mode 100644 index 0000000000..a3f44bd78b --- /dev/null +++ b/server/src/repositories/process.repository.spec.ts @@ -0,0 +1,85 @@ +import { ChildProcessWithoutNullStreams } from 'node:child_process'; +import { Readable, Writable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { ProcessRepository } from 'src/repositories/process.repository'; + +function* data() { + yield 'Hello, world!'; +} + +describe(ProcessRepository.name, () => { + let sut: ProcessRepository; + let sink: Writable; + + beforeAll(() => { + sut = new ProcessRepository(); + }); + + beforeEach(() => { + sink = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + + final(callback) { + callback(); + }, + }); + }); + + describe('createSpawnDuplexStream', () => { + it('should work (drain to stdout)', async () => { + const process = sut.spawnDuplexStream('bash', ['-c', 'exit 0']); + await pipeline(process, sink); + }); + + it('should throw on non-zero exit code', async () => { + const process = sut.spawnDuplexStream('bash', ['-c', 'echo "error message" >&2; exit 1']); + await expect(pipeline(process, sink)).rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: bash non-zero exit code (1) + error message + ] + `); + }); + + it('should accept stdin / output stdout', async () => { + let output = ''; + const sink = new Writable({ + write(chunk, _encoding, callback) { + output += chunk; + callback(); + }, + + final(callback) { + callback(); + }, + }); + + const echoProcess = sut.spawnDuplexStream('cat'); + await pipeline(Readable.from(data()), echoProcess, sink); + expect(output).toBe('Hello, world!'); + }); + + it('should drain stdin on process exit', async () => { + let resolve1: () => void; + let resolve2: () => void; + const promise1 = new Promise((r) => (resolve1 = r)); + const promise2 = new Promise((r) => (resolve2 = r)); + + async function* data() { + yield 'Hello, world!'; + await promise1; + await promise2; + yield 'Write after stdin close / process exit!'; + } + + const process = sut.spawnDuplexStream('bash', ['-c', 'exit 0']); + + const realProcess = (process as never as { _process: ChildProcessWithoutNullStreams })._process; + realProcess.on('close', () => setImmediate(() => resolve1())); + realProcess.stdin.on('close', () => setImmediate(() => resolve2())); + + await pipeline(Readable.from(data()), process); + }); + }); +}); diff --git a/server/src/repositories/process.repository.ts b/server/src/repositories/process.repository.ts index 5055c4f3b5..9d8cac1f40 100644 --- a/server/src/repositories/process.repository.ts +++ b/server/src/repositories/process.repository.ts @@ -1,9 +1,110 @@ import { Injectable } from '@nestjs/common'; -import { ChildProcessWithoutNullStreams, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; +import { ChildProcessWithoutNullStreams, fork, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; +import { Duplex } from 'node:stream'; @Injectable() export class ProcessRepository { - spawn(command: string, args: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { + spawn(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { return spawn(command, args, options); } + + spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex { + let stdinClosed = false; + let drainCallback: undefined | (() => void); + + const process = this.spawn(command, args, options); + const duplex = new Duplex({ + // duplex -> stdin + write(chunk, encoding, callback) { + // drain the input if process dies + if (stdinClosed) { + return callback(); + } + + // handle stream backpressure + if (process.stdin.write(chunk, encoding)) { + callback(); + } else { + drainCallback = callback; + process.stdin.once('drain', () => { + drainCallback = undefined; + callback(); + }); + } + }, + + read() { + // no-op + }, + + final(callback) { + if (stdinClosed) { + callback(); + } else { + process.stdin.end(callback); + } + }, + }); + + // stdout -> duplex + process.stdout.on('data', (chunk) => { + // handle stream backpressure + if (!duplex.push(chunk)) { + process.stdout.pause(); + } + }); + + duplex.on('resume', () => process.stdout.resume()); + + // end handling + let stdoutClosed = false; + function close(error?: Error) { + stdinClosed = true; + + if (error) { + duplex.destroy(error); + } else if (stdoutClosed && typeof process.exitCode === 'number') { + duplex.push(null); + } + } + + process.stdout.on('close', () => { + stdoutClosed = true; + close(); + }); + + // error handling + process.on('error', close); + process.stdout.on('error', close); + process.stdin.on('error', (error) => { + if ((error as { code?: 'EPIPE' })?.code === 'EPIPE') { + try { + drainCallback!(); + } catch (error) { + close(error as Error); + } + } else { + close(error); + } + }); + + let stderr = ''; + process.stderr.on('data', (chunk) => (stderr += chunk)); + + process.on('exit', (code) => { + console.info(`${command} exited (${code})`); + + if (code === 0) { + close(); + } else { + close(new Error(`${command} non-zero exit code (${code})\n${stderr}`)); + } + }); + + return Object.assign(duplex, { _process: process }); + } + + fork(...args: Parameters): ReturnType { + return fork(...args); + } } diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 3bb9ec5bfe..7345dfef5b 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -5,7 +5,8 @@ import { escapePath, glob, globStream } from 'fast-glob'; import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, ReadOptionsWithBuffer } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { Readable, Writable } from 'node:stream'; +import { PassThrough, Readable, Writable } from 'node:stream'; +import { createGunzip, createGzip } from 'node:zlib'; import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { mimeTypes } from 'src/utils/mime-types'; @@ -93,6 +94,14 @@ export class StorageRepository { return { stream: archive, addFile, finalize }; } + createGzip(): PassThrough { + return createGzip(); + } + + createGunzip(): PassThrough { + return createGunzip(); + } + createPlainReadStream(filepath: string): Readable { return createReadStream(filepath); } diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index c2da06786c..bfed556895 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -37,7 +37,7 @@ export interface ClientEventMap { AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; AppRestartV1: [AppRestartEvent]; - AssetEditReadyV1: [{ assetId: string }]; + AssetEditReadyV1: [{ asset: SyncAssetV1 }]; } export type AuthFn = (client: Socket) => Promise; diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 385db37cf8..d7dabfef4c 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -255,3 +255,34 @@ export const asset_face_audit = registerFunction({ RETURN NULL; END`, }); + +export const asset_edit_insert = registerFunction({ + name: 'asset_edit_insert', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + UPDATE asset + SET "isEdited" = true + FROM inserted_edit + WHERE asset.id = inserted_edit."assetId" AND NOT asset."isEdited"; + RETURN NULL; + END + `, +}); + +export const asset_edit_delete = registerFunction({ + name: 'asset_edit_delete', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + `, +}); diff --git a/server/src/schema/migrations/1768587436457-AddEditCountToAsset.ts b/server/src/schema/migrations/1768587436457-AddEditCountToAsset.ts new file mode 100644 index 0000000000..3dd60ccda0 --- /dev/null +++ b/server/src/schema/migrations/1768587436457-AddEditCountToAsset.ts @@ -0,0 +1,53 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_insert() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" + 1 + WHERE "id" = NEW."assetId"; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION asset_edit_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" - 1 + WHERE "id" = OLD."assetId"; + RETURN NULL; + END + $$;`.execute(db); + await sql`ALTER TABLE "asset" ADD "editCount" integer NOT NULL DEFAULT 0;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "old" + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_edit_delete();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert" + AFTER INSERT ON "asset_edit" + FOR EACH ROW + EXECUTE FUNCTION asset_edit_insert();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_insert', '{"type":"function","name":"asset_edit_insert","sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" + 1\\n WHERE \\"id\\" = NEW.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_delete', '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" - 1\\n WHERE \\"id\\" = OLD.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_delete', '{"type":"trigger","name":"asset_edit_delete","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_insert', '{"type":"trigger","name":"asset_edit_insert","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION asset_edit_insert();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "asset_edit_delete" ON "asset_edit";`.execute(db); + await sql`DROP TRIGGER "asset_edit_insert" ON "asset_edit";`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "editCount";`.execute(db); + await sql`DROP FUNCTION asset_edit_insert;`.execute(db); + await sql`DROP FUNCTION asset_edit_delete;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_insert';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_insert';`.execute(db); +} diff --git a/server/src/schema/migrations/1768757482271-SwitchToIsEdited.ts b/server/src/schema/migrations/1768757482271-SwitchToIsEdited.ts new file mode 100644 index 0000000000..0660b7303d --- /dev/null +++ b/server/src/schema/migrations/1768757482271-SwitchToIsEdited.ts @@ -0,0 +1,89 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_insert() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "isEdited" = true + FROM inserted_edit + WHERE asset.id = inserted_edit."assetId" AND NOT asset."isEdited"; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION asset_edit_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + $$;`.execute(db); + await sql`ALTER TABLE "asset" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "deleted_edit" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_edit_delete();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert" + AFTER INSERT ON "asset_edit" + REFERENCING NEW TABLE AS "inserted_edit" + FOR EACH STATEMENT + EXECUTE FUNCTION asset_edit_insert();`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "editCount";`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_insert","sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = true\\n FROM inserted_edit\\n WHERE asset.id = inserted_edit.\\"assetId\\" AND NOT asset.\\"isEdited\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"asset_edit_delete","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"deleted_edit\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();"}'::jsonb WHERE "name" = 'trigger_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"asset_edit_insert","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n REFERENCING NEW TABLE AS \\"inserted_edit\\"\\n FOR EACH STATEMENT\\n EXECUTE FUNCTION asset_edit_insert();"}'::jsonb WHERE "name" = 'trigger_asset_edit_insert';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION public.asset_edit_insert() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" + 1 + WHERE "id" = NEW."assetId"; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" - 1 + WHERE "id" = OLD."assetId"; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`ALTER TABLE "asset" ADD "editCount" integer NOT NULL DEFAULT 0;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "old" + FOR EACH ROW + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION asset_edit_delete();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert" + AFTER INSERT ON "asset_edit" + FOR EACH ROW + EXECUTE FUNCTION asset_edit_insert();`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "isEdited";`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" + 1\\n WHERE \\"id\\" = NEW.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_insert","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" - 1\\n WHERE \\"id\\" = OLD.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();","name":"asset_edit_delete","type":"trigger"}'::jsonb WHERE "name" = 'trigger_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION asset_edit_insert();","name":"asset_edit_insert","type":"trigger"}'::jsonb WHERE "name" = 'trigger_asset_edit_insert';`.execute(db); +} diff --git a/server/src/schema/migrations/1768828334807-AddIsEditedToAssetFile.ts b/server/src/schema/migrations/1768828334807-AddIsEditedToAssetFile.ts new file mode 100644 index 0000000000..b1daa3d72f --- /dev/null +++ b/server/src/schema/migrations/1768828334807-AddIsEditedToAssetFile.ts @@ -0,0 +1,13 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_uq";`.execute(db); + await sql`ALTER TABLE "asset_file" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db); + await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_isEdited_uq" UNIQUE ("assetId", "type", "isEdited");`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_isEdited_uq";`.execute(db); + await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_uq" UNIQUE ("assetId", "type");`.execute(db); + await sql`ALTER TABLE "asset_file" DROP COLUMN "isEdited";`.execute(db); +} diff --git a/server/src/schema/migrations/1768847456553-AddTagsToExif.ts b/server/src/schema/migrations/1768847456553-AddTagsToExif.ts new file mode 100644 index 0000000000..6839468cae --- /dev/null +++ b/server/src/schema/migrations/1768847456553-AddTagsToExif.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_exif" ADD "tags" character varying[];`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_exif" DROP COLUMN "tags";`.execute(db); +} diff --git a/server/src/schema/tables/asset-edit.table.ts b/server/src/schema/tables/asset-edit.table.ts index 84d95ca3c9..ad0b443b69 100644 --- a/server/src/schema/tables/asset-edit.table.ts +++ b/server/src/schema/tables/asset-edit.table.ts @@ -1,7 +1,24 @@ import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto'; +import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn } from 'src/sql-tools'; +import { + AfterDeleteTrigger, + AfterInsertTrigger, + Column, + ForeignKeyColumn, + Generated, + PrimaryGeneratedColumn, + Table, +} from 'src/sql-tools'; +@Table('asset_edit') +@AfterInsertTrigger({ scope: 'statement', function: asset_edit_insert, referencingNewTableAs: 'inserted_edit' }) +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_edit_delete, + referencingOldTableAs: 'deleted_edit', + when: 'pg_trigger_depth() = 0', +}) export class AssetEditTable { @PrimaryGeneratedColumn() id!: Generated; diff --git a/server/src/schema/tables/asset-exif.table.ts b/server/src/schema/tables/asset-exif.table.ts index 346098a72c..9dacb547cf 100644 --- a/server/src/schema/tables/asset-exif.table.ts +++ b/server/src/schema/tables/asset-exif.table.ts @@ -93,6 +93,9 @@ export class AssetExifTable { @Column({ type: 'integer', nullable: true }) rating!: number | null; + @Column({ type: 'character varying', array: true, nullable: true }) + tags!: string[] | null; + @UpdateDateColumn({ default: () => 'clock_timestamp()' }) updatedAt!: Generated; diff --git a/server/src/schema/tables/asset-file.table.ts b/server/src/schema/tables/asset-file.table.ts index 6456d1d535..41301f41b7 100644 --- a/server/src/schema/tables/asset-file.table.ts +++ b/server/src/schema/tables/asset-file.table.ts @@ -14,7 +14,7 @@ import { } from 'src/sql-tools'; @Table('asset_file') -@Unique({ columns: ['assetId', 'type'] }) +@Unique({ columns: ['assetId', 'type', 'isEdited'] }) @UpdatedAtTrigger('asset_file_updatedAt') export class AssetFileTable { @PrimaryGeneratedColumn() @@ -37,4 +37,7 @@ export class AssetFileTable { @UpdateIdColumn({ index: true }) updateId!: Generated; + + @Column({ type: 'boolean', default: false }) + isEdited!: Generated; } diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index 5b0645cfc7..956eb5ebda 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -143,4 +143,7 @@ export class AssetTable { @Column({ type: 'integer', nullable: true }) height!: number | null; + + @Column({ type: 'boolean', default: false }) + isEdited!: Generated; } diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 8d48b47f1e..14544f454f 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -107,6 +107,78 @@ describe(ApiKeyService.name, () => { permissions: newPermissions, }); }); + + describe('api key auth', () => { + it('should prevent adding Permission.all', async () => { + const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; + const auth = factory.auth({ apiKey: { permissions } }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + + await expect(sut.update(auth, apiKey.id, { permissions: [Permission.All] })).rejects.toThrow( + 'Cannot grant permissions you do not have', + ); + + expect(mocks.apiKey.update).not.toHaveBeenCalled(); + }); + + it('should prevent adding a new permission', async () => { + const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; + const auth = factory.auth({ apiKey: { permissions } }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + + await expect(sut.update(auth, apiKey.id, { permissions: [Permission.AssetCopy] })).rejects.toThrow( + 'Cannot grant permissions you do not have', + ); + + expect(mocks.apiKey.update).not.toHaveBeenCalled(); + }); + + it('should allow removing permissions', async () => { + const auth = factory.auth({ apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] } }); + const apiKey = factory.apiKey({ + userId: auth.user.id, + permissions: [Permission.AssetRead, Permission.AssetDelete], + }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + // remove Permission.AssetDelete + await sut.update(auth, apiKey.id, { permissions: [Permission.AssetRead] }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith( + auth.user.id, + apiKey.id, + expect.objectContaining({ permissions: [Permission.AssetRead] }), + ); + }); + + it('should allow adding new permissions', async () => { + const auth = factory.auth({ + apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] }, + }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.AssetRead] }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + // add Permission.AssetUpdate + await sut.update(auth, apiKey.id, { + name: apiKey.name, + permissions: [Permission.AssetRead, Permission.AssetUpdate], + }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith( + auth.user.id, + apiKey.id, + expect.objectContaining({ permissions: [Permission.AssetRead, Permission.AssetUpdate] }), + ); + }); + }); }); describe('delete', () => { diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 96671daab1..492ee9c0fd 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -32,6 +32,14 @@ export class ApiKeyService extends BaseService { throw new BadRequestException('API Key not found'); } + if ( + auth.apiKey && + dto.permissions && + !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions }) + ) { + throw new BadRequestException('Cannot grant permissions you do not have'); + } + const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions }); return this.map(key); diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index c19a1ad92e..0d099598e5 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -529,9 +529,10 @@ describe(AssetMediaService.name, () => { ...assetStub.withCropEdit.files, { id: 'edited-file', - type: AssetFileType.FullSizeEdited, + type: AssetFileType.FullSize, path: '/uploads/user-id/fullsize/edited.jpg', - } as AssetFile, + isEdited: true, + }, ], }; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); @@ -554,9 +555,10 @@ describe(AssetMediaService.name, () => { ...assetStub.withCropEdit.files, { id: 'edited-file', - type: AssetFileType.FullSizeEdited, + type: AssetFileType.FullSize, path: '/uploads/user-id/fullsize/edited.jpg', - } as AssetFile, + isEdited: true, + }, ], }; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); @@ -579,9 +581,10 @@ describe(AssetMediaService.name, () => { ...assetStub.withCropEdit.files, { id: 'edited-file', - type: AssetFileType.FullSizeEdited, + type: AssetFileType.FullSize, path: '/uploads/user-id/fullsize/edited.jpg', - } as AssetFile, + isEdited: true, + }, ], }; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); @@ -656,6 +659,7 @@ describe(AssetMediaService.name, () => { id: '42', path: '/path/to/preview', type: AssetFileType.Thumbnail, + isEdited: false, }, ], }); @@ -673,6 +677,7 @@ describe(AssetMediaService.name, () => { id: '42', path: '/path/to/preview.jpg', type: AssetFileType.Preview, + isEdited: false, }, ], }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index b92c339aab..2616a6baf5 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -4,7 +4,7 @@ import { DateTime, Duration } from 'luxon'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { AssetFile } from 'src/database'; import { OnJob } from 'src/decorators'; -import { AssetResponseDto, MapAsset, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, AssetBulkUpdateDto, @@ -112,7 +112,7 @@ export class AssetService extends BaseService { const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const repos = { asset: this.assetRepository, event: this.eventRepository }; - let previousMotion: MapAsset | null = null; + let previousMotion: { id: string } | null = null; if (rest.livePhotoVideoId) { await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId }); } else if (rest.livePhotoVideoId === null) { diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 9e25fbaf2e..ea80dd5759 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -5,7 +5,7 @@ import { StorageCore } from 'src/cores/storage.core'; import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; import { BackupService } from 'src/services/backup.service'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { mockSpawn, newTestService, ServiceMocks } from 'test/utils'; +import { mockDuplex, mockSpawn, newTestService, ServiceMocks } from 'test/utils'; import { describe } from 'vitest'; describe(BackupService.name, () => { @@ -147,6 +147,7 @@ describe(BackupService.name, () => { beforeEach(() => { mocks.storage.readdir.mockResolvedValue([]); mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); mocks.storage.rename.mockResolvedValue(); mocks.storage.unlink.mockResolvedValue(); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); @@ -165,7 +166,7 @@ describe(BackupService.name, () => { ({ sut, mocks } = newTestService(BackupService, { config: configMock })); mocks.storage.readdir.mockResolvedValue([]); - mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); mocks.storage.rename.mockResolvedValue(); mocks.storage.unlink.mockResolvedValue(); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); @@ -174,14 +175,16 @@ describe(BackupService.name, () => { await sut.handleBackupDatabase(); - expect(mocks.process.spawn).toHaveBeenCalled(); - const call = mocks.process.spawn.mock.calls[0]; + expect(mocks.process.spawnDuplexStream).toHaveBeenCalled(); + const call = mocks.process.spawnDuplexStream.mock.calls[0]; const args = call[1] as string[]; - // ['--dbname', '', '--clean', '--if-exists'] - expect(args[0]).toBe('--dbname'); - const passedUrl = args[1]; - expect(passedUrl).not.toContain('uselibpqcompat'); - expect(passedUrl).toContain('sslmode=require'); + expect(args).toMatchInlineSnapshot(` + [ + "postgresql://postgres:pwd@host:5432/immich?sslmode=require", + "--clean", + "--if-exists", + ] + `); }); it('should run a database backup successfully', async () => { @@ -196,21 +199,21 @@ describe(BackupService.name, () => { expect(mocks.storage.rename).toHaveBeenCalled(); }); - it('should fail if pg_dumpall fails', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); + it('should fail if pg_dump fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); }); it('should not rename file if pgdump fails and gzip succeeds', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); expect(mocks.storage.rename).not.toHaveBeenCalled(); }); it('should fail if gzip fails', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', '')); - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1'); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('gzip', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('gzip non-zero exit code (1)'); }); it('should fail if write stream fails', async () => { @@ -226,9 +229,9 @@ describe(BackupService.name, () => { }); it('should ignore unlink failing and still return failed job status', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); mocks.storage.unlink.mockRejectedValue(new Error('error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); expect(mocks.storage.unlink).toHaveBeenCalled(); }); @@ -242,12 +245,12 @@ describe(BackupService.name, () => { ${'17.15.1'} | ${17} ${'18.0.0'} | ${18} `( - `should use pg_dumpall $expectedVersion with postgres version $postgresVersion`, + `should use pg_dump $expectedVersion with postgres version $postgresVersion`, async ({ postgresVersion, expectedVersion }) => { mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); await sut.handleBackupDatabase(); - expect(mocks.process.spawn).toHaveBeenCalledWith( - `/usr/lib/postgresql/${expectedVersion}/bin/pg_dumpall`, + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledWith( + `/usr/lib/postgresql/${expectedVersion}/bin/pg_dump`, expect.any(Array), expect.any(Object), ); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 2ff3e5dd3e..637e968929 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -1,13 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { DateTime } from 'luxon'; import path from 'node:path'; -import semver from 'semver'; -import { serverVersion } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; +import { + createDatabaseBackup, + isFailedDatabaseBackupName, + isValidDatabaseRoutineBackupName, + UnsupportedPostgresError, +} from 'src/utils/database-backups'; import { handlePromiseError } from 'src/utils/misc'; @Injectable() @@ -53,16 +56,11 @@ export class BackupService extends BaseService { const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); const files = await this.storageRepository.readdir(backupsFolder); - const failedBackups = files.filter((file) => file.match(/immich-db-backup-.*\.sql\.gz\.tmp$/)); const backups = files - .filter((file) => { - const oldBackupStyle = file.match(/immich-db-backup-\d+\.sql\.gz$/); - //immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz - const newBackupStyle = file.match(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/); - return oldBackupStyle || newBackupStyle; - }) + .filter((filename) => isValidDatabaseRoutineBackupName(filename)) .toSorted() .toReversed(); + const failedBackups = files.filter((filename) => isFailedDatabaseBackupName(filename)); const toDelete = backups.slice(config.keepLastAmount); toDelete.push(...failedBackups); @@ -75,123 +73,27 @@ export class BackupService extends BaseService { @OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase }) async handleBackupDatabase(): Promise { - this.logger.debug(`Database Backup Started`); - const { database } = this.configRepository.getEnv(); - const config = database.config; - - const isUrlConnection = config.connectionType === 'url'; - - let connectionUrl: string = isUrlConnection ? config.url : ''; - if (URL.canParse(connectionUrl)) { - // remove known bad url parameters for pg_dumpall - const url = new URL(connectionUrl); - url.searchParams.delete('uselibpqcompat'); - connectionUrl = url.toString(); - } - - const databaseParams = isUrlConnection - ? ['--dbname', connectionUrl] - : [ - '--username', - config.username, - '--host', - config.host, - '--port', - `${config.port}`, - '--database', - config.database, - ]; - - databaseParams.push('--clean', '--if-exists'); - const databaseVersion = await this.databaseRepository.getPostgresVersion(); - const backupFilePath = path.join( - StorageCore.getBaseFolder(StorageFolder.Backups), - `immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, - ); - const databaseSemver = semver.coerce(databaseVersion); - const databaseMajorVersion = databaseSemver?.major; - - if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <19.0.0')) { - this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`); - return JobStatus.Failed; - } - - this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); - try { - await new Promise((resolve, reject) => { - const pgdump = this.processRepository.spawn( - `/usr/lib/postgresql/${databaseMajorVersion}/bin/pg_dumpall`, - databaseParams, - { - env: { - PATH: process.env.PATH, - PGPASSWORD: isUrlConnection ? new URL(connectionUrl).password : config.password, - }, - }, - ); - - // NOTE: `--rsyncable` is only supported in GNU gzip - const gzip = this.processRepository.spawn(`gzip`, ['--rsyncable']); - pgdump.stdout.pipe(gzip.stdin); - - const fileStream = this.storageRepository.createWriteStream(backupFilePath); - - gzip.stdout.pipe(fileStream); - - pgdump.on('error', (err) => { - this.logger.error(`Backup failed with error: ${err}`); - reject(err); - }); - - gzip.on('error', (err) => { - this.logger.error(`Gzip failed with error: ${err}`); - reject(err); - }); - - let pgdumpLogs = ''; - let gzipLogs = ''; - - pgdump.stderr.on('data', (data) => (pgdumpLogs += data)); - gzip.stderr.on('data', (data) => (gzipLogs += data)); - - pgdump.on('exit', (code) => { - if (code !== 0) { - this.logger.error(`Backup failed with code ${code}`); - reject(`Backup failed with code ${code}`); - this.logger.error(pgdumpLogs); - return; - } - if (pgdumpLogs) { - this.logger.debug(`pgdump_all logs\n${pgdumpLogs}`); - } - }); - - gzip.on('exit', (code) => { - if (code !== 0) { - this.logger.error(`Gzip failed with code ${code}`); - reject(`Gzip failed with code ${code}`); - this.logger.error(gzipLogs); - return; - } - if (pgdump.exitCode !== 0) { - this.logger.error(`Gzip exited with code 0 but pgdump exited with ${pgdump.exitCode}`); - return; - } - resolve(); - }); - }); - await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', '')); + await createDatabaseBackup(this.backupRepos); } catch (error) { - this.logger.error(`Database Backup Failure: ${error}`); - await this.storageRepository - .unlink(backupFilePath) - .catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`)); + if (error instanceof UnsupportedPostgresError) { + return JobStatus.Failed; + } + throw error; } - this.logger.log(`Database Backup Success`); await this.cleanupDatabaseBackups(); return JobStatus.Success; } + + private get backupRepos() { + return { + logger: this.logger, + storage: this.storageRepository, + config: this.configRepository, + process: this.processRepository, + database: this.databaseRepository, + }; + } } diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts index f4f14c3e68..36a3d2eb2c 100644 --- a/server/src/services/cli.service.spec.ts +++ b/server/src/services/cli.service.spec.ts @@ -1,5 +1,5 @@ import { jwtVerify } from 'jose'; -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { CliService } from 'src/services/cli.service'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -95,7 +95,14 @@ describe(CliService.name, () => { }); it('should disable maintenance mode', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); + await expect(sut.disableMaintenanceMode()).resolves.toEqual({ alreadyDisabled: false, }); @@ -109,7 +116,14 @@ describe(CliService.name, () => { describe('enableMaintenanceMode', () => { it('should not do anything if in maintenance mode', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); + await expect(sut.enableMaintenanceMode()).resolves.toEqual( expect.objectContaining({ alreadyEnabled: true, @@ -133,13 +147,22 @@ describe(CliService.name, () => { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret: expect.stringMatching(/^\w{128}$/), + action: { + action: 'start', + }, }); }); const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/; it('should return a valid login URL', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); const result = await sut.enableMaintenanceMode(); diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 8d2f1b0e99..ce62f98aa1 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -3,7 +3,7 @@ import { isAbsolute } from 'node:path'; import { SALT_ROUNDS } from 'src/constants'; import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; @@ -86,6 +86,9 @@ export class CliService extends BaseService { await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret, + action: { + action: MaintenanceAction.Start, + }, }); await this.appRepository.sendOneShotAppRestart({ diff --git a/server/src/services/database-backup.service.spec.ts b/server/src/services/database-backup.service.spec.ts new file mode 100644 index 0000000000..4d68b02325 --- /dev/null +++ b/server/src/services/database-backup.service.spec.ts @@ -0,0 +1,83 @@ +import { BadRequestException } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import { StorageCore } from 'src/cores/storage.core'; +import { StorageFolder } from 'src/enum'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import { newTestService, ServiceMocks } from 'test/utils'; + +describe(MaintenanceService.name, () => { + let sut: DatabaseBackupService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut, mocks } = newTestService(DatabaseBackupService)); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('listBackups', () => { + it('should give us all backups', async () => { + mocks.storage.readdir.mockResolvedValue([ + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + 'immich-db-backup-1753789649000.sql.gz', + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + ]); + mocks.storage.stat.mockResolvedValue({ size: 1024 } as any); + + await expect(sut.listBackups()).resolves.toMatchObject({ + backups: [ + { filename: 'immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, + { filename: 'immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, + { filename: 'immich-db-backup-1753789649000.sql.gz', filesize: 1024 }, + ], + }); + }); + }); + + describe('deleteBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.deleteBackup(['filename'])).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should unlink the target file', async () => { + await sut.deleteBackup(['filename.sql']); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename.sql`, + ); + }); + }); + + describe('uploadBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should write file', async () => { + await sut.uploadBackup({ originalname: 'path.sql.gz', buffer: 'buffer' } as never); + expect(mocks.storage.createOrOverwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', 'buffer'); + }); + }); + + describe('downloadBackup', () => { + it('should reject invalid file names', () => { + expect(() => sut.downloadBackup('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!')); + }); + + it('should get backup path', () => { + expect(sut.downloadBackup('hello.sql.gz')).toEqual( + expect.objectContaining({ + path: '/data/backups/hello.sql.gz', + }), + ); + }); + }); +}); diff --git a/server/src/services/database-backup.service.ts b/server/src/services/database-backup.service.ts new file mode 100644 index 0000000000..542e961b43 --- /dev/null +++ b/server/src/services/database-backup.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; +import { BaseService } from 'src/services/base.service'; +import { + deleteDatabaseBackup, + downloadDatabaseBackup, + listDatabaseBackups, + uploadDatabaseBackup, +} from 'src/utils/database-backups'; +import { ImmichFileResponse } from 'src/utils/file'; + +/** + * This service is available outside of maintenance mode to manage maintenance mode + */ +@Injectable() +export class DatabaseBackupService extends BaseService { + async listBackups(): Promise { + const backups = await listDatabaseBackups(this.backupRepos); + return { backups }; + } + + deleteBackup(files: string[]): Promise { + return deleteDatabaseBackup(this.backupRepos, files); + } + + async uploadBackup(file: Express.Multer.File): Promise { + return uploadDatabaseBackup(this.backupRepos, file); + } + + downloadBackup(fileName: string): ImmichFileResponse { + return downloadDatabaseBackup(fileName); + } + + private get backupRepos() { + return { + logger: this.logger, + storage: this.storageRepository, + config: this.configRepository, + process: this.processRepository, + database: this.databaseRepository, + }; + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 42bf467fd0..b68e311fe1 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -9,6 +9,7 @@ import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; import { BackupService } from 'src/services/backup.service'; import { CliService } from 'src/services/cli.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; import { DuplicateService } from 'src/services/duplicate.service'; @@ -60,6 +61,7 @@ export const services = [ AuthAdminService, BackupService, CliService, + DatabaseBackupService, DatabaseService, DownloadService, DuplicateService, diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 0a18d04701..0362cb10fd 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -136,7 +136,29 @@ export class JobService extends BaseService { const asset = await this.assetRepository.getById(item.data.id); if (asset) { - this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, { assetId: item.data.id }); + this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, { + asset: { + id: asset.id, + ownerId: asset.ownerId, + originalFileName: asset.originalFileName, + thumbhash: asset.thumbhash ? hexOrBufferToBase64(asset.thumbhash) : null, + checksum: hexOrBufferToBase64(asset.checksum), + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + localDateTime: asset.localDateTime, + duration: asset.duration, + type: asset.type, + deletedAt: asset.deletedAt, + isFavorite: asset.isFavorite, + visibility: asset.visibility, + livePhotoVideoId: asset.livePhotoVideoId, + stackId: asset.stackId, + libraryId: asset.libraryId, + width: asset.width, + height: asset.height, + isEdited: asset.isEdited, + }, + }); } break; @@ -189,6 +211,7 @@ export class JobService extends BaseService { libraryId: asset.libraryId, width: asset.width, height: asset.height, + isEdited: asset.isEdited, }, exif: { assetId: exif.assetId, diff --git a/server/src/services/maintenance.service.spec.ts b/server/src/services/maintenance.service.spec.ts index cc497a6ea4..e598f1c71d 100644 --- a/server/src/services/maintenance.service.spec.ts +++ b/server/src/services/maintenance.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { MaintenanceService } from 'src/services/maintenance.service'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -36,28 +36,96 @@ describe(MaintenanceService.name, () => { }); it('should return true if enabled', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: '' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: '', + action: { action: MaintenanceAction.Start }, + }); await expect(sut.getMaintenanceMode()).resolves.toEqual({ isMaintenanceMode: true, secret: '', + action: { + action: 'start', + }, }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); }); + describe('integrityCheck', () => { + it('generate integrity report', async () => { + mocks.storage.readdir.mockResolvedValue(['.immich', 'file1', 'file2']); + mocks.storage.readFile.mockResolvedValue(undefined as never); + mocks.storage.overwriteFile.mockRejectedValue(undefined as never); + + await expect(sut.detectPriorInstall()).resolves.toMatchInlineSnapshot(` + { + "storage": [ + { + "files": 2, + "folder": "encoded-video", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "library", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "upload", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "profile", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "thumbs", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "backups", + "readable": true, + "writable": false, + }, + ], + } + `); + }); + }); + describe('startMaintenance', () => { it('should set maintenance mode and return a secret', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); - await expect(sut.startMaintenance('admin')).resolves.toMatchObject({ + await expect( + sut.startMaintenance( + { + action: MaintenanceAction.Start, + }, + 'admin', + ), + ).resolves.toMatchObject({ jwt: expect.any(String), }); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret: expect.stringMatching(/^\w{128}$/), + action: { + action: 'start', + }, }); expect(mocks.event.emit).toHaveBeenCalledWith('AppRestart', { @@ -78,7 +146,13 @@ describe(MaintenanceService.name, () => { }); it('should generate a login url with JWT', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); await expect( sut.createLoginUrl({ diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index 0f5fa06957..8e711ef380 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,11 +1,21 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { SystemMetadataKey } from 'src/enum'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { MaintenanceModeState } from 'src/types'; -import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance'; +import { + createMaintenanceLoginUrl, + detectPriorInstall, + generateMaintenanceSecret, + signMaintenanceJwt, +} from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; /** @@ -19,9 +29,25 @@ export class MaintenanceService extends BaseService { .then((state) => state ?? { isMaintenanceMode: false }); } - async startMaintenance(username: string): Promise<{ jwt: string }> { + getMaintenanceStatus(): MaintenanceStatusResponseDto { + return { + active: false, + action: MaintenanceAction.End, + }; + } + + detectPriorInstall(): Promise { + return detectPriorInstall(this.storageRepository); + } + + async startMaintenance(action: SetMaintenanceModeDto, username: string): Promise<{ jwt: string }> { const secret = generateMaintenanceSecret(); - await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret }); + await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret, + action, + }); + await this.eventRepository.emit('AppRestart', { isMaintenanceMode: true }); return { @@ -31,6 +57,20 @@ export class MaintenanceService extends BaseService { }; } + async startRestoreFlow(): Promise<{ jwt: string }> { + const adminUser = await this.userRepository.getAdmin(); + if (adminUser) { + throw new BadRequestException('The server already has an admin'); + } + + return this.startMaintenance( + { + action: MaintenanceAction.SelectDatabaseRestore, + }, + 'admin', + ); + } + @OnEvent({ name: 'AppRestart', server: true }) onRestart(event: ArgOf<'AppRestart'>, ack?: (ok: 'ok') => void): void { this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`); diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index b94c5843ad..4310f678ab 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -241,21 +241,21 @@ describe(MediaService.name, () => { await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.FullSize, + pathType: AssetFileType.FullSize, oldPath: '/uploads/user-id/fullsize/path.webp', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-fullsize.jpeg'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_fullsize.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.Preview, + pathType: AssetFileType.Preview, oldPath: '/uploads/user-id/thumbs/path.jpg', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-preview.jpeg'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_preview.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.Thumbnail, + pathType: AssetFileType.Thumbnail, oldPath: '/uploads/user-id/webp/path.ext', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-thumbnail.webp'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_thumbnail.webp'), }); expect(mocks.move.create).toHaveBeenCalledTimes(3); }); @@ -385,11 +385,13 @@ describe(MediaService.name, () => { assetId: 'asset-id', type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, }, { assetId: 'asset-id', type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, }, ]); expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); @@ -421,11 +423,13 @@ describe(MediaService.name, () => { assetId: 'asset-id', type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, }, { assetId: 'asset-id', type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, }, ]); }); @@ -456,11 +460,13 @@ describe(MediaService.name, () => { assetId: 'asset-id', type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, }, { assetId: 'asset-id', type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, }, ]); }); @@ -548,8 +554,8 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = `/data/thumbs/user-id/as/se/asset-id-preview.${format}`; - const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id-thumbnail.webp`; + const previewPath = `/data/thumbs/user-id/as/se/asset-id_preview.${format}`; + const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id_thumbnail.webp`; await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -595,8 +601,8 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-preview.jpeg`); - const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-thumbnail.${format}`); + const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_preview.jpeg`); + const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_thumbnail.${format}`); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -1026,9 +1032,9 @@ describe(MediaService.name, () => { expect(mocks.asset.upsertFiles).toHaveBeenCalledWith( expect.arrayContaining([ - expect.objectContaining({ type: AssetFileType.FullSizeEdited }), - expect.objectContaining({ type: AssetFileType.PreviewEdited }), - expect.objectContaining({ type: AssetFileType.ThumbnailEdited }), + expect.objectContaining({ type: AssetFileType.FullSize, isEdited: true }), + expect.objectContaining({ type: AssetFileType.Preview, isEdited: true }), + expect.objectContaining({ type: AssetFileType.Thumbnail, isEdited: true }), ]), ); }); @@ -1098,17 +1104,17 @@ describe(MediaService.name, () => { expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, expect.anything(), - expect.stringContaining('edited_preview.jpeg'), + expect.stringContaining('preview_edited.jpeg'), ); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, expect.anything(), - expect.stringContaining('edited_thumbnail.webp'), + expect.stringContaining('thumbnail_edited.webp'), ); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, expect.anything(), - expect.stringContaining('edited_fullsize.jpeg'), + expect.stringContaining('fullsize_edited.jpeg'), ); }); @@ -3254,13 +3260,13 @@ describe(MediaService.name, () => { }; await sut['syncFiles'](asset, [ - { type: AssetFileType.Preview, newPath: '/new/preview.jpg' }, - { type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg' }, + { type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false }, + { type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg', isEdited: false }, ]); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ - { assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview }, - { assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail }, + { assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false }, + { assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail, isEdited: false }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled(); @@ -3270,19 +3276,31 @@ describe(MediaService.name, () => { const asset = { id: 'asset-id', files: [ - { id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }, - { id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' }, + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + }, ], }; await sut['syncFiles'](asset, [ - { type: AssetFileType.Preview, newPath: '/new/preview.jpg' }, - { type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg' }, + { type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false }, + { type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg', isEdited: false }, ]); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ - { assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview }, - { assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail }, + { assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false }, + { assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail, isEdited: false }, ]); expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -3295,17 +3313,38 @@ describe(MediaService.name, () => { const asset = { id: 'asset-id', files: [ - { id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }, - { id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' }, + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + }, ], }; - await sut['syncFiles'](asset, [{ type: AssetFileType.Preview }, { type: AssetFileType.Thumbnail }]); + await sut['syncFiles'](asset, [ + { type: AssetFileType.Preview, isEdited: false }, + { type: AssetFileType.Thumbnail, isEdited: false }, + ]); expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ - { id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }, - { id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' }, + { id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg', isEdited: false }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FileDelete, @@ -3317,14 +3356,26 @@ describe(MediaService.name, () => { const asset = { id: 'asset-id', files: [ - { id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/same/preview.jpg' }, - { id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/same/thumbnail.jpg' }, + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/same/preview.jpg', + isEdited: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/same/thumbnail.jpg', + isEdited: false, + }, ], }; await sut['syncFiles'](asset, [ - { type: AssetFileType.Preview, newPath: '/same/preview.jpg' }, - { type: AssetFileType.Thumbnail, newPath: '/same/thumbnail.jpg' }, + { type: AssetFileType.Preview, newPath: '/same/preview.jpg', isEdited: false }, + { type: AssetFileType.Thumbnail, newPath: '/same/thumbnail.jpg', isEdited: false }, ]); expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); @@ -3336,23 +3387,41 @@ describe(MediaService.name, () => { const asset = { id: 'asset-id', files: [ - { id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }, - { id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' }, + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + }, ], }; await sut['syncFiles'](asset, [ - { type: AssetFileType.Preview, newPath: '/new/preview.jpg' }, // replace - { type: AssetFileType.Thumbnail }, // delete - { type: AssetFileType.FullSize, newPath: '/new/fullsize.jpg' }, // new + { type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false }, // replace + { type: AssetFileType.Thumbnail, isEdited: false }, // delete + { type: AssetFileType.FullSize, newPath: '/new/fullsize.jpg', isEdited: false }, // new ]); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ - { assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview }, - { assetId: 'asset-id', path: '/new/fullsize.jpg', type: AssetFileType.FullSize }, + { assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false }, + { assetId: 'asset-id', path: '/new/fullsize.jpg', type: AssetFileType.FullSize, isEdited: false }, ]); expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ - { id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FileDelete, @@ -3376,11 +3445,19 @@ describe(MediaService.name, () => { it('should delete non-existent file types when newPath is not provided', async () => { const asset = { id: 'asset-id', - files: [{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }], + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + }, + ], }; await sut['syncFiles'](asset, [ - { type: AssetFileType.Thumbnail }, // file doesn't exist, newPath not provided + { type: AssetFileType.Thumbnail, isEdited: false }, // file doesn't exist, newPath not provided ]); expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index f66cbbaa0b..6d282004a3 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -8,7 +8,6 @@ import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetFileType, - AssetPathType, AssetType, AssetVisibility, AudioCodec, @@ -50,6 +49,7 @@ interface UpsertFileOptions { assetId: string; type: AssetFileType; path: string; + isEdited: boolean; } type ThumbnailAsset = NonNullable>>; @@ -160,9 +160,9 @@ export class MediaService extends BaseService { return JobStatus.Failed; } - await this.storageCore.moveAssetImage(asset, AssetPathType.FullSize, image.fullsize.format); - await this.storageCore.moveAssetImage(asset, AssetPathType.Preview, image.preview.format); - await this.storageCore.moveAssetImage(asset, AssetPathType.Thumbnail, image.thumbnail.format); + await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format); + await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format); + await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format); await this.storageCore.moveAssetVideo(asset); return JobStatus.Success; @@ -236,9 +236,9 @@ export class MediaService extends BaseService { } await this.syncFiles(asset, [ - { type: AssetFileType.Preview, newPath: generated.previewPath }, - { type: AssetFileType.Thumbnail, newPath: generated.thumbnailPath }, - { type: AssetFileType.FullSize, newPath: generated.fullsizePath }, + { type: AssetFileType.Preview, newPath: generated.previewPath, isEdited: false }, + { type: AssetFileType.Thumbnail, newPath: generated.thumbnailPath, isEdited: false }, + { type: AssetFileType.FullSize, newPath: generated.fullsizePath, isEdited: false }, ]); const editiedGenerated = await this.generateEditedThumbnails(asset); @@ -307,16 +307,16 @@ export class MediaService extends BaseService { private async generateImageThumbnails(asset: ThumbnailAsset, useEdits: boolean = false) { const { image } = await this.getConfig({ withCache: true }); - const previewPath = StorageCore.getImagePath( - asset, - useEdits ? AssetPathType.EditedPreview : AssetPathType.Preview, - image.preview.format, - ); - const thumbnailPath = StorageCore.getImagePath( - asset, - useEdits ? AssetPathType.EditedThumbnail : AssetPathType.Thumbnail, - image.thumbnail.format, - ); + const previewPath = StorageCore.getImagePath(asset, { + fileType: AssetFileType.Preview, + isEdited: useEdits, + format: image.preview.format, + }); + const thumbnailPath = StorageCore.getImagePath(asset, { + fileType: AssetFileType.Thumbnail, + isEdited: useEdits, + format: image.thumbnail.format, + }); this.storageCore.ensureFolders(previewPath); // Handle embedded preview extraction for RAW files @@ -343,11 +343,11 @@ export class MediaService extends BaseService { if (convertFullsize) { // convert a new fullsize image from the same source as the thumbnail - fullsizePath = StorageCore.getImagePath( - asset, - useEdits ? AssetPathType.EditedFullSize : AssetPathType.FullSize, - image.fullsize.format, - ); + fullsizePath = StorageCore.getImagePath(asset, { + fileType: AssetFileType.FullSize, + isEdited: useEdits, + format: image.fullsize.format, + }); const fullsizeOptions = { format: image.fullsize.format, quality: image.fullsize.quality, @@ -355,7 +355,11 @@ export class MediaService extends BaseService { }; promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath)); } else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) { - fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, extracted.format); + fullsizePath = StorageCore.getImagePath(asset, { + fileType: AssetFileType.FullSize, + format: extracted.format, + isEdited: false, + }); this.storageCore.ensureFolders(fullsizePath); // Write the buffer to disk with essential EXIF data @@ -489,8 +493,16 @@ export class MediaService extends BaseService { private async generateVideoThumbnails(asset: ThumbnailPathEntity & { originalPath: string }) { const { image, ffmpeg } = await this.getConfig({ withCache: true }); - const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format); - const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format); + const previewPath = StorageCore.getImagePath(asset, { + fileType: AssetFileType.Preview, + format: image.preview.format, + isEdited: false, + }); + const thumbnailPath = StorageCore.getImagePath(asset, { + fileType: AssetFileType.Thumbnail, + format: image.thumbnail.format, + isEdited: false, + }); this.storageCore.ensureFolders(previewPath); const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); @@ -779,18 +791,18 @@ export class MediaService extends BaseService { private async syncFiles( asset: { id: string; files: AssetFile[] }, - files: { type: AssetFileType; newPath?: string }[], + files: { type: AssetFileType; newPath?: string; isEdited: boolean }[], ) { const toUpsert: UpsertFileOptions[] = []; const pathsToDelete: string[] = []; const toDelete: AssetFile[] = []; - for (const { type, newPath } of files) { - const existingFile = asset.files.find((file) => file.type === type); + for (const { type, newPath, isEdited } of files) { + const existingFile = asset.files.find((file) => file.type === type && file.isEdited === isEdited); // upsert new file path if (newPath && existingFile?.path !== newPath) { - toUpsert.push({ assetId: asset.id, path: newPath, type }); + toUpsert.push({ assetId: asset.id, path: newPath, type, isEdited }); // delete old file from disk if (existingFile) { @@ -829,9 +841,9 @@ export class MediaService extends BaseService { const generated = asset.edits.length > 0 ? await this.generateImageThumbnails(asset, true) : undefined; await this.syncFiles(asset, [ - { type: AssetFileType.PreviewEdited, newPath: generated?.previewPath }, - { type: AssetFileType.ThumbnailEdited, newPath: generated?.thumbnailPath }, - { type: AssetFileType.FullSizeEdited, newPath: generated?.fullsizePath }, + { type: AssetFileType.Preview, newPath: generated?.previewPath, isEdited: true }, + { type: AssetFileType.Thumbnail, newPath: generated?.thumbnailPath, isEdited: true }, + { type: AssetFileType.FullSize, newPath: generated?.fullsizePath, isEdited: true }, ]); const crop = asset.edits.find((e) => e.action === AssetEditAction.Crop); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index b10325998e..6395e66e31 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -35,7 +35,7 @@ const forSidecarJob = ( asset: { id?: string; originalPath?: string; - files?: { id: string; type: AssetFileType; path: string }[]; + files?: { id: string; type: AssetFileType; path: string; isEdited: boolean }[]; } = {}, ) => { return { @@ -387,6 +387,7 @@ describe(MetadataService.name, () => { it('should extract tags from TagsList', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent'] } } as any); mockReadTags({ TagsList: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -397,6 +398,7 @@ describe(MetadataService.name, () => { it('should extract hierarchy from TagsList', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child'] } } as any); mockReadTags({ TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); @@ -417,6 +419,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a string', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent'] } } as any); mockReadTags({ Keywords: 'Parent' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -427,6 +430,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent'] } } as any); mockReadTags({ Keywords: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -437,6 +441,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list with a number', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent', '2024'] } } as any); mockReadTags({ Keywords: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -448,6 +453,7 @@ describe(MetadataService.name, () => { it('should extract hierarchal tags from Keywords', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child'] } } as any); mockReadTags({ Keywords: 'Parent/Child' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -467,6 +473,7 @@ describe(MetadataService.name, () => { it('should ignore Keywords when TagsList is present', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child', 'Child'] } } as any); mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -486,6 +493,7 @@ describe(MetadataService.name, () => { it('should extract hierarchy from HierarchicalSubject', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child', 'TagA'] } } as any); mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); @@ -507,6 +515,7 @@ describe(MetadataService.name, () => { it('should extract tags from HierarchicalSubject as a list with a number', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent', '2024'] } } as any); mockReadTags({ HierarchicalSubject: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -518,6 +527,7 @@ describe(MetadataService.name, () => { it('should extract ignore / characters in a HierarchicalSubject tag', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Mom|Dad'] } } as any); mockReadTags({ HierarchicalSubject: ['Mom/Dad'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); @@ -532,6 +542,7 @@ describe(MetadataService.name, () => { it('should ignore HierarchicalSubject when TagsList is present', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.asset.getById.mockResolvedValue({ exifInfo: { tags: ['Parent/Child', 'Parent2/Child2'] } } as any); mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -896,6 +907,7 @@ describe(MetadataService.name, () => { ProfileDescription: 'extensive description', ProjectionType: 'equirectangular', tz: 'UTC-11:30', + TagsList: ['parent/child'], Rating: 3, }; @@ -935,6 +947,7 @@ describe(MetadataService.name, () => { country: null, state: null, city: null, + tags: ['parent/child'], }, { lockedPropertiesBehavior: 'skip' }, ); @@ -1084,6 +1097,7 @@ describe(MetadataService.name, () => { id: 'some-id', type: AssetFileType.Sidecar, path: '/path/to/something', + isEdited: false, }, ], }); @@ -1691,7 +1705,7 @@ describe(MetadataService.name, () => { it('should unset sidecar path if file no longer exist', async () => { const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', - files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }], }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValue(false); @@ -1704,7 +1718,7 @@ describe(MetadataService.name, () => { it('should do nothing if the sidecar file still exists', async () => { const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', - files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }], }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index e6cc15bc77..f74f9f4cec 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -254,6 +254,8 @@ export class MetadataService extends BaseService { } } + const tags = this.getTagList(exifTags); + const exifData: Insertable = { assetId: asset.id, @@ -296,6 +298,8 @@ export class MetadataService extends BaseService { // grouping livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, autoStackId: this.getAutoStackId(exifTags), + + tags: tags.length > 0 ? tags : null, }; const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation); @@ -316,9 +320,10 @@ export class MetadataService extends BaseService { width: asset.width == null ? assetWidth : undefined, height: asset.height == null ? assetHeight : undefined, }), - this.applyTagList(asset, exifTags), ]; + await this.applyTagList(asset); + if (this.isMotionPhoto(asset, exifTags)) { promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats)); } @@ -405,35 +410,35 @@ export class MetadataService extends BaseService { @OnEvent({ name: 'AssetTag' }) async handleTagAsset({ assetId }: ArgOf<'AssetTag'>) { - await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId, tags: true } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId } }); } @OnEvent({ name: 'AssetUntag' }) async handleUntagAsset({ assetId }: ArgOf<'AssetUntag'>) { - await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId, tags: true } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId } }); } @OnJob({ name: JobName.SidecarWrite, queue: QueueName.Sidecar }) async handleSidecarWrite(job: JobOf): Promise { - const { id, tags } = job; + const { id } = job; const asset = await this.assetJobRepository.getForSidecarWriteJob(id); if (!asset) { return JobStatus.Failed; } const lockedProperties = await this.assetJobRepository.getLockedPropertiesForMetadataExtraction(id); - const tagsList = (asset.tags || []).map((tag) => tag.value); const { sidecarFile } = getAssetFiles(asset.files); const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`; - const { description, dateTimeOriginal, latitude, longitude, rating } = _.pick( + const { description, dateTimeOriginal, latitude, longitude, rating, tags } = _.pick( { description: asset.exifInfo.description, dateTimeOriginal: asset.exifInfo.dateTimeOriginal, latitude: asset.exifInfo.latitude, longitude: asset.exifInfo.longitude, rating: asset.exifInfo.rating, + tags: asset.exifInfo.tags, }, lockedProperties, ); @@ -446,7 +451,7 @@ export class MetadataService extends BaseService { GPSLatitude: latitude, GPSLongitude: longitude, Rating: rating, - TagsList: tags ? tagsList : undefined, + TagsList: tags?.length ? tags : undefined, }, _.isUndefined, ); @@ -560,11 +565,14 @@ export class MetadataService extends BaseService { return tags; } - private async applyTagList(asset: { id: string; ownerId: string }, exifTags: ImmichTags) { - const tags = this.getTagList(exifTags); - const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); + private async applyTagList({ id, ownerId }: { id: string; ownerId: string }) { + const asset = await this.assetRepository.getById(id, { exifInfo: true }); + const results = await upsertTags(this.tagRepository, { + userId: ownerId, + tags: asset?.exifInfo?.tags ?? [], + }); await this.tagRepository.replaceAssetTags( - asset.id, + id, results.map((tag) => tag.id), ); } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index daa3f221ae..6627ffea8a 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -372,7 +372,7 @@ describe(NotificationService.name, () => { mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ - { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' }, + { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg', isEdited: false }, ]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); @@ -403,7 +403,7 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([{ ...assetStub.image.files[2], isEdited: false }]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index eff16fea45..d484fe8b6a 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -117,7 +117,7 @@ export class SmartInfoService extends BaseService { const newConfig = await this.getConfig({ withCache: true }); if (machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) { - // Skip the job if the the model has changed since the embedding was generated. + // Skip the job if the model has changed since the embedding was generated. return JobStatus.Skipped; } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index cd641d7036..d5020a9c5e 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -240,11 +240,11 @@ export class StorageTemplateService extends BaseService { assetInfo: { sizeInBytes: fileSizeInByte, checksum }, }); - const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar)?.path; + const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar, { isEdited: false })?.path; if (sidecarPath) { await this.storageCore.moveFile({ entityId: id, - pathType: AssetPathType.Sidecar, + pathType: AssetFileType.Sidecar, oldPath: sidecarPath, newPath: `${newPath}.xmp`, }); diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index 6bb92abd8c..ff706552a9 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -191,6 +191,7 @@ describe(TagService.name, () => { it('should upsert records', async () => { mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2'])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); + mocks.asset.getById.mockResolvedValue({ tags: [{ value: 'tag-1' }, { value: 'tag-2' }] } as any); mocks.tag.upsertAssetIds.mockResolvedValue([ { tagId: 'tag-1', assetId: 'asset-1' }, { tagId: 'tag-1', assetId: 'asset-2' }, @@ -204,6 +205,18 @@ describe(TagService.name, () => { ).resolves.toEqual({ count: 6, }); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-1', tags: ['tag-1', 'tag-2'] }, + { lockedPropertiesBehavior: 'append' }, + ); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-2', tags: ['tag-1', 'tag-2'] }, + { lockedPropertiesBehavior: 'append' }, + ); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-3', tags: ['tag-1', 'tag-2'] }, + { lockedPropertiesBehavior: 'append' }, + ); expect(mocks.tag.upsertAssetIds).toHaveBeenCalledWith([ { tagId: 'tag-1', assetId: 'asset-1' }, { tagId: 'tag-1', assetId: 'asset-2' }, @@ -229,6 +242,7 @@ describe(TagService.name, () => { mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); mocks.tag.addAssetIds.mockResolvedValue(); + mocks.asset.getById.mockResolvedValue({ tags: [{ value: 'tag-1' }] } as any); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); await expect( @@ -240,6 +254,14 @@ describe(TagService.name, () => { { id: 'asset-2', success: true }, ]); + expect(mocks.asset.upsertExif).not.toHaveBeenCalledWith( + { assetId: 'asset-1', tags: ['tag-1'] }, + { lockedPropertiesBehavior: 'append' }, + ); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-2', tags: ['tag-1'] }, + { lockedPropertiesBehavior: 'append' }, + ); expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); expect(mocks.tag.addAssetIds).toHaveBeenCalledWith('tag-1', ['asset-2']); }); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 3ee5d29b75..3b3000f759 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -90,6 +90,7 @@ export class TagService extends BaseService { const results = await this.tagRepository.upsertAssetIds(items); for (const assetId of new Set(results.map((item) => item.assetId))) { + await this.updateTags(assetId); await this.eventRepository.emit('AssetTag', { assetId }); } @@ -107,6 +108,7 @@ export class TagService extends BaseService { for (const { id: assetId, success } of results) { if (success) { + await this.updateTags(assetId); await this.eventRepository.emit('AssetTag', { assetId }); } } @@ -125,6 +127,7 @@ export class TagService extends BaseService { for (const { id: assetId, success } of results) { if (success) { + await this.updateTags(assetId); await this.eventRepository.emit('AssetUntag', { assetId }); } } @@ -145,4 +148,12 @@ export class TagService extends BaseService { } return tag; } + + private async updateTags(assetId: string) { + const asset = await this.assetRepository.getById(assetId, { tags: true }); + await this.assetRepository.upsertExif( + { assetId, tags: asset?.tags?.map(({ value }) => value) ?? [] }, + { lockedPropertiesBehavior: 'append' }, + ); + } } diff --git a/server/src/types.ts b/server/src/types.ts index 560d168667..177af8be6c 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -4,6 +4,7 @@ import { Asset, AssetFile } from 'src/database'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; +import { SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { AssetOrder, AssetType, @@ -357,7 +358,7 @@ export type JobItem = // Sidecar Scanning | { name: JobName.SidecarQueueAll; data: IBaseJob } | { name: JobName.SidecarCheck; data: IEntityJob } - | { name: JobName.SidecarWrite; data: ISidecarWriteJob } + | { name: JobName.SidecarWrite; data: IEntityJob } // Facial Recognition | { name: JobName.AssetDetectFacesQueueAll; data: IBaseJob } @@ -533,7 +534,9 @@ export interface MemoryData { export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }; export type SystemFlags = { mountChecks: Record }; -export type MaintenanceModeState = { isMaintenanceMode: true; secret: string } | { isMaintenanceMode: false }; +export type MaintenanceModeState = + | { isMaintenanceMode: true; secret: string; action: SetMaintenanceModeDto } + | { isMaintenanceMode: false }; export type MemoriesState = { /** memories have already been created through this date */ lastOnThisDayDate: string; diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 94f7f231a8..f8fb3d215d 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { GeneratedImageType, StorageCore } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { AssetFile, Exif } from 'src/database'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; @@ -14,19 +14,19 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { IBulkAsset, ImmichFile, UploadFile, UploadRequest } from 'src/types'; import { checkAccess } from 'src/utils/access'; -export const getAssetFile = (files: AssetFile[], type: AssetFileType | GeneratedImageType) => { - return files.find((file) => file.type === type); +export const getAssetFile = (files: AssetFile[], type: AssetFileType, { isEdited }: { isEdited: boolean }) => { + return files.find((file) => file.type === type && file.isEdited === isEdited); }; export const getAssetFiles = (files: AssetFile[]) => ({ - fullsizeFile: getAssetFile(files, AssetFileType.FullSize), - previewFile: getAssetFile(files, AssetFileType.Preview), - thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail), - sidecarFile: getAssetFile(files, AssetFileType.Sidecar), + fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }), + previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }), + thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }), + sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }), - editedFullsizeFile: getAssetFile(files, AssetFileType.FullSizeEdited), - editedPreviewFile: getAssetFile(files, AssetFileType.PreviewEdited), - editedThumbnailFile: getAssetFile(files, AssetFileType.ThumbnailEdited), + editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }), + editedPreviewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }), + editedThumbnailFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }), }); export const addAssets = async ( diff --git a/server/src/utils/database-backups.ts b/server/src/utils/database-backups.ts new file mode 100644 index 0000000000..83d52fc531 --- /dev/null +++ b/server/src/utils/database-backups.ts @@ -0,0 +1,494 @@ +import { BadRequestException } from '@nestjs/common'; +import { debounce } from 'lodash'; +import { DateTime } from 'luxon'; +import path, { basename, join } from 'node:path'; +import { PassThrough, Readable, Writable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import semver from 'semver'; +import { serverVersion } from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { CacheControl, StorageFolder } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; + +export function isValidDatabaseBackupName(filename: string) { + return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/); +} + +export function isValidDatabaseRoutineBackupName(filename: string) { + const oldBackupStyle = filename.match(/^immich-db-backup-\d+\.sql\.gz$/); + //immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz + const newBackupStyle = filename.match(/^immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/); + return oldBackupStyle || newBackupStyle; +} + +export function isFailedDatabaseBackupName(filename: string) { + return filename.match(/^immich-db-backup-.*\.sql\.gz\.tmp$/); +} + +export function findVersion(filename: string) { + return /-v(.*)-/.exec(filename)?.[1]; +} + +type BackupRepos = { + logger: LoggingRepository; + storage: StorageRepository; + config: ConfigRepository; + process: ProcessRepository; + database: DatabaseRepository; + health: MaintenanceHealthRepository; +}; + +export class UnsupportedPostgresError extends Error { + constructor(databaseVersion: string) { + super(`Unsupported PostgreSQL version: ${databaseVersion}`); + } +} + +export async function buildPostgresLaunchArguments( + { logger, config, database }: Pick, + bin: 'pg_dump' | 'pg_dumpall' | 'psql', + options: { + singleTransaction?: boolean; + username?: string; + } = {}, +): Promise<{ + bin: string; + args: string[]; + databasePassword: string; + databaseVersion: string; + databaseMajorVersion?: number; +}> { + const { + database: { config: databaseConfig }, + } = config.getEnv(); + const isUrlConnection = databaseConfig.connectionType === 'url'; + + const databaseVersion = await database.getPostgresVersion(); + const databaseSemver = semver.coerce(databaseVersion); + const databaseMajorVersion = databaseSemver?.major; + + const args: string[] = []; + + if (isUrlConnection) { + if (bin !== 'pg_dump') { + args.push('--dbname'); + } + + let url = databaseConfig.url; + if (URL.canParse(databaseConfig.url)) { + const parsedUrl = new URL(databaseConfig.url); + // remove known bad parameters + parsedUrl.searchParams.delete('uselibpqcompat'); + + if (options.username) { + parsedUrl.username = options.username; + } + + url = parsedUrl.toString(); + } + + args.push(url); + } else { + args.push( + '--username', + options.username ?? databaseConfig.username, + '--host', + databaseConfig.host, + '--port', + databaseConfig.port.toString(), + ); + + switch (bin) { + case 'pg_dumpall': { + args.push('--database'); + break; + } + case 'psql': { + args.push('--dbname'); + break; + } + } + + args.push(databaseConfig.database); + } + + switch (bin) { + case 'pg_dump': + case 'pg_dumpall': { + args.push('--clean', '--if-exists'); + break; + } + case 'psql': { + if (options.singleTransaction) { + args.push( + // don't commit any transaction on failure + '--single-transaction', + // exit with non-zero code on error + '--set', + 'ON_ERROR_STOP=on', + ); + } + + args.push( + // used for progress monitoring + '--echo-all', + '--output=/dev/null', + ); + break; + } + } + + if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <19.0.0')) { + logger.error(`Database Restore Failure: Unsupported PostgreSQL version: ${databaseVersion}`); + throw new UnsupportedPostgresError(databaseVersion); + } + + return { + bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, + args, + databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, + databaseVersion, + databaseMajorVersion, + }; +} + +export async function createDatabaseBackup( + { logger, storage, process: processRepository, ...pgRepos }: Omit, + filenamePrefix: string = '', +): Promise { + logger.debug(`Database Backup Started`); + + const { bin, args, databasePassword, databaseVersion, databaseMajorVersion } = await buildPostgresLaunchArguments( + { logger, ...pgRepos }, + 'pg_dump', + ); + + logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); + + const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`; + const backupFilePath = join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + const temporaryFilePath = `${backupFilePath}.tmp`; + + try { + const pgdump = processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const gzip = processRepository.spawnDuplexStream('gzip', ['--rsyncable']); + const fileStream = storage.createWriteStream(temporaryFilePath); + + await pipeline(pgdump, gzip, fileStream); + await storage.rename(temporaryFilePath, backupFilePath); + } catch (error) { + logger.error(`Database Backup Failure: ${error}`); + await storage + .unlink(temporaryFilePath) + .catch((error) => logger.error(`Failed to delete failed backup file: ${error}`)); + throw error; + } + + logger.log(`Database Backup Success`); + return backupFilePath; +} + +const SQL_DROP_CONNECTIONS = ` + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); +`; + +const SQL_RESET_SCHEMA = ` + -- re-create the default schema + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + + -- restore access to schema + GRANT ALL ON SCHEMA public TO postgres; + GRANT ALL ON SCHEMA public TO public; +`; + +async function* sql(inputStream: Readable, isPgClusterDump: boolean) { + yield SQL_DROP_CONNECTIONS; + yield isPgClusterDump + ? String.raw` + \c postgres + ` + : SQL_RESET_SCHEMA; + + for await (const chunk of inputStream) { + yield chunk; + } +} + +async function* sqlRollback(inputStream: Readable, isPgClusterDump: boolean) { + yield SQL_DROP_CONNECTIONS; + + if (isPgClusterDump) { + yield String.raw` + -- try to create database + -- may fail but script will continue running + CREATE DATABASE immich; + + -- switch to database / newly created database + \c immich + `; + } + + yield SQL_RESET_SCHEMA; + + for await (const chunk of inputStream) { + yield chunk; + } +} + +export async function restoreDatabaseBackup( + { logger, storage, process: processRepository, database: databaseRepository, health, ...pgRepos }: BackupRepos, + filename: string, + progressCb?: (action: 'backup' | 'restore' | 'migrations' | 'rollback', progress: number) => void, +): Promise { + logger.debug(`Database Restore Started`); + + let complete = false; + try { + if (!isValidDatabaseBackupName(filename)) { + throw new Error('Invalid backup file format!'); + } + + const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + await storage.stat(backupFilePath); // => check file exists + + let isPgClusterDump = false; + const version = findVersion(filename); + if (version && semver.satisfies(version, '<= 2.4')) { + isPgClusterDump = true; + } + + const { bin, args, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments( + { logger, database: databaseRepository, ...pgRepos }, + 'psql', + { + singleTransaction: !isPgClusterDump, + username: isPgClusterDump ? 'postgres' : undefined, + }, + ); + + progressCb?.('backup', 0.05); + + const restorePointFilePath = await createDatabaseBackup( + { logger, storage, process: processRepository, database: databaseRepository, ...pgRepos }, + 'restore-point-', + ); + + logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`); + + let inputStream: Readable; + if (backupFilePath.endsWith('.gz')) { + const fileStream = storage.createPlainReadStream(backupFilePath); + const gunzip = storage.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + } else { + inputStream = storage.createPlainReadStream(backupFilePath); + } + + const sqlStream = Readable.from(sql(inputStream, isPgClusterDump)); + const psql = processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + logger.log(`Restore progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('restore', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + try { + progressCb?.('migrations', 0.9); + await databaseRepository.runMigrations(); + await health.checkApiHealth(); + } catch (error) { + progressCb?.('rollback', 0); + + const fileStream = storage.createPlainReadStream(restorePointFilePath); + const gunzip = storage.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + + const sqlStream = Readable.from(sqlRollback(inputStream, isPgClusterDump)); + const psql = processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + logger.log(`Rollback progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('rollback', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + throw error; + } + } catch (error) { + logger.error(`Database Restore Failure: ${error}`); + throw error; + } finally { + complete = true; + } + + logger.log(`Database Restore Success`); +} + +export async function deleteDatabaseBackup({ storage }: Pick, files: string[]): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + + if (files.some((filename) => !isValidDatabaseBackupName(filename))) { + throw new BadRequestException('Invalid backup name!'); + } + + await Promise.all(files.map((filename) => storage.unlink(path.join(backupsFolder, filename)))); +} + +export async function listDatabaseBackups({ + storage, +}: Pick): Promise<{ filename: string; filesize: number }[]> { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const files = await storage.readdir(backupsFolder); + + const validFiles = files + .filter((fn) => isValidDatabaseBackupName(fn)) + .toSorted((a, b) => (a.startsWith('uploaded-') === b.startsWith('uploaded-') ? a.localeCompare(b) : 1)) + .toReversed(); + + const backups = await Promise.all( + validFiles.map(async (filename) => { + const stats = await storage.stat(path.join(backupsFolder, filename)); + return { filename, filesize: stats.size }; + }), + ); + + return backups; +} + +export async function uploadDatabaseBackup( + { storage }: Pick, + file: Express.Multer.File, +): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const fn = basename(file.originalname); + if (!isValidDatabaseBackupName(fn)) { + throw new BadRequestException('Invalid backup name!'); + } + + const path = join(backupsFolder, `uploaded-${fn}`); + await storage.createOrOverwriteFile(path, file.buffer); +} + +export function downloadDatabaseBackup(fileName: string) { + if (!isValidDatabaseBackupName(fileName)) { + throw new BadRequestException('Invalid backup name!'); + } + + const path = join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName); + + return { + path, + fileName, + cacheControl: CacheControl.PrivateWithoutCache, + contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql', + }; +} + +function createSqlProgressStreams(cb: (progress: number) => void) { + const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin'); + const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`); + + let readingStdin = false; + let sequenceIdx = 0; + + let linesSent = 0; + let linesProcessed = 0; + + const startedAt = +Date.now(); + const cbDebounced = debounce( + () => { + const progress = source.writableEnded + ? Math.min(1, linesProcessed / linesSent) + : // progress simulation while we're in an indeterminate state + Math.min(0.3, 0.1 + (Date.now() - startedAt) / 1e4); + cb(progress); + }, + 100, + { + maxWait: 100, + }, + ); + + let lastByte = -1; + const source = new PassThrough({ + transform(chunk, _encoding, callback) { + for (const byte of chunk) { + if (!readingStdin && byte === 10 && lastByte !== 10) { + linesSent += 1; + } + + lastByte = byte; + + const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER; + if (sequence[sequenceIdx] === byte) { + sequenceIdx += 1; + + if (sequence.length === sequenceIdx) { + sequenceIdx = 0; + readingStdin = !readingStdin; + } + } else { + sequenceIdx = 0; + } + } + + cbDebounced(); + this.push(chunk); + callback(); + }, + }); + + const sink = new Writable({ + write(chunk, _encoding, callback) { + for (const byte of chunk) { + if (byte === 10) { + linesProcessed++; + } + } + + cbDebounced(); + callback(); + }, + }); + + return [source, sink]; +} diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index 29c7f6f772..6ec78b0f21 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -42,7 +42,7 @@ const cacheControlHeaders: Record = { export const sendFile = async ( res: Response, next: NextFunction, - handler: () => Promise, + handler: () => Promise | ImmichFileResponse, logger: LoggingRepository, ): Promise => { // promisified version of 'res.sendFile' for cleaner async handling diff --git a/server/src/utils/maintenance.ts b/server/src/utils/maintenance.ts index faa92395d6..47abb0ab89 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -1,6 +1,59 @@ +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis from 'ioredis'; import { SignJWT } from 'jose'; import { randomBytes } from 'node:crypto'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; +import { join } from 'node:path'; +import { Server as SocketIO } from 'socket.io'; +import { StorageCore } from 'src/cores/storage.core'; +import { MaintenanceAuthDto, MaintenanceDetectInstallResponseDto } from 'src/dtos/maintenance.dto'; +import { StorageFolder } from 'src/enum'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { AppRestartEvent } from 'src/repositories/event.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; + +export function sendOneShotAppRestart(state: AppRestartEvent): void { + const server = new SocketIO(); + const { redis } = new ConfigRepository().getEnv(); + const pubClient = new Redis(redis); + const subClient = pubClient.duplicate(); + server.adapter(createAdapter(pubClient, subClient)); + + /** + * Keep trying until we manage to stop Immich + * + * Sometimes there appear to be communication + * issues between to the other servers. + * + * This issue only occurs with this method. + */ + async function tryTerminate() { + while (true) { + try { + const responses = await server.serverSideEmitWithAck('AppRestart', state); + if (responses.length > 0) { + return; + } + } catch (error) { + console.error(error); + console.error('Encountered an error while telling Immich to stop.'); + } + + console.info( + "\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.", + ); + + await new Promise((r) => setTimeout(r, 1e3)); + } + } + + // => corresponds to notification.service.ts#onAppRestart + server.emit('AppRestartV1', state, () => { + void tryTerminate().finally(() => { + pubClient.disconnect(); + subClient.disconnect(); + }); + }); +} export async function createMaintenanceLoginUrl( baseUrl: string, @@ -23,3 +76,37 @@ export async function signMaintenanceJwt(secret: string, data: MaintenanceAuthDt export function generateMaintenanceSecret(): string { return randomBytes(64).toString('hex'); } + +export async function detectPriorInstall( + storageRepository: StorageRepository, +): Promise { + return { + storage: await Promise.all( + Object.values(StorageFolder).map(async (folder) => { + const path = StorageCore.getBaseFolder(folder); + const files = await storageRepository.readdir(path); + const filename = join(StorageCore.getBaseFolder(folder), '.immich'); + + let readable = false, + writable = false; + + try { + await storageRepository.readFile(filename); + readable = true; + + await storageRepository.overwriteFile(filename, Buffer.from(`${Date.now()}`)); + writable = true; + } catch { + // no-op + } + + return { + folder, + readable, + writable, + files: files.filter((fn) => fn !== '.immich').length, + }; + }), + ), + }; +} diff --git a/server/src/validation.ts b/server/src/validation.ts index 0c76866253..1a45a06b9f 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -153,6 +153,16 @@ export class IntegrityReportTypeParamDto { type!: IntegrityReportType; } +export class FilenameParamDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ format: 'string' }) + @Matches(/^[a-zA-Z0-9_\-.]+$/, { + message: 'Filename contains invalid characters', + }) + filename!: string; +} + type PinCodeOptions = { optional?: boolean } & OptionalOptions; export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => { const { optional, nullable, emptyToNull, ...apiPropertyOptions } = { diff --git a/server/src/workers/maintenance.ts b/server/src/workers/maintenance.ts index fcfe990121..035ec600af 100644 --- a/server/src/workers/maintenance.ts +++ b/server/src/workers/maintenance.ts @@ -12,12 +12,11 @@ async function bootstrap() { const app = await NestFactory.create(MaintenanceModule, { bufferLogs: true }); app.get(AppRepository).setCloseFn(() => app.close()); + void configureExpress(app, { permitSwaggerWrite: false, ssr: MaintenanceWorkerService, }); - - void app.get(MaintenanceWorkerService).logSecret(); } bootstrap().catch((error) => { diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 3478e31fe9..920b82f6e5 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -31,18 +31,21 @@ const sidecarFileWithoutExt = factory.assetFile({ }); const editedPreviewFile = factory.assetFile({ - type: AssetFileType.PreviewEdited, + type: AssetFileType.Preview, path: '/uploads/user-id/preview/path_edited.jpg', + isEdited: true, }); const editedThumbnailFile = factory.assetFile({ - type: AssetFileType.ThumbnailEdited, + type: AssetFileType.Thumbnail, path: '/uploads/user-id/thumbnail/path_edited.jpg', + isEdited: true, }); const editedFullsizeFile = factory.assetFile({ - type: AssetFileType.FullSizeEdited, + type: AssetFileType.FullSize, path: '/uploads/user-id/fullsize/path_edited.jpg', + isEdited: true, }); const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile]; @@ -86,6 +89,7 @@ export const assetStub = { make: 'FUJIFILM', model: 'X-T50', lensModel: 'XF27mm F2.8 R WR', + isEdited: false, ...asset, }), noResizePath: Object.freeze({ @@ -125,6 +129,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), noWebpPath: Object.freeze({ @@ -166,6 +171,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), noThumbhash: Object.freeze({ @@ -204,6 +210,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), primaryImage: Object.freeze({ @@ -252,6 +259,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), image: Object.freeze({ @@ -298,6 +306,7 @@ export const assetStub = { width: null, visibility: AssetVisibility.Timeline, edits: [], + isEdited: false, }), trashed: Object.freeze({ @@ -341,6 +350,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), trashedOffline: Object.freeze({ @@ -384,6 +394,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), archived: Object.freeze({ id: 'asset-id', @@ -426,6 +437,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), external: Object.freeze({ @@ -468,6 +480,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), image1: Object.freeze({ @@ -510,6 +523,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), imageFrom2015: Object.freeze({ @@ -551,6 +565,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), video: Object.freeze({ @@ -594,6 +609,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), livePhotoMotionAsset: Object.freeze({ @@ -614,6 +630,7 @@ export const assetStub = { width: null, height: null, edits: [] as AssetEditActionItem[], + isEdited: false, } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: AssetEditActionItem[] }), livePhotoStillAsset: Object.freeze({ @@ -635,6 +652,7 @@ export const assetStub = { width: null, height: null, edits: [] as AssetEditActionItem[], + isEdited: false, } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }), livePhotoWithOriginalFileName: Object.freeze({ @@ -658,6 +676,7 @@ export const assetStub = { width: null, height: null, edits: [] as AssetEditActionItem[], + isEdited: false, } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }), withLocation: Object.freeze({ @@ -705,6 +724,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), sidecar: Object.freeze({ @@ -743,6 +763,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), sidecarWithoutExt: Object.freeze({ @@ -778,6 +799,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), hasEncodedVideo: Object.freeze({ @@ -820,6 +842,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), hasFileExtension: Object.freeze({ @@ -859,6 +882,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), imageDng: Object.freeze({ @@ -902,6 +926,7 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), imageHif: Object.freeze({ @@ -945,7 +970,9 @@ export const assetStub = { width: null, height: null, edits: [], + isEdited: false, }), + panoramaTif: Object.freeze({ id: 'asset-id', status: AssetStatus.Active, @@ -988,6 +1015,7 @@ export const assetStub = { height: null, edits: [], }), + withCropEdit: Object.freeze({ id: 'asset-id', status: AssetStatus.Active, @@ -1043,7 +1071,9 @@ export const assetStub = { }, }, ] as AssetEditActionItem[], + isEdited: true, }), + withoutEdits: Object.freeze({ id: 'asset-id', status: AssetStatus.Active, @@ -1089,5 +1119,6 @@ export const assetStub = { width: 2160, visibility: AssetVisibility.Timeline, edits: [], + isEdited: false, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 6aa76dd4dc..859b6b6ae2 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -147,6 +147,7 @@ export const sharedLinkStub = { visibility: AssetVisibility.Timeline, width: 500, height: 500, + tags: [], }, sharedLinks: [], faces: [], @@ -159,6 +160,7 @@ export const sharedLinkStub = { visibility: AssetVisibility.Timeline, width: 500, height: 500, + isEdited: false, }, ], albumId: null, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 17b0e232b6..ac3ffed794 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -19,6 +19,7 @@ import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; @@ -384,6 +385,7 @@ const newRealRepository = (key: ClassConstructor, db: Kysely): T => { case AlbumUserRepository: case ActivityRepository: case AssetRepository: + case AssetEditRepository: case AssetJobRepository: case MemoryRepository: case NotificationRepository: @@ -535,6 +537,7 @@ const assetInsert = (asset: Partial> = {}) => { fileModifiedAt: now, localDateTime: now, visibility: AssetVisibility.Timeline, + isEdited: false, }; return { diff --git a/server/test/medium/specs/repositories/asset-edit.repository.spec.ts b/server/test/medium/specs/repositories/asset-edit.repository.spec.ts new file mode 100644 index 0000000000..512c6c73f4 --- /dev/null +++ b/server/test/medium/specs/repositories/asset-edit.repository.spec.ts @@ -0,0 +1,115 @@ +import { Kysely } from 'kysely'; +import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(AssetEditRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetEditRepository.name, () => { + describe('replaceAll', () => { + it('should set isEdited on insert', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + }); + + it('should set isEdited when inserting multiple edits', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + }); + + it('should keep isEdited when removing some edits', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + }); + + it('should set isEdited to false if all edits are deleted', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + await sut.replaceAll(asset.id, []); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + }); + }); +}); diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index 6c094c1121..123b6f9484 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -83,6 +83,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => { libraryId: asset.libraryId, width: asset.width, height: asset.height, + isEdited: asset.isEdited, }, type: SyncEntityType.AlbumAssetCreateV1, }, diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index acba274b4f..a1a898d9b3 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -64,6 +64,7 @@ describe(SyncEntityType.AssetV1, () => { libraryId: asset.libraryId, width: asset.width, height: asset.height, + isEdited: asset.isEdited, }, type: 'AssetV1', }, diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index 421423a741..345d4a1e29 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -63,6 +63,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => { type: asset.type, visibility: asset.visibility, duration: asset.duration, + isEdited: asset.isEdited, stackId: null, livePhotoVideoId: null, libraryId: asset.libraryId, diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index ae9f692ac4..f4eb981fc9 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -50,6 +50,9 @@ export const newStorageRepositoryMock = (): Mocked = {}) => ({ visibility: AssetVisibility.Timeline, width: null, height: null, + isEdited: false, ...asset, }); @@ -334,6 +335,7 @@ const assetSidecarWriteFactory = () => { id: newUuid(), path: '/path/to/original-path.jpg.xmp', type: AssetFileType.Sidecar, + isEdited: false, }, ], exifInfo: { @@ -385,6 +387,7 @@ const assetFileFactory = (file: Partial = {}): AssetFile => ({ id: newUuid(), type: AssetFileType.Preview, path: '/uploads/user-id/thumbs/path.jpg', + isEdited: false, ...file, }); diff --git a/server/test/utils.ts b/server/test/utils.ts index 3bbb497a7f..0a5715071b 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -7,7 +7,7 @@ import { NextFunction } from 'express'; import { Kysely } from 'kysely'; import multer from 'multer'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; -import { Readable, Writable } from 'node:stream'; +import { Duplex, Readable, Writable } from 'node:stream'; import { PNG } from 'pngjs'; import postgres from 'postgres'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; @@ -500,6 +500,74 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st } as unknown as ChildProcessWithoutNullStreams; }); +export const mockDuplex = vitest.fn( + (command: string, exitCode: number, stdout: string, stderr: string, error?: unknown) => { + const duplex = new Duplex({ + write(_chunk, _encoding, callback) { + callback(); + }, + + read() {}, + + final(callback) { + callback(); + }, + }); + + setImmediate(() => { + if (error) { + duplex.destroy(error as Error); + } else if (exitCode === 0) { + /* eslint-disable unicorn/prefer-single-call */ + duplex.push(stdout); + duplex.push(null); + /* eslint-enable unicorn/prefer-single-call */ + } else { + duplex.destroy(new Error(`${command} non-zero exit code (${exitCode})\n${stderr}`)); + } + }); + + return duplex; + }, +); + +export const mockFork = vitest.fn((exitCode: number, stdout: string, stderr: string, error?: unknown) => { + const stdoutStream = new Readable({ + read() { + this.push(stdout); // write mock data to stdout + this.push(null); // end stream + }, + }); + + return { + stdout: stdoutStream, + stderr: new Readable({ + read() { + this.push(stderr); // write mock data to stderr + this.push(null); // end stream + }, + }), + stdin: new Writable({ + write(chunk, encoding, callback) { + callback(); + }, + }), + exitCode, + on: vitest.fn((event, callback: any) => { + if (event === 'close') { + stdoutStream.once('end', () => callback(0)); + } + if (event === 'error' && error) { + stdoutStream.once('end', () => callback(error)); + } + if (event === 'exit') { + stdoutStream.once('end', () => callback(exitCode)); + } + }), + kill: vitest.fn(), + } as unknown as ChildProcessWithoutNullStreams; +}); + export async function* makeStream(items: T[] = []): AsyncIterableIterator { for (const item of items) { await Promise.resolve(); diff --git a/web/package.json b/web/package.json index d0971ac3fc..a97365f9fe 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.58.4", + "@immich/ui": "^0.59.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", @@ -71,7 +71,7 @@ "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.9.0", "@sveltejs/kit": "^2.27.1", - "@sveltejs/vite-plugin-svelte": "6.2.3", + "@sveltejs/vite-plugin-svelte": "6.2.4", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", diff --git a/web/src/lib/actions/scroll-memory.ts b/web/src/lib/actions/scroll-memory.ts index 1c19fdd8ab..9953bf00fb 100644 --- a/web/src/lib/actions/scroll-memory.ts +++ b/web/src/lib/actions/scroll-memory.ts @@ -1,14 +1,12 @@ import { navigating } from '$app/stores'; -import { AppRoute, SessionStorageKey } from '$lib/constants'; +import { SessionStorageKey } from '$lib/constants'; import { handlePromiseError } from '$lib/utils'; interface Options { /** - * {@link AppRoute} for subpages that scroll state should be kept while visiting. - * * This must be kept the same in all subpages of this route for the scroll memory clearer to work. */ - routeStartsWith: AppRoute; + routeStartsWith: string; /** * Function to clear additional data/state before scrolling (ex infinite scroll). */ diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index e67d3e1928..36cce538d1 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -1,48 +1,42 @@ import { photoZoomState } from '$lib/stores/zoom-image.store'; -import { useZoomImageWheel } from '@zoom-image/svelte'; +import { createZoomImageWheel } from '@zoom-image/core'; import { get } from 'svelte/store'; export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => { - const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel(); - - createZoomImage(node, { + const state = get(photoZoomState); + const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, + initialState: state, }); - const state = get(photoZoomState); - if (state) { - setZoomImageState(state); - } + const unsubscribes = [ + photoZoomState.subscribe((state) => zoomInstance.setState(state)), + zoomInstance.subscribe(({ state }) => { + photoZoomState.set(state); + }), + ]; - // Store original event handlers so we can prevent them when disabled - const wheelHandler = (event: WheelEvent) => { + const stopIfDisabled = (event: Event) => { if (options?.disabled) { event.stopImmediatePropagation(); } }; - const pointerDownHandler = (event: PointerEvent) => { - if (options?.disabled) { - event.stopImmediatePropagation(); - } - }; - - // Add handlers at capture phase with higher priority - node.addEventListener('wheel', wheelHandler, { capture: true }); - node.addEventListener('pointerdown', pointerDownHandler, { capture: true }); - - const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)]; + node.addEventListener('wheel', stopIfDisabled, { capture: true }); + node.addEventListener('pointerdown', stopIfDisabled, { capture: true }); + node.style.overflow = 'visible'; return { update(newOptions?: { disabled?: boolean }) { options = newOptions; }, destroy() { - node.removeEventListener('wheel', wheelHandler, { capture: true }); - node.removeEventListener('pointerdown', pointerDownHandler, { capture: true }); for (const unsubscribe of unsubscribes) { unsubscribe(); } + node.removeEventListener('wheel', stopIfDisabled, { capture: true }); + node.removeEventListener('pointerdown', stopIfDisabled, { capture: true }); + zoomInstance.cleanup(); }, }; }; diff --git a/web/src/lib/components/QueueCard.svelte b/web/src/lib/components/QueueCard.svelte index f57fb984a2..b98c732348 100644 --- a/web/src/lib/components/QueueCard.svelte +++ b/web/src/lib/components/QueueCard.svelte @@ -2,7 +2,8 @@ import QueueCardBadge from '$lib/components/QueueCardBadge.svelte'; import QueueCardButton from '$lib/components/QueueCardButton.svelte'; import Badge from '$lib/elements/Badge.svelte'; - import { asQueueItem, getQueueDetailUrl } from '$lib/services/queue.service'; + import { Route } from '$lib/route'; + import { asQueueItem } from '$lib/services/queue.service'; import { locale } from '$lib/stores/preferences.store'; import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk'; import { Icon, IconButton, Link } from '@immich/ui'; @@ -50,7 +51,7 @@ {/if}
- +