Skip to content

Commit 99f47e7

Browse files
JeromeBuclaude
andcommitted
refactor: convert timestamp columns from bigint to timestamptz
Replace unix millisecond timestamps (bigint) with PostgreSQL timestamp with timezone for improved readability and PostgreSQL native date handling. Changes: - Add migration to convert 5 columns: softwares.{referencedSinceTime,updateTime}, instances.{referencedSinceTime,updateTime}, software_external_datas.lastDataFetchAt - Update TypeScript types from number to Date for affected columns - Replace Date.now() with new Date() in repository and use case code - Update date comparisons and type conversions throughout codebase - Fix test fixtures to use Date objects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d28329a commit 99f47e7

13 files changed

+139
-31
lines changed

api/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
"main": "dist/src/lib/index.js",
1010
"types": "dist/src/lib/index.d.ts",
1111
"scripts": {
12-
"migrate": "dotenv -e ../.env -- kysely migrate",
13-
"db:up": "yarn migrate latest",
1412
"test": "vitest --watch=false --no-file-parallelism",
1513
"dev": "yarn build && yarn start",
1614
"generate-translation-schema": "node scripts/generate-translation-schema.js",
@@ -27,6 +25,8 @@
2725
"link-in-web": "ts-node --skipProject scripts/link-in-app.ts sill-web",
2826
"db:seed": "yarn build && dotenv -e ../.env -- node dist/scripts/seed.js",
2927
"typecheck": "tsc --noEmit",
28+
"migrate": "dotenv -e ../.env -- kysely migrate",
29+
"db:up": "yarn migrate latest",
3030
"dev:db:up": "docker compose -f ../docker-compose.resources.yml up -d",
3131
"dev:db:down": "docker compose -f ../docker-compose.resources.yml down",
3232
"dev:db:flush": "rm -rf ../docker-data",

api/src/core/adapters/dbApi/kysely/createPgInstanceRepository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const createPgInstanceRepository = (db: Kysely<Database>): InstanceReposi
1414
const { mainSoftwareSillId, organization, targetAudience, instanceUrl, isPublic, ...rest } = formData;
1515
assert<Equals<typeof rest, {}>>();
1616

17-
const now = Date.now();
17+
const now = new Date();
1818
const { instanceId } = await db
1919
.insertInto("instances")
2020
.values({
@@ -35,7 +35,7 @@ export const createPgInstanceRepository = (db: Kysely<Database>): InstanceReposi
3535
const { mainSoftwareSillId, organization, targetAudience, instanceUrl, isPublic, ...rest } = formData;
3636
assert<Equals<typeof rest, {}>>();
3737

38-
const now = Date.now();
38+
const now = new Date();
3939
await db
4040
.updateTable("instances")
4141
.set({

api/src/core/adapters/dbApi/kysely/createPgSoftwareExternalDataRepository.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
import { Kysely } from "kysely";
66
import { DatabaseDataType, PopulatedExternalData, SoftwareExternalDataRepository } from "../../../ports/DbApiV2";
77
import { Database, DatabaseRowOutput } from "./kysely.database";
8-
import { stripNullOrUndefinedValues, transformNullToUndefined, parseBigIntToNumber } from "./kysely.utils";
8+
import { stripNullOrUndefinedValues, transformNullToUndefined } from "./kysely.utils";
99
import { mergeArrays } from "../../../utils";
1010
import merge from "deepmerge";
1111

12-
const cleanDataForExternalData = (row: DatabaseRowOutput.SoftwareExternalData) =>
13-
transformNullToUndefined(parseBigIntToNumber(row, ["lastDataFetchAt"]));
12+
const cleanDataForExternalData = (row: DatabaseRowOutput.SoftwareExternalData) => transformNullToUndefined(row);
1413

1514
const mergeExternalData = (externalData: PopulatedExternalData[]) => {
1615
if (externalData.length === 0) return undefined;
@@ -113,9 +112,9 @@ export const createPgSoftwareExternalDataRepository = (db: Kysely<Database>): So
113112
let request = db.selectFrom("software_external_datas").select(["externalId", "sourceSlug"]);
114113

115114
if (minuteSkipSince) {
116-
const dateNum = new Date().valueOf() - minuteSkipSince * 1000 * 60;
115+
const thresholdDate = new Date(Date.now() - minuteSkipSince * 1000 * 60);
117116
request = request.where(eb =>
118-
eb.or([eb("lastDataFetchAt", "is", null), eb("lastDataFetchAt", "<", dateNum)])
117+
eb.or([eb("lastDataFetchAt", "is", null), eb("lastDataFetchAt", "<", thresholdDate)])
119118
);
120119
}
121120

@@ -216,7 +215,7 @@ export const createPgSoftwareExternalDataRepository = (db: Kysely<Database>): So
216215
.select(["s.kind", "s.priority", "s.url", "s.slug"])
217216
.where("softwareId", "=", softwareId)
218217
.execute();
219-
const cleanResult = result.map(row => transformNullToUndefined(parseBigIntToNumber(row, ["lastDataFetchAt"])));
218+
const cleanResult = result.map(row => transformNullToUndefined(row));
220219

221220
if (!cleanResult) return undefined;
222221

api/src/core/adapters/dbApi/kysely/createPgSoftwareRepository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const createPgSoftwareRepository = (db: Kysely<Database>): SoftwareReposi
5252

5353
assert<Equals<typeof rest, {}>>();
5454

55-
const now = Date.now();
55+
const now = new Date();
5656

5757
return db.transaction().execute(async trx => {
5858
const { softwareId } = await trx
@@ -106,7 +106,7 @@ export const createPgSoftwareRepository = (db: Kysely<Database>): SoftwareReposi
106106

107107
assert<Equals<typeof rest, {}>>();
108108

109-
const now = Date.now();
109+
const now = new Date();
110110
await db
111111
.updateTable("softwares")
112112
.set({

api/src/core/adapters/dbApi/kysely/kysely.database.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ type InstancesTable = {
115115
instanceUrl: string | null;
116116
isPublic: boolean;
117117
addedByUserId: number;
118-
referencedSinceTime: number;
119-
updateTime: number;
118+
referencedSinceTime: Date;
119+
updateTime: Date;
120120
};
121121

122122
type ExternalId = string;
@@ -157,7 +157,7 @@ export type SoftwareExternalDatasTable = {
157157
referencePublications: JSONColumnType<ScholarlyArticle[]> | null;
158158
publicationTime: Date | null;
159159
identifiers: JSONColumnType<SchemaIdentifier[]> | null;
160-
lastDataFetchAt: number | null;
160+
lastDataFetchAt: Date | null;
161161
providers: JSONColumnType<Array<SchemaOrganization>> | null;
162162
};
163163

@@ -173,8 +173,8 @@ type SoftwaresTable = {
173173
id: Generated<number>;
174174
name: string;
175175
description: string;
176-
referencedSinceTime: number;
177-
updateTime: number;
176+
referencedSinceTime: Date;
177+
updateTime: Date;
178178
dereferencing: JSONColumnType<{
179179
reason?: string;
180180
time: number;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-FileCopyrightText: 2021-2025 DINUM <[email protected]>
2+
// SPDX-FileCopyrightText: 2024-2025 Université Grenoble Alpes
3+
// SPDX-License-Identifier: MIT
4+
5+
import { Kysely, sql } from "kysely";
6+
7+
export async function up(db: Kysely<any>): Promise<void> {
8+
await db.schema.alterTable("softwares").addColumn("referencedSinceTime_temp", "timestamptz").execute();
9+
await sql`UPDATE softwares SET "referencedSinceTime_temp" = to_timestamp("referencedSinceTime" / 1000.0)`.execute(
10+
db
11+
);
12+
await db.schema
13+
.alterTable("softwares")
14+
.alterColumn("referencedSinceTime_temp", col => col.setNotNull())
15+
.execute();
16+
await db.schema.alterTable("softwares").dropColumn("referencedSinceTime").execute();
17+
await db.schema.alterTable("softwares").renameColumn("referencedSinceTime_temp", "referencedSinceTime").execute();
18+
19+
await db.schema.alterTable("softwares").addColumn("updateTime_temp", "timestamptz").execute();
20+
await sql`UPDATE softwares SET "updateTime_temp" = to_timestamp("updateTime" / 1000.0)`.execute(db);
21+
await db.schema
22+
.alterTable("softwares")
23+
.alterColumn("updateTime_temp", col => col.setNotNull())
24+
.execute();
25+
await db.schema.alterTable("softwares").dropColumn("updateTime").execute();
26+
await db.schema.alterTable("softwares").renameColumn("updateTime_temp", "updateTime").execute();
27+
28+
await db.schema.alterTable("instances").addColumn("referencedSinceTime_temp", "timestamptz").execute();
29+
await sql`UPDATE instances SET "referencedSinceTime_temp" = to_timestamp("referencedSinceTime" / 1000.0)`.execute(
30+
db
31+
);
32+
await db.schema
33+
.alterTable("instances")
34+
.alterColumn("referencedSinceTime_temp", col => col.setNotNull())
35+
.execute();
36+
await db.schema.alterTable("instances").dropColumn("referencedSinceTime").execute();
37+
await db.schema.alterTable("instances").renameColumn("referencedSinceTime_temp", "referencedSinceTime").execute();
38+
39+
await db.schema.alterTable("instances").addColumn("updateTime_temp", "timestamptz").execute();
40+
await sql`UPDATE instances SET "updateTime_temp" = to_timestamp("updateTime" / 1000.0)`.execute(db);
41+
await db.schema
42+
.alterTable("instances")
43+
.alterColumn("updateTime_temp", col => col.setNotNull())
44+
.execute();
45+
await db.schema.alterTable("instances").dropColumn("updateTime").execute();
46+
await db.schema.alterTable("instances").renameColumn("updateTime_temp", "updateTime").execute();
47+
48+
await db.schema.alterTable("software_external_datas").addColumn("lastDataFetchAt_temp", "timestamptz").execute();
49+
await sql`UPDATE software_external_datas SET "lastDataFetchAt_temp" = to_timestamp("lastDataFetchAt" / 1000.0) WHERE "lastDataFetchAt" IS NOT NULL`.execute(
50+
db
51+
);
52+
await db.schema.alterTable("software_external_datas").dropColumn("lastDataFetchAt").execute();
53+
await db.schema
54+
.alterTable("software_external_datas")
55+
.renameColumn("lastDataFetchAt_temp", "lastDataFetchAt")
56+
.execute();
57+
}
58+
59+
export async function down(db: Kysely<any>): Promise<void> {
60+
await db.schema.alterTable("softwares").addColumn("referencedSinceTime_temp", "bigint").execute();
61+
await sql`UPDATE softwares SET "referencedSinceTime_temp" = EXTRACT(EPOCH FROM "referencedSinceTime")::bigint * 1000`.execute(
62+
db
63+
);
64+
await db.schema
65+
.alterTable("softwares")
66+
.alterColumn("referencedSinceTime_temp", col => col.setNotNull())
67+
.execute();
68+
await db.schema.alterTable("softwares").dropColumn("referencedSinceTime").execute();
69+
await db.schema.alterTable("softwares").renameColumn("referencedSinceTime_temp", "referencedSinceTime").execute();
70+
71+
await db.schema.alterTable("softwares").addColumn("updateTime_temp", "bigint").execute();
72+
await sql`UPDATE softwares SET "updateTime_temp" = EXTRACT(EPOCH FROM "updateTime")::bigint * 1000`.execute(db);
73+
await db.schema
74+
.alterTable("softwares")
75+
.alterColumn("updateTime_temp", col => col.setNotNull())
76+
.execute();
77+
await db.schema.alterTable("softwares").dropColumn("updateTime").execute();
78+
await db.schema.alterTable("softwares").renameColumn("updateTime_temp", "updateTime").execute();
79+
80+
await db.schema.alterTable("instances").addColumn("referencedSinceTime_temp", "bigint").execute();
81+
await sql`UPDATE instances SET "referencedSinceTime_temp" = EXTRACT(EPOCH FROM "referencedSinceTime")::bigint * 1000`.execute(
82+
db
83+
);
84+
await db.schema
85+
.alterTable("instances")
86+
.alterColumn("referencedSinceTime_temp", col => col.setNotNull())
87+
.execute();
88+
await db.schema.alterTable("instances").dropColumn("referencedSinceTime").execute();
89+
await db.schema.alterTable("instances").renameColumn("referencedSinceTime_temp", "referencedSinceTime").execute();
90+
91+
await db.schema.alterTable("instances").addColumn("updateTime_temp", "bigint").execute();
92+
await sql`UPDATE instances SET "updateTime_temp" = EXTRACT(EPOCH FROM "updateTime")::bigint * 1000`.execute(db);
93+
await db.schema
94+
.alterTable("instances")
95+
.alterColumn("updateTime_temp", col => col.setNotNull())
96+
.execute();
97+
await db.schema.alterTable("instances").dropColumn("updateTime").execute();
98+
await db.schema.alterTable("instances").renameColumn("updateTime_temp", "updateTime").execute();
99+
100+
await db.schema.alterTable("software_external_datas").addColumn("lastDataFetchAt_temp", "bigint").execute();
101+
await sql`UPDATE software_external_datas SET "lastDataFetchAt_temp" = EXTRACT(EPOCH FROM "lastDataFetchAt")::bigint * 1000 WHERE "lastDataFetchAt" IS NOT NULL`.execute(
102+
db
103+
);
104+
await db.schema.alterTable("software_external_datas").dropColumn("lastDataFetchAt").execute();
105+
await db.schema
106+
.alterTable("software_external_datas")
107+
.renameColumn("lastDataFetchAt_temp", "lastDataFetchAt")
108+
.execute();
109+
}

api/src/core/ports/DbApiV2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export interface SoftwareExternalDataRepository {
102102
sourceSlug: string;
103103
externalId: string;
104104
softwareId?: number;
105-
lastDataFetchAt?: number;
105+
lastDataFetchAt?: Date;
106106
softwareExternalData: SoftwareExternalData;
107107
}) => Promise<void>;
108108
save: (params: { softwareExternalData: SoftwareExternalData; softwareId: number | undefined }) => Promise<void>; // TODO

api/src/core/usecases/createSoftware.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe("Create software - Trying all the cases", () => {
8484
"license": "MIT",
8585
"logoUrl": "https://example.com/logo.png",
8686
"name": "Create react app",
87-
"referencedSinceTime": expect.any(String), // To format
87+
"referencedSinceTime": expect.any(Date),
8888
"softwareType": {
8989
"type": "stack"
9090
},

api/src/core/usecases/createSoftware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const formDataToSoftwareRow = (softwareForm: SoftwareFormData, userId: nu
1717
license: softwareForm.softwareLicense,
1818
logoUrl: softwareForm.softwareLogoUrl,
1919
versionMin: softwareForm.softwareMinimalVersion,
20-
referencedSinceTime: Date.now(),
20+
referencedSinceTime: new Date(),
2121
dereferencing: undefined,
2222
isStillInObservation: false,
2323
doRespectRgaa: softwareForm.doRespectRgaa ?? undefined,

api/src/core/usecases/getPopulatedSoftware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ const formatSoftwareRowToUISoftware = (
148148
softwareId: software.id,
149149
softwareDescription: software.description,
150150
softwareName: software.name,
151-
updateTime: new Date(+software.updateTime).getTime(),
152-
addedTime: new Date(+software.referencedSinceTime).getTime(),
151+
updateTime: software.updateTime.getTime(),
152+
addedTime: software.referencedSinceTime.getTime(),
153153
logoUrl: software.logoUrl,
154154
applicationCategories: software.categories,
155155
versionMin: software.versionMin,

0 commit comments

Comments
 (0)