Skip to content

Commit 2445577

Browse files
committed
feat(api): get populated software with basic merge
refs: #299
1 parent d6751c6 commit 2445577

File tree

7 files changed

+232
-6
lines changed

7 files changed

+232
-6
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ export const createPgSoftwareExternalDataRepository = (db: Kysely<Database>): So
113113
.execute()
114114
.then(rows => rows.map(cleanDataForExternalData));
115115
},
116+
getPopulatedBySoftwareId: async ({ softwareId }) => {
117+
const result = await db
118+
.selectFrom("software_external_datas as ext")
119+
.selectAll("ext")
120+
.innerJoin("sources as s", "s.slug", "ext.sourceSlug")
121+
.select(["s.kind", "s.priority", "s.url", "s.slug"])
122+
.where("softwareId", "=", softwareId)
123+
.execute();
124+
return result.map(row => transformNullToUndefined(parseBigIntToNumber(row, ["lastDataFetchAt"])));
125+
},
116126
getIdsBySource: async ({ sourceSlug }) => {
117127
return db
118128
.selectFrom("software_external_datas")

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const dateParser = (str: string | Date | undefined | null) => {
1919
export const createPgSoftwareRepository = (db: Kysely<Database>): SoftwareRepository => {
2020
const getBySoftwareId = makeGetSoftwareById(db);
2121
return {
22+
getAllO: async () => {
23+
const rows = await db.selectFrom("softwares").selectAll().execute();
24+
return rows.map(row => stripNullOrUndefinedValues(row));
25+
},
2226
create: async ({ software }) => {
2327
const {
2428
name,
@@ -189,12 +193,12 @@ export const createPgSoftwareRepository = (db: Kysely<Database>): SoftwareReposi
189193
getById: getBySoftwareId,
190194
getSoftwareIdByExternalIdAndSlug: async ({ externalId, sourceSlug }) => {
191195
const result = await db
192-
.selectFrom("softwares")
193-
.select("softwares.id")
196+
.selectFrom("software_external_datas")
197+
.select("softwareId")
194198
.where("sourceSlug", "=", sourceSlug)
195-
.where("externalIdForSource", "=", externalId)
199+
.where("externalId", "=", externalId)
196200
.executeTakeFirst();
197-
return result?.id;
201+
return result?.softwareId ?? undefined;
198202
},
199203
getByIdWithLinkedSoftwaresExternalIds: async softwareId => {
200204
const software = await getBySoftwareId(softwareId);
@@ -281,6 +285,7 @@ export const createPgSoftwareRepository = (db: Kysely<Database>): SoftwareReposi
281285
);
282286
});
283287
},
288+
// TO Remove ?
284289
getAllSillSoftwareExternalIds: async sourceSlug =>
285290
db
286291
.selectFrom("softwares")

api/src/core/bootstrap.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { makeGetSoftwareFormAutoFillDataFromExternalAndOtherSources } from "./us
1010
import { makeCreateSofware } from "./usecases/createSoftware";
1111
import { makeUpdateSoftware } from "./usecases/updateSoftware";
1212
import { makeRefreshExternalDataForSoftware } from "./usecases/refreshExternalData";
13+
import { makeGetPopulatedSoftware } from "./usecases/getPopulatedSoftware";
1314

1415
type PgDbConfig = { dbKind: "kysely"; kyselyDb: Kysely<Database> };
1516

@@ -56,7 +57,8 @@ export async function bootstrapCore(
5657
getAgent: makeGetAgent({ agentRepository: dbApi.agent }),
5758
fetchAndSaveExternalDataForOneSoftwarePackage: makeRefreshExternalDataForSoftware({ dbApi }),
5859
createSoftware: makeCreateSofware(dbApi),
59-
updateSoftware: makeUpdateSoftware(dbApi)
60+
updateSoftware: makeUpdateSoftware(dbApi),
61+
getPopulateSoftware: makeGetPopulatedSoftware(dbApi)
6062
};
6163

6264
return { dbApi, context, useCases };

api/src/core/ports/DbApiV2.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface SoftwareRepository {
5656
externalId: string;
5757
sourceSlug: string;
5858
}) => Promise<number | undefined>;
59+
getAllO: () => Promise<DatabaseDataType.SoftwareRow[]>;
5960
// Save = insert or update
6061
saveSimilarSoftware: (
6162
params: {
@@ -82,6 +83,9 @@ export interface SoftwareRepository {
8283
unreference: (params: { softwareId: number; reason: string; time: number }) => Promise<void>;
8384
}
8485

86+
export type PopulatedExternalData = DatabaseDataType.SoftwareExternalDataRow &
87+
Pick<DatabaseDataType.SourceRow, "url" | "kind" | "slug" | "priority">;
88+
8589
export interface SoftwareExternalDataRepository {
8690
saveIds: (params: { sourceSlug: string; externalId: string; softwareId?: number }[]) => Promise<void>;
8791
update: (params: {
@@ -109,6 +113,7 @@ export interface SoftwareExternalDataRepository {
109113
getBySoftwareId: (params: {
110114
softwareId: number;
111115
}) => Promise<DatabaseDataType.SoftwareExternalDataRow[] | undefined>;
116+
getPopulatedBySoftwareId: (params: { softwareId: number }) => Promise<PopulatedExternalData[] | undefined>;
112117
getBySource: (params: { sourceSlug: string }) => Promise<DatabaseDataType.SoftwareExternalDataRow[] | undefined>;
113118
getIdsBySource: (params: { sourceSlug: string }) => Promise<string[] | undefined>;
114119
getAll: () => Promise<DatabaseDataType.SoftwareExternalDataRow[] | undefined>;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { DatabaseDataType, DbApiV2, PopulatedExternalData } from "../ports/DbApiV2";
2+
import { Software } from "./readWriteSillData";
3+
4+
export type MakeGetPopulatedSoftware = (dbApi: DbApiV2) => GetPopulatedSoftware;
5+
export type GetPopulatedSoftware = () => Promise<Software[]>;
6+
7+
const dateParser = (str: string | Date | undefined | null) => {
8+
if (str && typeof str === "string") {
9+
const date = new Date(str);
10+
return date.valueOf();
11+
}
12+
if (str && str instanceof Date) {
13+
return str.valueOf();
14+
}
15+
};
16+
17+
type MissingData = Pick<
18+
Software,
19+
| "serviceProviders"
20+
| "userAndReferentCountByOrganization"
21+
| "comptoirDuLibreServiceProviderCount"
22+
| "annuaireCnllServiceProviders"
23+
| "comptoirDuLibreId"
24+
| "similarSoftwares"
25+
>;
26+
27+
export const makeGetPopulatedSoftware: MakeGetPopulatedSoftware = (dbApi: DbApiV2) => async () => {
28+
const sofware = await dbApi.software.getAllO();
29+
30+
const UIsofware = await Promise.all(
31+
sofware.map(async softwareItem => {
32+
const formatedSoftwareUI = formatSoftwareRowToUISoftware(softwareItem);
33+
34+
const similarSoftwareIds = await dbApi.similarSoftware.getById({ softwareId: softwareItem.id });
35+
console.log(similarSoftwareIds);
36+
37+
const externalData = await dbApi.softwareExternalData.getPopulatedBySoftwareId({
38+
softwareId: softwareItem.id
39+
});
40+
if (!externalData || externalData.length === 0)
41+
throw new Error("Error in database, a software should have externalData");
42+
43+
const mergedExternalDataItem = mergeExternalData(externalData);
44+
const mergedFormatedExternalDataItem = formatExternalDataRowToUISoftware(mergedExternalDataItem);
45+
46+
const missingData: MissingData = {
47+
serviceProviders: [],
48+
userAndReferentCountByOrganization: {},
49+
comptoirDuLibreServiceProviderCount: 0,
50+
annuaireCnllServiceProviders: undefined,
51+
comptoirDuLibreId: undefined,
52+
similarSoftwares: []
53+
};
54+
// TODO serviceProviders: serviceProviders ?? [],
55+
// TODO userAndReferentCountByOrganization: {},
56+
// TODO comptoirDuLibreServiceProviderCount: software.comptoirDuLibreSoftware?.providers.length ?? 0,
57+
// TODO annuaireCnllServiceProviders
58+
// TODO REMOVE comptoirDuLibreId
59+
// TODO similarSoftwares: similarExternalSoftwares,
60+
61+
// TODO Which order ?
62+
const finalUISoftwareItem = Object.assign(formatedSoftwareUI, mergedFormatedExternalDataItem, missingData);
63+
return finalUISoftwareItem;
64+
})
65+
);
66+
67+
return UIsofware;
68+
};
69+
70+
type DataFromSofwareRow = Pick<
71+
Software,
72+
| "softwareId"
73+
| "softwareDescription"
74+
| "softwareName"
75+
| "updateTime"
76+
| "addedTime"
77+
| "logoUrl"
78+
| "applicationCategories"
79+
| "versionMin"
80+
| "license"
81+
| "keywords"
82+
| "softwareType"
83+
| "prerogatives"
84+
| "dereferencing"
85+
| "sourceSlug"
86+
| "externalId"
87+
| "comptoirDuLibreId"
88+
>;
89+
const formatSoftwareRowToUISoftware = (software: DatabaseDataType.SoftwareRow): DataFromSofwareRow => {
90+
return {
91+
softwareId: software.id,
92+
softwareDescription: software.description,
93+
softwareName: software.name,
94+
updateTime: new Date(+software.updateTime).getTime(),
95+
addedTime: new Date(+software.referencedSinceTime).getTime(),
96+
logoUrl: software.logoUrl,
97+
applicationCategories: software.categories,
98+
versionMin: software.versionMin,
99+
license: software.license,
100+
keywords: software.keywords,
101+
softwareType: software.softwareType,
102+
prerogatives: {
103+
isPresentInSupportContract: software.isPresentInSupportContract,
104+
isFromFrenchPublicServices: software.isFromFrenchPublicService,
105+
doRespectRgaa: software.doRespectRgaa ?? null
106+
},
107+
dereferencing: software.dereferencing,
108+
// TODO REMOVE
109+
sourceSlug: undefined,
110+
externalId: undefined,
111+
comptoirDuLibreId: undefined
112+
};
113+
};
114+
115+
type DataFromExternalRow = Pick<
116+
Software,
117+
| "externalId"
118+
| "authors"
119+
| "officialWebsiteUrl"
120+
| "logoUrl"
121+
| "codeRepositoryUrl"
122+
| "documentationUrl"
123+
| "programmingLanguages"
124+
| "referencePublications"
125+
| "identifiers"
126+
| "license"
127+
| "applicationCategories"
128+
| "latestVersion"
129+
>;
130+
const formatExternalDataRowToUISoftware = (
131+
externalDataRow: DatabaseDataType.SoftwareExternalDataRow
132+
): DataFromExternalRow => {
133+
return {
134+
externalId: externalDataRow.externalId,
135+
authors: (externalDataRow.developers ?? []).map(dev => ({
136+
"@type": "Person",
137+
name: dev.name,
138+
url: dev.url,
139+
affiliations: dev["@type"] === "Person" ? dev.affiliations : []
140+
})),
141+
officialWebsiteUrl: externalDataRow.websiteUrl,
142+
logoUrl: externalDataRow.logoUrl,
143+
codeRepositoryUrl: externalDataRow.sourceUrl,
144+
documentationUrl: externalDataRow.documentationUrl,
145+
programmingLanguages: externalDataRow.programmingLanguages ?? [],
146+
referencePublications: externalDataRow.referencePublications,
147+
identifiers: externalDataRow.identifiers,
148+
license: externalDataRow.license ?? "",
149+
applicationCategories: externalDataRow.applicationCategories ?? [],
150+
latestVersion: {
151+
semVer: externalDataRow.softwareVersion,
152+
publicationTime: dateParser(externalDataRow.publicationTime)
153+
}
154+
};
155+
};
156+
157+
const mergeExternalData = (externalData: PopulatedExternalData[]) => {
158+
if (externalData.length === 0) throw Error("Nothing to merge, the array should be filled");
159+
if (externalData.length === 1) return stripExternalDataFromSource(externalData[0]);
160+
161+
const sortedExternalData = externalData.sort((firstItem, secondItem) => firstItem.priority - secondItem.priority);
162+
163+
// TODO Merge
164+
const mergedItem = sortedExternalData.reduce((savedExternalDataItem, currentExternalDataItem) => {
165+
console.error(savedExternalDataItem);
166+
return currentExternalDataItem;
167+
}, sortedExternalData[0]);
168+
169+
return stripExternalDataFromSource(mergedItem);
170+
};
171+
172+
const stripExternalDataFromSource = (
173+
populatedExternalDataItem: PopulatedExternalData
174+
): DatabaseDataType.SoftwareExternalDataRow => {
175+
const { slug, priority, kind, url, ...externalDataItem } = populatedExternalDataItem;
176+
177+
return externalDataItem;
178+
};
179+
180+
/* const mergeObjects = (obj1: PopulatedExternalData, obj2: PopulatedExternalData): PopulatedExternalData => {
181+
const result: PopulatedExternalData = { ...obj1 };
182+
183+
for (const key in obj2) {
184+
if (obj2.hasOwnProperty(key)) {
185+
const value1 = obj1[key as keyof PopulatedExternalData];
186+
const value2 = obj2[key as keyof PopulatedExternalData];
187+
188+
if (value1 === undefined || value1 === null || value1 === '') {
189+
result[key as keyof PopulatedExternalData] = value2;
190+
} else if (Array.isArray(value1) && Array.isArray(value2)) {
191+
result[key] = Array.from(new Set([...value1, ...value2]));
192+
} else {
193+
result[key] = value1;
194+
}
195+
}
196+
}
197+
198+
return result;
199+
} */

api/src/core/usecases/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { GetSoftwareFormAutoFillDataFromExternalAndOtherSources } from "./g
77
import type { CreateSoftware } from "./createSoftware";
88
import type { UpdateSoftware } from "./updateSoftware";
99
import { ImportFromSource } from "./importFromSource";
10+
import { GetPopulatedSoftware } from "./getPopulatedSoftware";
1011

1112
export type UseCases = {
1213
getSoftwareFormAutoFillDataFromExternalAndOtherSources: GetSoftwareFormAutoFillDataFromExternalAndOtherSources;
@@ -16,4 +17,5 @@ export type UseCases = {
1617
importFromSource: ImportFromSource;
1718
createSoftware: CreateSoftware;
1819
updateSoftware: UpdateSoftware;
20+
getPopulateSoftware: GetPopulatedSoftware;
1921
};

api/src/rpc/router.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type UseCasesUsedOnRouter = Pick<
3232
| "createSoftware"
3333
| "updateSoftware"
3434
| "fetchAndSaveExternalDataForOneSoftwarePackage"
35+
| "getPopulateSoftware"
3536
>;
3637

3738
export function createRouter(params: {
@@ -80,7 +81,9 @@ export function createRouter(params: {
8081
return user;
8182
}),
8283
"getMainSource": loggedProcedure.query(() => dbApi.source.getMainSource()),
83-
"getSoftwares": loggedProcedure.query(() => dbApi.software.getAll()),
84+
"getSoftwares": loggedProcedure.query(() => {
85+
return useCases.getPopulateSoftware();
86+
}),
8487
"getInstances": loggedProcedure.query(() => dbApi.instance.getAll()),
8588
"getExternalSoftwareOptions": loggedProcedure
8689
.input(

0 commit comments

Comments
 (0)