Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
50c97e7
feat: better import of person data from GitHub and better merge
guillermau Feb 12, 2026
ff2a84a
feat: build index
guillermau Jan 8, 2026
e29a9c1
feat: add developer endoint
guillermau Jan 30, 2026
cdc1cd3
feat: improve import of organization in HAL
guillermau Jan 13, 2026
df0a903
feat: add new empoint for organization index
guillermau Jan 30, 2026
18068e1
feat: add button to load a new organization page
guillermau Feb 13, 2026
ad5949e
chore: remove deleted usecase
guillermau Feb 13, 2026
b2ae8f3
feat: change the API to add more info about authors
guillermau Feb 16, 2026
96919bf
feat: make api call and load data in the store
guillermau Feb 17, 2026
6526f42
feat: poc of organization list
guillermau Feb 17, 2026
f5df472
feat: change to key select and add organization details
guillermau Feb 17, 2026
19251b9
fix: clean the ror id
guillermau Feb 19, 2026
39f8e1e
feat: add optional rendering and show list of software
guillermau Feb 19, 2026
0cfed65
feat: add new table for author organization
guillermau Mar 3, 2026
29be549
feat: add fetching author organization info from ror api
guillermau Mar 3, 2026
db29c5b
feat: add wikidata fetcher, merge organization info, automated update…
guillermau Mar 12, 2026
8b890f7
feat: improve UI
guillermau Mar 12, 2026
e1af985
feat: fix config after rebase and integreate wikidata config
guillermau Mar 17, 2026
fa5e657
chore: remove unused route
guillermau Mar 18, 2026
fecda8b
fix: case empty array on kysely and name changed
guillermau Mar 18, 2026
d0e6a00
feat: use gateway and add RSNR adapter
guillermau Mar 19, 2026
43b029d
feat: add translation and ui elements for author organization
guillermau Mar 26, 2026
09bbfd1
feat: reorder migration after rebase
guillermau Mar 26, 2026
672b414
fix: minor fixes
guillermau Mar 31, 2026
636987a
fix: wait for update
guillermau Mar 31, 2026
182cb54
fix: broke merging orgs while filtering
guillermau Apr 7, 2026
c10fef8
feat: add tab to select type on org
guillermau Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions api/src/core/adapters/GitHub/getExternalData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const getGitHubSoftwareExternalData: GetSoftwareExternal = memoize(
const repoLanguages = await gitHubApi.repo.getLanguages({ repoUrl });
const devIds =
repoDevs
?.map(dev => dev.id)
?.filter(dev => dev && dev.type === "User") // FILTER BOT
.map(dev => dev.id)
.filter(a => {
return a !== undefined;
}) ?? [];
Expand Down Expand Up @@ -62,9 +63,21 @@ export const getGitHubSoftwareExternalData: GetSoftwareExternal = memoize(
"@type": "Person",
name: dev.name ?? dev.login,
identifiers: [
identifersUtils.makeUserGitHubIdentifer({ username: dev.name ?? dev.login, userId: dev.id })
identifersUtils.makeUserGitHubIdentifer({
name: dev.name ?? dev.login,
userId: dev.id,
url: dev.html_url
}),
...(dev.twitter_username
? [identifersUtils.makeTwitterPersonIdentifer({ username: dev.twitter_username })]
: []),
...(dev.gravatar_id
? [identifersUtils.makeGravatarPersonIdentifer({ gravatarId: dev.gravatar_id })]
: [])
// TODO Orcid when available
],
url: dev.blog ?? undefined,
...(dev.email ? { email: dev.email } : {}),
url: dev.blog ?? dev.html_url,
affiliations: dev.company
? [
{
Expand Down
121 changes: 121 additions & 0 deletions api/src/core/adapters/RNSR/API/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2021-2026 DINUM <floss@numerique.gouv.fr>
// SPDX-FileCopyrightText: 2024-2026 Université Grenoble Alpes
// SPDX-License-Identifier: MIT

import { identifersUtils } from "../../../../tools/identifiersTools";
import { SchemaOrganization, SchemaPostalAddress } from "../../dbApi/kysely/kysely.database";

type RNSROrganisation = {
numero_national_de_structure: string;
libelle: string;
sigle: string;
annee_de_creation: string;
type_de_structure: string;
code_de_type_de_structure: string;
code_de_niveau_de_structure: string;
site_web: string;
adresse: string;
code_postal: string;
commune: string;
nom_du_responsable: string[];
prenom_du_responsable: string[];
titre_du_responsable: string[];
label_numero: string[];
tutelles: string[];
sigles_des_tutelles: string[];
code_de_nature_de_tutelle: string[];
nature_de_tutelle: string[];
uai_des_tutelles: string[];
siret_des_tutelles: string[];
code_de_type_de_tutelle: string[];
type_de_tutelle: string[];
numero_de_structure_enfant: string[] | null;
numero_de_structure_parent: string | null;
numero_de_structure_historique: string | null;
type_de_succession: string | null;
code_de_type_de_succession: string | null;
annee_d_effet_historique: string | null;
code_domaine_scientifique: string[];
domaine_scientifique: string[];
code_panel_erc: string | null;
panel_erc: string | null;
fiche_rnsr: string;
};

type RNSRResponse = {
total_count: number;
results: Array<RNSROrganisation>;
};

const convertToSchemaOrganization = (organisation: RNSROrganisation): SchemaOrganization => {
const address: SchemaPostalAddress = {
"@type": "PostalAddress",
addressCountry: "France",
addressCountryCode: "FR",
postalCode: organisation.code_postal,
addressLocality: organisation.commune,
streetAddress: organisation.adresse
};

const alternateName: string[] = organisation.sigle ? [organisation.sigle] : [];

const parentOrganizations: SchemaOrganization[] = [];
if (organisation.code_de_type_de_tutelle) {
organisation.code_de_type_de_tutelle.forEach((code, index) => {
if (code === "TUTE") {
parentOrganizations.push({
"@type": "Organization",
name: organisation.tutelles[index],
alternateName: [organisation.sigles_des_tutelles[index]],
identifiers: [
identifersUtils.makeSIRENIdentifier({ SIREN: organisation.siret_des_tutelles[index] })
]
});
}
});
}

const schemaOrganization: SchemaOrganization = {
"@type": "Organization",
name: organisation.libelle,
url: organisation.site_web,
identifiers: [identifersUtils.makeRNSROrgaIdentifer({ rnrsId: organisation.numero_national_de_structure })],
parentOrganizations: parentOrganizations.length > 0 ? parentOrganizations : undefined,
foundingDate: organisation.annee_de_creation,
alternateName: alternateName.length > 0 ? alternateName : undefined,
address: address,
additionalType: [organisation.type_de_structure]
};

return schemaOrganization;
};

const getOrganizationByRNSR = async (rnsrId: string): Promise<RNSRResponse> => {
const url = `https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-structures-recherche-publiques-actives/records/?limit=10&offset=0&where=numero_national_de_structure%3A%22${rnsrId}%22`;

try {
const response = await fetch(url);

if (!response.ok) {
throw new Error(`getOrganizationByRNSR : Erreur HTTP: ${response.status}`);
}

const data = await response.json();

return data;
} catch (error) {
console.error("getOrganizationByRNSR : Erreur lors de la récupération des données:", error);
throw error;
}
};

export const getOrganisationFromRNSRApi = async (params: {
rnsrId: string;
}): Promise<SchemaOrganization | undefined> => {
const { rnsrId } = params;
const result = await getOrganizationByRNSR(rnsrId);
if (result.total_count === 0) return undefined;
if (result.total_count > 2) throw Error("Too much results");

return convertToSchemaOrganization(result.results[0]);
};
43 changes: 43 additions & 0 deletions api/src/core/adapters/RNSR/API/getOrganisation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2021-2026 DINUM <floss@numerique.gouv.fr>
// SPDX-FileCopyrightText: 2024-2026 Université Grenoble Alpes
// SPDX-License-Identifier: MIT

interface ApiResponse {
total_count: number;
results: Array<Record<string, unknown>>;
// Ajoute d'autres champs si nécessaire selon la structure de la réponse
}

interface ApiError {
message: string;
status: number;
}

/**
* Récupère les données d'une structure depuis l'API du MESRI.
* @param numeroNationalDeStructure Le numéro national de la structure (ex: "201221027H")
* @returns Une promesse avec les données de la structure ou une erreur
*/
async function getStructureData(numeroNationalDeStructure: string): Promise<ApiResponse | ApiError> {
const baseUrl = "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog";
const dataset = "fr-esr-repertoire-national-structures-recherche";
const url = new URL(`${baseUrl}/datasets/${dataset}/records`);

// Construction des paramètres de la requête
url.searchParams.append("where", `numero_national_de_structure="${numeroNationalDeStructure}"`);
url.searchParams.append("limit", "1");

try {
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data: ApiResponse = await response.json();
return data;
} catch (error) {
return {
message: error instanceof Error ? error.message : "Erreur inconnue",
status: error instanceof Error && "status" in error ? (error.status as number) : 500
};
}
}
21 changes: 21 additions & 0 deletions api/src/core/adapters/RNSR/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2021-2026 DINUM <floss@numerique.gouv.fr>
// SPDX-FileCopyrightText: 2024-2026 Université Grenoble Alpes
// SPDX-License-Identifier: MIT

import { SourceGateway } from "../../ports/SourceGateway";
import { Source } from "../../usecases/readWriteSillData";
import { getOrganisationFromRNSRApi } from "./API/get";

export type RNSRSourceGateway = SourceGateway & {
organization: NonNullable<SourceGateway["organization"]>;
};

export const rnsrSourceGateway: RNSRSourceGateway = {
sourceType: "RNSR",
organization: {
getOrganization: (params: { organizationId: string; source?: Source }) => {
const org = getOrganisationFromRNSRApi({ rnsrId: params.organizationId });
return org;
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2021-2025 DINUM <floss@numerique.gouv.fr>
// SPDX-FileCopyrightText: 2024-2025 Université Grenoble Alpes
// SPDX-License-Identifier: MIT

import { Kysely } from "kysely";
import { AuthorOrganizationsRepository } from "../../../ports/DbApiV2";
import { Database } from "./kysely.database";

export const createPgAuthorOrganisationsRepository = (db: Kysely<Database>): AuthorOrganizationsRepository => ({
getAll: async (params?: { ids?: Array<string> }) => {
let query = db.selectFrom("author_organizations").select("organization");

if (params?.ids && params.ids.length > 0) {
query = query.where("id", "in", params.ids);
}

const result = await query.execute();
return result.map(row => row.organization);
},
get: async ({ id }) => {
const result = await db
.selectFrom("author_organizations")
.select("organization")
.where("id", "==", id)
.executeTakeFirstOrThrow();
return result.organization;
},
save: async ({ organization }) => {
await db
.insertInto("author_organizations")
.values({ id: organization.name, organization: organization })
.onConflict(oc =>
oc.columns(["id"]).doUpdateSet({
organization: organization
})
)
.execute();

return;
},
checkIfSaved: async ({ ids }) => {
const result = await db.selectFrom("author_organizations").select("id").execute();

const flatResult = result.map(org => org.id);
const output: Record<string, boolean> = {};
ids.forEach(id => {
output[id] = flatResult.includes(id);
});
return output;
},
flush: async () => {
await db.deleteFrom("author_organizations").execute();
}
});
2 changes: 2 additions & 0 deletions api/src/core/adapters/dbApi/kysely/createPgDbApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
createPgSoftwareReferentRepository,
createPgSoftwareUserRepository
} from "./createPgUserAndReferentRepository";
import { createPgAuthorOrganisationsRepository } from "./createAuthorOrganizationsRepository";
import { Database } from "./kysely.database";

export const createKyselyPgDbApi = (db: Kysely<Database>): DbApiV2 => {
Expand All @@ -28,6 +29,7 @@ export const createKyselyPgDbApi = (db: Kysely<Database>): DbApiV2 => {
softwareReferent: createPgSoftwareReferentRepository(db),
softwareUser: createPgSoftwareUserRepository(db),
session: createPgSessionRepository(db),
authorOrganization: createPgAuthorOrganisationsRepository(db),
attributeDefinition: createPgAttributeDefinitionRepository(db),
getCompiledDataPrivate: createGetCompiledData(db)
};
Expand Down
Loading