Skip to content

Commit 22e2149

Browse files
authored
Merge pull request finos#1656 from sankalpsthakur/prep/1486-repo-date-fields
feat(db): add dateCreated and lastModified to Repo
2 parents 78a34ef + 561fa18 commit 22e2149

6 files changed

Lines changed: 222 additions & 12 deletions

File tree

src/db/file/repo.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ export const getRepoById = async (_id: string): Promise<Repo | null> => {
106106
};
107107

108108
export const createRepo = async (repo: Repo): Promise<Repo> => {
109+
const now = new Date().toISOString();
110+
if (!repo.dateCreated) repo.dateCreated = now;
111+
if (!repo.lastModified) repo.lastModified = now;
112+
109113
return new Promise<Repo>((resolve, reject) => {
110114
db.insert(repo, (err, doc) => {
111115
// ignore for code coverage as neDB rarely returns errors even for an invalid query
@@ -158,6 +162,7 @@ export const addUserCanPush = async (_id: string, user: string): Promise<void> =
158162
return;
159163
}
160164
repo.users?.canPush.push(user);
165+
repo.lastModified = new Date().toISOString();
161166

162167
const options = { multi: false, upsert: false };
163168

@@ -186,6 +191,7 @@ export const addUserCanAuthorise = async (_id: string, user: string): Promise<vo
186191
}
187192

188193
repo.users.canAuthorise.push(user);
194+
repo.lastModified = new Date().toISOString();
189195

190196
const options = { multi: false, upsert: false };
191197

@@ -210,6 +216,7 @@ export const removeUserCanAuthorise = async (_id: string, user: string): Promise
210216
}
211217

212218
repo.users.canAuthorise = repo.users.canAuthorise.filter((x: string) => x != user);
219+
repo.lastModified = new Date().toISOString();
213220

214221
const options = { multi: false, upsert: false };
215222

@@ -234,6 +241,7 @@ export const removeUserCanPush = async (_id: string, user: string): Promise<void
234241
}
235242

236243
repo.users.canPush = repo.users.canPush.filter((x) => x != user);
244+
repo.lastModified = new Date().toISOString();
237245

238246
const options = { multi: false, upsert: false };
239247

src/db/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,15 @@ export const createUser = async (
113113
};
114114

115115
export const createRepo = async (repo: AuthorisedRepo) => {
116+
const now = new Date().toISOString();
116117
const toCreate = {
117118
...repo,
118119
users: {
119120
canPush: [],
120121
canAuthorise: [],
121122
},
123+
dateCreated: now,
124+
lastModified: now,
122125
};
123126
toCreate.name = repo.name.toLowerCase();
124127

src/db/mongo/repo.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export const getRepoById = async (_id: string): Promise<Repo | null> => {
4848
};
4949

5050
export const createRepo = async (repo: Repo): Promise<Repo> => {
51+
const now = new Date().toISOString();
52+
if (!repo.dateCreated) repo.dateCreated = now;
53+
if (!repo.lastModified) repo.lastModified = now;
54+
5155
const collection = await connect(collectionName);
5256
const response = await collection.insertOne(repo as OptionalId<Document>);
5357
console.log(`created new repo ${JSON.stringify(repo)}`);
@@ -76,25 +80,37 @@ export const updateRepo = async (repo: Partial<Repo>): Promise<void> => {
7680
export const addUserCanPush = async (_id: string, user: string): Promise<void> => {
7781
user = user.toLowerCase();
7882
const collection = await connect(collectionName);
79-
await collection.updateOne({ _id: new ObjectId(_id) }, { $push: { 'users.canPush': user } });
83+
await collection.updateOne(
84+
{ _id: new ObjectId(_id) },
85+
{ $push: { 'users.canPush': user }, $set: { lastModified: new Date().toISOString() } },
86+
);
8087
};
8188

8289
export const addUserCanAuthorise = async (_id: string, user: string): Promise<void> => {
8390
user = user.toLowerCase();
8491
const collection = await connect(collectionName);
85-
await collection.updateOne({ _id: new ObjectId(_id) }, { $push: { 'users.canAuthorise': user } });
92+
await collection.updateOne(
93+
{ _id: new ObjectId(_id) },
94+
{ $push: { 'users.canAuthorise': user }, $set: { lastModified: new Date().toISOString() } },
95+
);
8696
};
8797

8898
export const removeUserCanPush = async (_id: string, user: string): Promise<void> => {
8999
user = user.toLowerCase();
90100
const collection = await connect(collectionName);
91-
await collection.updateOne({ _id: new ObjectId(_id) }, { $pull: { 'users.canPush': user } });
101+
await collection.updateOne(
102+
{ _id: new ObjectId(_id) },
103+
{ $pull: { 'users.canPush': user }, $set: { lastModified: new Date().toISOString() } },
104+
);
92105
};
93106

94107
export const removeUserCanAuthorise = async (_id: string, user: string): Promise<void> => {
95108
user = user.toLowerCase();
96109
const collection = await connect(collectionName);
97-
await collection.updateOne({ _id: new ObjectId(_id) }, { $pull: { 'users.canAuthorise': user } });
110+
await collection.updateOne(
111+
{ _id: new ObjectId(_id) },
112+
{ $pull: { 'users.canAuthorise': user }, $set: { lastModified: new Date().toISOString() } },
113+
);
98114
};
99115

100116
export const deleteRepo = async (_id: string): Promise<void> => {

src/db/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ export class Repo {
5858
name: string;
5959
url: string;
6060
users: { canPush: string[]; canAuthorise: string[] };
61+
/**
62+
* ISO-8601; set on create, never overwritten thereafter.
63+
* Existing repos missing this field are intentionally left unset here —
64+
* backfill belongs in a follow-up versioned migration (Mongo: prefer
65+
* `$toDate: "$_id"` over an epoch default). Do not reintroduce startup
66+
* one-off backfills.
67+
*/
68+
dateCreated?: string;
69+
/**
70+
* ISO-8601; set on create and bumped on repo metadata mutations.
71+
* Same migration note as {@link Repo.dateCreated}.
72+
*/
73+
lastModified?: string;
6174
_id?: string;
6275

6376
constructor(
@@ -66,12 +79,16 @@ export class Repo {
6679
url: string,
6780
users?: Record<UserRole, string[]>,
6881
_id?: string,
82+
dateCreated?: string,
83+
lastModified?: string,
6984
) {
7085
this.project = project;
7186
this.name = name;
7287
this.url = url;
7388
this.users = users ?? { canPush: [], canAuthorise: [] };
7489
this._id = _id;
90+
this.dateCreated = dateCreated;
91+
this.lastModified = lastModified;
7592
}
7693
}
7794

test/db/mongo/repo.test.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ describe('MongoDB Repo', async () => {
216216
expect(mockInsertOne).toHaveBeenCalledWith(newRepo);
217217
expect(result._id).toBe(insertedId.toString());
218218
expect(result.name).toBe('new-repo');
219+
expect(result.dateCreated).toEqual(expect.any(String));
220+
expect(result.lastModified).toEqual(expect.any(String));
219221
expect(consoleSpy).toHaveBeenCalled();
220222

221223
consoleSpy.mockRestore();
@@ -282,7 +284,10 @@ describe('MongoDB Repo', async () => {
282284
expect(mockConnect).toHaveBeenCalledWith('repos');
283285
expect(mockUpdateOne).toHaveBeenCalledWith(
284286
{ _id: new ObjectId(TEST_REPO._id!) },
285-
{ $push: { 'users.canPush': 'newuser' } },
287+
{
288+
$push: { 'users.canPush': 'newuser' },
289+
$set: { lastModified: expect.any(String) },
290+
},
286291
);
287292
});
288293

@@ -293,7 +298,10 @@ describe('MongoDB Repo', async () => {
293298

294299
expect(mockUpdateOne).toHaveBeenCalledWith(
295300
{ _id: new ObjectId(TEST_REPO._id!) },
296-
{ $push: { 'users.canPush': 'uppercase' } },
301+
{
302+
$push: { 'users.canPush': 'uppercase' },
303+
$set: { lastModified: expect.any(String) },
304+
},
297305
);
298306
});
299307
});
@@ -307,7 +315,10 @@ describe('MongoDB Repo', async () => {
307315
expect(mockConnect).toHaveBeenCalledWith('repos');
308316
expect(mockUpdateOne).toHaveBeenCalledWith(
309317
{ _id: new ObjectId(TEST_REPO._id!) },
310-
{ $push: { 'users.canAuthorise': 'newadmin' } },
318+
{
319+
$push: { 'users.canAuthorise': 'newadmin' },
320+
$set: { lastModified: expect.any(String) },
321+
},
311322
);
312323
});
313324

@@ -318,7 +329,10 @@ describe('MongoDB Repo', async () => {
318329

319330
expect(mockUpdateOne).toHaveBeenCalledWith(
320331
{ _id: new ObjectId(TEST_REPO._id!) },
321-
{ $push: { 'users.canAuthorise': 'admin' } },
332+
{
333+
$push: { 'users.canAuthorise': 'admin' },
334+
$set: { lastModified: expect.any(String) },
335+
},
322336
);
323337
});
324338
});
@@ -332,7 +346,10 @@ describe('MongoDB Repo', async () => {
332346
expect(mockConnect).toHaveBeenCalledWith('repos');
333347
expect(mockUpdateOne).toHaveBeenCalledWith(
334348
{ _id: new ObjectId(TEST_REPO._id!) },
335-
{ $pull: { 'users.canPush': 'user1' } },
349+
{
350+
$pull: { 'users.canPush': 'user1' },
351+
$set: { lastModified: expect.any(String) },
352+
},
336353
);
337354
});
338355

@@ -343,7 +360,10 @@ describe('MongoDB Repo', async () => {
343360

344361
expect(mockUpdateOne).toHaveBeenCalledWith(
345362
{ _id: new ObjectId(TEST_REPO._id!) },
346-
{ $pull: { 'users.canPush': 'user' } },
363+
{
364+
$pull: { 'users.canPush': 'user' },
365+
$set: { lastModified: expect.any(String) },
366+
},
347367
);
348368
});
349369
});
@@ -357,7 +377,10 @@ describe('MongoDB Repo', async () => {
357377
expect(mockConnect).toHaveBeenCalledWith('repos');
358378
expect(mockUpdateOne).toHaveBeenCalledWith(
359379
{ _id: new ObjectId(TEST_REPO._id!) },
360-
{ $pull: { 'users.canAuthorise': 'admin1' } },
380+
{
381+
$pull: { 'users.canAuthorise': 'admin1' },
382+
$set: { lastModified: expect.any(String) },
383+
},
361384
);
362385
});
363386

@@ -368,7 +391,10 @@ describe('MongoDB Repo', async () => {
368391

369392
expect(mockUpdateOne).toHaveBeenCalledWith(
370393
{ _id: new ObjectId(TEST_REPO._id!) },
371-
{ $pull: { 'users.canAuthorise': 'admin' } },
394+
{
395+
$pull: { 'users.canAuthorise': 'admin' },
396+
$set: { lastModified: expect.any(String) },
397+
},
372398
);
373399
});
374400
});

test/db/repo.date-fields.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* Copyright 2026 GitProxy Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
18+
import * as repoModule from '../../src/db/file/repo';
19+
import { Repo } from '../../src/db/types';
20+
21+
describe('Repo dateCreated / lastModified (#1486)', () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
});
25+
26+
afterEach(() => {
27+
vi.restoreAllMocks();
28+
});
29+
30+
it('createRepo persists dateCreated and lastModified as ISO-8601', async () => {
31+
const inserted: Repo[] = [];
32+
vi.spyOn(repoModule.db, 'insert').mockImplementation((doc: unknown, cb: any) => {
33+
const stored = { ...(doc as Repo), _id: 'new-id' };
34+
inserted.push(stored);
35+
cb(null, stored);
36+
});
37+
38+
const before = Date.now();
39+
const result = await repoModule.createRepo(
40+
new Repo('finos', 'sample', 'https://github.com/finos/sample.git'),
41+
);
42+
const after = Date.now();
43+
44+
expect(result.dateCreated).toEqual(expect.any(String));
45+
expect(result.lastModified).toEqual(expect.any(String));
46+
expect(Date.parse(result.dateCreated!)).toBeGreaterThanOrEqual(before);
47+
expect(Date.parse(result.dateCreated!)).toBeLessThanOrEqual(after);
48+
expect(result.dateCreated).toBe(result.lastModified);
49+
expect(inserted[0].dateCreated).toBe(result.dateCreated);
50+
});
51+
52+
it('addUserCanPush bumps lastModified but leaves dateCreated unchanged', async () => {
53+
const existing: Repo = {
54+
project: 'finos',
55+
name: 'sample',
56+
url: 'https://github.com/finos/sample.git',
57+
users: { canPush: [], canAuthorise: [] },
58+
dateCreated: '2020-01-01T00:00:00.000Z',
59+
lastModified: '2020-01-01T00:00:00.000Z',
60+
_id: 'abc',
61+
};
62+
63+
vi.spyOn(repoModule.db, 'findOne').mockImplementation((_: unknown, cb: any) =>
64+
cb(null, { ...existing }),
65+
);
66+
let updatedDoc: Repo | null = null;
67+
vi.spyOn(repoModule.db, 'update').mockImplementation(
68+
(_q: unknown, doc: any, _o: unknown, cb: any) => {
69+
updatedDoc = doc;
70+
cb(null, 1);
71+
},
72+
);
73+
74+
await repoModule.addUserCanPush('abc', 'alice');
75+
76+
expect(updatedDoc!.dateCreated).toBe('2020-01-01T00:00:00.000Z');
77+
expect(updatedDoc!.lastModified).not.toBe('2020-01-01T00:00:00.000Z');
78+
expect(Date.parse(updatedDoc!.lastModified!)).toBeGreaterThan(
79+
Date.parse('2020-01-01T00:00:00.000Z'),
80+
);
81+
expect(updatedDoc!.users.canPush).toContain('alice');
82+
});
83+
84+
it('removeUserCanAuthorise bumps lastModified but leaves dateCreated unchanged', async () => {
85+
const existing: Repo = {
86+
project: 'finos',
87+
name: 'sample',
88+
url: 'https://github.com/finos/sample.git',
89+
users: { canPush: [], canAuthorise: ['bob'] },
90+
dateCreated: '2020-01-01T00:00:00.000Z',
91+
lastModified: '2020-01-01T00:00:00.000Z',
92+
_id: 'abc',
93+
};
94+
95+
vi.spyOn(repoModule.db, 'findOne').mockImplementation((_: unknown, cb: any) =>
96+
cb(null, { ...existing }),
97+
);
98+
let updatedDoc: Repo | null = null;
99+
vi.spyOn(repoModule.db, 'update').mockImplementation(
100+
(_q: unknown, doc: any, _o: unknown, cb: any) => {
101+
updatedDoc = doc;
102+
cb(null, 1);
103+
},
104+
);
105+
106+
await repoModule.removeUserCanAuthorise('abc', 'bob');
107+
108+
expect(updatedDoc!.dateCreated).toBe('2020-01-01T00:00:00.000Z');
109+
expect(updatedDoc!.lastModified).not.toBe('2020-01-01T00:00:00.000Z');
110+
expect(updatedDoc!.users.canAuthorise).not.toContain('bob');
111+
});
112+
113+
it('sort by dateCreated orders oldest → newest (asc)', () => {
114+
const repos: Pick<Repo, 'name' | 'dateCreated'>[] = [
115+
{ name: 'zeta', dateCreated: '2024-06-01T00:00:00.000Z' },
116+
{ name: 'alpha', dateCreated: '2023-01-01T00:00:00.000Z' },
117+
{ name: 'mid', dateCreated: '2023-12-01T00:00:00.000Z' },
118+
];
119+
120+
const sorted = [...repos].sort(
121+
(a, b) => new Date(a.dateCreated || 0).getTime() - new Date(b.dateCreated || 0).getTime(),
122+
);
123+
124+
expect(sorted.map((r) => r.name)).toEqual(['alpha', 'mid', 'zeta']);
125+
});
126+
127+
it('sort by lastModified orders oldest → newest (asc)', () => {
128+
const repos: Pick<Repo, 'name' | 'lastModified'>[] = [
129+
{ name: 'fresh', lastModified: '2025-01-01T00:00:00.000Z' },
130+
{ name: 'stale', lastModified: '2022-01-01T00:00:00.000Z' },
131+
{ name: 'mid', lastModified: '2024-01-01T00:00:00.000Z' },
132+
];
133+
134+
const sorted = [...repos].sort(
135+
(a, b) => new Date(a.lastModified || 0).getTime() - new Date(b.lastModified || 0).getTime(),
136+
);
137+
138+
expect(sorted.map((r) => r.name)).toEqual(['stale', 'mid', 'fresh']);
139+
});
140+
});

0 commit comments

Comments
 (0)