{"record":{"id":"ba2af9b02febd720","repo":"immich-app/immich","slug":"album-must-have-an-owner","errorCode":null,"errorMessage":"Album must have an owner","messagePattern":"Album must have an owner","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/repositories/album.repository.ts","lineNumber":318,"sourceCode":"      .onConflict((oc) => oc.doNothing())\n      .execute();\n  }\n\n  @GenerateSql({\n    params: [\n      { albumName: DummyValue.STRING },\n      [],\n      [{ userId: DummyValue.UUID, role: AlbumUserRole.Owner }, DummyValue.UUID],\n    ],\n  })\n  async create(\n    album: Insertable<AlbumTable>,\n    assetIds: string[],\n    albumUsers: AlbumUserCreateDto[],\n    authUserId: string,\n  ) {\n    if (albumUsers.every((u) => u.role !== AlbumUserRole.Owner)) {\n      throw new Error('Album must have an owner');\n    }\n\n    const userIds = albumUsers.map((u) => u.userId);\n    const roles = albumUsers.map((u) => u.role);\n\n    const result = await this.db\n      .with('album', (db) => db.insertInto('album').values(album).returningAll())\n      .with('album_user', (db) =>\n        db\n          .insertInto('album_user')\n          .expression((eb) =>\n            eb\n              .selectFrom('album')\n              .select(({ ref }) => [\n                ref('album.id').as('albumId'),\n                sql`unnest(${userIds}::uuid[])`.as('userId'),\n                sql`unnest(${roles}::album_user_role_enum[])`.as('role'),\n              ]),","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/repositories/album.repository.ts#L300-L336","documentation":"AlbumRepository.create() asserts that among the albumUsers passed in, at least one has role AlbumUserRole.Owner (the every(...) check requires NOT all users to be non-owner). This plain Error enforces the invariant that a newly created album always has exactly an owner entry; a plain insert without any owner row would leave the album ownerless. Note the check is written so it fires when no user in the list has the Owner role.","triggerScenarios":"Calling albumRepository.create(album, assetIds, albumUsers, authUserId) where every entry in albumUsers has role Editor or Viewer (none with AlbumUserRole.Owner); constructing the album share list without including the creator as an owner.","commonSituations":"Service code building shared-user lists from a DTO where the creator was omitted from the users array; a refactor or API change that stopped seeding the owner role for the authenticated user; bulk-import scripts inserting collaborators (editors/viewers) only.","solutions":["Include at least one entry in albumUsers with role AlbumUserRole.Owner (normally the creator, using authUserId).","If callers pass only collaborators, prepend { userId: authUserId, role: AlbumUserRole.Owner } in the calling service before create().","Audit DTO-to-repository mapping so the owner role isn't dropped or defaulted to Editor/Viewer.","Fix the check itself if intent was 'at least one owner' versus current logic (every non-owner => throw) — consider a clearer assertion with the creator always inserted."],"exampleFix":"// before\nawait albumRepo.create(album, assetIds, [\n  { userId: friendId, role: AlbumUserRole.Editor },\n], authUserId); // Error: Album must have an owner\n// after\nawait albumRepo.create(album, assetIds, [\n  { userId: authUserId, role: AlbumUserRole.Owner },\n  { userId: friendId, role: AlbumUserRole.Editor },\n], authUserId);","handlingStrategy":"validation","validationCode":"if (!albumUsers.some(u => u.role === AlbumUserRole.Owner)) {\n  albumUsers = [{ userId: authUserId, role: AlbumUserRole.Owner }, ...albumUsers];\n}","typeGuard":"function hasOwner(users: { role: AlbumUserRole }[]): boolean {\n  return users.some(u => u.role === AlbumUserRole.Owner);\n}","tryCatchPattern":"try {\n  await albumRepo.create(album, assetIds, albumUsers, authUserId);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Album must have an owner') {\n    await albumRepo.create(album, assetIds,\n      [{ userId: authUserId, role: AlbumUserRole.Owner }, ...albumUsers], authUserId);\n  } else throw e;\n}","preventionTips":["Always seed the creator as an owner entry when building the albumUsers list.","Add a repository/service-level validation that an owner exists before calling create().","Map DTOs carefully so Owner roles are not downgraded to Editor/Viewer in bulk imports.","Prefer storing the owner separately from shared users to make the invariant explicit."],"tags":["database","invariant","albums","ownership"],"backgroundTag":"internal-invariant-violation","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}