Use nulls, make-sql

This commit is contained in:
Min Idzelis 2025-05-03 02:06:34 +00:00
parent 0ed2a2fd2e
commit aea2c9506d
19 changed files with 186 additions and 205 deletions

View File

@ -104,7 +104,7 @@ describe('/timeline', () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
.query({ withPartners: true, isArchived: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
@ -112,7 +112,7 @@ describe('/timeline', () => {
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
.query({ withPartners: true, isArchived: undefined });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
@ -122,7 +122,7 @@ describe('/timeline', () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
.query({ withPartners: true, isFavorite: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
@ -130,7 +130,7 @@ describe('/timeline', () => {
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
.query({ withPartners: true, isFavorite: false });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());
@ -140,7 +140,7 @@ describe('/timeline', () => {
const req = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
.query({ withPartners: true, isTrashed: true });
expect(req.status).toBe(400);
expect(req.body).toEqual(errorDto.badRequest());
@ -150,7 +150,6 @@ describe('/timeline', () => {
describe('GET /timeline/bucket', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/timeline/bucket').query({
size: TimeBucketSize.Month,
timeBucket: '1900-01-01',
});
@ -161,7 +160,7 @@ describe('/timeline', () => {
it('should handle 5 digit years', async () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
.query({ timeBucket: '012345-01-01' })
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
expect(status).toBe(200);
@ -173,7 +172,7 @@ describe('/timeline', () => {
// const { status, body } = await request(app)
// .get('/timeline/bucket')
// .set('Authorization', `Bearer ${user.accessToken}`)
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
// .query({ timeBucket: 'foo' });
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.badRequest);
@ -183,7 +182,7 @@ describe('/timeline', () => {
const { status, body } = await request(app)
.get('/timeline/bucket')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
.query({ timeBucket: '1970-02-10' });
expect(status).toBe(200);
expect(body).toEqual([]);

View File

@ -479,10 +479,10 @@ Class | Method | HTTP request | Description
- [TestEmailResponseDto](doc//TestEmailResponseDto.md)
- [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md)
- [TimeBucketAssetResponseDtoDurationInner](doc//TimeBucketAssetResponseDtoDurationInner.md)
- [TimeBucketAssetResponseDtoStackInner](doc//TimeBucketAssetResponseDtoStackInner.md)
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
- [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.md)
- [TimelineAssetDescriptionDto](doc//TimelineAssetDescriptionDto.md)
- [TimelineStackResponseDto](doc//TimelineStackResponseDto.md)
- [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md)

View File

@ -283,10 +283,10 @@ part 'model/template_response_dto.dart';
part 'model/test_email_response_dto.dart';
part 'model/time_bucket_asset_response_dto.dart';
part 'model/time_bucket_asset_response_dto_duration_inner.dart';
part 'model/time_bucket_asset_response_dto_stack_inner.dart';
part 'model/time_bucket_response_dto.dart';
part 'model/time_buckets_response_dto.dart';
part 'model/timeline_asset_description_dto.dart';
part 'model/timeline_stack_response_dto.dart';
part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart';

View File

@ -622,14 +622,14 @@ class ApiClient {
return TimeBucketAssetResponseDto.fromJson(value);
case 'TimeBucketAssetResponseDtoDurationInner':
return TimeBucketAssetResponseDtoDurationInner.fromJson(value);
case 'TimeBucketAssetResponseDtoStackInner':
return TimeBucketAssetResponseDtoStackInner.fromJson(value);
case 'TimeBucketResponseDto':
return TimeBucketResponseDto.fromJson(value);
case 'TimeBucketsResponseDto':
return TimeBucketsResponseDto.fromJson(value);
case 'TimelineAssetDescriptionDto':
return TimelineAssetDescriptionDto.fromJson(value);
case 'TimelineStackResponseDto':
return TimelineStackResponseDto.fromJson(value);
case 'ToneMapping':
return ToneMappingTypeTransformer().decode(value);
case 'TranscodeHWAccel':

View File

@ -56,7 +56,7 @@ class TimeBucketAssetResponseDto {
List<num> ratio;
List<TimelineStackResponseDto> stack;
List<TimeBucketAssetResponseDtoStackInner> stack;
List<TimeBucketAssetResponseDtoDurationInner> thumbhash;
@ -158,7 +158,7 @@ class TimeBucketAssetResponseDto {
ratio: json[r'ratio'] is Iterable
? (json[r'ratio'] as Iterable).cast<num>().toList(growable: false)
: const [],
stack: TimelineStackResponseDto.listFromJson(json[r'stack']),
stack: TimeBucketAssetResponseDtoStackInner.listFromJson(json[r'stack']),
thumbhash: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'thumbhash']),
);
}

View File

@ -0,0 +1,91 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class TimeBucketAssetResponseDtoStackInner {
/// Returns a new [TimeBucketAssetResponseDtoStackInner] instance.
TimeBucketAssetResponseDtoStackInner({
});
@override
bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDtoStackInner &&
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
@override
String toString() => 'TimeBucketAssetResponseDtoStackInner[]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
return json;
}
/// Returns a new [TimeBucketAssetResponseDtoStackInner] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TimeBucketAssetResponseDtoStackInner? fromJson(dynamic value) {
upgradeDto(value, "TimeBucketAssetResponseDtoStackInner");
if (value is Map) {
final json = value.cast<String, dynamic>();
return TimeBucketAssetResponseDtoStackInner(
);
}
return null;
}
static List<TimeBucketAssetResponseDtoStackInner> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimeBucketAssetResponseDtoStackInner>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimeBucketAssetResponseDtoStackInner.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, TimeBucketAssetResponseDtoStackInner> mapFromJson(dynamic json) {
final map = <String, TimeBucketAssetResponseDtoStackInner>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TimeBucketAssetResponseDtoStackInner.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of TimeBucketAssetResponseDtoStackInner-objects as value to a dart map
static Map<String, List<TimeBucketAssetResponseDtoStackInner>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TimeBucketAssetResponseDtoStackInner>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TimeBucketAssetResponseDtoStackInner.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@ -1,115 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class TimelineStackResponseDto {
/// Returns a new [TimelineStackResponseDto] instance.
TimelineStackResponseDto({
required this.assetCount,
required this.id,
required this.primaryAssetId,
});
num assetCount;
String id;
String primaryAssetId;
@override
bool operator ==(Object other) => identical(this, other) || other is TimelineStackResponseDto &&
other.assetCount == assetCount &&
other.id == id &&
other.primaryAssetId == primaryAssetId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetCount.hashCode) +
(id.hashCode) +
(primaryAssetId.hashCode);
@override
String toString() => 'TimelineStackResponseDto[assetCount=$assetCount, id=$id, primaryAssetId=$primaryAssetId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assetCount'] = this.assetCount;
json[r'id'] = this.id;
json[r'primaryAssetId'] = this.primaryAssetId;
return json;
}
/// Returns a new [TimelineStackResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TimelineStackResponseDto? fromJson(dynamic value) {
upgradeDto(value, "TimelineStackResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return TimelineStackResponseDto(
assetCount: num.parse('${json[r'assetCount']}'),
id: mapValueOfType<String>(json, r'id')!,
primaryAssetId: mapValueOfType<String>(json, r'primaryAssetId')!,
);
}
return null;
}
static List<TimelineStackResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TimelineStackResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TimelineStackResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, TimelineStackResponseDto> mapFromJson(dynamic json) {
final map = <String, TimelineStackResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TimelineStackResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of TimelineStackResponseDto-objects as value to a dart map
static Map<String, List<TimelineStackResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TimelineStackResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TimelineStackResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetCount',
'id',
'primaryAssetId',
};
}

View File

@ -13576,7 +13576,7 @@
"type": "string"
},
{
"type": "number"
"type": "null"
}
]
},
@ -13632,7 +13632,7 @@
"type": "string"
},
{
"type": "number"
"type": "null"
}
]
},
@ -13661,7 +13661,7 @@
"type": "string"
},
{
"type": "number"
"type": "null"
}
]
},
@ -13677,7 +13677,14 @@
"stack": {
"default": [],
"items": {
"$ref": "#/components/schemas/TimelineStackResponseDto"
"oneOf": [
{
"type": "TimelineStackResponseDto"
},
{
"type": "null"
}
]
},
"type": "array"
},
@ -13689,7 +13696,7 @@
"type": "string"
},
{
"type": "number"
"type": "null"
}
]
},
@ -13762,25 +13769,6 @@
],
"type": "object"
},
"TimelineStackResponseDto": {
"properties": {
"assetCount": {
"type": "number"
},
"id": {
"type": "string"
},
"primaryAssetId": {
"type": "string"
}
},
"required": [
"assetCount",
"id",
"primaryAssetId"
],
"type": "object"
},
"ToneMapping": {
"enum": [
"hable",

View File

@ -1388,27 +1388,22 @@ export type TimelineAssetDescriptionDto = {
city: string | null;
country: string | null;
};
export type TimelineStackResponseDto = {
assetCount: number;
id: string;
primaryAssetId: string;
};
export type TimeBucketAssetResponseDto = {
description: TimelineAssetDescriptionDto[];
duration: (string | number)[];
duration: (string | null)[];
id: string[];
isArchived: number[];
isFavorite: number[];
isImage: number[];
isTrashed: number[];
isVideo: number[];
livePhotoVideoId: (string | number)[];
livePhotoVideoId: (string | null)[];
localDateTime: string[];
ownerId: string[];
projectionType: (string | number)[];
projectionType: (string | null)[];
ratio: number[];
stack: TimelineStackResponseDto[];
thumbhash: (string | number)[];
stack: (any | null)[];
thumbhash: (string | null)[];
};
export type TimeBucketResponseDto = {
bucketAssets: TimeBucketAssetResponseDto;

View File

@ -72,7 +72,9 @@ class SqlGenerator {
await rm(this.options.targetDir, { force: true, recursive: true });
await mkdir(this.options.targetDir);
process.env.DB_HOSTNAME = 'localhost';
if (!process.env.DB_HOSTNAME) {
process.env.DB_HOSTNAME = 'localhost';
}
const { database, cls, otel } = new ConfigRepository().getEnv();
const moduleFixture = await Test.createTestingModule({

View File

@ -183,7 +183,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
tags: entity.tags?.map((tag) => mapTag(tag)),
people: peopleWithFaces(entity.faces),
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
checksum: hexOrBufferToBase64(entity.checksum),
checksum: hexOrBufferToBase64(entity.checksum)!,
stack: withStack ? mapStack(entity) : undefined,
isOffline: entity.isOffline,
hasMetadata: true,

View File

@ -104,12 +104,12 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets {
type: 'string',
},
{
type: 'number',
type: 'null',
},
],
},
})
thumbhash: (string | number)[] = [];
thumbhash: (string | null)[] = [];
@ApiProperty()
localDateTime: Date[] = [];
@ -122,15 +122,27 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets {
type: 'string',
},
{
type: 'number',
type: 'null',
},
],
},
})
duration: (string | number)[] = [];
duration: (string | null)[] = [];
@ApiProperty({ type: [TimelineStackResponseDto] })
stack: (TimelineStackResponseDto | number)[] = [];
@ApiProperty({
type: 'array',
items: {
oneOf: [
{
type: 'TimelineStackResponseDto',
},
{
type: 'null',
},
],
},
})
stack: (TimelineStackResponseDto | null)[] = [];
@ApiProperty({
type: 'array',
@ -140,12 +152,12 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets {
type: 'string',
},
{
type: 'number',
type: 'null',
},
],
},
})
projectionType: (string | number)[] = [];
projectionType: (string | null)[] = [];
@ApiProperty({
type: 'array',
@ -155,12 +167,12 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets {
type: 'string',
},
{
type: 'number',
type: 'null',
},
],
},
})
livePhotoVideoId: (string | number)[] = [];
livePhotoVideoId: (string | null)[] = [];
@ApiProperty()
description: TimelineAssetDescriptionDto[] = [];

View File

@ -255,8 +255,23 @@ order by
-- AssetRepository.getTimeBucket
select
"assets".*,
to_json("exif") as "exifInfo",
"assets"."id" as "id",
"assets"."ownerId",
"assets"."status",
"deletedAt",
"type",
"duration",
"isFavorite",
"isArchived",
"thumbhash",
"localDateTime",
"livePhotoVideoId",
"exif"."exifImageHeight" as "height",
"exifImageWidth" as "width",
"exif"."orientation",
"exif"."projectionType",
"exif"."city" as "city",
"exif"."country" as "country",
to_json("stacked_assets") as "stack"
from
"assets"

View File

@ -142,7 +142,7 @@ export class SyncService extends BaseService {
updateId,
data: {
...data,
checksum: hexOrBufferToBase64(checksum),
checksum: hexOrBufferToBase64(checksum)!,
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
},
}),
@ -172,7 +172,7 @@ export class SyncService extends BaseService {
updateId,
data: {
...data,
checksum: hexOrBufferToBase64(checksum),
checksum: hexOrBufferToBase64(checksum)!,
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
},
}),

View File

@ -75,12 +75,12 @@ export class TimelineService extends BaseService {
bucketAssets.isArchived.push(item.isArchived ? 1 : 0);
bucketAssets.isFavorite.push(item.isFavorite ? 1 : 0);
bucketAssets.isTrashed.push(item.deletedAt === null ? 0 : 1);
bucketAssets.thumbhash.push(item.thumbhash ? hexOrBufferToBase64(item.thumbhash) : 0);
bucketAssets.thumbhash.push(hexOrBufferToBase64(item.thumbhash));
bucketAssets.localDateTime.push(item.localDateTime);
bucketAssets.stack.push(this.mapStack(item.stack) || 0);
bucketAssets.duration.push(item.duration || 0);
bucketAssets.projectionType.push(item.projectionType || 0);
bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId || 0);
bucketAssets.stack.push(this.mapStack(item.stack));
bucketAssets.duration.push(item.duration);
bucketAssets.projectionType.push(item.projectionType);
bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId);
bucketAssets.isImage.push(item.type === AssetType.IMAGE ? 1 : 0);
bucketAssets.isVideo.push(item.type === AssetType.VIDEO ? 1 : 0);
bucketAssets.description.push({
@ -97,7 +97,7 @@ export class TimelineService extends BaseService {
mapStack(entity?: Stack | null) {
if (!entity) {
return;
return null;
}
return {

View File

@ -18,11 +18,11 @@ export type TimeBucketAssets = {
isTrashed: number[];
isVideo: number[];
isImage: number[];
thumbhash: (string | number)[];
thumbhash: (string | null)[];
localDateTime: Date[];
stack: (TimelineStack | number)[];
duration: (string | number)[];
projectionType: (string | number)[];
livePhotoVideoId: (string | number)[];
stack: (TimelineStack | null)[];
duration: (string | null)[];
projectionType: (string | null)[];
livePhotoVideoId: (string | null)[];
description: AssetDescription[];
};

View File

@ -24,7 +24,10 @@ export function asHumanReadable(bytes: number, precision = 1): string {
}
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
export const hexOrBufferToBase64 = (encoded: string | Buffer | null) => {
if (!encoded) {
return null;
}
if (typeof encoded === 'string') {
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
}

View File

@ -409,10 +409,6 @@
}
};
const assetOnFocusHandler = (asset: TimelineAsset) => {
assetInteraction.focussedAssetId = asset.id;
};
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
@ -481,7 +477,6 @@
}}
onSelect={() => handleSelectAssets(toTimelineAsset(asset))}
onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))}
handleFocus={() => assetOnFocusHandler(toTimelineAsset(asset))}
{showArchiveIcon}
asset={toTimelineAsset(asset)}
selected={assetInteraction.hasSelectedAsset(asset.id)}

View File

@ -419,10 +419,6 @@ export class AssetBucket {
};
}
#decodeString(stringOrNumber: string | number) {
return typeof stringOrNumber === 'number' ? null : (stringOrNumber as string);
}
// note - if the assets are not part of this bucket, they will not be added
addAssets(bucketResponse: TimeBucketResponseDto) {
const addContext = new AddContext();
@ -432,20 +428,20 @@ export class AssetBucket {
...bucketResponse.bucketAssets.description[i],
people: [],
},
duration: this.#decodeString(bucketResponse.bucketAssets.duration[i]),
duration: bucketResponse.bucketAssets.duration[i],
id: bucketResponse.bucketAssets.id[i],
isArchived: !!bucketResponse.bucketAssets.isArchived[i],
isFavorite: !!bucketResponse.bucketAssets.isFavorite[i],
isImage: !!bucketResponse.bucketAssets.isImage[i],
isTrashed: !!bucketResponse.bucketAssets.isTrashed[i],
isVideo: !!bucketResponse.bucketAssets.isVideo[i],
livePhotoVideoId: this.#decodeString(bucketResponse.bucketAssets.livePhotoVideoId[i]),
livePhotoVideoId: bucketResponse.bucketAssets.livePhotoVideoId[i],
localDateTime: bucketResponse.bucketAssets.localDateTime[i],
ownerId: bucketResponse.bucketAssets.ownerId[i],
projectionType: this.#decodeString(bucketResponse.bucketAssets.projectionType[i]),
projectionType: bucketResponse.bucketAssets.projectionType[i],
ratio: bucketResponse.bucketAssets.ratio[i],
stack: bucketResponse.bucketAssets.stack[i],
thumbhash: this.#decodeString(bucketResponse.bucketAssets.thumbhash[i]),
thumbhash: bucketResponse.bucketAssets.thumbhash[i],
};
this.addTimelineAsset(timelineAsset, addContext);
}