Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Commit

Permalink
refactor(api): réecrit les routes
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelBitard committed Sep 17, 2024
1 parent c32d0d1 commit 6584499
Show file tree
Hide file tree
Showing 15 changed files with 222 additions and 122 deletions.
6 changes: 3 additions & 3 deletions packages/api/src/api/rest/titres.test.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { titreCreate, titreUpdate } from '../../database/queries/titres'
import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
import { titreEtapeCreate } from '../../database/queries/titres-etapes'
import { userSuper } from '../../database/user-super'
import { restCall, restDeleteCall, restPostCall } from '../../../tests/_utils/index'
import { restCall, restDeleteCall, restNewCall, restNewPostCall, restPostCall } from '../../../tests/_utils/index'
import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
import { ITitreDemarche, ITitreEtape } from '../../types'
import { entreprisesUpsert } from '../../database/queries/entreprises'
Expand Down Expand Up @@ -212,7 +212,7 @@ describe('titresLiaisons', () => {
{}
)

const tested = await restPostCall(
const tested = await restNewPostCall(
dbPool,
'/rest/titres/:id/titreLiaisons',
{ id: axm.id },
Expand All @@ -231,7 +231,7 @@ describe('titresLiaisons', () => {
nom: getTitres.body[0].nom,
})

const avalTested = await restCall(
const avalTested = await restNewCall(
dbPool,
'/rest/titres/:id/titreLiaisons',
{ id: titreId },
Expand Down
237 changes: 152 additions & 85 deletions packages/api/src/api/rest/titres.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { titreArchive, titresGet, titreGet, titreUpsert } from '../../database/queries/titres'
import { HTTP_STATUS } from 'camino-common/src/http'
import { ITitre } from '../../types'
import { CommonTitreAdministration, editableTitreValidator, TitreLink, TitreLinks, TitreGet, titreLinksValidator, utilisateurTitreAbonneValidator } from 'camino-common/src/titres'
import { CaminoApiError, ITitre } from '../../types'
import { CommonTitreAdministration, editableTitreValidator, TitreLink, TitreLinks, TitreGet, utilisateurTitreAbonneValidator } from 'camino-common/src/titres'
import { machineFind } from '../../business/rules-demarches/definitions'
import { CaminoRequest, CustomResponse } from './express-type'
import { userSuper } from '../../database/user-super'
import { NotNullableKeys, isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, onlyUnique } from 'camino-common/src/typescript-tools'
import { DeepReadonly, NotNullableKeys, isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty, onlyUnique } from 'camino-common/src/typescript-tools'
import TitresTitres from '../../database/models/titres--titres'
import { titreAdministrationsGet } from '../_format/titres'
import { canDeleteTitre, canEditTitre, canLinkTitres } from 'camino-common/src/permissions/titres'
import { linkTitres } from '../../database/queries/titres-titres.queries'
import { checkTitreLinks } from '../../business/validations/titre-links-validate'
import { linkTitres, LinkTitresErrors } from '../../database/queries/titres-titres.queries'
import { checkTitreLinks, CheckTitreLinksError } from '../../business/validations/titre-links-validate'
import { titreEtapeForMachineValidator, toMachineEtapes } from '../../business/rules-demarches/machine-common'
import { TitreReference } from 'camino-common/src/titres-references'
import { DemarchesStatutsIds } from 'camino-common/src/static/demarchesStatuts'
Expand All @@ -22,11 +22,13 @@ import { utilisateurTitreCreate, utilisateurTitreDelete } from '../../database/q
import { titreUpdateTask } from '../../business/titre-update'
import { getDoublonsByTitreId, getTitre as getTitreDb } from './titres.queries'
import type { Pool } from 'pg'
import { z } from 'zod'
import { TitresStatutIds } from 'camino-common/src/static/titresStatuts'
import { getTitreUtilisateur } from '../../database/queries/titres-utilisateurs.queries'
import { titreIdValidator, titreIdOrSlugValidator, TitreId } from 'camino-common/src/validators/titres'
import { callAndExit } from '../../tools/fp-tools'
import { RestNewGetCall, RestNewPostCall } from '../../server/rest'
import { Effect, Match, pipe } from 'effect'
import { CaminoError } from 'camino-common/src/zod-tools'
import { DbQueryAccessError } from '../../pg-database'

const etapesAMasquer = [ETAPES_TYPES.classementSansSuite, ETAPES_TYPES.desistementDuDemandeur, ETAPES_TYPES.decisionImplicite, ETAPES_TYPES.demandeDeComplements_RecevabiliteDeLaDemande_]

Expand Down Expand Up @@ -187,90 +189,155 @@ export const titresAdministrations =
}
}

export const postTitreLiaisons =
(pool: Pool) =>
async (req: CaminoRequest, res: CustomResponse<TitreLinks>): Promise<void> => {
const user = req.auth

const titreId = titreIdValidator.safeParse(req.params.id)
const titreFromIds = z.array(titreIdValidator).safeParse(req.body)

if (!titreFromIds.success || titreFromIds.data.length === 0) {
throw new Error(`un tableau est attendu en corps de message : '${titreFromIds}'`)
}

if (!titreId.success) {
throw new Error('le paramètre id est obligatoire')
}
const linkTitreError = 'Droit insuffisant pour lier des titres entre eux' as const
const demarcheNonChargeesError = 'Les démarches ne sont pas chargées' as const
type PostTitreLiaisonErrors = DbQueryAccessError | typeof droitInsuffisant | typeof linkTitreError | typeof demarcheNonChargeesError | LinkTitresErrors | CheckTitreLinksError

export const postTitreLiaisons: RestNewPostCall<'/rest/titres/:id/titreLiaisons'> = ({
pool,
user,
params: { id: titreId },
body: titreFromIds,
}): Effect.Effect<TitreLinks, CaminoApiError<PostTitreLiaisonErrors>> => {
return Effect.Do.pipe(
Effect.bind('titre', () =>
Effect.tryPromise({
try: async () =>
titreGet(
titreId,
{
fields: {
pointsEtape: { id: {} },
demarches: { id: {} },
},
},
user
),
catch: e => ({ message: "Impossible d'accéder à la base de données" as const, extra: e }),
})
),
Effect.filterOrFail(
(binded): binded is NotNullableKeys<typeof binded> => isNotNullNorUndefined(binded.titre),
() => ({ message: droitInsuffisant })
),
Effect.bind('administrations', ({ titre }) =>
Effect.tryPromise({
try: async () => titreAdministrationsGet(titre),
catch: e => ({ message: "Impossible d'accéder à la base de données" as const, extra: e }),
})
),
Effect.filterOrFail(
({ administrations }) => canLinkTitres(user, administrations),
() => ({ message: linkTitreError })
),
Effect.filterOrFail(
({ titre }) => isNotNullNorUndefined(titre.demarches),
() => ({ message: demarcheNonChargeesError })
),
Effect.bind('titresFrom', () =>
Effect.tryPromise({
try: async () => titresGet({ ids: [...titreFromIds] }, { fields: { id: {} } }, user),
catch: e => ({ message: "Impossible d'accéder à la base de données" as const, extra: e }),
})
),
Effect.bind('unused', ({ titre, titresFrom }) => {
const result = checkTitreLinks(titre.typeId, titreFromIds, titresFrom, titre.demarches ?? [])

if (result.valid) {
return Effect.succeed('')
} else {
console.warn(result.errors)

const titre = await titreGet(
titreId.data,
{
fields: {
pointsEtape: { id: {} },
demarches: { id: {} },
},
},
user
return Effect.fail({ message: result.errors[0], extra: result.errors })
}
}),
Effect.tap(linkTitres(pool, { linkTo: titreId, linkFrom: titreFromIds })),
Effect.bind('amont', () => titreLinksGet(titreId, 'titreFromId', user)),
Effect.bind('aval', () => titreLinksGet(titreId, 'titreToId', user)),
Effect.map(({ amont, aval }) => {
const result: TitreLinks = { amont, aval }

return result
}),
Effect.mapError(caminoError =>
Match.value(caminoError.message).pipe(
Match.whenOr("Impossible d'accéder à la base de données", demarcheNonChargeesError, () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
Match.whenOr('Droit insuffisant pour accéder au titre ou titre inexistant', linkTitreError, 'droits insuffisants ou titre inexistant', () => ({
...caminoError,
status: HTTP_STATUS.FORBIDDEN,
})),
Match.whenOr(
'ce titre peut avoir un seul titre lié',
'ce titre ne peut pas être lié à d’autres titres',
'lien incompatible entre ces types de titre',
'Problème de validation de données',
() => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })
),
Match.exhaustive
)
)
)

if (!titre) throw new Error("le titre n'existe pas")

const administrations = titreAdministrationsGet(titre)
if (!canLinkTitres(user, administrations)) throw new Error('droits insuffisants')

if (!titre.demarches) {
throw new Error('les démarches ne sont pas chargées')
}

const titresFrom = await titresGet({ ids: titreFromIds.data }, { fields: { id: {} } }, user)

const check = checkTitreLinks(titre.typeId, titreFromIds.data, titresFrom, titre.demarches)
if (!check.valid) {
throw new Error(check.errors.join('. '))
}

await callAndExit(linkTitres(pool, { linkTo: titreId.data, linkFrom: titreFromIds.data }), async () => {})

res.json({
amont: await titreLinksGet(titreId.data, 'titreFromId', user),
aval: await titreLinksGet(titreId.data, 'titreToId', user),
})
}
export const getTitreLiaisons =
(_pool: Pool) =>
async (req: CaminoRequest, res: CustomResponse<TitreLinks>): Promise<void> => {
const user = req.auth
// await callAndExit(linkTitres(pool, { linkTo: titreId.data, linkFrom: titreFromIds.data }), async () => {})

const titreId = req.params.id

if (!titreId) {
res.json({ amont: [], aval: [] })
} else {
const titre = await titreGet(titreId, { fields: { id: {} } }, user)
if (!titre) {
res.sendStatus(HTTP_STATUS.FORBIDDEN)
} else {
const value: TitreLinks = {
amont: await titreLinksGet(titreId, 'titreFromId', user),
aval: await titreLinksGet(titreId, 'titreToId', user),
}
res.json(titreLinksValidator.parse(value))
}
}
}

const titreLinksGet = async (titreId: string, link: 'titreToId' | 'titreFromId', user: User): Promise<TitreLink[]> => {
const titresTitres = await TitresTitres.query().where(link === 'titreToId' ? 'titreFromId' : 'titreToId', titreId)
const titreIds = titresTitres.map(r => r[link])
// res.json({
// amont: await titreLinksGet(titreId.data, 'titreFromId', user),
// aval: await titreLinksGet(titreId.data, 'titreToId', user),
// })
}

if (titreIds.length > 0) {
const titres = await titresGet({ ids: titreIds }, { fields: { id: {} } }, user)
const titreLinksGet = (titreId: string, link: 'titreToId' | 'titreFromId', user: DeepReadonly<User>): Effect.Effect<TitreLink[], CaminoError<DbQueryAccessError>> => {
return pipe(
Effect.tryPromise({
try: async () => TitresTitres.query().where(link === 'titreToId' ? 'titreFromId' : 'titreToId', titreId),
catch: e => ({ message: "Impossible d'accéder à la base de données" as const, extra: e }),
}),
Effect.map(titresTitres => titresTitres.map(t => t[link])),
Effect.flatMap(titreIds =>
Effect.tryPromise({
try: async () => {
if (isNullOrUndefinedOrEmpty(titreIds)) {
return []
} else {
return titresGet({ ids: titreIds }, { fields: { id: {} } }, user)
}
},
catch: e => ({ message: "Impossible d'accéder à la base de données" as const, extra: e }),
})
),
Effect.map(titres => titres.map(({ id, nom }) => ({ id, nom })))
)
}

return titres.map(({ id, nom }) => ({ id, nom }))
} else {
return []
}
const droitInsuffisant = 'Droit insuffisant pour accéder au titre ou titre inexistant' as const
type GetTitreLiaisonErrors = DbQueryAccessError | typeof droitInsuffisant
export const getTitreLiaisons: RestNewGetCall<'/rest/titres/:id/titreLiaisons'> = ({ user, params }): Effect.Effect<TitreLinks, CaminoApiError<GetTitreLiaisonErrors>> => {
return Effect.Do.pipe(
Effect.flatMap(() =>
Effect.tryPromise({
try: async () => titreGet(params.id, { fields: { id: {} } }, user),
catch: e => ({ message: "Impossible d'accéder à la base de données" as const, extra: e }),
})
),
Effect.filterOrFail(
titre => isNotNullNorUndefined(titre),
() => ({ message: droitInsuffisant })
),
Effect.bind('amont', () => titreLinksGet(params.id, 'titreFromId', user)),
Effect.bind('aval', () => titreLinksGet(params.id, 'titreToId', user)),
Effect.map(({ amont, aval }) => {
const value: TitreLinks = { amont, aval }

return value
}),
Effect.mapError(caminoError =>
Match.value(caminoError.message).pipe(
Match.when("Impossible d'accéder à la base de données", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
Match.when('Droit insuffisant pour accéder au titre ou titre inexistant', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
Match.exhaustive
)
)
)
}

export const removeTitre =
Expand Down
16 changes: 10 additions & 6 deletions packages/api/src/business/validations/titre-links-validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,34 @@ import { getLinkConfig } from 'camino-common/src/permissions/titres'
import { NonEmptyArray, isNonEmptyArray, isNullOrUndefined } from 'camino-common/src/typescript-tools'
import { TitreTypeId } from 'camino-common/src/static/titresTypes'

const linkImpossible = 'ce titre ne peut pas être lié à d’autres titres' as const
const oneLinkTitre = 'ce titre peut avoir un seul titre lié' as const
const droitsInsuffisants = 'droits insuffisants ou titre inexistant' as const
export type CheckTitreLinksError = typeof linkImpossible | typeof oneLinkTitre | typeof droitsInsuffisants | 'lien incompatible entre ces types de titre'
export const checkTitreLinks = (
titreTypeId: TitreTypeId,
titreFromIds: Readonly<TitreId[]>,
titresFrom: ITitre[],
demarches: ITitreDemarche[]
): { valid: true } | { valid: false; errors: NonEmptyArray<string> } => {
): { valid: true } | { valid: false; errors: NonEmptyArray<CheckTitreLinksError> } => {
const linkConfig = getLinkConfig(
titreTypeId,
demarches.map(({ typeId }) => ({ demarche_type_id: typeId }))
)
const errors = []
const errors: CheckTitreLinksError[] = []
if (isNullOrUndefined(linkConfig)) {
errors.push('ce titre ne peut pas être lié à d’autres titres')
errors.push(linkImpossible)
} else {
if (linkConfig.count === 'single' && titreFromIds.length > 1) {
errors.push('ce titre peut avoir un seul titre lié')
errors.push(oneLinkTitre)
}

if (titresFrom.length !== titreFromIds.length) {
errors.push('droits insuffisants ou titre inexistant')
errors.push(droitsInsuffisants)
}

if (titresFrom.some(({ typeId }) => typeId !== linkConfig.typeId)) {
errors.push(`un titre de type ${titreTypeId} ne peut-être lié qu’à un titre de type ${linkConfig.typeId}`)
errors.push('lien incompatible entre ces types de titre')
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/api/src/database/queries/titres-titres.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { CaminoError } from 'camino-common/src/zod-tools.js'
import { Pool } from 'pg'
import { z } from 'zod'
import { ZodUnparseable } from '../../tools/fp-tools.js'
import { DeepReadonly } from 'camino-common/src/typescript-tools.js'

interface LinkTitre {
linkTo: TitreId
linkFrom: TitreId[]
}
type LinkTitresErrors = DbQueryAccessError | ZodUnparseable
export const linkTitres = (pool: Pool, link: LinkTitre): Effect.Effect<void, CaminoError<LinkTitresErrors>> => {
export type LinkTitresErrors = DbQueryAccessError | ZodUnparseable
export const linkTitres = (pool: Pool, link: DeepReadonly<LinkTitre>): Effect.Effect<void, CaminoError<LinkTitresErrors>> => {
return pipe(
effectDbQueryAndValidate(deleteTitreTitreInternal, { linkTo: link.linkTo }, pool, z.void()),
Effect.flatMap(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/server/config.test.integration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { restCall } from '../../tests/_utils/index'
import { restNewCall } from '../../tests/_utils/index'
import { expect, test, describe, vi } from 'vitest'
import type { Pool } from 'pg'

Expand All @@ -7,7 +7,7 @@ console.error = vi.fn()

describe('config', () => {
test('récupère la configuration', async () => {
const tested = await restCall(null as unknown as Pool, '/config', {}, undefined)
const tested = await restNewCall(null as unknown as Pool, '/config', {}, undefined)
expect(tested.body).toMatchInlineSnapshot(`
{
"API_MATOMO_ID": "plop",
Expand Down
Loading

0 comments on commit 6584499

Please sign in to comment.