feat: store null instead of empty string for album.description (#30123)
Addresses the first column of issue #28832: album.description now stores and returns null instead of an empty string. Co-authored-by: Giacomo Pinato <giacomo.pinato@gmail.com>
This commit is contained in:
co-authored by
Giacomo Pinato
parent
e7aace436d
commit
6b6058c463
@@ -25,7 +25,9 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
_api.createAlbum(
|
||||
CreateAlbumDto(
|
||||
albumName: name,
|
||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
||||
description: description == null
|
||||
? const Optional.absent()
|
||||
: Optional.present(description.isEmpty ? null : description),
|
||||
assetIds: Optional.present(assetIds.toList()),
|
||||
),
|
||||
),
|
||||
@@ -88,7 +90,9 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
albumId,
|
||||
UpdateAlbumDto(
|
||||
albumName: name == null ? const Optional.absent() : Optional.present(name),
|
||||
description: description == null ? const Optional.absent() : Optional.present(description),
|
||||
description: description == null
|
||||
? const Optional.absent()
|
||||
: Optional.present(description.isEmpty ? null : description),
|
||||
albumThumbnailAssetId: thumbnailAssetId == null
|
||||
? const Optional.absent()
|
||||
: Optional.present(thumbnailAssetId),
|
||||
|
||||
@@ -16585,7 +16585,18 @@
|
||||
},
|
||||
"description": {
|
||||
"description": "Album description",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Updated",
|
||||
"description": "An empty string is returned instead of null for backwards compatibility; null will be returned in v4."
|
||||
}
|
||||
]
|
||||
},
|
||||
"endDate": {
|
||||
"description": "End date (latest asset)",
|
||||
@@ -18443,7 +18454,19 @@
|
||||
},
|
||||
"description": {
|
||||
"description": "Album description",
|
||||
"type": "string"
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Updated",
|
||||
"description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -27115,7 +27138,19 @@
|
||||
},
|
||||
"description": {
|
||||
"description": "Album description",
|
||||
"type": "string"
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Updated",
|
||||
"description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4."
|
||||
}
|
||||
]
|
||||
},
|
||||
"isActivityEnabled": {
|
||||
"description": "Enable activity feed",
|
||||
|
||||
@@ -540,7 +540,7 @@ export type CreateAlbumDto = {
|
||||
/** Initial asset IDs */
|
||||
assetIds?: string[];
|
||||
/** Album description */
|
||||
description?: string;
|
||||
description?: string | null;
|
||||
};
|
||||
export type AlbumsAddAssetsDto = {
|
||||
/** Album IDs */
|
||||
@@ -567,7 +567,7 @@ export type UpdateAlbumDto = {
|
||||
/** Album thumbnail asset ID */
|
||||
albumThumbnailAssetId?: string;
|
||||
/** Album description */
|
||||
description?: string;
|
||||
description?: string | null;
|
||||
/** Enable activity feed */
|
||||
isActivityEnabled?: boolean;
|
||||
order?: AssetOrder;
|
||||
|
||||
@@ -34,7 +34,22 @@ const AlbumUserCreateSchema = z
|
||||
const CreateAlbumSchema = z
|
||||
.object({
|
||||
albumName: z.string().describe('Album name'),
|
||||
description: z.string().optional().describe('Album description'),
|
||||
// TODO: drop the empty-string-to-null transform in v4 (clients should send null)
|
||||
description: z
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((value) => (value === '' ? null : value))
|
||||
.optional()
|
||||
.describe('Album description')
|
||||
.meta({
|
||||
...new HistoryBuilder()
|
||||
.added('v1')
|
||||
.updated(
|
||||
'v3',
|
||||
'Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4.',
|
||||
)
|
||||
.getExtensions(),
|
||||
}),
|
||||
albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'),
|
||||
assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'),
|
||||
})
|
||||
@@ -57,7 +72,22 @@ const AlbumsAddAssetsResponseSchema = z
|
||||
const UpdateAlbumSchema = z
|
||||
.object({
|
||||
albumName: z.string().optional().describe('Album name'),
|
||||
description: z.string().optional().describe('Album description'),
|
||||
// TODO: drop the empty-string-to-null transform in v4 (clients should send null)
|
||||
description: z
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((value) => (value === '' ? null : value))
|
||||
.optional()
|
||||
.describe('Album description')
|
||||
.meta({
|
||||
...new HistoryBuilder()
|
||||
.added('v1')
|
||||
.updated(
|
||||
'v3',
|
||||
'Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4.',
|
||||
)
|
||||
.getExtensions(),
|
||||
}),
|
||||
albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'),
|
||||
isActivityEnabled: z.boolean().optional().describe('Enable activity feed'),
|
||||
order: AssetOrderSchema.optional(),
|
||||
@@ -110,7 +140,18 @@ export const AlbumResponseSchema = z
|
||||
.object({
|
||||
id: z.uuidv4().describe('Album ID'),
|
||||
albumName: z.string().describe('Album name'),
|
||||
description: z.string().describe('Album description'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('Album description')
|
||||
.meta({
|
||||
...new HistoryBuilder()
|
||||
.added('v1')
|
||||
.updated(
|
||||
'v3',
|
||||
'An empty string is returned instead of null for backwards compatibility; null will be returned in v4.',
|
||||
)
|
||||
.getExtensions(),
|
||||
}),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'),
|
||||
// TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers.
|
||||
@@ -171,7 +212,7 @@ export type MapAlbumDto = {
|
||||
assets?: ShallowDehydrateObject<MapAsset>[];
|
||||
sharedLinks?: ShallowDehydrateObject<AuthSharedLink>[];
|
||||
albumName: string;
|
||||
description: string;
|
||||
description: string | null;
|
||||
albumThumbnailAssetId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -207,7 +248,8 @@ export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): AlbumResponseDto
|
||||
|
||||
return {
|
||||
albumName: entity.albumName,
|
||||
description: entity.description,
|
||||
// TODO: return null instead of '' in v4
|
||||
description: entity.description ?? '',
|
||||
albumThumbnailAssetId: entity.albumThumbnailAssetId,
|
||||
createdAt: asDateTimeString(entity.createdAt),
|
||||
updatedAt: asDateTimeString(entity.updatedAt),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "album" ALTER COLUMN "description" DROP NOT NULL;`.execute(db);
|
||||
await sql`ALTER TABLE "album" ALTER COLUMN "description" SET DEFAULT NULL;`.execute(db);
|
||||
await sql`UPDATE "album" SET "description" = NULL WHERE "description" = '';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`UPDATE "album" SET "description" = '' WHERE "description" IS NULL;`.execute(db);
|
||||
await sql`ALTER TABLE "album" ALTER COLUMN "description" SET DEFAULT ''::text;`.execute(db);
|
||||
await sql`ALTER TABLE "album" ALTER COLUMN "description" SET NOT NULL;`.execute(db);
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export class AlbumTable {
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@Column({ type: 'text', default: '' })
|
||||
description!: Generated<string>;
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Timestamp | null;
|
||||
|
||||
@@ -441,7 +441,12 @@ export class SyncService extends BaseService {
|
||||
const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
const albumUsers = await this.syncRepository.album.getAlbumUsers(data.id);
|
||||
send(response, { type: upsertType, ids: [updateId], data: syncAlbumV2ToV1(data, albumUsers) });
|
||||
send(response, {
|
||||
type: upsertType,
|
||||
ids: [updateId],
|
||||
// TODO: return null instead of '' in v4
|
||||
data: syncAlbumV2ToV1({ ...data, description: data.description ?? '' }, albumUsers),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +460,8 @@ export class SyncService extends BaseService {
|
||||
const upsertType = SyncEntityType.AlbumV2;
|
||||
const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
// TODO: return null instead of '' in v4
|
||||
send(response, { type: upsertType, ids: [updateId], data: { ...data, description: data.description ?? '' } });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let description = $state(album.description);
|
||||
|
||||
const onSubmit = async () => {
|
||||
const success = await handleUpdateAlbum(album, { albumName, description });
|
||||
const success = await handleUpdateAlbum(album, { albumName, description: description || null });
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
const response = await updateAlbumInfo({
|
||||
id,
|
||||
updateAlbumDto: {
|
||||
description,
|
||||
description: description || null,
|
||||
},
|
||||
});
|
||||
eventManager.emit('AlbumUpdate', response);
|
||||
|
||||
Reference in New Issue
Block a user