diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ebe91923..e16a8948 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,17 @@ on: jobs: validations: runs-on: ubuntu-latest + env: + DATABASE_URL: postgresql://sill:pg_password@localhost:5432/sill + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: sill + POSTGRES_PASSWORD: pg_password + POSTGRES_DB: sill + ports: + - 5432:5432 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -18,6 +29,8 @@ jobs: - uses: bahmutov/npm-install@v1 - name: Build back run: cd api && yarn build + - name: Migrate db + run: cd api && yarn migrate latest - name: Fullcheck run: yarn fullcheck # diff --git a/Dockerfile b/Dockerfile index a965d9fa..cf45011f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ RUN yarn build WORKDIR /app/api RUN rm -r src -RUN cp dist -r src/ +RUN cp dist/src -r src/ RUN npx ncc build src/main.js WORKDIR /app @@ -39,7 +39,7 @@ RUN apk add --no-cache \ git \ openssh-client \ ca-certificates -COPY --from=build /app/api/dist/index.js . +COPY --from=build /app/api/dist/src/lib/index.js . # For reading the version number COPY --from=build /app/package.json . ENTRYPOINT sh -c "forever index.js" diff --git a/api/.config/kysely.config.ts b/api/.config/kysely.config.ts new file mode 100644 index 00000000..22c9b636 --- /dev/null +++ b/api/.config/kysely.config.ts @@ -0,0 +1,16 @@ +import { PostgresDialect } from "kysely"; +import { defineConfig } from "kysely-ctl"; +import { Pool } from "pg"; + +const dialect = new PostgresDialect({ + pool: new Pool({ + connectionString: process.env.DATABASE_URL + }) +}); + +export default defineConfig({ + dialect, + migrations: { + migrationFolder: "src/core/adapters/dbApi/kysely/migrations" + } +}); diff --git a/api/package.json b/api/package.json index 2f671083..738269e9 100644 --- a/api/package.json +++ b/api/package.json @@ -4,22 +4,24 @@ "description": "The backend of code.gouv.fr/sill", "repository": { "type": "git", - "url": "git://github.com/codegouvfr/sill-api.git" + "url": "git://github.com/codegouvfr/sill.git" }, - "main": "dist/lib/index.js", - "types": "dist/lib/index.d.ts", + "main": "dist/src/lib/index.js", + "types": "dist/src/lib/index.d.ts", "scripts": { + "migrate": "dotenv -e ../.env -- kysely migrate", "prepare": "[ ! -f .env.local.sh ] && cp .env.sh .env.local.sh || true", "test": "vitest --watch=false", "dev": "yarn build && yarn start", "build": "tsc", - "start": "dotenv -e ../.env -- forever dist/main.js", + "start": "dotenv -e ../.env -- forever dist/src/main.js", "_format": "prettier \"**/*.{ts,tsx,json,md}\"", "format": "yarn run _format --write", "format:check": "yarn run _format --list-different", "link-in-web": "ts-node --skipProject scripts/link-in-app.ts sill-web", "reset-data-test": "ts-node --skipProject scripts/reset-data-test.ts", "compile-data": "./.env.local.sh ts-node --skipProject scripts/compile-data.ts", + "load-git-repo-in-pg": "dotenv -e ../.env -- ts-node --skipProject scripts/load-git-repo-in-pg.ts", "typecheck": "tsc --noEmit" }, "author": "DINUM", @@ -30,7 +32,7 @@ "!dist/tsconfig.tsbuildinfo" ], "keywords": [], - "homepage": "https://github.com/codegouvfr/sill-api", + "homepage": "https://github.com/codegouvfr/sill", "devDependencies": { "@octokit/rest": "^18.12.0", "@types/compression": "^1.7.2", @@ -74,7 +76,11 @@ "@octokit/graphql": "^7.0.2", "@retorquere/bibtex-parser": "^7.0.11", "@trpc/server": "^10.18.0", + "@types/pg": "^8.11.6", "jwt-decode": "^3.1.2", + "kysely": "^0.27.4", + "kysely-ctl": "^0.8.10", + "pg": "^8.11.5", "semver": "^7.5.4", "tsafe": "^1.6.6", "zod": "^3.21.4" diff --git a/api/scripts/compile-data.ts b/api/scripts/compile-data.ts index b4a45328..4fc09872 100644 --- a/api/scripts/compile-data.ts +++ b/api/scripts/compile-data.ts @@ -4,7 +4,8 @@ import { env } from "../src/env"; (async () => { const { core } = await bootstrapCore({ "keycloakUserApiParams": undefined, - "gitDbApiParams": { + "dbConfig": { + "dbKind": "git", "dataRepoSshUrl": "git@github.com:codegouvfr/sill-data.git", "sshPrivateKey": env.sshPrivateKeyForGit, "sshPrivateKeyName": env.sshPrivateKeyForGitName diff --git a/api/scripts/load-git-repo-in-pg.ts b/api/scripts/load-git-repo-in-pg.ts new file mode 100644 index 00000000..4f19cc0b --- /dev/null +++ b/api/scripts/load-git-repo-in-pg.ts @@ -0,0 +1,265 @@ +import { InsertObject, Kysely } from "kysely"; +import { z } from "zod"; +import { createGitDbApi, GitDbApiParams } from "../src/core/adapters/dbApi/createGitDbApi"; +import { Database } from "../src/core/adapters/dbApi/kysely/kysely.database"; +import { createPgDialect } from "../src/core/adapters/dbApi/kysely/kysely.dialect"; +import { CompiledData } from "../src/core/ports/CompileData"; +import { Db } from "../src/core/ports/DbApi"; +import { ExternalDataOrigin } from "../src/core/ports/GetSoftwareExternalData"; +import SoftwareRow = Db.SoftwareRow; + +export type Params = { + pgConfig: { dbUrl: string }; + gitDbConfig: GitDbApiParams; +}; + +const saveGitDbInPostgres = async ({ pgConfig, gitDbConfig }: Params) => { + const { dbApi: gitDbApi } = createGitDbApi(gitDbConfig); + if (!pgConfig.dbUrl) throw new Error("Missing PG database url, please set the DATABASE_URL environnement variable"); + const pgDb = new Kysely({ dialect: createPgDialect(pgConfig.dbUrl) }); + + const { softwareRows, agentRows, softwareReferentRows, softwareUserRows, instanceRows } = await gitDbApi.fetchDb(); + + await insertSoftwares(softwareRows, pgDb); + await insertAgents(agentRows, pgDb); + + const agentIdByEmail = await makeGetAgentIdByEmail(pgDb); + await insertSoftwareReferents({ + softwareReferentRows: softwareReferentRows, + agentIdByEmail: agentIdByEmail, + db: pgDb + }); + await insertSoftwareUsers({ + softwareUserRows: softwareUserRows, + agentIdByEmail: agentIdByEmail, + db: pgDb + }); + await insertInstances({ + instanceRows: instanceRows, + db: pgDb + }); + + const compiledSoftwares = await gitDbApi.fetchCompiledData(); + await insertCompiledSoftwaresAndSoftwareExternalData(compiledSoftwares, pgDb); +}; + +const insertSoftwares = async (softwareRows: SoftwareRow[], db: Kysely) => { + console.info("Deleting than Inserting softwares"); + console.info("Number of softwares to insert : ", softwareRows.length); + await db.transaction().execute(async trx => { + await trx.deleteFrom("softwares").execute(); + await trx.deleteFrom("softwares__similar_software_external_datas").execute(); + await trx + .insertInto("softwares") + .values( + softwareRows.map(({ similarSoftwareExternalDataIds: _, ...row }) => ({ + ...row, + dereferencing: row.dereferencing ? JSON.stringify(row.dereferencing) : null, + softwareType: JSON.stringify(row.softwareType), + workshopUrls: JSON.stringify(row.workshopUrls), + testUrls: JSON.stringify(row.testUrls), + categories: JSON.stringify(row.categories), + keywords: JSON.stringify(row.keywords) + })) + ) + .executeTakeFirst(); + + await trx + .insertInto("softwares__similar_software_external_datas") + .values( + softwareRows.flatMap(row => + Array.from(new Set(row.similarSoftwareExternalDataIds)).map(externalId => ({ + softwareId: row.id, + similarExternalId: externalId + })) + ) + ) + .execute(); + }); +}; + +const insertAgents = async (agentRows: Db.AgentRow[], db: Kysely) => { + console.log("Deleting than Inserting agents"); + console.info("Number of agents to insert : ", agentRows.length); + await db.transaction().execute(async trx => { + await trx.deleteFrom("agents").execute(); + await trx.insertInto("agents").values(agentRows).executeTakeFirst(); + }); +}; + +const makeGetAgentIdByEmail = async (db: Kysely): Promise> => { + console.info("Fetching agents, to map email to id"); + const agents = await db.selectFrom("agents").select(["email", "id"]).execute(); + return agents.reduce((acc, agent) => ({ ...acc, [agent.email]: agent.id }), {}); +}; + +const insertSoftwareReferents = async ({ + softwareReferentRows, + agentIdByEmail, + db +}: { + softwareReferentRows: Db.SoftwareReferentRow[]; + agentIdByEmail: Record; + db: Kysely; +}) => { + console.info("Deleting than Inserting software referents"); + console.info("Number of software referents to insert : ", softwareReferentRows.length); + await db.transaction().execute(async trx => { + await trx.deleteFrom("software_referents").execute(); + await trx + .insertInto("software_referents") + .values( + softwareReferentRows.map(({ agentEmail, ...rest }) => ({ + ...rest, + agentId: agentIdByEmail[agentEmail] + })) + ) + .executeTakeFirst(); + }); +}; + +const insertSoftwareUsers = async ({ + softwareUserRows, + agentIdByEmail, + db +}: { + softwareUserRows: Db.SoftwareUserRow[]; + agentIdByEmail: Record; + db: Kysely; +}) => { + console.info("Deleting than Inserting software users"); + console.info("Number of software users to insert : ", softwareUserRows.length); + await db.transaction().execute(async trx => { + await trx.deleteFrom("software_users").execute(); + await trx + .insertInto("software_users") + .values( + softwareUserRows.map(({ agentEmail, ...rest }) => ({ + ...rest, + agentId: agentIdByEmail[agentEmail] + })) + ) + .executeTakeFirst(); + }); +}; + +const insertInstances = async ({ instanceRows, db }: { instanceRows: Db.InstanceRow[]; db: Kysely }) => { + console.info("Deleting than Inserting instances"); + console.info("Number of instances to insert : ", instanceRows.length); + await db.transaction().execute(async trx => { + await trx.deleteFrom("instances").execute(); + await trx.insertInto("instances").values(instanceRows).executeTakeFirst(); + }); +}; + +const insertCompiledSoftwaresAndSoftwareExternalData = async ( + compiledSoftwares: CompiledData.Software<"private">[], + pgDb: Kysely +) => { + console.info("Deleting than Inserting compiled softwares"); + console.info("Number of compiled softwares to insert : ", compiledSoftwares.length); + await pgDb.transaction().execute(async trx => { + await trx.deleteFrom("compiled_softwares").execute(); + await trx + .insertInto("compiled_softwares") + .values( + compiledSoftwares.map( + (software): InsertObject => ({ + softwareId: software.id, + serviceProviders: JSON.stringify(software.serviceProviders), + comptoirDuLibreSoftware: JSON.stringify(software.comptoirDuLibreSoftware), + annuaireCnllServiceProviders: JSON.stringify(software.annuaireCnllServiceProviders), + latestVersion: JSON.stringify(software.latestVersion) + }) + ) + ) + .executeTakeFirst(); + + await trx.deleteFrom("software_external_datas").execute(); + + await trx + .insertInto("software_external_datas") + .values( + compiledSoftwares + .filter( + ( + software + ): software is CompiledData.Software.Private & { + softwareExternalData: { + externalId: string; + externalDataOrigin: ExternalDataOrigin; + }; + } => + software.softwareExternalData?.externalId !== undefined && + software.softwareExternalData?.externalDataOrigin !== undefined + ) + .map( + ({ softwareExternalData }): InsertObject => ({ + externalId: softwareExternalData.externalId, + externalDataOrigin: softwareExternalData.externalDataOrigin, + developers: JSON.stringify(softwareExternalData?.developers ?? []), + label: JSON.stringify(softwareExternalData?.label ?? {}), + description: JSON.stringify(softwareExternalData?.description ?? {}), + isLibreSoftware: softwareExternalData?.isLibreSoftware ?? false, + logoUrl: softwareExternalData?.logoUrl ?? null, + framaLibreId: softwareExternalData?.framaLibreId ?? null, + websiteUrl: softwareExternalData?.websiteUrl ?? null, + sourceUrl: softwareExternalData?.sourceUrl ?? null, + documentationUrl: softwareExternalData?.documentationUrl ?? null, + license: softwareExternalData?.license ?? null + }) + ) + ) + .onConflict(conflict => conflict.column("externalId").doNothing()) + .executeTakeFirst(); + + await trx + .insertInto("software_external_datas") + .values( + compiledSoftwares + .filter(s => s.similarExternalSoftwares.length > 0) + .flatMap(s => + (s.similarExternalSoftwares ?? []).map(similarExternalSoftware => ({ + externalId: similarExternalSoftware.externalId, + externalDataOrigin: similarExternalSoftware.externalDataOrigin, + developers: JSON.stringify([]), + label: JSON.stringify(similarExternalSoftware?.label ?? {}), + description: JSON.stringify(similarExternalSoftware?.description ?? {}), + isLibreSoftware: similarExternalSoftware?.isLibreSoftware ?? false + })) + ) + ) + .onConflict(conflict => conflict.column("externalId").doNothing()) + .executeTakeFirst(); + }); +}; + +const paramsSchema: z.Schema = z.object({ + pgConfig: z.object({ + dbUrl: z.string() + }), + gitDbConfig: z.object({ + dataRepoSshUrl: z.string(), + sshPrivateKey: z.string(), + sshPrivateKeyName: z.string() + }) +}); + +const timerName = "Script duration"; +console.time(timerName); + +saveGitDbInPostgres( + paramsSchema.parse({ + pgConfig: { dbUrl: process.env.DATABASE_URL }, + gitDbConfig: { + dataRepoSshUrl: process.env.SILL_DATA_REPO_SSH_URL, + sshPrivateKey: process.env.SILL_SSH_PRIVATE_KEY, + sshPrivateKeyName: process.env.SILL_SSH_NAME + } + }) +) + .then(() => { + console.log("Load git db in postgres with success"); + process.exit(0); + }) + .finally(() => console.timeEnd(timerName)); diff --git a/api/scripts/migration/instance.ts b/api/scripts/migration/instance.ts index 289e2336..8cf861cf 100644 --- a/api/scripts/migration/instance.ts +++ b/api/scripts/migration/instance.ts @@ -15,7 +15,6 @@ const zInstanceRow = z.object({ "organization": z.string(), "targetAudience": z.string(), "publicUrl": z.string().optional(), - "otherSoftwareWikidataIds": z.array(z.string()), "addedByAgentEmail": z.string(), "referencedSinceTime": z.number(), "updateTime": z.number() @@ -43,7 +42,6 @@ fs.writeFileSync( id, mainSoftwareSillId, organization, - otherSoftwareWikidataIds, publicUrl, targetAudience, addedByAgentEmail, @@ -58,7 +56,6 @@ fs.writeFileSync( id, mainSoftwareSillId, organization, - otherSoftwareWikidataIds, publicUrl, targetAudience, addedByAgentEmail, diff --git a/api/src/core/adapters/dbApi/kysely/createGetCompiledData.ts b/api/src/core/adapters/dbApi/kysely/createGetCompiledData.ts new file mode 100644 index 00000000..19d1f1fd --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createGetCompiledData.ts @@ -0,0 +1,187 @@ +import { Kysely } from "kysely"; +import { CompiledData } from "../../../ports/CompileData"; +import { Db } from "../../../ports/DbApi"; +import { ParentSoftwareExternalData, SoftwareExternalData } from "../../../ports/GetSoftwareExternalData"; +import { Database } from "./kysely.database"; +import { convertNullValuesToUndefined, isNotNull, jsonBuildObject, jsonStripNulls } from "./kysely.utils"; + +export const createGetCompiledData = (db: Kysely) => async (): Promise> => { + console.time("agentById query"); + const agentById: Record = await db + .selectFrom("agents") + .selectAll() + .execute() + .then(agents => agents.reduce((acc, agent) => ({ ...acc, [agent.id]: agent }), {})); + console.timeEnd("agentById query"); + + console.time("softwares query"); + const compliedSoftwares = await 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("instances", "s.id", "instances.mainSoftwareSillId") + .leftJoin("software_external_datas as ext", "ext.externalId", "s.externalId") + .leftJoin("software_external_datas as parentExt", "parentExt.externalId", "s.parentSoftwareWikidataId") + .leftJoin( + "softwares__similar_software_external_datas", + "softwares__similar_software_external_datas.softwareId", + "s.id" + ) + .leftJoin( + "software_external_datas as similarExt", + "softwares__similar_software_external_datas.similarExternalId", + "similarExt.externalId" + ) + .groupBy([ + "s.id", + "csft.softwareId", + "csft.annuaireCnllServiceProviders", + "csft.comptoirDuLibreSoftware", + "csft.latestVersion", + "csft.serviceProviders", + "parentExt.externalId", + "ext.externalId" + ]) + .select([ + "s.id", + "s.addedByAgentEmail", + "s.catalogNumeriqueGouvFrId", + "s.categories", + "s.dereferencing", + "s.description", + "s.doRespectRgaa", + "s.externalDataOrigin", + "s.externalId", + "s.generalInfoMd", + "s.isFromFrenchPublicService", + "s.isPresentInSupportContract", + "s.isStillInObservation", + "s.keywords", + "s.license", + "s.logoUrl", + "s.name", + "s.referencedSinceTime", + "s.softwareType", + "s.testUrls", + "s.updateTime", + "s.versionMin", + "s.workshopUrls", + "csft.softwareId as externalDataSoftwareId", + "csft.annuaireCnllServiceProviders", + "csft.comptoirDuLibreSoftware", + "csft.latestVersion", + "csft.serviceProviders", + ({ ref, ...qb }) => + qb + .case() + .when("parentExt.externalId", "is not", null) + .then( + jsonBuildObject({ + externalId: ref("parentExt.externalId"), + label: ref("parentExt.label"), + description: ref("parentExt.description") + }).$castTo() + ) + .end() + .as("parentWikidataSoftware"), + ({ ref, ...qb }) => + qb + .case() + .when("ext.externalId", "is not", null) + .then( + jsonStripNulls( + jsonBuildObject({ + externalId: ref("ext.externalId"), + externalDataOrigin: ref("ext.externalDataOrigin"), + developers: ref("ext.developers"), + label: ref("ext.label"), + description: ref("ext.description"), + isLibreSoftware: ref("ext.isLibreSoftware"), + logoUrl: ref("ext.logoUrl"), + framaLibreId: ref("ext.framaLibreId"), + websiteUrl: ref("ext.websiteUrl"), + sourceUrl: ref("ext.sourceUrl"), + documentationUrl: ref("ext.documentationUrl"), + license: ref("ext.license") + }) + ).$castTo() + ) + .end() + .as("softwareExternalData"), + ({ fn }) => fn.jsonAgg("similarExt").distinct().as("similarExternalSoftwares"), + ({ fn }) => fn.jsonAgg("users").distinct().as("users"), + ({ fn }) => fn.jsonAgg("referents").distinct().as("referents"), + ({ fn }) => fn.jsonAgg("instances").distinct().as("instances") + ]) + .execute() + .then(results => { + console.timeEnd("softwares query"); + console.time("software processing"); + const processedSoftwares = results.map( + ({ + externalDataSoftwareId, + annuaireCnllServiceProviders, + comptoirDuLibreSoftware, + latestVersion, + parentWikidataSoftware, + serviceProviders, + similarExternalSoftwares, + dereferencing, + doRespectRgaa, + users, + referents, + instances, + softwareExternalData, + updateTime, + referencedSinceTime, + ...software + }): CompiledData.Software<"private"> => { + return { + ...convertNullValuesToUndefined(software), + updateTime: new Date(+updateTime).getTime(), + referencedSinceTime: new Date(+referencedSinceTime).getTime(), + doRespectRgaa, + softwareExternalData: softwareExternalData ?? undefined, + annuaireCnllServiceProviders: annuaireCnllServiceProviders ?? undefined, + comptoirDuLibreSoftware: comptoirDuLibreSoftware ?? undefined, + latestVersion: latestVersion ?? undefined, + parentWikidataSoftware: parentWikidataSoftware ?? undefined, + dereferencing: dereferencing ?? undefined, + serviceProviders: serviceProviders ?? [], + similarExternalSoftwares: (similarExternalSoftwares ?? []) + .filter(isNotNull) + .map(similar => ({ + "externalId": similar.externalId!, + "externalDataOrigin": similar.externalDataOrigin!, + "label": similar.label!, + "description": similar.description!, + "isLibreSoftware": similar.isLibreSoftware! + })) + .sort((a, b) => a.externalId.localeCompare(b.externalId)), + users: users.filter(isNotNull).map(user => ({ + ...(user as any), + organization: agentById[user.agentId!]?.organization + })), + referents: referents.filter(isNotNull).map(referent => ({ + ...(referent as any), + organization: agentById[referent.agentId!]?.organization + })), + instances: (instances ?? []).filter(isNotNull).map(instance => ({ + id: instance.id!, + organization: instance.organization!, + targetAudience: instance.targetAudience!, + publicUrl: instance.publicUrl ?? undefined, + addedByAgentEmail: instance.addedByAgentEmail!, + otherWikidataSoftwares: [] + })) + }; + } + ); + console.timeEnd("software processing"); + return processedSoftwares; + }); + + console.log("numberOfCompiledSoftwares : ", compliedSoftwares.length); + return compliedSoftwares; +}; diff --git a/api/src/core/adapters/dbApi/kysely/createPgAgentRepository.ts b/api/src/core/adapters/dbApi/kysely/createPgAgentRepository.ts new file mode 100644 index 00000000..e2e0db47 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createPgAgentRepository.ts @@ -0,0 +1,31 @@ +import type { Kysely, Selectable } from "kysely"; +import { Agent, AgentRepository } from "../../../ports/DbApiV2"; +import type { Database } from "./kysely.database"; + +export const createPgAgentRepository = (db: Kysely): AgentRepository => ({ + add: async agent => { + await db.insertInto("agents").values(agent).execute(); + }, + update: async agent => { + await db.updateTable("agents").set(agent).where("id", "=", agent.id).execute(); + }, + remove: async agentId => { + await db.deleteFrom("agents").where("id", "=", agentId).execute(); + }, + getByEmail: async email => { + const dbAgent = await db.selectFrom("agents").selectAll().where("email", "=", email).executeTakeFirst(); + if (!dbAgent) return; + return toAgent(dbAgent); + }, + getAll: async () => + db + .selectFrom("agents") + .selectAll() + .execute() + .then(dbAgent => dbAgent.map(toAgent)) +}); + +const toAgent = (row: Selectable): Agent => ({ + ...row, + about: row.about ?? undefined +}); 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..610d8014 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createPgDbApi.ts @@ -0,0 +1,19 @@ +import { Kysely } from "kysely"; +import { DbApiV2 } from "../../../ports/DbApiV2"; +import { createGetCompiledData } from "./createGetCompiledData"; +import { createPgAgentRepository } from "./createPgAgentRepository"; +import { createPgInstanceRepository } from "./createPgInstanceRepository"; +import { createPgSoftwareRepository } from "./createPgSoftwareRepository"; +import { createPgReferentRepository, createPgUserRepository } from "./createPgUserAndReferentRepository"; +import { Database } from "./kysely.database"; + +export const createKyselyPgDbApi = (db: Kysely): DbApiV2 => { + return { + software: createPgSoftwareRepository(db), + instance: createPgInstanceRepository(db), + agent: createPgAgentRepository(db), + softwareReferent: createPgReferentRepository(db), + softwareUser: createPgUserRepository(db), + getCompiledDataPrivate: createGetCompiledData(db) + }; +}; diff --git a/api/src/core/adapters/dbApi/kysely/createPgInstanceRepository.ts b/api/src/core/adapters/dbApi/kysely/createPgInstanceRepository.ts new file mode 100644 index 00000000..62429782 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createPgInstanceRepository.ts @@ -0,0 +1,58 @@ +import { Kysely } from "kysely"; +import type { Equals } from "tsafe"; +import { assert } from "tsafe/assert"; +import { InstanceRepository } from "../../../ports/DbApiV2"; +import { Instance } from "../../../usecases/readWriteSillData"; +import { Database } from "./kysely.database"; + +export const createPgInstanceRepository = (db: Kysely): InstanceRepository => ({ + create: async ({ fromData, agentEmail }) => { + const { mainSoftwareSillId, organization, targetAudience, publicUrl, ...rest } = fromData; + assert>(); + + const now = Date.now(); + await db + .insertInto("instances") + .values({ + addedByAgentEmail: agentEmail, + updateTime: now, + referencedSinceTime: now, + mainSoftwareSillId, + organization, + targetAudience, + publicUrl + }) + .executeTakeFirstOrThrow(); + }, + update: async ({ fromData, instanceId }) => { + const { mainSoftwareSillId, organization, targetAudience, publicUrl, ...rest } = fromData; + assert>(); + + const now = Date.now(); + await db + .updateTable("instances") + .set({ + updateTime: now, + mainSoftwareSillId, + organization, + targetAudience, + publicUrl + }) + .where("id", "=", instanceId) + .execute(); + }, + getAll: async () => + db + .selectFrom("instances as i") + .groupBy(["i.id"]) + .select(["i.id", "i.mainSoftwareSillId", "i.organization", "i.targetAudience", "i.publicUrl"]) + .execute() + .then(instances => + instances.map( + (instance): Instance => ({ + ...instance, + publicUrl: instance.publicUrl ?? undefined + }) + ) + ) +}); diff --git a/api/src/core/adapters/dbApi/kysely/createPgSoftwareRepository.ts b/api/src/core/adapters/dbApi/kysely/createPgSoftwareRepository.ts new file mode 100644 index 00000000..7fe35a9b --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createPgSoftwareRepository.ts @@ -0,0 +1,279 @@ +import { Kysely, sql } from "kysely"; +import type { Equals } from "tsafe"; +import { assert } from "tsafe/assert"; +import { SoftwareRepository } from "../../../ports/DbApiV2"; +import { ParentSoftwareExternalData } from "../../../ports/GetSoftwareExternalData"; +import { Software } from "../../../usecases/readWriteSillData"; +import { Database } from "./kysely.database"; +import { convertNullValuesToUndefined, jsonBuildObject } from "./kysely.utils"; + +export const createPgSoftwareRepository = (db: Kysely): SoftwareRepository => ({ + create: async ({ formData, externalDataOrigin, agentEmail }) => { + const { + softwareName, + softwareDescription, + softwareLicense, + softwareLogoUrl, + softwareMinimalVersion, + isPresentInSupportContract, + isFromFrenchPublicService, + doRespectRgaa, + similarSoftwareExternalDataIds, + softwareType, + externalId, + comptoirDuLibreId, + softwareKeywords, + ...rest + } = formData; + + assert>(); + + const now = Date.now(); + + await db.transaction().execute(async trx => { + const { softwareId } = await trx + .insertInto("softwares") + .values({ + name: softwareName, + description: softwareDescription, + license: softwareLicense, + logoUrl: softwareLogoUrl, + versionMin: softwareMinimalVersion, + referencedSinceTime: now, + updateTime: now, + dereferencing: undefined, + isStillInObservation: false, + parentSoftwareWikidataId: undefined, + doRespectRgaa: doRespectRgaa, + isFromFrenchPublicService: isFromFrenchPublicService, + isPresentInSupportContract: isPresentInSupportContract, + externalId: externalId, + externalDataOrigin: externalDataOrigin, + comptoirDuLibreId: comptoirDuLibreId, + softwareType: JSON.stringify(softwareType), + catalogNumeriqueGouvFrId: undefined, + workshopUrls: JSON.stringify([]), + testUrls: JSON.stringify([]), + categories: JSON.stringify([]), + generalInfoMd: undefined, + addedByAgentEmail: agentEmail, + keywords: JSON.stringify(softwareKeywords) + }) + .returning("id as softwareId") + .executeTakeFirstOrThrow(); + + await trx + .insertInto("softwares__similar_software_external_datas") + .values(similarSoftwareExternalDataIds.map(similarExternalId => ({ softwareId, similarExternalId }))) + .execute(); + }); + }, + update: async ({ formData, softwareSillId, agentEmail }) => { + const { + softwareName, + softwareDescription, + softwareLicense, + softwareLogoUrl, + softwareMinimalVersion, + isPresentInSupportContract, + isFromFrenchPublicService, + doRespectRgaa, + similarSoftwareExternalDataIds, + softwareType, + externalId, + comptoirDuLibreId, + softwareKeywords, + ...rest + } = formData; + + assert>(); + + const now = Date.now(); + await db + .updateTable("softwares") + .set({ + name: softwareName, + description: softwareDescription, + license: softwareLicense, + logoUrl: softwareLogoUrl, + versionMin: softwareMinimalVersion, + updateTime: now, + isStillInObservation: false, + parentSoftwareWikidataId: undefined, + doRespectRgaa: doRespectRgaa, + isFromFrenchPublicService: isFromFrenchPublicService, + isPresentInSupportContract: isPresentInSupportContract, + externalId: externalId, + comptoirDuLibreId: comptoirDuLibreId, + softwareType: JSON.stringify(softwareType), + catalogNumeriqueGouvFrId: undefined, + workshopUrls: JSON.stringify([]), + testUrls: JSON.stringify([]), + categories: JSON.stringify([]), + generalInfoMd: undefined, + addedByAgentEmail: agentEmail, + keywords: JSON.stringify(softwareKeywords) + }) + .where("id", "=", softwareSillId) + .execute(); + }, + getAll: (): Promise => + db + .selectFrom("softwares as s") + .leftJoin("software_external_datas as ext", "ext.externalId", "s.externalId") + .leftJoin("compiled_softwares as cs", "cs.softwareId", "s.id") + .leftJoin("software_external_datas as parentExt", "s.parentSoftwareWikidataId", "parentExt.externalId") + .leftJoin( + "softwares__similar_software_external_datas", + "softwares__similar_software_external_datas.softwareId", + "s.id" + ) + .leftJoin( + "software_external_datas as similarExt", + "softwares__similar_software_external_datas.similarExternalId", + "similarExt.externalId" + ) + .groupBy([ + "s.id", + "cs.softwareId", + "cs.annuaireCnllServiceProviders", + "cs.comptoirDuLibreSoftware", + "cs.latestVersion", + "cs.serviceProviders", + "ext.externalId", + "parentExt.externalId" + ]) + .select([ + "s.id as softwareId", + "s.logoUrl", + "s.name as softwareName", + "s.description as softwareDescription", + "cs.serviceProviders", + "cs.latestVersion", + "s.testUrls", + "s.referencedSinceTime as addedTime", + "s.updateTime", + "s.dereferencing", + "s.categories", + ({ ref }) => + jsonBuildObject({ + isPresentInSupportContract: ref("isPresentInSupportContract"), + isFromFrenchPublicServices: ref("isFromFrenchPublicService"), + doRespectRgaa: ref("doRespectRgaa") + }).as("prerogatives"), + "s.comptoirDuLibreId", + "cs.comptoirDuLibreSoftware", + "s.versionMin", + "s.license", + "annuaireCnllServiceProviders", + "s.externalId", + "s.externalDataOrigin", + "s.softwareType", + ({ ref, ...qb }) => + qb + .case() + .when("parentExt.externalId", "is not", null) + .then( + jsonBuildObject({ + externalId: ref("ext.externalId"), + label: ref("ext.label"), + description: ref("ext.description") + }).$castTo() + ) + .else(null) + .end() + .as("parentExternalData"), + "s.keywords", + + ({ ref }) => + jsonBuildObject({ + externalId: ref("ext.externalId"), + externalDataOrigin: ref("ext.externalDataOrigin"), + developers: ref("ext.developers"), + label: ref("ext.label"), + description: ref("ext.description"), + isLibreSoftware: ref("ext.isLibreSoftware"), + logoUrl: ref("ext.logoUrl"), + framaLibreId: ref("ext.framaLibreId"), + websiteUrl: ref("ext.websiteUrl"), + sourceUrl: ref("ext.sourceUrl"), + documentationUrl: ref("ext.documentationUrl") + }).as("softwareExternalData"), + ({ ref, fn }) => + fn + .coalesce( + fn + .jsonAgg( + jsonBuildObject({ + isInSill: sql`false`, + externalId: ref("similarExt.externalId"), + label: ref("similarExt.label"), + description: ref("similarExt.description"), + isLibreSoftware: ref("similarExt.isLibreSoftware"), + externalDataOrigin: ref("similarExt.externalDataOrigin") + }).$castTo() + ) + .filterWhere("similarExt.externalId", "is not", null), + sql<[]>`'[]'` + ) + .as("similarExternalSoftwares") + ]) + .execute() + .then(softwares => + softwares.map( + ({ + testUrls, + serviceProviders, + parentExternalData, + updateTime, + addedTime, + softwareExternalData, + similarExternalSoftwares, + ...software + }): Software => { + return { + ...convertNullValuesToUndefined(software), + updateTime: new Date(+updateTime).getTime(), + addedTime: new Date(+addedTime).getTime(), + serviceProviders: serviceProviders ?? [], + similarSoftwares: similarExternalSoftwares, + // (similarSoftwares ?? []).map( + // (s): SimilarSoftware => ({ + // softwareName: + // typeof s.label === "string" ? s.label : Object.values(s.label)[0]!, + // softwareDescription: + // typeof s.label === "string" ? s.label : Object.values(s.label)[0]!, + // isInSill: true // TODO: check if this is true + // }) + // ) ?? [], + userAndReferentCountByOrganization: {}, + authors: (softwareExternalData?.developers ?? []).map(dev => ({ + authorName: dev.name, + authorUrl: `https://www.wikidata.org/wiki/${dev.id}` + })), + officialWebsiteUrl: + softwareExternalData?.websiteUrl ?? + software.comptoirDuLibreSoftware?.external_resources.website ?? + undefined, + codeRepositoryUrl: + softwareExternalData?.sourceUrl ?? + software.comptoirDuLibreSoftware?.external_resources.repository ?? + undefined, + documentationUrl: softwareExternalData?.documentationUrl ?? undefined, + comptoirDuLibreServiceProviderCount: + software.comptoirDuLibreSoftware?.providers.length ?? 0, + testUrl: testUrls[0]?.url, + parentWikidataSoftware: parentExternalData ?? undefined + }; + } + ) + ), + getAllSillSoftwareExternalIds: async externalDataOrigin => + db + .selectFrom("softwares") + .select("externalId") + .where("externalDataOrigin", "=", externalDataOrigin) + .execute() + .then(rows => rows.map(row => row.externalId!)), + unreference: async () => {} +}); diff --git a/api/src/core/adapters/dbApi/kysely/createPgUserAndReferentRepository.ts b/api/src/core/adapters/dbApi/kysely/createPgUserAndReferentRepository.ts new file mode 100644 index 00000000..e9d8eb96 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/createPgUserAndReferentRepository.ts @@ -0,0 +1,36 @@ +import { Kysely } from "kysely"; +import { SoftwareReferentRepository, SoftwareUserRepository } from "../../../ports/DbApiV2"; +import { Database } from "./kysely.database"; + +export const createPgUserRepository = (db: Kysely): SoftwareUserRepository => ({ + add: async user => { + await db.insertInto("software_users").values(user).execute(); + }, + remove: async ({ softwareId, agentId }) => { + await db + .deleteFrom("software_users") + .where("softwareId", "=", softwareId) + .where("agentId", "=", agentId) + .execute(); + } +}); + +export const createPgReferentRepository = (db: Kysely): SoftwareReferentRepository => ({ + add: async referent => { + await db.insertInto("software_referents").values(referent).execute(); + }, + remove: async ({ softwareId, agentId }) => { + await db + .deleteFrom("software_referents") + .where("softwareId", "=", softwareId) + .where("agentId", "=", agentId) + .execute(); + }, + getTotalCount: async () => { + const { total_referents } = await db + .selectFrom("software_referents") + .select(qb => qb.fn.countAll().as("total_referents")) + .executeTakeFirstOrThrow(); + return parseInt(total_referents); + } +}); diff --git a/api/src/core/adapters/dbApi/kysely/kysely.database.ts b/api/src/core/adapters/dbApi/kysely/kysely.database.ts new file mode 100644 index 00000000..fd490947 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/kysely.database.ts @@ -0,0 +1,190 @@ +import { Generated, JSONColumnType } from "kysely"; + +export type Database = { + agents: AgentsTable; + software_referents: SoftwareReferentsTable; + software_users: SoftwareUsersTable; + instances: InstancesTable; + softwares: SoftwaresTable; + software_external_datas: SoftwareExternalDatasTable; + softwares__similar_software_external_datas: SimilarExternalSoftwareExternalDataTable; + compiled_softwares: CompiledSoftwaresTable; +}; + +type AgentsTable = { + id: Generated; + email: string; + organization: string; + about: string | null; + isPublic: boolean; +}; + +type Os = "windows" | "linux" | "mac" | "android" | "ios"; + +type SoftwareUsersTable = { + softwareId: number; + agentId: number; + useCaseDescription: string; + os: Os | null; + version: string; + serviceUrl: string | null; +}; + +type SoftwareReferentsTable = { + softwareId: number; + agentId: number; + isExpert: boolean; + useCaseDescription: string; + serviceUrl: string | null; +}; + +type InstancesTable = { + id: Generated; + mainSoftwareSillId: number; + organization: string; + targetAudience: string; + publicUrl: string | null; + addedByAgentEmail: string; + referencedSinceTime: number; + updateTime: number; +}; + +type ExternalId = string; +type ExternalDataOrigin = "wikidata" | "HAL"; +type LocalizedString = Partial>; + +type SimilarExternalSoftwareExternalDataTable = { + softwareId: number; + similarExternalId: ExternalId; +}; + +type SoftwareExternalDatasTable = { + externalId: ExternalId; + externalDataOrigin: ExternalDataOrigin; + developers: JSONColumnType< + { + name: string; + id: string; + }[] + >; + label: string | JSONColumnType; + description: string | JSONColumnType; + isLibreSoftware: boolean; + logoUrl: string | null; + framaLibreId: string | null; + websiteUrl: string | null; + sourceUrl: string | null; + documentationUrl: string | null; + license: string | null; +}; + +type SoftwareType = + | { type: "cloud" } + | { type: "stack" } + | { + type: "desktop/mobile"; + os: Record; + }; + +type SoftwaresTable = { + id: Generated; + name: string; + description: string; + referencedSinceTime: number; + updateTime: number; + dereferencing: JSONColumnType<{ + reason?: string; + time: number; + lastRecommendedVersion?: string; + }> | null; + isStillInObservation: boolean; + doRespectRgaa: boolean | null; + isFromFrenchPublicService: boolean; + isPresentInSupportContract: boolean; + parentSoftwareWikidataId: string | null; + externalId: string | null; + externalDataOrigin: "wikidata" | "HAL" | null; + comptoirDuLibreId: number | null; + license: string; + softwareType: JSONColumnType; + catalogNumeriqueGouvFrId: string | null; + versionMin: string; + workshopUrls: JSONColumnType; + testUrls: JSONColumnType< + { + description: string; + url: string; + }[] + >; + categories: JSONColumnType; + generalInfoMd: string | null; + addedByAgentEmail: string; + logoUrl: string | null; + keywords: JSONColumnType; +}; + +// ---------- compiled data ---------- + +export namespace PgComptoirDuLibre { + type Provider = { + 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; + }; + }; + + 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[]; + }; +} + +type ServiceProvider = { + name: string; + website?: string; + cdlUrl?: string; + cnllUrl?: string; + siren?: string; +}; + +type CompiledSoftwaresTable = { + softwareId: number; + serviceProviders: JSONColumnType; + comptoirDuLibreSoftware: JSONColumnType | null; + annuaireCnllServiceProviders: JSONColumnType< + { + name: string; + siren: string; + url: string; + }[] + > | null; + latestVersion: JSONColumnType<{ + semVer: string; + publicationTime: number; + }> | null; +}; diff --git a/api/src/core/adapters/dbApi/kysely/kysely.dialect.ts b/api/src/core/adapters/dbApi/kysely/kysely.dialect.ts new file mode 100644 index 00000000..587ef12e --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/kysely.dialect.ts @@ -0,0 +1,9 @@ +import { PostgresDialect } from "kysely"; +import { Pool } from "pg"; + +export const createPgDialect = (connectionString: string) => + new PostgresDialect({ + pool: new Pool({ + connectionString + }) + }); 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..a7f5f4c1 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/kysely.utils.ts @@ -0,0 +1,34 @@ +import { Expression, FunctionModule, RawBuilder, SelectExpression, 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`'[]'`); + +export const castSql = ( + expression: SelectExpression, + type: "int" | "text" | "bool" | "uuid" +): SelectExpression => sql`CAST(${expression} AS ${sql.raw(type)})` as any; + +export const isNotNull = (value: T | null): value is T => value !== null; + +export const convertNullValuesToUndefined = >( + obj: T +): { [K in keyof T]: null extends T[K] ? Exclude | undefined : T[K] } => + Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? undefined : value])) as any; diff --git a/api/src/core/adapters/dbApi/kysely/migrations/1717162141365_create-initial-tables.ts b/api/src/core/adapters/dbApi/kysely/migrations/1717162141365_create-initial-tables.ts new file mode 100644 index 00000000..f2508b46 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/migrations/1717162141365_create-initial-tables.ts @@ -0,0 +1,109 @@ +import { Kysely, sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await db.schema.createType("external_data_origin_type").asEnum(["wikidata", "HAL"]).execute(); + await db.schema + .createTable("agents") + .addColumn("id", "serial", col => col.primaryKey()) + .addColumn("email", "text", col => col.notNull()) + .addColumn("organization", "text", col => col.notNull()) + .addColumn("about", "text") + .addColumn("isPublic", "boolean", col => col.notNull()) + .execute(); + + await db.schema + .createTable("softwares") + // from form + .addColumn("id", "serial", col => col.primaryKey()) + .addColumn("softwareType", "jsonb", col => col.notNull()) + .addColumn("externalId", "text") + .addColumn("externalDataOrigin", sql`external_data_origin_type`) + .addColumn("comptoirDuLibreId", "integer") + .addColumn("name", "text", col => col.notNull()) + .addColumn("description", "text", col => col.notNull()) + .addColumn("license", "text", col => col.notNull()) + .addColumn("versionMin", "text", col => col.notNull()) + .addColumn("isPresentInSupportContract", "boolean", col => col.notNull()) + .addColumn("isFromFrenchPublicService", "boolean", col => col.notNull()) + .addColumn("logoUrl", "text") + .addColumn("keywords", "jsonb", col => col.notNull()) + .addColumn("doRespectRgaa", "boolean") + // from ??? + .addColumn("isStillInObservation", "boolean", col => col.notNull()) + .addColumn("parentSoftwareWikidataId", "text") + .addColumn("catalogNumeriqueGouvFrId", "text") + .addColumn("workshopUrls", "jsonb", col => col.notNull()) + .addColumn("testUrls", "jsonb", col => col.notNull()) + .addColumn("categories", "jsonb", col => col.notNull()) + .addColumn("generalInfoMd", "text") + .addColumn("addedByAgentEmail", "text", col => col.notNull()) + .addColumn("dereferencing", "jsonb") + .addColumn("referencedSinceTime", "bigint", col => col.notNull()) + .addColumn("updateTime", "bigint", col => col.notNull()) + .execute(); + + await db.schema + .createTable("software_external_datas") + .addColumn("externalId", "text", col => col.primaryKey()) + .addColumn("externalDataOrigin", sql`external_data_origin_type`, col => col.notNull()) + .addColumn("developers", "jsonb", col => col.notNull()) + .addColumn("label", "jsonb", col => col.notNull()) + .addColumn("description", "jsonb", col => col.notNull()) + .addColumn("isLibreSoftware", "boolean", col => col.notNull()) + .addColumn("logoUrl", "text") + .addColumn("framaLibreId", "text") + .addColumn("websiteUrl", "text") + .addColumn("sourceUrl", "text") + .addColumn("documentationUrl", "text") + .addColumn("license", "text") + .execute(); + + await db.schema + .createTable("softwares__similar_software_external_datas") + .addColumn("softwareId", "integer", col => col.notNull().references("softwares.id").onDelete("cascade")) + .addColumn("similarExternalId", "text", col => col.notNull()) + .addUniqueConstraint("uniq_software_id__similar_software_external_id", ["softwareId", "similarExternalId"]) + .execute(); + + await db.schema + .createTable("software_users") + .addColumn("softwareId", "integer", col => col.notNull().references("softwares.id").onDelete("cascade")) + .addColumn("agentId", "integer", col => col.notNull().references("agents.id").onDelete("cascade")) + .addColumn("useCaseDescription", "text", col => col.notNull()) + .addColumn("os", "text") + .addColumn("version", "text", col => col.notNull()) + .addColumn("serviceUrl", "text") + .execute(); + + await db.schema + .createTable("software_referents") + .addColumn("softwareId", "integer", col => col.notNull().references("softwares.id").onDelete("cascade")) + .addColumn("agentId", "integer", col => col.notNull().references("agents.id").onDelete("cascade")) + .addColumn("useCaseDescription", "text", col => col.notNull()) + .addColumn("isExpert", "boolean", col => col.notNull()) + .addColumn("serviceUrl", "text") + .execute(); + + await db.schema + .createTable("instances") + .addColumn("id", "serial", col => col.primaryKey()) + .addColumn("mainSoftwareSillId", "integer", col => col.notNull().references("softwares.id").onDelete("cascade")) + .addColumn("addedByAgentEmail", "text", col => col.notNull()) + .addColumn("organization", "text", col => col.notNull()) + .addColumn("targetAudience", "text", col => col.notNull()) + .addColumn("publicUrl", "text") + .addColumn("referencedSinceTime", "bigint", col => col.notNull()) + .addColumn("updateTime", "bigint", col => col.notNull()) + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable("softwares__similar_software_external_datas").execute(); + await db.schema.dropTable("software_external_datas").execute(); + await db.schema.dropTable("instances").execute(); + await db.schema.dropTable("software_referents").execute(); + await db.schema.dropTable("software_users").execute(); + await db.schema.dropTable("softwares").execute(); + await db.schema.dropTable("agents").execute(); + await db.schema.dropType("external_data_origin_type").execute(); +} diff --git a/api/src/core/adapters/dbApi/kysely/migrations/1719576701920_add-compiled-tables.ts b/api/src/core/adapters/dbApi/kysely/migrations/1719576701920_add-compiled-tables.ts new file mode 100644 index 00000000..14eb2765 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/migrations/1719576701920_add-compiled-tables.ts @@ -0,0 +1,16 @@ +import type { Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable("compiled_softwares") + .addColumn("softwareId", "integer", col => col.notNull().references("softwares.id").onDelete("cascade")) + .addColumn("serviceProviders", "jsonb", col => col.notNull()) + .addColumn("comptoirDuLibreSoftware", "jsonb") + .addColumn("annuaireCnllServiceProviders", "jsonb") + .addColumn("latestVersion", "jsonb") + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable("compiled_softwares").execute(); +} diff --git a/api/src/core/adapters/dbApi/kysely/migrations/1720187269028_add-indexes.ts b/api/src/core/adapters/dbApi/kysely/migrations/1720187269028_add-indexes.ts new file mode 100644 index 00000000..8c6e51ff --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/migrations/1720187269028_add-indexes.ts @@ -0,0 +1,54 @@ +import type { Kysely } from "kysely"; + +const softwares_externalIdIdx = "softwares__externalId_idx"; +const softwares_parentExternalIdIdx = "softwares__parentExternalId_idx"; +const compiledSoftwares_SoftwareIdIdx = "compiled_softwares__softwareId_idx"; +const software_similarExternalIdIdx = "softwares_similarExternalId_idx"; +const software_softwareIdIdx = "softwares_similarSoftwareId_idx"; +const softwareReferents_softwareIdIdx = "softwareReferents_software_idx"; +const softwareUsers_softwareIdIdx = "softwareUsers_software_idx"; +const instances_mainSoftwareSillIdIdx = "instances_mainSoftwareSillId_idx"; + +export async function up(db: Kysely): Promise { + await db.schema.createIndex(softwares_externalIdIdx).on("softwares").column("externalId").execute(); + await db.schema + .createIndex(softwares_parentExternalIdIdx) + .on("softwares") + .column("parentSoftwareWikidataId") + .execute(); + + await db.schema + .createIndex(software_similarExternalIdIdx) + .on("softwares__similar_software_external_datas") + .column("similarExternalId") + .execute(); + await db.schema + .createIndex(software_softwareIdIdx) + .on("softwares__similar_software_external_datas") + .column("softwareId") + .execute(); + + await db.schema + .createIndex(compiledSoftwares_SoftwareIdIdx) + .on("compiled_softwares") + .column("softwareId") + .execute(); + await db.schema + .createIndex(softwareReferents_softwareIdIdx) + .on("software_referents") + .column("softwareId") + .execute(); + await db.schema.createIndex(softwareUsers_softwareIdIdx).on("software_users").column("softwareId").execute(); + await db.schema.createIndex(instances_mainSoftwareSillIdIdx).on("instances").column("mainSoftwareSillId").execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex(softwares_parentExternalIdIdx).execute(); + await db.schema.dropIndex(softwares_externalIdIdx).execute(); + await db.schema.dropIndex(software_similarExternalIdIdx).execute(); + await db.schema.dropIndex(software_softwareIdIdx).execute(); + await db.schema.dropIndex(compiledSoftwares_SoftwareIdIdx).execute(); + await db.schema.dropIndex(softwareReferents_softwareIdIdx).execute(); + await db.schema.dropIndex(softwareUsers_softwareIdIdx).execute(); + await db.schema.dropIndex(instances_mainSoftwareSillIdIdx).execute(); +} diff --git a/api/src/core/adapters/dbApi/kysely/pgDbApi.integration.test.ts b/api/src/core/adapters/dbApi/kysely/pgDbApi.integration.test.ts new file mode 100644 index 00000000..3f73e313 --- /dev/null +++ b/api/src/core/adapters/dbApi/kysely/pgDbApi.integration.test.ts @@ -0,0 +1,356 @@ +import { Kysely } from "kysely"; +import { beforeEach, describe, expect, it, afterEach } from "vitest"; +import { expectPromiseToFailWith, expectToEqual } from "../../../../tools/test.helpers"; +import { Agent, DbApiV2 } from "../../../ports/DbApiV2"; +import { SoftwareExternalData } from "../../../ports/GetSoftwareExternalData"; +import { SoftwareFormData } from "../../../usecases/readWriteSillData"; +import { createKyselyPgDbApi } from "./createPgDbApi"; +import { Database } from "./kysely.database"; +import { createPgDialect } from "./kysely.dialect"; + +const agent = { + id: 1, + email: "test@test.com", + organization: "test-orga" +}; +const externalId = "external-id-111"; +const similarExternalId = "external-id-222"; +const softwareFormData: SoftwareFormData = { + comptoirDuLibreId: 50, + doRespectRgaa: true, + externalId, + isFromFrenchPublicService: false, + isPresentInSupportContract: true, + similarSoftwareExternalDataIds: [similarExternalId], + softwareDescription: "Super software", + softwareKeywords: ["bob", "l'éponge"], + softwareLicense: "MIT", + softwareLogoUrl: "https://example.com/logo.png", + softwareMinimalVersion: "", + softwareName: "", + softwareType: { + type: "desktop/mobile", + os: { + ios: true, + android: true, + mac: true, + linux: false, + windows: true + } + } +}; + +const softwareExternalData: SoftwareExternalData = { + externalId, + externalDataOrigin: "wikidata", + developers: [{ name: "Bob", id: "bob" }], + label: { en: "Some software" }, + description: { en: "Some software description" }, + isLibreSoftware: true, + logoUrl: "https://example.com/logo.png", + framaLibreId: "", + websiteUrl: "https://example.com", + sourceUrl: "https://example.com/source", + documentationUrl: "https://example.com/documentation", + license: "MIT" +}; + +const similarSoftwareExternalData: SoftwareExternalData = { + externalId: similarExternalId, + externalDataOrigin: "wikidata", + developers: [{ name: "Bobby", id: "similar-bob" }], + label: "Some similar software", + description: { en: "Some similar software description" }, + isLibreSoftware: true, + logoUrl: "https://example.com/similar-logo.png", + framaLibreId: "", + websiteUrl: "https://example.similar.com", + sourceUrl: "https://example.similar.com/source", + documentationUrl: "https://example.similar.com/documentation", + license: "MIT" +}; + +const db = new Kysely({ dialect: createPgDialect("postgresql://sill:pg_password@localhost:5432/sill") }); + +describe("pgDbApi", () => { + let dbApi: DbApiV2; + + beforeEach(async () => { + dbApi = createKyselyPgDbApi(db); + await db.deleteFrom("software_referents").execute(); + await db.deleteFrom("software_users").execute(); + await db.deleteFrom("softwares").execute(); + await db.deleteFrom("software_external_datas").execute(); + await db.deleteFrom("instances").execute(); + await db.deleteFrom("agents").execute(); + }); + + afterEach(() => { + console.log("------ END OF TEST ------"); + }); + + describe("getCompiledDataPrivate", () => { + it("gets private compiled data", async () => { + // const compiledDataPrivate = await dbApi.getCompiledDataPrivate(); + // console.log("compiledDataPrivate.length : ", compiledDataPrivate.length); + // // write softwares to file + // const publicCompiledData = compiledDataPrivateToPublic(compiledDataPrivate); + // publicCompiledData.sort((a, b) => (a.id >= b.id ? 1 : -1)); + // const data = JSON.stringify(publicCompiledData, null, 2); + // fs.writeFileSync("./my-ordered-from-db.json", data); + // + // console.log("publicCompiledData", JSON.stringify(publicCompiledData, null, 2)); + // + // const { users, referents, instances, ...firstSoftware } = compiledDataPrivate.find(s => s.id === 42)!; + // console.log(firstSoftware); + // // + // console.log(`Users n = ${users?.length} : `, users); + // console.log(`Referents n = ${referents?.length} : `, referents); + // console.log(`Instances n = ${instances?.length} : `, instances); + // expect(compiledDataPrivate).toHaveLength(100); + }); + }); + + describe("software", () => { + it("creates a software, than gets it with getAll", async () => { + console.log("------ software scenario ------"); + await insertSoftwareExternalDataAndSoftware(); + + const softwares = await dbApi.software.getAll(); + + expectToEqual(softwares[0], { + addedTime: expect.any(Number), + updateTime: expect.any(Number), + annuaireCnllServiceProviders: undefined, + authors: softwareExternalData.developers.map(dev => ({ + authorName: dev.name, + authorUrl: `https://www.wikidata.org/wiki/${dev.id}` + })), + categories: [], + codeRepositoryUrl: softwareExternalData.sourceUrl, + comptoirDuLibreId: 50, + comptoirDuLibreServiceProviderCount: 0, + dereferencing: undefined, + documentationUrl: softwareExternalData.documentationUrl, + externalDataOrigin: "wikidata", + externalId, + keywords: ["bob", "l'éponge"], + latestVersion: undefined, + license: "MIT", + logoUrl: "https://example.com/logo.png", + officialWebsiteUrl: softwareExternalData.websiteUrl, + parentWikidataSoftware: undefined, + prerogatives: { + doRespectRgaa: true, + isFromFrenchPublicServices: false, + isPresentInSupportContract: true + }, + serviceProviders: [], + similarSoftwares: [ + { + externalDataOrigin: "wikidata", + externalId: similarSoftwareExternalData.externalId, + label: similarSoftwareExternalData.label, + description: similarSoftwareExternalData.description, + isLibreSoftware: similarSoftwareExternalData.isLibreSoftware, + isInSill: false + } + ], + softwareDescription: "Super software", + softwareId: expect.any(Number), + softwareName: softwareFormData.softwareName, + softwareType: { + os: { + android: true, + ios: true, + linux: false, + mac: true, + windows: true + }, + type: "desktop/mobile" + }, + testUrl: undefined, + userAndReferentCountByOrganization: {}, + versionMin: "" + }); + + console.log("getting all sill software external ids"); + const softwareExternalIds = await dbApi.software.getAllSillSoftwareExternalIds("wikidata"); + expectToEqual(softwareExternalIds, [externalId]); + }); + }); + + describe("instance", () => { + it("creates an instance, than gets it with getAll", async () => { + console.log("------ instance scenario ------"); + await insertSoftwareExternalDataAndSoftware(); + const softwares = await dbApi.software.getAll(); + const softwareId = softwares[0].softwareId; + console.log("saving instance"); + await dbApi.instance.create({ + agentEmail: agent.email, + fromData: { + mainSoftwareSillId: softwareId, + organization: "test-orga", + targetAudience: "test-audience", + publicUrl: "https://example.com" + } + }); + + console.log("getting instance"); + const instances = await dbApi.instance.getAll(); + + expectToEqual(instances[0], { + id: expect.any(Number), + mainSoftwareSillId: softwareId, + organization: "test-orga", + targetAudience: "test-audience", + publicUrl: "https://example.com" + }); + }); + }); + + describe("agents", () => { + it("adds an agent, get it by email, updates it, getAll", async () => { + console.log("------ agent scenario------"); + const insertedAgent = { + email: "test@test.com", + organization: "test-organization", + isPublic: true, + about: "test about" + }; + console.log("inserting agent"); + await dbApi.agent.add(insertedAgent); + + console.log("getting agent by email"); + const agent = await dbApi.agent.getByEmail(insertedAgent.email); + expectToEqual(agent, { id: expect.any(Number), ...insertedAgent }); + + const updatedAgent: Agent = { + id: agent!.id, + organization: "updated-test-organization", + about: "updated about", + email: "updated@test.com", + isPublic: !insertedAgent.isPublic + }; + + console.log("updating agent"); + await dbApi.agent.update(updatedAgent); + + console.log("getting all agents"); + const allAgents = await dbApi.agent.getAll(); + expectToEqual(allAgents, [updatedAgent]); + + console.log("removing agent"); + await dbApi.agent.remove(updatedAgent.id); + + console.log("getting all agents after delete"); + const allAgentsAfterDelete = await dbApi.agent.getAll(); + expectToEqual(allAgentsAfterDelete, []); + }); + }); + + describe("users and referents", () => { + let softwareId: number; + let agentId: number; + beforeEach(async () => { + console.log("before -- setting up test with software and agent"); + await insertSoftwareExternalDataAndSoftware(); + + await dbApi.agent.add({ + email: "test@test.com", + organization: "test-organization", + isPublic: true, + about: "test about" + }); + + softwareId = (await dbApi.software.getAll())[0].softwareId; + agentId = (await dbApi.agent.getAll())[0].id; + }); + + it("cannot add a user or referent if the software or agent is missing in db", async () => { + console.log("------ wrong path for user or referent ------"); + await expectPromiseToFailWith( + dbApi.softwareReferent.add({ + agentId, + softwareId: 404, + isExpert: true, + serviceUrl: "https://example.com", + useCaseDescription: "my use case description" + }), + 'insert or update on table "software_referents" violates foreign key constraint "software_referents_softwareId_fkey"' + ); + + await expectPromiseToFailWith( + dbApi.softwareUser.add({ + agentId: 404, + softwareId, + serviceUrl: "https://example.com", + useCaseDescription: "my use case description", + os: "windows", + version: "1.0.0" + }), + 'insert or update on table "software_users" violates foreign key constraint "software_users_agentId_fkey"' + ); + }); + + it("adds the user or referent correctly, than removes them", async () => { + console.log("------ user or referent scenario ------"); + const user = { + agentId, + softwareId, + serviceUrl: "https://example.com", + useCaseDescription: "my use case description", + os: "windows" as const, + version: "1.0.0" + }; + await dbApi.softwareUser.add(user); + + const referent = { + agentId, + softwareId, + serviceUrl: "https://example.com", + useCaseDescription: "my use case description", + isExpert: true + }; + await dbApi.softwareReferent.add(referent); + + const totalReferentCount = await dbApi.softwareReferent.getTotalCount(); + expect(totalReferentCount).toBe(1); + + const referents = await db.selectFrom("software_referents").selectAll().execute(); + expectToEqual(referents, [referent]); + + const users = await db.selectFrom("software_users").selectAll().execute(); + expectToEqual(users, [user]); + + await dbApi.softwareUser.remove({ softwareId, agentId }); + const usersAfterDelete = await db.selectFrom("software_users").selectAll().execute(); + expectToEqual(usersAfterDelete, []); + + await dbApi.softwareReferent.remove({ softwareId, agentId }); + const referentsAfterDelete = await db.selectFrom("software_referents").selectAll().execute(); + expectToEqual(referentsAfterDelete, []); + }); + }); + + const insertSoftwareExternalDataAndSoftware = async () => { + await db + .insertInto("software_external_datas") + .values( + [softwareExternalData, similarSoftwareExternalData].map(softExtData => ({ + ...softExtData, + developers: JSON.stringify(softExtData.developers), + label: JSON.stringify(softExtData.label), + description: JSON.stringify(softExtData.description) + })) + ) + .execute(); + + await dbApi.software.create({ + formData: softwareFormData, + agentEmail: agent.email, + externalDataOrigin: "wikidata" + }); + }; +}); diff --git a/api/src/core/adapters/getWikidataSoftware.ts b/api/src/core/adapters/wikidata/getWikidataSoftware.ts similarity index 99% rename from api/src/core/adapters/getWikidataSoftware.ts rename to api/src/core/adapters/wikidata/getWikidataSoftware.ts index 5f66e318..e76dbc62 100644 --- a/api/src/core/adapters/getWikidataSoftware.ts +++ b/api/src/core/adapters/wikidata/getWikidataSoftware.ts @@ -15,8 +15,8 @@ import { type GetSoftwareExternalData, type SoftwareExternalData, type LocalizedString -} from "../ports/GetSoftwareExternalData"; -import type { Entity, DataValue, LocalizedString as WikiDataLocalizedString } from "../../tools/WikidataEntity"; +} from "../../ports/GetSoftwareExternalData"; +import type { Entity, DataValue, LocalizedString as WikiDataLocalizedString } from "../../../tools/WikidataEntity"; const { resolveLocalizedString } = createResolveLocalizedString({ "currentLanguage": id("en"), "fallbackLanguage": "en" diff --git a/api/src/core/adapters/getWikidataSoftwareOptions.ts b/api/src/core/adapters/wikidata/getWikidataSoftwareOptions.ts similarity index 96% rename from api/src/core/adapters/getWikidataSoftwareOptions.ts rename to api/src/core/adapters/wikidata/getWikidataSoftwareOptions.ts index 8f6cbacb..917a4c77 100644 --- a/api/src/core/adapters/getWikidataSoftwareOptions.ts +++ b/api/src/core/adapters/wikidata/getWikidataSoftwareOptions.ts @@ -1,4 +1,4 @@ -import type { GetSoftwareExternalDataOptions } from "../ports/GetSoftwareExternalDataOptions"; +import type { GetSoftwareExternalDataOptions } from "../../ports/GetSoftwareExternalDataOptions"; import fetch from "node-fetch"; import { freeSoftwareLicensesWikidataIds } from "./getWikidataSoftware"; diff --git a/api/src/core/bootstrap.ts b/api/src/core/bootstrap.ts index 0e40229e..b21133cd 100644 --- a/api/src/core/bootstrap.ts +++ b/api/src/core/bootstrap.ts @@ -6,8 +6,8 @@ import { InMemoryDbApi } from "./adapters/dbApi/InMemoryDbApi"; import { getCnllPrestatairesSill } from "./adapters/getCnllPrestatairesSill"; import { getServiceProviders } from "./adapters/getServiceProviders"; import { createGetSoftwareLatestVersion } from "./adapters/getSoftwareLatestVersion"; -import { getWikidataSoftware } from "./adapters/getWikidataSoftware"; -import { getWikidataSoftwareOptions } from "./adapters/getWikidataSoftwareOptions"; +import { getWikidataSoftware } from "./adapters/wikidata/getWikidataSoftware"; +import { getWikidataSoftwareOptions } from "./adapters/wikidata/getWikidataSoftwareOptions"; import { getHalSoftware } from "./adapters/hal/getHalSoftware"; import { getHalSoftwareOptions } from "./adapters/hal/getHalSoftwareOptions"; import { createKeycloakUserApi, type KeycloakUserApiParams } from "./adapters/userApi"; diff --git a/api/src/core/ports/CompileData.ts b/api/src/core/ports/CompileData.ts index 1e299409..7f765d75 100644 --- a/api/src/core/ports/CompileData.ts +++ b/api/src/core/ports/CompileData.ts @@ -1,6 +1,10 @@ import { ServiceProvider } from "../usecases/readWriteSillData"; import type { Db } from "./DbApi"; -import type { SoftwareExternalData } from "./GetSoftwareExternalData"; +import { + ParentSoftwareExternalData, + SimilarSoftwareExternalData, + SoftwareExternalData +} from "./GetSoftwareExternalData"; import type { ComptoirDuLibre } from "./ComptoirDuLibreApi"; export type CompileData = (params: { @@ -28,6 +32,7 @@ export type CompiledData = CompiledData.Software export namespace CompiledData { export type Software = T extends "private" ? Software.Private : Software.Public; + export namespace Software { export type Common = Pick< Db.SoftwareRow, @@ -56,11 +61,8 @@ export namespace CompiledData { > & { serviceProviders: ServiceProvider[]; softwareExternalData: SoftwareExternalData | undefined; - similarExternalSoftwares: Pick< - SoftwareExternalData, - "externalId" | "label" | "description" | "isLibreSoftware" | "externalDataOrigin" - >[]; - parentWikidataSoftware: Pick | undefined; + similarExternalSoftwares: SimilarSoftwareExternalData[]; + parentWikidataSoftware: ParentSoftwareExternalData | undefined; comptoirDuLibreSoftware: | (ComptoirDuLibre.Software & { logoUrl: string | undefined; keywords: string[] | undefined }) | undefined; diff --git a/api/src/core/ports/DbApiV2.ts b/api/src/core/ports/DbApiV2.ts new file mode 100644 index 00000000..1510b14a --- /dev/null +++ b/api/src/core/ports/DbApiV2.ts @@ -0,0 +1,68 @@ +import type { Database } from "../adapters/dbApi/kysely/kysely.database"; +import type { Instance, InstanceFormData, Software, SoftwareFormData } from "../usecases/readWriteSillData"; +import type { OmitFromExisting } from "../utils"; +import type { CompiledData } from "./CompileData"; + +import type { ExternalDataOrigin } from "./GetSoftwareExternalData"; + +type WithAgentEmail = { agentEmail: string }; + +export interface SoftwareRepository { + create: ( + params: { + formData: SoftwareFormData; + externalDataOrigin: ExternalDataOrigin; + } & WithAgentEmail + ) => Promise; + update: ( + params: { + softwareSillId: number; + formData: SoftwareFormData; + } & WithAgentEmail + ) => Promise; + getAll: () => Promise; + getAllSillSoftwareExternalIds: (externalDataOrigin: ExternalDataOrigin) => Promise; + unreference: () => {}; +} + +export interface InstanceRepository { + create: (params: { fromData: InstanceFormData } & WithAgentEmail) => Promise; + update: (params: { fromData: InstanceFormData; instanceId: number }) => Promise; + getAll: () => Promise; +} + +export type Agent = { + id: number; + email: string; + organization: string; + about: string | undefined; + isPublic: boolean; +}; + +export interface AgentRepository { + add: (agent: OmitFromExisting) => Promise; + update: (agent: Agent) => Promise; + remove: (agentId: number) => Promise; + getByEmail: (email: string) => Promise; + getAll: () => Promise; +} + +export interface SoftwareReferentRepository { + add: (params: Database["software_referents"]) => Promise; + remove: (params: { softwareId: number; agentId: number }) => Promise; + getTotalCount: () => Promise; +} + +export interface SoftwareUserRepository { + add: (params: Database["software_users"]) => Promise; + remove: (params: { softwareId: number; agentId: number }) => Promise; +} + +export type DbApiV2 = { + software: SoftwareRepository; + instance: InstanceRepository; + agent: AgentRepository; + softwareReferent: SoftwareReferentRepository; + softwareUser: SoftwareUserRepository; + getCompiledDataPrivate: () => Promise>; +}; diff --git a/api/src/core/ports/GetSoftwareExternalData.ts b/api/src/core/ports/GetSoftwareExternalData.ts index f31680e9..26f57124 100644 --- a/api/src/core/ports/GetSoftwareExternalData.ts +++ b/api/src/core/ports/GetSoftwareExternalData.ts @@ -31,6 +31,13 @@ export type SoftwareExternalData = { license: string; }>; +export type SimilarSoftwareExternalData = Pick< + SoftwareExternalData, + "externalId" | "label" | "description" | "isLibreSoftware" | "externalDataOrigin" +>; + +export type ParentSoftwareExternalData = Pick; + export const languages = ["fr", "en"] as const; export type Language = (typeof languages)[number]; diff --git a/api/src/core/usecases/readWriteSillData/selectors.ts b/api/src/core/usecases/readWriteSillData/selectors.ts index 21fca1c7..d5a705ba 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/core/usecases/readWriteSillData/thunks.ts b/api/src/core/usecases/readWriteSillData/thunks.ts index 9b648841..2f3b117f 100644 --- a/api/src/core/usecases/readWriteSillData/thunks.ts +++ b/api/src/core/usecases/readWriteSillData/thunks.ts @@ -139,8 +139,8 @@ export const thunks = { "comptoirDuLibreId": formData.comptoirDuLibreId, "license": formData.softwareLicense, "softwareType": formData.softwareType, - "catalogNumeriqueGouvFrId": undefined, "versionMin": formData.softwareMinimalVersion, + "catalogNumeriqueGouvFrId": undefined, "workshopUrls": [], "testUrls": [], "categories": [], diff --git a/api/src/core/usecases/readWriteSillData/types.ts b/api/src/core/usecases/readWriteSillData/types.ts index a5ca9f91..d0b91bc5 100644 --- a/api/src/core/usecases/readWriteSillData/types.ts +++ b/api/src/core/usecases/readWriteSillData/types.ts @@ -1,4 +1,8 @@ -import type { ExternalDataOrigin, SoftwareExternalData } from "../../ports/GetSoftwareExternalData"; +import type { + ExternalDataOrigin, + ParentSoftwareExternalData, + SimilarSoftwareExternalData +} from "../../ports/GetSoftwareExternalData"; export type ServiceProvider = { name: string; @@ -54,7 +58,7 @@ export type Software = { externalId: string | undefined; externalDataOrigin: ExternalDataOrigin | undefined; softwareType: SoftwareType; - parentWikidataSoftware: Pick | undefined; + parentWikidataSoftware: ParentSoftwareExternalData | undefined; similarSoftwares: Software.SimilarSoftware[]; keywords: string[]; }; @@ -63,10 +67,7 @@ export namespace Software { export type SimilarSoftware = SimilarSoftware.ExternalSoftwareData | SimilarSoftware.Sill; export namespace SimilarSoftware { - export type ExternalSoftwareData = { isInSill: false } & Pick< - SoftwareExternalData, - "externalId" | "label" | "description" | "isLibreSoftware" | "externalDataOrigin" - >; + export type ExternalSoftwareData = { isInSill: false } & SimilarSoftwareExternalData; export type Sill = { isInSill: true; softwareName: string; softwareDescription: string }; } diff --git a/api/src/core/utils.ts b/api/src/core/utils.ts new file mode 100644 index 00000000..105b6157 --- /dev/null +++ b/api/src/core/utils.ts @@ -0,0 +1 @@ +export type OmitFromExisting = Omit; diff --git a/api/src/lib/index.ts b/api/src/lib/index.ts index edebceed..a5d192ff 100644 --- a/api/src/lib/index.ts +++ b/api/src/lib/index.ts @@ -5,8 +5,10 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; export type TrpcRouterInput = inferRouterInputs; export type TrpcRouterOutput = inferRouterOutputs; -export type * as ApiTypes from "./ApiTypes"; - export { type User, createAccessTokenToUser } from "../rpc/user"; export { type Language, type LocalizedString, languages } from "../core/ports/GetSoftwareExternalData"; export type { ExternalDataOrigin } from "../core/ports/GetSoftwareExternalData"; + +import type * as ApiTypes from "./ApiTypes"; + +export type { ApiTypes }; diff --git a/api/src/rpc/router.ts b/api/src/rpc/router.ts index 4c90e632..9e39b7fa 100644 --- a/api/src/rpc/router.ts +++ b/api/src/rpc/router.ts @@ -161,6 +161,9 @@ export function createRouter(params: { const { formData } = input; + // TODO : there is some logic with logoUrl that should be moved here + // from readWriteSillData/thunks/getStorableLogo + try { await core.functions.readWriteSillData.createSoftware({ formData, diff --git a/api/src/rpc/routes.e2e.test.ts b/api/src/rpc/routes.e2e.test.ts index 53f8c918..d6a8a2e9 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/src/tools/test.helpers.ts b/api/src/tools/test.helpers.ts index 109639e6..cc079f4b 100644 --- a/api/src/tools/test.helpers.ts +++ b/api/src/tools/test.helpers.ts @@ -2,6 +2,10 @@ import { expect } from "vitest"; import { Db } from "../core/ports/DbApi"; import { DeclarationFormData, InstanceFormData, SoftwareFormData } from "../core/usecases/readWriteSillData"; +export const expectPromiseToFailWith = (promise: Promise, errorMessage: string) => { + return expect(promise).rejects.toThrow(errorMessage); +}; + export const expectToEqual = (actual: T, expected: T) => { expect(actual).toEqual(expected); }; diff --git a/api/tsconfig.json b/api/tsconfig.json index a24cbb8d..582496c0 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -17,5 +17,5 @@ "noFallthroughCasesInSwitch": true, "skipLibCheck": true }, - "include": ["src"] + "include": ["src", "scripts"] } diff --git a/docker-compose.resources.yml b/docker-compose.resources.yml new file mode 100644 index 00000000..97d0952e --- /dev/null +++ b/docker-compose.resources.yml @@ -0,0 +1,24 @@ +# this is for local use only + +version: '3.9' +services: + postgres: + image: postgres:16 + shm_size: 256m + ports: + - "5432:5432" + environment: + POSTGRES_LOG_STATEMENTS: all + POSTGRES_DB: sill + POSTGRES_USER: sill + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-pg_password} + volumes: + - ./docker-data/test-postgresql:/var/lib/postgresql/data + +# To create an easy-to-use interface for PostgreSQL administration, +# you can use the Adminer web interface. +# + adminer: + image: adminer + ports: + - "8080:8080" diff --git a/package.json b/package.json index 4892b5a2..4166f5f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sill", - "version": "1.42.45", + "version": "1.42.46", "license": "MIT", "private": true, "scripts": { diff --git a/web/src/ui/shared/Footer.tsx b/web/src/ui/shared/Footer.tsx index b1ec0a8f..04f5e61d 100644 --- a/web/src/ui/shared/Footer.tsx +++ b/web/src/ui/shared/Footer.tsx @@ -33,13 +33,13 @@ export const Footer = memo( { "text": `sill-api: v${apiVersion}`, "linkProps": { - "href": `https://github.com/codegouvfr/sill-api/tree/v${apiVersion}` + "href": `https://github.com/codegouvfr/sill/tree/v${apiVersion}` } }, { "text": `sill-web: v${webVersion}`, "linkProps": { - "href": `https://github.com/codegouvfr/sill-web/tree/v${webVersion}` + "href": `https://github.com/codegouvfr/sill/tree/v${webVersion}` } }, { diff --git a/yarn.lock b/yarn.lock index 66a6532d..9edbf90d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1556,116 +1556,231 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + "@esbuild/android-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + "@esbuild/android-arm@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + "@esbuild/android-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + "@esbuild/darwin-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + "@esbuild/darwin-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + "@esbuild/freebsd-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + "@esbuild/freebsd-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + "@esbuild/linux-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + "@esbuild/linux-arm@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + "@esbuild/linux-ia32@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + "@esbuild/linux-loong64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + "@esbuild/linux-mips64el@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + "@esbuild/linux-ppc64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + "@esbuild/linux-riscv64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + "@esbuild/linux-s390x@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + "@esbuild/linux-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + "@esbuild/netbsd-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + "@esbuild/openbsd-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + "@esbuild/sunos-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + "@esbuild/win32-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + "@esbuild/win32-ia32@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + "@esbuild/win32-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -4028,6 +4143,15 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== +"@types/pg@^8.11.6": + version "8.11.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.6.tgz#a2d0fb0a14b53951a17df5197401569fb9c0c54b" + integrity sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -5954,6 +6078,24 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +c12@^1.8.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/c12/-/c12-1.10.0.tgz#e1936baa26fd03a9427875554aa6aeb86077b7fb" + integrity sha512-0SsG7UDhoRWcuSvKWHaXmu5uNjDCDN3nkQLRL4Q42IlFy+ze58FcCoI3uPwINXinkz7ZinbhEgyzYFw9u9ZV8g== + dependencies: + chokidar "^3.6.0" + confbox "^0.1.3" + defu "^6.1.4" + dotenv "^16.4.5" + giget "^1.2.1" + jiti "^1.21.0" + mlly "^1.6.1" + ohash "^1.1.3" + pathe "^1.1.2" + perfect-debounce "^1.0.0" + pkg-types "^1.0.3" + rc9 "^2.1.1" + c8@^7.6.0: version "7.14.0" resolved "https://registry.yarnpkg.com/c8/-/c8-7.14.0.tgz#f368184c73b125a80565e9ab2396ff0be4d732f3" @@ -6285,7 +6427,7 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.3: +chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -6338,6 +6480,13 @@ circular-dependency-plugin@^5.2.2: resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz#39e836079db1d3cf2f988dc48c5188a44058b600" integrity sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ== +citty@^0.1.4, citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + cjs-module-lexer@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" @@ -6661,6 +6810,11 @@ concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" +confbox@^0.1.3, confbox@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" + integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + configstore@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7" @@ -6683,6 +6837,11 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== +consola@^3.2.0, consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -7359,6 +7518,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -7397,6 +7561,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destr@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.3.tgz#7f9e97cb3d16dbdca7be52aca1644ce402cfe449" + integrity sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -7674,7 +7843,7 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -dotenv@^16.3.0: +dotenv@^16.3.0, dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== @@ -8058,6 +8227,35 @@ esbuild@^0.19.3: "@esbuild/win32-ia32" "0.19.12" "@esbuild/win32-x64" "0.19.12" +esbuild@~0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -9237,11 +9435,32 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" +get-tsconfig@^4.7.5: + version "4.7.5" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.5.tgz#5e012498579e9a6947511ed0cd403272c7acbbaf" + integrity sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw== + dependencies: + resolve-pkg-maps "^1.0.0" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== +giget@^1.2.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.3.tgz#ef6845d1140e89adad595f7f3bb60aa31c672cb6" + integrity sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + defu "^6.1.4" + node-fetch-native "^1.6.3" + nypm "^0.3.8" + ohash "^1.1.3" + pathe "^1.1.2" + tar "^6.2.0" + github-slugger@^1.0.0: version "1.5.0" resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" @@ -11344,6 +11563,11 @@ jiti@^1.19.1: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jiti@^1.21.0: + version "1.21.3" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.3.tgz#b2adb07489d7629b344d59082bbedb8c21c5f755" + integrity sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -11633,6 +11857,26 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +kysely-ctl@^0.8.10: + version "0.8.10" + resolved "https://registry.yarnpkg.com/kysely-ctl/-/kysely-ctl-0.8.10.tgz#799d9df83badbb88c3adc0cd4b0ab949e13376d6" + integrity sha512-uDfYhG2AJwJ1/kVRUvHewHvcvI6F8Aqnoiyx9cOzr2RZ+mip2sm5YngCJi4S/rb4/1fICIL2MuIdAGizVgpbzg== + dependencies: + c12 "^1.8.0" + citty "^0.1.4" + consola "^3.2.0" + nypm "^0.3.1" + ofetch "^1.3.4" + pathe "^1.1.2" + pkg-types "^1.1.0" + std-env "^3.4.0" + tsx "^4.9.0" + +kysely@^0.27.4: + version "0.27.4" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.4.tgz#96a0285467b380948b4de03b20d87e82d797449b" + integrity sha512-dyNKv2KRvYOQPLCAOCjjQuCk4YFd33BvGdf/o5bC7FiW+BB6snA81Zt+2wT9QDFzKqxKa5rrOmvlK/anehCcgA== + language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -12891,6 +13135,16 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.2" +mlly@^1.6.1, mlly@^1.7.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" + integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== + dependencies: + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.1.1" + ufo "^1.5.3" + moment@^2.29.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" @@ -13046,6 +13300,11 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" +node-fetch-native@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" + integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== + node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -13197,6 +13456,17 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== +nypm@^0.3.1, nypm@^0.3.8: + version "0.3.8" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.8.tgz#a16b078b161be5885351e72cf0b97326973722bf" + integrity sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + execa "^8.0.1" + pathe "^1.1.2" + ufo "^1.4.0" + object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -13320,11 +13590,25 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== -obuf@^1.0.0, obuf@^1.1.2: +obuf@^1.0.0, obuf@^1.1.2, obuf@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +ofetch@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.4.tgz#7ea65ced3c592ec2b9906975ae3fe1d26a56f635" + integrity sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw== + dependencies: + destr "^2.0.3" + node-fetch-native "^1.6.3" + ufo "^1.5.3" + +ohash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" + integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== + oidc-client-ts@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz#764c8a33de542026e2798de9849ce8049047d7e5" @@ -13813,11 +14097,90 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-pool@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== + +pg-protocol@*, pg-protocol@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + +pg@^8.11.5: + version "8.12.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" + integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== + dependencies: + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + picocolors@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" @@ -13895,6 +14258,15 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" +pkg-types@^1.1.0, pkg-types@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.1.tgz#07b626880749beb607b0c817af63aac1845a73f2" + integrity sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ== + dependencies: + confbox "^0.1.7" + mlly "^1.7.0" + pathe "^1.1.2" + pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -14546,6 +14918,55 @@ postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4. picocolors "^1.0.0" source-map-js "^1.0.2" +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + powerhooks@^1.0.5: version "1.0.8" resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-1.0.8.tgz#e64e10b7f9cb2f367a3e92da2be8d2373f3d2418" @@ -14886,6 +15307,14 @@ raw-loader@^4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" +rc9@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d" + integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg== + dependencies: + defu "^6.1.4" + destr "^2.0.3" + react-app-polyfill@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" @@ -15657,6 +16086,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve-url-loader@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" @@ -16453,6 +16887,11 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -16548,7 +16987,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.5.0: +std-env@^3.4.0, std-env@^3.5.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== @@ -17042,6 +17481,18 @@ tar@^6.0.2, tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + telejson@^6.0.8: version "6.0.8" resolved "https://registry.yarnpkg.com/telejson/-/telejson-6.0.8.tgz#1c432db7e7a9212c1fbd941c3e5174ec385148f7" @@ -17444,6 +17895,16 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tsx@^4.9.0: + version "4.13.2" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.13.2.tgz#e59c0a0abf955d1c352081352657a3f92c832e36" + integrity sha512-s+WGqChkA77uU8xij1IdO9jQnwJAiWJto0bF5yJLbAZpLtNs82Qa5CwMBxWjJ7QOYU9MzBf4MCNt6lZduwkQ+g== + dependencies: + esbuild "~0.20.2" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -17626,6 +18087,11 @@ ufo@^1.3.2: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32" integrity sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ== +ufo@^1.4.0, ufo@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" + integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== + uglify-js@^3.1.4: version "3.17.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"