Skip to content

Commit 943dc14

Browse files
committed
feat(api): add a ComptoirDuLibre source gateway
refs: #295
1 parent e6ffccb commit 943dc14

15 files changed

+377
-19
lines changed

api/scripts/load-git-repo-in-pg.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import { InsertObject, Kysely, sql } from "kysely";
22
import { z } from "zod";
33
import { createGitDbApi, GitDbApiParams } from "../src/core/adapters/dbApi/createGitDbApi";
44
import { makeGetAgentIdByEmail } from "../src/core/adapters/dbApi/kysely/createPgAgentRepository";
5-
import { Database } from "../src/core/adapters/dbApi/kysely/kysely.database";
5+
import { Database, ExternalDataOriginKind } from "../src/core/adapters/dbApi/kysely/kysely.database";
66
import { createPgDialect } from "../src/core/adapters/dbApi/kysely/kysely.dialect";
77
import { CompiledData } from "../src/core/ports/CompileData";
88
import { Db } from "../src/core/ports/DbApi";
9-
import { ExternalDataOrigin } from "../src/core/ports/GetSoftwareExternalData";
109
import { Source } from "../src/core/usecases/readWriteSillData";
1110
import { stripNullOrUndefinedValues } from "../src/core/adapters/dbApi/kysely/kysely.utils";
1211

@@ -257,7 +256,7 @@ const insertCompiledSoftwaresAndSoftwareExternalData = async ({
257256
): software is CompiledData.Software.Private & {
258257
softwareExternalData: {
259258
externalId: string;
260-
externalDataOrigin: ExternalDataOrigin;
259+
externalDataOrigin: ExternalDataOriginKind;
261260
};
262261
} =>
263262
software.softwareExternalData?.externalId !== undefined &&
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import memoize from "memoizee";
2+
3+
import { GetSoftwareExternalData, SoftwareExternalData } from "../../ports/GetSoftwareExternalData";
4+
import { Source } from "../../usecases/readWriteSillData";
5+
import { comptoirDuLibreApi } from "../comptoirDuLibreApi";
6+
import { ComptoirDuLibre } from "../../ports/ComptoirDuLibreApi";
7+
import { SchemaOrganization } from "../dbApi/kysely/kysely.database";
8+
import { identifersUtils } from "../../utils";
9+
10+
export const getCDLSoftwareExternalData: GetSoftwareExternalData = memoize(
11+
async ({
12+
externalId,
13+
source
14+
}: {
15+
externalId: string;
16+
source: Source;
17+
}): Promise<SoftwareExternalData | undefined> => {
18+
const comptoirAPi = await comptoirDuLibreApi.getComptoirDuLibre();
19+
20+
const comptoirSoftware = comptoirAPi.softwares.find(softwareItem => softwareItem.id.toString() === externalId);
21+
22+
if (!comptoirSoftware) return undefined;
23+
24+
return formatCDLSoftwareToExternalData(comptoirSoftware, source);
25+
}
26+
);
27+
28+
const cDLproviderToCMProdivers = (provider: ComptoirDuLibre.Provider): SchemaOrganization => {
29+
return {
30+
"@type": "Organization" as const,
31+
name: provider.name,
32+
url: provider.external_resources.website ?? undefined,
33+
identifiers: [
34+
identifersUtils.makeCDLIdentifier({
35+
cdlId: provider.id.toString(),
36+
url: provider.url,
37+
additionalType: "Organization"
38+
})
39+
]
40+
};
41+
};
42+
43+
const formatCDLSoftwareToExternalData = (
44+
cDLSoftwareItem: ComptoirDuLibre.Software,
45+
source: Source
46+
): SoftwareExternalData => {
47+
const splittedCNLLUrl = !Array.isArray(cDLSoftwareItem.external_resources.cnll)
48+
? cDLSoftwareItem.external_resources.cnll.url.split("/")
49+
: undefined;
50+
51+
return {
52+
externalId: cDLSoftwareItem.id.toString(),
53+
sourceSlug: source.slug,
54+
developers: [],
55+
label: { "fr": cDLSoftwareItem.name },
56+
description: { "fr": "" },
57+
isLibreSoftware: true,
58+
//
59+
logoUrl: undefined, // Use scrapper ?
60+
websiteUrl: cDLSoftwareItem.external_resources.website ?? undefined,
61+
sourceUrl: cDLSoftwareItem.external_resources.repository ?? undefined,
62+
documentationUrl: undefined,
63+
license: cDLSoftwareItem.licence,
64+
softwareVersion: undefined,
65+
keywords: [],
66+
programmingLanguages: [],
67+
applicationCategories: [],
68+
publicationTime: undefined,
69+
referencePublications: [],
70+
identifiers: [
71+
identifersUtils.makeCDLIdentifier({
72+
cdlId: cDLSoftwareItem.id.toString(),
73+
url: cDLSoftwareItem.url,
74+
additionalType: "Software"
75+
}),
76+
...(!Array.isArray(cDLSoftwareItem.external_resources.cnll) && splittedCNLLUrl
77+
? [
78+
identifersUtils.makeCNLLIdentifier({
79+
cNNLId: splittedCNLLUrl[splittedCNLLUrl.length - 1],
80+
url: cDLSoftwareItem.external_resources.cnll.url
81+
})
82+
]
83+
: []),
84+
...(!Array.isArray(cDLSoftwareItem.external_resources.framalibre)
85+
? [
86+
identifersUtils.makeFramaIndentifier({
87+
framaLibreId: cDLSoftwareItem.external_resources.framalibre.slug,
88+
url: cDLSoftwareItem.external_resources.framalibre.url,
89+
additionalType: "Software"
90+
})
91+
]
92+
: []),
93+
...(!Array.isArray(cDLSoftwareItem.external_resources.wikidata)
94+
? [
95+
identifersUtils.makeWikidataIdentifier({
96+
wikidataId: cDLSoftwareItem.external_resources.wikidata.id,
97+
url: cDLSoftwareItem.external_resources.wikidata.url,
98+
additionalType: "Software"
99+
})
100+
]
101+
: [])
102+
],
103+
providers: cDLSoftwareItem.providers.map(prodiver => cDLproviderToCMProdivers(prodiver))
104+
};
105+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import memoize from "memoizee";
2+
3+
import { GetSoftwareFormData } from "../../ports/GetSoftwareFormData";
4+
import { SoftwareFormData, Source } from "../../usecases/readWriteSillData";
5+
import { comptoirDuLibreApi } from "../comptoirDuLibreApi";
6+
import { ComptoirDuLibre } from "../../ports/ComptoirDuLibreApi";
7+
8+
const formatCDLSoftwareToExternalData = async (
9+
comptoirSoftware: ComptoirDuLibre.Software,
10+
source: Source
11+
): Promise<SoftwareFormData> => {
12+
const keywords = await comptoirDuLibreApi.getKeywords({ comptoirDuLibreId: comptoirSoftware.id });
13+
const logoUrl = await comptoirDuLibreApi.getIconUrl({ comptoirDuLibreId: comptoirSoftware.id });
14+
15+
return {
16+
softwareName: comptoirSoftware.name,
17+
softwareDescription: "",
18+
softwareType: {
19+
type: "desktop/mobile",
20+
os: { "linux": false, "windows": false, "android": false, "ios": false, "mac": false }
21+
}, // TODO Check Mandatory, Incorrect data
22+
externalIdForSource: comptoirSoftware.id.toString(),
23+
sourceSlug: source.slug,
24+
comptoirDuLibreId: undefined, // TODO DELETE
25+
softwareLicense: comptoirSoftware.licence,
26+
softwareMinimalVersion: undefined,
27+
similarSoftwareExternalDataIds: [],
28+
softwareLogoUrl: logoUrl,
29+
softwareKeywords: keywords,
30+
31+
isPresentInSupportContract: false,
32+
isFromFrenchPublicService: false,
33+
doRespectRgaa: null
34+
};
35+
};
36+
37+
export const getCDLFormData: GetSoftwareFormData = memoize(
38+
async ({ externalId, source }: { externalId: string; source: Source }): Promise<SoftwareFormData | undefined> => {
39+
const comptoirAPi = await comptoirDuLibreApi.getComptoirDuLibre();
40+
41+
const comptoirSoftware = comptoirAPi.softwares.find(softwareItem => softwareItem.id.toString() === externalId);
42+
43+
if (!comptoirSoftware) return undefined;
44+
45+
return formatCDLSoftwareToExternalData(comptoirSoftware, source);
46+
}
47+
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type {
2+
GetSoftwareExternalDataOptions,
3+
SoftwareExternalDataOption
4+
} from "../../ports/GetSoftwareExternalDataOptions";
5+
import { Language } from "../../ports/GetSoftwareExternalData";
6+
import { Source } from "../../usecases/readWriteSillData";
7+
import { comptoirDuLibreApi } from "../comptoirDuLibreApi";
8+
import { ComptoirDuLibre } from "../../ports/ComptoirDuLibreApi";
9+
10+
export const rawCDLSoftwareToExternalOption =
11+
({ source }: { language: Language; source: Source }) =>
12+
(cDLSoftware: ComptoirDuLibre.Software): SoftwareExternalDataOption => {
13+
return {
14+
externalId: cDLSoftware.id.toString(),
15+
label: cDLSoftware.name,
16+
description: "",
17+
isLibreSoftware: true,
18+
sourceSlug: source.slug
19+
};
20+
};
21+
22+
// HAL documentation is here : https://api.archives-ouvertes.fr/docs/search
23+
24+
export const getCDLSoftwareOptions: GetSoftwareExternalDataOptions = async ({ queryString, language, source }) => {
25+
if (source.kind !== "ComptoirDuLibre") throw new Error(`Not a Comptoir Du Libre source, was : ${source.kind}`);
26+
27+
const comptoirAPi = await comptoirDuLibreApi.getComptoirDuLibre();
28+
const comptoirSoftware = comptoirAPi.softwares.filter(softwareItem => softwareItem.name.includes(queryString));
29+
30+
if (!comptoirSoftware) return [];
31+
32+
return comptoirSoftware.map(rawCDLSoftwareToExternalOption({ language, source }));
33+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { getCDLSoftwareOptions } from "./getCDLSoftwareOptions";
2+
import { getCDLSoftwareExternalData } from "./getCDLExternalData";
3+
import { getCDLFormData } from "./getCDLFormData";
4+
import { SourceGateway } from "../../ports/SourceGateway";
5+
6+
export const comptoirDuLibreSourceGateway: SourceGateway = {
7+
sourceType: "ComptoirDuLibre",
8+
softwareExternalData: {
9+
getById: getCDLSoftwareExternalData
10+
},
11+
softwareOptions: {
12+
getById: getCDLSoftwareOptions
13+
},
14+
softwareForm: {
15+
getById: getCDLFormData
16+
}
17+
};

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ type InstancesTable = {
114114
};
115115

116116
type ExternalId = string;
117-
type ExternalDataOriginKind = "wikidata" | "HAL";
117+
export type ExternalDataOriginKind = "wikidata" | "HAL" | "ComptoirDuLibre";
118118
type LocalizedString = Partial<Record<string, string>>;
119119

120120
type SimilarExternalSoftwareExternalDataTable = {
@@ -240,10 +240,47 @@ export namespace PgComptoirDuLibre {
240240
external_resources: {
241241
website: string | null;
242242
repository: string | null;
243+
wikidata: WikidataIdentifier | any[];
244+
sill: SILLIdentifier | any[];
245+
wikipedia: WikipediaIdentifier | any[];
246+
cnll: CNLLIdentifier | any[];
247+
framalibre: FramaLibreIdentifier | any[];
243248
};
244249
providers: Provider[];
245250
users: User[];
246251
};
252+
253+
type CNLLIdentifier = {
254+
url: string;
255+
};
256+
257+
type FramaLibreIdentifier = {
258+
slug: string;
259+
url: string;
260+
};
261+
262+
type WikidataIdentifier = {
263+
id: string;
264+
url: string;
265+
data: string;
266+
};
267+
268+
type SILLIdentifier = {
269+
id: number;
270+
url: string;
271+
i18n_url: {
272+
fr: string;
273+
en: string;
274+
};
275+
};
276+
277+
type WikipediaIdentifier = {
278+
url: string;
279+
i18n_url: {
280+
fr: string;
281+
en: string;
282+
};
283+
};
247284
}
248285

249286
type ServiceProvider = {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { sql, type Kysely } from "kysely";
2+
3+
export async function up(db: Kysely<any>): Promise<void> {
4+
await db.schema
5+
.alterTable("sources")
6+
.alterColumn("kind", col => col.setDataType("text"))
7+
.execute();
8+
9+
await db.schema.dropType("external_data_origin_type").execute();
10+
await db.schema.createType("external_data_origin_type").asEnum(["wikidata", "HAL", "ComptoirDuLibre"]).execute();
11+
12+
await db.schema
13+
.alterTable("sources")
14+
.alterColumn("kind", col =>
15+
col.setDataType(sql`external_data_origin_type USING kind::external_data_origin_type`)
16+
)
17+
.execute();
18+
}
19+
20+
export async function down(db: Kysely<any>): Promise<void> {
21+
await db.schema
22+
.alterTable("sources")
23+
.alterColumn("kind", col => col.setDataType("text"))
24+
.execute();
25+
26+
await db.schema.dropType("external_data_origin_type").execute();
27+
await db.schema.createType("external_data_origin_type").asEnum(["wikidata", "HAL"]).execute();
28+
29+
await db.schema
30+
.alterTable("sources")
31+
.alterColumn("kind", col =>
32+
col.setDataType(sql`external_data_origin_type USING kind::external_data_origin_type`)
33+
)
34+
.execute();
35+
}

api/src/core/adapters/hal/getSoftwareForm.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import memoize from "memoizee";
2-
import { SoftwareFormData, SoftwareType } from "../../usecases/readWriteSillData";
2+
import { SoftwareFormData, SoftwareType, Source } from "../../usecases/readWriteSillData";
33
import { halAPIGateway } from "./HalAPI";
44
import { HAL } from "./HalAPI/types/HAL";
5-
import { Source } from "../../usecases/readWriteSillData";
65
import { GetSoftwareFormData } from "../../ports/GetSoftwareFormData";
76

87
const stringOfArrayIncluded = (stringArray: Array<string>, text: string): boolean => {

api/src/core/adapters/resolveAdapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { SourceGateway } from "../ports/SourceGateway";
22
import { DatabaseDataType } from "../ports/DbApiV2";
33
import { halSourceGateway } from "./hal";
44
import { wikidataSourceGateway } from "./wikidata";
5+
import { comptoirDuLibreSourceGateway } from "./comptoirDuLibre";
56

67
export const resolveAdapterFromSource = (source: DatabaseDataType.SourceRow): SourceGateway => {
78
switch (source.kind) {
89
case "HAL":
910
return halSourceGateway;
1011
case "wikidata":
1112
return wikidataSourceGateway;
13+
case "ComptoirDuLibre":
14+
return comptoirDuLibreSourceGateway;
1215
default:
1316
const unreachableCase: never = source.kind;
1417
throw new Error(`Unreachable case: ${unreachableCase}`);

0 commit comments

Comments
 (0)