From 6d63d8061aac70787f2bc2ccce1bf3ac06f5f900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Burkard?= <22095555+JeromeBu@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:35:37 +0200 Subject: [PATCH] write first kysely query to get compiled data private --- .../adapters/dbApi/kysely/createPgDbApi.ts | 172 ++++++++++++++++++ .../adapters/dbApi/kysely/kysely.database.ts | 67 +++---- .../adapters/dbApi/kysely/kysely.utils.ts | 22 +++ api/src/core/ports/CompileData.ts | 5 + .../usecases/readWriteSillData/selectors.ts | 2 +- api/src/rpc/routes.e2e.test.ts | 2 +- api/tsconfig.json | 2 +- 7 files changed, 236 insertions(+), 36 deletions(-) create mode 100644 api/src/core/adapters/dbApi/kysely/createPgDbApi.ts create mode 100644 api/src/core/adapters/dbApi/kysely/kysely.utils.ts diff --git a/api/src/core/adapters/dbApi/kysely/createPgDbApi.ts b/api/src/core/adapters/dbApi/kysely/createPgDbApi.ts new file mode 100644 index 00000000..6fc2c5bc --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createPgDbApi.ts @@ -0,0 +1,172 @@ +import { Kysely } from "kysely"; +import { CompiledData } from "../../../ports/CompileData"; +import { SoftwareExternalData } from "../../../ports/GetSoftwareExternalData"; +import { ServiceProvider } from "../../../usecases/readWriteSillData"; +import { Database } from "./kysely.database"; +import { createPgDialect } from "./kysely.dialect"; +import { emptyArrayIfNull, jsonAggOrEmptyArray, jsonBuildObject, jsonStripNulls } from "./kysely.utils"; + +export const createKyselyPgDbApi = (dbUrl: string) => { + const db = new Kysely({ dialect: createPgDialect(dbUrl) }); + + return { + // getSoftwareById: async (id: number): Promise => { + // const result = await db.selectFrom("softwares").selectAll().where("id", "=", id).executeTakeFirst(); + // if (!result) return; + // + // return { + // ...result, + // parentSoftwareWikidataId: result?.parentSoftwareWikidataId ?? undefined, + // dereferencing: result?.dereferencing ?? undefined, + // externalId: result?.externalId ?? undefined, + // externalDataOrigin: result?.externalDataOrigin ?? "wikidata", + // comptoirDuLibreId: result?.comptoirDuLibreId ?? undefined, + // catalogNumeriqueGouvFrId: result?.catalogNumeriqueGouvFrId ?? undefined, + // generalInfoMd: result?.generalInfoMd ?? undefined, + // logoUrl: result?.logoUrl ?? undefined + // }; + // }, + getCompiledDataPrivate: (): Promise> => { + return db + .selectFrom("softwares as s") + .leftJoin("compiled_softwares as csft", "csft.softwareId", "s.id") + .leftJoin("software_referents as referents", "s.id", "referents.softwareId") + .leftJoin("software_users as users", "s.id", "users.softwareId") + .leftJoin("agents as ar", "referents.agentId", "ar.id") + .leftJoin("agents as au", "referents.agentId", "au.id") + .leftJoin("instances", "s.id", "instances.mainSoftwareSillId") + .select(({ ref, fn }) => + // jsonStripNulls( + jsonStripNulls( + jsonBuildObject({ + addedByAgentEmail: ref("s.addedByAgentEmail"), + annuaireCnllServiceProviders: ref("annuaireCnllServiceProviders"), + catalogNumeriqueGouvFrId: ref("s.catalogNumeriqueGouvFrId"), + categories: ref("s.categories"), + comptoirDuLibreSoftware: ref("csft.comptoirDuLibreSoftware"), + dereferencing: ref("s.dereferencing"), + description: ref("s.description"), + doRespectRgaa: ref("s.doRespectRgaa"), + externalDataOrigin: ref("s.externalDataOrigin"), + externalId: ref("s.externalId"), + generalInfoMd: ref("s.generalInfoMd"), + id: ref("s.id"), + isFromFrenchPublicService: ref("s.isFromFrenchPublicService"), + isPresentInSupportContract: ref("s.isPresentInSupportContract"), + isStillInObservation: ref("s.isStillInObservation"), + keywords: ref("s.keywords"), + latestVersion: ref("csft.latestVersion"), + license: ref("s.license"), + logoUrl: ref("s.logoUrl"), + name: ref("s.name"), + parentWikidataSoftware: ref("csft.parentWikidataSoftware"), + referencedSinceTime: ref("s.referencedSinceTime"), + serviceProviders: emptyArrayIfNull(fn, ref("csft.serviceProviders")).$castTo< + ServiceProvider[] + >(), + similarExternalSoftwares: emptyArrayIfNull( + fn, + ref("csft.similarExternalSoftwares") + ).$castTo(), + softwareExternalData: ref("csft.softwareExternalData"), + softwareType: ref("s.softwareType"), + testUrls: ref("s.testUrls"), + updateTime: ref("s.updateTime"), + versionMin: ref("s.versionMin"), + workshopUrls: ref("s.workshopUrls"), + referents: jsonAggOrEmptyArray( + fn, + jsonStripNulls( + jsonBuildObject({ + email: ref("ar.email").$castTo(), + organization: ref("ar.organization").$castTo(), + isExpert: ref("referents.isExpert").$castTo(), + serviceUrl: ref("referents.serviceUrl"), + useCaseDescription: ref("referents.useCaseDescription").$castTo() + }) + ) + ), + users: jsonAggOrEmptyArray( + fn, + jsonStripNulls( + jsonBuildObject({ + os: ref("users.os"), + serviceUrl: ref("users.serviceUrl"), + useCaseDescription: ref("users.useCaseDescription").$castTo(), + version: ref("users.version").$castTo(), + organization: ref("au.organization").$castTo() + }) + ) + ), + instances: jsonAggOrEmptyArray( + fn, + jsonStripNulls( + jsonBuildObject({ + id: ref("instances.id").$castTo(), + organization: ref("instances.organization").$castTo(), + targetAudience: ref("instances.targetAudience").$castTo(), + publicUrl: ref("instances.publicUrl"), + otherWikidataSoftwares: ref("instances.otherSoftwareWikidataIds").$castTo< + SoftwareExternalData[] + >(), // todo fetch the corresponding softwares, + addedByAgentEmail: ref("instances.addedByAgentEmail").$castTo() + }) + ) + ) + }) + ).as("compliedSoftware") + ) + .execute() + .then(results => + results.map( + ({ compliedSoftware }): CompiledData.Software<"private"> => ({ + ...compliedSoftware, + doRespectRgaa: compliedSoftware.doRespectRgaa ?? null + }) + ) + ); + } + }; +}; + +// ----------- common ----------- +// annuaireCnllServiceProviders +// catalogNumeriqueGouvFrId +// categories +// comptoirDuLibreSoftware +// dereferencing +// description +// doRespectRgaa +// externalDataOrigin +// externalId +// generalInfoMd +// id +// isFromFrenchPublicService +// isPresentInSupportContract +// isStillInObservation +// keywords +// latestVersion +// license +// logoUrl +// name +// parentWikidataSoftware +// referencedSinceTime +// serviceProviders +// similarExternalSoftwares +// softwareExternalData +// softwareType +// testUrls +// updateTime +// versionMin +// workshopUrls +// +// ----------- private ----------- +// addedByAgentEmail +// users +// referents +// instances +// +// ----------- public ----------- +// userAndReferentCountByOrganization +// hasExpertReferent +// instances diff --git a/api/src/core/adapters/dbApi/kysely/kysely.database.ts b/api/src/core/adapters/dbApi/kysely/kysely.database.ts index 5cf69f05..bc35b193 100644 --- a/api/src/core/adapters/dbApi/kysely/kysely.database.ts +++ b/api/src/core/adapters/dbApi/kysely/kysely.database.ts @@ -97,43 +97,44 @@ type SoftwaresTable = { // ---------- compiled data ---------- -type ComptoirDuLibreProvider = { - id: number; - url: string; - name: string; - type: string; - external_resources: { - website: string | null; +export namespace PgComptoirDuLibre { + type Provider = { + id: number; + url: string; + name: string; + type: string; + external_resources: { + website: string | null; + }; }; -}; -type ComptoirDuLibreUser = { - id: number; - url: string; - name: string; - type: string; - external_resources: { - website: string | null; + type User = { + id: number; + url: string; + name: string; + type: string; + external_resources: { + website: string | null; + }; }; -}; -type ComptoirDuLibreSoftware = { - softwareId: number; - comptoirDuLibreId: number; - logoUrl: string | undefined; - keywords: string[] | undefined; - created: string; - modified: string; - url: string; - name: string; - licence: string; - external_resources: { - website: string | null; - repository: string | null; + export type Software = { + id: number; + logoUrl: string | undefined; + keywords: string[] | undefined; + created: string; + modified: string; + url: string; + name: string; + licence: string; + external_resources: { + website: string | null; + repository: string | null; + }; + providers: Provider[]; + users: User[]; }; - providers: ComptoirDuLibreProvider[]; - users: ComptoirDuLibreUser[]; -}; +} type ServiceProvider = { name: string; @@ -174,7 +175,7 @@ type CompiledSoftwaresTable = { Pick[] >; parentWikidataSoftware: JSONColumnType> | null; - comptoirDuLibreSoftware: JSONColumnType | null; + comptoirDuLibreSoftware: JSONColumnType | null; annuaireCnllServiceProviders: JSONColumnType< { name: string; diff --git a/api/src/core/adapters/dbApi/kysely/kysely.utils.ts b/api/src/core/adapters/dbApi/kysely/kysely.utils.ts new file mode 100644 index 00000000..9644598f --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/kysely.utils.ts @@ -0,0 +1,22 @@ +import { Expression, FunctionModule, RawBuilder, Simplify, sql } from "kysely"; + +export const jsonBuildObject = >>( + obj: O +): RawBuilder< + Simplify<{ + [K in keyof O]: O[K] extends Expression ? V : never; + }> +> => sql`json_build_object(${sql.join(Object.keys(obj).flatMap(k => [sql.lit(k), obj[k]]))})`; + +type NullableToUndefined = A extends null ? Exclude | undefined : A; +type StripNullRecursive = { + [K in keyof T]: T[K] extends Record ? StripNullRecursive : NullableToUndefined; +}; +export const jsonStripNulls = (obj: RawBuilder): RawBuilder> => + sql`json_strip_nulls(${obj})`; + +export const jsonAggOrEmptyArray = >(fn: FunctionModule, value: E) => + emptyArrayIfNull(fn, fn.jsonAgg(value)); + +export const emptyArrayIfNull = >(fn: FunctionModule, value: E) => + fn.coalesce(value, sql`'[]'`); diff --git a/api/src/core/ports/CompileData.ts b/api/src/core/ports/CompileData.ts index d9983c72..96245e0e 100644 --- a/api/src/core/ports/CompileData.ts +++ b/api/src/core/ports/CompileData.ts @@ -28,6 +28,11 @@ export type CompiledData = CompiledData.Software export namespace CompiledData { export type Software = T extends "private" ? Software.Private : Software.Public; + + export type SimilarSoftware = Pick< + SoftwareExternalData, + "externalId" | "label" | "description" | "isLibreSoftware" | "externalDataOrigin" + >; export namespace Software { export type Common = Pick< Db.SoftwareRow, diff --git a/api/src/core/usecases/readWriteSillData/selectors.ts b/api/src/core/usecases/readWriteSillData/selectors.ts index 1cabde02..4bd39781 100644 --- a/api/src/core/usecases/readWriteSillData/selectors.ts +++ b/api/src/core/usecases/readWriteSillData/selectors.ts @@ -194,7 +194,7 @@ const softwares = createSelector(compiledData, similarSoftwarePartition, (compil "versionMin": o.versionMin, "license": o.license, "comptoirDuLibreServiceProviderCount": o.comptoirDuLibreSoftware?.providers.length ?? 0, - "annuaireCnllServiceProviders": o.annuaireCnllServiceProviders, + "annuaireCnllServiceProviders": o.annuaireCnllServiceProviders ?? [], "comptoirDuLibreId": o.comptoirDuLibreSoftware?.id, "externalId": o.softwareExternalData?.externalId, "externalDataOrigin": o.softwareExternalData?.externalDataOrigin, diff --git a/api/src/rpc/routes.e2e.test.ts b/api/src/rpc/routes.e2e.test.ts index f603c775..2fb57022 100644 --- a/api/src/rpc/routes.e2e.test.ts +++ b/api/src/rpc/routes.e2e.test.ts @@ -85,7 +85,7 @@ describe("RPC e2e tests", () => { const expectedSoftware: Partial> = { "description": softwareFormData.softwareDescription, "externalId": softwareFormData.externalId, - "doRespectRgaa": softwareFormData.doRespectRgaa, + "doRespectRgaa": softwareFormData.doRespectRgaa ?? undefined, "isFromFrenchPublicService": softwareFormData.isFromFrenchPublicService, "isPresentInSupportContract": softwareFormData.isPresentInSupportContract, "keywords": softwareFormData.softwareKeywords, diff --git a/api/tsconfig.json b/api/tsconfig.json index 5093ffc7..582496c0 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -17,5 +17,5 @@ "noFallthroughCasesInSwitch": true, "skipLibCheck": true }, - "include": ["src", "scripts"], + "include": ["src", "scripts"] }