chore: require number format (#31222)

This commit is contained in:
Jason Rasmussen
2026-09-02 12:33:22 -04:00
committed by GitHub
parent a68937cee7
commit 2c53b76400
7 changed files with 85 additions and 28 deletions
+33 -1
View File
@@ -19889,12 +19889,14 @@
},
"latitude": {
"description": "Latitude coordinate",
"format": "double",
"maximum": 90,
"minimum": -90,
"type": "number"
},
"longitude": {
"description": "Longitude coordinate",
"format": "double",
"maximum": 180,
"minimum": -180,
"type": "number"
@@ -22131,6 +22133,7 @@
"fNumber": {
"default": null,
"description": "F-number (aperture)",
"format": "double",
"nullable": true,
"type": "number"
},
@@ -22145,6 +22148,7 @@
"focalLength": {
"default": null,
"description": "Focal length in mm",
"format": "double",
"nullable": true,
"type": "number"
},
@@ -22159,6 +22163,7 @@
"latitude": {
"default": null,
"description": "GPS latitude",
"format": "double",
"nullable": true,
"type": "number"
},
@@ -22171,6 +22176,7 @@
"longitude": {
"default": null,
"description": "GPS longitude",
"format": "double",
"nullable": true,
"type": "number"
},
@@ -24287,32 +24293,40 @@
"NumberFilter": {
"properties": {
"eq": {
"format": "double",
"type": "number"
},
"gt": {
"format": "double",
"type": "number"
},
"gte": {
"format": "double",
"type": "number"
},
"in": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
"type": "array"
},
"lt": {
"format": "double",
"type": "number"
},
"lte": {
"format": "double",
"type": "number"
},
"ne": {
"format": "double",
"type": "number"
},
"notIn": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
@@ -24324,34 +24338,42 @@
"NumberFilterNullable": {
"properties": {
"eq": {
"format": "double",
"nullable": true,
"type": "number"
},
"gt": {
"format": "double",
"type": "number"
},
"gte": {
"format": "double",
"type": "number"
},
"in": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
"type": "array"
},
"lt": {
"format": "double",
"type": "number"
},
"lte": {
"format": "double",
"type": "number"
},
"ne": {
"format": "double",
"nullable": true,
"type": "number"
},
"notIn": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
@@ -25096,10 +25118,12 @@
},
"latitude": {
"description": "Latitude coordinate",
"format": "double",
"type": "number"
},
"longitude": {
"description": "Longitude coordinate",
"format": "double",
"type": "number"
},
"name": {
@@ -26489,7 +26513,9 @@
"properties": {
"angle": {
"description": "Rotation angle in degrees",
"type": "number"
"maximum": 270,
"minimum": 0,
"type": "integer"
}
},
"required": [
@@ -31554,6 +31580,7 @@
"latitude": {
"description": "Array of latitude coordinates extracted from EXIF GPS data",
"items": {
"format": "double",
"nullable": true,
"type": "number"
},
@@ -31570,6 +31597,7 @@
"localOffsetHours": {
"description": "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
@@ -31577,6 +31605,7 @@
"longitude": {
"description": "Array of longitude coordinates extracted from EXIF GPS data",
"items": {
"format": "double",
"nullable": true,
"type": "number"
},
@@ -31600,6 +31629,7 @@
"ratio": {
"description": "Array of aspect ratios (width/height) for each asset",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
@@ -31783,6 +31813,7 @@
},
"latitude": {
"description": "Latitude coordinate",
"format": "double",
"maximum": 90,
"minimum": -90,
"type": "number"
@@ -31796,6 +31827,7 @@
},
"longitude": {
"description": "Longitude coordinate",
"format": "double",
"maximum": 180,
"minimum": -180,
"type": "number"
+3 -1
View File
@@ -31,7 +31,9 @@ const CropParametersSchema = z
const RotateParametersSchema = z
.object({
angle: z
.number()
.int()
.min(0)
.max(270)
.refine((v) => [0, 90, 180, 270].includes(v), {
error: 'Angle must be one of the following values: 0, 90, 180, 270',
})
+4 -4
View File
@@ -18,12 +18,12 @@ export const ExifResponseSchema = z
modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'),
timeZone: z.string().nullish().default(null).describe('Time zone'),
lensModel: z.string().nullish().default(null).describe('Lens model'),
fNumber: z.number().nullish().default(null).describe('F-number (aperture)'),
focalLength: z.number().nullish().default(null).describe('Focal length in mm'),
fNumber: z.number().meta({ format: 'double' }).nullish().default(null).describe('F-number (aperture)'),
focalLength: z.number().meta({ format: 'double' }).nullish().default(null).describe('Focal length in mm'),
iso: z.int().nullish().default(null).describe('ISO sensitivity'),
exposureTime: z.string().nullish().default(null).describe('Exposure time'),
latitude: z.number().nullish().default(null).describe('GPS latitude'),
longitude: z.number().nullish().default(null).describe('GPS longitude'),
latitude: z.number().meta({ format: 'double' }).nullish().default(null).describe('GPS latitude'),
longitude: z.number().meta({ format: 'double' }).nullish().default(null).describe('GPS longitude'),
city: z.string().nullish().default(null).describe('City name'),
state: z.string().nullish().default(null).describe('State/province name'),
country: z.string().nullish().default(null).describe('Country name'),
+12 -12
View File
@@ -94,8 +94,8 @@ const SearchPeopleSchema = z
const PlacesResponseSchema = z
.object({
name: z.string().describe('Place name'),
latitude: z.number().describe('Latitude coordinate'),
longitude: z.number().describe('Longitude coordinate'),
latitude: z.number().meta({ format: 'double' }).describe('Latitude coordinate'),
longitude: z.number().meta({ format: 'double' }).describe('Longitude coordinate'),
admin1name: z.string().optional().describe('Administrative level 1 name (state/province)'),
admin2name: z.string().optional().describe('Administrative level 2 name (county/district)'),
})
@@ -174,23 +174,23 @@ const StringPatternFilterSchema = nonEmptyPartial({
}).meta({ id: 'StringPatternFilter' });
const numberRangeShape = {
lt: z.number(),
lte: z.number(),
gt: z.number(),
gte: z.number(),
in: z.array(z.number()).min(1),
notIn: z.array(z.number()).min(1),
lt: z.number().meta({ format: 'double' }),
lte: z.number().meta({ format: 'double' }),
gt: z.number().meta({ format: 'double' }),
gte: z.number().meta({ format: 'double' }),
in: z.array(z.number().meta({ format: 'double' })).min(1),
notIn: z.array(z.number().meta({ format: 'double' })).min(1),
};
const NumberFilterSchema = nonEmptyPartial({
eq: z.number(),
ne: z.number(),
eq: z.number().meta({ format: 'double' }),
ne: z.number().meta({ format: 'double' }),
...numberRangeShape,
}).meta({ id: 'NumberFilter' });
const NumberFilterNullableSchema = nonEmptyPartial({
eq: z.number().nullable(),
ne: z.number().nullable(),
eq: z.number().meta({ format: 'double' }).nullable(),
ne: z.number().meta({ format: 'double' }).nullable(),
...numberRangeShape,
}).meta({ id: 'NumberFilterNullable' });
+6 -4
View File
@@ -75,7 +75,9 @@ const TimeBucketAssetResponseSchema = z
.object({
id: z.array(z.string()).describe('Array of asset IDs in the time bucket'),
ownerId: z.array(z.string()).describe('Array of owner IDs for each asset'),
ratio: z.array(z.number()).describe('Array of aspect ratios (width/height) for each asset'),
ratio: z
.array(z.number().meta({ format: 'double' }))
.describe('Array of aspect ratios (width/height) for each asset'),
isFavorite: z.array(z.boolean()).describe('Array indicating whether each asset is favorited'),
visibility: z
.array(AssetVisibilitySchema)
@@ -90,7 +92,7 @@ const TimeBucketAssetResponseSchema = z
.describe('Array of UTC timestamps when each asset was originally uploaded to Immich'),
fileCreatedAt: z.array(z.string()).describe('Array of file creation timestamps in UTC'),
localOffsetHours: z
.array(z.number())
.array(z.number().meta({ format: 'double' }))
.describe(
"Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.",
),
@@ -110,11 +112,11 @@ const TimeBucketAssetResponseSchema = z
city: z.array(z.string().nullable()).optional().describe('Array of city names extracted from EXIF GPS data'),
country: z.array(z.string().nullable()).optional().describe('Array of country names extracted from EXIF GPS data'),
latitude: z
.array(z.number().nullable())
.array(z.number().meta({ format: 'double' }).nullable())
.optional()
.describe('Array of latitude coordinates extracted from EXIF GPS data'),
longitude: z
.array(z.number().nullable())
.array(z.number().meta({ format: 'double' }).nullable())
.optional()
.describe('Array of longitude coordinates extracted from EXIF GPS data'),
})
+25 -4
View File
@@ -225,6 +225,8 @@ const patchOpenAPI = (document: OpenAPIObject) => {
document.components.schemas = sortKeys(schemas);
const errors: string[] = [];
for (const [schemaName, schema] of Object.entries(schemas)) {
if (!schema.properties) {
continue;
@@ -232,16 +234,35 @@ const patchOpenAPI = (document: OpenAPIObject) => {
schema.properties = sortKeys(schema.properties);
for (const [key, value] of Object.entries(schema.properties)) {
if (typeof value === 'string') {
for (const [key, initialValue] of Object.entries(schema.properties)) {
if (typeof initialValue === 'string' || !isSchema(initialValue)) {
continue;
}
if (isSchema(value) && value.type === 'number' && value.format === 'float') {
throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
// check array types
let value: SchemaObject | ReferenceObject = initialValue;
if (value.type === 'array' && value.items) {
value = value.items;
}
if (isSchema(value) && value.type === 'number') {
if (value.format === 'float') {
errors.push(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
}
// verify it was meant to be a number (and not an integer)
if (!value.format) {
errors.push(
`${schemaName}.${key} is a number (not an integer) and requires a format (e.g .meta({ format: 'double' })). `,
);
}
}
}
schema.required?.sort();
if (errors.length > 0) {
throw new Error(`Schema validation failed:\n ${errors.join('\n ')}`);
}
}
}
+2 -2
View File
@@ -185,7 +185,7 @@ export const isoDateToDate = z
* // Pipe (query): coerce string to number then validate range
* z.coerce.number().pipe(latitudeSchema).describe('Latitude (-90 to 90)')
*/
export const latitudeSchema = z.number().min(-90).max(90);
export const latitudeSchema = z.number().meta({ format: 'double' }).min(-90).max(90);
/**
* Longitude in range [-180, 180]. Reuse for body or query params.
@@ -198,7 +198,7 @@ export const latitudeSchema = z.number().min(-90).max(90);
* // Pipe (query): coerce string to number then validate range
* z.coerce.number().pipe(longitudeSchema).describe('Longitude (-180 to 180)')
*/
export const longitudeSchema = z.number().min(-180).max(180);
export const longitudeSchema = z.number().meta({ format: 'double' }).min(-180).max(180);
/**
* Parse string to boolean