Skip to content

Commit b4bdacd

Browse files
committed
feat(api): support new source zenodo
refs: #204
1 parent f633e30 commit b4bdacd

11 files changed

+420
-1
lines changed

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

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

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

120120
type SimilarExternalSoftwareExternalDataTable = {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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
11+
.createType("external_data_origin_type")
12+
.asEnum(["wikidata", "HAL", "ComptoirDuLibre", "CNLL", "Zenodo"])
13+
.execute();
14+
15+
await db.schema
16+
.alterTable("sources")
17+
.alterColumn("kind", col =>
18+
col.setDataType(sql`external_data_origin_type USING kind::external_data_origin_type`)
19+
)
20+
.execute();
21+
}
22+
23+
export async function down(db: Kysely<any>): Promise<void> {
24+
await db.schema
25+
.alterTable("sources")
26+
.alterColumn("kind", col => col.setDataType("text"))
27+
.execute();
28+
29+
await db.schema.dropType("external_data_origin_type").execute();
30+
await db.schema
31+
.createType("external_data_origin_type")
32+
.asEnum(["wikidata", "HAL", "ComptoirDuLibre", "CNLL"])
33+
.execute();
34+
35+
await db.schema
36+
.alterTable("sources")
37+
.alterColumn("kind", col =>
38+
col.setDataType(sql`external_data_origin_type USING kind::external_data_origin_type`)
39+
)
40+
.execute();
41+
}

api/src/core/adapters/resolveAdapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DatabaseDataType } from "../ports/DbApiV2";
33
import { halSourceGateway } from "./hal";
44
import { wikidataSourceGateway } from "./wikidata";
55
import { comptoirDuLibreSourceGateway } from "./comptoirDuLibre";
6+
import { zenodoSourceGateway } from "./zenodo";
67
import { cNLLSourceGateway } from "./CNLL";
78

89
export const resolveAdapterFromSource = (
@@ -17,6 +18,8 @@ export const resolveAdapterFromSource = (
1718
return comptoirDuLibreSourceGateway;
1819
case "CNLL":
1920
return cNLLSourceGateway;
21+
case "Zenodo":
22+
return zenodoSourceGateway;
2023
default:
2124
const unreachableCase: never = source.kind;
2225
throw new Error(`Unreachable case: ${unreachableCase}`);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import memoize from "memoizee";
2+
3+
import { GetSoftwareExternalData, SoftwareExternalData } from "../../ports/GetSoftwareExternalData";
4+
import { Source } from "../../usecases/readWriteSillData";
5+
import { SchemaPerson } from "../dbApi/kysely/kysely.database";
6+
import { identifersUtils } from "../../utils";
7+
import { makeZenodoApi } from "./zenodoAPI";
8+
import { Zenodo } from "./zenodoAPI/type";
9+
10+
export const getZenodoExternalData: GetSoftwareExternalData = memoize(
11+
async ({
12+
externalId,
13+
source
14+
}: {
15+
externalId: string;
16+
source: Source;
17+
}): Promise<SoftwareExternalData | undefined> => {
18+
if (source.kind !== "Zenodo" && source.url !== "https://zenodo.org/")
19+
throw new Error(`Not a Zenodo source, was : ${source.kind}`);
20+
21+
const zenodoApi = makeZenodoApi();
22+
const record = await zenodoApi.records.get(Number(externalId));
23+
24+
if (!record) return undefined;
25+
if (record.metadata.resource_type.type !== "software")
26+
throw new TypeError(`The record corresponding at ${externalId} is not a software`);
27+
28+
return formatRecordToExternalData(record, source);
29+
}
30+
);
31+
32+
const creatorToPerson = (creator: Zenodo.Creator): SchemaPerson => {
33+
return {
34+
"@type": "Person" as const,
35+
name: creator.name,
36+
affiliations: creator.affiliation
37+
? [
38+
{
39+
"@type": "Organization",
40+
name: creator.affiliation
41+
}
42+
]
43+
: [],
44+
identifiers: [...(creator.orcid ? [identifersUtils.makeOrcidIdentifer({ orcidId: creator.orcid })] : [])]
45+
};
46+
};
47+
48+
const formatRecordToExternalData = (recordSoftwareItem: Zenodo.Record, source: Source): SoftwareExternalData => {
49+
return {
50+
externalId: recordSoftwareItem.id.toString(),
51+
sourceSlug: source.slug,
52+
developers: recordSoftwareItem.metadata.creators.map(creatorToPerson),
53+
label: { "en": recordSoftwareItem.metadata.title },
54+
description: { "en": recordSoftwareItem.metadata.description },
55+
isLibreSoftware: recordSoftwareItem.metadata.access_right === "open", // Not sure
56+
logoUrl: undefined,
57+
websiteUrl: undefined,
58+
sourceUrl:
59+
recordSoftwareItem.metadata.related_identifiers?.filter(
60+
identifier => identifier.relation === "isSupplementTo"
61+
)?.[0]?.identifier ?? undefined,
62+
documentationUrl: undefined,
63+
license: recordSoftwareItem.metadata.license.id,
64+
softwareVersion: recordSoftwareItem.metadata.version,
65+
keywords: recordSoftwareItem.metadata.keywords ?? [],
66+
programmingLanguages: [],
67+
applicationCategories: recordSoftwareItem.metadata.communities?.map(commu => commu.id),
68+
publicationTime: recordSoftwareItem.metadata.publication_date,
69+
referencePublications: [], // TODO reliotated identifers // relation type // ??
70+
identifiers: [
71+
identifersUtils.makeZenodoIdentifer({
72+
zenodoId: recordSoftwareItem.id.toString(),
73+
url: `htpps://zenodo.org/records/${recordSoftwareItem.id.toString()}`,
74+
additionalType: "Software"
75+
}),
76+
...(recordSoftwareItem.metadata.doi
77+
? [identifersUtils.makeDOIIdentifier({ doi: recordSoftwareItem.metadata.doi })]
78+
: []),
79+
...(recordSoftwareItem.swh.swhid
80+
? [identifersUtils.makeSWHIdentifier({ swhId: recordSoftwareItem.swh.swhid })]
81+
: [])
82+
],
83+
providers: []
84+
};
85+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import memoize from "memoizee";
2+
3+
import { SoftwareFormData, Source } from "../../usecases/readWriteSillData";
4+
import { makeZenodoApi } from "./zenodoAPI";
5+
import { Zenodo } from "./zenodoAPI/type";
6+
import { GetSoftwareFormData } from "../../ports/GetSoftwareFormData";
7+
8+
export const getZenodoSoftwareFormData: GetSoftwareFormData = memoize(
9+
async ({ externalId, source }: { externalId: string; source: Source }) => {
10+
if (source.kind !== "Zenodo" && source.url !== "https://zenodo.org/")
11+
throw new Error(`Not a Zenodo source, was : ${source.kind}`);
12+
13+
const zenodoApi = makeZenodoApi();
14+
const record = await zenodoApi.records.get(Number(externalId));
15+
16+
if (!record) return undefined;
17+
if (record.metadata.resource_type.type !== "software")
18+
throw new TypeError(`The record corresponding at ${externalId} is not a software`);
19+
20+
return formatRecordToSoftwareFormData(record, source);
21+
}
22+
);
23+
24+
const formatRecordToSoftwareFormData = (recordSoftwareItem: Zenodo.Record, source: Source): SoftwareFormData => {
25+
return {
26+
softwareName: recordSoftwareItem.title,
27+
softwareDescription: recordSoftwareItem.metadata.description,
28+
softwareType: {
29+
// Probably wrong
30+
type: "desktop/mobile",
31+
os: { "linux": false, "windows": false, "android": false, "ios": false, "mac": false }
32+
},
33+
externalIdForSource: recordSoftwareItem.id.toString(),
34+
sourceSlug: source.slug,
35+
comptoirDuLibreId: undefined, // TODO remove
36+
softwareLicense: recordSoftwareItem.metadata.license.id,
37+
softwareMinimalVersion: undefined,
38+
similarSoftwareExternalDataIds: [],
39+
softwareLogoUrl: undefined,
40+
softwareKeywords: recordSoftwareItem.metadata.keywords ?? [],
41+
42+
isPresentInSupportContract: false,
43+
isFromFrenchPublicService: false,
44+
doRespectRgaa: null
45+
};
46+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { GetSoftwareExternalDataOptions, SoftwareExternalDataOption } from "../../ports/GetSoftwareExternalDataOptions";
2+
import { makeZenodoApi } from "./zenodoAPI";
3+
import { Zenodo } from "./zenodoAPI/type";
4+
import { Source } from "../../usecases/readWriteSillData";
5+
6+
export const getZenodoSoftwareOptions: GetSoftwareExternalDataOptions = async ({ source, queryString }) => {
7+
if (source.kind !== "Zenodo" && source.url !== "https://zenodo.org/")
8+
throw new Error(`Not a Zenodo source, was : ${source.kind}`);
9+
10+
const zenodoApi = makeZenodoApi();
11+
const records = await zenodoApi.records.getByNameAndType(queryString, "software");
12+
13+
if (!records || records.length === 0) return [];
14+
15+
return records.map(record => formatRecordToSoftwareOption(record, source));
16+
};
17+
18+
function formatRecordToSoftwareOption(record: Zenodo.Record, source: Source): SoftwareExternalDataOption {
19+
return {
20+
externalId: record.id.toString(),
21+
label: record.title,
22+
description: record.metadata.description,
23+
isLibreSoftware: record.metadata.access_right === "open", // Not sure
24+
sourceSlug: source.slug
25+
};
26+
}

api/src/core/adapters/zenodo/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { PrimarySourceGateway } from "../../ports/SourceGateway";
2+
import { getZenodoExternalData } from "./getZenodoExternalData";
3+
import { getZenodoSoftwareFormData } from "./getZenodoSoftwareForm";
4+
import { getZenodoSoftwareOptions } from "./getZenodoSoftwareOptions";
5+
6+
export const zenodoSourceGateway: PrimarySourceGateway = {
7+
sourceType: "Zenodo",
8+
sourceProfile: "Primary",
9+
softwareExternalData: {
10+
getById: getZenodoExternalData
11+
},
12+
softwareOptions: {
13+
getById: getZenodoSoftwareOptions
14+
},
15+
softwareForm: {
16+
getById: getZenodoSoftwareFormData
17+
}
18+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Zenodo } from "./type";
2+
3+
export const makeZenodoApi = () => {
4+
return {
5+
records: {
6+
get: async (zenodoRecordId: number): Promise<Zenodo.Record> => {
7+
const url = `https://zenodo.org/api/records/${zenodoRecordId}`;
8+
9+
const res = await fetch(url).catch(err => {
10+
console.error(err);
11+
throw new Error(err);
12+
});
13+
14+
if (res.status === 404) {
15+
throw new Error(`Could find ${zenodoRecordId}`);
16+
}
17+
18+
return res.json();
19+
},
20+
getByNameAndType: async (name: string, type: string): Promise<Zenodo.Record[]> => {
21+
const url = `https://zenodo.org/api/records/q=title:${name} AND type:${type}`;
22+
23+
const res = await fetch(url).catch(err => {
24+
console.error(err);
25+
throw new Error(err);
26+
});
27+
28+
if (res.status === 404) {
29+
throw new Error(`Could find endpoint`);
30+
}
31+
32+
return res.json();
33+
}
34+
}
35+
};
36+
};

0 commit comments

Comments
 (0)