diff --git a/packages/server/src/api/storage/sql/sql.js b/packages/server/src/api/storage/sql/sql.js index 403d0c301..0b7e2efae 100644 --- a/packages/server/src/api/storage/sql/sql.js +++ b/packages/server/src/api/storage/sql/sql.js @@ -9,7 +9,7 @@ const path = require('path'); const uuid = require('uuid'); const Umzug = require('umzug'); const Sequelize = require('sequelize'); -const {omit} = require('@lhci/utils/src/lodash.js'); +const {omit, padEnd} = require('@lhci/utils/src/lodash.js'); const {E422} = require('../../express-utils.js'); const StorageMethod = require('../storage-method.js'); const projectModelDefn = require('./project-model.js'); @@ -40,6 +40,17 @@ function isUuid(id) { ); } +/** + * @param {string} uuid + * @param {string} filler + */ +function formatAsUuid(uuid, filler = '0') { + const parts = padEnd(uuid, 32, filler).match(/\w{4}/g); + if (!parts || parts.length !== 8) throw new Error('Invalid UUID'); + const [p1, p2, p3, p4, p5, p6, p7, p8] = parts; + return `${p1}${p2}-${p3}-${p4}-${p5}-${p6}${p7}${p8}`; +} + /** * @param {string|undefined} id */ @@ -357,10 +368,26 @@ class SqlStorageMethod { */ async findBuildById(projectId, buildId) { const {buildModel} = this._sql(); + if (isUuid(buildId)) { + const build = await this._findByPk(buildModel, buildId); + if (build && build.projectId !== projectId) return undefined; + return clone(build || undefined); + } + if (!validatePartialUuidOrUndefined(buildId)) return undefined; - const idMatch = isUuid(buildId) ? buildId : {[Sequelize.Op.like]: `${buildId}%`}; + + // Postgres stores UUIDs as numbers so it's impossible to do a pattern match. + // Instead we'll do a range check which works whether the UUID is treated as a string or a + // number. For example... + // + // Given the prefix `a82fb732` we can look for all UUIDs that are... + // >= a82fb732-0000-0000-0000-000000000000 + // <= a82fb732-ffff-ffff-ffff-ffffffffffff + const numericValue = parseInt(buildId.replace(/-/g, ''), 16); + const lowerUuid = formatAsUuid(numericValue.toString(16), '0'); + const upperUuid = formatAsUuid(numericValue.toString(16), 'f'); const builds = await buildModel.findAll({ - where: {id: idMatch, projectId}, + where: {id: {[Sequelize.Op.gte]: lowerUuid, [Sequelize.Op.lte]: upperUuid}, projectId}, limit: 2, }); diff --git a/packages/utils/src/lodash.js b/packages/utils/src/lodash.js index 28064e394..98afbd84e 100644 --- a/packages/utils/src/lodash.js +++ b/packages/utils/src/lodash.js @@ -107,6 +107,15 @@ module.exports = { if (s.length >= length) return s; return `${padding.repeat(length)}${s}`.slice(-length); }, + /** + * @param {string} s + * @param {number} length + * @param {string} [padding] + */ + padEnd(s, length, padding = ' ') { + if (s.length >= length) return s; + return `${s}${padding.repeat(length)}`.slice(0, length); + }, /** * Deep clones an object via JSON.parse/JSON.stringify. * @template T