Skip to content

Commit

Permalink
Fix error when seeding in AWS non-production (#1400)
Browse files Browse the repository at this point in the history
Unlike migrations, which are only expected to be run once against a DB, seeders may be run repeatedly. For example, a new charge version, 'change reason', is required in the future. We would add it to our `db/seeds/data/change-reasons.js` and re-run `npm run seed`.

Because of this, each seeder needs to be able to handle being run against an environment where the records may already exist.

In most cases, we can exploit constraints in the DB and PostgreSQL's [ON CONFLICT clause](https://www.postgresql.org/docs/current/sql-insert.html), also known as an 'upsert'.

We try to insert a record, but when a conflict error is thrown because a matching record already exists, we can instruct PostgreSQL to update certain fields instead.

That is unless it is `idm.users`! The previous team appear to have opted instead to create their own custom sequence and use a number as the ID. 😩

Instead of the expected `on conflict` being raised, we're getting a primary key violation. The simplest solution is to check if a record exists first. We can then determine if we need to insert our user record or update an existing one.

If we are updating, we must then check if the seed ID we want to use is already in use. If it is, we have to drop ID from our insert statement and let the custom sequence assign us one instead.
  • Loading branch information
Cruikshanks authored Oct 10, 2024
1 parent 06c15dd commit f710548
Showing 1 changed file with 90 additions and 14 deletions.
104 changes: 90 additions & 14 deletions db/seeds/09-users.seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,29 @@ async function seed () {
const password = _generateHashedPassword()

for (const user of users) {
await _upsert(user, password)
const exists = await _exists(user)

if (exists) {
await _update(user, password)
} else {
await _insert(user, password)
}
}
}

async function _exists (user) {
const { application, username } = user

const result = await UserModel.query()
.select('id')
.where('application', application)
.andWhere('username', username)
.limit(1)
.first()

return !!result
}

function _generateHashedPassword () {
// 10 is the number of salt rounds to perform to generate the salt. The legacy code uses
// const salt = bcrypt.genSaltSync(10) to pre-generate the salt before passing it to hashSync(). But this is
Expand All @@ -31,20 +50,77 @@ function _generateHashedPassword () {
return bcrypt.hashSync(DatabaseConfig.defaultUserPassword, 10)
}

async function _upsert (user, password) {
async function _idInUse (id) {
const result = await UserModel.query()
.findById(id)

return !!result
}

async function _insert (user, password) {
const {
application,
badLogins,
enabled,
id,
lastLogin,
resetGuid,
resetGuidCreatedAt,
resetRequired,
username
} = user

// NOTE: Seeding users is a pain (!) because of the previous teams choice to use a custom sequence for the ID instead
// of sticking with UUIDs. This means it is possible that, for example, a user with
//
// `username = '[email protected]' && application = 'water_admin'`
//
// does not exist. _But_ a user with ID 100000 does! So, we do want to insert our record, but we can't use the ID
// because it is already in use. We only really face this problem when running the seed in our AWS environments.
const idInUse = await _idInUse(id)

if (idInUse) {
return UserModel.query().insert({
application,
badLogins,
enabled,
lastLogin,
password,
resetGuid,
resetGuidCreatedAt,
resetRequired,
username
})
}

return UserModel.query().insert({ ...user, password })
}

async function _update (user, password) {
const {
application,
badLogins,
enabled,
lastLogin,
resetGuid,
resetGuidCreatedAt,
resetRequired,
username
} = user

return UserModel.query()
.insert({ ...user, password, updatedAt: timestampForPostgres() })
.onConflict(['application', 'username'])
.merge([
'badLogins',
'enabled',
'lastLogin',
'password',
'resetGuid',
'resetGuidCreatedAt',
'resetRequired',
'updatedAt'
])
.patch({
badLogins,
enabled,
lastLogin,
password,
resetGuid,
resetGuidCreatedAt,
resetRequired,
updatedAt: timestampForPostgres()
})
.where('application', application)
.andWhere('username', username)
}

module.exports = {
Expand Down

0 comments on commit f710548

Please sign in to comment.