1002 feature request add creation timestamp to the clan data - #1004
Conversation
Codecov Report❌ Patch coverage is
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
tickBit
left a comment
There was a problem hiding this comment.
Clear and good work 👍 .
Running the migration in the production server is beyond my scope in this project, though.
That is why I propose a similar temporal refresh system for clans, that we have for the daily tasks. It would be less work and easier to implement.
The flow could be:
- New temporal file
src/clan/clanTimestampsStartupRefresh.service.ts:
import {
Injectable,
Logger,
OnApplicationBootstrap,
} from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
import { randomUUID } from 'node:crypto';
import { hostname } from 'node:os';
import { ModelName } from '../common/enum/modelName.enum';
const LOCK_ID = 'clan-timestamps-startup-refresh';
const LOCK_TTL_MS = 30 * 60 * 1000;
/**
* This represents 1 September 2026 at 00:00 in Finland
* while daylight saving time is active.
*
* MongoDB stores it internally as 2026-08-31T21:00:00.000Z.
*/
const INITIAL_CLAN_TIMESTAMP = new Date(
'2026-09-01T00:00:00.000+03:00',
);
type MaintenanceLock = {
_id: string;
ownerId: string;
lockedAt: Date;
expiresAt: Date;
};
@Injectable()
export class ClanTimestampsStartupRefreshService
implements OnApplicationBootstrap
{
private readonly logger = new Logger(
ClanTimestampsStartupRefreshService.name,
);
private readonly ownerId =
`${hostname()}-${process.pid}-${randomUUID()}`;
constructor(
@InjectConnection()
private readonly connection: Connection,
) {}
async onApplicationBootstrap(): Promise<void> {
try {
if (!(await this.hasClansWithoutTimestamps())) {
return;
}
const lockAcquired = await this.tryAcquireLock();
if (!lockAcquired) {
this.logger.log(
'Clan timestamp initialization skipped; another instance is running it.',
);
return;
}
try {
// Another instance could have completed the operation
// between the first check and lock acquisition.
if (!(await this.hasClansWithoutTimestamps())) {
return;
}
await this.initializeMissingTimestamps();
} finally {
await this.releaseLock();
}
} catch (error) {
this.logger.error(
'Clan timestamp initialization failed',
error instanceof Error ? error.stack : String(error),
);
// Choose this if the API must not run without the timestamps:
throw error;
}
}
private async hasClansWithoutTimestamps(): Promise<boolean> {
const clan = await this.connection.db
.collection(ModelName.CLAN)
.findOne({
$or: [
{ createdAt: { $exists: false } },
{ updatedAt: { $exists: false } },
],
});
return clan !== null;
}
private async initializeMissingTimestamps(): Promise<void> {
const clans = this.connection.db.collection(ModelName.CLAN);
/*
* These are deliberately two separate updates:
* an existing timestamp must never be overwritten merely because
* the other timestamp is missing.
*
* Using the native collection also prevents Mongoose timestamps
* middleware from replacing updatedAt with the current time.
*/
const createdAtResult = await clans.updateMany(
{ createdAt: { $exists: false } },
{
$set: {
createdAt: INITIAL_CLAN_TIMESTAMP,
},
},
);
const updatedAtResult = await clans.updateMany(
{ updatedAt: { $exists: false } },
{
$set: {
updatedAt: INITIAL_CLAN_TIMESTAMP,
},
},
);
this.logger.log(
[
'Clan timestamp initialization completed.',
`createdAt initialized for ${createdAtResult.modifiedCount} clans.`,
`updatedAt initialized for ${updatedAtResult.modifiedCount} clans.`,
].join(' '),
);
}
private async tryAcquireLock(): Promise<boolean> {
const now = new Date();
const expiresAt = new Date(now.getTime() + LOCK_TTL_MS);
try {
const lock = await this.connection.db
.collection<MaintenanceLock>('MaintenanceLock')
.findOneAndUpdate(
{
_id: LOCK_ID,
$or: [
{ expiresAt: { $lte: now } },
{ expiresAt: { $exists: false } },
],
},
{
$set: {
ownerId: this.ownerId,
lockedAt: now,
expiresAt,
},
},
{
upsert: true,
returnDocument: 'after',
},
);
return lock?.ownerId === this.ownerId;
} catch {
// Concurrent upserts can cause a duplicate-key error.
// In that situation another instance owns the lock.
return false;
}
}
private async releaseLock(): Promise<void> {
await this.connection.db
.collection<MaintenanceLock>('MaintenanceLock')
.deleteOne({
_id: LOCK_ID,
ownerId: this.ownerId,
});
}
}- Register the service in
src/clan/clan.module.ts:
Import:
import { ClanTimestampsStartupRefreshService } from './clanTimestampsStartupRefresh.service';Provider:
providers: [
ClanService,
isClanExists,
PlayerCounterFactory,
JoinService,
ClanHelperService,
ClanRoleService,
ClanRoleVotingProcessor,
PasswordGenerator,
ClanTimestampsStartupRefreshService,
],Registering the service as a provider is all it takes for Nest to create the service and call the onApplicationBootstrap() method.
For the Startup service, you should test at least the following:
- Both are missing → both are initialized.
- Only
createdAtis missing → the existingupdatedAtis retained. - Only
updatedAtis missing → the existingcreatedAtis retained. - Both are present → the document is not modified.
- Re-run → nothing changes.
- Lock cannot be obtained → the update is not executed.
- The lock is released after a successful run.
- The lock is also released if the update fails.
- A new clan receives the current timestamps, not a fixed backfill date.
- A normal clan update changes the
updatedAtvalue but not thecreatedAtvalue. - The API response includes both fields.
It’s a good idea to keep the startup service running at least until we’re certain that all production environments have been upgraded to the new version. After that, the service and any MaintenanceLock document can be removed in a separate cleanup PR.
When we know, that there won't be any new daily tasks or any of the currently available is not be removed, we could clean the code by removing the daily tasks refresh service, too.
|
Seems like a good idea if we cant run a migration, I can implement it in this PR |
|
Is it necessary to have updatedAt field if the issue only requires data of creation date timestamp? |
Good point 👍 ! I don't think it's necessary. We were asked to add createdAt only 👍 |
tickBit
left a comment
There was a problem hiding this comment.
Good work 👍 ! Approved.
Brief description
This adds creation date timestamp for Clans. Mongoose now sets createdAt field automatically to clans upon creation.
Change list
npm run migrate:up.