diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 5105d2eb75..833b70f77a 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -62,6 +62,7 @@ Once you have a new OAuth client application configured, Immich can be configure | Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) | | Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | | Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** | +| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** | | Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** | | Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) | | Button Text | string | Login with OAuth | Text for the OAuth button on the web | diff --git a/docs/docs/features/supported-formats.md b/docs/docs/features/supported-formats.md index e6fb2c8f00..16f1ab0b6b 100644 --- a/docs/docs/features/supported-formats.md +++ b/docs/docs/features/supported-formats.md @@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a | `HEIC` | `.heic` | :white_check_mark: | | | `HEIF` | `.heif` | :white_check_mark: | | | `JPEG 2000` | `.jp2` | :white_check_mark: | | -| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | | +| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | | | `JPEG XL` | `.jxl` | :white_check_mark: | | | `PNG` | `.png` | :white_check_mark: | | | `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop | diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 9e4d64892e..58fc43a2d5 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -227,6 +227,21 @@ describe(`/oauth`, () => { expect(user.storageLabel).toBe('user-username'); }); + it('should set the admin status from a role claim', async () => { + const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE); + const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); + expect(status).toBe(201); + expect(body).toMatchObject({ + accessToken: expect.any(String), + userId: expect.any(String), + userEmail: 'oauth-with-role@immich.app', + isAdmin: true, + }); + + const user = await getMyUser({ headers: asBearerAuth(body.accessToken) }); + expect(user.isAdmin).toBe(true); + }); + it('should work with RS256 signed tokens', async () => { await setupOAuth(admin.accessToken, { enabled: true, diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts index 575e97d291..489bda2ee4 100644 --- a/e2e/src/setup/auth-server.ts +++ b/e2e/src/setup/auth-server.ts @@ -12,6 +12,7 @@ export enum OAuthUser { NO_NAME = 'no-name', WITH_QUOTA = 'with-quota', WITH_USERNAME = 'with-username', + WITH_ROLE = 'with-role', } const claims = [ @@ -34,6 +35,12 @@ const claims = [ preferred_username: 'user-quota', immich_quota: 25, }, + { + sub: OAuthUser.WITH_ROLE, + email: 'oauth-with-role@immich.app', + email_verified: true, + immich_role: 'admin', + }, ]; const withDefaultClaims = (sub: string) => ({ @@ -64,7 +71,15 @@ const setup = async () => { claims: { openid: ['sub'], email: ['email', 'email_verified'], - profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'], + profile: [ + 'name', + 'given_name', + 'family_name', + 'preferred_username', + 'immich_quota', + 'immich_username', + 'immich_role', + ], }, features: { jwtUserinfo: { diff --git a/i18n/en.json b/i18n/en.json index 43b26389bb..4d5a05fae3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -196,6 +196,8 @@ "oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override", "oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''", + "oauth_role_claim": "Role Claim", + "oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.", "oauth_settings": "OAuth", "oauth_settings_description": "Manage OAuth login settings", "oauth_settings_more_details": "For more details about this feature, refer to the docs.", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 4e40ede5af..7da2fd3920 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -517,16 +517,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.13" +version = "0.115.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, ] [[package]] @@ -900,7 +900,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.33.0" +version = "0.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -912,9 +912,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" }, + { url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" }, ] [[package]] @@ -1044,7 +1044,7 @@ requires-dist = [ { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" }, { name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" }, { name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" }, - { name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" }, + { name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" }, { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "orjson", specifier = ">=3.9.5" }, @@ -1568,7 +1568,7 @@ wheels = [ [[package]] name = "onnxruntime-gpu" version = "1.19.2" -source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" } +source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" } dependencies = [ { name = "coloredlogs" }, { name = "flatbuffers" }, @@ -1936,16 +1936,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.9.1" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, ] [[package]] @@ -2304,27 +2304,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.0" +version = "0.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, - { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, - { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" }, + { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" }, + { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" }, + { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" }, ] [[package]] @@ -2504,27 +2504,27 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.1" +version = "0.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" }, - { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" }, - { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" }, - { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" }, - { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" }, + { url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" }, + { url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" }, + { url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" }, ] [[package]] @@ -2628,16 +2628,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.3" +version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] [package.optional-dependencies] diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 30f92cf8db..493a34cc94 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +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":"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":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"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":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"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":[]}],"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":"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"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":4,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":6,"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":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":7,"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":8,"references":[],"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":"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":9,"references":[2,8],"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"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":10,"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":11,"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":12,"references":[1,11],"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":13,"references":[11,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"]}}]} \ No newline at end of file +{"_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":"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":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"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":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"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":[]}],"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":"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"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[2],"type":"index","data":{"on":2,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":4,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}},{"id":5,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":6,"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":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":7,"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":8,"references":[],"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":"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":9,"references":[2,8],"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"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":10,"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":11,"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":12,"references":[1,11],"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":13,"references":[11,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":14,"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":15,"references":[1,14],"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":16,"references":[0,1],"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,"defaultConstraints":"REFERENCES remote_asset_entity (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}}]} \ 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 dd57456c76..1226d1730f 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -1,5 +1,5 @@ -part 'remote_asset.model.dart'; part 'local_asset.model.dart'; +part 'remote_asset.model.dart'; enum AssetType { // do not change this order! @@ -48,6 +48,13 @@ sealed class BaseAsset { return null; } + bool get hasRemote => + storage == AssetState.remote || storage == AssetState.merged; + bool get hasLocal => + storage == AssetState.local || storage == AssetState.merged; + bool get isLocalOnly => storage == AssetState.local; + bool get isRemoteOnly => storage == AssetState.remote; + // Overridden in subclasses AssetState get storage; String get heroTag; diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index 8aab1e3431..30a4955fa8 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -25,8 +25,6 @@ class LocalAsset extends BaseAsset { @override String get heroTag => '${id}_${remoteId ?? checksum}'; - bool get hasRemote => remoteId != null; - @override String toString() { return '''LocalAsset { diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 9a4ba1ebeb..57b5f03c16 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -41,8 +41,6 @@ class RemoteAsset extends BaseAsset { @override String get heroTag => '${localId ?? checksum}_$id'; - bool get hasLocal => localId != null; - @override String toString() { return '''Asset { @@ -85,4 +83,38 @@ class RemoteAsset extends BaseAsset { thumbHash.hashCode ^ localDateTime.hashCode ^ visibility.hashCode; + + RemoteAsset copyWith({ + String? id, + String? localId, + String? name, + String? ownerId, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + String? thumbHash, + AssetVisibility? visibility, + }) { + return RemoteAsset( + id: id ?? this.id, + localId: localId ?? this.localId, + name: name ?? this.name, + ownerId: ownerId ?? this.ownerId, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + isFavorite: isFavorite ?? this.isFavorite, + thumbHash: thumbHash ?? this.thumbHash, + visibility: visibility ?? this.visibility, + ); + } } diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart new file mode 100644 index 0000000000..ba2a43428f --- /dev/null +++ b/mobile/lib/domain/models/memory.model.dart @@ -0,0 +1,166 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:collection/collection.dart'; + +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; + +enum MemoryTypeEnum { + // do not change this order! + onThisDay, +} + +class MemoryData { + final int year; + + const MemoryData({ + required this.year, + }); + + MemoryData copyWith({ + int? year, + }) { + return MemoryData( + year: year ?? this.year, + ); + } + + Map toMap() { + return { + 'year': year, + }; + } + + factory MemoryData.fromMap(Map map) { + return MemoryData( + year: map['year'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory MemoryData.fromJson(String source) => + MemoryData.fromMap(json.decode(source) as Map); + + @override + String toString() => 'MemoryData(year: $year)'; + + @override + bool operator ==(covariant MemoryData other) { + if (identical(this, other)) return true; + + return other.year == year; + } + + @override + int get hashCode => year.hashCode; +} + +// Model for a memory stored in the server +class DriftMemory { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + + // enum + final MemoryTypeEnum type; + final MemoryData data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + final List assets; + + const DriftMemory({ + 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, + required this.assets, + }); + + DriftMemory copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + String? ownerId, + MemoryTypeEnum? type, + MemoryData? data, + bool? isSaved, + DateTime? memoryAt, + DateTime? seenAt, + DateTime? showAt, + DateTime? hideAt, + List? assets, + }) { + return DriftMemory( + 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, + assets: assets ?? this.assets, + ); + } + + @override + String toString() { + return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)'; + } + + @override + bool operator ==(covariant DriftMemory other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.deletedAt == deletedAt && + other.ownerId == ownerId && + other.type == type && + other.data == data && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt && + other.showAt == showAt && + other.hideAt == hideAt && + listEquals(other.assets, assets); + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + deletedAt.hashCode ^ + ownerId.hashCode ^ + type.hashCode ^ + data.hashCode ^ + isSaved.hashCode ^ + memoryAt.hashCode ^ + seenAt.hashCode ^ + showAt.hashCode ^ + hideAt.hashCode ^ + assets.hashCode; + } +} diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index fe341dc028..a256ee3589 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -6,6 +6,7 @@ enum Setting { showStorageIndicator(StoreKey.storageIndicator, true), loadOriginal(StoreKey.loadOriginal, false), preferRemoteImage(StoreKey.preferRemoteImage, false), + advancedTroubleshooting(StoreKey.advancedTroubleshooting, false), ; const Setting(this.storeKey, this.defaultValue); diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart new file mode 100644 index 0000000000..5404eb8f42 --- /dev/null +++ b/mobile/lib/domain/models/stack.model.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +// Model for a stack stored in the server +class Stack { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + + const Stack({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + + Stack copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) { + return Stack( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + Map toMap() { + return { + 'id': id, + 'createdAt': createdAt.millisecondsSinceEpoch, + 'updatedAt': updatedAt.millisecondsSinceEpoch, + 'ownerId': ownerId, + 'primaryAssetId': primaryAssetId, + }; + } + + factory Stack.fromMap(Map map) { + return Stack( + id: map['id'] as String, + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int), + updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int), + ownerId: map['ownerId'] as String, + primaryAssetId: map['primaryAssetId'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory Stack.fromJson(String source) => + Stack.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)'; + } + + @override + bool operator ==(covariant Stack other) { + if (identical(this, other)) return true; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.ownerId == ownerId && + other.primaryAssetId == primaryAssetId; + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + ownerId.hashCode ^ + primaryAssetId.hashCode; + } +} diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index ee39220554..c4a0766601 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -1,13 +1,24 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; class AssetService { final RemoteAssetRepository _remoteAssetRepository; + final DriftLocalAssetRepository _localAssetRepository; const AssetService({ required RemoteAssetRepository remoteAssetRepository, - }) : _remoteAssetRepository = remoteAssetRepository; + required DriftLocalAssetRepository localAssetRepository, + }) : _remoteAssetRepository = remoteAssetRepository, + _localAssetRepository = localAssetRepository; + + Stream watchAsset(BaseAsset asset) { + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; + return asset is LocalAsset + ? _localAssetRepository.watchAsset(id) + : _remoteAssetRepository.watchAsset(id); + } Future getExif(BaseAsset asset) async { if (asset is LocalAsset || asset is! RemoteAsset) { diff --git a/mobile/lib/domain/services/memory.service.dart b/mobile/lib/domain/services/memory.service.dart new file mode 100644 index 0000000000..c94b8a9f0a --- /dev/null +++ b/mobile/lib/domain/services/memory.service.dart @@ -0,0 +1,15 @@ +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; +import 'package:logging/logging.dart'; + +class DriftMemoryService { + final log = Logger("DriftMemoryService"); + + final DriftMemoryRepository _repository; + + DriftMemoryService(this._repository); + + Future> getMemoryLane(String ownerId) { + return _repository.getAll(ownerId); + } +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 2160018df5..ee0ec6c44f 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -146,6 +146,33 @@ class SyncStreamService { // to acknowledge that the client has processed all the backfill events case SyncEntityType.syncAckV1: return; + case SyncEntityType.memoryV1: + return _syncStreamRepository.updateMemoriesV1(data.cast()); + case SyncEntityType.memoryDeleteV1: + return _syncStreamRepository.deleteMemoriesV1(data.cast()); + case SyncEntityType.memoryToAssetV1: + return _syncStreamRepository.updateMemoryAssetsV1(data.cast()); + case SyncEntityType.memoryToAssetDeleteV1: + return _syncStreamRepository.deleteMemoryAssetsV1(data.cast()); + case SyncEntityType.stackV1: + return _syncStreamRepository.updateStacksV1(data.cast()); + case SyncEntityType.stackDeleteV1: + return _syncStreamRepository.deleteStacksV1(data.cast()); + case SyncEntityType.partnerStackV1: + return _syncStreamRepository.updateStacksV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerStackBackfillV1: + return _syncStreamRepository.updateStacksV1( + data.cast(), + debugLabel: 'partner backfill', + ); + case SyncEntityType.partnerStackDeleteV1: + return _syncStreamRepository.deleteStacksV1( + data.cast(), + debugLabel: 'partner', + ); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 54a9a3a142..2e56e6e10c 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/setting.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; @@ -52,6 +53,29 @@ class TimelineFactory { bucketSource: () => _timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy), ); + + TimelineService trash(String userId) => TimelineService( + assetSource: (offset, count) => _timelineRepository + .getTrashBucketAssets(userId, offset: offset, count: count), + bucketSource: () => + _timelineRepository.watchTrashBucket(userId, groupBy: groupBy), + ); + + TimelineService archive(String userId) => TimelineService( + assetSource: (offset, count) => _timelineRepository + .getArchiveBucketAssets(userId, offset: offset, count: count), + bucketSource: () => + _timelineRepository.watchArchiveBucket(userId, groupBy: groupBy), + ); + + TimelineService lockedFolder(String userId) => TimelineService( + assetSource: (offset, count) => _timelineRepository + .getLockedFolderBucketAssets(userId, offset: offset, count: count), + bucketSource: () => _timelineRepository.watchLockedFolderBucket( + userId, + groupBy: groupBy, + ), + ); } class TimelineService { @@ -68,7 +92,7 @@ class TimelineService { _bucketSubscription = _bucketSource().listen((buckets) { _totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); - unawaited(reloadBucket()); + unawaited(_reloadBucket()); }); } @@ -79,8 +103,9 @@ class TimelineService { Stream> Function() get watchBuckets => _bucketSource; - Future reloadBucket() => _mutex.run(() async { + Future _reloadBucket() => _mutex.run(() async { _buffer = await _assetSource(_bufferOffset, _buffer.length); + EventStream.shared.emit(const TimelineReloadEvent()); }); Future> loadAssets(int index, int count) => diff --git a/mobile/lib/domain/utils/event_stream.dart b/mobile/lib/domain/utils/event_stream.dart new file mode 100644 index 0000000000..65ee17e12b --- /dev/null +++ b/mobile/lib/domain/utils/event_stream.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +sealed class Event { + const Event(); +} + +class TimelineReloadEvent extends Event { + const TimelineReloadEvent(); +} + +class ViewerOpenBottomSheetEvent extends Event { + const ViewerOpenBottomSheetEvent(); +} + +class EventStream { + EventStream._(); + + static final EventStream shared = EventStream._(); + + final StreamController _controller = + StreamController.broadcast(); + + void emit(Event event) { + _controller.add(event); + } + + Stream where() { + if (T == Event) { + return _controller.stream as Stream; + } + return _controller.stream.where((event) => event is T).cast(); + } + + StreamSubscription listen( + void Function(T event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return where().listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + /// Closes the stream controller + void dispose() { + _controller.close(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 39c3822b04..62f91ae458 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -28,5 +28,8 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData { updatedAt: updatedAt, durationInSeconds: durationInSeconds, isFavorite: isFavorite, + height: height, + width: width, + remoteId: null, ); } diff --git a/mobile/lib/infrastructure/entities/memory.entity.dart b/mobile/lib/infrastructure/entities/memory.entity.dart new file mode 100644 index 0000000000..0e19802103 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory.entity.dart @@ -0,0 +1,36 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class MemoryEntity extends Table with DriftDefaultsMixin { + const MemoryEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get type => intEnum()(); + + TextColumn get data => text()(); + + BoolColumn get isSaved => boolean().withDefault(const Constant(false))(); + + DateTimeColumn get memoryAt => dateTime()(); + + DateTimeColumn get seenAt => dateTime().nullable()(); + + DateTimeColumn get showAt => dateTime().nullable()(); + + DateTimeColumn get hideAt => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/memory.entity.drift.dart b/mobile/lib/infrastructure/entities/memory.entity.drift.dart new file mode 100644 index 0000000000..cb88651ba4 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory.entity.drift.dart @@ -0,0 +1,970 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/memory.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart' as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$MemoryEntityTableCreateCompanionBuilder = i1.MemoryEntityCompanion + Function({ + required String id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value deletedAt, + required String ownerId, + required i2.MemoryTypeEnum type, + required String data, + i0.Value isSaved, + required DateTime memoryAt, + i0.Value seenAt, + i0.Value showAt, + i0.Value hideAt, +}); +typedef $$MemoryEntityTableUpdateCompanionBuilder = i1.MemoryEntityCompanion + Function({ + i0.Value id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value deletedAt, + i0.Value ownerId, + i0.Value type, + i0.Value data, + i0.Value isSaved, + i0.Value memoryAt, + i0.Value seenAt, + i0.Value showAt, + i0.Value hideAt, +}); + +final class $$MemoryEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$MemoryEntityTable, i1.MemoryEntityData> { + $$MemoryEntityTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('memory_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MemoryEntityTableFilterComposer + extends i0.Composer { + $$MemoryEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get data => $composableBuilder( + column: $table.data, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isSaved => $composableBuilder( + column: $table.isSaved, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get memoryAt => $composableBuilder( + column: $table.memoryAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get seenAt => $composableBuilder( + column: $table.seenAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get showAt => $composableBuilder( + column: $table.showAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get hideAt => $composableBuilder( + column: $table.hideAt, builder: (column) => i0.ColumnFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryEntityTableOrderingComposer + extends i0.Composer { + $$MemoryEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get data => $composableBuilder( + column: $table.data, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isSaved => $composableBuilder( + column: $table.isSaved, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get memoryAt => $composableBuilder( + column: $table.memoryAt, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get seenAt => $composableBuilder( + column: $table.seenAt, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get showAt => $composableBuilder( + column: $table.showAt, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get hideAt => $composableBuilder( + column: $table.hideAt, builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryEntityTableAnnotationComposer + extends i0.Composer { + $$MemoryEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get data => + $composableBuilder(column: $table.data, builder: (column) => column); + + i0.GeneratedColumn get isSaved => + $composableBuilder(column: $table.isSaved, builder: (column) => column); + + i0.GeneratedColumn get memoryAt => + $composableBuilder(column: $table.memoryAt, builder: (column) => column); + + i0.GeneratedColumn get seenAt => + $composableBuilder(column: $table.seenAt, builder: (column) => column); + + i0.GeneratedColumn get showAt => + $composableBuilder(column: $table.showAt, builder: (column) => column); + + i0.GeneratedColumn get hideAt => + $composableBuilder(column: $table.hideAt, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$MemoryEntityTable, + i1.MemoryEntityData, + i1.$$MemoryEntityTableFilterComposer, + i1.$$MemoryEntityTableOrderingComposer, + i1.$$MemoryEntityTableAnnotationComposer, + $$MemoryEntityTableCreateCompanionBuilder, + $$MemoryEntityTableUpdateCompanionBuilder, + (i1.MemoryEntityData, i1.$$MemoryEntityTableReferences), + i1.MemoryEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$MemoryEntityTableTableManager( + i0.GeneratedDatabase db, i1.$MemoryEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$MemoryEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$MemoryEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$MemoryEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value data = const i0.Value.absent(), + i0.Value isSaved = const i0.Value.absent(), + i0.Value memoryAt = const i0.Value.absent(), + i0.Value seenAt = const i0.Value.absent(), + i0.Value showAt = const i0.Value.absent(), + i0.Value hideAt = const i0.Value.absent(), + }) => + i1.MemoryEntityCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: data, + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + ), + createCompanionCallback: ({ + required String id, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + required String ownerId, + required i2.MemoryTypeEnum type, + required String data, + i0.Value isSaved = const i0.Value.absent(), + required DateTime memoryAt, + i0.Value seenAt = const i0.Value.absent(), + i0.Value showAt = const i0.Value.absent(), + i0.Value hideAt = const i0.Value.absent(), + }) => + i1.MemoryEntityCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: data, + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$MemoryEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$MemoryEntityTableReferences._ownerIdTable(db), + referencedColumn: + i1.$$MemoryEntityTableReferences._ownerIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MemoryEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$MemoryEntityTable, + i1.MemoryEntityData, + i1.$$MemoryEntityTableFilterComposer, + i1.$$MemoryEntityTableOrderingComposer, + i1.$$MemoryEntityTableAnnotationComposer, + $$MemoryEntityTableCreateCompanionBuilder, + $$MemoryEntityTableUpdateCompanionBuilder, + (i1.MemoryEntityData, i1.$$MemoryEntityTableReferences), + i1.MemoryEntityData, + i0.PrefetchHooks Function({bool ownerId})>; + +class $MemoryEntityTable extends i3.MemoryEntity + with i0.TableInfo<$MemoryEntityTable, i1.MemoryEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $MemoryEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _deletedAtMeta = + const i0.VerificationMeta('deletedAt'); + @override + late final i0.GeneratedColumn deletedAt = + i0.GeneratedColumn('deleted_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$MemoryEntityTable.$convertertype); + static const i0.VerificationMeta _dataMeta = + const i0.VerificationMeta('data'); + @override + late final i0.GeneratedColumn data = i0.GeneratedColumn( + 'data', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _isSavedMeta = + const i0.VerificationMeta('isSaved'); + @override + late final i0.GeneratedColumn isSaved = i0.GeneratedColumn( + 'is_saved', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _memoryAtMeta = + const i0.VerificationMeta('memoryAt'); + @override + late final i0.GeneratedColumn memoryAt = + i0.GeneratedColumn('memory_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: true); + static const i0.VerificationMeta _seenAtMeta = + const i0.VerificationMeta('seenAt'); + @override + late final i0.GeneratedColumn seenAt = i0.GeneratedColumn( + 'seen_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _showAtMeta = + const i0.VerificationMeta('showAt'); + @override + late final i0.GeneratedColumn showAt = i0.GeneratedColumn( + 'show_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _hideAtMeta = + const i0.VerificationMeta('hideAt'); + @override + late final i0.GeneratedColumn hideAt = i0.GeneratedColumn( + 'hide_at', aliasedName, true, + type: i0.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 + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('data')) { + context.handle( + _dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta)); + } else if (isInserting) { + context.missing(_dataMeta); + } + if (data.containsKey('is_saved')) { + context.handle(_isSavedMeta, + isSaved.isAcceptableOrUnknown(data['is_saved']!, _isSavedMeta)); + } + if (data.containsKey('memory_at')) { + context.handle(_memoryAtMeta, + memoryAt.isAcceptableOrUnknown(data['memory_at']!, _memoryAtMeta)); + } else if (isInserting) { + context.missing(_memoryAtMeta); + } + if (data.containsKey('seen_at')) { + context.handle(_seenAtMeta, + seenAt.isAcceptableOrUnknown(data['seen_at']!, _seenAtMeta)); + } + if (data.containsKey('show_at')) { + context.handle(_showAtMeta, + showAt.isAcceptableOrUnknown(data['show_at']!, _showAtMeta)); + } + if (data.containsKey('hide_at')) { + context.handle(_hideAtMeta, + hideAt.isAcceptableOrUnknown(data['hide_at']!, _hideAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.MemoryEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + type: i1.$MemoryEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + data: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}data'])!, + isSaved: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_saved'])!, + memoryAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!, + seenAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}seen_at']), + showAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}show_at']), + hideAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}hide_at']), + ); + } + + @override + $MemoryEntityTable createAlias(String alias) { + return $MemoryEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.MemoryTypeEnum.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final i2.MemoryTypeEnum 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'] = i0.Variable(id); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } + map['owner_id'] = i0.Variable(ownerId); + { + map['type'] = + i0.Variable(i1.$MemoryEntityTable.$convertertype.toSql(type)); + } + map['data'] = i0.Variable(data); + map['is_saved'] = i0.Variable(isSaved); + map['memory_at'] = i0.Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = i0.Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = i0.Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = i0.Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.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: i1.$MemoryEntityTable.$convertertype + .fromJson(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({i0.ValueSerializer? serializer}) { + serializer ??= i0.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(i1.$MemoryEntityTable.$convertertype.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), + }; + } + + i1.MemoryEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value deletedAt = const i0.Value.absent(), + String? ownerId, + i2.MemoryTypeEnum? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + i0.Value seenAt = const i0.Value.absent(), + i0.Value showAt = const i0.Value.absent(), + i0.Value hideAt = const i0.Value.absent()}) => + i1.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(i1.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 i1.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 i0.UpdateCompanion { + final i0.Value id; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value deletedAt; + final i0.Value ownerId; + final i0.Value type; + final i0.Value data; + final i0.Value isSaved; + final i0.Value memoryAt; + final i0.Value seenAt; + final i0.Value showAt; + final i0.Value hideAt; + const MemoryEntityCompanion({ + this.id = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.data = const i0.Value.absent(), + this.isSaved = const i0.Value.absent(), + this.memoryAt = const i0.Value.absent(), + this.seenAt = const i0.Value.absent(), + this.showAt = const i0.Value.absent(), + this.hideAt = const i0.Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + required String ownerId, + required i2.MemoryTypeEnum type, + required String data, + this.isSaved = const i0.Value.absent(), + required DateTime memoryAt, + this.seenAt = const i0.Value.absent(), + this.showAt = const i0.Value.absent(), + this.hideAt = const i0.Value.absent(), + }) : id = i0.Value(id), + ownerId = i0.Value(ownerId), + type = i0.Value(type), + data = i0.Value(data), + memoryAt = i0.Value(memoryAt); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? deletedAt, + i0.Expression? ownerId, + i0.Expression? type, + i0.Expression? data, + i0.Expression? isSaved, + i0.Expression? memoryAt, + i0.Expression? seenAt, + i0.Expression? showAt, + i0.Expression? hideAt, + }) { + return i0.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, + }); + } + + i1.MemoryEntityCompanion copyWith( + {i0.Value? id, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? deletedAt, + i0.Value? ownerId, + i0.Value? type, + i0.Value? data, + i0.Value? isSaved, + i0.Value? memoryAt, + i0.Value? seenAt, + i0.Value? showAt, + i0.Value? hideAt}) { + return i1.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'] = i0.Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$MemoryEntityTable.$convertertype.toSql(type.value)); + } + if (data.present) { + map['data'] = i0.Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = i0.Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = i0.Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = i0.Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = i0.Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = i0.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(); + } +} diff --git a/mobile/lib/infrastructure/entities/memory_asset.entity.dart b/mobile/lib/infrastructure/entities/memory_asset.entity.dart new file mode 100644 index 0000000000..c304b03724 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class MemoryAssetEntity extends Table with DriftDefaultsMixin { + const MemoryAssetEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get memoryId => + text().references(MemoryEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, memoryId}; +} diff --git a/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart new file mode 100644 index 0000000000..9253e8bc05 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart @@ -0,0 +1,550 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' + as i5; + +typedef $$MemoryAssetEntityTableCreateCompanionBuilder + = i1.MemoryAssetEntityCompanion Function({ + required String assetId, + required String memoryId, +}); +typedef $$MemoryAssetEntityTableUpdateCompanionBuilder + = i1.MemoryAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value memoryId, +}); + +final class $$MemoryAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$MemoryAssetEntityTable, + i1.MemoryAssetEntityData> { + $$MemoryAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('memory_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$MemoryEntityTable _memoryIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('memory_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('memory_asset_entity') + .memoryId, + i4.ReadDatabaseContainer(db) + .resultSet('memory_entity') + .id)); + + i5.$$MemoryEntityTableProcessedTableManager get memoryId { + final $_column = $_itemColumn('memory_id')!; + + final manager = i5 + .$$MemoryEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('memory_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_memoryIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MemoryAssetEntityTableFilterComposer + extends i0.Composer { + $$MemoryAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$MemoryEntityTableFilterComposer get memoryId { + final i5.$$MemoryEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.memoryId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$MemoryEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryAssetEntityTableOrderingComposer + extends i0.Composer { + $$MemoryAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$MemoryEntityTableOrderingComposer get memoryId { + final i5.$$MemoryEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.memoryId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$MemoryEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryAssetEntityTableAnnotationComposer + extends i0.Composer { + $$MemoryAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$MemoryEntityTableAnnotationComposer get memoryId { + final i5.$$MemoryEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.memoryId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$MemoryEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('memory_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MemoryAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$MemoryAssetEntityTable, + i1.MemoryAssetEntityData, + i1.$$MemoryAssetEntityTableFilterComposer, + i1.$$MemoryAssetEntityTableOrderingComposer, + i1.$$MemoryAssetEntityTableAnnotationComposer, + $$MemoryAssetEntityTableCreateCompanionBuilder, + $$MemoryAssetEntityTableUpdateCompanionBuilder, + (i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences), + i1.MemoryAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool memoryId})> { + $$MemoryAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$MemoryAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$MemoryAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$MemoryAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$MemoryAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value memoryId = const i0.Value.absent(), + }) => + i1.MemoryAssetEntityCompanion( + assetId: assetId, + memoryId: memoryId, + ), + createCompanionCallback: ({ + required String assetId, + required String memoryId, + }) => + i1.MemoryAssetEntityCompanion.insert( + assetId: assetId, + memoryId: memoryId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$MemoryAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, memoryId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: + i1.$$MemoryAssetEntityTableReferences._assetIdTable(db), + referencedColumn: i1.$$MemoryAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (memoryId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.memoryId, + referencedTable: i1.$$MemoryAssetEntityTableReferences + ._memoryIdTable(db), + referencedColumn: i1.$$MemoryAssetEntityTableReferences + ._memoryIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MemoryAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$MemoryAssetEntityTable, + i1.MemoryAssetEntityData, + i1.$$MemoryAssetEntityTableFilterComposer, + i1.$$MemoryAssetEntityTableOrderingComposer, + i1.$$MemoryAssetEntityTableAnnotationComposer, + $$MemoryAssetEntityTableCreateCompanionBuilder, + $$MemoryAssetEntityTableUpdateCompanionBuilder, + (i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences), + i1.MemoryAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool memoryId})>; + +class $MemoryAssetEntityTable extends i2.MemoryAssetEntity + with i0.TableInfo<$MemoryAssetEntityTable, i1.MemoryAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $MemoryAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _memoryIdMeta = + const i0.VerificationMeta('memoryId'); + @override + late final i0.GeneratedColumn memoryId = i0.GeneratedColumn( + 'memory_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.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 + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('memory_id')) { + context.handle(_memoryIdMeta, + memoryId.isAcceptableOrUnknown(data['memory_id']!, _memoryIdMeta)); + } else if (isInserting) { + context.missing(_memoryIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, memoryId}; + @override + i1.MemoryAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + memoryId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}memory_id'])!, + ); + } + + @override + $MemoryAssetEntityTable createAlias(String alias) { + return $MemoryAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends i0.DataClass + implements i0.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'] = i0.Variable(assetId); + map['memory_id'] = i0.Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + i1.MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + i1.MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(i1.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 i1.MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.memoryId = const i0.Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = i0.Value(assetId), + memoryId = i0.Value(memoryId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? memoryId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + i1.MemoryAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? memoryId}) { + return i1.MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = i0.Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart new file mode 100644 index 0000000000..92375f19db --- /dev/null +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -0,0 +1,22 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class StackEntity extends Table with DriftDefaultsMixin { + const StackEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/stack.entity.drift.dart b/mobile/lib/infrastructure/entities/stack.entity.drift.dart new file mode 100644 index 0000000000..c0d000e02a --- /dev/null +++ b/mobile/lib/infrastructure/entities/stack.entity.drift.dart @@ -0,0 +1,706 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/stack.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i4; +import 'package:drift/internal/modular.dart' as i5; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i6; + +typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion + Function({ + required String id, + i0.Value createdAt, + i0.Value updatedAt, + required String ownerId, + required String primaryAssetId, +}); +typedef $$StackEntityTableUpdateCompanionBuilder = i1.StackEntityCompanion + Function({ + i0.Value id, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value ownerId, + i0.Value primaryAssetId, +}); + +final class $$StackEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$StackEntityTable, i1.StackEntityData> { + $$StackEntityTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet('stack_entity') + .ownerId, + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i4.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i4 + .$$UserEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i6.$RemoteAssetEntityTable _primaryAssetIdTable( + i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet('stack_entity') + .primaryAssetId, + i5.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId { + final $_column = $_itemColumn('primary_asset_id')!; + + final manager = i6 + .$$RemoteAssetEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$StackEntityTableFilterComposer + extends i0.Composer { + $$StackEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i4.$$UserEntityTableFilterComposer get ownerId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId { + final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.primaryAssetId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$StackEntityTableOrderingComposer + extends i0.Composer { + $$StackEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i4.$$UserEntityTableOrderingComposer get ownerId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId { + final i6.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.primaryAssetId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$StackEntityTableAnnotationComposer + extends i0.Composer { + $$StackEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i4.$$UserEntityTableAnnotationComposer get ownerId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId { + final i6.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.primaryAssetId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$StackEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$StackEntityTable, + i1.StackEntityData, + i1.$$StackEntityTableFilterComposer, + i1.$$StackEntityTableOrderingComposer, + i1.$$StackEntityTableAnnotationComposer, + $$StackEntityTableCreateCompanionBuilder, + $$StackEntityTableUpdateCompanionBuilder, + (i1.StackEntityData, i1.$$StackEntityTableReferences), + i1.StackEntityData, + i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> { + $$StackEntityTableTableManager( + i0.GeneratedDatabase db, i1.$StackEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$StackEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$StackEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$StackEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value primaryAssetId = const i0.Value.absent(), + }) => + i1.StackEntityCompanion( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ), + createCompanionCallback: ({ + required String id, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + required String ownerId, + required String primaryAssetId, + }) => + i1.StackEntityCompanion.insert( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$StackEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false, primaryAssetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$StackEntityTableReferences._ownerIdTable(db), + referencedColumn: + i1.$$StackEntityTableReferences._ownerIdTable(db).id, + ) as T; + } + if (primaryAssetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.primaryAssetId, + referencedTable: i1.$$StackEntityTableReferences + ._primaryAssetIdTable(db), + referencedColumn: i1.$$StackEntityTableReferences + ._primaryAssetIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$StackEntityTable, + i1.StackEntityData, + i1.$$StackEntityTableFilterComposer, + i1.$$StackEntityTableOrderingComposer, + i1.$$StackEntityTableAnnotationComposer, + $$StackEntityTableCreateCompanionBuilder, + $$StackEntityTableUpdateCompanionBuilder, + (i1.StackEntityData, i1.$$StackEntityTableReferences), + i1.StackEntityData, + i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>; + +class $StackEntityTable extends i2.StackEntity + with i0.TableInfo<$StackEntityTable, i1.StackEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $StackEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i3.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i3.currentDateAndTime); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _primaryAssetIdMeta = + const i0.VerificationMeta('primaryAssetId'); + @override + late final i0.GeneratedColumn primaryAssetId = + i0.GeneratedColumn( + 'primary_asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id)')); + @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 + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('primary_asset_id')) { + context.handle( + _primaryAssetIdMeta, + primaryAssetId.isAcceptableOrUnknown( + data['primary_asset_id']!, _primaryAssetIdMeta)); + } else if (isInserting) { + context.missing(_primaryAssetIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.StackEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + primaryAssetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!, + ); + } + + @override + $StackEntityTable createAlias(String alias) { + return $StackEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends i0.DataClass + implements i0.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'] = i0.Variable(id); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + map['owner_id'] = i0.Variable(ownerId); + map['primary_asset_id'] = i0.Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.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({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + i1.StackEntityData copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId}) => + i1.StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(i1.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 i1.StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value ownerId; + final i0.Value primaryAssetId; + const StackEntityCompanion({ + this.id = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.primaryAssetId = const i0.Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = i0.Value(id), + ownerId = i0.Value(ownerId), + primaryAssetId = i0.Value(primaryAssetId); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? ownerId, + i0.Expression? primaryAssetId, + }) { + return i0.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, + }); + } + + i1.StackEntityCompanion copyWith( + {i0.Value? id, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? ownerId, + i0.Value? primaryAssetId}) { + return i1.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'] = i0.Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = i0.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(); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index dbe491b035..a7920cf7b2 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -7,11 +7,14 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:isar/isar.dart'; @@ -46,6 +49,9 @@ class IsarDatabaseRepository implements IDatabaseRepository { RemoteAlbumEntity, RemoteAlbumAssetEntity, RemoteAlbumUserEntity, + MemoryEntity, + MemoryAssetEntity, + StackEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 69fd84b79a..15d445d226 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -23,9 +23,15 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity. as i10; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' as i11; -import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' as i12; -import 'package:drift/internal/modular.dart' as i13; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' + as i13; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' + as i14; +import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' + as i15; +import 'package:drift/internal/modular.dart' as i16; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -51,8 +57,12 @@ abstract class $Drift extends i0.GeneratedDatabase { i10.$RemoteAlbumAssetEntityTable(this); late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i11.$RemoteAlbumUserEntityTable(this); - i12.MergedAssetDrift get mergedAssetDrift => i13.ReadDatabaseContainer(this) - .accessor(i12.MergedAssetDrift.new); + late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this); + late final i13.$MemoryAssetEntityTable memoryAssetEntity = + i13.$MemoryAssetEntityTable(this); + late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this); + i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this) + .accessor(i15.MergedAssetDrift.new); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -71,7 +81,10 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteExifEntity, remoteAlbumEntity, remoteAlbumAssetEntity, - remoteAlbumUserEntity + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + stackEntity ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => @@ -175,6 +188,34 @@ abstract class $Drift extends i0.GeneratedDatabase { kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('memory_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('memory_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -208,4 +249,10 @@ class $DriftManager { _db, _db.remoteAlbumAssetEntity); i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11 .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); + i12.$$MemoryEntityTableTableManager get memoryEntity => + i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); + i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => + i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); + i14.$$StackEntityTableTableManager get stackEntity => + i14.$$StackEntityTableTableManager(_db, _db.stackEntity); } diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 2efa04cc1b..28ca600f61 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,5 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -7,6 +8,26 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { final Drift _db; const DriftLocalAssetRepository(this._db) : super(_db); + Stream watchAsset(String id) { + final query = _db.localAssetEntity + .select() + .addColumns([_db.localAssetEntity.id]).join([ + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where(_db.localAssetEntity.id.equals(id)); + + return query.map((row) { + final asset = row.readTable(_db.localAssetEntity).toDto(); + return asset.copyWith( + remoteId: row.read(_db.remoteAssetEntity.id), + ); + }).watchSingleOrNull(); + } + Future updateHashes(Iterable hashes) { if (hashes.isEmpty) { return Future.value(); diff --git a/mobile/lib/infrastructure/repositories/memory.repository.dart b/mobile/lib/infrastructure/repositories/memory.repository.dart new file mode 100644 index 0000000000..ff5f75c2ac --- /dev/null +++ b/mobile/lib/infrastructure/repositories/memory.repository.dart @@ -0,0 +1,81 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftMemoryRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftMemoryRepository(this._db) : super(_db); + + Future> getAll(String ownerId) async { + final now = DateTime.now(); + final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0); + + final query = _db.select(_db.memoryEntity).join([ + leftOuterJoin( + _db.memoryAssetEntity, + _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id), + ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) & + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility + .equalsValue(AssetVisibility.timeline), + ), + ]) + ..where(_db.memoryEntity.ownerId.equals(ownerId)) + ..where(_db.memoryEntity.deletedAt.isNull()) + ..where( + _db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc), + ) + ..where( + _db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc), + ) + ..orderBy([ + OrderingTerm.desc(_db.memoryEntity.memoryAt), + OrderingTerm.asc(_db.remoteAssetEntity.createdAt), + ]); + + final rows = await query.get(); + + final Map memoriesMap = {}; + + for (final row in rows) { + final memory = row.readTable(_db.memoryEntity); + final asset = row.readTable(_db.remoteAssetEntity); + + final existingMemory = memoriesMap[memory.id]; + if (existingMemory != null) { + existingMemory.assets.add(asset.toDto()); + } else { + final assets = [asset.toDto()]; + memoriesMap[memory.id] = memory.toDto().copyWith(assets: assets); + } + } + + return memoriesMap.values.toList(); + } +} + +extension on MemoryEntityData { + DriftMemory toDto() { + return DriftMemory( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: MemoryData.fromJson(data), + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + assets: [], + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 7319feff30..c50cdbdd0a 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -13,6 +13,26 @@ class RemoteAssetRepository extends DriftDatabaseRepository { final Drift _db; const RemoteAssetRepository(this._db) : super(_db); + Stream watchAsset(String id) { + final query = _db.remoteAssetEntity + .select() + .addColumns([_db.localAssetEntity.id]).join([ + leftOuterJoin( + _db.localAssetEntity, + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + useColumns: false, + ), + ]) + ..where(_db.remoteAssetEntity.id.equals(id)); + + return query.map((row) { + final asset = row.readTable(_db.remoteAssetEntity).toDto(); + return asset.copyWith( + localId: row.read(_db.localAssetEntity.id), + ); + }).watchSingleOrNull(); + } + Future getExif(String id) { return _db.managers.remoteExifEntity .filter((row) => row.assetId.id.equals(id)) diff --git a/mobile/lib/infrastructure/repositories/stack.repository.dart b/mobile/lib/infrastructure/repositories/stack.repository.dart new file mode 100644 index 0000000000..7f97f3d9ae --- /dev/null +++ b/mobile/lib/infrastructure/repositories/stack.repository.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/stack.model.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftStackRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftStackRepository(this._db) : super(_db); + + Future> getAll(String userId) { + final query = _db.stackEntity.select() + ..where((e) => e.ownerId.equals(userId)); + + return query.map((stack) { + return stack.toDto(); + }).get(); + } +} + +extension on StackEntityData { + Stack toDto() { + return Stack( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index ccc79fa818..d43f786a29 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -52,6 +52,10 @@ class SyncApiRepository { SyncRequestType.albumAssetsV1, SyncRequestType.albumAssetExifsV1, SyncRequestType.albumToAssetsV1, + SyncRequestType.memoriesV1, + SyncRequestType.memoryToAssetsV1, + SyncRequestType.stacksV1, + SyncRequestType.partnerStacksV1, ], ).toJson(), ); @@ -157,6 +161,15 @@ const _kResponseMap = { SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson, SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson, SyncEntityType.syncAckV1: _SyncAckV1.fromJson, + SyncEntityType.memoryV1: SyncMemoryV1.fromJson, + SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson, + SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson, + SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson, + SyncEntityType.stackV1: SyncStackV1.fromJson, + SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson, + SyncEntityType.partnerStackV1: SyncStackV1.fromJson, + SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson, + SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, }; class _SyncAckV1 { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 015e09d663..89f5c2f59a 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,12 +1,18 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; @@ -64,8 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: SyncPartnerDeleteV1', e, s); + } catch (error, stack) { + _logger.severe('Error: SyncPartnerDeleteV1', error, stack); rethrow; } } @@ -87,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: SyncPartnerV1', e, s); + } catch (error, stack) { + _logger.severe('Error: SyncPartnerV1', error, stack); rethrow; } } @@ -98,10 +104,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { String debugLabel = 'user', }) async { try { - await _db.remoteAssetEntity - .deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId))); - } catch (e, s) { - _logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s); + await _db.remoteAssetEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.assetId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack); rethrow; } } @@ -136,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAssetsV1 - $debugLabel', e, s); + } catch (error, stack) { + _logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack); rethrow; } } @@ -180,18 +187,23 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s); + } catch (error, stack) { + _logger.severe( + 'Error: updateAssetsExifV1 - $debugLabel', + error, + stack, + ); rethrow; } } Future deleteAlbumsV1(Iterable data) async { try { - await _db.remoteAlbumEntity - .deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId))); - } catch (e, s) { - _logger.severe('Error: deleteAlbumsV1', e, s); + await _db.remoteAlbumEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.albumId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumsV1', error, stack); rethrow; } } @@ -218,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAlbumsV1', e, s); + } catch (error, stack) { + _logger.severe('Error: updateAlbumsV1', error, stack); rethrow; } } @@ -237,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: deleteAlbumUsersV1', e, s); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumUsersV1', error, stack); rethrow; } } @@ -264,8 +276,12 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s); + } catch (error, stack) { + _logger.severe( + 'Error: updateAlbumUsersV1 - $debugLabel', + error, + stack, + ); rethrow; } } @@ -285,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: deleteAlbumToAssetsV1', e, s); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumToAssetsV1', error, stack); rethrow; } } @@ -310,8 +326,137 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s); + } catch (error, stack) { + _logger.severe( + 'Error: updateAlbumToAssetsV1 - $debugLabel', + error, + stack, + ); + rethrow; + } + } + + Future updateMemoriesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final memory in data) { + final companion = MemoryEntityCompanion( + createdAt: Value(memory.createdAt), + deletedAt: Value(memory.deletedAt), + ownerId: Value(memory.ownerId), + type: Value(memory.type.toMemoryType()), + data: Value(jsonEncode(memory.data)), + isSaved: Value(memory.isSaved), + memoryAt: Value(memory.memoryAt), + seenAt: Value.absentIfNull(memory.seenAt), + showAt: Value.absentIfNull(memory.showAt), + hideAt: Value.absentIfNull(memory.hideAt), + ); + + batch.insert( + _db.memoryEntity, + companion.copyWith(id: Value(memory.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateMemoriesV1', error, stack); + rethrow; + } + } + + Future deleteMemoriesV1(Iterable data) async { + try { + await _db.memoryEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.memoryId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteMemoriesV1', error, stack); + rethrow; + } + } + + Future updateMemoryAssetsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final asset in data) { + final companion = MemoryAssetEntityCompanion( + memoryId: Value(asset.memoryId), + assetId: Value(asset.assetId), + ); + + batch.insert( + _db.memoryAssetEntity, + companion, + onConflict: DoNothing(), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateMemoryAssetsV1', error, stack); + rethrow; + } + } + + Future deleteMemoryAssetsV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final asset in data) { + batch.delete( + _db.memoryAssetEntity, + MemoryAssetEntityCompanion( + memoryId: Value(asset.memoryId), + assetId: Value(asset.assetId), + ), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteMemoryAssetsV1', error, stack); + rethrow; + } + } + + Future updateStacksV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final stack in data) { + final companion = StackEntityCompanion( + createdAt: Value(stack.createdAt), + updatedAt: Value(stack.updatedAt), + ownerId: Value(stack.ownerId), + primaryAssetId: Value(stack.primaryAssetId), + ); + + batch.insert( + _db.stackEntity, + companion.copyWith(id: Value(stack.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateStacksV1 - $debugLabel', error, stack); + rethrow; + } + } + + Future deleteStacksV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.stackEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.stackId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack); rethrow; } } @@ -335,6 +480,13 @@ extension on AssetOrder { }; } +extension on MemoryType { + MemoryTypeEnum toMemoryType() => switch (this) { + MemoryType.onThisDay => MemoryTypeEnum.onThisDay, + _ => throw Exception('Unknown MemoryType value: $this'), + }; +} + extension on api.AlbumUserRole { AlbumUserRole toAlbumUserRole() => switch (this) { api.AlbumUserRole.editor => AlbumUserRole.editor, diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index fcd92cb30c..45e2c5dac9 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -213,6 +213,158 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .map((row) => row.readTable(_db.remoteAssetEntity).toDto()) .get(); } + + Stream> watchTrashBucket( + String userId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + return _db.remoteAssetEntity + .count( + where: (row) => + row.deletedAt.isNotNull() & row.ownerId.equals(userId), + ) + .map(_generateBuckets) + .watchSingle(); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..where( + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteAssetEntity.deletedAt.isNotNull(), + ) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> getTrashBucketAssets( + String userId, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select() + ..where( + (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) => row.toDto()).get(); + } + + Stream> watchArchiveBucket( + String userId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + return _db.remoteAssetEntity + .count( + where: (row) => + row.visibility.equalsValue(AssetVisibility.archive) & + row.ownerId.equals(userId), + ) + .map(_generateBuckets) + .watchSingle(); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..where( + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteAssetEntity.visibility + .equalsValue(AssetVisibility.archive), + ) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> getArchiveBucketAssets( + String userId, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select() + ..where( + (row) => + row.ownerId.equals(userId) & + row.visibility.equalsValue(AssetVisibility.archive), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) => row.toDto()).get(); + } + + Stream> watchLockedFolderBucket( + String userId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + return _db.remoteAssetEntity + .count( + where: (row) => + row.visibility.equalsValue(AssetVisibility.locked) & + row.ownerId.equals(userId), + ) + .map(_generateBuckets) + .watchSingle(); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..where( + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteAssetEntity.visibility + .equalsValue(AssetVisibility.locked), + ) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> getLockedFolderBucketAssets( + String userId, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select() + ..where( + (row) => + row.visibility.equalsValue(AssetVisibility.locked) & + row.ownerId.equals(userId), + ) + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) => row.toDto()).get(); + } } extension on Expression { diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index 31ccb12392..452c153342 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -42,22 +42,6 @@ class TabShellPage extends ConsumerWidget { ); } - void onNavigationSelected(TabsRouter router, int index) { - // On Photos page menu tapped - if (router.activeIndex == 0 && index == 0) { - scrollToTopNotifierProvider.scrollToTop(); - } - - // On Search page tapped - if (router.activeIndex == 1 && index == 1) { - ref.read(searchInputFocusProvider).requestFocus(); - } - - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - router.setActiveIndex(index); - ref.read(tabProvider.notifier).state = TabEnum.values[index]; - } - final navigationDestinations = [ NavigationDestination( label: 'photos'.tr(), @@ -110,15 +94,6 @@ class TabShellPage extends ConsumerWidget { ), ]; - Widget bottomNavigationBar(TabsRouter tabsRouter) { - return NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: (index) => - onNavigationSelected(tabsRouter, index), - destinations: navigationDestinations, - ); - } - Widget navigationRail(TabsRouter tabsRouter) { return NavigationRail( destinations: navigationDestinations @@ -131,15 +106,13 @@ class TabShellPage extends ConsumerWidget { ) .toList(), onDestinationSelected: (index) => - onNavigationSelected(tabsRouter, index), + _onNavigationSelected(tabsRouter, index, ref), selectedIndex: tabsRouter.activeIndex, labelType: NavigationRailLabelType.all, groupAlignment: 0.0, ); } - final multiselectEnabled = - ref.watch(multiSelectProvider.select((s) => s.isEnabled)); return AutoTabsRouter( routes: [ const MainTimelineRoute(), @@ -173,12 +146,57 @@ class TabShellPage extends ConsumerWidget { ], ) : heroedChild, - bottomNavigationBar: multiselectEnabled || isScreenLandscape - ? null - : bottomNavigationBar(tabsRouter), + bottomNavigationBar: _BottomNavigationBar( + tabsRouter: tabsRouter, + destinations: navigationDestinations, + ), ), ); }, ); } } + +void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { + // On Photos page menu tapped + if (router.activeIndex == 0 && index == 0) { + scrollToTopNotifierProvider.scrollToTop(); + } + + // On Search page tapped + if (router.activeIndex == 1 && index == 1) { + ref.read(searchInputFocusProvider).requestFocus(); + } + + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + router.setActiveIndex(index); + ref.read(tabProvider.notifier).state = TabEnum.values[index]; +} + +class _BottomNavigationBar extends ConsumerWidget { + const _BottomNavigationBar({ + required this.tabsRouter, + required this.destinations, + }); + + final List destinations; + final TabsRouter tabsRouter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isScreenLandscape = context.orientation == Orientation.landscape; + final isMultiselectEnabled = + ref.watch(multiSelectProvider.select((s) => s.isEnabled)); + + if (isScreenLandscape || isMultiselectEnabled) { + return const SizedBox.shrink(); + } + + return NavigationBar( + selectedIndex: tabsRouter.activeIndex, + onDestinationSelected: (index) => + _onNavigationSelected(tabsRouter, index, ref), + destinations: destinations, + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/drift_archive.page.dart b/mobile/lib/presentation/pages/dev/drift_archive.page.dart new file mode 100644 index 0000000000..14657f7149 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/drift_archive.page.dart @@ -0,0 +1,33 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +@RoutePage() +class DriftArchivePage extends StatelessWidget { + const DriftArchivePage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access archive'); + } + + final timelineService = + ref.watch(timelineFactoryProvider).archive(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: const Timeline(), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/dev/drift_locked_folder.page.dart new file mode 100644 index 0000000000..5ab7c71347 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/drift_locked_folder.page.dart @@ -0,0 +1,33 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +@RoutePage() +class DriftLockedFolderPage extends StatelessWidget { + const DriftLockedFolderPage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access locked folder'); + } + + final timelineService = + ref.watch(timelineFactoryProvider).lockedFolder(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: const Timeline(), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/drift_trash.page.dart b/mobile/lib/presentation/pages/dev/drift_trash.page.dart new file mode 100644 index 0000000000..cbcfe50112 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/drift_trash.page.dart @@ -0,0 +1,33 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +@RoutePage() +class DriftTrashPage extends StatelessWidget { + const DriftTrashPage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access trash'); + } + + final timelineService = + ref.watch(timelineFactoryProvider).trash(user.id); + ref.onDispose(timelineService.dispose); + return timelineService; + }, + ), + ], + child: const Timeline(), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index f10c042e10..e4c4f1c879 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -66,6 +66,9 @@ final _features = [ await db.remoteAlbumEntity.deleteAll(); await db.remoteAlbumUserEntity.deleteAll(); await db.remoteAlbumAssetEntity.deleteAll(); + await db.memoryEntity.deleteAll(); + await db.memoryAssetEntity.deleteAll(); + await db.stackEntity.deleteAll(); }, ), _Feature( @@ -96,6 +99,21 @@ final _features = [ icon: Icons.timeline_rounded, onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), ), + _Feature( + name: 'Trash', + icon: Icons.delete_outline_rounded, + onTap: (ctx, _) => ctx.pushRoute(const DriftTrashRoute()), + ), + _Feature( + name: 'Archive', + icon: Icons.archive_outlined, + onTap: (ctx, _) => ctx.pushRoute(const DriftArchiveRoute()), + ), + _Feature( + name: 'Locked Folder', + icon: Icons.lock_outline_rounded, + onTap: (ctx, _) => ctx.pushRoute(const DriftLockedFolderRoute()), + ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index 090db4f6ba..9ec8002463 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -1,7 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; @RoutePage() class MainTimelinePage extends ConsumerWidget { @@ -9,6 +11,22 @@ class MainTimelinePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return const Timeline(); + final memoryLaneProvider = ref.watch(driftMemoryFutureProvider); + + return memoryLaneProvider.when( + data: (memories) { + return memories.isEmpty + ? const Timeline() + : Timeline( + topSliverWidget: SliverToBoxAdapter( + key: Key('memory-lane-${memories.first.assets.first.id}'), + child: DriftMemoryLane(memories: memories), + ), + topSliverWidgetHeight: 200, + ); + }, + loading: () => const Timeline(), + error: (error, stackTrace) => const Timeline(), + ); } } diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index 10d09f8de5..e5745fa629 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -154,6 +154,18 @@ final _remoteStats = [ name: 'Remote Albums', load: (db) => db.managers.remoteAlbumEntity.count(), ), + _Stat( + name: 'Memories', + load: (db) => db.managers.memoryEntity.count(), + ), + _Stat( + name: 'Memories Assets', + load: (db) => db.managers.memoryAssetEntity.count(), + ), + _Stat( + name: 'Stacks', + load: (db) => db.managers.stackEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart new file mode 100644 index 0000000000..7da2d1a4c7 --- /dev/null +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -0,0 +1,394 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart'; +import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; +import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; + +/// Expects [currentAssetNotifier] to be set before navigating to this page +@RoutePage() +class DriftMemoryPage extends HookConsumerWidget { + final List memories; + final int memoryIndex; + + const DriftMemoryPage({ + required this.memories, + required this.memoryIndex, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentMemory = useState(memories[memoryIndex]); + final currentAssetPage = useState(0); + final currentMemoryIndex = useState(memoryIndex); + final assetProgress = useState( + "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}", + ); + const bgColor = Colors.black; + final currentAsset = useState(null); + + /// The list of all of the asset page controllers + final memoryAssetPageControllers = + List.generate(memories.length, (i) => usePageController()); + + /// The main vertically scrolling page controller with each list of memories + final memoryPageController = usePageController(initialPage: memoryIndex); + + useEffect(() { + // Memories is an immersive activity + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + return () { + // Clean up to normal edge to edge when we are done + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + }; + }); + + toNextMemory() { + memoryPageController.nextPage( + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn, + ); + } + + void toPreviousMemory() { + if (currentMemoryIndex.value > 0) { + // Move to the previous memory page + memoryPageController.previousPage( + duration: const Duration(milliseconds: 500), + curve: Curves.easeIn, + ); + + // Wait for the next frame to ensure the page is built + SchedulerBinding.instance.addPostFrameCallback((_) { + final previousIndex = currentMemoryIndex.value - 1; + final previousMemoryController = + memoryAssetPageControllers[previousIndex]; + + // Ensure the controller is attached + if (previousMemoryController.hasClients) { + previousMemoryController + .jumpToPage(memories[previousIndex].assets.length - 1); + } else { + // Wait for the next frame until it is attached + SchedulerBinding.instance.addPostFrameCallback((_) { + if (previousMemoryController.hasClients) { + previousMemoryController + .jumpToPage(memories[previousIndex].assets.length - 1); + } + }); + } + }); + } + } + + toNextAsset(int currentAssetIndex) { + if (currentAssetIndex + 1 < currentMemory.value.assets.length) { + // Go to the next asset + PageController controller = + memoryAssetPageControllers[currentMemoryIndex.value]; + + controller.nextPage( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + ); + } else { + // Go to the next memory since we are at the end of our assets + toNextMemory(); + } + } + + toPreviousAsset(int currentAssetIndex) { + if (currentAssetIndex > 0) { + // Go to the previous asset + PageController controller = + memoryAssetPageControllers[currentMemoryIndex.value]; + + controller.previousPage( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + ); + } else { + // Go to the previous memory since we are at the end of our assets + toPreviousMemory(); + } + } + + updateProgressText() { + assetProgress.value = + "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; + } + + /// Downloads and caches the image for the asset at this [currentMemory]'s index + precacheAsset(int index) async { + // Guard index out of range + if (index < 0) { + return; + } + + // Context might be removed due to popping out of Memory Lane during Scroll handling + if (!context.mounted) { + return; + } + + late RemoteAsset asset; + if (index < currentMemory.value.assets.length) { + // Uses the next asset in this current memory + asset = currentMemory.value.assets[index]; + } else { + // Precache the first asset in the next memory if available + final currentMemoryIndex = memories.indexOf(currentMemory.value); + + // Guard no memory found + if (currentMemoryIndex == -1) { + return; + } + + final nextMemoryIndex = currentMemoryIndex + 1; + // Guard no next memory + if (nextMemoryIndex >= memories.length) { + return; + } + + // Get the first asset from the next memory + asset = memories[nextMemoryIndex].assets.first; + } + + // Precache the asset + final size = MediaQuery.sizeOf(context); + await precacheImage( + getFullImageProvider( + asset, + size: Size(size.width, size.height), + ), + context, + size: size, + ); + } + + // Precache the next page right away if we are on the first page + if (currentAssetPage.value == 0) { + Future.delayed(const Duration(milliseconds: 200)) + .then((_) => precacheAsset(1)); + } + + Future onAssetChanged(int otherIndex) async { + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + currentAssetPage.value = otherIndex; + updateProgressText(); + + // Wait for page change animation to finish + await Future.delayed(const Duration(milliseconds: 400)); + // And then precache the next asset + await precacheAsset(otherIndex + 1); + + final asset = currentMemory.value.assets[otherIndex]; + currentAsset.value = asset; + ref.read(currentAssetNotifier.notifier).setAsset(asset); + // if (asset.isVideo || asset.isMotionPhoto) { + if (asset.isVideo) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + } + } + + /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called + * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final + * page during the end of scroll is different than the current page + */ + return NotificationListener( + onNotification: (ScrollNotification notification) { + // Calculate OverScroll manually using the number of pixels away from maxScrollExtent + // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 + // or sum of vertical pixels of all memories for depth = 0 + if (notification is ScrollUpdateNotification) { + final isEpiloguePage = + (memoryPageController.page?.floor() ?? 0) >= memories.length; + + final offset = notification.metrics.pixels; + if (isEpiloguePage && + (offset > notification.metrics.maxScrollExtent + 150)) { + context.maybePop(); + return true; + } + } + + return false; + }, + child: Scaffold( + backgroundColor: bgColor, + body: SafeArea( + child: PageView.builder( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + scrollDirection: Axis.vertical, + controller: memoryPageController, + onPageChanged: (pageNumber) { + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + if (pageNumber < memories.length) { + currentMemoryIndex.value = pageNumber; + currentMemory.value = memories[pageNumber]; + } + + currentAssetPage.value = 0; + + updateProgressText(); + }, + itemCount: memories.length + 1, + itemBuilder: (context, mIndex) { + // Build last page + if (mIndex == memories.length) { + return MemoryEpilogue( + onStartOver: () => memoryPageController.animateToPage( + 0, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + ), + ); + } + + final yearsAgo = DateTime.now().year - memories[mIndex].data.year; + final title = 'years_ago'.t( + context: context, + args: { + 'years': yearsAgo.toString(), + }, + ); + // Build horizontal page + final assetController = memoryAssetPageControllers[mIndex]; + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + top: 8.0, + bottom: 2.0, + ), + child: AnimatedBuilder( + animation: assetController, + builder: (context, child) { + double value = 0.0; + if (assetController.hasClients) { + // We can only access [page] if this has clients + value = assetController.page ?? 0; + } + return MemoryProgressIndicator( + ticks: memories[mIndex].assets.length, + value: (value + 1) / memories[mIndex].assets.length, + ); + }, + ), + ), + Expanded( + child: Stack( + children: [ + PageView.builder( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + controller: assetController, + onPageChanged: onAssetChanged, + scrollDirection: Axis.horizontal, + itemCount: memories[mIndex].assets.length, + itemBuilder: (context, index) { + final asset = memories[mIndex].assets[index]; + return Stack( + children: [ + Container( + color: Colors.black, + child: DriftMemoryCard( + asset: asset, + title: title, + showTitle: index == 0, + ), + ), + Positioned.fill( + child: Row( + children: [ + // Left side of the screen + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + toPreviousAsset(index); + }, + ), + ), + + // Right side of the screen + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + toNextAsset(index); + }, + ), + ), + ], + ), + ), + ], + ); + }, + ), + Positioned( + top: 8, + left: 8, + child: MaterialButton( + minWidth: 0, + onPressed: () { + // auto_route doesn't invoke pop scope, so + // turn off full screen mode here + // https://github.com/Milad-Akarie/auto_route_library/issues/1799 + context.maybePop(); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + ); + }, + shape: const CircleBorder(), + color: Colors.white.withValues(alpha: 0.2), + elevation: 0, + child: const Icon( + Icons.close_rounded, + color: Colors.white, + ), + ), + ), + if (currentAsset.value != null && + currentAsset.value!.isVideo) + Positioned( + bottom: 24, + right: 32, + child: Icon( + Icons.videocam_outlined, + color: Colors.grey[200], + ), + ), + ], + ), + ), + DriftMemoryBottomInfo( + memory: memories[mIndex], + title: title, + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart index d1d0695a99..86537816e3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -20,7 +19,6 @@ class ArchiveActionButton extends ConsumerWidget { } final result = await ref.read(actionProvider.notifier).archive(source); - await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'archive_action_prompt'.t( diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart index 6c60b47535..94e3610a57 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -9,17 +9,33 @@ class BaseActionButton extends StatelessWidget { this.onPressed, this.onLongPressed, this.maxWidth = 90.0, + this.minWidth, + this.menuItem = false, }); final String label; final IconData iconData; final double maxWidth; + final double? minWidth; + final bool menuItem; final void Function()? onPressed; final void Function()? onLongPressed; @override Widget build(BuildContext context) { - final minWidth = context.isMobile ? context.width / 4.5 : 75.0; + final miniWidth = + minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); + final iconTheme = IconTheme.of(context); + final iconSize = iconTheme.size ?? 24.0; + final iconColor = iconTheme.color ?? context.themeData.iconTheme.color; + final textColor = context.themeData.textTheme.labelLarge?.color; + + if (menuItem) { + return IconButton( + onPressed: onPressed, + icon: Icon(iconData, size: iconSize, color: iconColor), + ); + } return ConstrainedBox( constraints: BoxConstraints( @@ -30,19 +46,22 @@ class BaseActionButton extends StatelessWidget { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20)), ), + textColor: textColor, onPressed: onPressed, onLongPress: onLongPressed, - minWidth: minWidth, + minWidth: miniWidth, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(iconData, size: 24), + Icon(iconData, size: iconSize, color: iconColor), const SizedBox(height: 8), Text( label, - style: - const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400), + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w400, + ), maxLines: 3, textAlign: TextAlign.center, ), diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart index 6f8c0f5227..d81f998a7b 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -20,7 +19,6 @@ class DeletePermanentActionButton extends ConsumerWidget { } final result = await ref.read(actionProvider.notifier).delete(source); - await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'delete_action_prompt'.t( diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart index 50d13e6b4e..39d059c2d1 100644 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -5,14 +5,18 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class FavoriteActionButton extends ConsumerWidget { final ActionSource source; + final bool menuItem; - const FavoriteActionButton({super.key, required this.source}); + const FavoriteActionButton({ + super.key, + required this.source, + this.menuItem = false, + }); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -20,7 +24,11 @@ class FavoriteActionButton extends ConsumerWidget { } final result = await ref.read(actionProvider.notifier).favorite(source); - await ref.read(timelineServiceProvider).reloadBucket(); + + if (source == ActionSource.viewer) { + return; + } + ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'favorite_action_prompt'.t( @@ -45,6 +53,7 @@ class FavoriteActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.favorite_border_rounded, label: "favorite".t(context: context), + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart index 503dd34403..7546f07961 100644 --- a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -21,7 +20,6 @@ class MoveToLockFolderActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); - await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'move_to_lock_folder_action_prompt'.t( diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart index 32857f300e..20fb62013f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -21,7 +20,6 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).removeFromLockFolder(source); - await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'remove_from_lock_folder_action_prompt'.t( diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_buton.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart similarity index 92% rename from mobile/lib/presentation/widgets/action_buttons/trash_action_buton.widget.dart rename to mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart index 1d287e34e2..449b688550 100644 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_buton.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -20,7 +19,6 @@ class TrashActionButton extends ConsumerWidget { } final result = await ref.read(actionProvider.notifier).trash(source); - await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'trash_action_prompt'.t( diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart index b5e210eb3b..a58d9f1ee1 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -20,7 +19,6 @@ class UnarchiveActionButton extends ConsumerWidget { } final result = await ref.read(actionProvider.notifier).unArchive(source); - await ref.read(timelineServiceProvider).reloadBucket(); ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'unarchive_action_prompt'.t( diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart index 2d485f3418..b465643796 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart @@ -5,14 +5,18 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class UnFavoriteActionButton extends ConsumerWidget { final ActionSource source; + final bool menuItem; - const UnFavoriteActionButton({super.key, required this.source}); + const UnFavoriteActionButton({ + super.key, + required this.source, + this.menuItem = false, + }); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -20,7 +24,11 @@ class UnFavoriteActionButton extends ConsumerWidget { } final result = await ref.read(actionProvider.notifier).unFavorite(source); - await ref.read(timelineServiceProvider).reloadBucket(); + + if (source == ActionSource.viewer) { + return; + } + ref.read(multiSelectProvider.notifier).reset(); final successMessage = 'unfavorite_action_prompt'.t( @@ -46,6 +54,7 @@ class UnFavoriteActionButton extends ConsumerWidget { iconData: Icons.favorite_rounded, label: "unfavorite".t(context: context), onPressed: () => _onTap(context, ref), + menuItem: menuItem, ); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 6f5ceb6da1..c91b71319c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -4,9 +4,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; @@ -57,9 +61,10 @@ const double _kBottomSheetSnapExtent = 0.7; class _AssetViewerState extends ConsumerState { late PageController pageController; late DraggableScrollableController bottomSheetController; - PersistentBottomSheetController? sheetCloseNotifier; + PersistentBottomSheetController? sheetCloseController; // PhotoViewGallery takes care of disposing it's controllers PhotoViewControllerBase? viewController; + StreamSubscription? reloadSubscription; late Platform platform; late PhotoViewControllerValue initialPhotoViewState; @@ -68,12 +73,11 @@ class _AssetViewerState extends ConsumerState { bool blockGestures = false; bool dragInProgress = false; bool shouldPopOnDrag = false; - bool showingBottomSheet = false; double? initialScale; double previousExtent = _kBottomSheetMinimumExtent; Offset dragDownPosition = Offset.zero; int totalAssets = 0; - int backgroundOpacity = 255; + BuildContext? scaffoldContext; // Delayed operations that should be cancelled on disposal final List _delayedOperations = []; @@ -88,6 +92,7 @@ class _AssetViewerState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { _onAssetChanged(widget.initialIndex); }); + reloadSubscription = EventStream.shared.listen(_onEvent); } @override @@ -95,14 +100,17 @@ class _AssetViewerState extends ConsumerState { pageController.dispose(); bottomSheetController.dispose(); _cancelTimers(); + reloadSubscription?.cancel(); super.dispose(); } + bool get showingBottomSheet => + ref.read(assetViewerProvider.select((s) => s.showingBottomSheet)); + Color get backgroundColor { - if (showingBottomSheet && !context.isDarkTheme) { - return Colors.white; - } - return Colors.black.withAlpha(backgroundOpacity); + final opacity = + ref.read(assetViewerProvider.select((s) => s.backgroundOpacity)); + return Colors.black.withAlpha(opacity); } void _cancelTimers() { @@ -119,6 +127,9 @@ class _AssetViewerState extends ConsumerState { (viewController?.prevValue.scale ?? viewController?.value.scale ?? 1.0) + 0.01; + double _getVerticalOffsetForBottomSheet(double extent) => + (context.height * extent) - (context.height * _kBottomSheetMinimumExtent); + Future _precacheImage(int index) async { if (!mounted || index < 0 || index >= totalAssets) { return; @@ -186,11 +197,12 @@ class _AssetViewerState extends ConsumerState { void _onDragStart( _, DragStartDetails details, - PhotoViewControllerValue value, + PhotoViewControllerBase controller, PhotoViewScaleStateController scaleStateController, ) { + viewController = controller; dragDownPosition = details.localPosition; - initialPhotoViewState = value; + initialPhotoViewState = controller.value; final isZoomed = scaleStateController.scaleState == PhotoViewScaleState.zoomedIn || scaleStateController.scaleState == PhotoViewScaleState.covering; @@ -220,16 +232,14 @@ class _AssetViewerState extends ConsumerState { return; } - setState(() { - shouldPopOnDrag = false; - hasDraggedDown = null; - backgroundOpacity = 255; - viewController?.animateMultiple( - position: initialPhotoViewState.position, - scale: initialPhotoViewState.scale, - rotation: initialPhotoViewState.rotation, - ); - }); + shouldPopOnDrag = false; + hasDraggedDown = null; + viewController?.animateMultiple( + position: initialPhotoViewState.position, + scale: initialPhotoViewState.scale, + rotation: initialPhotoViewState.rotation, + ); + ref.read(assetViewerProvider.notifier).setOpacity(255); } void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) { @@ -250,18 +260,10 @@ class _AssetViewerState extends ConsumerState { void _handleDragUp(BuildContext ctx, Offset delta) { const double openThreshold = 50; - const double closeThreshold = 25; final position = initialPhotoViewState.position + Offset(0, delta.dy); final distanceToOrigin = position.distance; - if (showingBottomSheet && distanceToOrigin < closeThreshold) { - // Prevents the user from dragging the bottom sheet further down - blockGestures = true; - sheetCloseNotifier?.close(); - return; - } - viewController?.updateMultiple(position: position); // Moves the bottom sheet when the asset is being dragged up if (showingBottomSheet && bottomSheetController.isAttached) { @@ -274,68 +276,37 @@ class _AssetViewerState extends ConsumerState { } } - void _openBottomSheet(BuildContext ctx) { - setState(() { - initialScale = viewController?.scale; - viewController?.animateMultiple(scale: _getScaleForBottomSheet); - showingBottomSheet = true; - previousExtent = _kBottomSheetMinimumExtent; - sheetCloseNotifier = showBottomSheet( - context: ctx, - sheetAnimationStyle: AnimationStyle( - duration: Duration.zero, - reverseDuration: Duration.zero, - ), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), - ), - backgroundColor: ctx.colorScheme.surfaceContainerLowest, - builder: (_) { - return NotificationListener( - onNotification: _onNotification, - child: AssetDetailBottomSheet( - controller: bottomSheetController, - initialChildSize: _kBottomSheetMinimumExtent, - ), - ); - }, - ); - sheetCloseNotifier?.closed.then((_) => _handleSheetClose()); - }); - } + void _handleDragDown(BuildContext ctx, Offset delta) { + const double dragRatio = 0.2; + const double popThreshold = 75; - void _handleSheetClose() { - setState(() { - showingBottomSheet = false; - sheetCloseNotifier = null; - viewController?.animateMultiple( - position: Offset.zero, - scale: initialScale, - ); - shouldPopOnDrag = false; - hasDraggedDown = null; - }); - } + final distance = delta.distance; + shouldPopOnDrag = delta.dy > 0 && distance > popThreshold; - void _snapBottomSheet() { - if (bottomSheetController.size > _kBottomSheetSnapExtent || - bottomSheetController.size < 0.4) { - return; + final maxScaleDistance = ctx.height * 0.5; + final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio); + double? updatedScale; + if (initialPhotoViewState.scale != null) { + updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction); } - isSnapping = true; - bottomSheetController.animateTo( - _kBottomSheetSnapExtent, - duration: Durations.short3, - curve: Curves.easeOut, + + final backgroundOpacity = + (255 * (1.0 - (scaleReduction / dragRatio))).round(); + + viewController?.updateMultiple( + position: initialPhotoViewState.position + delta, + scale: updatedScale, ); + ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity); + } + + void _onTapDown(_, __, ___) { + if (!showingBottomSheet) { + ref.read(assetViewerProvider.notifier).toggleControls(); + } } bool _onNotification(Notification delta) { - // Ignore notifications when user dragging the asset - if (dragInProgress) { - return false; - } - if (delta is DraggableScrollableNotification) { _handleDraggableNotification(delta); } @@ -350,50 +321,117 @@ class _AssetViewerState extends ConsumerState { } void _handleDraggableNotification(DraggableScrollableNotification delta) { - final verticalOffset = (context.height * delta.extent) - - (context.height * _kBottomSheetMinimumExtent); + final currentExtent = delta.extent; + final isDraggingDown = currentExtent < previousExtent; + previousExtent = currentExtent; + // Closes the bottom sheet if the user is dragging down + if (isDraggingDown && delta.extent < 0.5) { + if (dragInProgress) { + blockGestures = true; + } + sheetCloseController?.close(); + } + + // If the asset is being dragged down, we do not want to update the asset position again + if (dragInProgress) { + return; + } + + final verticalOffset = _getVerticalOffsetForBottomSheet(delta.extent); // Moves the asset when the bottom sheet is being dragged if (verticalOffset > 0) { viewController?.position = Offset(0, -verticalOffset); } + } - final currentExtent = delta.extent; - final isDraggingDown = currentExtent < previousExtent; - previousExtent = currentExtent; - // Closes the bottom sheet if the user is dragging down and the extent is less than the snap extent - if (isDraggingDown && delta.extent < _kBottomSheetSnapExtent - 0.1) { - sheetCloseNotifier?.close(); + void _onEvent(Event event) { + if (event is TimelineReloadEvent) { + _onTimelineReload(event); + return; + } + + if (event is ViewerOpenBottomSheetEvent) { + final extent = _kBottomSheetMinimumExtent + 0.3; + _openBottomSheet(scaffoldContext!, extent: extent); + final offset = _getVerticalOffsetForBottomSheet(extent); + viewController?.position = Offset(0, -offset); + return; } } - void _handleDragDown(BuildContext ctx, Offset delta) { - const double dragRatio = 0.2; - const double popThreshold = 75; + void _onTimelineReload(_) { + setState(() { + totalAssets = ref.read(timelineServiceProvider).totalAssets; + if (totalAssets == 0) { + context.maybePop(); + return; + } - final distance = delta.distance; - final newShouldPopOnDrag = delta.dy > 0 && distance > popThreshold; + final index = pageController.page?.round() ?? 0; + final newAsset = ref.read(timelineServiceProvider).getAsset(index); + final currentAsset = ref.read(currentAssetNotifier); + // Do not reload / close the bottom sheet if the asset has not changed + if (newAsset.heroTag == currentAsset?.heroTag) { + return; + } - final maxScaleDistance = ctx.height * 0.5; - final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio); - double? updatedScale; - if (initialPhotoViewState.scale != null) { - updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction); - } + _onAssetChanged(pageController.page!.round()); + sheetCloseController?.close(); + }); + } - final newBackgroundOpacity = - (255 * (1.0 - (scaleReduction / dragRatio))).round(); - - viewController?.updateMultiple( - position: initialPhotoViewState.position + delta, - scale: updatedScale, + void _openBottomSheet( + BuildContext ctx, { + double extent = _kBottomSheetMinimumExtent, + }) { + ref.read(assetViewerProvider.notifier).setBottomSheet(true); + initialScale = viewController?.scale; + viewController?.updateMultiple(scale: _getScaleForBottomSheet); + previousExtent = _kBottomSheetMinimumExtent; + sheetCloseController = showBottomSheet( + context: ctx, + sheetAnimationStyle: AnimationStyle( + duration: Durations.short4, + reverseDuration: Durations.short2, + ), + constraints: const BoxConstraints(maxWidth: double.infinity), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), + ), + backgroundColor: ctx.colorScheme.surfaceContainerLowest, + builder: (_) { + return NotificationListener( + onNotification: _onNotification, + child: AssetDetailBottomSheet( + controller: bottomSheetController, + initialChildSize: extent, + ), + ); + }, ); - if (shouldPopOnDrag != newShouldPopOnDrag || - backgroundOpacity != newBackgroundOpacity) { - setState(() { - shouldPopOnDrag = newShouldPopOnDrag; - backgroundOpacity = newBackgroundOpacity; - }); + sheetCloseController?.closed.then((_) => _handleSheetClose()); + } + + void _handleSheetClose() { + viewController?.animateMultiple(position: Offset.zero); + viewController?.updateMultiple(scale: initialScale); + ref.read(assetViewerProvider.notifier).setBottomSheet(false); + sheetCloseController = null; + shouldPopOnDrag = false; + hasDraggedDown = null; + } + + void _snapBottomSheet() { + if (bottomSheetController.size > _kBottomSheetSnapExtent || + bottomSheetController.size < 0.4) { + return; } + isSnapping = true; + bottomSheetController.animateTo( + _kBottomSheetSnapExtent, + duration: Durations.short3, + curve: Curves.easeOut, + ); } Widget _placeholderBuilder( @@ -418,12 +456,13 @@ class _AssetViewerState extends ConsumerState { } PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) { + scaffoldContext ??= ctx; final asset = ref.read(timelineServiceProvider).getAsset(index); final size = Size(ctx.width, ctx.height); - final imageProvider = getFullImageProvider(asset, size: size); return PhotoViewGalleryPageOptions( - imageProvider: imageProvider, + key: ValueKey(asset.heroTag), + imageProvider: getFullImageProvider(asset, size: size), heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag), filterQuality: FilterQuality.high, tightMode: true, @@ -433,6 +472,7 @@ class _AssetViewerState extends ConsumerState { onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, + onTapDown: _onTapDown, errorBuilder: (_, __, ___) => Container( width: ctx.width, height: ctx.height, @@ -446,27 +486,43 @@ class _AssetViewerState extends ConsumerState { ); } + void _onPop(bool didPop, T? result) { + ref.read(currentAssetNotifier.notifier).dispose(); + } + @override Widget build(BuildContext context) { + // Rebuild the widget when the asset viewer state changes + // Using multiple selectors to avoid unnecessary rebuilds for other state changes + ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); + // Currently it is not possible to scroll the asset when the bottom sheet is open all the way. // Issue: https://github.com/flutter/flutter/issues/109037 // TODO: Add a custom scrum builder once the fix lands on stable - return Scaffold( - backgroundColor: Colors.black.withAlpha(backgroundOpacity), - body: PhotoViewGallery.builder( - gaplessPlayback: true, - loadingBuilder: _placeholderBuilder, - pageController: pageController, - scrollPhysics: platform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics() // Use heavy physics for Android - , - itemCount: totalAssets, - onPageChanged: _onPageChanged, - onPageBuild: _onPageBuild, - builder: _assetBuilder, - backgroundDecoration: BoxDecoration(color: backgroundColor), - enablePanAlways: true, + return PopScope( + onPopInvokedWithResult: _onPop, + child: Scaffold( + backgroundColor: backgroundColor, + appBar: const ViewerTopAppBar(), + extendBody: true, + extendBodyBehindAppBar: true, + body: PhotoViewGallery.builder( + gaplessPlayback: true, + loadingBuilder: _placeholderBuilder, + pageController: pageController, + scrollPhysics: platform.isIOS + ? const FastScrollPhysics() // Use bouncing physics for iOS + : const FastClampingScrollPhysics() // Use heavy physics for Android + , + itemCount: totalAssets, + onPageChanged: _onPageChanged, + onPageBuild: _onPageBuild, + builder: _assetBuilder, + backgroundDecoration: BoxDecoration(color: backgroundColor), + enablePanAlways: true, + ), + bottomNavigationBar: const ViewerBottomBar(), ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart new file mode 100644 index 0000000000..231d40c9ca --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart @@ -0,0 +1,76 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +class AssetViewerState { + final int backgroundOpacity; + final bool showingBottomSheet; + final bool showingControls; + + const AssetViewerState({ + this.backgroundOpacity = 255, + this.showingBottomSheet = false, + this.showingControls = true, + }); + + AssetViewerState copyWith({ + int? backgroundOpacity, + bool? showingBottomSheet, + bool? showingControls, + }) { + return AssetViewerState( + backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity, + showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet, + showingControls: showingControls ?? this.showingControls, + ); + } + + @override + String toString() { + return 'AssetViewerState(opacity: $backgroundOpacity, bottomSheet: $showingBottomSheet, controls: $showingControls)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is AssetViewerState && + other.backgroundOpacity == backgroundOpacity && + other.showingBottomSheet == showingBottomSheet && + other.showingControls == showingControls; + } + + @override + int get hashCode => + backgroundOpacity.hashCode ^ + showingBottomSheet.hashCode ^ + showingControls.hashCode; +} + +class AssetViewerStateNotifier extends AutoDisposeNotifier { + @override + AssetViewerState build() { + return const AssetViewerState(); + } + + void setOpacity(int opacity) { + state = state.copyWith( + backgroundOpacity: opacity, + showingControls: opacity == 255 ? true : state.showingControls, + ); + } + + void setBottomSheet(bool showing) { + state = state.copyWith( + showingBottomSheet: showing, + showingControls: showing ? true : state.showingControls, + ); + } + + void toggleControls() { + state = state.copyWith(showingControls: !state.showingControls); + } +} + +final assetViewerProvider = + AutoDisposeNotifierProvider( + AssetViewerStateNotifier.new, +); diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart new file mode 100644 index 0000000000..6269bef6be --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +class ViewerBottomBar extends ConsumerWidget { + const ViewerBottomBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + + final user = ref.watch(currentUserProvider); + final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; + final isSheetOpen = ref.watch( + assetViewerProvider.select((s) => s.showingBottomSheet), + ); + int opacity = ref.watch( + assetViewerProvider.select((state) => state.backgroundOpacity), + ); + final showControls = + ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0; + } + + final actions = [ + const ShareActionButton(), + const _EditActionButton(), + if (asset.hasRemote && isOwner) + const ArchiveActionButton(source: ActionSource.viewer), + ]; + + return IgnorePointer( + ignoring: opacity < 255, + child: AnimatedOpacity( + opacity: opacity / 255, + duration: Durations.short2, + child: AnimatedSwitcher( + duration: Durations.short4, + child: isSheetOpen + ? const SizedBox.shrink() + : SafeArea( + child: Theme( + data: context.themeData.copyWith( + iconTheme: + const IconThemeData(size: 22, color: Colors.white), + textTheme: context.themeData.textTheme.copyWith( + labelLarge: + context.themeData.textTheme.labelLarge?.copyWith( + color: Colors.white, + ), + ), + ), + child: Container( + height: 80, + color: Colors.black.withAlpha(125), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: actions, + ), + ), + ), + ), + ), + ), + ); + } +} + +class _EditActionButton extends ConsumerWidget { + const _EditActionButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.tune_outlined, + label: 'edit'.t(context: context), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart similarity index 66% rename from mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.dart rename to mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 39f28d2f60..d0bdc28d10 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -1,30 +1,80 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +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/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; const _kSeparator = ' • '; -class AssetDetailBottomSheet extends BaseBottomSheet { +class AssetDetailBottomSheet extends ConsumerWidget { + final DraggableScrollableController? controller; + final double initialChildSize; + const AssetDetailBottomSheet({ - super.controller, - super.initialChildSize, + this.controller, + this.initialChildSize = 0.35, super.key, - }) : super( - actions: const [], - slivers: const [_AssetDetailBottomSheet()], - minChildSize: 0.1, - maxChildSize: 1.0, - expand: false, - shouldCloseOnMinExtent: false, - resizeOnScroll: false, - ); + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + + final isTrashEnable = ref.watch( + serverInfoProvider.select((state) => state.serverFeatures.trash), + ); + + final actions = [ + const ShareActionButton(), + if (asset.hasRemote) ...[ + const ShareLinkActionButton(source: ActionSource.viewer), + const ArchiveActionButton(source: ActionSource.viewer), + if (!asset.hasLocal) const DownloadActionButton(), + isTrashEnable + ? const TrashActionButton(source: ActionSource.viewer) + : const DeletePermanentActionButton(source: ActionSource.viewer), + const MoveToLockFolderActionButton( + source: ActionSource.viewer, + ), + ], + if (asset.storage == AssetState.local) ...[ + const DeleteLocalActionButton(), + const UploadActionButton(), + ], + ]; + + return BaseBottomSheet( + actions: actions, + slivers: const [_AssetDetailBottomSheet()], + controller: controller, + initialChildSize: initialChildSize, + minChildSize: 0.1, + maxChildSize: 0.88, + expand: false, + shouldCloseOnMinExtent: false, + resizeOnScroll: false, + ); + } } class _AssetDetailBottomSheet extends ConsumerWidget { @@ -88,6 +138,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final cameraTitle = _getCameraInfoTitle(exifInfo); @@ -96,16 +150,16 @@ class _AssetDetailBottomSheet extends ConsumerWidget { // Asset Date and Time _SheetTile( title: _getDateTime(context, asset), - titleStyle: context.textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.w600), + titleStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16, + ), ), + const SheetLocationDetails(), // Details header _SheetTile( title: 'exif_bottom_sheet_details'.t(context: context), - titleStyle: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color, - fontWeight: FontWeight.w600, - ), + titleStyle: context.textTheme.labelLarge, ), // File info _SheetTile( diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart new file mode 100644 index 0000000000..2d22d063bd --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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/translate_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +class SheetLocationDetails extends ConsumerStatefulWidget { + const SheetLocationDetails({super.key}); + + @override + ConsumerState createState() => _SheetLocationDetailsState(); +} + +class _SheetLocationDetailsState extends ConsumerState { + BaseAsset? asset; + ExifInfo? exifInfo; + MapLibreMapController? _mapController; + + String? _getLocationName(ExifInfo? exifInfo) { + if (exifInfo == null) { + return null; + } + + final cityName = exifInfo.city; + final stateName = exifInfo.state; + + if (cityName != null && stateName != null) { + return "$cityName, $stateName"; + } + return null; + } + + void _onMapCreated(MapLibreMapController controller) { + _mapController = controller; + } + + void _onExifChanged( + AsyncValue? previous, + AsyncValue current, + ) { + asset = ref.read(currentAssetNotifier); + setState(() { + exifInfo = current.valueOrNull; + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + if (exifInfo != null && hasCoordinates) { + _mapController?.moveCamera( + CameraUpdate.newLatLng( + LatLng(exifInfo!.latitude!, exifInfo!.longitude!), + ), + ); + } + }); + } + + @override + void initState() { + super.initState(); + ref.listenManual( + currentAssetExifProvider, + _onExifChanged, + fireImmediately: true, + ); + } + + @override + Widget build(BuildContext context) { + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + + // Guard no lat/lng + if (!hasCoordinates || + (asset is LocalAsset && !(asset as LocalAsset).hasRemote)) { + return const SizedBox.shrink(); + } + + final remoteId = asset is LocalAsset + ? (asset as LocalAsset).remoteId + : (asset as RemoteAsset).id; + final locationName = _getLocationName(exifInfo); + final coordinates = + "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}"; + + return Padding( + padding: EdgeInsets.symmetric( + vertical: 16.0, + horizontal: context.isMobile ? 16.0 : 56.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + "exif_bottom_sheet_location".t(context: context), + style: context.textTheme.labelLarge, + ), + ), + ExifMap( + exifInfo: exifInfo!, + markerId: remoteId, + onMapCreated: _onMapCreated, + ), + const SizedBox(height: 15), + if (locationName != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + locationName, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + Text( + coordinates, + style: context.textTheme.labelLarge?.copyWith( + color: context.textTheme.labelLarge?.color?.withAlpha(150), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart new file mode 100644 index 0000000000..b7e8477073 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -0,0 +1,114 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; + +class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { + const ViewerTopAppBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + + final user = ref.watch(currentUserProvider); + final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; + + final isShowingSheet = ref + .watch(assetViewerProvider.select((state) => state.showingBottomSheet)); + int opacity = ref.watch( + assetViewerProvider.select((state) => state.backgroundOpacity), + ); + final showControls = + ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0; + } + + final actions = [ + if (asset.hasRemote && isOwner && !asset.isFavorite) + const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), + if (asset.hasRemote && isOwner && asset.isFavorite) + const UnFavoriteActionButton( + source: ActionSource.viewer, + menuItem: true, + ), + const _KebabMenu(), + ]; + + return IgnorePointer( + ignoring: opacity < 255, + child: AnimatedOpacity( + opacity: opacity / 255, + duration: Durations.short2, + child: AppBar( + backgroundColor: + isShowingSheet ? Colors.transparent : Colors.black.withAlpha(125), + leading: const _AppBarBackButton(), + iconTheme: const IconThemeData(size: 22, color: Colors.white), + actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), + shape: const Border(), + actions: isShowingSheet ? null : actions, + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(60.0); +} + +class _KebabMenu extends ConsumerWidget { + const _KebabMenu(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return IconButton( + onPressed: () { + EventStream.shared.emit(const ViewerOpenBottomSheetEvent()); + }, + icon: const Icon(Icons.more_vert_rounded), + ); + } +} + +class _AppBarBackButton extends ConsumerWidget { + const _AppBarBackButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isShowingSheet = ref + .watch(assetViewerProvider.select((state) => state.showingBottomSheet)); + final backgroundColor = + isShowingSheet && !context.isDarkTheme ? Colors.white : Colors.black; + final foregroundColor = + isShowingSheet && !context.isDarkTheme ? Colors.black : Colors.white; + + return Padding( + padding: const EdgeInsets.only(left: 12.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor, + shape: const CircleBorder(), + iconSize: 22, + iconColor: foregroundColor, + padding: EdgeInsets.zero, + elevation: isShowingSheet ? 4 : 0, + ), + onPressed: context.maybePop, + child: const Icon(Icons.arrow_back_rounded), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart index 1fb98a0032..2db8ae2b4c 100644 --- a/mobile/lib/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart @@ -89,17 +89,17 @@ class _BaseDraggableScrollableSheetState const SizedBox(height: 14), if (widget.actions.isNotEmpty) SizedBox( - height: 80, + height: 115, child: ListView( shrinkWrap: true, scrollDirection: Axis.horizontal, children: widget.actions, ), ), - if (widget.actions.isNotEmpty) const SizedBox(height: 14), - if (widget.actions.isNotEmpty) - const Divider(indent: 20, endIndent: 20), - if (widget.actions.isNotEmpty) const SizedBox(height: 14), + if (widget.actions.isNotEmpty) ...[ + const Divider(indent: 16, endIndent: 16), + const SizedBox(height: 16), + ], ], ), ), diff --git a/mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart b/mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart index 4bfac063c9..b71f53acae 100644 --- a/mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_buton.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; diff --git a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart new file mode 100644 index 0000000000..79e6288a72 --- /dev/null +++ b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart @@ -0,0 +1,64 @@ +// ignore_for_file: require_trailing_commas + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; + +import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart'; + +class DriftMemoryBottomInfo extends StatelessWidget { + final DriftMemory memory; + final String title; + const DriftMemoryBottomInfo({ + super.key, + required this.memory, + required this.title, + }); + + @override + Widget build(BuildContext context) { + final df = DateFormat.yMMMMd(); + final fileCreatedDate = memory.assets.first.createdAt; + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: Colors.grey[400], + fontSize: 13.0, + fontWeight: FontWeight.w500, + ), + ), + Text( + df.format(fileCreatedDate), + style: const TextStyle( + color: Colors.white, + fontSize: 15.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + MaterialButton( + minWidth: 0, + onPressed: () { + context.maybePop(); + scrollToDateNotifierProvider.scrollToDate(fileCreatedDate); + }, + shape: const CircleBorder(), + color: Colors.white.withValues(alpha: 0.2), + elevation: 0, + child: const Icon( + Icons.open_in_new, + color: Colors.white, + ), + ), + ]), + ); + } +} diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart new file mode 100644 index 0000000000..8268196089 --- /dev/null +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -0,0 +1,159 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; + +class DriftMemoryCard extends StatelessWidget { + final RemoteAsset asset; + final String title; + final bool showTitle; + final Function()? onVideoEnded; + + const DriftMemoryCard({ + required this.asset, + required this.title, + required this.showTitle, + this.onVideoEnded, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.black, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + side: BorderSide( + color: Colors.black, + width: 1.0, + ), + ), + clipBehavior: Clip.hardEdge, + child: Stack( + children: [ + SizedBox.expand( + child: _BlurredBackdrop(asset: asset), + ), + LayoutBuilder( + builder: (context, constraints) { + // Determine the fit using the aspect ratio + BoxFit fit = BoxFit.contain; + if (asset.width != null && asset.height != null) { + final aspectRatio = asset.width! / asset.height!; + final phoneAspectRatio = + constraints.maxWidth / constraints.maxHeight; + // Look for a 25% difference in either direction + if (phoneAspectRatio * .75 < aspectRatio && + phoneAspectRatio * 1.25 > aspectRatio) { + // Cover to look nice if we have nearly the same aspect ratio + fit = BoxFit.cover; + } + } + + if (asset.isImage) { + return Hero( + tag: 'memory-${asset.id}', + child: FullImage( + asset, + fit: fit, + size: const Size(double.infinity, double.infinity), + ), + ); + } else { + return Hero( + tag: 'memory-${asset.id}', + // child: SizedBox( + // width: context.width, + // height: context.height, + // child: NativeVideoViewerPage( + // key: ValueKey(asset.id), + // asset: asset, + // showControls: false, + // playbackDelayFactor: 2, + // image: ImmichImage( + // asset, + // width: context.width, + // height: context.height, + // fit: BoxFit.contain, + // ), + // ), + // ), + child: FullImage( + asset, + fit: fit, + size: const Size(double.infinity, double.infinity), + ), + ); + } + }, + ), + if (showTitle) + Positioned( + left: 18.0, + bottom: 18.0, + child: Text( + title, + style: context.textTheme.headlineMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } +} + +class _BlurredBackdrop extends HookWidget { + final RemoteAsset asset; + + const _BlurredBackdrop({required this.asset}); + + @override + Widget build(BuildContext context) { + final blurhash = useDriftBlurHashRef(asset).value; + if (blurhash != null) { + // Use a nice cheap blur hash image decoration + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: MemoryImage( + blurhash, + ), + fit: BoxFit.cover, + ), + ), + child: Container( + color: Colors.black.withValues(alpha: 0.2), + ), + ); + } else { + // Fall back to using a more expensive image filtered + // Since the ImmichImage is already precached, we can + // safely use that as the image provider + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: getFullImageProvider( + asset, + size: Size(context.width, context.height), + ), + fit: BoxFit.cover, + ), + ), + child: Container( + color: Colors.black.withValues(alpha: 0.2), + ), + ), + ); + } + } +} diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart new file mode 100644 index 0000000000..aa21f36dd1 --- /dev/null +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -0,0 +1,115 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class DriftMemoryLane extends ConsumerWidget { + final List memories; + + const DriftMemoryLane({super.key, required this.memories}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200, + ), + child: CarouselView( + itemExtent: 145.0, + shrinkExtent: 1.0, + elevation: 2, + backgroundColor: Colors.black, + overlayColor: WidgetStateProperty.all( + Colors.white.withValues(alpha: 0.1), + ), + onTap: (index) { + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + + if (memories[index].assets.isNotEmpty) { + final asset = memories[index].assets[0]; + ref.read(currentAssetNotifier.notifier).setAsset(asset); + + if (asset.isVideo) { + ref.read(videoPlaybackValueProvider.notifier).reset(); + } + } + + context.pushRoute( + DriftMemoryRoute( + memories: memories, + memoryIndex: index, + ), + ); + }, + children: + memories.map((memory) => DriftMemoryCard(memory: memory)).toList(), + ), + ); + } +} + +class DriftMemoryCard extends ConsumerWidget { + const DriftMemoryCard({ + super.key, + required this.memory, + }); + + final DriftMemory memory; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final yearsAgo = DateTime.now().year - memory.data.year; + final title = 'years_ago'.t( + context: context, + args: { + 'years': yearsAgo.toString(), + }, + ); + return Center( + child: Stack( + children: [ + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withValues(alpha: 0.2), + BlendMode.darken, + ), + child: Hero( + tag: 'memory-${memory.assets[0].id}', + child: SizedBox( + width: 205, + height: 200, + child: Thumbnail( + remoteId: memory.assets[0].id, + fit: BoxFit.cover, + ), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 114, + ), + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index e660f77767..f88c123e1a 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -26,6 +26,8 @@ class Scrubber extends ConsumerStatefulWidget { final double bottomPadding; + final double? monthSegmentSnappingOffset; + Scrubber({ super.key, Key? scrollThumbKey, @@ -33,6 +35,7 @@ class Scrubber extends ConsumerStatefulWidget { required this.timelineHeight, this.topPadding = 0, this.bottomPadding = 0, + this.monthSegmentSnappingOffset, required this.child, }) : assert(child.scrollDirection == Axis.vertical); @@ -296,7 +299,10 @@ class ScrubberState extends ConsumerState final viewportHeight = _scrollController.position.viewportDimension; final targetScrollOffset = layoutSegment.startOffset; - final centeredOffset = targetScrollOffset - (viewportHeight / 4) + 100; + final centeredOffset = targetScrollOffset - + (viewportHeight / 4) + + 100 + + (widget.monthSegmentSnappingOffset ?? 0.0); _scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent)); } diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index fd0806cff0..04015aafe9 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:collection/collection.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart'; @@ -18,7 +20,10 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; class Timeline extends StatelessWidget { - const Timeline({super.key}); + const Timeline({super.key, this.topSliverWidget, this.topSliverWidgetHeight}); + + final Widget? topSliverWidget; + final double? topSliverWidgetHeight; @override Widget build(BuildContext context) { @@ -36,103 +41,135 @@ class Timeline extends StatelessWidget { ), ), ], - child: const _SliverTimeline(), + child: _SliverTimeline( + topSliverWidget: topSliverWidget, + topSliverWidgetHeight: topSliverWidgetHeight, + ), ), ), ); } } -class _SliverTimeline extends StatefulWidget { - const _SliverTimeline(); +class _SliverTimeline extends ConsumerStatefulWidget { + const _SliverTimeline({this.topSliverWidget, this.topSliverWidgetHeight}); + + final Widget? topSliverWidget; + final double? topSliverWidgetHeight; @override - State createState() => _SliverTimelineState(); + ConsumerState createState() => _SliverTimelineState(); } -class _SliverTimelineState extends State<_SliverTimeline> { +class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final _scrollController = ScrollController(); + StreamSubscription? _reloadSubscription; + + @override + void initState() { + super.initState(); + _reloadSubscription = + EventStream.shared.listen((_) => setState(() {})); + } @override void dispose() { _scrollController.dispose(); + _reloadSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext _) { - return Consumer( - builder: (context, ref, child) { - final asyncSegments = ref.watch(timelineSegmentProvider); - final maxHeight = - ref.watch(timelineArgsProvider.select((args) => args.maxHeight)); - final isMultiSelectEnabled = - ref.watch(multiSelectProvider.select((s) => s.isEnabled)); - return asyncSegments.widgetWhen( - onData: (segments) { - final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; - final statusBarHeight = context.padding.top; - final totalAppBarHeight = statusBarHeight + kToolbarHeight; - const scrubberBottomPadding = 100.0; + final asyncSegments = ref.watch(timelineSegmentProvider); + final maxHeight = + ref.watch(timelineArgsProvider.select((args) => args.maxHeight)); + return asyncSegments.widgetWhen( + onData: (segments) { + final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; + final statusBarHeight = context.padding.top; + final totalAppBarHeight = statusBarHeight + kToolbarHeight; + const scrubberBottomPadding = 100.0; - return PrimaryScrollController( - controller: _scrollController, - child: Stack( - children: [ - Scrubber( - layoutSegments: segments, - timelineHeight: maxHeight, - topPadding: totalAppBarHeight + 10, - bottomPadding: - context.padding.bottom + scrubberBottomPadding, - child: CustomScrollView( - primary: true, - cacheExtent: maxHeight * 2, - slivers: [ - SliverAnimatedOpacity( - duration: Durations.medium1, - opacity: isMultiSelectEnabled ? 0 : 1, - sliver: const ImmichSliverAppBar( - floating: true, - pinned: false, - snap: false, - ), - ), - _SliverSegmentedList( - segments: segments, - delegate: SliverChildBuilderDelegate( - (ctx, index) { - if (index >= childCount) return null; - final segment = segments.findByIndex(index); - return segment?.builder(ctx, index) ?? - const SizedBox.shrink(); - }, - childCount: childCount, - addAutomaticKeepAlives: false, - // We add repaint boundary around tiles, so skip the auto boundaries - addRepaintBoundaries: false, - ), - ), - const SliverPadding( - padding: EdgeInsets.only( - bottom: scrubberBottomPadding, - ), - ), - ], + return PrimaryScrollController( + controller: _scrollController, + child: Stack( + children: [ + Scrubber( + layoutSegments: segments, + timelineHeight: maxHeight, + topPadding: totalAppBarHeight + 10, + bottomPadding: context.padding.bottom + scrubberBottomPadding, + monthSegmentSnappingOffset: widget.topSliverWidgetHeight, + child: CustomScrollView( + primary: true, + cacheExtent: maxHeight * 2, + slivers: [ + const ImmichSliverAppBar( + floating: true, + pinned: false, + snap: false, ), - ), - if (isMultiSelectEnabled) ...[ - const Positioned( - top: 60, - left: 25, - child: _MultiSelectStatusButton(), + if (widget.topSliverWidget != null) widget.topSliverWidget!, + _SliverSegmentedList( + segments: segments, + delegate: SliverChildBuilderDelegate( + (ctx, index) { + if (index >= childCount) return null; + final segment = segments.findByIndex(index); + return segment?.builder(ctx, index) ?? + const SizedBox.shrink(); + }, + childCount: childCount, + addAutomaticKeepAlives: false, + // We add repaint boundary around tiles, so skip the auto boundaries + addRepaintBoundaries: false, + ), + ), + const SliverPadding( + padding: EdgeInsets.only( + bottom: scrubberBottomPadding, + ), ), - const HomeBottomAppBar(), ], - ], + ), ), - ); - }, + Consumer( + builder: (_, consumerRef, child) { + final isMultiSelectEnabled = consumerRef.watch( + multiSelectProvider.select( + (s) => s.isEnabled, + ), + ); + + if (isMultiSelectEnabled) { + return child!; + } + return const SizedBox.shrink(); + }, + child: const Positioned( + top: 60, + left: 25, + child: _MultiSelectStatusButton(), + ), + ), + Consumer( + builder: (_, consumerRef, child) { + final isMultiSelectEnabled = consumerRef.watch( + multiSelectProvider.select( + (s) => s.isEnabled, + ), + ); + + if (isMultiSelectEnabled) { + return child!; + } + return const SizedBox.shrink(); + }, + child: const HomeBottomAppBar(), + ), + ], + ), ); }, ); diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index da36ae02cf..ad62c1ac1c 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.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/services/action.service.dart'; @@ -55,7 +56,10 @@ class ActionNotifier extends Notifier { final Set assets = switch (source) { ActionSource.timeline => ref.read(multiSelectProvider.select((s) => s.selectedAssets)), - ActionSource.viewer => {}, + ActionSource.viewer => switch (ref.read(currentAssetNotifier)) { + BaseAsset asset => {asset}, + null => {}, + }, }; return switch (T) { diff --git a/mobile/lib/providers/infrastructure/asset.provider.dart b/mobile/lib/providers/infrastructure/asset.provider.dart index 860af134ae..0015986243 100644 --- a/mobile/lib/providers/infrastructure/asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset.provider.dart @@ -15,5 +15,6 @@ final remoteAssetRepositoryProvider = Provider( final assetServiceProvider = Provider( (ref) => AssetService( remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider), + localAssetRepository: ref.watch(localAssetRepository), ), ); diff --git a/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart index 48cf190bbc..996d5d816f 100644 --- a/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart @@ -1,26 +1,48 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; final currentAssetNotifier = - NotifierProvider(CurrentAssetNotifier.new); + AutoDisposeNotifierProvider( + CurrentAssetNotifier.new, +); + +class CurrentAssetNotifier extends AutoDisposeNotifier { + KeepAliveLink? _keepAliveLink; + StreamSubscription? _assetSubscription; -class CurrentAssetNotifier extends Notifier { @override - BaseAsset build() { - throw UnimplementedError( - 'An asset must be set before using the currentAssetProvider.', - ); - } + BaseAsset? build() => null; void setAsset(BaseAsset asset) { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); state = asset; + _assetSubscription = ref + .watch(assetServiceProvider) + .watchAsset(asset) + .listen((updatedAsset) { + if (updatedAsset != null) { + state = updatedAsset; + } + }); + _keepAliveLink = ref.keepAlive(); + } + + void dispose() { + _keepAliveLink?.close(); + _assetSubscription?.cancel(); } } -final currentAssetExifProvider = FutureProvider( +final currentAssetExifProvider = FutureProvider.autoDispose( (ref) { final currentAsset = ref.watch(currentAssetNotifier); + if (currentAsset == null) { + return null; + } return ref.watch(assetServiceProvider).getExif(currentAsset); }, ); diff --git a/mobile/lib/providers/infrastructure/memory.provider.dart b/mobile/lib/providers/infrastructure/memory.provider.dart new file mode 100644 index 0000000000..0e58943f55 --- /dev/null +++ b/mobile/lib/providers/infrastructure/memory.provider.dart @@ -0,0 +1,27 @@ +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/domain/services/memory.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'db.provider.dart'; + +final driftMemoryRepositoryProvider = Provider( + (ref) => DriftMemoryRepository(ref.watch(driftProvider)), +); + +final driftMemoryServiceProvider = Provider( + (ref) => DriftMemoryService(ref.watch(driftMemoryRepositoryProvider)), +); + +final driftMemoryFutureProvider = + FutureProvider.autoDispose>((ref) async { + final user = ref.watch(currentUserProvider); + if (user == null) { + return []; + } + + final service = ref.watch(driftMemoryServiceProvider); + + return service.getMemoryLane(user.id); +}); diff --git a/mobile/lib/providers/stack.provider.dart b/mobile/lib/providers/stack.provider.dart new file mode 100644 index 0000000000..71abd1e87a --- /dev/null +++ b/mobile/lib/providers/stack.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftStackProvider = Provider( + (ref) => DriftStackRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 3ad8e34580..4ee4d8c131 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -40,6 +40,9 @@ class AuthRepository extends DatabaseRepository { _drift.remoteAlbumEntity.deleteAll(), _drift.remoteAlbumAssetEntity.deleteAll(), _drift.remoteAlbumUserEntity.deleteAll(), + _drift.memoryEntity.deleteAll(), + _drift.memoryAssetEntity.deleteAll(), + _drift.stackEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index dae2dcdbfb..8513b0606c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -65,12 +66,16 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/drift_locked_folder.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; +import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; @@ -83,6 +88,7 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; + import 'package:maplibre_gl/maplibre_gl.dart'; part 'router.gr.dart'; @@ -385,6 +391,22 @@ class AppRouter extends RootStackRouter { ), ), ), + AutoRoute( + page: DriftMemoryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftTrashRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftArchiveRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: DriftLockedFolderRoute.page, + guards: [_authGuard, _duplicateGuard], + ), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 348cea656e..cf34bc0d50 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -618,6 +618,106 @@ class DriftAlbumsRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftArchivePage] +class DriftArchiveRoute extends PageRouteInfo { + const DriftArchiveRoute({List? children}) + : super(DriftArchiveRoute.name, initialChildren: children); + + static const String name = 'DriftArchiveRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftArchivePage(); + }, + ); +} + +/// generated route for +/// [DriftLockedFolderPage] +class DriftLockedFolderRoute extends PageRouteInfo { + const DriftLockedFolderRoute({List? children}) + : super(DriftLockedFolderRoute.name, initialChildren: children); + + static const String name = 'DriftLockedFolderRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftLockedFolderPage(); + }, + ); +} + +/// generated route for +/// [DriftMemoryPage] +class DriftMemoryRoute extends PageRouteInfo { + DriftMemoryRoute({ + required List memories, + required int memoryIndex, + Key? key, + List? children, + }) : super( + DriftMemoryRoute.name, + args: DriftMemoryRouteArgs( + memories: memories, + memoryIndex: memoryIndex, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'DriftMemoryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return DriftMemoryPage( + memories: args.memories, + memoryIndex: args.memoryIndex, + key: args.key, + ); + }, + ); +} + +class DriftMemoryRouteArgs { + const DriftMemoryRouteArgs({ + required this.memories, + required this.memoryIndex, + this.key, + }); + + final List memories; + + final int memoryIndex; + + final Key? key; + + @override + String toString() { + return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; + } +} + +/// generated route for +/// [DriftTrashPage] +class DriftTrashRoute extends PageRouteInfo { + const DriftTrashRoute({List? children}) + : super(DriftTrashRoute.name, initialChildren: children); + + static const String name = 'DriftTrashRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftTrashPage(); + }, + ); +} + /// generated route for /// [EditImagePage] class EditImageRoute extends PageRouteInfo { diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index 9231e2d972..62208c4cf5 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:thumbhash/thumbhash.dart' as thumbhash; @@ -15,3 +16,15 @@ ObjectRef useBlurHashRef(Asset? asset) { return useRef(thumbhash.rgbaToBmp(rbga)); } + +ObjectRef useDriftBlurHashRef(RemoteAsset? asset) { + if (asset?.thumbHash == null) { + return useRef(null); + } + + final rbga = thumbhash.thumbHashToRGBA( + base64Decode(asset!.thumbHash!), + ); + + return useRef(thumbhash.rgbaToBmp(rbga)); +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index f3f72dfd87..7b6325cf2c 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -9,11 +9,13 @@ import 'package:url_launcher/url_launcher.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; final String? markerId; + final MapCreatedCallback? onMapCreated; const ExifMap({ super.key, required this.exifInfo, this.markerId = 'marker', + this.onMapCreated, }); @override @@ -82,6 +84,7 @@ class ExifMap extends StatelessWidget { debugPrint('Opening Map Uri: $uri'); launchUrl(uri); }, + onCreated: onMapCreated, ); }, ); diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 51a3a136b9..ff0e88e5d7 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.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/cast.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; @@ -39,64 +40,70 @@ class ImmichSliverAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + final isMultiSelectEnabled = + ref.watch(multiSelectProvider.select((s) => s.isEnabled)); - return SliverAppBar( - floating: floating, - pinned: pinned, - snap: snap, - expandedHeight: expandedHeight, - backgroundColor: context.colorScheme.surfaceContainer, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(5), - ), - ), - automaticallyImplyLeading: false, - centerTitle: false, - title: title ?? const _ImmichLogoWithText(), - actions: [ - if (actions != null) - ...actions!.map( - (action) => Padding( - padding: const EdgeInsets.only(right: 16), - child: action, - ), - ), - IconButton( - icon: const Icon(Icons.swipe_left_alt_rounded), - onPressed: () => context.pop(), - ), - IconButton( - onPressed: () => ref.read(backgroundSyncProvider).syncRemote(), - icon: const Icon( - Icons.sync, + return SliverAnimatedOpacity( + duration: Durations.medium1, + opacity: isMultiSelectEnabled ? 0 : 1, + sliver: SliverAppBar( + floating: floating, + pinned: pinned, + snap: snap, + expandedHeight: expandedHeight, + backgroundColor: context.colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), ), ), - if (isCasting) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => const CastDialog(), - ); - }, - icon: Icon( - isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + automaticallyImplyLeading: false, + centerTitle: false, + title: title ?? const _ImmichLogoWithText(), + actions: [ + if (actions != null) + ...actions!.map( + (action) => Padding( + padding: const EdgeInsets.only(right: 16), + child: action, ), ), + IconButton( + icon: const Icon(Icons.swipe_left_alt_rounded), + onPressed: () => context.pop(), ), - if (showUploadButton) + IconButton( + onPressed: () => ref.read(backgroundSyncProvider).syncRemote(), + icon: const Icon( + Icons.sync, + ), + ), + if (isCasting) + Padding( + padding: const EdgeInsets.only(right: 12), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const CastDialog(), + ); + }, + icon: Icon( + isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + ), + ), + ), + if (showUploadButton) + const Padding( + padding: EdgeInsets.only(right: 20), + child: _BackupIndicator(), + ), const Padding( padding: EdgeInsets.only(right: 20), - child: _BackupIndicator(), + child: _ProfileIndicator(), ), - const Padding( - padding: EdgeInsets.only(right: 20), - child: _ProfileIndicator(), - ), - ], + ], + ), ); } } diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index 8866cb01b0..e8501f1184 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -5,9 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/widgets/common/transparent_image.dart'; @@ -26,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - bool isDarkTheme = context.themeData.brightness == Brightness.dark; + final userAvatarColor = user.avatarColor.toColor(); final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}'; @@ -34,14 +32,14 @@ class UserCircleAvatar extends ConsumerWidget { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, - color: isDarkTheme && user.avatarColor == AvatarColor.primary + color: userAvatarColor.computeLuminance() > 0.5 ? Colors.black : Colors.white, ), child: Text(user.name[0].toUpperCase()), ); return CircleAvatar( - backgroundColor: user.avatarColor.toColor(), + backgroundColor: userAvatarColor, radius: radius, child: user.profileImagePath == null ? textIcon diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index b225a2edcb..06935cd4b5 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -4,6 +4,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/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; @@ -24,6 +25,7 @@ class MapThumbnail extends HookConsumerWidget { final double width; final ThemeMode? themeMode; final bool showAttribution; + final MapCreatedCallback? onCreated; const MapThumbnail({ super.key, @@ -36,16 +38,19 @@ class MapThumbnail extends HookConsumerWidget { this.showMarkerPin = false, this.themeMode, this.showAttribution = true, + this.onCreated, }); @override Widget build(BuildContext context, WidgetRef ref) { final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude); final controller = useRef(null); + final styleLoaded = useState(false); final position = useValueNotifier?>(null); Future onMapCreated(MapLibreMapController mapController) async { controller.value = mapController; + styleLoaded.value = false; if (assetMarkerRemoteId != null) { // The iOS impl returns wrong toScreenLocation without the delay Future.delayed( @@ -54,17 +59,26 @@ class MapThumbnail extends HookConsumerWidget { position.value = await mapController.toScreenLocation(centre), ); } + onCreated?.call(mapController); } Future onStyleLoaded() async { if (showMarkerPin && controller.value != null) { await controller.value?.addMarkerAtLatLng(centre); } + styleLoaded.value = true; } return MapThemeOverride( themeMode: themeMode, - mapBuilder: (style) => SizedBox( + mapBuilder: (style) => AnimatedContainer( + duration: Durations.medium2, + curve: Curves.easeOut, + foregroundDecoration: BoxDecoration( + color: context.colorScheme.inverseSurface + .withAlpha(styleLoaded.value ? 0 : 200), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), height: height, width: width, child: ClipRRect( diff --git a/mobile/lib/widgets/photo_view/photo_view.dart b/mobile/lib/widgets/photo_view/photo_view.dart index 0c1a4f4855..30e08748b8 100644 --- a/mobile/lib/widgets/photo_view/photo_view.dart +++ b/mobile/lib/widgets/photo_view/photo_view.dart @@ -660,7 +660,7 @@ typedef PhotoViewImageTapDownCallback = Function( typedef PhotoViewImageDragStartCallback = Function( BuildContext context, DragStartDetails details, - PhotoViewControllerValue controllerValue, + PhotoViewControllerBase controllerValue, PhotoViewScaleStateController scaleStateController, ); diff --git a/mobile/lib/widgets/photo_view/photo_view_gallery.dart b/mobile/lib/widgets/photo_view/photo_view_gallery.dart index cf026288fb..1cd4d4b217 100644 --- a/mobile/lib/widgets/photo_view/photo_view_gallery.dart +++ b/mobile/lib/widgets/photo_view/photo_view_gallery.dart @@ -271,7 +271,7 @@ class _PhotoViewGalleryState extends State { final PhotoView photoView = isCustomChild ? PhotoView.customChild( - key: ObjectKey(index), + key: pageOption.key ?? ObjectKey(index), childSize: pageOption.childSize, backgroundDecoration: widget.backgroundDecoration, wantKeepAlive: widget.wantKeepAlive, @@ -304,7 +304,7 @@ class _PhotoViewGalleryState extends State { child: pageOption.child, ) : PhotoView( - key: ObjectKey(index), + key: pageOption.key ?? ObjectKey(index), index: index, imageProvider: pageOption.imageProvider, loadingBuilder: widget.loadingBuilder, @@ -363,7 +363,7 @@ class _PhotoViewGalleryState extends State { /// class PhotoViewGalleryPageOptions { PhotoViewGalleryPageOptions({ - Key? key, + this.key, required this.imageProvider, this.heroAttributes, this.semanticLabel, @@ -392,6 +392,7 @@ class PhotoViewGalleryPageOptions { assert(imageProvider != null); const PhotoViewGalleryPageOptions.customChild({ + this.key, required this.child, this.childSize, this.semanticLabel, @@ -418,6 +419,8 @@ class PhotoViewGalleryPageOptions { }) : errorBuilder = null, imageProvider = null; + final Key? key; + /// Mirror to [PhotoView.imageProvider] final ImageProvider? imageProvider; diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart index bc97da1e06..6b6e5067c5 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart @@ -416,7 +416,7 @@ class PhotoViewCoreState extends State ? (details) => widget.onDragStart!( context, details, - widget.controller.value, + widget.controller, widget.scaleStateController, ) : null, diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index e100e8e5ca..c8f91be1f1 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -24,6 +24,7 @@ class SystemConfigOAuthDto { required this.mobileOverrideEnabled, required this.mobileRedirectUri, required this.profileSigningAlgorithm, + required this.roleClaim, required this.scope, required this.signingAlgorithm, required this.storageLabelClaim, @@ -55,6 +56,8 @@ class SystemConfigOAuthDto { String profileSigningAlgorithm; + String roleClaim; + String scope; String signingAlgorithm; @@ -81,6 +84,7 @@ class SystemConfigOAuthDto { other.mobileOverrideEnabled == mobileOverrideEnabled && other.mobileRedirectUri == mobileRedirectUri && other.profileSigningAlgorithm == profileSigningAlgorithm && + other.roleClaim == roleClaim && other.scope == scope && other.signingAlgorithm == signingAlgorithm && other.storageLabelClaim == storageLabelClaim && @@ -102,6 +106,7 @@ class SystemConfigOAuthDto { (mobileOverrideEnabled.hashCode) + (mobileRedirectUri.hashCode) + (profileSigningAlgorithm.hashCode) + + (roleClaim.hashCode) + (scope.hashCode) + (signingAlgorithm.hashCode) + (storageLabelClaim.hashCode) + @@ -110,7 +115,7 @@ class SystemConfigOAuthDto { (tokenEndpointAuthMethod.hashCode); @override - String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; + String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]'; Map toJson() { final json = {}; @@ -129,6 +134,7 @@ class SystemConfigOAuthDto { json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled; json[r'mobileRedirectUri'] = this.mobileRedirectUri; json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm; + json[r'roleClaim'] = this.roleClaim; json[r'scope'] = this.scope; json[r'signingAlgorithm'] = this.signingAlgorithm; json[r'storageLabelClaim'] = this.storageLabelClaim; @@ -158,6 +164,7 @@ class SystemConfigOAuthDto { mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, mobileRedirectUri: mapValueOfType(json, r'mobileRedirectUri')!, profileSigningAlgorithm: mapValueOfType(json, r'profileSigningAlgorithm')!, + roleClaim: mapValueOfType(json, r'roleClaim')!, scope: mapValueOfType(json, r'scope')!, signingAlgorithm: mapValueOfType(json, r'signingAlgorithm')!, storageLabelClaim: mapValueOfType(json, r'storageLabelClaim')!, @@ -222,6 +229,7 @@ class SystemConfigOAuthDto { 'mobileOverrideEnabled', 'mobileRedirectUri', 'profileSigningAlgorithm', + 'roleClaim', 'scope', 'signingAlgorithm', 'storageLabelClaim', diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index a0b61bcaff..27cd8c5b21 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -81,6 +81,26 @@ void main() { debugLabel: any(named: 'debugLabel'), ), ).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateMemoriesV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteMemoriesV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any())) + .thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateStacksV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.deleteStacksV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, @@ -227,5 +247,94 @@ void main() { verify(() => mockSyncApiRepo.ack(["2"])).called(1); }, ); + + test("processes memory sync events successfully", () async { + final events = [ + SyncStreamStub.memoryV1, + SyncStreamStub.memoryDeleteV1, + SyncStreamStub.memoryToAssetV1, + SyncStreamStub.memoryToAssetDeleteV1, + ]; + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.updateMemoriesV1(any()), + () => mockSyncApiRepo.ack(["5"]), + () => mockSyncStreamRepo.deleteMemoriesV1(any()), + () => mockSyncApiRepo.ack(["6"]), + () => mockSyncStreamRepo.updateMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["7"]), + () => mockSyncStreamRepo.deleteMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["8"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); + }); + + test("processes mixed memory and user events in correct order", () async { + final events = [ + SyncStreamStub.memoryDeleteV1, + SyncStreamStub.userV1Admin, + SyncStreamStub.memoryToAssetV1, + SyncStreamStub.memoryV1, + ]; + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.deleteMemoriesV1(any()), + () => mockSyncApiRepo.ack(["6"]), + () => mockSyncStreamRepo.updateUsersV1(any()), + () => mockSyncApiRepo.ack(["1"]), + () => mockSyncStreamRepo.updateMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["7"]), + () => mockSyncStreamRepo.updateMemoriesV1(any()), + () => mockSyncApiRepo.ack(["5"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); + }); + + test("handles memory sync failure gracefully", () async { + when(() => mockSyncStreamRepo.updateMemoriesV1(any())) + .thenThrow(Exception("Memory sync failed")); + + final events = [ + SyncStreamStub.memoryV1, + SyncStreamStub.userV1Admin, + ]; + + expect( + () async => await simulateEvents(events), + throwsA(isA()), + ); + }); + + test("processes memory asset events with correct data types", () async { + final events = [SyncStreamStub.memoryToAssetV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["7"])).called(1); + }); + + test("processes memory delete events with correct data types", () async { + final events = [SyncStreamStub.memoryDeleteV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.deleteMemoriesV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["6"])).called(1); + }); + + test("processes memory create/update events with correct data types", + () async { + final events = [SyncStreamStub.memoryV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.updateMemoriesV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["5"])).called(1); + }); }); } diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index ba97f1434a..de2d58bc9d 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -42,4 +42,47 @@ abstract final class SyncStreamStub { data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"), ack: "4", ); + + static final memoryV1 = SyncEvent( + type: SyncEntityType.memoryV1, + data: SyncMemoryV1( + createdAt: DateTime(2023, 1, 1), + data: {"year": 2023, "title": "Test Memory"}, + deletedAt: null, + hideAt: null, + id: "memory-1", + isSaved: false, + memoryAt: DateTime(2023, 1, 1), + ownerId: "user-1", + seenAt: null, + showAt: DateTime(2023, 1, 1), + type: MemoryType.onThisDay, + updatedAt: DateTime(2023, 1, 1), + ), + ack: "5", + ); + + static final memoryDeleteV1 = SyncEvent( + type: SyncEntityType.memoryDeleteV1, + data: SyncMemoryDeleteV1(memoryId: "memory-2"), + ack: "6", + ); + + static final memoryToAssetV1 = SyncEvent( + type: SyncEntityType.memoryToAssetV1, + data: SyncMemoryAssetV1( + assetId: "asset-1", + memoryId: "memory-1", + ), + ack: "7", + ); + + static final memoryToAssetDeleteV1 = SyncEvent( + type: SyncEntityType.memoryToAssetDeleteV1, + data: SyncMemoryAssetDeleteV1( + assetId: "asset-2", + memoryId: "memory-1", + ), + ack: "8", + ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 0dc0c43ec8..7a44a5cf6f 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -14654,6 +14654,9 @@ "profileSigningAlgorithm": { "type": "string" }, + "roleClaim": { + "type": "string" + }, "scope": { "type": "string" }, @@ -14690,6 +14693,7 @@ "mobileOverrideEnabled", "mobileRedirectUri", "profileSigningAlgorithm", + "roleClaim", "scope", "signingAlgorithm", "storageLabelClaim", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 24f9a6d75d..9eb9990d2c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1398,6 +1398,7 @@ export type SystemConfigOAuthDto = { mobileOverrideEnabled: boolean; mobileRedirectUri: string; profileSigningAlgorithm: string; + roleClaim: string; scope: string; signingAlgorithm: string; storageLabelClaim: string; diff --git a/server/src/config.ts b/server/src/config.ts index ae4bdcd906..1fcc2e9782 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -101,6 +101,7 @@ export interface SystemConfig { timeout: number; storageLabelClaim: string; storageQuotaClaim: string; + roleClaim: string; }; passwordLogin: { enabled: boolean; @@ -263,6 +264,7 @@ export const defaults = Object.freeze({ profileSigningAlgorithm: 'none', storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', + roleClaim: 'immich_role', tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, timeout: 30_000, }, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 03ef9192db..b0385984b4 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -395,6 +395,9 @@ class SystemConfigOAuthDto { @IsString() storageQuotaClaim!: string; + + @IsString() + roleClaim!: string; } class SystemConfigPasswordLoginDto { diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 3568bb9d6b..85c9f07815 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -711,6 +711,7 @@ describe(AuthService.name, () => { expect(mocks.user.create).toHaveBeenCalledWith({ email: user.email, + isAdmin: false, name: ' ', oauthId: user.oauthId, quotaSizeInBytes: 0, @@ -739,6 +740,7 @@ describe(AuthService.name, () => { expect(mocks.user.create).toHaveBeenCalledWith({ email: user.email, + isAdmin: false, name: ' ', oauthId: user.oauthId, quotaSizeInBytes: 5_368_709_120, @@ -805,6 +807,93 @@ describe(AuthService.name, () => { expect(mocks.user.update).not.toHaveBeenCalled(); expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled(); }); + + it('should only allow "admin" and "user" for the role claim', async () => { + const user = factory.userAdmin({ oauthId: 'oauth-id' }); + + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister); + mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'foo' }); + mocks.user.getByEmail.mockResolvedValue(void 0); + mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); + mocks.user.getByOAuthId.mockResolvedValue(void 0); + mocks.user.create.mockResolvedValue(user); + mocks.session.create.mockResolvedValue(factory.session()); + + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); + + expect(mocks.user.create).toHaveBeenCalledWith({ + email: user.email, + name: ' ', + oauthId: user.oauthId, + quotaSizeInBytes: null, + storageLabel: null, + isAdmin: false, + }); + }); + + it('should create an admin user if the role claim is set to admin', async () => { + const user = factory.userAdmin({ oauthId: 'oauth-id' }); + + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister); + mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'admin' }); + mocks.user.getByEmail.mockResolvedValue(void 0); + mocks.user.getByOAuthId.mockResolvedValue(void 0); + mocks.user.create.mockResolvedValue(user); + mocks.session.create.mockResolvedValue(factory.session()); + + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); + + expect(mocks.user.create).toHaveBeenCalledWith({ + email: user.email, + name: ' ', + oauthId: user.oauthId, + quotaSizeInBytes: null, + storageLabel: null, + isAdmin: true, + }); + }); + + it('should accept a custom role claim', async () => { + const user = factory.userAdmin({ oauthId: 'oauth-id' }); + + mocks.systemMetadata.get.mockResolvedValue({ + oauth: { ...systemConfigStub.oauthWithAutoRegister, roleClaim: 'my_role' }, + }); + mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, my_role: 'admin' }); + mocks.user.getByEmail.mockResolvedValue(void 0); + mocks.user.getByOAuthId.mockResolvedValue(void 0); + mocks.user.create.mockResolvedValue(user); + mocks.session.create.mockResolvedValue(factory.session()); + + await expect( + sut.callback( + { url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' }, + {}, + loginDetails, + ), + ).resolves.toEqual(oauthResponse(user)); + + expect(mocks.user.create).toHaveBeenCalledWith({ + email: user.email, + name: ' ', + oauthId: user.oauthId, + quotaSizeInBytes: null, + storageLabel: null, + isAdmin: true, + }); + }); }); describe('link', () => { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 70da8d81d3..ec3415ec8c 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -250,7 +250,7 @@ export class AuthService extends BaseService { const { oauth } = await this.getConfig({ withCache: false }); const url = this.resolveRedirectUri(oauth, dto.url); const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier); - const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth; + const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth; this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`); let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub); @@ -290,6 +290,11 @@ export class AuthService extends BaseService { default: defaultStorageQuota, isValid: (value: unknown) => Number(value) >= 0, }); + const role = this.getClaim<'admin' | 'user'>(profile, { + key: roleClaim, + default: 'user', + isValid: (value: unknown) => isString(value) && ['admin', 'user'].includes(value), + }); const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`; user = await this.createUser({ @@ -298,6 +303,7 @@ export class AuthService extends BaseService { oauthId: profile.sub, quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB, storageLabel: storageLabel || null, + isAdmin: role === 'admin', }); } diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 10f7becc7d..afef497e59 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -1,6 +1,8 @@ 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'; @@ -88,13 +90,11 @@ export class BackupService extends BaseService { ]; databaseParams.push('--clean', '--if-exists'); - + const databaseVersion = await this.databaseRepository.getPostgresVersion(); const backupFilePath = path.join( StorageCore.getBaseFolder(StorageFolder.BACKUPS), - `immich-db-backup-${Date.now()}.sql.gz.tmp`, + `immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, ); - - const databaseVersion = await this.databaseRepository.getPostgresVersion(); const databaseSemver = semver.coerce(databaseVersion); const databaseMajorVersion = databaseSemver?.major; diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 87bd92129e..c7b98cc990 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -124,6 +124,7 @@ const updatedConfig = Object.freeze({ timeout: 30_000, storageLabelClaim: 'preferred_username', storageQuotaClaim: 'immich_quota', + roleClaim: 'immich_role', }, passwordLogin: { enabled: true, diff --git a/server/src/sql-tools/diff/comparers/column.comparer.spec.ts b/server/src/sql-tools/comparers/column.comparer.spec.ts similarity index 89% rename from server/src/sql-tools/diff/comparers/column.comparer.spec.ts rename to server/src/sql-tools/comparers/column.comparer.spec.ts index 082d15f0db..25ef8543a8 100644 --- a/server/src/sql-tools/diff/comparers/column.comparer.spec.ts +++ b/server/src/sql-tools/comparers/column.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareColumns } from 'src/sql-tools/diff/comparers/column.comparer'; +import { compareColumns } from 'src/sql-tools/comparers/column.comparer'; import { DatabaseColumn, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -18,7 +18,7 @@ describe('compareColumns', () => { { tableName: 'table1', columnName: 'test', - type: 'column.drop', + type: 'ColumnDrop', reason: Reason.MissingInSource, }, ]); @@ -29,7 +29,7 @@ describe('compareColumns', () => { it('should work', () => { expect(compareColumns.onMissing(testColumn)).toEqual([ { - type: 'column.add', + type: 'ColumnAdd', column: testColumn, reason: Reason.MissingInTarget, }, @@ -50,11 +50,11 @@ describe('compareColumns', () => { { columnName: 'test', tableName: 'table1', - type: 'column.drop', + type: 'ColumnDrop', reason, }, { - type: 'column.add', + type: 'ColumnAdd', column: source, reason, }, @@ -69,7 +69,7 @@ describe('compareColumns', () => { { columnName: 'test', tableName: 'table1', - type: 'column.alter', + type: 'ColumnAlter', changes: { comment: 'new comment', }, diff --git a/server/src/sql-tools/diff/comparers/column.comparer.ts b/server/src/sql-tools/comparers/column.comparer.ts similarity index 90% rename from server/src/sql-tools/diff/comparers/column.comparer.ts rename to server/src/sql-tools/comparers/column.comparer.ts index 205bd594ae..5cc3f7a930 100644 --- a/server/src/sql-tools/diff/comparers/column.comparer.ts +++ b/server/src/sql-tools/comparers/column.comparer.ts @@ -4,14 +4,14 @@ import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/type export const compareColumns: Comparer = { onMissing: (source) => [ { - type: 'column.add', + type: 'ColumnAdd', column: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'column.drop', + type: 'ColumnDrop', tableName: target.tableName, columnName: target.name, reason: Reason.MissingInSource, @@ -31,7 +31,7 @@ export const compareColumns: Comparer = { const items: SchemaDiff[] = []; if (source.nullable !== target.nullable) { items.push({ - type: 'column.alter', + type: 'ColumnAlter', tableName: source.tableName, columnName: source.name, changes: { @@ -43,7 +43,7 @@ export const compareColumns: Comparer = { if (!isDefaultEqual(source, target)) { items.push({ - type: 'column.alter', + type: 'ColumnAlter', tableName: source.tableName, columnName: source.name, changes: { @@ -55,7 +55,7 @@ export const compareColumns: Comparer = { if (source.comment !== target.comment) { items.push({ - type: 'column.alter', + type: 'ColumnAlter', tableName: source.tableName, columnName: source.name, changes: { @@ -72,11 +72,11 @@ export const compareColumns: Comparer = { const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { return [ { - type: 'column.drop', + type: 'ColumnDrop', tableName: target.tableName, columnName: target.name, reason, }, - { type: 'column.add', column: source, reason }, + { type: 'ColumnAdd', column: source, reason }, ]; }; diff --git a/server/src/sql-tools/diff/comparers/constraint.comparer.spec.ts b/server/src/sql-tools/comparers/constraint.comparer.spec.ts similarity index 80% rename from server/src/sql-tools/diff/comparers/constraint.comparer.spec.ts rename to server/src/sql-tools/comparers/constraint.comparer.spec.ts index 69d8a8cc43..b5da19e8df 100644 --- a/server/src/sql-tools/diff/comparers/constraint.comparer.spec.ts +++ b/server/src/sql-tools/comparers/constraint.comparer.spec.ts @@ -1,9 +1,9 @@ -import { compareConstraints } from 'src/sql-tools/diff/comparers/constraint.comparer'; -import { DatabaseConstraint, DatabaseConstraintType, Reason } from 'src/sql-tools/types'; +import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer'; +import { ConstraintType, DatabaseConstraint, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; const testConstraint: DatabaseConstraint = { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'test', tableName: 'table1', columnNames: ['column1'], @@ -15,7 +15,7 @@ describe('compareConstraints', () => { it('should work', () => { expect(compareConstraints.onExtra(testConstraint)).toEqual([ { - type: 'constraint.drop', + type: 'ConstraintDrop', constraintName: 'test', tableName: 'table1', reason: Reason.MissingInSource, @@ -28,7 +28,7 @@ describe('compareConstraints', () => { it('should work', () => { expect(compareConstraints.onMissing(testConstraint)).toEqual([ { - type: 'constraint.add', + type: 'ConstraintAdd', constraint: testConstraint, reason: Reason.MissingInTarget, }, @@ -49,11 +49,11 @@ describe('compareConstraints', () => { { constraintName: 'test', tableName: 'table1', - type: 'constraint.drop', + type: 'ConstraintDrop', reason, }, { - type: 'constraint.add', + type: 'ConstraintAdd', constraint: source, reason, }, diff --git a/server/src/sql-tools/diff/comparers/constraint.comparer.ts b/server/src/sql-tools/comparers/constraint.comparer.ts similarity index 91% rename from server/src/sql-tools/diff/comparers/constraint.comparer.ts rename to server/src/sql-tools/comparers/constraint.comparer.ts index ccb594741c..0ff6fbe131 100644 --- a/server/src/sql-tools/diff/comparers/constraint.comparer.ts +++ b/server/src/sql-tools/comparers/constraint.comparer.ts @@ -2,9 +2,9 @@ import { haveEqualColumns } from 'src/sql-tools/helpers'; import { CompareFunction, Comparer, + ConstraintType, DatabaseCheckConstraint, DatabaseConstraint, - DatabaseConstraintType, DatabaseForeignKeyConstraint, DatabasePrimaryKeyConstraint, DatabaseUniqueConstraint, @@ -15,14 +15,14 @@ import { export const compareConstraints: Comparer = { onMissing: (source) => [ { - type: 'constraint.add', + type: 'ConstraintAdd', constraint: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'constraint.drop', + type: 'ConstraintDrop', tableName: target.tableName, constraintName: target.name, reason: Reason.MissingInSource, @@ -30,19 +30,19 @@ export const compareConstraints: Comparer = { ], onCompare: (source, target) => { switch (source.type) { - case DatabaseConstraintType.PRIMARY_KEY: { + case ConstraintType.PRIMARY_KEY: { return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint); } - case DatabaseConstraintType.FOREIGN_KEY: { + case ConstraintType.FOREIGN_KEY: { return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint); } - case DatabaseConstraintType.UNIQUE: { + case ConstraintType.UNIQUE: { return compareUniqueConstraint(source, target as DatabaseUniqueConstraint); } - case DatabaseConstraintType.CHECK: { + case ConstraintType.CHECK: { return compareCheckConstraint(source, target as DatabaseCheckConstraint); } @@ -123,11 +123,11 @@ const dropAndRecreateConstraint = ( ): SchemaDiff[] => { return [ { - type: 'constraint.drop', + type: 'ConstraintDrop', tableName: target.tableName, constraintName: target.name, reason, }, - { type: 'constraint.add', constraint: source, reason }, + { type: 'ConstraintAdd', constraint: source, reason }, ]; }; diff --git a/server/src/sql-tools/diff/comparers/enum.comparer.spec.ts b/server/src/sql-tools/comparers/enum.comparer.spec.ts similarity index 87% rename from server/src/sql-tools/diff/comparers/enum.comparer.spec.ts rename to server/src/sql-tools/comparers/enum.comparer.spec.ts index 6e1ad992d5..82fc205662 100644 --- a/server/src/sql-tools/diff/comparers/enum.comparer.spec.ts +++ b/server/src/sql-tools/comparers/enum.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareEnums } from 'src/sql-tools/diff/comparers/enum.comparer'; +import { compareEnums } from 'src/sql-tools/comparers/enum.comparer'; import { DatabaseEnum, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -10,7 +10,7 @@ describe('compareEnums', () => { expect(compareEnums.onExtra(testEnum)).toEqual([ { enumName: 'test', - type: 'enum.drop', + type: 'EnumDrop', reason: Reason.MissingInSource, }, ]); @@ -21,7 +21,7 @@ describe('compareEnums', () => { it('should work', () => { expect(compareEnums.onMissing(testEnum)).toEqual([ { - type: 'enum.create', + type: 'EnumCreate', enum: testEnum, reason: Reason.MissingInTarget, }, @@ -40,11 +40,11 @@ describe('compareEnums', () => { expect(compareEnums.onCompare(source, target)).toEqual([ { enumName: 'test', - type: 'enum.drop', + type: 'EnumDrop', reason: 'enum values has changed (foo,bar vs foo,bar,world)', }, { - type: 'enum.create', + type: 'EnumCreate', enum: source, reason: 'enum values has changed (foo,bar vs foo,bar,world)', }, diff --git a/server/src/sql-tools/diff/comparers/enum.comparer.ts b/server/src/sql-tools/comparers/enum.comparer.ts similarity index 87% rename from server/src/sql-tools/diff/comparers/enum.comparer.ts rename to server/src/sql-tools/comparers/enum.comparer.ts index 408f01050b..d81f9ed3c0 100644 --- a/server/src/sql-tools/diff/comparers/enum.comparer.ts +++ b/server/src/sql-tools/comparers/enum.comparer.ts @@ -3,14 +3,14 @@ import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types'; export const compareEnums: Comparer = { onMissing: (source) => [ { - type: 'enum.create', + type: 'EnumCreate', enum: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'enum.drop', + type: 'EnumDrop', enumName: target.name, reason: Reason.MissingInSource, }, @@ -21,12 +21,12 @@ export const compareEnums: Comparer = { const reason = `enum values has changed (${source.values} vs ${target.values})`; return [ { - type: 'enum.drop', + type: 'EnumDrop', enumName: source.name, reason, }, { - type: 'enum.create', + type: 'EnumCreate', enum: source, reason, }, diff --git a/server/src/sql-tools/diff/comparers/extension.comparer.spec.ts b/server/src/sql-tools/comparers/extension.comparer.spec.ts similarity index 84% rename from server/src/sql-tools/diff/comparers/extension.comparer.spec.ts rename to server/src/sql-tools/comparers/extension.comparer.spec.ts index 753c461c69..38e553719d 100644 --- a/server/src/sql-tools/diff/comparers/extension.comparer.spec.ts +++ b/server/src/sql-tools/comparers/extension.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareExtensions } from 'src/sql-tools/diff/comparers/extension.comparer'; +import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer'; import { Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -10,7 +10,7 @@ describe('compareExtensions', () => { expect(compareExtensions.onExtra(testExtension)).toEqual([ { extensionName: 'test', - type: 'extension.drop', + type: 'ExtensionDrop', reason: Reason.MissingInSource, }, ]); @@ -21,7 +21,7 @@ describe('compareExtensions', () => { it('should work', () => { expect(compareExtensions.onMissing(testExtension)).toEqual([ { - type: 'extension.create', + type: 'ExtensionCreate', extension: testExtension, reason: Reason.MissingInTarget, }, diff --git a/server/src/sql-tools/diff/comparers/extension.comparer.ts b/server/src/sql-tools/comparers/extension.comparer.ts similarity index 87% rename from server/src/sql-tools/diff/comparers/extension.comparer.ts rename to server/src/sql-tools/comparers/extension.comparer.ts index 1c9d19165a..441b00e3e3 100644 --- a/server/src/sql-tools/diff/comparers/extension.comparer.ts +++ b/server/src/sql-tools/comparers/extension.comparer.ts @@ -3,14 +3,14 @@ import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types'; export const compareExtensions: Comparer = { onMissing: (source) => [ { - type: 'extension.create', + type: 'ExtensionCreate', extension: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'extension.drop', + type: 'ExtensionDrop', extensionName: target.name, reason: Reason.MissingInSource, }, diff --git a/server/src/sql-tools/diff/comparers/function.comparer.spec.ts b/server/src/sql-tools/comparers/function.comparer.spec.ts similarity index 88% rename from server/src/sql-tools/diff/comparers/function.comparer.spec.ts rename to server/src/sql-tools/comparers/function.comparer.spec.ts index ac478ed000..964768cf98 100644 --- a/server/src/sql-tools/diff/comparers/function.comparer.spec.ts +++ b/server/src/sql-tools/comparers/function.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareFunctions } from 'src/sql-tools/diff/comparers/function.comparer'; +import { compareFunctions } from 'src/sql-tools/comparers/function.comparer'; import { DatabaseFunction, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -14,7 +14,7 @@ describe('compareFunctions', () => { expect(compareFunctions.onExtra(testFunction)).toEqual([ { functionName: 'test', - type: 'function.drop', + type: 'FunctionDrop', reason: Reason.MissingInSource, }, ]); @@ -25,7 +25,7 @@ describe('compareFunctions', () => { it('should work', () => { expect(compareFunctions.onMissing(testFunction)).toEqual([ { - type: 'function.create', + type: 'FunctionCreate', function: testFunction, reason: Reason.MissingInTarget, }, @@ -43,7 +43,7 @@ describe('compareFunctions', () => { const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' }; expect(compareFunctions.onCompare(source, target)).toEqual([ { - type: 'function.create', + type: 'FunctionCreate', reason: 'function expression has changed (SELECT 1 vs SELECT 2)', function: source, }, diff --git a/server/src/sql-tools/diff/comparers/function.comparer.ts b/server/src/sql-tools/comparers/function.comparer.ts similarity index 87% rename from server/src/sql-tools/diff/comparers/function.comparer.ts rename to server/src/sql-tools/comparers/function.comparer.ts index d10353b89c..000cf07058 100644 --- a/server/src/sql-tools/diff/comparers/function.comparer.ts +++ b/server/src/sql-tools/comparers/function.comparer.ts @@ -3,14 +3,14 @@ import { Comparer, DatabaseFunction, Reason } from 'src/sql-tools/types'; export const compareFunctions: Comparer = { onMissing: (source) => [ { - type: 'function.create', + type: 'FunctionCreate', function: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'function.drop', + type: 'FunctionDrop', functionName: target.name, reason: Reason.MissingInSource, }, @@ -20,7 +20,7 @@ export const compareFunctions: Comparer = { const reason = `function expression has changed (${source.expression} vs ${target.expression})`; return [ { - type: 'function.create', + type: 'FunctionCreate', function: source, reason, }, diff --git a/server/src/sql-tools/diff/comparers/index.comparer.spec.ts b/server/src/sql-tools/comparers/index.comparer.spec.ts similarity index 89% rename from server/src/sql-tools/diff/comparers/index.comparer.spec.ts rename to server/src/sql-tools/comparers/index.comparer.spec.ts index 806bab190c..b00be386e0 100644 --- a/server/src/sql-tools/diff/comparers/index.comparer.spec.ts +++ b/server/src/sql-tools/comparers/index.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareIndexes } from 'src/sql-tools/diff/comparers/index.comparer'; +import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; import { DatabaseIndex, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -15,7 +15,7 @@ describe('compareIndexes', () => { it('should work', () => { expect(compareIndexes.onExtra(testIndex)).toEqual([ { - type: 'index.drop', + type: 'IndexDrop', indexName: 'test', reason: Reason.MissingInSource, }, @@ -27,7 +27,7 @@ describe('compareIndexes', () => { it('should work', () => { expect(compareIndexes.onMissing(testIndex)).toEqual([ { - type: 'index.create', + type: 'IndexCreate', index: testIndex, reason: Reason.MissingInTarget, }, @@ -58,11 +58,11 @@ describe('compareIndexes', () => { expect(compareIndexes.onCompare(source, target)).toEqual([ { indexName: 'test', - type: 'index.drop', + type: 'IndexDrop', reason: 'columns are different (column1 vs column1,column2)', }, { - type: 'index.create', + type: 'IndexCreate', index: source, reason: 'columns are different (column1 vs column1,column2)', }, diff --git a/server/src/sql-tools/diff/comparers/index.comparer.ts b/server/src/sql-tools/comparers/index.comparer.ts similarity index 88% rename from server/src/sql-tools/diff/comparers/index.comparer.ts rename to server/src/sql-tools/comparers/index.comparer.ts index ef07e3a17b..99571cf61a 100644 --- a/server/src/sql-tools/diff/comparers/index.comparer.ts +++ b/server/src/sql-tools/comparers/index.comparer.ts @@ -4,14 +4,14 @@ import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types'; export const compareIndexes: Comparer = { onMissing: (source) => [ { - type: 'index.create', + type: 'IndexCreate', index: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'index.drop', + type: 'IndexDrop', indexName: target.name, reason: Reason.MissingInSource, }, @@ -36,8 +36,8 @@ export const compareIndexes: Comparer = { if (reason) { return [ - { type: 'index.drop', indexName: target.name, reason }, - { type: 'index.create', index: source, reason }, + { type: 'IndexDrop', indexName: target.name, reason }, + { type: 'IndexCreate', index: source, reason }, ]; } diff --git a/server/src/sql-tools/diff/comparers/parameter.comparer.spec.ts b/server/src/sql-tools/comparers/parameter.comparer.spec.ts similarity index 86% rename from server/src/sql-tools/diff/comparers/parameter.comparer.spec.ts rename to server/src/sql-tools/comparers/parameter.comparer.spec.ts index 517ec79341..cd1520faff 100644 --- a/server/src/sql-tools/diff/comparers/parameter.comparer.spec.ts +++ b/server/src/sql-tools/comparers/parameter.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareParameters } from 'src/sql-tools/diff/comparers/parameter.comparer'; +import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer'; import { DatabaseParameter, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -15,7 +15,7 @@ describe('compareParameters', () => { it('should work', () => { expect(compareParameters.onExtra(testParameter)).toEqual([ { - type: 'parameter.reset', + type: 'ParameterReset', databaseName: 'immich', parameterName: 'test', reason: Reason.MissingInSource, @@ -28,7 +28,7 @@ describe('compareParameters', () => { it('should work', () => { expect(compareParameters.onMissing(testParameter)).toEqual([ { - type: 'parameter.set', + type: 'ParameterSet', parameter: testParameter, reason: Reason.MissingInTarget, }, diff --git a/server/src/sql-tools/diff/comparers/parameter.comparer.ts b/server/src/sql-tools/comparers/parameter.comparer.ts similarity index 88% rename from server/src/sql-tools/diff/comparers/parameter.comparer.ts rename to server/src/sql-tools/comparers/parameter.comparer.ts index 03c24bada7..d1a33ad090 100644 --- a/server/src/sql-tools/diff/comparers/parameter.comparer.ts +++ b/server/src/sql-tools/comparers/parameter.comparer.ts @@ -3,14 +3,14 @@ import { Comparer, DatabaseParameter, Reason } from 'src/sql-tools/types'; export const compareParameters: Comparer = { onMissing: (source) => [ { - type: 'parameter.set', + type: 'ParameterSet', parameter: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'parameter.reset', + type: 'ParameterReset', databaseName: target.databaseName, parameterName: target.name, reason: Reason.MissingInSource, diff --git a/server/src/sql-tools/diff/comparers/table.comparer.spec.ts b/server/src/sql-tools/comparers/table.comparer.spec.ts similarity index 86% rename from server/src/sql-tools/diff/comparers/table.comparer.spec.ts rename to server/src/sql-tools/comparers/table.comparer.spec.ts index 0b1873b2ba..575e25ab44 100644 --- a/server/src/sql-tools/diff/comparers/table.comparer.spec.ts +++ b/server/src/sql-tools/comparers/table.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareTables } from 'src/sql-tools/diff/comparers/table.comparer'; +import { compareTables } from 'src/sql-tools/comparers/table.comparer'; import { DatabaseTable, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -16,7 +16,7 @@ describe('compareParameters', () => { it('should work', () => { expect(compareTables.onExtra(testTable)).toEqual([ { - type: 'table.drop', + type: 'TableDrop', tableName: 'test', reason: Reason.MissingInSource, }, @@ -28,7 +28,7 @@ describe('compareParameters', () => { it('should work', () => { expect(compareTables.onMissing(testTable)).toEqual([ { - type: 'table.create', + type: 'TableCreate', table: testTable, reason: Reason.MissingInTarget, }, diff --git a/server/src/sql-tools/diff/comparers/table.comparer.ts b/server/src/sql-tools/comparers/table.comparer.ts similarity index 54% rename from server/src/sql-tools/diff/comparers/table.comparer.ts rename to server/src/sql-tools/comparers/table.comparer.ts index 8f6d0e04f8..c920a1d07a 100644 --- a/server/src/sql-tools/diff/comparers/table.comparer.ts +++ b/server/src/sql-tools/comparers/table.comparer.ts @@ -1,47 +1,33 @@ -import { compareColumns } from 'src/sql-tools/diff/comparers/column.comparer'; -import { compareConstraints } from 'src/sql-tools/diff/comparers/constraint.comparer'; -import { compareIndexes } from 'src/sql-tools/diff/comparers/index.comparer'; -import { compareTriggers } from 'src/sql-tools/diff/comparers/trigger.comparer'; +import { compareColumns } from 'src/sql-tools/comparers/column.comparer'; +import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer'; +import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; +import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; import { compare } from 'src/sql-tools/helpers'; import { Comparer, DatabaseTable, Reason, SchemaDiff } from 'src/sql-tools/types'; +const newTable = (name: string) => ({ + name, + columns: [], + indexes: [], + constraints: [], + triggers: [], + synchronize: true, +}); + export const compareTables: Comparer = { onMissing: (source) => [ { - type: 'table.create', + type: 'TableCreate', table: source, reason: Reason.MissingInTarget, }, // TODO merge constraints into table create record when possible - ...compareTable( - source, - { - name: source.name, - columns: [], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - - { columns: false }, - ), + ...compareTable(source, newTable(source.name), { columns: false }), ], onExtra: (target) => [ - ...compareTable( - { - name: target.name, - columns: [], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - target, - { columns: false }, - ), + ...compareTable(newTable(target.name), target, { columns: false }), { - type: 'table.drop', + type: 'TableDrop', tableName: target.name, reason: Reason.MissingInSource, }, diff --git a/server/src/sql-tools/diff/comparers/trigger.comparer.spec.ts b/server/src/sql-tools/comparers/trigger.comparer.spec.ts similarity index 87% rename from server/src/sql-tools/diff/comparers/trigger.comparer.spec.ts rename to server/src/sql-tools/comparers/trigger.comparer.spec.ts index 800cb4d66b..731fae8da2 100644 --- a/server/src/sql-tools/diff/comparers/trigger.comparer.spec.ts +++ b/server/src/sql-tools/comparers/trigger.comparer.spec.ts @@ -1,4 +1,4 @@ -import { compareTriggers } from 'src/sql-tools/diff/comparers/trigger.comparer'; +import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; import { DatabaseTrigger, Reason } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; @@ -17,7 +17,7 @@ describe('compareTriggers', () => { it('should work', () => { expect(compareTriggers.onExtra(testTrigger)).toEqual([ { - type: 'trigger.drop', + type: 'TriggerDrop', tableName: 'table1', triggerName: 'test', reason: Reason.MissingInSource, @@ -30,7 +30,7 @@ describe('compareTriggers', () => { it('should work', () => { expect(compareTriggers.onMissing(testTrigger)).toEqual([ { - type: 'trigger.create', + type: 'TriggerCreate', trigger: testTrigger, reason: Reason.MissingInTarget, }, @@ -47,42 +47,42 @@ describe('compareTriggers', () => { const source: DatabaseTrigger = { ...testTrigger, functionName: 'my_new_name' }; const target: DatabaseTrigger = { ...testTrigger, functionName: 'my_old_name' }; const reason = `function is different (my_new_name vs my_old_name)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]); + expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in actions', () => { const source: DatabaseTrigger = { ...testTrigger, actions: ['delete'] }; const target: DatabaseTrigger = { ...testTrigger, actions: ['delete', 'insert'] }; const reason = `action is different (delete vs delete,insert)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]); + expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in timing', () => { const source: DatabaseTrigger = { ...testTrigger, timing: 'before' }; const target: DatabaseTrigger = { ...testTrigger, timing: 'after' }; const reason = `timing method is different (before vs after)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]); + expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in scope', () => { const source: DatabaseTrigger = { ...testTrigger, scope: 'row' }; const target: DatabaseTrigger = { ...testTrigger, scope: 'statement' }; const reason = `scope is different (row vs statement)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]); + expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in new table reference', () => { const source: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: 'new_table' }; const target: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: undefined }; const reason = `new table reference is different (new_table vs undefined)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]); + expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in old table reference', () => { const source: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: 'old_table' }; const target: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: undefined }; const reason = `old table reference is different (old_table vs undefined)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]); + expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); }); }); diff --git a/server/src/sql-tools/diff/comparers/trigger.comparer.ts b/server/src/sql-tools/comparers/trigger.comparer.ts similarity index 92% rename from server/src/sql-tools/diff/comparers/trigger.comparer.ts rename to server/src/sql-tools/comparers/trigger.comparer.ts index 38adae9905..da1de6e48b 100644 --- a/server/src/sql-tools/diff/comparers/trigger.comparer.ts +++ b/server/src/sql-tools/comparers/trigger.comparer.ts @@ -3,14 +3,14 @@ import { Comparer, DatabaseTrigger, Reason } from 'src/sql-tools/types'; export const compareTriggers: Comparer = { onMissing: (source) => [ { - type: 'trigger.create', + type: 'TriggerCreate', trigger: source, reason: Reason.MissingInTarget, }, ], onExtra: (target) => [ { - type: 'trigger.drop', + type: 'TriggerDrop', tableName: target.tableName, triggerName: target.name, reason: Reason.MissingInSource, @@ -33,7 +33,7 @@ export const compareTriggers: Comparer = { } if (reason) { - return [{ type: 'trigger.create', trigger: source, reason }]; + return [{ type: 'TriggerCreate', trigger: source, reason }]; } return []; diff --git a/server/src/sql-tools/from-code/decorators/after-delete.decorator.ts b/server/src/sql-tools/decorators/after-delete.decorator.ts similarity index 81% rename from server/src/sql-tools/from-code/decorators/after-delete.decorator.ts rename to server/src/sql-tools/decorators/after-delete.decorator.ts index 7713c4b625..181bfab6c8 100644 --- a/server/src/sql-tools/from-code/decorators/after-delete.decorator.ts +++ b/server/src/sql-tools/decorators/after-delete.decorator.ts @@ -1,4 +1,4 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator'; +import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; export const AfterDeleteTrigger = (options: Omit) => TriggerFunction({ diff --git a/server/src/sql-tools/from-code/decorators/after-insert.decorator.ts b/server/src/sql-tools/decorators/after-insert.decorator.ts similarity index 81% rename from server/src/sql-tools/from-code/decorators/after-insert.decorator.ts rename to server/src/sql-tools/decorators/after-insert.decorator.ts index 103d59b4fc..c302a5cebe 100644 --- a/server/src/sql-tools/from-code/decorators/after-insert.decorator.ts +++ b/server/src/sql-tools/decorators/after-insert.decorator.ts @@ -1,4 +1,4 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator'; +import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; export const AfterInsertTrigger = (options: Omit) => TriggerFunction({ diff --git a/server/src/sql-tools/from-code/decorators/before-update.decorator.ts b/server/src/sql-tools/decorators/before-update.decorator.ts similarity index 81% rename from server/src/sql-tools/from-code/decorators/before-update.decorator.ts rename to server/src/sql-tools/decorators/before-update.decorator.ts index 03dad25ed0..2119e29c9b 100644 --- a/server/src/sql-tools/from-code/decorators/before-update.decorator.ts +++ b/server/src/sql-tools/decorators/before-update.decorator.ts @@ -1,4 +1,4 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator'; +import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; export const BeforeUpdateTrigger = (options: Omit) => TriggerFunction({ diff --git a/server/src/sql-tools/from-code/decorators/check.decorator.ts b/server/src/sql-tools/decorators/check.decorator.ts similarity index 84% rename from server/src/sql-tools/from-code/decorators/check.decorator.ts rename to server/src/sql-tools/decorators/check.decorator.ts index 7d046df0c3..56fe1ecc3f 100644 --- a/server/src/sql-tools/from-code/decorators/check.decorator.ts +++ b/server/src/sql-tools/decorators/check.decorator.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; export type CheckOptions = { name?: string; diff --git a/server/src/sql-tools/from-code/decorators/column.decorator.ts b/server/src/sql-tools/decorators/column.decorator.ts similarity index 93% rename from server/src/sql-tools/from-code/decorators/column.decorator.ts rename to server/src/sql-tools/decorators/column.decorator.ts index 7b00af80cc..adb3d0ed59 100644 --- a/server/src/sql-tools/from-code/decorators/column.decorator.ts +++ b/server/src/sql-tools/decorators/column.decorator.ts @@ -1,5 +1,5 @@ -import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; +import { register } from 'src/sql-tools/register'; import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types'; export type ColumnValue = null | boolean | string | number | object | Date | (() => string); diff --git a/server/src/sql-tools/from-code/decorators/configuration-parameter.decorator.ts b/server/src/sql-tools/decorators/configuration-parameter.decorator.ts similarity index 76% rename from server/src/sql-tools/from-code/decorators/configuration-parameter.decorator.ts rename to server/src/sql-tools/decorators/configuration-parameter.decorator.ts index 6a987884d1..953027d25c 100644 --- a/server/src/sql-tools/from-code/decorators/configuration-parameter.decorator.ts +++ b/server/src/sql-tools/decorators/configuration-parameter.decorator.ts @@ -1,5 +1,5 @@ -import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { register } from 'src/sql-tools/from-code/register'; +import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; +import { register } from 'src/sql-tools/register'; import { ParameterScope } from 'src/sql-tools/types'; export type ConfigurationParameterOptions = { diff --git a/server/src/sql-tools/from-code/decorators/create-date-column.decorator.ts b/server/src/sql-tools/decorators/create-date-column.decorator.ts similarity index 67% rename from server/src/sql-tools/from-code/decorators/create-date-column.decorator.ts rename to server/src/sql-tools/decorators/create-date-column.decorator.ts index 8f81d59914..1a3362a614 100644 --- a/server/src/sql-tools/from-code/decorators/create-date-column.decorator.ts +++ b/server/src/sql-tools/decorators/create-date-column.decorator.ts @@ -1,4 +1,4 @@ -import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; +import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { return Column({ diff --git a/server/src/sql-tools/from-code/decorators/database.decorator.ts b/server/src/sql-tools/decorators/database.decorator.ts similarity index 84% rename from server/src/sql-tools/from-code/decorators/database.decorator.ts rename to server/src/sql-tools/decorators/database.decorator.ts index 3bcc464f74..17b2460df6 100644 --- a/server/src/sql-tools/from-code/decorators/database.decorator.ts +++ b/server/src/sql-tools/decorators/database.decorator.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; export type DatabaseOptions = { name?: string; diff --git a/server/src/sql-tools/from-code/decorators/delete-date-column.decorator.ts b/server/src/sql-tools/decorators/delete-date-column.decorator.ts similarity index 66% rename from server/src/sql-tools/from-code/decorators/delete-date-column.decorator.ts rename to server/src/sql-tools/decorators/delete-date-column.decorator.ts index 518c4e76fc..ca5427c27f 100644 --- a/server/src/sql-tools/from-code/decorators/delete-date-column.decorator.ts +++ b/server/src/sql-tools/decorators/delete-date-column.decorator.ts @@ -1,4 +1,4 @@ -import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; +import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { return Column({ diff --git a/server/src/sql-tools/from-code/decorators/extension.decorator.ts b/server/src/sql-tools/decorators/extension.decorator.ts similarity index 86% rename from server/src/sql-tools/from-code/decorators/extension.decorator.ts rename to server/src/sql-tools/decorators/extension.decorator.ts index c43a18c16f..d431cbfd02 100644 --- a/server/src/sql-tools/from-code/decorators/extension.decorator.ts +++ b/server/src/sql-tools/decorators/extension.decorator.ts @@ -1,5 +1,5 @@ -import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; +import { register } from 'src/sql-tools/register'; export type ExtensionOptions = { name: string; diff --git a/server/src/sql-tools/from-code/decorators/extensions.decorator.ts b/server/src/sql-tools/decorators/extensions.decorator.ts similarity index 88% rename from server/src/sql-tools/from-code/decorators/extensions.decorator.ts rename to server/src/sql-tools/decorators/extensions.decorator.ts index 9d3769a210..724446c5fa 100644 --- a/server/src/sql-tools/from-code/decorators/extensions.decorator.ts +++ b/server/src/sql-tools/decorators/extensions.decorator.ts @@ -1,5 +1,5 @@ -import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; +import { register } from 'src/sql-tools/register'; export type ExtensionsOptions = { name: string; diff --git a/server/src/sql-tools/decorators/foreign-key-column.decorator.ts b/server/src/sql-tools/decorators/foreign-key-column.decorator.ts new file mode 100644 index 0000000000..c9c83f010d --- /dev/null +++ b/server/src/sql-tools/decorators/foreign-key-column.decorator.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { ForeignKeyAction } from 'src/sql-tools//decorators/foreign-key-constraint.decorator'; +import { ColumnBaseOptions } from 'src/sql-tools/decorators/column.decorator'; +import { register } from 'src/sql-tools/register'; + +export type ForeignKeyColumnOptions = ColumnBaseOptions & { + onUpdate?: ForeignKeyAction; + onDelete?: ForeignKeyAction; + constraintName?: string; +}; + +export const ForeignKeyColumn = (target: () => Function, options: ForeignKeyColumnOptions): PropertyDecorator => { + return (object: object, propertyName: string | symbol) => { + register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } }); + }; +}; diff --git a/server/src/sql-tools/from-code/decorators/foreign-key-constraint.decorator.ts b/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts similarity index 92% rename from server/src/sql-tools/from-code/decorators/foreign-key-constraint.decorator.ts rename to server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts index 7d18a9fda0..e5d2f513dc 100644 --- a/server/src/sql-tools/from-code/decorators/foreign-key-constraint.decorator.ts +++ b/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION'; diff --git a/server/src/sql-tools/from-code/decorators/generated-column.decorator.ts b/server/src/sql-tools/decorators/generated-column.decorator.ts similarity index 95% rename from server/src/sql-tools/from-code/decorators/generated-column.decorator.ts rename to server/src/sql-tools/decorators/generated-column.decorator.ts index 82d3131b5c..4338b4146c 100644 --- a/server/src/sql-tools/from-code/decorators/generated-column.decorator.ts +++ b/server/src/sql-tools/decorators/generated-column.decorator.ts @@ -1,4 +1,4 @@ -import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator'; +import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/decorators/column.decorator'; import { ColumnType } from 'src/sql-tools/types'; export type GeneratedColumnStrategy = 'uuid' | 'identity'; diff --git a/server/src/sql-tools/from-code/decorators/index.decorator.ts b/server/src/sql-tools/decorators/index.decorator.ts similarity index 89% rename from server/src/sql-tools/from-code/decorators/index.decorator.ts rename to server/src/sql-tools/decorators/index.decorator.ts index 5d90c4f58d..1b6d38e390 100644 --- a/server/src/sql-tools/from-code/decorators/index.decorator.ts +++ b/server/src/sql-tools/decorators/index.decorator.ts @@ -1,5 +1,5 @@ -import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; +import { register } from 'src/sql-tools/register'; export type IndexOptions = { name?: string; diff --git a/server/src/sql-tools/decorators/index.ts b/server/src/sql-tools/decorators/index.ts new file mode 100644 index 0000000000..86affe5002 --- /dev/null +++ b/server/src/sql-tools/decorators/index.ts @@ -0,0 +1,22 @@ +export * from 'src/sql-tools/decorators/after-delete.decorator'; +export * from 'src/sql-tools/decorators/after-insert.decorator'; +export * from 'src/sql-tools/decorators/before-update.decorator'; +export * from 'src/sql-tools/decorators/check.decorator'; +export * from 'src/sql-tools/decorators/column.decorator'; +export * from 'src/sql-tools/decorators/configuration-parameter.decorator'; +export * from 'src/sql-tools/decorators/create-date-column.decorator'; +export * from 'src/sql-tools/decorators/database.decorator'; +export * from 'src/sql-tools/decorators/delete-date-column.decorator'; +export * from 'src/sql-tools/decorators/extension.decorator'; +export * from 'src/sql-tools/decorators/extensions.decorator'; +export * from 'src/sql-tools/decorators/foreign-key-column.decorator'; +export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; +export * from 'src/sql-tools/decorators/generated-column.decorator'; +export * from 'src/sql-tools/decorators/index.decorator'; +export * from 'src/sql-tools/decorators/primary-column.decorator'; +export * from 'src/sql-tools/decorators/primary-generated-column.decorator'; +export * from 'src/sql-tools/decorators/table.decorator'; +export * from 'src/sql-tools/decorators/trigger-function.decorator'; +export * from 'src/sql-tools/decorators/trigger.decorator'; +export * from 'src/sql-tools/decorators/unique.decorator'; +export * from 'src/sql-tools/decorators/update-date-column.decorator'; diff --git a/server/src/sql-tools/from-code/decorators/primary-column.decorator.ts b/server/src/sql-tools/decorators/primary-column.decorator.ts similarity index 56% rename from server/src/sql-tools/from-code/decorators/primary-column.decorator.ts rename to server/src/sql-tools/decorators/primary-column.decorator.ts index f702965675..e605b4be5d 100644 --- a/server/src/sql-tools/from-code/decorators/primary-column.decorator.ts +++ b/server/src/sql-tools/decorators/primary-column.decorator.ts @@ -1,3 +1,3 @@ -import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; +import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; export const PrimaryColumn = (options: Omit = {}) => Column({ ...options, primary: true }); diff --git a/server/src/sql-tools/from-code/decorators/primary-generated-column.decorator.ts b/server/src/sql-tools/decorators/primary-generated-column.decorator.ts similarity index 79% rename from server/src/sql-tools/from-code/decorators/primary-generated-column.decorator.ts rename to server/src/sql-tools/decorators/primary-generated-column.decorator.ts index 9dc8ca6817..25e125ebf6 100644 --- a/server/src/sql-tools/from-code/decorators/primary-generated-column.decorator.ts +++ b/server/src/sql-tools/decorators/primary-generated-column.decorator.ts @@ -1,4 +1,4 @@ -import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/from-code/decorators/generated-column.decorator'; +import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/decorators/generated-column.decorator'; export const PrimaryGeneratedColumn = (options: Omit = {}) => GeneratedColumn({ ...options, primary: true }); diff --git a/server/src/sql-tools/from-code/decorators/table.decorator.ts b/server/src/sql-tools/decorators/table.decorator.ts similarity index 88% rename from server/src/sql-tools/from-code/decorators/table.decorator.ts rename to server/src/sql-tools/decorators/table.decorator.ts index 589a88aa29..7ea5882147 100644 --- a/server/src/sql-tools/from-code/decorators/table.decorator.ts +++ b/server/src/sql-tools/decorators/table.decorator.ts @@ -1,5 +1,5 @@ -import { register } from 'src/sql-tools/from-code/register'; import { asOptions } from 'src/sql-tools/helpers'; +import { register } from 'src/sql-tools/register'; export type TableOptions = { name?: string; diff --git a/server/src/sql-tools/from-code/decorators/trigger-function.decorator.ts b/server/src/sql-tools/decorators/trigger-function.decorator.ts similarity index 78% rename from server/src/sql-tools/from-code/decorators/trigger-function.decorator.ts rename to server/src/sql-tools/decorators/trigger-function.decorator.ts index cb2fa36800..17016f7946 100644 --- a/server/src/sql-tools/from-code/decorators/trigger-function.decorator.ts +++ b/server/src/sql-tools/decorators/trigger-function.decorator.ts @@ -1,4 +1,4 @@ -import { Trigger, TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; +import { Trigger, TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; import { DatabaseFunction } from 'src/sql-tools/types'; export type TriggerFunctionOptions = Omit & { function: DatabaseFunction }; diff --git a/server/src/sql-tools/from-code/decorators/trigger.decorator.ts b/server/src/sql-tools/decorators/trigger.decorator.ts similarity index 90% rename from server/src/sql-tools/from-code/decorators/trigger.decorator.ts rename to server/src/sql-tools/decorators/trigger.decorator.ts index e0c0ccf3e4..ce9a5c17f7 100644 --- a/server/src/sql-tools/from-code/decorators/trigger.decorator.ts +++ b/server/src/sql-tools/decorators/trigger.decorator.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; export type TriggerOptions = { diff --git a/server/src/sql-tools/from-code/decorators/unique.decorator.ts b/server/src/sql-tools/decorators/unique.decorator.ts similarity index 84% rename from server/src/sql-tools/from-code/decorators/unique.decorator.ts rename to server/src/sql-tools/decorators/unique.decorator.ts index c7186d7296..1f61fccb6f 100644 --- a/server/src/sql-tools/from-code/decorators/unique.decorator.ts +++ b/server/src/sql-tools/decorators/unique.decorator.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; export type UniqueOptions = { name?: string; diff --git a/server/src/sql-tools/from-code/decorators/update-date-column.decorator.ts b/server/src/sql-tools/decorators/update-date-column.decorator.ts similarity index 67% rename from server/src/sql-tools/from-code/decorators/update-date-column.decorator.ts rename to server/src/sql-tools/decorators/update-date-column.decorator.ts index ddc7a6a1e8..68dd50c617 100644 --- a/server/src/sql-tools/from-code/decorators/update-date-column.decorator.ts +++ b/server/src/sql-tools/decorators/update-date-column.decorator.ts @@ -1,4 +1,4 @@ -import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; +import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { return Column({ diff --git a/server/src/sql-tools/diff/index.ts b/server/src/sql-tools/diff/index.ts deleted file mode 100644 index dd90293dc3..0000000000 --- a/server/src/sql-tools/diff/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { compareEnums } from 'src/sql-tools/diff/comparers/enum.comparer'; -import { compareExtensions } from 'src/sql-tools/diff/comparers/extension.comparer'; -import { compareFunctions } from 'src/sql-tools/diff/comparers/function.comparer'; -import { compareParameters } from 'src/sql-tools/diff/comparers/parameter.comparer'; -import { compareTables } from 'src/sql-tools/diff/comparers/table.comparer'; -import { compare } from 'src/sql-tools/helpers'; -import { schemaDiffToSql } from 'src/sql-tools/to-sql'; -import { - DatabaseConstraintType, - DatabaseSchema, - SchemaDiff, - SchemaDiffOptions, - SchemaDiffToSqlOptions, -} from 'src/sql-tools/types'; - -/** - * Compute the difference between two database schemas - */ -export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => { - const items = [ - ...compare(source.parameters, target.parameters, options.parameters, compareParameters), - ...compare(source.extensions, target.extensions, options.extension, compareExtensions), - ...compare(source.functions, target.functions, options.functions, compareFunctions), - ...compare(source.enums, target.enums, options.enums, compareEnums), - ...compare(source.tables, target.tables, options.tables, compareTables), - ]; - - type SchemaName = SchemaDiff['type']; - const itemMap: Record = { - 'enum.create': [], - 'enum.drop': [], - 'extension.create': [], - 'extension.drop': [], - 'function.create': [], - 'function.drop': [], - 'table.create': [], - 'table.drop': [], - 'column.add': [], - 'column.alter': [], - 'column.drop': [], - 'constraint.add': [], - 'constraint.drop': [], - 'index.create': [], - 'index.drop': [], - 'trigger.create': [], - 'trigger.drop': [], - 'parameter.set': [], - 'parameter.reset': [], - }; - - for (const item of items) { - itemMap[item.type].push(item); - } - - const constraintAdds = itemMap['constraint.add'].filter((item) => item.type === 'constraint.add'); - - const orderedItems = [ - ...itemMap['extension.create'], - ...itemMap['function.create'], - ...itemMap['parameter.set'], - ...itemMap['parameter.reset'], - ...itemMap['enum.create'], - ...itemMap['trigger.drop'], - ...itemMap['index.drop'], - ...itemMap['constraint.drop'], - ...itemMap['table.create'], - ...itemMap['column.alter'], - ...itemMap['column.add'], - ...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.PRIMARY_KEY), - ...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.FOREIGN_KEY), - ...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.UNIQUE), - ...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.CHECK), - ...itemMap['index.create'], - ...itemMap['trigger.create'], - ...itemMap['column.drop'], - ...itemMap['table.drop'], - ...itemMap['enum.drop'], - ...itemMap['function.drop'], - ]; - - return { - items: orderedItems, - asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options), - }; -}; diff --git a/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts b/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts deleted file mode 100644 index d2b7d623a7..0000000000 --- a/server/src/sql-tools/from-code/decorators/foreign-key-column.decorator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { ForeignKeyAction } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator'; -import { register } from 'src/sql-tools/from-code/register'; - -export type ForeignKeyColumnOptions = ColumnBaseOptions & { - onUpdate?: ForeignKeyAction; - onDelete?: ForeignKeyAction; - constraintName?: string; -}; - -export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => { - register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } }); - }; -}; diff --git a/server/src/sql-tools/from-code/index.spec.ts b/server/src/sql-tools/from-code/index.spec.ts deleted file mode 100644 index 5306722c76..0000000000 --- a/server/src/sql-tools/from-code/index.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; -import { reset, schemaFromCode } from 'src/sql-tools/from-code'; -import { describe, expect, it } from 'vitest'; - -describe(schemaFromCode.name, () => { - beforeEach(() => { - reset(); - }); - - it('should work', () => { - expect(schemaFromCode()).toEqual({ - name: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - tables: [], - warnings: [], - }); - }); - - describe('test files', () => { - const files = readdirSync('test/sql-tools', { withFileTypes: true }); - for (const file of files) { - const filePath = join(file.parentPath, file.name); - it(filePath, async () => { - const module = await import(filePath); - expect(module.description).toBeDefined(); - expect(module.schema).toBeDefined(); - expect(schemaFromCode(), module.description).toEqual(module.schema); - }); - } - }); -}); diff --git a/server/src/sql-tools/from-code/index.ts b/server/src/sql-tools/from-code/index.ts deleted file mode 100644 index d820f236df..0000000000 --- a/server/src/sql-tools/from-code/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import 'reflect-metadata'; -import { processCheckConstraints } from 'src/sql-tools/from-code/processors/check-constraint.processor'; -import { processColumns } from 'src/sql-tools/from-code/processors/column.processor'; -import { processConfigurationParameters } from 'src/sql-tools/from-code/processors/configuration-parameter.processor'; -import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor'; -import { processEnums } from 'src/sql-tools/from-code/processors/enum.processor'; -import { processExtensions } from 'src/sql-tools/from-code/processors/extension.processor'; -import { processForeignKeyColumns } from 'src/sql-tools/from-code/processors/foreign-key-column.processor'; -import { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constraint.processor'; -import { processFunctions } from 'src/sql-tools/from-code/processors/function.processor'; -import { processIndexes } from 'src/sql-tools/from-code/processors/index.processor'; -import { processPrimaryKeyConstraints } from 'src/sql-tools/from-code/processors/primary-key-contraint.processor'; -import { processTables } from 'src/sql-tools/from-code/processors/table.processor'; -import { processTriggers } from 'src/sql-tools/from-code/processors/trigger.processor'; -import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type'; -import { processUniqueConstraints } from 'src/sql-tools/from-code/processors/unique-constraint.processor'; -import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/from-code/register'; -import { DatabaseSchema } from 'src/sql-tools/types'; - -let initialized = false; -let schema: DatabaseSchema; - -export const reset = () => { - initialized = false; - resetRegisteredItems(); -}; - -const processors: Processor[] = [ - processDatabases, - processConfigurationParameters, - processEnums, - processExtensions, - processFunctions, - processTables, - processColumns, - processForeignKeyColumns, - processForeignKeyConstraints, - processUniqueConstraints, - processCheckConstraints, - processPrimaryKeyConstraints, - processIndexes, - processTriggers, -]; - -export type SchemaFromCodeOptions = { - /** automatically create indexes on foreign key columns */ - createForeignKeyIndexes?: boolean; -}; -export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { - if (!initialized) { - const globalOptions = { - createForeignKeyIndexes: options.createForeignKeyIndexes ?? true, - }; - - const builder: SchemaBuilder = { - name: 'postgres', - schemaName: 'public', - tables: [], - functions: [], - enums: [], - extensions: [], - parameters: [], - warnings: [], - }; - - const items = getRegisteredItems(); - - for (const processor of processors) { - processor(builder, items, globalOptions); - } - - schema = { ...builder, tables: builder.tables.map(({ metadata: _, ...table }) => table) }; - initialized = true; - } - - return schema; -}; diff --git a/server/src/sql-tools/from-code/processors/column.processor.ts b/server/src/sql-tools/from-code/processors/column.processor.ts deleted file mode 100644 index 6fff3070e3..0000000000 --- a/server/src/sql-tools/from-code/processors/column.processor.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type'; -import { addWarning, asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers'; -import { DatabaseColumn } from 'src/sql-tools/types'; - -export const processColumns: Processor = (builder, items) => { - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const table = resolveTable(builder, object.constructor); - if (!table) { - onMissingTable(builder, type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName); - continue; - } - - const columnName = options.name ?? String(propertyName); - const existingColumn = table.columns.find((column) => column.name === columnName); - if (existingColumn) { - // TODO log warnings if column name is not unique - continue; - } - - const tableName = table.name; - - let defaultValue = fromColumnValue(options.default); - let nullable = options.nullable ?? false; - - // map `{ default: null }` to `{ nullable: true }` - if (defaultValue === null) { - nullable = true; - defaultValue = undefined; - } - - const isEnum = !!(options as ColumnOptions).enum; - - const column: DatabaseColumn = { - name: columnName, - tableName, - primary: options.primary ?? false, - default: defaultValue, - nullable, - isArray: (options as ColumnOptions).array ?? false, - length: options.length, - type: isEnum ? 'enum' : options.type || 'character varying', - enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined, - comment: options.comment, - storage: options.storage, - identity: options.identity, - synchronize: options.synchronize ?? true, - }; - - writeMetadata(object, propertyName, { name: column.name, options }); - - table.columns.push(column); - } -}; - -type ColumnMetadata = { name: string; options: ColumnOptions }; - -export const resolveColumn = (builder: SchemaBuilder, object: object, propertyName: string | symbol) => { - const table = resolveTable(builder, object.constructor); - if (!table) { - return {}; - } - - const metadata = readMetadata(object, propertyName); - if (!metadata) { - return { table }; - } - - const column = table.columns.find((column) => column.name === metadata.name); - return { table, column }; -}; - -export const onMissingColumn = ( - builder: SchemaBuilder, - context: string, - object: object, - propertyName?: symbol | string, -) => { - const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); - addWarning(builder, context, `Unable to find column (${label})`); -}; - -const METADATA_KEY = asMetadataKey('table-metadata'); - -const writeMetadata = (object: object, propertyName: symbol | string, metadata: ColumnMetadata) => - Reflect.defineMetadata(METADATA_KEY, metadata, object, propertyName); - -const readMetadata = (object: object, propertyName: symbol | string): ColumnMetadata | undefined => - Reflect.getMetadata(METADATA_KEY, object, propertyName); diff --git a/server/src/sql-tools/from-code/processors/table.processor.ts b/server/src/sql-tools/from-code/processors/table.processor.ts deleted file mode 100644 index e96f858266..0000000000 --- a/server/src/sql-tools/from-code/processors/table.processor.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator'; -import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type'; -import { addWarning, asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers'; - -export const processTables: Processor = (builder, items) => { - for (const { - item: { options, object }, - } of items.filter((item) => item.type === 'table')) { - const test = readMetadata(object); - if (test) { - throw new Error( - `Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`, - ); - } - - const tableName = options.name || asSnakeCase(object.name); - - writeMetadata(object, { name: tableName, options }); - - builder.tables.push({ - name: tableName, - columns: [], - constraints: [], - indexes: [], - triggers: [], - synchronize: options.synchronize ?? true, - metadata: { options, object }, - }); - } -}; - -export const resolveTable = (builder: SchemaBuilder, object: object) => { - const metadata = readMetadata(object); - if (!metadata) { - return; - } - - return builder.tables.find((table) => table.name === metadata.name); -}; - -export const onMissingTable = ( - builder: SchemaBuilder, - context: string, - object: object, - propertyName?: symbol | string, -) => { - const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); - addWarning(builder, context, `Unable to find table (${label})`); -}; - -const METADATA_KEY = asMetadataKey('table-metadata'); - -type TableMetadata = { name: string; options: TableOptions }; - -const readMetadata = (object: object): TableMetadata | undefined => Reflect.getMetadata(METADATA_KEY, object); - -const writeMetadata = (object: object, metadata: TableMetadata): void => - Reflect.defineMetadata(METADATA_KEY, metadata, object); diff --git a/server/src/sql-tools/from-code/processors/type.ts b/server/src/sql-tools/from-code/processors/type.ts deleted file mode 100644 index deb142d278..0000000000 --- a/server/src/sql-tools/from-code/processors/type.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { SchemaFromCodeOptions } from 'src/sql-tools/from-code'; -import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator'; -import { RegisterItem } from 'src/sql-tools/from-code/register-item'; -import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types'; - -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export type TableWithMetadata = DatabaseTable & { metadata: { options: TableOptions; object: Function } }; -export type SchemaBuilder = Omit & { tables: TableWithMetadata[] }; - -export type Processor = (builder: SchemaBuilder, items: RegisterItem[], options: SchemaFromCodeOptions) => void; diff --git a/server/src/sql-tools/from-database/index.ts b/server/src/sql-tools/from-database/index.ts deleted file mode 100644 index 771a24d7b1..0000000000 --- a/server/src/sql-tools/from-database/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Kysely } from 'kysely'; -import { PostgresJSDialect } from 'kysely-postgres-js'; -import { Sql } from 'postgres'; -import { readColumns } from 'src/sql-tools/from-database/readers/column.reader'; -import { readComments } from 'src/sql-tools/from-database/readers/comment.reader'; -import { readConstraints } from 'src/sql-tools/from-database/readers/constraint.reader'; -import { readExtensions } from 'src/sql-tools/from-database/readers/extension.reader'; -import { readFunctions } from 'src/sql-tools/from-database/readers/function.reader'; -import { readIndexes } from 'src/sql-tools/from-database/readers/index.reader'; -import { readName } from 'src/sql-tools/from-database/readers/name.reader'; -import { readParameters } from 'src/sql-tools/from-database/readers/parameter.reader'; -import { readTables } from 'src/sql-tools/from-database/readers/table.reader'; -import { readTriggers } from 'src/sql-tools/from-database/readers/trigger.reader'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; -import { DatabaseSchema, LoadSchemaOptions, PostgresDB } from 'src/sql-tools/types'; - -const readers: DatabaseReader[] = [ - // - readName, - readParameters, - readExtensions, - readFunctions, - readTables, - readColumns, - readIndexes, - readConstraints, - readTriggers, - readComments, -]; - -/** - * Load the database schema from the database - */ -export const schemaFromDatabase = async (postgres: Sql, options: LoadSchemaOptions = {}): Promise => { - const schema: DatabaseSchema = { - name: 'immich', - schemaName: options.schemaName || 'public', - parameters: [], - functions: [], - enums: [], - extensions: [], - tables: [], - warnings: [], - }; - - const db = new Kysely({ dialect: new PostgresJSDialect({ postgres }) }); - for (const reader of readers) { - await reader(schema, db); - } - - await db.destroy(); - - return schema; -}; diff --git a/server/src/sql-tools/from-database/readers/type.ts b/server/src/sql-tools/from-database/readers/type.ts deleted file mode 100644 index d8a21d486b..0000000000 --- a/server/src/sql-tools/from-database/readers/type.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DatabaseClient, DatabaseSchema } from 'src/sql-tools/types'; - -export type DatabaseReader = (schema: DatabaseSchema, db: DatabaseClient) => Promise; diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts index 015bbe4d9c..119fe63291 100644 --- a/server/src/sql-tools/helpers.ts +++ b/server/src/sql-tools/helpers.ts @@ -1,15 +1,6 @@ import { createHash } from 'node:crypto'; -import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { SchemaBuilder } from 'src/sql-tools/from-code/processors/type'; -import { - Comparer, - DatabaseColumn, - DiffOptions, - SchemaDiff, - TriggerAction, - TriggerScope, - TriggerTiming, -} from 'src/sql-tools/types'; +import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; +import { Comparer, DatabaseColumn, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types'; export const asMetadataKey = (name: string) => `sql-tools:${name}`; @@ -27,46 +18,6 @@ export const asOptions = (options: string | T): T = }; export const sha1 = (value: string) => createHash('sha1').update(value).digest('hex'); -export const hasMask = (input: number, mask: number) => (input & mask) === mask; - -export const parseTriggerType = (type: number) => { - // eslint-disable-next-line unicorn/prefer-math-trunc - const scope: TriggerScope = hasMask(type, 1 << 0) ? 'row' : 'statement'; - - let timing: TriggerTiming = 'after'; - const timingMasks: Array<{ mask: number; value: TriggerTiming }> = [ - { mask: 1 << 1, value: 'before' }, - { mask: 1 << 6, value: 'instead of' }, - ]; - - for (const { mask, value } of timingMasks) { - if (hasMask(type, mask)) { - timing = value; - break; - } - } - - const actions: TriggerAction[] = []; - const actionMasks: Array<{ mask: number; value: TriggerAction }> = [ - { mask: 1 << 2, value: 'insert' }, - { mask: 1 << 3, value: 'delete' }, - { mask: 1 << 4, value: 'update' }, - { mask: 1 << 5, value: 'truncate' }, - ]; - - for (const { mask, value } of actionMasks) { - if (hasMask(type, mask)) { - actions.push(value); - break; - } - } - - if (actions.length === 0) { - throw new Error(`Unable to parse trigger type ${type}`); - } - - return { actions, timing, scope }; -}; export const fromColumnValue = (columnValue?: ColumnValue) => { if (columnValue === undefined) { @@ -108,7 +59,7 @@ export const haveEqualColumns = (sourceColumns?: string[], targetColumns?: strin export const compare = ( sources: T[], targets: T[], - options: DiffOptions | undefined, + options: IgnoreOptions | undefined, comparer: Comparer, ) => { options = options || {}; @@ -144,7 +95,7 @@ export const compare = ( const isIgnored = ( source: { synchronize?: boolean } | undefined, target: { synchronize?: boolean } | undefined, - options: DiffOptions, + options: IgnoreOptions, ) => { return (options.ignoreExtra && !source) || (options.ignoreMissing && !target); }; @@ -214,20 +165,3 @@ export const asColumnComment = (tableName: string, columnName: string, comment: export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', '); export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, [...columns]); - -export const asIndexName = (table: string, columns?: string[], where?: string) => { - const items: string[] = []; - for (const columnName of columns ?? []) { - items.push(columnName); - } - - if (where) { - items.push(where); - } - - return asKey('IDX_', table, items); -}; - -export const addWarning = (builder: SchemaBuilder, context: string, message: string) => { - builder.warnings.push(`[${context}] ${message}`); -}; diff --git a/server/src/sql-tools/from-code/processors/check-constraint.processor.ts b/server/src/sql-tools/processors/check-constraint.processor.ts similarity index 63% rename from server/src/sql-tools/from-code/processors/check-constraint.processor.ts rename to server/src/sql-tools/processors/check-constraint.processor.ts index feb21b9894..10e5c18791 100644 --- a/server/src/sql-tools/from-code/processors/check-constraint.processor.ts +++ b/server/src/sql-tools/processors/check-constraint.processor.ts @@ -1,22 +1,20 @@ -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; import { asKey } from 'src/sql-tools/helpers'; -import { DatabaseConstraintType } from 'src/sql-tools/types'; +import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processCheckConstraints: Processor = (builder, items) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'checkConstraint')) { - const table = resolveTable(builder, object); + const table = builder.getTableByObject(object); if (!table) { - onMissingTable(builder, '@Check', object); + builder.warnMissingTable('@Check', object); continue; } const tableName = table.name; table.constraints.push({ - type: DatabaseConstraintType.CHECK, + type: ConstraintType.CHECK, name: options.name || asCheckConstraintName(tableName, options.expression), tableName, expression: options.expression, diff --git a/server/src/sql-tools/processors/column.processor.ts b/server/src/sql-tools/processors/column.processor.ts new file mode 100644 index 0000000000..3981ee4036 --- /dev/null +++ b/server/src/sql-tools/processors/column.processor.ts @@ -0,0 +1,55 @@ +import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; +import { fromColumnValue } from 'src/sql-tools/helpers'; +import { Processor } from 'src/sql-tools/types'; + +export const processColumns: Processor = (builder, items) => { + for (const { + type, + item: { object, propertyName, options }, + } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { + const table = builder.getTableByObject(object.constructor); + if (!table) { + builder.warnMissingTable(type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName); + continue; + } + + const columnName = options.name ?? String(propertyName); + const existingColumn = table.columns.find((column) => column.name === columnName); + if (existingColumn) { + // TODO log warnings if column name is not unique + continue; + } + + let defaultValue = fromColumnValue(options.default); + let nullable = options.nullable ?? false; + + // map `{ default: null }` to `{ nullable: true }` + if (defaultValue === null) { + nullable = true; + defaultValue = undefined; + } + + const isEnum = !!(options as ColumnOptions).enum; + + builder.addColumn( + table, + { + name: columnName, + tableName: table.name, + primary: options.primary ?? false, + default: defaultValue, + nullable, + isArray: (options as ColumnOptions).array ?? false, + length: options.length, + type: isEnum ? 'enum' : options.type || 'character varying', + enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined, + comment: options.comment, + storage: options.storage, + identity: options.identity, + synchronize: options.synchronize ?? true, + }, + options, + propertyName, + ); + } +}; diff --git a/server/src/sql-tools/from-code/processors/configuration-parameter.processor.ts b/server/src/sql-tools/processors/configuration-parameter.processor.ts similarity index 81% rename from server/src/sql-tools/from-code/processors/configuration-parameter.processor.ts rename to server/src/sql-tools/processors/configuration-parameter.processor.ts index 493214e5b8..4b2d56f103 100644 --- a/server/src/sql-tools/from-code/processors/configuration-parameter.processor.ts +++ b/server/src/sql-tools/processors/configuration-parameter.processor.ts @@ -1,12 +1,12 @@ -import { Processor } from 'src/sql-tools/from-code/processors/type'; import { fromColumnValue } from 'src/sql-tools/helpers'; +import { Processor } from 'src/sql-tools/types'; export const processConfigurationParameters: Processor = (builder, items) => { for (const { item: { options }, } of items.filter((item) => item.type === 'configurationParameter')) { builder.parameters.push({ - databaseName: builder.name, + databaseName: builder.databaseName, name: options.name, value: fromColumnValue(options.value), scope: options.scope, diff --git a/server/src/sql-tools/from-code/processors/database.processor.ts b/server/src/sql-tools/processors/database.processor.ts similarity index 63% rename from server/src/sql-tools/from-code/processors/database.processor.ts rename to server/src/sql-tools/processors/database.processor.ts index 9b0662f7e0..9c9e61a6cf 100644 --- a/server/src/sql-tools/from-code/processors/database.processor.ts +++ b/server/src/sql-tools/processors/database.processor.ts @@ -1,10 +1,10 @@ -import { Processor } from 'src/sql-tools/from-code/processors/type'; import { asSnakeCase } from 'src/sql-tools/helpers'; +import { Processor } from 'src/sql-tools/types'; export const processDatabases: Processor = (builder, items) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'database')) { - builder.name = options.name || asSnakeCase(object.name); + builder.databaseName = options.name || asSnakeCase(object.name); } }; diff --git a/server/src/sql-tools/from-code/processors/enum.processor.ts b/server/src/sql-tools/processors/enum.processor.ts similarity index 76% rename from server/src/sql-tools/from-code/processors/enum.processor.ts rename to server/src/sql-tools/processors/enum.processor.ts index d6d19ec025..d396089c8f 100644 --- a/server/src/sql-tools/from-code/processors/enum.processor.ts +++ b/server/src/sql-tools/processors/enum.processor.ts @@ -1,4 +1,4 @@ -import { Processor } from 'src/sql-tools/from-code/processors/type'; +import { Processor } from 'src/sql-tools/types'; export const processEnums: Processor = (builder, items) => { for (const { item } of items.filter((item) => item.type === 'enum')) { diff --git a/server/src/sql-tools/from-code/processors/extension.processor.ts b/server/src/sql-tools/processors/extension.processor.ts similarity index 80% rename from server/src/sql-tools/from-code/processors/extension.processor.ts rename to server/src/sql-tools/processors/extension.processor.ts index 4b12054aa3..97a39c0d8b 100644 --- a/server/src/sql-tools/from-code/processors/extension.processor.ts +++ b/server/src/sql-tools/processors/extension.processor.ts @@ -1,4 +1,4 @@ -import { Processor } from 'src/sql-tools/from-code/processors/type'; +import { Processor } from 'src/sql-tools/types'; export const processExtensions: Processor = (builder, items) => { for (const { diff --git a/server/src/sql-tools/from-code/processors/foreign-key-column.processor.ts b/server/src/sql-tools/processors/foreign-key-column.processor.ts similarity index 64% rename from server/src/sql-tools/from-code/processors/foreign-key-column.processor.ts rename to server/src/sql-tools/processors/foreign-key-column.processor.ts index d706763d1d..a3cac0a85e 100644 --- a/server/src/sql-tools/from-code/processors/foreign-key-column.processor.ts +++ b/server/src/sql-tools/processors/foreign-key-column.processor.ts @@ -1,28 +1,25 @@ -import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers'; -import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types'; +import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; export const processForeignKeyColumns: Processor = (builder, items) => { for (const { item: { object, propertyName, options, target }, } of items.filter((item) => item.type === 'foreignKeyColumn')) { - const { table, column } = resolveColumn(builder, object, propertyName); + const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName); if (!table) { - onMissingTable(builder, '@ForeignKeyColumn', object); + builder.warnMissingTable('@ForeignKeyColumn', object); continue; } if (!column) { // should be impossible since they are pre-created in `column.processor.ts` - onMissingColumn(builder, '@ForeignKeyColumn', object, propertyName); + builder.warnMissingColumn('@ForeignKeyColumn', object, propertyName); continue; } - const referenceTable = resolveTable(builder, target()); + const referenceTable = builder.getTableByObject(target()); if (!referenceTable) { - onMissingTable(builder, '@ForeignKeyColumn', object, propertyName); + builder.warnMissingTable('@ForeignKeyColumn', object, propertyName); continue; } @@ -41,11 +38,11 @@ export const processForeignKeyColumns: Processor = (builder, items) => { name, tableName: table.name, columnNames, - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, referenceTableName: referenceTable.name, referenceColumnNames, - onUpdate: options.onUpdate as DatabaseActionType, - onDelete: options.onDelete as DatabaseActionType, + onUpdate: options.onUpdate as ActionType, + onDelete: options.onDelete as ActionType, synchronize: options.synchronize ?? true, }); @@ -54,7 +51,7 @@ export const processForeignKeyColumns: Processor = (builder, items) => { name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames), tableName: table.name, columnNames, - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, synchronize: options.synchronize ?? true, }); } diff --git a/server/src/sql-tools/from-code/processors/foreign-key-constraint.processor.ts b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts similarity index 61% rename from server/src/sql-tools/from-code/processors/foreign-key-constraint.processor.ts rename to server/src/sql-tools/processors/foreign-key-constraint.processor.ts index e88297b6c6..704e6ed8e4 100644 --- a/server/src/sql-tools/from-code/processors/foreign-key-constraint.processor.ts +++ b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts @@ -1,23 +1,20 @@ -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { addWarning, asForeignKeyConstraintName, asIndexName } from 'src/sql-tools/helpers'; -import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types'; +import { asForeignKeyConstraintName } from 'src/sql-tools/helpers'; +import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; export const processForeignKeyConstraints: Processor = (builder, items, config) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'foreignKeyConstraint')) { - const table = resolveTable(builder, object); + const table = builder.getTableByObject(object); if (!table) { - onMissingTable(builder, '@ForeignKeyConstraint', { name: 'referenceTable' }); + builder.warnMissingTable('@ForeignKeyConstraint', { name: 'referenceTable' }); continue; } - const referenceTable = resolveTable(builder, options.referenceTable()); + const referenceTable = builder.getTableByObject(options.referenceTable()); if (!referenceTable) { const referenceTableName = options.referenceTable()?.name; - addWarning( - builder, + builder.warn( '@ForeignKeyConstraint.referenceTable', `Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''), ); @@ -28,21 +25,18 @@ export const processForeignKeyConstraints: Processor = (builder, items, config) for (const columnName of options.columns) { if (!table.columns.some(({ name }) => name === columnName)) { - addWarning( - builder, - '@ForeignKeyConstraint.columns', - `Unable to find column (${table.metadata.object.name}.${columnName})`, - ); + const metadata = builder.getTableMetadata(table); + builder.warn('@ForeignKeyConstraint.columns', `Unable to find column (${metadata.object.name}.${columnName})`); missingColumn = true; } } for (const columnName of options.referenceColumns || []) { if (!referenceTable.columns.some(({ name }) => name === columnName)) { - addWarning( - builder, + const metadata = builder.getTableMetadata(referenceTable); + builder.warn( '@ForeignKeyConstraint.referenceColumns', - `Unable to find column (${referenceTable.metadata.object.name}.${columnName})`, + `Unable to find column (${metadata.object.name}.${columnName})`, ); missingColumn = true; } @@ -58,14 +52,14 @@ export const processForeignKeyConstraints: Processor = (builder, items, config) const name = options.name || asForeignKeyConstraintName(table.name, options.columns); table.constraints.push({ - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name, tableName: table.name, columnNames: options.columns, referenceTableName: referenceTable.name, referenceColumnNames: referenceColumns, - onUpdate: options.onUpdate as DatabaseActionType, - onDelete: options.onDelete as DatabaseActionType, + onUpdate: options.onUpdate as ActionType, + onDelete: options.onDelete as ActionType, synchronize: options.synchronize ?? true, }); @@ -75,7 +69,7 @@ export const processForeignKeyConstraints: Processor = (builder, items, config) if (options.index || options.indexName || config.createForeignKeyIndexes) { table.indexes.push({ - name: options.indexName || asIndexName(table.name, options.columns), + name: options.indexName || builder.asIndexName(table.name, options.columns), tableName: table.name, columnNames: options.columns, unique: false, diff --git a/server/src/sql-tools/from-code/processors/function.processor.ts b/server/src/sql-tools/processors/function.processor.ts similarity index 77% rename from server/src/sql-tools/from-code/processors/function.processor.ts rename to server/src/sql-tools/processors/function.processor.ts index cbd9c87abc..669d1a5ec0 100644 --- a/server/src/sql-tools/from-code/processors/function.processor.ts +++ b/server/src/sql-tools/processors/function.processor.ts @@ -1,4 +1,4 @@ -import { Processor } from 'src/sql-tools/from-code/processors/type'; +import { Processor } from 'src/sql-tools/types'; export const processFunctions: Processor = (builder, items) => { for (const { item } of items.filter((item) => item.type === 'function')) { diff --git a/server/src/sql-tools/from-code/processors/index.processor.ts b/server/src/sql-tools/processors/index.processor.ts similarity index 70% rename from server/src/sql-tools/from-code/processors/index.processor.ts rename to server/src/sql-tools/processors/index.processor.ts index 4de8914231..546bac236a 100644 --- a/server/src/sql-tools/from-code/processors/index.processor.ts +++ b/server/src/sql-tools/processors/index.processor.ts @@ -1,20 +1,17 @@ -import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; -import { asIndexName } from 'src/sql-tools/helpers'; +import { Processor } from 'src/sql-tools/types'; export const processIndexes: Processor = (builder, items, config) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'index')) { - const table = resolveTable(builder, object); + const table = builder.getTableByObject(object); if (!table) { - onMissingTable(builder, '@Check', object); + builder.warnMissingTable('@Check', object); continue; } table.indexes.push({ - name: options.name || asIndexName(table.name, options.columns, options.where), + name: options.name || builder.asIndexName(table.name, options.columns, options.where), tableName: table.name, unique: options.unique ?? false, expression: options.expression, @@ -31,15 +28,15 @@ export const processIndexes: Processor = (builder, items, config) => { type, item: { object, propertyName, options }, } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const { table, column } = resolveColumn(builder, object, propertyName); + const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName); if (!table) { - onMissingTable(builder, '@Column', object); + builder.warnMissingTable('@Column', object); continue; } if (!column) { // should be impossible since they are created in `column.processor.ts` - onMissingColumn(builder, '@Column', object, propertyName); + builder.warnMissingColumn('@Column', object, propertyName); continue; } @@ -53,7 +50,7 @@ export const processIndexes: Processor = (builder, items, config) => { continue; } - const indexName = options.indexName || asIndexName(table.name, [column.name]); + const indexName = options.indexName || builder.asIndexName(table.name, [column.name]); const isIndexPresent = table.indexes.some((index) => index.name === indexName); if (isIndexPresent) { diff --git a/server/src/sql-tools/processors/index.ts b/server/src/sql-tools/processors/index.ts new file mode 100644 index 0000000000..054540f7b0 --- /dev/null +++ b/server/src/sql-tools/processors/index.ts @@ -0,0 +1,32 @@ +import { processCheckConstraints } from 'src/sql-tools/processors/check-constraint.processor'; +import { processColumns } from 'src/sql-tools/processors/column.processor'; +import { processConfigurationParameters } from 'src/sql-tools/processors/configuration-parameter.processor'; +import { processDatabases } from 'src/sql-tools/processors/database.processor'; +import { processEnums } from 'src/sql-tools/processors/enum.processor'; +import { processExtensions } from 'src/sql-tools/processors/extension.processor'; +import { processForeignKeyColumns } from 'src/sql-tools/processors/foreign-key-column.processor'; +import { processForeignKeyConstraints } from 'src/sql-tools/processors/foreign-key-constraint.processor'; +import { processFunctions } from 'src/sql-tools/processors/function.processor'; +import { processIndexes } from 'src/sql-tools/processors/index.processor'; +import { processPrimaryKeyConstraints } from 'src/sql-tools/processors/primary-key-contraint.processor'; +import { processTables } from 'src/sql-tools/processors/table.processor'; +import { processTriggers } from 'src/sql-tools/processors/trigger.processor'; +import { processUniqueConstraints } from 'src/sql-tools/processors/unique-constraint.processor'; +import { Processor } from 'src/sql-tools/types'; + +export const processors: Processor[] = [ + processDatabases, + processConfigurationParameters, + processEnums, + processExtensions, + processFunctions, + processTables, + processColumns, + processForeignKeyColumns, + processForeignKeyConstraints, + processUniqueConstraints, + processCheckConstraints, + processPrimaryKeyConstraints, + processIndexes, + processTriggers, +]; diff --git a/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/processors/primary-key-contraint.processor.ts similarity index 61% rename from server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts rename to server/src/sql-tools/processors/primary-key-contraint.processor.ts index 74aecc5ea0..be8983f79a 100644 --- a/server/src/sql-tools/from-code/processors/primary-key-contraint.processor.ts +++ b/server/src/sql-tools/processors/primary-key-contraint.processor.ts @@ -1,6 +1,5 @@ -import { Processor } from 'src/sql-tools/from-code/processors/type'; import { asKey } from 'src/sql-tools/helpers'; -import { DatabaseConstraintType } from 'src/sql-tools/types'; +import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processPrimaryKeyConstraints: Processor = (builder) => { for (const table of builder.tables) { @@ -11,13 +10,15 @@ export const processPrimaryKeyConstraints: Processor = (builder) => { columnNames.push(column.name); } } + if (columnNames.length > 0) { + const tableMetadata = builder.getTableMetadata(table); table.constraints.push({ - type: DatabaseConstraintType.PRIMARY_KEY, - name: table.metadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames), + type: ConstraintType.PRIMARY_KEY, + name: tableMetadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames), tableName: table.name, columnNames, - synchronize: table.metadata.options.synchronize ?? true, + synchronize: tableMetadata.options.synchronize ?? true, }); } } diff --git a/server/src/sql-tools/processors/table.processor.ts b/server/src/sql-tools/processors/table.processor.ts new file mode 100644 index 0000000000..967e4a023b --- /dev/null +++ b/server/src/sql-tools/processors/table.processor.ts @@ -0,0 +1,28 @@ +import { asSnakeCase } from 'src/sql-tools/helpers'; +import { Processor } from 'src/sql-tools/types'; + +export const processTables: Processor = (builder, items) => { + for (const { + item: { options, object }, + } of items.filter((item) => item.type === 'table')) { + const test = builder.getTableByObject(object); + if (test) { + throw new Error( + `Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`, + ); + } + + builder.addTable( + { + name: options.name || asSnakeCase(object.name), + columns: [], + constraints: [], + indexes: [], + triggers: [], + synchronize: options.synchronize ?? true, + }, + options, + object, + ); + } +}; diff --git a/server/src/sql-tools/from-code/processors/trigger.processor.ts b/server/src/sql-tools/processors/trigger.processor.ts similarity index 71% rename from server/src/sql-tools/from-code/processors/trigger.processor.ts rename to server/src/sql-tools/processors/trigger.processor.ts index 4b875f353b..03e7076fdb 100644 --- a/server/src/sql-tools/from-code/processors/trigger.processor.ts +++ b/server/src/sql-tools/processors/trigger.processor.ts @@ -1,15 +1,14 @@ -import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; +import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; import { asKey } from 'src/sql-tools/helpers'; +import { Processor } from 'src/sql-tools/types'; export const processTriggers: Processor = (builder, items) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'trigger')) { - const table = resolveTable(builder, object); + const table = builder.getTableByObject(object); if (!table) { - onMissingTable(builder, '@Trigger', object); + builder.warnMissingTable('@Trigger', object); continue; } diff --git a/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts b/server/src/sql-tools/processors/unique-constraint.processor.ts similarity index 66% rename from server/src/sql-tools/from-code/processors/unique-constraint.processor.ts rename to server/src/sql-tools/processors/unique-constraint.processor.ts index 9014378085..eed9597879 100644 --- a/server/src/sql-tools/from-code/processors/unique-constraint.processor.ts +++ b/server/src/sql-tools/processors/unique-constraint.processor.ts @@ -1,16 +1,13 @@ -import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor'; -import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor'; -import { Processor } from 'src/sql-tools/from-code/processors/type'; import { asKey } from 'src/sql-tools/helpers'; -import { DatabaseConstraintType } from 'src/sql-tools/types'; +import { ConstraintType, Processor } from 'src/sql-tools/types'; export const processUniqueConstraints: Processor = (builder, items) => { for (const { item: { object, options }, } of items.filter((item) => item.type === 'uniqueConstraint')) { - const table = resolveTable(builder, object); + const table = builder.getTableByObject(object); if (!table) { - onMissingTable(builder, '@Unique', object); + builder.warnMissingTable('@Unique', object); continue; } @@ -18,7 +15,7 @@ export const processUniqueConstraints: Processor = (builder, items) => { const columnNames = options.columns; table.constraints.push({ - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: options.name || asUniqueConstraintName(tableName, columnNames), tableName, columnNames, @@ -31,21 +28,21 @@ export const processUniqueConstraints: Processor = (builder, items) => { type, item: { object, propertyName, options }, } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const { table, column } = resolveColumn(builder, object, propertyName); + const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName); if (!table) { - onMissingTable(builder, '@Column', object); + builder.warnMissingTable('@Column', object); continue; } if (!column) { // should be impossible since they are created in `column.processor.ts` - onMissingColumn(builder, '@Column', object, propertyName); + builder.warnMissingColumn('@Column', object, propertyName); continue; } if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { table.constraints.push({ - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]), tableName: table.name, columnNames: [column.name], diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts index 61e4a3e431..aaef55dd8d 100644 --- a/server/src/sql-tools/public_api.ts +++ b/server/src/sql-tools/public_api.ts @@ -1,29 +1,7 @@ -export { schemaDiff } from 'src/sql-tools/diff'; -export { schemaFromCode } from 'src/sql-tools/from-code'; -export * from 'src/sql-tools/from-code/decorators/after-delete.decorator'; -export * from 'src/sql-tools/from-code/decorators/after-insert.decorator'; -export * from 'src/sql-tools/from-code/decorators/before-update.decorator'; -export * from 'src/sql-tools/from-code/decorators/check.decorator'; -export * from 'src/sql-tools/from-code/decorators/column.decorator'; -export * from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator'; -export * from 'src/sql-tools/from-code/decorators/create-date-column.decorator'; -export * from 'src/sql-tools/from-code/decorators/database.decorator'; -export * from 'src/sql-tools/from-code/decorators/delete-date-column.decorator'; -export * from 'src/sql-tools/from-code/decorators/extension.decorator'; -export * from 'src/sql-tools/from-code/decorators/extensions.decorator'; -export * from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator'; -export * from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator'; -export * from 'src/sql-tools/from-code/decorators/generated-column.decorator'; -export * from 'src/sql-tools/from-code/decorators/index.decorator'; -export * from 'src/sql-tools/from-code/decorators/primary-column.decorator'; -export * from 'src/sql-tools/from-code/decorators/primary-generated-column.decorator'; -export * from 'src/sql-tools/from-code/decorators/table.decorator'; -export * from 'src/sql-tools/from-code/decorators/trigger-function.decorator'; -export * from 'src/sql-tools/from-code/decorators/trigger.decorator'; -export * from 'src/sql-tools/from-code/decorators/unique.decorator'; -export * from 'src/sql-tools/from-code/decorators/update-date-column.decorator'; -export * from 'src/sql-tools/from-code/register-enum'; -export * from 'src/sql-tools/from-code/register-function'; -export { schemaFromDatabase } from 'src/sql-tools/from-database'; -export { schemaDiffToSql } from 'src/sql-tools/to-sql'; +export * from 'src/sql-tools/decorators'; +export * from 'src/sql-tools/register-enum'; +export * from 'src/sql-tools/register-function'; +export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff'; +export { schemaFromCode } from 'src/sql-tools/schema-from-code'; +export { schemaFromDatabase } from 'src/sql-tools/schema-from-database'; export * from 'src/sql-tools/types'; diff --git a/server/src/sql-tools/from-database/readers/column.reader.ts b/server/src/sql-tools/readers/column.reader.ts similarity index 96% rename from server/src/sql-tools/from-database/readers/column.reader.ts rename to server/src/sql-tools/readers/column.reader.ts index 167124bac9..c81fafa68f 100644 --- a/server/src/sql-tools/from-database/readers/column.reader.ts +++ b/server/src/sql-tools/readers/column.reader.ts @@ -1,7 +1,6 @@ import { sql } from 'kysely'; import { jsonArrayFrom } from 'kysely/helpers/postgres'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; -import { ColumnType, DatabaseColumn } from 'src/sql-tools/types'; +import { ColumnType, DatabaseColumn, DatabaseReader } from 'src/sql-tools/types'; export const readColumns: DatabaseReader = async (schema, db) => { const columns = await db diff --git a/server/src/sql-tools/from-database/readers/comment.reader.ts b/server/src/sql-tools/readers/comment.reader.ts similarity index 93% rename from server/src/sql-tools/from-database/readers/comment.reader.ts rename to server/src/sql-tools/readers/comment.reader.ts index 3dd4f4adc9..a06e111f84 100644 --- a/server/src/sql-tools/from-database/readers/comment.reader.ts +++ b/server/src/sql-tools/readers/comment.reader.ts @@ -1,4 +1,4 @@ -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; +import { DatabaseReader } from 'src/sql-tools/types'; export const readComments: DatabaseReader = async (schema, db) => { const comments = await db diff --git a/server/src/sql-tools/from-database/readers/constraint.reader.ts b/server/src/sql-tools/readers/constraint.reader.ts similarity index 88% rename from server/src/sql-tools/from-database/readers/constraint.reader.ts rename to server/src/sql-tools/readers/constraint.reader.ts index a633324f25..0e4334bf04 100644 --- a/server/src/sql-tools/from-database/readers/constraint.reader.ts +++ b/server/src/sql-tools/readers/constraint.reader.ts @@ -1,6 +1,5 @@ import { sql } from 'kysely'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; -import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types'; +import { ActionType, ConstraintType, DatabaseReader } from 'src/sql-tools/types'; export const readConstraints: DatabaseReader = async (schema, db) => { const constraints = await db @@ -60,7 +59,7 @@ export const readConstraints: DatabaseReader = async (schema, db) => { continue; } table.constraints.push({ - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: constraintName, tableName: constraint.table_name, columnNames: constraint.column_names, @@ -79,7 +78,7 @@ export const readConstraints: DatabaseReader = async (schema, db) => { } table.constraints.push({ - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: constraintName, tableName: constraint.table_name, columnNames: constraint.column_names, @@ -95,7 +94,7 @@ export const readConstraints: DatabaseReader = async (schema, db) => { // unique constraint case 'u': { table.constraints.push({ - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: constraintName, tableName: constraint.table_name, columnNames: constraint.column_names as string[], @@ -107,7 +106,7 @@ export const readConstraints: DatabaseReader = async (schema, db) => { // check constraint case 'c': { table.constraints.push({ - type: DatabaseConstraintType.CHECK, + type: ConstraintType.CHECK, name: constraint.constraint_name, tableName: constraint.table_name, expression: constraint.expression.replace('CHECK ', ''), @@ -122,23 +121,23 @@ export const readConstraints: DatabaseReader = async (schema, db) => { const asDatabaseAction = (action: string) => { switch (action) { case 'a': { - return DatabaseActionType.NO_ACTION; + return ActionType.NO_ACTION; } case 'c': { - return DatabaseActionType.CASCADE; + return ActionType.CASCADE; } case 'r': { - return DatabaseActionType.RESTRICT; + return ActionType.RESTRICT; } case 'n': { - return DatabaseActionType.SET_NULL; + return ActionType.SET_NULL; } case 'd': { - return DatabaseActionType.SET_DEFAULT; + return ActionType.SET_DEFAULT; } default: { - return DatabaseActionType.NO_ACTION; + return ActionType.NO_ACTION; } } }; diff --git a/server/src/sql-tools/from-database/readers/extension.reader.ts b/server/src/sql-tools/readers/extension.reader.ts similarity index 86% rename from server/src/sql-tools/from-database/readers/extension.reader.ts rename to server/src/sql-tools/readers/extension.reader.ts index c0a59f85d8..6c7e6dd28a 100644 --- a/server/src/sql-tools/from-database/readers/extension.reader.ts +++ b/server/src/sql-tools/readers/extension.reader.ts @@ -1,4 +1,4 @@ -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; +import { DatabaseReader } from 'src/sql-tools/types'; export const readExtensions: DatabaseReader = async (schema, db) => { const extensions = await db diff --git a/server/src/sql-tools/from-database/readers/function.reader.ts b/server/src/sql-tools/readers/function.reader.ts similarity index 92% rename from server/src/sql-tools/from-database/readers/function.reader.ts rename to server/src/sql-tools/readers/function.reader.ts index 0c81117d7c..385d061181 100644 --- a/server/src/sql-tools/from-database/readers/function.reader.ts +++ b/server/src/sql-tools/readers/function.reader.ts @@ -1,5 +1,5 @@ import { sql } from 'kysely'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; +import { DatabaseReader } from 'src/sql-tools/types'; export const readFunctions: DatabaseReader = async (schema, db) => { const routines = await db diff --git a/server/src/sql-tools/from-database/readers/index.reader.ts b/server/src/sql-tools/readers/index.reader.ts similarity index 96% rename from server/src/sql-tools/from-database/readers/index.reader.ts rename to server/src/sql-tools/readers/index.reader.ts index 681ba8f9ef..e3fea7b74f 100644 --- a/server/src/sql-tools/from-database/readers/index.reader.ts +++ b/server/src/sql-tools/readers/index.reader.ts @@ -1,5 +1,5 @@ import { sql } from 'kysely'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; +import { DatabaseReader } from 'src/sql-tools/types'; export const readIndexes: DatabaseReader = async (schema, db) => { const indexes = await db diff --git a/server/src/sql-tools/readers/index.ts b/server/src/sql-tools/readers/index.ts new file mode 100644 index 0000000000..d710fb4acb --- /dev/null +++ b/server/src/sql-tools/readers/index.ts @@ -0,0 +1,25 @@ +import { readColumns } from 'src/sql-tools/readers/column.reader'; +import { readComments } from 'src/sql-tools/readers/comment.reader'; +import { readConstraints } from 'src/sql-tools/readers/constraint.reader'; +import { readExtensions } from 'src/sql-tools/readers/extension.reader'; +import { readFunctions } from 'src/sql-tools/readers/function.reader'; +import { readIndexes } from 'src/sql-tools/readers/index.reader'; +import { readName } from 'src/sql-tools/readers/name.reader'; +import { readParameters } from 'src/sql-tools/readers/parameter.reader'; +import { readTables } from 'src/sql-tools/readers/table.reader'; +import { readTriggers } from 'src/sql-tools/readers/trigger.reader'; +import { DatabaseReader } from 'src/sql-tools/types'; + +export const readers: DatabaseReader[] = [ + // + readName, + readParameters, + readExtensions, + readFunctions, + readTables, + readColumns, + readIndexes, + readConstraints, + readTriggers, + readComments, +]; diff --git a/server/src/sql-tools/from-database/readers/name.reader.ts b/server/src/sql-tools/readers/name.reader.ts similarity index 66% rename from server/src/sql-tools/from-database/readers/name.reader.ts rename to server/src/sql-tools/readers/name.reader.ts index dc3d4ec745..5989ccb4c9 100644 --- a/server/src/sql-tools/from-database/readers/name.reader.ts +++ b/server/src/sql-tools/readers/name.reader.ts @@ -1,8 +1,8 @@ import { QueryResult, sql } from 'kysely'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; +import { DatabaseReader } from 'src/sql-tools/types'; export const readName: DatabaseReader = async (schema, db) => { const result = (await sql`SELECT current_database() as name`.execute(db)) as QueryResult<{ name: string }>; - schema.name = result.rows[0].name; + schema.databaseName = result.rows[0].name; }; diff --git a/server/src/sql-tools/from-database/readers/parameter.reader.ts b/server/src/sql-tools/readers/parameter.reader.ts similarity index 76% rename from server/src/sql-tools/from-database/readers/parameter.reader.ts rename to server/src/sql-tools/readers/parameter.reader.ts index f58718a6b6..7930c251f4 100644 --- a/server/src/sql-tools/from-database/readers/parameter.reader.ts +++ b/server/src/sql-tools/readers/parameter.reader.ts @@ -1,6 +1,5 @@ import { sql } from 'kysely'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; -import { ParameterScope } from 'src/sql-tools/types'; +import { DatabaseReader, ParameterScope } from 'src/sql-tools/types'; export const readParameters: DatabaseReader = async (schema, db) => { const parameters = await db @@ -13,7 +12,7 @@ export const readParameters: DatabaseReader = async (schema, db) => { schema.parameters.push({ name: parameter.name, value: parameter.value, - databaseName: schema.name, + databaseName: schema.databaseName, scope: parameter.scope as ParameterScope, synchronize: true, }); diff --git a/server/src/sql-tools/from-database/readers/table.reader.ts b/server/src/sql-tools/readers/table.reader.ts similarity index 87% rename from server/src/sql-tools/from-database/readers/table.reader.ts rename to server/src/sql-tools/readers/table.reader.ts index 6dbf9fc8b8..793da20644 100644 --- a/server/src/sql-tools/from-database/readers/table.reader.ts +++ b/server/src/sql-tools/readers/table.reader.ts @@ -1,5 +1,5 @@ import { sql } from 'kysely'; -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; +import { DatabaseReader } from 'src/sql-tools/types'; export const readTables: DatabaseReader = async (schema, db) => { const tables = await db diff --git a/server/src/sql-tools/from-database/readers/trigger.reader.ts b/server/src/sql-tools/readers/trigger.reader.ts similarity index 56% rename from server/src/sql-tools/from-database/readers/trigger.reader.ts rename to server/src/sql-tools/readers/trigger.reader.ts index a174a7502b..09e7ae9c8a 100644 --- a/server/src/sql-tools/from-database/readers/trigger.reader.ts +++ b/server/src/sql-tools/readers/trigger.reader.ts @@ -1,5 +1,4 @@ -import { DatabaseReader } from 'src/sql-tools/from-database/readers/type'; -import { parseTriggerType } from 'src/sql-tools/helpers'; +import { DatabaseReader, TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; export const readTriggers: DatabaseReader = async (schema, db) => { const triggers = await db @@ -44,3 +43,44 @@ export const readTriggers: DatabaseReader = async (schema, db) => { }); } }; + +export const hasMask = (input: number, mask: number) => (input & mask) === mask; + +export const parseTriggerType = (type: number) => { + // eslint-disable-next-line unicorn/prefer-math-trunc + const scope: TriggerScope = hasMask(type, 1 << 0) ? 'row' : 'statement'; + + let timing: TriggerTiming = 'after'; + const timingMasks: Array<{ mask: number; value: TriggerTiming }> = [ + { mask: 1 << 1, value: 'before' }, + { mask: 1 << 6, value: 'instead of' }, + ]; + + for (const { mask, value } of timingMasks) { + if (hasMask(type, mask)) { + timing = value; + break; + } + } + + const actions: TriggerAction[] = []; + const actionMasks: Array<{ mask: number; value: TriggerAction }> = [ + { mask: 1 << 2, value: 'insert' }, + { mask: 1 << 3, value: 'delete' }, + { mask: 1 << 4, value: 'update' }, + { mask: 1 << 5, value: 'truncate' }, + ]; + + for (const { mask, value } of actionMasks) { + if (hasMask(type, mask)) { + actions.push(value); + break; + } + } + + if (actions.length === 0) { + throw new Error(`Unable to parse trigger type ${type}`); + } + + return { actions, timing, scope }; +}; diff --git a/server/src/sql-tools/from-code/register-enum.ts b/server/src/sql-tools/register-enum.ts similarity index 86% rename from server/src/sql-tools/from-code/register-enum.ts rename to server/src/sql-tools/register-enum.ts index e2415cebff..5e9b41adcb 100644 --- a/server/src/sql-tools/from-code/register-enum.ts +++ b/server/src/sql-tools/register-enum.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; import { DatabaseEnum } from 'src/sql-tools/types'; export type EnumOptions = { diff --git a/server/src/sql-tools/from-code/register-function.ts b/server/src/sql-tools/register-function.ts similarity index 96% rename from server/src/sql-tools/from-code/register-function.ts rename to server/src/sql-tools/register-function.ts index 3e1e7054be..9f1c84c4fa 100644 --- a/server/src/sql-tools/from-code/register-function.ts +++ b/server/src/sql-tools/register-function.ts @@ -1,4 +1,4 @@ -import { register } from 'src/sql-tools/from-code/register'; +import { register } from 'src/sql-tools/register'; import { ColumnType, DatabaseFunction } from 'src/sql-tools/types'; export type FunctionOptions = { diff --git a/server/src/sql-tools/from-code/register-item.ts b/server/src/sql-tools/register-item.ts similarity index 53% rename from server/src/sql-tools/from-code/register-item.ts rename to server/src/sql-tools/register-item.ts index 2f394cf9c1..fede281a1b 100644 --- a/server/src/sql-tools/from-code/register-item.ts +++ b/server/src/sql-tools/register-item.ts @@ -1,17 +1,17 @@ -import { CheckOptions } from 'src/sql-tools/from-code/decorators/check.decorator'; -import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator'; -import { ConfigurationParameterOptions } from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator'; -import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator'; -import { ExtensionOptions } from 'src/sql-tools/from-code/decorators/extension.decorator'; -import { ForeignKeyColumnOptions } from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator'; -import { ForeignKeyConstraintOptions } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator'; -import { IndexOptions } from 'src/sql-tools/from-code/decorators/index.decorator'; -import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator'; -import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator'; -import { UniqueOptions } from 'src/sql-tools/from-code/decorators/unique.decorator'; +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { CheckOptions } from 'src/sql-tools/decorators/check.decorator'; +import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; +import { ConfigurationParameterOptions } from 'src/sql-tools/decorators/configuration-parameter.decorator'; +import { DatabaseOptions } from 'src/sql-tools/decorators/database.decorator'; +import { ExtensionOptions } from 'src/sql-tools/decorators/extension.decorator'; +import { ForeignKeyColumnOptions } from 'src/sql-tools/decorators/foreign-key-column.decorator'; +import { ForeignKeyConstraintOptions } from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; +import { IndexOptions } from 'src/sql-tools/decorators/index.decorator'; +import { TableOptions } from 'src/sql-tools/decorators/table.decorator'; +import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; +import { UniqueOptions } from 'src/sql-tools/decorators/unique.decorator'; import { DatabaseEnum, DatabaseFunction } from 'src/sql-tools/types'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export type ClassBased = { object: Function } & T; export type PropertyBased = { object: object; propertyName: string | symbol } & T; export type RegisterItem = @@ -26,6 +26,6 @@ export type RegisterItem = | { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> } | { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> } | { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> } - | { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> } + | { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => Function }> } | { type: 'foreignKeyConstraint'; item: ClassBased<{ options: ForeignKeyConstraintOptions }> }; export type RegisterItemType = Extract['item']; diff --git a/server/src/sql-tools/from-code/register.ts b/server/src/sql-tools/register.ts similarity index 76% rename from server/src/sql-tools/from-code/register.ts rename to server/src/sql-tools/register.ts index 824af28c52..4df04c935a 100644 --- a/server/src/sql-tools/from-code/register.ts +++ b/server/src/sql-tools/register.ts @@ -1,4 +1,4 @@ -import { RegisterItem } from 'src/sql-tools/from-code/register-item'; +import { RegisterItem } from 'src/sql-tools/register-item'; const items: RegisterItem[] = []; diff --git a/server/src/sql-tools/schema-builder.ts b/server/src/sql-tools/schema-builder.ts new file mode 100644 index 0000000000..e9d82b509a --- /dev/null +++ b/server/src/sql-tools/schema-builder.ts @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { ColumnOptions, TableOptions } from 'src/sql-tools/decorators'; +import { asKey } from 'src/sql-tools/helpers'; +import { + DatabaseColumn, + DatabaseEnum, + DatabaseExtension, + DatabaseFunction, + DatabaseParameter, + DatabaseSchema, + DatabaseTable, + SchemaFromCodeOptions, +} from 'src/sql-tools/types'; + +type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map }; + +export class SchemaBuilder { + databaseName: string; + schemaName: string; + tables: DatabaseTable[] = []; + functions: DatabaseFunction[] = []; + enums: DatabaseEnum[] = []; + extensions: DatabaseExtension[] = []; + parameters: DatabaseParameter[] = []; + warnings: string[] = []; + + classToTable: WeakMap = new WeakMap(); + tableToMetadata: WeakMap = new WeakMap(); + + constructor(options: SchemaFromCodeOptions) { + this.databaseName = options.databaseName ?? 'postgres'; + this.schemaName = options.schemaName ?? 'public'; + } + + getTableByObject(object: Function) { + return this.classToTable.get(object); + } + + getTableByName(name: string) { + return this.tables.find((table) => table.name === name); + } + + getTableMetadata(table: DatabaseTable) { + const metadata = this.tableToMetadata.get(table); + if (!metadata) { + throw new Error(`Table metadata not found for table: ${table.name}`); + } + return metadata; + } + + addTable(table: DatabaseTable, options: TableOptions, object: Function) { + this.tables.push(table); + this.classToTable.set(object, table); + this.tableToMetadata.set(table, { options, object, methodToColumn: new Map() }); + } + + getColumnByObjectAndPropertyName( + object: object, + propertyName: string | symbol, + ): { table?: DatabaseTable; column?: DatabaseColumn } { + const table = this.getTableByObject(object.constructor); + if (!table) { + return {}; + } + + const tableMetadata = this.tableToMetadata.get(table); + if (!tableMetadata) { + return {}; + } + + const column = tableMetadata.methodToColumn.get(propertyName); + + return { table, column }; + } + + addColumn(table: DatabaseTable, column: DatabaseColumn, options: ColumnOptions, propertyName: string | symbol) { + table.columns.push(column); + const tableMetadata = this.getTableMetadata(table); + tableMetadata.methodToColumn.set(propertyName, column); + } + + asIndexName(table: string, columns?: string[], where?: string) { + const items: string[] = []; + for (const columnName of columns ?? []) { + items.push(columnName); + } + + if (where) { + items.push(where); + } + + return asKey('IDX_', table, items); + } + + warn(context: string, message: string) { + this.warnings.push(`[${context}] ${message}`); + } + + warnMissingTable(context: string, object: object, propertyName?: symbol | string) { + const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); + this.warn(context, `Unable to find table (${label})`); + } + + warnMissingColumn(context: string, object: object, propertyName?: symbol | string) { + const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); + this.warn(context, `Unable to find column (${label})`); + } + + build(): DatabaseSchema { + return { + databaseName: this.databaseName, + schemaName: this.schemaName, + tables: this.tables, + functions: this.functions, + enums: this.enums, + extensions: this.extensions, + parameters: this.parameters, + warnings: this.warnings, + }; + } +} diff --git a/server/src/sql-tools/diff/index.spec.ts b/server/src/sql-tools/schema-diff.spec.ts similarity index 90% rename from server/src/sql-tools/diff/index.spec.ts rename to server/src/sql-tools/schema-diff.spec.ts index 7ffd3946f2..98153eabd9 100644 --- a/server/src/sql-tools/diff/index.spec.ts +++ b/server/src/sql-tools/schema-diff.spec.ts @@ -1,10 +1,10 @@ -import { schemaDiff } from 'src/sql-tools/diff'; +import { schemaDiff } from 'src/sql-tools/schema-diff'; import { + ActionType, ColumnType, - DatabaseActionType, + ConstraintType, DatabaseColumn, DatabaseConstraint, - DatabaseConstraintType, DatabaseIndex, DatabaseSchema, DatabaseTable, @@ -15,7 +15,7 @@ const fromColumn = (column: Partial>): Databas const tableName = 'table1'; return { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -49,7 +49,7 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => { const tableName = constraint?.tableName || 'table1'; return { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -82,7 +82,7 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => { const tableName = index?.tableName || 'table1'; return { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -155,7 +155,7 @@ const newSchema = (schema: { } return { - name: 'immich', + databaseName: 'immich', schemaName: schema?.name || 'public', functions: [], enums: [], @@ -166,14 +166,14 @@ const newSchema = (schema: { }; }; -describe('schemaDiff', () => { +describe(schemaDiff.name, () => { it('should work', () => { const diff = schemaDiff(newSchema({ tables: [] }), newSchema({ tables: [] })); expect(diff.items).toEqual([]); }); describe('table', () => { - describe('table.create', () => { + describe('TableCreate', () => { it('should find a missing table', () => { const column: DatabaseColumn = { type: 'character varying', @@ -190,7 +190,7 @@ describe('schemaDiff', () => { expect(diff.items).toHaveLength(1); expect(diff.items[0]).toEqual({ - type: 'table.create', + type: 'TableCreate', table: { name: 'table1', columns: [column], @@ -204,7 +204,7 @@ describe('schemaDiff', () => { }); }); - describe('table.drop', () => { + describe('TableDrop', () => { it('should find an extra table', () => { const diff = schemaDiff( newSchema({ tables: [] }), @@ -216,7 +216,7 @@ describe('schemaDiff', () => { expect(diff.items).toHaveLength(1); expect(diff.items[0]).toEqual({ - type: 'table.drop', + type: 'TableDrop', tableName: 'table1', reason: 'missing in source', }); @@ -238,7 +238,7 @@ describe('schemaDiff', () => { }); describe('column', () => { - describe('column.add', () => { + describe('ColumnAdd', () => { it('should find a new column', () => { const diff = schemaDiff( newSchema({ @@ -256,7 +256,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'column.add', + type: 'ColumnAdd', column: { tableName: 'table1', isArray: false, @@ -271,7 +271,7 @@ describe('schemaDiff', () => { }); }); - describe('column.drop', () => { + describe('ColumnDrop', () => { it('should find an extra column', () => { const diff = schemaDiff( newSchema({ @@ -289,7 +289,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'column.drop', + type: 'ColumnDrop', tableName: 'table1', columnName: 'column2', reason: 'missing in source', @@ -307,7 +307,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'column.alter', + type: 'ColumnAlter', tableName: 'table1', columnName: 'column1', changes: { @@ -326,7 +326,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'column.alter', + type: 'ColumnAlter', tableName: 'table1', columnName: 'column1', changes: { @@ -347,7 +347,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'column.alter', + type: 'ColumnAlter', tableName: 'table1', columnName: 'column1', changes: { @@ -388,12 +388,12 @@ describe('schemaDiff', () => { }); describe('constraint', () => { - describe('constraint.add', () => { + describe('ConstraintAdd', () => { it('should detect a new constraint', () => { const diff = schemaDiff( fromConstraint({ name: 'PK_test', - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, tableName: 'table1', columnNames: ['id'], synchronize: true, @@ -403,9 +403,9 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'constraint.add', + type: 'ConstraintAdd', constraint: { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_test', columnNames: ['id'], tableName: 'table1', @@ -417,13 +417,13 @@ describe('schemaDiff', () => { }); }); - describe('constraint.drop', () => { + describe('ConstraintDrop', () => { it('should detect an extra constraint', () => { const diff = schemaDiff( fromConstraint(), fromConstraint({ name: 'PK_test', - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, tableName: 'table1', columnNames: ['id'], synchronize: true, @@ -432,7 +432,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'constraint.drop', + type: 'ConstraintDrop', tableName: 'table1', constraintName: 'PK_test', reason: 'missing in source', @@ -444,7 +444,7 @@ describe('schemaDiff', () => { describe('primary key', () => { it('should skip identical primary key constraints', () => { const constraint: DatabaseConstraint = { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_test', tableName: 'table1', columnNames: ['id'], @@ -460,7 +460,7 @@ describe('schemaDiff', () => { describe('foreign key', () => { it('should skip identical foreign key constraints', () => { const constraint: DatabaseConstraint = { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_test', tableName: 'table1', columnNames: ['parentId'], @@ -476,7 +476,7 @@ describe('schemaDiff', () => { it('should drop and recreate when the column changes', () => { const constraint: DatabaseConstraint = { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_test', tableName: 'table1', columnNames: ['parentId'], @@ -495,7 +495,7 @@ describe('schemaDiff', () => { constraintName: 'FK_test', reason: 'columns are different (parentId vs parentId2)', tableName: 'table1', - type: 'constraint.drop', + type: 'ConstraintDrop', }, { constraint: { @@ -508,20 +508,20 @@ describe('schemaDiff', () => { type: 'foreign-key', }, reason: 'columns are different (parentId vs parentId2)', - type: 'constraint.add', + type: 'ConstraintAdd', }, ]); }); it('should drop and recreate when the ON DELETE action changes', () => { const constraint: DatabaseConstraint = { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_test', tableName: 'table1', columnNames: ['parentId'], referenceTableName: 'table2', referenceColumnNames: ['id'], - onDelete: DatabaseActionType.CASCADE, + onDelete: ActionType.CASCADE, synchronize: true, }; @@ -532,7 +532,7 @@ describe('schemaDiff', () => { constraintName: 'FK_test', reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', tableName: 'table1', - type: 'constraint.drop', + type: 'ConstraintDrop', }, { constraint: { @@ -540,13 +540,13 @@ describe('schemaDiff', () => { name: 'FK_test', referenceColumnNames: ['id'], referenceTableName: 'table2', - onDelete: DatabaseActionType.CASCADE, + onDelete: ActionType.CASCADE, synchronize: true, tableName: 'table1', type: 'foreign-key', }, reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', - type: 'constraint.add', + type: 'ConstraintAdd', }, ]); }); @@ -555,7 +555,7 @@ describe('schemaDiff', () => { describe('unique', () => { it('should skip identical unique constraints', () => { const constraint: DatabaseConstraint = { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'UQ_test', tableName: 'table1', columnNames: ['id'], @@ -571,7 +571,7 @@ describe('schemaDiff', () => { describe('check', () => { it('should skip identical check constraints', () => { const constraint: DatabaseConstraint = { - type: DatabaseConstraintType.CHECK, + type: ConstraintType.CHECK, name: 'CHK_test', tableName: 'table1', expression: 'column1 > 0', @@ -586,7 +586,7 @@ describe('schemaDiff', () => { }); describe('index', () => { - describe('index.create', () => { + describe('IndexCreate', () => { it('should detect a new index', () => { const diff = schemaDiff( fromIndex({ @@ -601,7 +601,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'index.create', + type: 'IndexCreate', index: { name: 'IDX_test', columnNames: ['id'], @@ -615,7 +615,7 @@ describe('schemaDiff', () => { }); }); - describe('index.drop', () => { + describe('IndexDrop', () => { it('should detect an extra index', () => { const diff = schemaDiff( fromIndex(), @@ -630,7 +630,7 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'index.drop', + type: 'IndexDrop', indexName: 'IDX_test', reason: 'missing in source', }, @@ -650,12 +650,12 @@ describe('schemaDiff', () => { expect(diff.items).toEqual([ { - type: 'index.drop', + type: 'IndexDrop', indexName: 'IDX_test', reason: 'uniqueness is different (true vs false)', }, { - type: 'index.create', + type: 'IndexCreate', index, reason: 'uniqueness is different (true vs false)', }, diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts new file mode 100644 index 0000000000..9708a11c4a --- /dev/null +++ b/server/src/sql-tools/schema-diff.ts @@ -0,0 +1,121 @@ +import { compareEnums } from 'src/sql-tools/comparers/enum.comparer'; +import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer'; +import { compareFunctions } from 'src/sql-tools/comparers/function.comparer'; +import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer'; +import { compareTables } from 'src/sql-tools/comparers/table.comparer'; +import { compare } from 'src/sql-tools/helpers'; +import { transformers } from 'src/sql-tools/transformers'; +import { + ConstraintType, + DatabaseSchema, + SchemaDiff, + SchemaDiffOptions, + SchemaDiffToSqlOptions, +} from 'src/sql-tools/types'; + +/** + * Compute the difference between two database schemas + */ +export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => { + const items = [ + ...compare(source.parameters, target.parameters, options.parameters, compareParameters), + ...compare(source.extensions, target.extensions, options.extension, compareExtensions), + ...compare(source.functions, target.functions, options.functions, compareFunctions), + ...compare(source.enums, target.enums, options.enums, compareEnums), + ...compare(source.tables, target.tables, options.tables, compareTables), + ]; + + type SchemaName = SchemaDiff['type']; + const itemMap: Record = { + EnumCreate: [], + EnumDrop: [], + ExtensionCreate: [], + ExtensionDrop: [], + FunctionCreate: [], + FunctionDrop: [], + TableCreate: [], + TableDrop: [], + ColumnAdd: [], + ColumnAlter: [], + ColumnDrop: [], + ConstraintAdd: [], + ConstraintDrop: [], + IndexCreate: [], + IndexDrop: [], + TriggerCreate: [], + TriggerDrop: [], + ParameterSet: [], + ParameterReset: [], + }; + + for (const item of items) { + itemMap[item.type].push(item); + } + + const constraintAdds = itemMap.ConstraintAdd.filter((item) => item.type === 'ConstraintAdd'); + + const orderedItems = [ + ...itemMap.ExtensionCreate, + ...itemMap.FunctionCreate, + ...itemMap.ParameterSet, + ...itemMap.ParameterReset, + ...itemMap.EnumCreate, + ...itemMap.TriggerDrop, + ...itemMap.IndexDrop, + ...itemMap.ConstraintDrop, + ...itemMap.TableCreate, + ...itemMap.ColumnAlter, + ...itemMap.ColumnAdd, + ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY), + ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY), + ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE), + ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK), + ...itemMap.IndexCreate, + ...itemMap.TriggerCreate, + ...itemMap.ColumnDrop, + ...itemMap.TableDrop, + ...itemMap.EnumDrop, + ...itemMap.FunctionDrop, + ]; + + return { + items: orderedItems, + asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options), + }; +}; + +/** + * Convert schema diffs into SQL statements + */ +export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => { + return items.flatMap((item) => asSql(item).map((result) => result + withComments(options.comments, item))); +}; + +const asSql = (item: SchemaDiff): string[] => { + for (const transform of transformers) { + const result = transform(item); + if (!result) { + continue; + } + + return asArray(result); + } + + throw new Error(`Unhandled schema diff type: ${item.type}`); +}; + +const withComments = (comments: boolean | undefined, item: SchemaDiff): string => { + if (!comments) { + return ''; + } + + return ` -- ${item.reason}`; +}; + +const asArray = (items: T | T[]): T[] => { + if (Array.isArray(items)) { + return items; + } + + return [items]; +}; diff --git a/server/src/sql-tools/schema-from-code.spec.ts b/server/src/sql-tools/schema-from-code.spec.ts new file mode 100644 index 0000000000..55e64e0b4e --- /dev/null +++ b/server/src/sql-tools/schema-from-code.spec.ts @@ -0,0 +1,46 @@ +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { schemaFromCode } from 'src/sql-tools/schema-from-code'; +import { describe, expect, it } from 'vitest'; + +describe(schemaFromCode.name, () => { + it('should work', () => { + expect(schemaFromCode({ reset: true })).toEqual({ + databaseName: 'postgres', + schemaName: 'public', + functions: [], + enums: [], + extensions: [], + parameters: [], + tables: [], + warnings: [], + }); + }); + + describe('test files', () => { + const errorStubs = readdirSync('test/sql-tools/errors', { withFileTypes: true }); + for (const file of errorStubs) { + const filePath = join(file.parentPath, file.name); + it(filePath, async () => { + const module = await import(filePath); + expect(module.message).toBeDefined(); + expect(() => schemaFromCode({ reset: true })).toThrowError(module.message); + }); + } + + const stubs = readdirSync('test/sql-tools', { withFileTypes: true }); + for (const file of stubs) { + if (file.isDirectory()) { + continue; + } + + const filePath = join(file.parentPath, file.name); + it(filePath, async () => { + const module = await import(filePath); + expect(module.description).toBeDefined(); + expect(module.schema).toBeDefined(); + expect(schemaFromCode({ reset: true }), module.description).toEqual(module.schema); + }); + } + }); +}); diff --git a/server/src/sql-tools/schema-from-code.ts b/server/src/sql-tools/schema-from-code.ts new file mode 100644 index 0000000000..f36e0ee07e --- /dev/null +++ b/server/src/sql-tools/schema-from-code.ts @@ -0,0 +1,29 @@ +import { processors } from 'src/sql-tools/processors'; +import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/register'; +import { SchemaBuilder } from 'src/sql-tools/schema-builder'; +import { SchemaFromCodeOptions } from 'src/sql-tools/types'; + +/** + * Load schema from code (decorators, etc) + */ +export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { + try { + const globalOptions = { + createForeignKeyIndexes: options.createForeignKeyIndexes ?? true, + }; + + const builder = new SchemaBuilder(options); + const items = getRegisteredItems(); + for (const processor of processors) { + processor(builder, items, globalOptions); + } + + const newSchema = builder.build(); + + return newSchema; + } finally { + if (options.reset) { + resetRegisteredItems(); + } + } +}; diff --git a/server/src/sql-tools/schema-from-database.ts b/server/src/sql-tools/schema-from-database.ts new file mode 100644 index 0000000000..7122ec1105 --- /dev/null +++ b/server/src/sql-tools/schema-from-database.ts @@ -0,0 +1,33 @@ +import { Kysely } from 'kysely'; +import { PostgresJSDialect } from 'kysely-postgres-js'; +import { Sql } from 'postgres'; +import { readers } from 'src/sql-tools/readers'; +import { DatabaseSchema, PostgresDB, SchemaFromDatabaseOptions } from 'src/sql-tools/types'; + +/** + * Load schema from a database url + */ +export const schemaFromDatabase = async ( + postgres: Sql, + options: SchemaFromDatabaseOptions = {}, +): Promise => { + const schema: DatabaseSchema = { + databaseName: 'immich', + schemaName: options.schemaName || 'public', + parameters: [], + functions: [], + enums: [], + extensions: [], + tables: [], + warnings: [], + }; + + const db = new Kysely({ dialect: new PostgresJSDialect({ postgres }) }); + for (const reader of readers) { + await reader(schema, db); + } + + await db.destroy(); + + return schema; +}; diff --git a/server/src/sql-tools/to-sql/index.spec.ts b/server/src/sql-tools/to-sql/index.spec.ts deleted file mode 100644 index 509f44ebe5..0000000000 --- a/server/src/sql-tools/to-sql/index.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { schemaDiffToSql } from 'src/sql-tools'; -import { describe, expect, it } from 'vitest'; - -describe(schemaDiffToSql.name, () => { - describe('comments', () => { - it('should include the reason in a SQL comment', () => { - expect( - schemaDiffToSql( - [ - { - type: 'index.drop', - indexName: 'IDX_test', - reason: 'unknown', - }, - ], - { comments: true }, - ), - ).toEqual([`DROP INDEX "IDX_test"; -- unknown`]); - }); - }); -}); diff --git a/server/src/sql-tools/to-sql/index.ts b/server/src/sql-tools/to-sql/index.ts deleted file mode 100644 index 973c7ef287..0000000000 --- a/server/src/sql-tools/to-sql/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { transformColumns } from 'src/sql-tools/to-sql/transformers/column.transformer'; -import { transformConstraints } from 'src/sql-tools/to-sql/transformers/constraint.transformer'; -import { transformEnums } from 'src/sql-tools/to-sql/transformers/enum.transformer'; -import { transformExtensions } from 'src/sql-tools/to-sql/transformers/extension.transformer'; -import { transformFunctions } from 'src/sql-tools/to-sql/transformers/function.transformer'; -import { transformIndexes } from 'src/sql-tools/to-sql/transformers/index.transformer'; -import { transformParameters } from 'src/sql-tools/to-sql/transformers/parameter.transformer'; -import { transformTables } from 'src/sql-tools/to-sql/transformers/table.transformer'; -import { transformTriggers } from 'src/sql-tools/to-sql/transformers/trigger.transformer'; -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; -import { SchemaDiff, SchemaDiffToSqlOptions } from 'src/sql-tools/types'; - -/** - * Convert schema diffs into SQL statements - */ -export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => { - return items.flatMap((item) => asSql(item).map((result) => result + withComments(options.comments, item))); -}; - -const transformers: SqlTransformer[] = [ - transformColumns, - transformConstraints, - transformEnums, - transformExtensions, - transformFunctions, - transformIndexes, - transformParameters, - transformTables, - transformTriggers, -]; - -const asSql = (item: SchemaDiff): string[] => { - for (const transform of transformers) { - const result = transform(item); - if (!result) { - continue; - } - - return asArray(result); - } - - throw new Error(`Unhandled schema diff type: ${item.type}`); -}; - -const withComments = (comments: boolean | undefined, item: SchemaDiff): string => { - if (!comments) { - return ''; - } - - return ` -- ${item.reason}`; -}; - -const asArray = (items: T | T[]): T[] => { - if (Array.isArray(items)) { - return items; - } - - return [items]; -}; diff --git a/server/src/sql-tools/to-sql/transformers/column.transformer.spec.ts b/server/src/sql-tools/transformers/column.transformer.spec.ts similarity index 87% rename from server/src/sql-tools/to-sql/transformers/column.transformer.spec.ts rename to server/src/sql-tools/transformers/column.transformer.spec.ts index 8bf5ac3bc4..2f975381aa 100644 --- a/server/src/sql-tools/to-sql/transformers/column.transformer.spec.ts +++ b/server/src/sql-tools/transformers/column.transformer.spec.ts @@ -1,12 +1,12 @@ -import { transformColumns } from 'src/sql-tools/to-sql/transformers/column.transformer'; +import { transformColumns } from 'src/sql-tools/transformers/column.transformer'; import { describe, expect, it } from 'vitest'; describe(transformColumns.name, () => { - describe('column.add', () => { + describe('ColumnAdd', () => { it('should work', () => { expect( transformColumns({ - type: 'column.add', + type: 'ColumnAdd', column: { name: 'column1', tableName: 'table1', @@ -23,7 +23,7 @@ describe(transformColumns.name, () => { it('should add a nullable column', () => { expect( transformColumns({ - type: 'column.add', + type: 'ColumnAdd', column: { name: 'column1', tableName: 'table1', @@ -40,7 +40,7 @@ describe(transformColumns.name, () => { it('should add a column with an enum type', () => { expect( transformColumns({ - type: 'column.add', + type: 'ColumnAdd', column: { name: 'column1', tableName: 'table1', @@ -58,7 +58,7 @@ describe(transformColumns.name, () => { it('should add a column that is an array type', () => { expect( transformColumns({ - type: 'column.add', + type: 'ColumnAdd', column: { name: 'column1', tableName: 'table1', @@ -73,11 +73,11 @@ describe(transformColumns.name, () => { }); }); - describe('column.alter', () => { + describe('ColumnAlter', () => { it('should make a column nullable', () => { expect( transformColumns({ - type: 'column.alter', + type: 'ColumnAlter', tableName: 'table1', columnName: 'column1', changes: { nullable: true }, @@ -89,7 +89,7 @@ describe(transformColumns.name, () => { it('should make a column non-nullable', () => { expect( transformColumns({ - type: 'column.alter', + type: 'ColumnAlter', tableName: 'table1', columnName: 'column1', changes: { nullable: false }, @@ -101,7 +101,7 @@ describe(transformColumns.name, () => { it('should update the default value', () => { expect( transformColumns({ - type: 'column.alter', + type: 'ColumnAlter', tableName: 'table1', columnName: 'column1', changes: { default: 'uuid_generate_v4()' }, @@ -111,11 +111,11 @@ describe(transformColumns.name, () => { }); }); - describe('column.drop', () => { + describe('ColumnDrop', () => { it('should work', () => { expect( transformColumns({ - type: 'column.drop', + type: 'ColumnDrop', tableName: 'table1', columnName: 'column1', reason: 'unknown', diff --git a/server/src/sql-tools/to-sql/transformers/column.transformer.ts b/server/src/sql-tools/transformers/column.transformer.ts similarity index 91% rename from server/src/sql-tools/to-sql/transformers/column.transformer.ts rename to server/src/sql-tools/transformers/column.transformer.ts index 117b460938..ab1135379c 100644 --- a/server/src/sql-tools/to-sql/transformers/column.transformer.ts +++ b/server/src/sql-tools/transformers/column.transformer.ts @@ -1,18 +1,18 @@ import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { ColumnChanges, DatabaseColumn, SchemaDiff } from 'src/sql-tools/types'; export const transformColumns: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'column.add': { + case 'ColumnAdd': { return asColumnAdd(item.column); } - case 'column.alter': { + case 'ColumnAlter': { return asColumnAlter(item.tableName, item.columnName, item.changes); } - case 'column.drop': { + case 'ColumnDrop': { return asColumnDrop(item.tableName, item.columnName); } diff --git a/server/src/sql-tools/to-sql/transformers/constraint.transformer.spec.ts b/server/src/sql-tools/transformers/constraint.transformer.spec.ts similarity index 78% rename from server/src/sql-tools/to-sql/transformers/constraint.transformer.spec.ts rename to server/src/sql-tools/transformers/constraint.transformer.spec.ts index 59d21e7b50..595db10a39 100644 --- a/server/src/sql-tools/to-sql/transformers/constraint.transformer.spec.ts +++ b/server/src/sql-tools/transformers/constraint.transformer.spec.ts @@ -1,16 +1,16 @@ -import { transformConstraints } from 'src/sql-tools/to-sql/transformers/constraint.transformer'; -import { DatabaseConstraintType } from 'src/sql-tools/types'; +import { transformConstraints } from 'src/sql-tools/transformers/constraint.transformer'; +import { ConstraintType } from 'src/sql-tools/types'; import { describe, expect, it } from 'vitest'; describe(transformConstraints.name, () => { - describe('constraint.add', () => { + describe('ConstraintAdd', () => { describe('primary keys', () => { it('should work', () => { expect( transformConstraints({ - type: 'constraint.add', + type: 'ConstraintAdd', constraint: { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_test', tableName: 'table1', columnNames: ['id'], @@ -26,9 +26,9 @@ describe(transformConstraints.name, () => { it('should work', () => { expect( transformConstraints({ - type: 'constraint.add', + type: 'ConstraintAdd', constraint: { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_test', tableName: 'table1', columnNames: ['parentId'], @@ -48,9 +48,9 @@ describe(transformConstraints.name, () => { it('should work', () => { expect( transformConstraints({ - type: 'constraint.add', + type: 'ConstraintAdd', constraint: { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'UQ_test', tableName: 'table1', columnNames: ['id'], @@ -66,9 +66,9 @@ describe(transformConstraints.name, () => { it('should work', () => { expect( transformConstraints({ - type: 'constraint.add', + type: 'ConstraintAdd', constraint: { - type: DatabaseConstraintType.CHECK, + type: ConstraintType.CHECK, name: 'CHK_test', tableName: 'table1', expression: '"id" IS NOT NULL', @@ -81,11 +81,11 @@ describe(transformConstraints.name, () => { }); }); - describe('constraint.drop', () => { + describe('ConstraintDrop', () => { it('should work', () => { expect( transformConstraints({ - type: 'constraint.drop', + type: 'ConstraintDrop', tableName: 'table1', constraintName: 'PK_test', reason: 'unknown', diff --git a/server/src/sql-tools/to-sql/transformers/constraint.transformer.ts b/server/src/sql-tools/transformers/constraint.transformer.ts similarity index 68% rename from server/src/sql-tools/to-sql/transformers/constraint.transformer.ts rename to server/src/sql-tools/transformers/constraint.transformer.ts index ec65143eba..d9c2fcd8d1 100644 --- a/server/src/sql-tools/to-sql/transformers/constraint.transformer.ts +++ b/server/src/sql-tools/transformers/constraint.transformer.ts @@ -1,14 +1,14 @@ import { asColumnList } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; -import { DatabaseActionType, DatabaseConstraint, DatabaseConstraintType, SchemaDiff } from 'src/sql-tools/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; +import { ActionType, ConstraintType, DatabaseConstraint, SchemaDiff } from 'src/sql-tools/types'; export const transformConstraints: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'constraint.add': { + case 'ConstraintAdd': { return asConstraintAdd(item.constraint); } - case 'constraint.drop': { + case 'ConstraintDrop': { return asConstraintDrop(item.tableName, item.constraintName); } default: { @@ -17,18 +17,18 @@ export const transformConstraints: SqlTransformer = (item: SchemaDiff) => { } }; -const withAction = (constraint: { onDelete?: DatabaseActionType; onUpdate?: DatabaseActionType }) => - ` ON UPDATE ${constraint.onUpdate ?? DatabaseActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? DatabaseActionType.NO_ACTION}`; +const withAction = (constraint: { onDelete?: ActionType; onUpdate?: ActionType }) => + ` ON UPDATE ${constraint.onUpdate ?? ActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? ActionType.NO_ACTION}`; export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => { const base = `ALTER TABLE "${constraint.tableName}" ADD CONSTRAINT "${constraint.name}"`; switch (constraint.type) { - case DatabaseConstraintType.PRIMARY_KEY: { + case ConstraintType.PRIMARY_KEY: { const columnNames = asColumnList(constraint.columnNames); return `${base} PRIMARY KEY (${columnNames});`; } - case DatabaseConstraintType.FOREIGN_KEY: { + case ConstraintType.FOREIGN_KEY: { const columnNames = asColumnList(constraint.columnNames); const referenceColumnNames = asColumnList(constraint.referenceColumnNames); return ( @@ -38,12 +38,12 @@ export const asConstraintAdd = (constraint: DatabaseConstraint): string | string ); } - case DatabaseConstraintType.UNIQUE: { + case ConstraintType.UNIQUE: { const columnNames = asColumnList(constraint.columnNames); return `${base} UNIQUE (${columnNames});`; } - case DatabaseConstraintType.CHECK: { + case ConstraintType.CHECK: { return `${base} CHECK (${constraint.expression});`; } diff --git a/server/src/sql-tools/to-sql/transformers/enum.transformer.ts b/server/src/sql-tools/transformers/enum.transformer.ts similarity index 81% rename from server/src/sql-tools/to-sql/transformers/enum.transformer.ts rename to server/src/sql-tools/transformers/enum.transformer.ts index d5764d9b16..b012ff1a56 100644 --- a/server/src/sql-tools/to-sql/transformers/enum.transformer.ts +++ b/server/src/sql-tools/transformers/enum.transformer.ts @@ -1,13 +1,13 @@ -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseEnum, SchemaDiff } from 'src/sql-tools/types'; export const transformEnums: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'enum.create': { + case 'EnumCreate': { return asEnumCreate(item.enum); } - case 'enum.drop': { + case 'EnumDrop': { return asEnumDrop(item.enumName); } diff --git a/server/src/sql-tools/to-sql/transformers/extension.transformer.spec.ts b/server/src/sql-tools/transformers/extension.transformer.spec.ts similarity index 70% rename from server/src/sql-tools/to-sql/transformers/extension.transformer.spec.ts rename to server/src/sql-tools/transformers/extension.transformer.spec.ts index 81b2db4d27..655b6ad20d 100644 --- a/server/src/sql-tools/to-sql/transformers/extension.transformer.spec.ts +++ b/server/src/sql-tools/transformers/extension.transformer.spec.ts @@ -1,12 +1,12 @@ -import { transformExtensions } from 'src/sql-tools/to-sql/transformers/extension.transformer'; +import { transformExtensions } from 'src/sql-tools/transformers/extension.transformer'; import { describe, expect, it } from 'vitest'; describe(transformExtensions.name, () => { - describe('extension.drop', () => { + describe('ExtensionDrop', () => { it('should work', () => { expect( transformExtensions({ - type: 'extension.drop', + type: 'ExtensionDrop', extensionName: 'cube', reason: 'unknown', }), @@ -14,11 +14,11 @@ describe(transformExtensions.name, () => { }); }); - describe('extension.create', () => { + describe('ExtensionCreate', () => { it('should work', () => { expect( transformExtensions({ - type: 'extension.create', + type: 'ExtensionCreate', extension: { name: 'cube', synchronize: true, diff --git a/server/src/sql-tools/to-sql/transformers/extension.transformer.ts b/server/src/sql-tools/transformers/extension.transformer.ts similarity index 81% rename from server/src/sql-tools/to-sql/transformers/extension.transformer.ts rename to server/src/sql-tools/transformers/extension.transformer.ts index 2d51a26444..df1ee7da97 100644 --- a/server/src/sql-tools/to-sql/transformers/extension.transformer.ts +++ b/server/src/sql-tools/transformers/extension.transformer.ts @@ -1,13 +1,13 @@ -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseExtension, SchemaDiff } from 'src/sql-tools/types'; export const transformExtensions: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'extension.create': { + case 'ExtensionCreate': { return asExtensionCreate(item.extension); } - case 'extension.drop': { + case 'ExtensionDrop': { return asExtensionDrop(item.extensionName); } diff --git a/server/src/sql-tools/to-sql/transformers/function.transformer.spec.ts b/server/src/sql-tools/transformers/function.transformer.spec.ts similarity index 65% rename from server/src/sql-tools/to-sql/transformers/function.transformer.spec.ts rename to server/src/sql-tools/transformers/function.transformer.spec.ts index 6e9a5bac56..b24256cb38 100644 --- a/server/src/sql-tools/to-sql/transformers/function.transformer.spec.ts +++ b/server/src/sql-tools/transformers/function.transformer.spec.ts @@ -1,12 +1,12 @@ -import { transformFunctions } from 'src/sql-tools/to-sql/transformers/function.transformer'; +import { transformFunctions } from 'src/sql-tools/transformers/function.transformer'; import { describe, expect, it } from 'vitest'; describe(transformFunctions.name, () => { - describe('function.drop', () => { + describe('FunctionDrop', () => { it('should work', () => { expect( transformFunctions({ - type: 'function.drop', + type: 'FunctionDrop', functionName: 'test_func', reason: 'unknown', }), diff --git a/server/src/sql-tools/to-sql/transformers/function.transformer.ts b/server/src/sql-tools/transformers/function.transformer.ts similarity index 79% rename from server/src/sql-tools/to-sql/transformers/function.transformer.ts rename to server/src/sql-tools/transformers/function.transformer.ts index f05eca099a..01915349f0 100644 --- a/server/src/sql-tools/to-sql/transformers/function.transformer.ts +++ b/server/src/sql-tools/transformers/function.transformer.ts @@ -1,13 +1,13 @@ -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseFunction, SchemaDiff } from 'src/sql-tools/types'; export const transformFunctions: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'function.create': { + case 'FunctionCreate': { return asFunctionCreate(item.function); } - case 'function.drop': { + case 'FunctionDrop': { return asFunctionDrop(item.functionName); } diff --git a/server/src/sql-tools/to-sql/transformers/index.transformer.spec.ts b/server/src/sql-tools/transformers/index.transformer.spec.ts similarity index 87% rename from server/src/sql-tools/to-sql/transformers/index.transformer.spec.ts rename to server/src/sql-tools/transformers/index.transformer.spec.ts index af3cc0286c..0599b00bdd 100644 --- a/server/src/sql-tools/to-sql/transformers/index.transformer.spec.ts +++ b/server/src/sql-tools/transformers/index.transformer.spec.ts @@ -1,12 +1,12 @@ -import { transformIndexes } from 'src/sql-tools/to-sql/transformers/index.transformer'; +import { transformIndexes } from 'src/sql-tools/transformers/index.transformer'; import { describe, expect, it } from 'vitest'; describe(transformIndexes.name, () => { - describe('index.create', () => { + describe('IndexCreate', () => { it('should work', () => { expect( transformIndexes({ - type: 'index.create', + type: 'IndexCreate', index: { name: 'IDX_test', tableName: 'table1', @@ -22,7 +22,7 @@ describe(transformIndexes.name, () => { it('should create an unique index', () => { expect( transformIndexes({ - type: 'index.create', + type: 'IndexCreate', index: { name: 'IDX_test', tableName: 'table1', @@ -38,7 +38,7 @@ describe(transformIndexes.name, () => { it('should create an index with a custom expression', () => { expect( transformIndexes({ - type: 'index.create', + type: 'IndexCreate', index: { name: 'IDX_test', tableName: 'table1', @@ -54,7 +54,7 @@ describe(transformIndexes.name, () => { it('should create an index with a where clause', () => { expect( transformIndexes({ - type: 'index.create', + type: 'IndexCreate', index: { name: 'IDX_test', tableName: 'table1', @@ -71,7 +71,7 @@ describe(transformIndexes.name, () => { it('should create an index with a custom expression', () => { expect( transformIndexes({ - type: 'index.create', + type: 'IndexCreate', index: { name: 'IDX_test', tableName: 'table1', @@ -86,11 +86,11 @@ describe(transformIndexes.name, () => { }); }); - describe('index.drop', () => { + describe('IndexDrop', () => { it('should work', () => { expect( transformIndexes({ - type: 'index.drop', + type: 'IndexDrop', indexName: 'IDX_test', reason: 'unknown', }), diff --git a/server/src/sql-tools/to-sql/transformers/index.transformer.ts b/server/src/sql-tools/transformers/index.transformer.ts similarity index 89% rename from server/src/sql-tools/to-sql/transformers/index.transformer.ts rename to server/src/sql-tools/transformers/index.transformer.ts index 73d9ac9615..001b15315f 100644 --- a/server/src/sql-tools/to-sql/transformers/index.transformer.ts +++ b/server/src/sql-tools/transformers/index.transformer.ts @@ -1,14 +1,14 @@ import { asColumnList } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseIndex, SchemaDiff } from 'src/sql-tools/types'; export const transformIndexes: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'index.create': { + case 'IndexCreate': { return asIndexCreate(item.index); } - case 'index.drop': { + case 'IndexDrop': { return asIndexDrop(item.indexName); } diff --git a/server/src/sql-tools/transformers/index.ts b/server/src/sql-tools/transformers/index.ts new file mode 100644 index 0000000000..2865a376d7 --- /dev/null +++ b/server/src/sql-tools/transformers/index.ts @@ -0,0 +1,22 @@ +import { transformColumns } from 'src/sql-tools/transformers/column.transformer'; +import { transformConstraints } from 'src/sql-tools/transformers/constraint.transformer'; +import { transformEnums } from 'src/sql-tools/transformers/enum.transformer'; +import { transformExtensions } from 'src/sql-tools/transformers/extension.transformer'; +import { transformFunctions } from 'src/sql-tools/transformers/function.transformer'; +import { transformIndexes } from 'src/sql-tools/transformers/index.transformer'; +import { transformParameters } from 'src/sql-tools/transformers/parameter.transformer'; +import { transformTables } from 'src/sql-tools/transformers/table.transformer'; +import { transformTriggers } from 'src/sql-tools/transformers/trigger.transformer'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; + +export const transformers: SqlTransformer[] = [ + transformColumns, + transformConstraints, + transformEnums, + transformExtensions, + transformFunctions, + transformIndexes, + transformParameters, + transformTables, + transformTriggers, +]; diff --git a/server/src/sql-tools/to-sql/transformers/parameter.transformer.ts b/server/src/sql-tools/transformers/parameter.transformer.ts similarity index 85% rename from server/src/sql-tools/to-sql/transformers/parameter.transformer.ts rename to server/src/sql-tools/transformers/parameter.transformer.ts index 0b12cdb27b..7768298d1d 100644 --- a/server/src/sql-tools/to-sql/transformers/parameter.transformer.ts +++ b/server/src/sql-tools/transformers/parameter.transformer.ts @@ -1,13 +1,13 @@ -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseParameter, SchemaDiff } from 'src/sql-tools/types'; export const transformParameters: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'parameter.set': { + case 'ParameterSet': { return asParameterSet(item.parameter); } - case 'parameter.reset': { + case 'ParameterReset': { return asParameterReset(item.databaseName, item.parameterName); } diff --git a/server/src/sql-tools/to-sql/transformers/table.transformer.spec.ts b/server/src/sql-tools/transformers/table.transformer.spec.ts similarity index 91% rename from server/src/sql-tools/to-sql/transformers/table.transformer.spec.ts rename to server/src/sql-tools/transformers/table.transformer.spec.ts index db3ffa22ec..c6f20b3da3 100644 --- a/server/src/sql-tools/to-sql/transformers/table.transformer.spec.ts +++ b/server/src/sql-tools/transformers/table.transformer.spec.ts @@ -1,12 +1,12 @@ -import { transformTables } from 'src/sql-tools/to-sql/transformers/table.transformer'; +import { transformTables } from 'src/sql-tools/transformers/table.transformer'; import { describe, expect, it } from 'vitest'; describe(transformTables.name, () => { - describe('table.drop', () => { + describe('TableDrop', () => { it('should work', () => { expect( transformTables({ - type: 'table.drop', + type: 'TableDrop', tableName: 'table1', reason: 'unknown', }), @@ -14,11 +14,11 @@ describe(transformTables.name, () => { }); }); - describe('table.create', () => { + describe('TableCreate', () => { it('should work', () => { expect( transformTables({ - type: 'table.create', + type: 'TableCreate', table: { name: 'table1', columns: [ @@ -44,7 +44,7 @@ describe(transformTables.name, () => { it('should handle a non-nullable column', () => { expect( transformTables({ - type: 'table.create', + type: 'TableCreate', table: { name: 'table1', columns: [ @@ -70,7 +70,7 @@ describe(transformTables.name, () => { it('should handle a default value', () => { expect( transformTables({ - type: 'table.create', + type: 'TableCreate', table: { name: 'table1', columns: [ @@ -97,7 +97,7 @@ describe(transformTables.name, () => { it('should handle a string with a fixed length', () => { expect( transformTables({ - type: 'table.create', + type: 'TableCreate', table: { name: 'table1', columns: [ @@ -124,7 +124,7 @@ describe(transformTables.name, () => { it('should handle an array type', () => { expect( transformTables({ - type: 'table.create', + type: 'TableCreate', table: { name: 'table1', columns: [ diff --git a/server/src/sql-tools/to-sql/transformers/table.transformer.ts b/server/src/sql-tools/transformers/table.transformer.ts similarity index 83% rename from server/src/sql-tools/to-sql/transformers/table.transformer.ts rename to server/src/sql-tools/transformers/table.transformer.ts index f376b65274..b1fefab3e0 100644 --- a/server/src/sql-tools/to-sql/transformers/table.transformer.ts +++ b/server/src/sql-tools/transformers/table.transformer.ts @@ -1,15 +1,15 @@ import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers'; -import { asColumnAlter } from 'src/sql-tools/to-sql/transformers/column.transformer'; -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { asColumnAlter } from 'src/sql-tools/transformers/column.transformer'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseTable, SchemaDiff } from 'src/sql-tools/types'; export const transformTables: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'table.create': { + case 'TableCreate': { return asTableCreate(item.table); } - case 'table.drop': { + case 'TableDrop': { return asTableDrop(item.tableName); } diff --git a/server/src/sql-tools/to-sql/transformers/trigger.transformer.spec.ts b/server/src/sql-tools/transformers/trigger.transformer.spec.ts similarity index 87% rename from server/src/sql-tools/to-sql/transformers/trigger.transformer.spec.ts rename to server/src/sql-tools/transformers/trigger.transformer.spec.ts index 778de88cba..9fdb8d1c23 100644 --- a/server/src/sql-tools/to-sql/transformers/trigger.transformer.spec.ts +++ b/server/src/sql-tools/transformers/trigger.transformer.spec.ts @@ -1,12 +1,12 @@ -import { transformTriggers } from 'src/sql-tools/to-sql/transformers/trigger.transformer'; +import { transformTriggers } from 'src/sql-tools/transformers/trigger.transformer'; import { describe, expect, it } from 'vitest'; describe(transformTriggers.name, () => { - describe('trigger.create', () => { + describe('TriggerCreate', () => { it('should work', () => { expect( transformTriggers({ - type: 'trigger.create', + type: 'TriggerCreate', trigger: { name: 'trigger1', tableName: 'table1', @@ -29,7 +29,7 @@ describe(transformTriggers.name, () => { it('should work with multiple actions', () => { expect( transformTriggers({ - type: 'trigger.create', + type: 'TriggerCreate', trigger: { name: 'trigger1', tableName: 'table1', @@ -52,7 +52,7 @@ describe(transformTriggers.name, () => { it('should work with old/new reference table aliases', () => { expect( transformTriggers({ - type: 'trigger.create', + type: 'TriggerCreate', trigger: { name: 'trigger1', tableName: 'table1', @@ -76,11 +76,11 @@ describe(transformTriggers.name, () => { }); }); - describe('trigger.drop', () => { + describe('TriggerDrop', () => { it('should work', () => { expect( transformTriggers({ - type: 'trigger.drop', + type: 'TriggerDrop', tableName: 'table1', triggerName: 'trigger1', reason: 'unknown', diff --git a/server/src/sql-tools/to-sql/transformers/trigger.transformer.ts b/server/src/sql-tools/transformers/trigger.transformer.ts similarity index 91% rename from server/src/sql-tools/to-sql/transformers/trigger.transformer.ts rename to server/src/sql-tools/transformers/trigger.transformer.ts index c104a2ed6b..167449e249 100644 --- a/server/src/sql-tools/to-sql/transformers/trigger.transformer.ts +++ b/server/src/sql-tools/transformers/trigger.transformer.ts @@ -1,13 +1,13 @@ -import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types'; +import { SqlTransformer } from 'src/sql-tools/transformers/types'; import { DatabaseTrigger, SchemaDiff } from 'src/sql-tools/types'; export const transformTriggers: SqlTransformer = (item: SchemaDiff) => { switch (item.type) { - case 'trigger.create': { + case 'TriggerCreate': { return asTriggerCreate(item.trigger); } - case 'trigger.drop': { + case 'TriggerDrop': { return asTriggerDrop(item.tableName, item.triggerName); } diff --git a/server/src/sql-tools/to-sql/transformers/types.ts b/server/src/sql-tools/transformers/types.ts similarity index 100% rename from server/src/sql-tools/to-sql/transformers/types.ts rename to server/src/sql-tools/transformers/types.ts diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts index 0641487e0e..a3aec7a061 100644 --- a/server/src/sql-tools/types.ts +++ b/server/src/sql-tools/types.ts @@ -1,4 +1,38 @@ import { Kysely, ColumnType as KyselyColumnType } from 'kysely'; +import { RegisterItem } from 'src/sql-tools/register-item'; +import { SchemaBuilder } from 'src/sql-tools/schema-builder'; + +export type SchemaFromCodeOptions = { + /** automatically create indexes on foreign key columns */ + createForeignKeyIndexes?: boolean; + databaseName?: string; + schemaName?: string; + reset?: boolean; +}; + +export type SchemaFromDatabaseOptions = { + schemaName?: string; +}; + +export type SchemaDiffToSqlOptions = { + comments?: boolean; +}; + +export type SchemaDiffOptions = { + tables?: IgnoreOptions; + functions?: IgnoreOptions; + enums?: IgnoreOptions; + extension?: IgnoreOptions; + parameters?: IgnoreOptions; +}; + +export type IgnoreOptions = { + ignoreExtra?: boolean; + ignoreMissing?: boolean; +}; + +export type Processor = (builder: SchemaBuilder, items: RegisterItem[], options: SchemaFromCodeOptions) => void; +export type DatabaseReader = (schema: DatabaseSchema, db: DatabaseClient) => Promise; export type PostgresDB = { pg_am: { @@ -237,14 +271,14 @@ type PostgresYesOrNo = 'YES' | 'NO'; export type DatabaseClient = Kysely; -export enum DatabaseConstraintType { +export enum ConstraintType { PRIMARY_KEY = 'primary-key', FOREIGN_KEY = 'foreign-key', UNIQUE = 'unique', CHECK = 'check', } -export enum DatabaseActionType { +export enum ActionType { NO_ACTION = 'NO ACTION', RESTRICT = 'RESTRICT', CASCADE = 'CASCADE', @@ -278,7 +312,7 @@ export type ColumnType = | 'serial'; export type DatabaseSchema = { - name: string; + databaseName: string; schemaName: string; functions: DatabaseFunction[]; enums: DatabaseEnum[]; @@ -288,19 +322,6 @@ export type DatabaseSchema = { warnings: string[]; }; -export type SchemaDiffOptions = { - tables?: DiffOptions; - functions?: DiffOptions; - enums?: DiffOptions; - extension?: DiffOptions; - parameters?: DiffOptions; -}; - -export type DiffOptions = { - ignoreExtra?: boolean; - ignoreMissing?: boolean; -}; - export type DatabaseParameter = { name: string; databaseName: string; @@ -381,26 +402,26 @@ type ColumBasedConstraint = { }; export type DatabasePrimaryKeyConstraint = ColumBasedConstraint & { - type: DatabaseConstraintType.PRIMARY_KEY; + type: ConstraintType.PRIMARY_KEY; synchronize: boolean; }; export type DatabaseUniqueConstraint = ColumBasedConstraint & { - type: DatabaseConstraintType.UNIQUE; + type: ConstraintType.UNIQUE; synchronize: boolean; }; export type DatabaseForeignKeyConstraint = ColumBasedConstraint & { - type: DatabaseConstraintType.FOREIGN_KEY; + type: ConstraintType.FOREIGN_KEY; referenceTableName: string; referenceColumnNames: string[]; - onUpdate?: DatabaseActionType; - onDelete?: DatabaseActionType; + onUpdate?: ActionType; + onDelete?: ActionType; synchronize: boolean; }; export type DatabaseCheckConstraint = { - type: DatabaseConstraintType.CHECK; + type: ConstraintType.CHECK; name: string; tableName: string; expression: string; @@ -435,34 +456,26 @@ export type DatabaseIndex = { synchronize: boolean; }; -export type LoadSchemaOptions = { - schemaName?: string; -}; - -export type SchemaDiffToSqlOptions = { - comments?: boolean; -}; - export type SchemaDiff = { reason: string } & ( - | { type: 'extension.create'; extension: DatabaseExtension } - | { type: 'extension.drop'; extensionName: string } - | { type: 'function.create'; function: DatabaseFunction } - | { type: 'function.drop'; functionName: string } - | { type: 'table.create'; table: DatabaseTable } - | { type: 'table.drop'; tableName: string } - | { type: 'column.add'; column: DatabaseColumn } - | { type: 'column.alter'; tableName: string; columnName: string; changes: ColumnChanges } - | { type: 'column.drop'; tableName: string; columnName: string } - | { type: 'constraint.add'; constraint: DatabaseConstraint } - | { type: 'constraint.drop'; tableName: string; constraintName: string } - | { type: 'index.create'; index: DatabaseIndex } - | { type: 'index.drop'; indexName: string } - | { type: 'trigger.create'; trigger: DatabaseTrigger } - | { type: 'trigger.drop'; tableName: string; triggerName: string } - | { type: 'parameter.set'; parameter: DatabaseParameter } - | { type: 'parameter.reset'; databaseName: string; parameterName: string } - | { type: 'enum.create'; enum: DatabaseEnum } - | { type: 'enum.drop'; enumName: string } + | { type: 'ExtensionCreate'; extension: DatabaseExtension } + | { type: 'ExtensionDrop'; extensionName: string } + | { type: 'FunctionCreate'; function: DatabaseFunction } + | { type: 'FunctionDrop'; functionName: string } + | { type: 'TableCreate'; table: DatabaseTable } + | { type: 'TableDrop'; tableName: string } + | { type: 'ColumnAdd'; column: DatabaseColumn } + | { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges } + | { type: 'ColumnDrop'; tableName: string; columnName: string } + | { type: 'ConstraintAdd'; constraint: DatabaseConstraint } + | { type: 'ConstraintDrop'; tableName: string; constraintName: string } + | { type: 'IndexCreate'; index: DatabaseIndex } + | { type: 'IndexDrop'; indexName: string } + | { type: 'TriggerCreate'; trigger: DatabaseTrigger } + | { type: 'TriggerDrop'; tableName: string; triggerName: string } + | { type: 'ParameterSet'; parameter: DatabaseParameter } + | { type: 'ParameterReset'; databaseName: string; parameterName: string } + | { type: 'EnumCreate'; enum: DatabaseEnum } + | { type: 'EnumDrop'; enumName: string } ); export type CompareFunction = (source: T, target: T) => SchemaDiff[]; diff --git a/server/test/sql-tools/check-constraint-default-name.stub.ts b/server/test/sql-tools/check-constraint-default-name.stub.ts index af03e02a2e..4ed0e6ad3a 100644 --- a/server/test/sql-tools/check-constraint-default-name.stub.ts +++ b/server/test/sql-tools/check-constraint-default-name.stub.ts @@ -1,4 +1,4 @@ -import { Check, Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; +import { Check, Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; @Table() @Check({ expression: '1=1' }) @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should create a check constraint with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -33,7 +33,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.CHECK, + type: ConstraintType.CHECK, name: 'CHK_8d2ecfd49b984941f6b2589799', tableName: 'table1', expression: '1=1', diff --git a/server/test/sql-tools/check-constraint-override-name.stub.ts b/server/test/sql-tools/check-constraint-override-name.stub.ts index b30025e2fc..1dfcfe7566 100644 --- a/server/test/sql-tools/check-constraint-override-name.stub.ts +++ b/server/test/sql-tools/check-constraint-override-name.stub.ts @@ -1,4 +1,4 @@ -import { Check, Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; +import { Check, Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; @Table() @Check({ name: 'CHK_test', expression: '1=1' }) @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should create a check constraint with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -33,7 +33,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.CHECK, + type: ConstraintType.CHECK, name: 'CHK_test', tableName: 'table1', expression: '1=1', diff --git a/server/test/sql-tools/column-create-date.stub.ts b/server/test/sql-tools/column-create-date.stub.ts index 7a284c674c..42fe3f98ae 100644 --- a/server/test/sql-tools/column-create-date.stub.ts +++ b/server/test/sql-tools/column-create-date.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with an created at date column'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-default-boolean.stub.ts b/server/test/sql-tools/column-default-boolean.stub.ts index 962b023a25..eafefc524e 100644 --- a/server/test/sql-tools/column-default-boolean.stub.ts +++ b/server/test/sql-tools/column-default-boolean.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a default value (boolean)'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-default-date.stub.ts b/server/test/sql-tools/column-default-date.stub.ts index 00f2db2c27..b1998b47b2 100644 --- a/server/test/sql-tools/column-default-date.stub.ts +++ b/server/test/sql-tools/column-default-date.stub.ts @@ -10,7 +10,7 @@ export class Table1 { export const description = 'should register a table with a column with a default value (date)'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-default-function.stub.ts b/server/test/sql-tools/column-default-function.stub.ts index b13bd14c93..57579cb230 100644 --- a/server/test/sql-tools/column-default-function.stub.ts +++ b/server/test/sql-tools/column-default-function.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a default function'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-default-null.stub.ts b/server/test/sql-tools/column-default-null.stub.ts index c88ed218b3..869d02cf94 100644 --- a/server/test/sql-tools/column-default-null.stub.ts +++ b/server/test/sql-tools/column-default-null.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a nullable column from a default of null'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-default-number.stub.ts b/server/test/sql-tools/column-default-number.stub.ts index 36d0af5273..afec0da8fb 100644 --- a/server/test/sql-tools/column-default-number.stub.ts +++ b/server/test/sql-tools/column-default-number.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a default value (number)'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-default-string.stub.ts b/server/test/sql-tools/column-default-string.stub.ts index 04a00a4dfe..4b989a3f3b 100644 --- a/server/test/sql-tools/column-default-string.stub.ts +++ b/server/test/sql-tools/column-default-string.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a default value (string)'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-delete-date.stub.ts b/server/test/sql-tools/column-delete-date.stub.ts index facbfb0328..437407e33a 100644 --- a/server/test/sql-tools/column-delete-date.stub.ts +++ b/server/test/sql-tools/column-delete-date.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a deleted at date column'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-enum-type.stub.ts b/server/test/sql-tools/column-enum-type.stub.ts index 878910dcdb..02fbeeabde 100644 --- a/server/test/sql-tools/column-enum-type.stub.ts +++ b/server/test/sql-tools/column-enum-type.stub.ts @@ -15,7 +15,7 @@ export class Table1 { export const description = 'should accept an enum type'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [ diff --git a/server/test/sql-tools/column-generated-identity.ts b/server/test/sql-tools/column-generated-identity.ts index 98b0f582a6..a6525fe9ac 100644 --- a/server/test/sql-tools/column-generated-identity.ts +++ b/server/test/sql-tools/column-generated-identity.ts @@ -1,4 +1,4 @@ -import { DatabaseConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +import { ConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a generated identity column'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -33,7 +33,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_50c4f9905061b1e506d38a2a380', tableName: 'table1', columnNames: ['column1'], diff --git a/server/test/sql-tools/column-generated-uuid.stub.ts b/server/test/sql-tools/column-generated-uuid.stub.ts index 69cc59530e..4d355f2cf6 100644 --- a/server/test/sql-tools/column-generated-uuid.stub.ts +++ b/server/test/sql-tools/column-generated-uuid.stub.ts @@ -1,4 +1,4 @@ -import { DatabaseConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +import { ConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a primary generated uuid column'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -33,7 +33,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_50c4f9905061b1e506d38a2a380', tableName: 'table1', columnNames: ['column1'], diff --git a/server/test/sql-tools/column-index-name-default.ts b/server/test/sql-tools/column-index-name-default.ts index cedae006be..703a526285 100644 --- a/server/test/sql-tools/column-index-name-default.ts +++ b/server/test/sql-tools/column-index-name-default.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should create a column with an index'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-index-name.ts b/server/test/sql-tools/column-index-name.ts index 8ba18a8851..c72759c74d 100644 --- a/server/test/sql-tools/column-index-name.ts +++ b/server/test/sql-tools/column-index-name.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should create a column with an index if a name is provided'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-inferred-nullable.stub.ts b/server/test/sql-tools/column-inferred-nullable.stub.ts index 70495db800..64c6a8be99 100644 --- a/server/test/sql-tools/column-inferred-nullable.stub.ts +++ b/server/test/sql-tools/column-inferred-nullable.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should infer nullable from the default value'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-name-default.stub.ts b/server/test/sql-tools/column-name-default.stub.ts index e1db458e4b..fc16c7540d 100644 --- a/server/test/sql-tools/column-name-default.stub.ts +++ b/server/test/sql-tools/column-name-default.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-name-override.stub.ts b/server/test/sql-tools/column-name-override.stub.ts index 250e295280..5e5594a166 100644 --- a/server/test/sql-tools/column-name-override.stub.ts +++ b/server/test/sql-tools/column-name-override.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-name-string.stub.ts b/server/test/sql-tools/column-name-string.stub.ts index 12f8b4a537..d0455ee773 100644 --- a/server/test/sql-tools/column-name-string.stub.ts +++ b/server/test/sql-tools/column-name-string.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with a column with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-nullable.stub.ts b/server/test/sql-tools/column-nullable.stub.ts index 2b82a3de13..037b2db948 100644 --- a/server/test/sql-tools/column-nullable.stub.ts +++ b/server/test/sql-tools/column-nullable.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should set nullable correctly'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-string-length.stub.ts b/server/test/sql-tools/column-string-length.stub.ts index 47400f25e0..00d7264914 100644 --- a/server/test/sql-tools/column-string-length.stub.ts +++ b/server/test/sql-tools/column-string-length.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should use create a string column with a fixed length'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts b/server/test/sql-tools/column-unique-constraint-name-default.stub.ts index e1e1619679..b1dfaf4308 100644 --- a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts +++ b/server/test/sql-tools/column-unique-constraint-name-default.stub.ts @@ -1,4 +1,4 @@ -import { Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should create a unique key constraint with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -32,7 +32,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'UQ_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts b/server/test/sql-tools/column-unique-constraint-name-override.stub.ts index 36ce80efb6..b873085fa0 100644 --- a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts +++ b/server/test/sql-tools/column-unique-constraint-name-override.stub.ts @@ -1,4 +1,4 @@ -import { Column, DatabaseConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should create a unique key constraint with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -32,7 +32,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'UQ_test', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/column-update-date.stub.ts b/server/test/sql-tools/column-update-date.stub.ts index bbdb6df923..f63f568cf6 100644 --- a/server/test/sql-tools/column-update-date.stub.ts +++ b/server/test/sql-tools/column-update-date.stub.ts @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should register a table with an updated at date column'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts b/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts new file mode 100644 index 0000000000..3b7a8781b9 --- /dev/null +++ b/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts @@ -0,0 +1,7 @@ +import { Table } from 'src/sql-tools'; + +@Table({ name: 'table-1' }) +@Table({ name: 'table-2' }) +export class Table1 {} + +export const message = 'Table table-2 has already been registered'; diff --git a/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts b/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts index b211620343..c31a00c186 100644 --- a/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts @@ -1,11 +1,4 @@ -import { - Column, - DatabaseConstraintType, - DatabaseSchema, - ForeignKeyConstraint, - PrimaryColumn, - Table, -} from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -32,7 +25,7 @@ export class Table2 { export const description = 'should create a foreign key constraint to the target table'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -65,7 +58,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_e457e8b1301b7bc06ef78188ee4', tableName: 'table1', columnNames: ['id1', 'id2'], @@ -108,7 +101,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_aed36d04470eba20161aa8b1dc6', tableName: 'table2', columnNames: ['parentId1', 'parentId2'], diff --git a/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts index 5bb335030a..f42f1251f4 100644 --- a/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts @@ -1,11 +1,4 @@ -import { - Column, - DatabaseConstraintType, - DatabaseSchema, - ForeignKeyConstraint, - PrimaryColumn, - Table, -} from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -22,7 +15,7 @@ export class Table2 { export const description = 'should warn against missing column in foreign key constraint'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -46,7 +39,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts index 83c3adeb6d..8eb7b5cde8 100644 --- a/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts @@ -1,11 +1,4 @@ -import { - Column, - DatabaseConstraintType, - DatabaseSchema, - ForeignKeyConstraint, - PrimaryColumn, - Table, -} from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -22,7 +15,7 @@ export class Table2 { export const description = 'should warn against missing reference column in foreign key constraint'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -46,7 +39,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts index 54cf731479..47c9f18e1d 100644 --- a/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts @@ -14,7 +14,7 @@ export class Table1 { export const description = 'should warn against missing reference table'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts b/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts index 30f18eaf9d..6e2ee7741e 100644 --- a/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts @@ -1,11 +1,4 @@ -import { - Column, - DatabaseConstraintType, - DatabaseSchema, - ForeignKeyConstraint, - PrimaryColumn, - Table, -} from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -28,7 +21,7 @@ export class Table2 { export const description = 'should create a foreign key constraint to the target table'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -61,7 +54,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_e457e8b1301b7bc06ef78188ee4', tableName: 'table1', columnNames: ['id1', 'id2'], @@ -104,7 +97,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_aed36d04470eba20161aa8b1dc6', tableName: 'table2', columnNames: ['parentId1', 'parentId2'], diff --git a/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts b/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts index 5ad0aa7a6b..508110f687 100644 --- a/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts @@ -1,11 +1,4 @@ -import { - Column, - DatabaseConstraintType, - DatabaseSchema, - ForeignKeyConstraint, - PrimaryColumn, - Table, -} from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -22,7 +15,7 @@ export class Table2 { export const description = 'should create a foreign key constraint to the target table without an index'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -46,7 +39,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], @@ -72,7 +65,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_3fcca5cc563abf256fc346e3ff4', tableName: 'table2', columnNames: ['parentId'], diff --git a/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts b/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts index 645b0e76f2..dbcefa1d39 100644 --- a/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts @@ -1,4 +1,4 @@ -import { Column, DatabaseConstraintType, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -19,7 +19,7 @@ export class Table2 { export const description = 'should create a foreign key constraint to the target table without a primary key'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -69,7 +69,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_7d9c784c98d12365d198d52e4e6', tableName: 'table2', columnNames: ['bar'], diff --git a/server/test/sql-tools/foreign-key-constraint.stub.ts b/server/test/sql-tools/foreign-key-constraint.stub.ts index c8117bd96d..81f6292782 100644 --- a/server/test/sql-tools/foreign-key-constraint.stub.ts +++ b/server/test/sql-tools/foreign-key-constraint.stub.ts @@ -1,11 +1,4 @@ -import { - Column, - DatabaseConstraintType, - DatabaseSchema, - ForeignKeyConstraint, - PrimaryColumn, - Table, -} from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -22,7 +15,7 @@ export class Table2 { export const description = 'should create a foreign key constraint to the target table'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -46,7 +39,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], @@ -80,7 +73,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_3fcca5cc563abf256fc346e3ff4', tableName: 'table2', columnNames: ['parentId'], diff --git a/server/test/sql-tools/foreign-key-inferred-type.stub.ts b/server/test/sql-tools/foreign-key-inferred-type.stub.ts index 0b66a1acd4..626bb2a752 100644 --- a/server/test/sql-tools/foreign-key-inferred-type.stub.ts +++ b/server/test/sql-tools/foreign-key-inferred-type.stub.ts @@ -1,4 +1,4 @@ -import { DatabaseConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; +import { ConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -14,7 +14,7 @@ export class Table2 { export const description = 'should infer the column type from the reference column'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -38,7 +38,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], @@ -72,7 +72,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_3fcca5cc563abf256fc346e3ff4', tableName: 'table2', columnNames: ['parentId'], diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts index 109a3dfc85..ad4142a503 100644 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts @@ -1,4 +1,4 @@ -import { DatabaseConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; +import { ConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -14,7 +14,7 @@ export class Table2 { export const description = 'should create a foreign key constraint with a unique constraint'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -38,7 +38,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], @@ -72,7 +72,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.FOREIGN_KEY, + type: ConstraintType.FOREIGN_KEY, name: 'FK_3fcca5cc563abf256fc346e3ff4', tableName: 'table2', columnNames: ['parentId'], @@ -81,7 +81,7 @@ export const schema: DatabaseSchema = { synchronize: true, }, { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'REL_3fcca5cc563abf256fc346e3ff', tableName: 'table2', columnNames: ['parentId'], diff --git a/server/test/sql-tools/index-name-default.stub.ts b/server/test/sql-tools/index-name-default.stub.ts index 06ccd7e173..b309e93d66 100644 --- a/server/test/sql-tools/index-name-default.stub.ts +++ b/server/test/sql-tools/index-name-default.stub.ts @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should create an index with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/index-name-override.stub.ts b/server/test/sql-tools/index-name-override.stub.ts index afdc26dcc0..daa4fa208b 100644 --- a/server/test/sql-tools/index-name-override.stub.ts +++ b/server/test/sql-tools/index-name-override.stub.ts @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should create an index with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/index-with-expression.ts b/server/test/sql-tools/index-with-expression.ts index dec31ebe02..57bce5731b 100644 --- a/server/test/sql-tools/index-with-expression.ts +++ b/server/test/sql-tools/index-with-expression.ts @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should create an index based off of an expression'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/index-with-where.stub.ts b/server/test/sql-tools/index-with-where.stub.ts index ce4236e490..f88702010a 100644 --- a/server/test/sql-tools/index-with-where.stub.ts +++ b/server/test/sql-tools/index-with-where.stub.ts @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should create an index with a where clause'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts b/server/test/sql-tools/primary-key-constraint-name-default.stub.ts index 22a515735a..3047605b8e 100644 --- a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts +++ b/server/test/sql-tools/primary-key-constraint-name-default.stub.ts @@ -1,4 +1,4 @@ -import { DatabaseConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; +import { ConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; @Table() export class Table1 { @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should add a primary key constraint to the table with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -32,7 +32,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts b/server/test/sql-tools/primary-key-constraint-name-override.stub.ts index e1e0daa82e..ffc014c91f 100644 --- a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts +++ b/server/test/sql-tools/primary-key-constraint-name-override.stub.ts @@ -1,4 +1,4 @@ -import { DatabaseConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; +import { ConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; @Table({ primaryConstraintName: 'PK_test' }) export class Table1 { @@ -8,7 +8,7 @@ export class Table1 { export const description = 'should add a primary key constraint to the table with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -32,7 +32,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.PRIMARY_KEY, + type: ConstraintType.PRIMARY_KEY, name: 'PK_test', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/table-name-default.stub.ts b/server/test/sql-tools/table-name-default.stub.ts index 6ecc042a58..0cb84f518d 100644 --- a/server/test/sql-tools/table-name-default.stub.ts +++ b/server/test/sql-tools/table-name-default.stub.ts @@ -5,7 +5,7 @@ export class Table1 {} export const description = 'should register a table with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/table-name-override.stub.ts b/server/test/sql-tools/table-name-override.stub.ts index 929a4c4b28..1d66e0b7fb 100644 --- a/server/test/sql-tools/table-name-override.stub.ts +++ b/server/test/sql-tools/table-name-override.stub.ts @@ -5,7 +5,7 @@ export class Table1 {} export const description = 'should register a table with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/table-name-string-option.stub.ts b/server/test/sql-tools/table-name-string-option.stub.ts index 33e582fb6b..ae1dfe1b2d 100644 --- a/server/test/sql-tools/table-name-string-option.stub.ts +++ b/server/test/sql-tools/table-name-string-option.stub.ts @@ -5,7 +5,7 @@ export class Table1 {} export const description = 'should register a table with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/trigger-after-delete.stub.ts b/server/test/sql-tools/trigger-after-delete.stub.ts index 1eb063c968..56c5d5f267 100644 --- a/server/test/sql-tools/trigger-after-delete.stub.ts +++ b/server/test/sql-tools/trigger-after-delete.stub.ts @@ -16,7 +16,7 @@ export class Table1 {} export const description = 'should create a trigger'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [expect.any(Object)], enums: [], diff --git a/server/test/sql-tools/trigger-before-update.stub.ts b/server/test/sql-tools/trigger-before-update.stub.ts index a88675a9ef..cbc3454a0b 100644 --- a/server/test/sql-tools/trigger-before-update.stub.ts +++ b/server/test/sql-tools/trigger-before-update.stub.ts @@ -16,7 +16,7 @@ export class Table1 {} export const description = 'should create a trigger '; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [expect.any(Object)], enums: [], diff --git a/server/test/sql-tools/trigger-name-default.stub.ts b/server/test/sql-tools/trigger-name-default.stub.ts index a9951aef18..a6cf8d237a 100644 --- a/server/test/sql-tools/trigger-name-default.stub.ts +++ b/server/test/sql-tools/trigger-name-default.stub.ts @@ -11,7 +11,7 @@ export class Table1 {} export const description = 'should register a trigger with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/trigger-name-override.stub.ts b/server/test/sql-tools/trigger-name-override.stub.ts index 3fba0e12ab..aba0addb71 100644 --- a/server/test/sql-tools/trigger-name-override.stub.ts +++ b/server/test/sql-tools/trigger-name-override.stub.ts @@ -12,7 +12,7 @@ export class Table1 {} export const description = 'should a trigger with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], diff --git a/server/test/sql-tools/unique-constraint-name-default.stub.ts b/server/test/sql-tools/unique-constraint-name-default.stub.ts index a3b9c512c5..bf302412c1 100644 --- a/server/test/sql-tools/unique-constraint-name-default.stub.ts +++ b/server/test/sql-tools/unique-constraint-name-default.stub.ts @@ -1,4 +1,4 @@ -import { Column, DatabaseConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; @Table() @Unique({ columns: ['id'] }) @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should add a unique constraint to the table with a default name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -33,7 +33,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'UQ_b249cc64cf63b8a22557cdc8537', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/sql-tools/unique-constraint-name-override.stub.ts b/server/test/sql-tools/unique-constraint-name-override.stub.ts index 4def45043f..189c0c38d6 100644 --- a/server/test/sql-tools/unique-constraint-name-override.stub.ts +++ b/server/test/sql-tools/unique-constraint-name-override.stub.ts @@ -1,4 +1,4 @@ -import { Column, DatabaseConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; +import { Column, ConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; @Table() @Unique({ name: 'UQ_test', columns: ['id'] }) @@ -9,7 +9,7 @@ export class Table1 { export const description = 'should add a unique constraint to the table with a specific name'; export const schema: DatabaseSchema = { - name: 'postgres', + databaseName: 'postgres', schemaName: 'public', functions: [], enums: [], @@ -33,7 +33,7 @@ export const schema: DatabaseSchema = { triggers: [], constraints: [ { - type: DatabaseConstraintType.UNIQUE, + type: ConstraintType.UNIQUE, name: 'UQ_test', tableName: 'table1', columnNames: ['id'], diff --git a/server/test/vitest.config.mjs b/server/test/vitest.config.mjs index a22a6751c3..6d9ee3a564 100644 --- a/server/test/vitest.config.mjs +++ b/server/test/vitest.config.mjs @@ -18,7 +18,6 @@ export default defineConfig({ 'src/services/api.service.ts', 'src/services/microservices.service.ts', 'src/services/index.ts', - 'src/sql-tools/from-database/index.ts', ], }, server: { diff --git a/web/package-lock.json b/web/package-lock.json index e998a46a10..82942102fb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,6 +20,7 @@ "@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/video-plugin": "^5.11.5", "@zoom-image/svelte": "^0.3.0", + "async-mutex": "^0.5.0", "dom-to-image": "^2.6.0", "fabric": "^6.5.4", "geojson": "^0.5.0", @@ -3924,6 +3925,15 @@ "dev": true, "license": "MIT" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/web/package.json b/web/package.json index 08afe31762..2ad2a8361a 100644 --- a/web/package.json +++ b/web/package.json @@ -37,6 +37,7 @@ "@photo-sphere-viewer/settings-plugin": "^5.11.5", "@photo-sphere-viewer/video-plugin": "^5.11.5", "@zoom-image/svelte": "^0.3.0", + "async-mutex": "^0.5.0", "dom-to-image": "^2.6.0", "fabric": "^6.5.4", "geojson": "^0.5.0", diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index f0dbc1a2da..a1926b4020 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -167,6 +167,16 @@ isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)} /> + + { + const release = await mutex.acquire(); const laterAsset = await timelineManager.getLaterAsset($viewingAsset); if (laterAsset) { @@ -447,11 +448,14 @@ await navigate({ targetRoute: 'current', assetId: laterAsset.id }); } + release(); return !!laterAsset; }; const handleNext = async () => { + const release = await mutex.acquire(); const earlierAsset = await timelineManager.getEarlierAsset($viewingAsset); + if (earlierAsset) { const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset); const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key }); @@ -459,6 +463,7 @@ await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); } + release(); return !!earlierAsset; }; diff --git a/web/src/lib/components/shared-components/qrcode.svelte b/web/src/lib/components/shared-components/qrcode.svelte index 5fa83e880c..7ac23f8a34 100644 --- a/web/src/lib/components/shared-components/qrcode.svelte +++ b/web/src/lib/components/shared-components/qrcode.svelte @@ -10,7 +10,7 @@ const { value, width, alt = $t('alt_text_qr_code') }: Props = $props(); - let promise = $derived(QRCode.toDataURL(value, { margin: 0, width })); + let promise = $derived(QRCode.toDataURL(value, { margin: 4, width }));
diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index 2cd20d9d20..997f6d20fa 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -2,12 +2,14 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { type AssetGridRouteSearchParams } from '$lib/utils/navigation'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; +import { Mutex } from 'async-mutex'; import { readonly, writable } from 'svelte/store'; function createAssetViewingStore() { const viewingAssetStoreState = writable(); const preloadAssets = writable([]); const viewState = writable(false); + const viewingAssetMutex = new Mutex(); const gridScrollTarget = writable(); const setAsset = (asset: AssetResponseDto, assetsToPreload: TimelineAsset[] = []) => { @@ -28,6 +30,7 @@ function createAssetViewingStore() { return { asset: readonly(viewingAssetStoreState), + mutex: viewingAssetMutex, preloadAssets: readonly(preloadAssets), isViewing: viewState, gridScrollTarget,