Skip to content

Commit 3d86296

Browse files
authored
Merge branch 'main' into update-governance-3
2 parents 995397b + 112fde6 commit 3d86296

9 files changed

Lines changed: 318 additions & 1290 deletions

File tree

.github/workflows/unused-dependencies.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
node-version: '24.x'
2222
- name: 'Run depcheck'
2323
run: |
24-
npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths,quicktype,history,@types/domutils,@vitest/coverage-v8,cross-env,c8,tailwindcss,react-is"
24+
npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,ts-node,concurrently,prettier,typescript,tsconfig-paths,vite-tsconfig-paths,quicktype,history,@types/domutils,@vitest/coverage-v8,cross-env,c8,tailwindcss,react-is"
2525
echo $?
2626
if [[ $? == 1 ]]; then
2727
echo "Unused dependencies or devDependencies found"

nyc.config.js

Lines changed: 0 additions & 23 deletions
This file was deleted.

package-lock.json

Lines changed: 111 additions & 1243 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@
195195
"globals": "^17.6.0",
196196
"husky": "^9.1.7",
197197
"lint-staged": "^17.0.5",
198-
"nyc": "^18.0.0",
199198
"prettier": "^3.8.1",
200199
"quicktype": "^23.2.6",
201200
"supertest": "^7.2.2",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 type { Migration } from './index';
18+
19+
/**
20+
* Backfills dateCreated/lastModified on repos created before those fields
21+
* existed. Reads through the sink, asks the backend to derive a creation time
22+
* (Mongo derives it from the ObjectId; the file backend has no source and
23+
* returns undefined), and writes through the sink. Repos that already have a
24+
* dateCreated are skipped, so the migration is safe to re-run.
25+
*/
26+
export const populateRepoDates: Migration = {
27+
id: '20260729-populate-repo-dates',
28+
29+
up: async (sink) => {
30+
const repos = await sink.getRepos();
31+
for (const repo of repos) {
32+
if (repo.dateCreated || !repo._id) {
33+
continue;
34+
}
35+
const created = sink.deriveCreatedAt(repo._id) ?? new Date().toISOString();
36+
await sink.updateRepo({
37+
_id: repo._id,
38+
dateCreated: created,
39+
lastModified: repo.lastModified ?? created,
40+
});
41+
}
42+
},
43+
44+
down: async (sink) => {
45+
const repos = await sink.getRepos();
46+
for (const repo of repos) {
47+
if (!repo._id) {
48+
continue;
49+
}
50+
await sink.updateRepo({
51+
_id: repo._id,
52+
dateCreated: undefined,
53+
lastModified: undefined,
54+
});
55+
}
56+
},
57+
};

src/db/migrations/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
*/
1616

1717
import type { Migration } from './index';
18+
import { populateRepoDates } from './populateRepoDates';
1819

19-
export const migrations: Migration[] = [];
20+
export const migrations: Migration[] = [populateRepoDates];

test/db/db.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ import { SAMPLE_REPO } from '../../src/proxy/constants';
1919

2020
vi.mock('../../src/db/mongo', () => ({
2121
getRepoByUrl: vi.fn(),
22+
getRepos: vi.fn().mockResolvedValue([]),
23+
updateRepo: vi.fn(),
24+
deriveCreatedAt: vi.fn(),
2225
getAppliedMigrations: vi.fn().mockResolvedValue([]),
2326
recordMigration: vi.fn(),
2427
unrecordMigration: vi.fn(),
2528
}));
2629

2730
vi.mock('../../src/db/file', () => ({
2831
getRepoByUrl: vi.fn(),
32+
getRepos: vi.fn().mockResolvedValue([]),
33+
updateRepo: vi.fn(),
34+
deriveCreatedAt: vi.fn(),
2935
getAppliedMigrations: vi.fn().mockResolvedValue([]),
3036
recordMigration: vi.fn(),
3137
unrecordMigration: vi.fn(),
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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, vi } from 'vitest';
18+
import { populateRepoDates } from '../../../src/db/migrations/populateRepoDates';
19+
import type { Sink } from '../../../src/db/types';
20+
21+
type RepoRecord = Record<string, unknown> & { _id: string };
22+
23+
const makeSink = (repos: RepoRecord[], deriveCreatedAt: (id: string) => string | undefined) => {
24+
const store = new Map(repos.map((r) => [r._id, { ...r }]));
25+
const updateRepo = vi.fn(async (repo: Record<string, unknown>) => {
26+
const { _id, ...fields } = repo;
27+
Object.assign(store.get(_id as string) as RepoRecord, fields);
28+
});
29+
const sink = {
30+
getRepos: async () => [...store.values()],
31+
updateRepo,
32+
deriveCreatedAt,
33+
} as unknown as Sink;
34+
return { sink, updateRepo, get: (id: string) => store.get(id) };
35+
};
36+
37+
describe('populateRepoDates migration', () => {
38+
it('is registered under a sortable, timestamped id', () => {
39+
expect(populateRepoDates.id).toBe('20260729-populate-repo-dates');
40+
});
41+
42+
it('backfills dateCreated from the backend-derived value', async () => {
43+
const { sink, get } = makeSink([{ _id: 'r1' }], () => '2020-01-01T00:00:00.000Z');
44+
45+
await populateRepoDates.up(sink);
46+
47+
expect(get('r1')?.dateCreated).toBe('2020-01-01T00:00:00.000Z');
48+
expect(get('r1')?.lastModified).toBe('2020-01-01T00:00:00.000Z');
49+
});
50+
51+
it('falls back to the run time when the backend cannot derive one', async () => {
52+
// the file backend always returns undefined - NeDB ids carry no timestamp
53+
const before = Date.now();
54+
const { sink, get } = makeSink([{ _id: 'r1' }], () => undefined);
55+
56+
await populateRepoDates.up(sink);
57+
58+
const created = get('r1')?.dateCreated as string;
59+
expect(Date.parse(created)).toBeGreaterThanOrEqual(before);
60+
expect(get('r1')?.lastModified).toBe(created);
61+
});
62+
63+
it('preserves an existing lastModified instead of overwriting it', async () => {
64+
// role mutations bump lastModified on repos that never had a dateCreated
65+
const { sink, get } = makeSink(
66+
[{ _id: 'r1', lastModified: '2024-06-01T00:00:00.000Z' }],
67+
() => '2020-01-01T00:00:00.000Z',
68+
);
69+
70+
await populateRepoDates.up(sink);
71+
72+
expect(get('r1')?.dateCreated).toBe('2020-01-01T00:00:00.000Z');
73+
expect(get('r1')?.lastModified).toBe('2024-06-01T00:00:00.000Z');
74+
});
75+
76+
it('skips repos that already have a dateCreated, so it is safe to re-run', async () => {
77+
const { sink, updateRepo, get } = makeSink(
78+
[{ _id: 'r1', dateCreated: '2019-05-05T00:00:00.000Z' }],
79+
() => '2020-01-01T00:00:00.000Z',
80+
);
81+
82+
await populateRepoDates.up(sink);
83+
84+
expect(updateRepo).not.toHaveBeenCalled();
85+
expect(get('r1')?.dateCreated).toBe('2019-05-05T00:00:00.000Z');
86+
});
87+
88+
it('backfills only the repos that need it', async () => {
89+
const { sink, updateRepo, get } = makeSink(
90+
[{ _id: 'r1' }, { _id: 'r2', dateCreated: '2019-05-05T00:00:00.000Z' }, { _id: 'r3' }],
91+
(id) => `2020-01-0${id === 'r1' ? 1 : 3}T00:00:00.000Z`,
92+
);
93+
94+
await populateRepoDates.up(sink);
95+
96+
expect(updateRepo).toHaveBeenCalledTimes(2);
97+
expect(get('r1')?.dateCreated).toBe('2020-01-01T00:00:00.000Z');
98+
expect(get('r2')?.dateCreated).toBe('2019-05-05T00:00:00.000Z');
99+
expect(get('r3')?.dateCreated).toBe('2020-01-03T00:00:00.000Z');
100+
});
101+
102+
it('down clears both timestamps', async () => {
103+
const { sink, get } = makeSink(
104+
[
105+
{
106+
_id: 'r1',
107+
dateCreated: '2020-01-01T00:00:00.000Z',
108+
lastModified: '2020-01-01T00:00:00.000Z',
109+
},
110+
],
111+
() => undefined,
112+
);
113+
114+
await populateRepoDates.down!(sink);
115+
116+
expect(get('r1')?.dateCreated).toBeUndefined();
117+
expect(get('r1')?.lastModified).toBeUndefined();
118+
});
119+
});

test/fixtures/test-package/package-lock.json

Lines changed: 22 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)