Add presentation page for "Codes, lois et règlements".

This commit is contained in:
Emmanuel 2022-08-24 23:12:07 +02:00
parent 72399dfb07
commit 7fb08b413d
26 changed files with 855 additions and 90 deletions

View file

@ -1,6 +1,22 @@
import type { Article, SectionTa, Textelr, TexteVersion } from "$lib/legal"
export interface Aggregate {
article?: { [id: string]: Article }
id?: string
ids?: string[]
section_ta?: { [id: string]: SectionTa }
texte_version?: { [id: string]: TexteVersion }
textelr?: { [id: string]: Textelr }
}
export type Follow = typeof allFollows[number] export type Follow = typeof allFollows[number]
export const allFollows = [ export const allFollows = [
"LIENS.LIEN[@sens=cible,@typelien=CREATION].@id", "LIENS.LIEN[@sens=cible,@typelien=CREATION].@id",
"STRUCT.LIEN_ART.@id",
"STRUCT.LIEN_SECTION_TA.@id",
"STRUCTURE_TA.LIEN_ART.@id",
"STRUCTURE_TA.LIEN_SECTION_TA.@id",
"TEXTELR",
] as const ] as const
export const allFollowsMutable = [...allFollows] export const allFollowsMutable = [...allFollows]

View file

@ -57,7 +57,7 @@ export function auditLimitSearchParam(
(value: number) => value <= 100, (value: number) => value <= 100,
"Value must be less than or equal to 100", "Value must be less than or equal to 100",
), ),
auditSetNullish(10), auditSetNullish(20),
), ),
) )
} }

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { SummaryView } from "augmented-data-viewer"
import type { Aggregate } from "$lib/aggregates"
import type { Article } from "$lib/legal"
import { summarizeLegalObject } from "$lib/summaries"
export let article: Article
export let data: Aggregate
export let level = 1
$: summary = summarizeLegalObject({ key: "article" }, "article", article)
</script>
{#if summary !== undefined}
<svelte:element this={`h${Math.min(level, 6)}`} class="text-lg font-bold">
<SummaryView {summary} />
</svelte:element>
{/if}
{@html article.BLOC_TEXTUEL.CONTENU}

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { page } from "$app/stores"
import { menuItemsFromLegalId } from "$lib/legal"
export let id: string | undefined | null
$: items = menuItemsFromLegalId(id)
$: pathname = $page.url.pathname
</script>
{#if items !== undefined}
<div class="tabs">
{#each items as { href, label }}
<a class="tab tab-bordered" class:tab-active={href === pathname} {href}
>{label}</a
>
{/each}
</div>
{/if}

View file

@ -38,7 +38,17 @@
function newPaginationUrl( function newPaginationUrl(
pathname: string, pathname: string,
query: Partial<PaginationQuery> | { [key: string]: string }, query:
| Partial<PaginationQuery>
| {
[key: string]:
| boolean
| number
| string
| undefined
| null
| Array<boolean | number | string | undefined | null>
},
{ limit, offset }: { limit?: number; offset?: number } = {}, { limit, offset }: { limit?: number; offset?: number } = {},
) { ) {
query = { ...query } query = { ...query }
@ -53,10 +63,20 @@
delete query.offset delete query.offset
} }
const search = new URLSearchParams( const search = new URLSearchParams(
Object.entries(query).map(([key, value]) => [ Object.entries(query).reduce((couples, [key, value]) => {
key, if (value != null) {
typeof value === "number" ? value.toString() : value, if (Array.isArray(value)) {
]), for (const item of value) {
if (item != null) {
couples.push([key, item.toString()])
}
}
} else {
couples.push([key, value.toString()])
}
}
return couples
}, [] as Array<[string, string]>),
).toString() ).toString()
return search ? `${pathname}?${search}` : pathname return search ? `${pathname}?${search}` : pathname
} }

View file

@ -0,0 +1,50 @@
<script lang="ts">
import { iterArrayOrSingleton } from "@tricoteuses/explorer-tools"
import { SummaryView } from "augmented-data-viewer"
import type { Aggregate } from "$lib/aggregates"
import ArticleView from "$lib/components/ArticleView.svelte"
import type { SectionTa } from "$lib/legal"
import { summarizeLegalObject } from "$lib/summaries"
export let data: Aggregate
export let level = 1
export let sectionTa: SectionTa
export let showArticles: boolean
$: summary = summarizeLegalObject(
{ key: "section_ta" },
"section_ta",
sectionTa,
)
</script>
<svelte:element this={`h${Math.min(level, 6)}`} class="text-1xl font-extrabold">
{sectionTa.TITRE_TA}
</svelte:element>
<div class="ml-4">
{#if showArticles}
{#each [...iterArrayOrSingleton(sectionTa.STRUCTURE_TA.LIEN_ART)] as lienArt}
{@const article = data.article?.[lienArt["@id"]]}
{#if article !== undefined}
<ArticleView {article} {data} level={level + 1} />
{/if}
{/each}
{:else if sectionTa.STRUCTURE_TA.LIEN_ART !== undefined}
<ul class="inline">
{#each [...iterArrayOrSingleton(sectionTa.STRUCTURE_TA.LIEN_ART)] as lienArt}
<li class="inline after:content-[',_'] after:last:content-['']">
Article {lienArt["@num"]}
</li>
{/each}
</ul>
{/if}
{#each [...iterArrayOrSingleton(sectionTa.STRUCTURE_TA.LIEN_SECTION_TA)] as lienSectionTa}
{@const sectionTa = data.section_ta?.[lienSectionTa["@id"]]}
{#if sectionTa !== undefined}
<svelte:self {data} level={level + 1} {sectionTa} {showArticles} />
{/if}
{/each}
</div>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { SummaryView } from "augmented-data-viewer"
import type { Aggregate } from "$lib/aggregates"
import TextelrView from "$lib/components/TextelrView.svelte"
import type { TexteVersion } from "$lib/legal"
import { summarizeLegalObject } from "$lib/summaries"
export let data: Aggregate
export let level = 1
export let showArticles: boolean
export let texteVersion: TexteVersion
$: id = texteVersion.META.META_COMMUN.ID
$: textelr = data.textelr?.[id]
$: summary = summarizeLegalObject(
{ key: "texte_version" },
"texte_version",
texteVersion,
)
</script>
{#if summary !== undefined}
<svelte:element
this={`h${Math.min(level, 6)}`}
class="text-4xl font-extrabold"
>
<SummaryView {summary} />
</svelte:element>
{/if}
{#if textelr !== undefined}
<TextelrView {data} {level} {showArticles} {textelr} />
{/if}

View file

@ -0,0 +1,37 @@
<script lang="ts">
import { iterArrayOrSingleton } from "@tricoteuses/explorer-tools"
import type { Aggregate } from "$lib/aggregates"
import ArticleView from "$lib/components/ArticleView.svelte"
import SectionTaView from "$lib/components/SectionTaView.svelte"
import type { Textelr } from "$lib/legal"
export let data: Aggregate
export let level = 1
export let showArticles: boolean
export let textelr: Textelr
</script>
{#if showArticles}
{#each [...iterArrayOrSingleton(textelr.STRUCT.LIEN_ART)] as lienArt}
{@const article = data.article?.[lienArt["@id"]]}
{#if article !== undefined}
<ArticleView {article} {data} level={level + 1} />
{/if}
{/each}
{:else if textelr.STRUCT.LIEN_ART !== undefined}
<ul class="inline">
{#each [...iterArrayOrSingleton(textelr.STRUCT.LIEN_ART)] as lienArt}
<li class="inline after:content-[',_'] after:last:content-['']">
Article {lienArt["@num"]}
</li>
{/each}
</ul>
{/if}
{#each [...iterArrayOrSingleton(textelr.STRUCT.LIEN_SECTION_TA)] as lienSectionTa}
{@const sectionTa = data.section_ta?.[lienSectionTa["@id"]]}
{#if sectionTa !== undefined}
<SectionTaView {data} level={level + 1} {sectionTa} {showArticles} />
{/if}
{/each}

View file

@ -140,6 +140,17 @@ export interface LienArt {
"@origine": "LEGI" "@origine": "LEGI"
} }
export interface LienSectionTa {
"@id": string
"@cid": string
"@fin": string
"@niv": string
"@url": string
"@etat": Etat
"@debut": string
"#text": string
}
export interface MetaCommun { export interface MetaCommun {
ID: string ID: string
} }
@ -165,6 +176,7 @@ export interface SectionTa {
TITRE_TA: string TITRE_TA: string
STRUCTURE_TA: { STRUCTURE_TA: {
LIEN_ART?: LienArt | LienArt[] LIEN_ART?: LienArt | LienArt[]
LIEN_SECTION_TA?: LienSectionTa | LienSectionTa[]
} }
} }
@ -186,6 +198,7 @@ export interface Textelr {
} }
STRUCT: { STRUCT: {
LIEN_ART?: LienArt | LienArt[] LIEN_ART?: LienArt | LienArt[]
LIEN_SECTION_TA?: LienSectionTa | LienSectionTa[]
} }
VERSIONS: { VERSIONS: {
VERSION: TextelrVersion | TextelrVersion[] VERSION: TextelrVersion | TextelrVersion[]
@ -293,7 +306,7 @@ export const appMenu: MenuItem[] = [
{ {
items: [ items: [
{ href: "/idcc", label: "Accords de branche et conventions collectives" }, { href: "/idcc", label: "Accords de branche et conventions collectives" },
{ href: "/texte_version", label: "Codes, lois et règlements" }, { href: "/textes", label: "Codes, lois et règlements" },
{ href: "/dossier_legislatif", label: "Dossiers législatifs" }, { href: "/dossier_legislatif", label: "Dossiers législatifs" },
{ href: "/jo", label: "Journal officiel" }, { href: "/jo", label: "Journal officiel" },
], ],
@ -355,36 +368,34 @@ export function bestItemForDate<T extends { "@debut": string; "@fin": string }>(
return items[0] return items[0]
} }
export function menuItemsFromLegalId(
id: string | undefined | null,
): MenuItem[] | undefined {
if (id == null) {
return undefined
}
const rootType = rootTypeFromLegalId(id)
if (rootType === undefined) {
return undefined
}
switch (rootType) {
case "texte_version":
return [
{ href: `/textes/${id}`, label: "Présentation" },
{ href: `/texte_version/${id}`, label: "TEXTE_VERSION" },
{ href: `/textelr/${id}`, label: "TEXTELR" },
]
default:
return undefined
}
}
export function pathnameFromLegalId(id: string): string | undefined { export function pathnameFromLegalId(id: string): string | undefined {
if (id.match(/^[A-Z]{4}ARTI/) !== null) { const rootType = rootTypeFromLegalId(id)
return `/article/${id}` if (rootType === undefined) {
return undefined
} }
if (id.match(/^[A-Z]{4}DOLE/) !== null) { return `/${rootType}/${id}`
return `/dossier_legislatif/${id}`
}
if (id.match(/^KALICONT/) !== null) {
return `/idcc/${id}`
}
if (id.match(/^JORFCONT/) !== null) {
return `/jo/${id}`
}
if (id.match(/^[A-Z]{4}SCTA/) !== null) {
return `/section_ta/${id}`
}
if (id.match(/^[A-Z]{4}TEXT/) !== null) {
return `/texte_version/${id}`
}
// TODO: Ce test est redondant avec le précédent (pour KALITEXT).
// => Trouver quel lien mettre par défaut.
// if (id.match(/^KALITEXT/) !== null) {
// return `/textekali/${id}`
// }
// TODO: Ce test est redondant avec le précédent (pour JORFTEXT & LEGITEXT).
// => Trouver quel lien mettre par défaut.
// if (id.match(/^[A-Z]{4}TEXT/) !== null) {
// return `/textelr/${id}`
// }
return undefined
} }
export function pathnameFromLegalObject( export function pathnameFromLegalObject(
@ -450,3 +461,28 @@ export function pathnameFromLegalObjectId(
assertNeverLegalObjectType(type) assertNeverLegalObjectType(type)
} }
} }
export function rootTypeFromLegalId(id: string): LegalObjectType | undefined {
if (!id) {
return undefined
}
if (id.match(/^[A-Z]{4}ARTI/) !== null) {
return "article"
}
if (id.match(/^[A-Z]{4}DOLE/) !== null) {
return "dossier_legislatif"
}
if (id.match(/^KALICONT/) !== null) {
return "idcc"
}
if (id.match(/^JORFCONT/) !== null) {
return "jo"
}
if (id.match(/^[A-Z]{4}SCTA/) !== null) {
return "section_ta"
}
if (id.match(/^[A-Z]{4}TEXT/) !== null) {
return "texte_version"
}
throw new Error(`Unexpected legal ID: "${id}"`)
}

View file

@ -1,20 +1,24 @@
import { iterArrayOrSingleton } from "@tricoteuses/explorer-tools" import { iterArrayOrSingleton } from "@tricoteuses/explorer-tools"
import type { Follow } from "$lib/aggregates" import type { Aggregate, Follow } from "$lib/aggregates"
import type { Article, Lien } from "$lib/legal" import {
rootTypeFromLegalId,
type Article,
type LegalObjectType,
type SectionTa,
type Textelr,
type TexteVersion,
} from "$lib/legal"
import { db } from "$lib/server/database" import { db } from "$lib/server/database"
export interface Aggregate {
article?: { [id: string]: Article }
id?: string
ids?: string[]
}
export class Aggregator { export class Aggregator {
article: { [id: string]: Article } = {} article: { [id: string]: Article } = {}
follow: Set<Follow> follow: Set<Follow>
requestedIds: Set<string> = new Set() requestedTypeAndIds: Set<TypeAndId> = new Set()
visitedIds: Set<string> = new Set() section_ta: { [id: string]: SectionTa } = {}
texte_version: { [id: string]: TexteVersion } = {}
textelr: { [id: string]: Textelr } = {}
visitedTypeAndIds: Set<TypeAndId> = new Set()
constructor(follow: Iterable<Follow>) { constructor(follow: Iterable<Follow>) {
this.follow = new Set(follow) this.follow = new Set(follow)
@ -33,46 +37,135 @@ export class Aggregator {
} }
} }
addToVisitedIds(ids: string[]) { addSectionTa(sectionTa: SectionTa): void {
for (const id of ids) { const id = sectionTa.ID
this.visitedIds.add(id) this.section_ta[id] = sectionTa
if (this.follow.has("STRUCTURE_TA.LIEN_ART.@id")) {
for (const lien of iterArrayOrSingleton(
sectionTa.STRUCTURE_TA.LIEN_ART,
)) {
this.requestId(lien["@id"])
}
}
if (this.follow.has("STRUCTURE_TA.LIEN_SECTION_TA.@id")) {
for (const lien of iterArrayOrSingleton(
sectionTa.STRUCTURE_TA.LIEN_SECTION_TA,
)) {
this.requestId(lien["@id"])
}
} }
} }
deleteFromRequestedIds(ids: string[]) { addTextelr(textelr: Textelr): void {
for (const id of ids) { const id = textelr.META.META_COMMUN.ID
this.requestedIds.delete(id) this.textelr[id] = textelr
if (this.follow.has("STRUCT.LIEN_ART.@id")) {
for (const lien of iterArrayOrSingleton(textelr.STRUCT.LIEN_ART)) {
this.requestId(lien["@id"])
}
}
if (this.follow.has("STRUCT.LIEN_SECTION_TA.@id")) {
for (const lien of iterArrayOrSingleton(textelr.STRUCT.LIEN_SECTION_TA)) {
this.requestId(lien["@id"])
}
}
}
addTexteVersion(texteVersion: TexteVersion): void {
const id = texteVersion.META.META_COMMUN.ID
this.texte_version[id] = texteVersion
if (this.follow.has("TEXTELR")) {
this.requestTypeAndId(["textelr", id])
}
}
addToVisitedTypeAndIds(typeAndIds: TypeAndId[]) {
for (const typeAndId of typeAndIds) {
this.visitedTypeAndIds.add(typeAndId)
}
}
deleteFromRequestedTypeAndIds(typeAndIds: TypeAndId[]) {
for (const typeAndId of typeAndIds) {
this.requestedTypeAndIds.delete(typeAndId)
} }
} }
async getAll(): Promise<void> { async getAll(): Promise<void> {
while (true) { while (true) {
if (this.requestedIds.size === 0) { if (this.requestedTypeAndIds.size === 0) {
break break
} }
console.log("this.requestedIds.size", this.requestedIds.size) console.log(
"this.requestedTypeAndIds.size",
this.requestedTypeAndIds.size,
)
{ {
const articleIds = [...this.requestedIds].filter( const articleTypeAndIds = [...this.requestedTypeAndIds].filter(
(id) => id.match(/^[A-Z]{4}ARTI/) !== null, ([type]) => type === "article",
) )
if (articleIds.length > 0) { if (articleTypeAndIds.length > 0) {
this.deleteFromRequestedIds(articleIds) this.deleteFromRequestedTypeAndIds(articleTypeAndIds)
for (const { data } of await db<{ id: string; data: Article }[]>` for (const { data } of await db<{ id: string; data: Article }[]>`
SELECT id, data FROM article SELECT id, data FROM article
WHERE id IN ${db(articleIds)} WHERE id IN ${db(articleTypeAndIds.map(([, id]) => id))}
`) { `) {
this.addArticle(data) this.addArticle(data)
} }
this.addToVisitedIds(articleIds) this.addToVisitedTypeAndIds(articleTypeAndIds)
}
}
{
const sectionTaTypeAndIds = [...this.requestedTypeAndIds].filter(
([type]) => type === "section_ta",
)
if (sectionTaTypeAndIds.length > 0) {
this.deleteFromRequestedTypeAndIds(sectionTaTypeAndIds)
for (const { data } of await db<{ id: string; data: SectionTa }[]>`
SELECT id, data FROM section_ta
WHERE id IN ${db(sectionTaTypeAndIds.map(([, id]) => id))}
`) {
this.addSectionTa(data)
}
this.addToVisitedTypeAndIds(sectionTaTypeAndIds)
}
}
{
const textelrTypeAndIds = [...this.requestedTypeAndIds].filter(
([type]) => type === "textelr",
)
if (textelrTypeAndIds.length > 0) {
this.deleteFromRequestedTypeAndIds(textelrTypeAndIds)
for (const { data } of await db<{ id: string; data: Textelr }[]>`
SELECT id, data FROM textelr
WHERE id IN ${db(textelrTypeAndIds.map(([, id]) => id))}
`) {
this.addTextelr(data)
}
this.addToVisitedTypeAndIds(textelrTypeAndIds)
} }
} }
} }
} }
requestId(id: string): void { requestId(id: string): void {
if (!this.visitedIds.has(id)) { const rootType = rootTypeFromLegalId(id)
this.requestedIds.add(id) if (rootType !== undefined) {
return this.requestTypeAndId([rootType, id])
}
}
requestTypeAndId(typeAndId: TypeAndId): void {
if (!this.visitedTypeAndIds.has(typeAndId)) {
this.requestedTypeAndIds.add(typeAndId)
} }
} }
@ -81,6 +174,17 @@ export class Aggregator {
if (Object.keys(this.article).length > 0) { if (Object.keys(this.article).length > 0) {
json.article = this.article json.article = this.article
} }
if (Object.keys(this.section_ta).length > 0) {
json.section_ta = this.section_ta
}
if (Object.keys(this.texte_version).length > 0) {
json.texte_version = this.texte_version
}
if (Object.keys(this.textelr).length > 0) {
json.textelr = this.textelr
}
return json return json
} }
} }
export type TypeAndId = [LegalObjectType, string]

View file

@ -1,13 +1,13 @@
import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core"
import { error } from "@sveltejs/kit" import { error } from "@sveltejs/kit"
import type { Follow } from "$lib/aggregates" import type { Aggregate, Follow } from "$lib/aggregates"
import { import {
auditFollowSearchParams, auditFollowSearchParams,
auditQSearchParam, auditQSearchParam,
} from "$lib/auditors/search_params" } from "$lib/auditors/search_params"
import type { Article } from "$lib/legal" import type { Article } from "$lib/legal"
import { type Aggregate, Aggregator } from "$lib/server/aggregates" import { Aggregator } from "$lib/server/aggregates"
import { db } from "$lib/server/database" import { db } from "$lib/server/database"
export function auditSearchParams( export function auditSearchParams(
@ -41,15 +41,15 @@ export function auditSearchParams(
export const doGetRecherche = async ( export const doGetRecherche = async (
url: URL, url: URL,
): Promise< ): Promise<
| (Aggregate & { Aggregate & {
q: string follow: Follow[]
}) q?: string
| null }
> => { > => {
const [query, queryError] = auditSearchParams( const [query, queryError] = auditSearchParams(
cleanAudit, cleanAudit,
url.searchParams, url.searchParams,
) as [{ follow: Follow[]; q?: string }, unknown] ) as [{ follow: Set<Follow>; q?: string }, unknown]
if (queryError !== null) { if (queryError !== null) {
console.error( console.error(
`Error in ${url.pathname} query:\n${JSON.stringify( `Error in ${url.pathname} query:\n${JSON.stringify(
@ -62,6 +62,8 @@ export const doGetRecherche = async (
} }
const { follow, q } = query const { follow, q } = query
const aggregator = new Aggregator(follow)
let id: string | undefined = undefined
if (q !== undefined) { if (q !== undefined) {
// https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/ // https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/
// https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/1983-12-30/ // https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006308296/1983-12-30/
@ -69,23 +71,25 @@ export const doGetRecherche = async (
// https://www.legifrance.gouv.fr/loda/article_lc/LEGIARTI000036456533 // https://www.legifrance.gouv.fr/loda/article_lc/LEGIARTI000036456533
// https://www.legifrance.gouv.fr/loda/article_lc/LEGIARTI000036456533/2018-01-01 // https://www.legifrance.gouv.fr/loda/article_lc/LEGIARTI000036456533/2018-01-01
// https://www.legifrance.gouv.fr/loda/id/LEGIARTI000006317314/1983-12-30 // https://www.legifrance.gouv.fr/loda/id/LEGIARTI000006317314/1983-12-30
const articleId = q.match(/LEGIARTI\d+/)?.[0] id = q.match(/LEGIARTI\d+/)?.[0]
if (articleId != null) { if (id != null) {
const article = ( const article = (
await db<{ data: Article }[]>` await db<{ data: Article }[]>`
SELECT data FROM article SELECT data FROM article
WHERE id = ${articleId} WHERE id = ${id}
LIMIT 1
` `
).map(({ data }) => data)[0] ).map(({ data }) => data)[0]
if (article !== undefined) { if (article !== undefined) {
const aggregator = new Aggregator(follow)
aggregator.addArticle(article) aggregator.addArticle(article)
await aggregator.getAll()
return { ...aggregator.toJson(), id: article.META.META_COMMUN.ID, q }
} }
} }
} }
return null await aggregator.getAll()
return {
...aggregator.toJson(),
follow: [...follow],
id,
q,
}
} }

View file

@ -0,0 +1,178 @@
import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core"
import { error } from "@sveltejs/kit"
import type { Aggregate, Follow } from "$lib/aggregates"
import {
auditFollowSearchParams,
auditLimitSearchParam,
auditOffsetSearchParam,
auditQSearchParam,
} from "$lib/auditors/search_params"
import type { TexteVersion } from "$lib/legal"
import { Aggregator } from "$lib/server/aggregates"
import { db } from "$lib/server/database"
export function auditGetTexteSearchParams(
audit: Audit,
query: URLSearchParams,
): [unknown, unknown] {
if (query == null) {
return [query, null]
}
if (!(query instanceof URLSearchParams)) {
return audit.unexpectedType(query, "URLSearchParams")
}
const data: { [key: string]: unknown } = {}
for (const [key, value] of query.entries()) {
let values = data[key] as string[] | undefined
if (values === undefined) {
values = data[key] = []
}
values.push(value)
}
const errors: { [key: string]: unknown } = {}
const remainingKeys = new Set(Object.keys(data))
auditFollowSearchParams(audit, data, errors, remainingKeys)
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
}
export function auditListTextesSearchParams(
audit: Audit,
query: URLSearchParams,
): [unknown, unknown] {
if (query == null) {
return [query, null]
}
if (!(query instanceof URLSearchParams)) {
return audit.unexpectedType(query, "URLSearchParams")
}
const data: { [key: string]: unknown } = {}
for (const [key, value] of query.entries()) {
let values = data[key] as string[] | undefined
if (values === undefined) {
values = data[key] = []
}
values.push(value)
}
const errors: { [key: string]: unknown } = {}
const remainingKeys = new Set(Object.keys(data))
auditFollowSearchParams(audit, data, errors, remainingKeys)
auditLimitSearchParam(audit, data, errors, remainingKeys)
auditOffsetSearchParam(audit, data, errors, remainingKeys)
auditQSearchParam(audit, data, errors, remainingKeys)
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
}
export const doGetTexte = async (
id: string,
url: URL,
): Promise<
Aggregate & {
follow: Follow[]
}
> => {
const [query, queryError] = auditGetTexteSearchParams(
cleanAudit,
url.searchParams,
) as [
{
follow: Set<Follow>
},
unknown,
]
if (queryError !== null) {
console.error(
`Error in ${url.pathname} query:\n${JSON.stringify(
query,
null,
2,
)}\n\nError:\n${JSON.stringify(queryError, null, 2)}`,
)
throw error(400, JSON.stringify(queryError, null, 2))
}
const { follow } = query
const texteVersion = (
await db<{ data: TexteVersion }[]>`
SELECT data FROM texte_version
WHERE ID = ${id}
`
).map(({ data }) => data)[0]
if (texteVersion === undefined) {
throw error(404, `TEXTE_VERSION ${id} non trouvé`)
}
const aggregator = new Aggregator(follow)
aggregator.addTexteVersion(texteVersion)
await aggregator.getAll()
return {
...aggregator.toJson(),
follow: [...follow],
id,
}
}
export const doListTextes = async (
url: URL,
): Promise<
Aggregate & {
follow: Follow[]
limit: number
offset: number
q?: string
}
> => {
const [query, queryError] = auditListTextesSearchParams(
cleanAudit,
url.searchParams,
) as [
{
follow: Set<Follow>
limit: number
offset: number
q?: string
},
unknown,
]
if (queryError !== null) {
console.error(
`Error in ${url.pathname} query:\n${JSON.stringify(
query,
null,
2,
)}\n\nError:\n${JSON.stringify(queryError, null, 2)}`,
)
throw error(400, JSON.stringify(queryError, null, 2))
}
const { follow, limit, offset, q } = query
const texteVersionArray = (
await db<{ data: TexteVersion }[]>`
SELECT data FROM texte_version
OFFSET ${offset}
LIMIT ${limit}
`
).map(({ data }) => data)
const aggregator = new Aggregator(follow)
for (const texteVersion of texteVersionArray) {
aggregator.addTexteVersion(texteVersion)
}
await aggregator.getAll()
return {
...aggregator.toJson(),
follow: [...follow],
ids: texteVersionArray.map(
(texteVersion) => texteVersion.META.META_COMMUN.ID,
),
limit,
offset,
q,
}
}

View file

@ -14,6 +14,7 @@ import {
type LegalObjectType, type LegalObjectType,
type Lien, type Lien,
type LienArt, type LienArt,
type LienSectionTa,
pathnameFromLegalId, pathnameFromLegalId,
pathnameFromLegalObject, pathnameFromLegalObject,
pathnameFromLegalObjectId, pathnameFromLegalObjectId,
@ -261,7 +262,7 @@ export function summarizeLegalObject(
] ]
return { return {
items: [ items: [
`Article ${metaArticle.TYPE} ${metaArticle.ETAT}${metaArticle.NUM}, en vigueur`, `Article ${metaArticle.NUM} ${metaArticle.TYPE} ${metaArticle.ETAT}, en vigueur`,
...(metaArticle.DATE_FIN === "2999-01-01" ...(metaArticle.DATE_FIN === "2999-01-01"
? ([ ? ([
" depuis le ", " depuis le ",
@ -373,7 +374,7 @@ export const summarizeLienArt: Summarizer = (access, value) => {
return undefined return undefined
} }
return { return {
content: `Article ${lienArt["@num"]}`, content: `Article ${lienArt["@num"]}`,
href: pathnameFromLegalObjectId("article", lienArt["@id"]), href: pathnameFromLegalObjectId("article", lienArt["@id"]),
type: "link", type: "link",
} }
@ -423,6 +424,36 @@ export const summarizeLienId: Summarizer = (access, value) => {
} }
} }
export const summarizeLienSectionTa: Summarizer = (access, value) => {
const lienSectionTa = value as LienSectionTa | undefined
if (lienSectionTa === undefined) {
return undefined
}
return {
content: lienSectionTa["#text"],
href: pathnameFromLegalObjectId("section_ta", lienSectionTa["@id"]),
type: "link",
}
}
export const summarizeLienSectionTaId: Summarizer = (access, value) => {
const lienSectionTa = value as LienSectionTa | undefined
if (lienSectionTa === undefined) {
return undefined
}
return {
items: [
{
content: JSON.stringify(lienSectionTa["@id"]),
type: "raw_data",
},
{ class: "mt-1 mx-1", icon: arrowRight, inline: true, type: "icon" },
summarizeLienSectionTa(access, lienSectionTa)!,
],
type: "concatenation",
}
}
export const summarizeSectionTaProperties: Summarizer = (access, value) => { export const summarizeSectionTaProperties: Summarizer = (access, value) => {
if (access?.key === "section_ta" && typeof value !== "number") { if (access?.key === "section_ta" && typeof value !== "number") {
return summarizeLegalObject(access, "section_ta", value) return summarizeLegalObject(access, "section_ta", value)
@ -434,6 +465,14 @@ export const summarizeSectionTaProperties: Summarizer = (access, value) => {
if (access?.key === "@cid" && access?.access?.key === "TEXTE") { if (access?.key === "@cid" && access?.access?.key === "TEXTE") {
return summarizeLegalIdToLink(access, value) return summarizeLegalIdToLink(access, value)
} }
if (
access?.key === "@cid" &&
(access?.access?.key === "LIEN_SECTION_TA" ||
(typeof access?.access?.key === "number" &&
access?.access?.access?.key === "LIEN_SECTION_TA"))
) {
return summarizeLegalIdToLink(access, value)
}
if ( if (
access?.key === "@id" && access?.key === "@id" &&
(access?.access?.key === "LIEN_ART" || (access?.access?.key === "LIEN_ART" ||
@ -442,12 +481,27 @@ export const summarizeSectionTaProperties: Summarizer = (access, value) => {
) { ) {
return summarizeLienArtId(access.access, access.parent) return summarizeLienArtId(access.access, access.parent)
} }
if (
access?.key === "@id" &&
(access?.access?.key === "LIEN_SECTION_TA" ||
(typeof access?.access?.key === "number" &&
access?.access?.access?.key === "LIEN_SECTION_TA"))
) {
return summarizeLienSectionTaId(access.access, access.parent)
}
if ( if (
(access?.key === "LIEN_ART" && !Array.isArray(value)) || (access?.key === "LIEN_ART" && !Array.isArray(value)) ||
(typeof access?.key === "number" && access?.access?.key === "LIEN_ART") (typeof access?.key === "number" && access?.access?.key === "LIEN_ART")
) { ) {
return summarizeLienArt(access, value) return summarizeLienArt(access, value)
} }
if (
(access?.key === "LIEN_SECTION_TA" && !Array.isArray(value)) ||
(typeof access?.key === "number" &&
access?.access?.key === "LIEN_SECTION_TA")
) {
return summarizeLienSectionTa(access, value)
}
return undefined return undefined
} }
@ -490,6 +544,14 @@ export const summarizeTextelrProperties: Summarizer = (access, value) => {
return summarizeLegalObjectToLink(access, "textelr", value) return summarizeLegalObjectToLink(access, "textelr", value)
} }
if (
access?.key === "@cid" &&
(access?.access?.key === "LIEN_SECTION_TA" ||
(typeof access?.access?.key === "number" &&
access?.access?.access?.key === "LIEN_SECTION_TA"))
) {
return summarizeLegalIdToLink(access, value)
}
if ( if (
access?.key === "@id" && access?.key === "@id" &&
(access?.access?.key === "LIEN_ART" || (access?.access?.key === "LIEN_ART" ||
@ -498,6 +560,14 @@ export const summarizeTextelrProperties: Summarizer = (access, value) => {
) { ) {
return summarizeLienArtId(access.access, access.parent) return summarizeLienArtId(access.access, access.parent)
} }
if (
access?.key === "@id" &&
(access?.access?.key === "LIEN_SECTION_TA" ||
(typeof access?.access?.key === "number" &&
access?.access?.access?.key === "LIEN_SECTION_TA"))
) {
return summarizeLienSectionTaId(access.access, access.parent)
}
if (access?.key === "CID") { if (access?.key === "CID") {
return summarizeLegalIdToLink(access, value) return summarizeLegalIdToLink(access, value)
} }
@ -507,6 +577,13 @@ export const summarizeTextelrProperties: Summarizer = (access, value) => {
) { ) {
return summarizeLienArt(access, value) return summarizeLienArt(access, value)
} }
if (
(access?.key === "LIEN_SECTION_TA" && !Array.isArray(value)) ||
(typeof access?.key === "number" &&
access?.access?.key === "LIEN_SECTION_TA")
) {
return summarizeLienSectionTa(access, value)
}
if (access?.key === "LIEN_TXT") { if (access?.key === "LIEN_TXT") {
return summarizeTextelrVersionLienTxt(access, value) return summarizeTextelrVersionLienTxt(access, value)
} }

View file

@ -4,11 +4,9 @@ import type { RequestHandler } from "./$types"
export const GET: RequestHandler = async ({ url }) => { export const GET: RequestHandler = async ({ url }) => {
const result = await doGetRecherche(url) const result = await doGetRecherche(url)
return result === null return new Response(JSON.stringify(result, null, 2), {
? new Response(null, { status: 204 }) headers: {
: new Response(JSON.stringify(result, null, 2), { "Content-Type": "application/json; charset=utf-8",
headers: { },
"Content-Type": "application/json; charset=utf-8", })
},
})
} }

View file

@ -0,0 +1,12 @@
import { doListTextes } from "$lib/server/doers/textes"
import type { RequestHandler } from "./$types"
export const GET: RequestHandler = async ({ url }) => {
const result = await doListTextes(url)
return new Response(JSON.stringify(result, null, 2), {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
}

View file

@ -0,0 +1,12 @@
import { doGetTexte } from "$lib/server/doers/textes"
import type { RequestHandler } from "./$types"
export const GET: RequestHandler = async ({ params, url }) => {
const result = await doGetTexte(params.id, url)
return new Response(JSON.stringify(result, null, 2), {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
}

View file

@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ params }) => {
).map(({ data }) => data)[0] ).map(({ data }) => data)[0]
if (sectionTa === undefined) { if (sectionTa === undefined) {
throw error(404, `Section TA ${params.id} non trouvée`) throw error(404, `SECTION_TA ${params.id} non trouvé`)
} }
return { section_ta: sectionTa } return { section_ta: sectionTa }
} }

View file

@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ params }) => {
).map(({ data }) => data)[0] ).map(({ data }) => data)[0]
if (texteVersion === undefined) { if (texteVersion === undefined) {
throw error(404, `Texte version ${params.id} non trouvé`) throw error(404, `TEXTE_VERSION ${params.id} non trouvé`)
} }
return { texte_version: texteVersion } return { texte_version: texteVersion }
} }

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { TreeView, SummaryView } from "augmented-data-viewer" import { TreeView, SummaryView } from "augmented-data-viewer"
import IdPagesSwitcher from "$lib/components/IdPagesSwitcher.svelte"
import { import {
summarizeTexteVersionProperties, summarizeTexteVersionProperties,
summarizeLegalObject, summarizeLegalObject,
@ -19,6 +20,8 @@
) )
</script> </script>
<IdPagesSwitcher id={texteVersion.META.META_COMMUN.ID} />
<header class="prose my-6 max-w-full"> <header class="prose my-6 max-w-full">
<h2>TEXTE_VERSION</h2> <h2>TEXTE_VERSION</h2>
{#if summary !== undefined} {#if summary !== undefined}

View file

@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ params }) => {
).map(({ data }) => data)[0] ).map(({ data }) => data)[0]
if (textekali === undefined) { if (textekali === undefined) {
throw error(404, `Texte KALI ${params.id} non trouvé`) throw error(404, `TEXTEKALI ${params.id} non trouvé`)
} }
return { textekali } return { textekali }
} }

View file

@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ params }) => {
).map(({ data }) => data)[0] ).map(({ data }) => data)[0]
if (textelr === undefined) { if (textelr === undefined) {
throw error(404, `Texte LR ${params.id} non trouvé`) throw error(404, `TEXTELR ${params.id} non trouvé`)
} }
return { textelr: textelr } return { textelr: textelr }
} }

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { TreeView, SummaryView } from "augmented-data-viewer" import { TreeView, SummaryView } from "augmented-data-viewer"
import IdPagesSwitcher from "$lib/components/IdPagesSwitcher.svelte"
import { import {
summarizeTextelrProperties, summarizeTextelrProperties,
summarizeLegalObject, summarizeLegalObject,
@ -15,6 +16,8 @@
$: summary = summarizeLegalObject({ key: "textelr" }, "textelr", textelr) $: summary = summarizeLegalObject({ key: "textelr" }, "textelr", textelr)
</script> </script>
<IdPagesSwitcher id={textelr.META.META_COMMUN.ID} />
<header class="prose my-6 max-w-full"> <header class="prose my-6 max-w-full">
<h2>TEXTELR</h2> <h2>TEXTELR</h2>
{#if summary !== undefined} {#if summary !== undefined}

View file

@ -0,0 +1,7 @@
import { doListTextes } from "$lib/server/doers/textes"
import type { PageServerLoad } from "./$types"
export const load: PageServerLoad = async ({ url }) => {
return (await doListTextes(url)) ?? {}
}

View file

@ -0,0 +1,69 @@
<script lang="ts">
// import Icon from "@iconify/svelte"
// import searchIcon from "@iconify-icons/codicon/search"
import { TreeView } from "augmented-data-viewer"
// import { page } from "$app/stores"
import Pagination from "$lib/components/Pagination.svelte"
import type { Article, SectionTa, Textelr, TexteVersion } from "$lib/legal"
import { summarizeTexteVersionProperties } from "$lib/summaries"
import type { PageData } from "./$types"
export let data: PageData
const articleById = data.article as { [id: string]: Article }
const sectionTaById = data.sectionTa as { [id: string]: SectionTa }
const texteVersionById = data.texte_version as { [id: string]: TexteVersion }
const texteVersionArray = (data.ids as string[]).map(
(id) => texteVersionById[id],
)
const textelrById = data.textelr as { [id: string]: Textelr[] }
</script>
<header class="prose my-6 max-w-full">
<h1>Codes, lois et règlements</h1>
</header>
<!-- <form action={$page.url.pathname} class="mx-auto max-w-sm" method="get">
<div class="form-control">
<div class="input-group">
<input
class="input input-bordered"
name="q"
type="search"
bind:value={q}
/>
<button class="btn btn-square" type="submit">
<Icon icon={searchIcon} />
</button>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Législature</span>
<select
class="select select-bordered"
name="legislature"
bind:value={legislature}
>
<option value="">Toutes</option>
{#each Object.entries(Legislature) as [key, value]}
{#if value !== "*"}
<option {value}>{key}</option>
{/if}
{/each}
</select>
</label>
</div>
</form> -->
<TreeView
access={{ key: "texte_version" }}
frame={false}
open
summarize={summarizeTexteVersionProperties}
value={texteVersionArray}
/>
<Pagination currentPageCount={texteVersionArray.length} />

View file

@ -0,0 +1,7 @@
import { doGetTexte } from "$lib/server/doers/textes"
import type { PageServerLoad } from "./$types"
export const load: PageServerLoad = async ({ params, url }) => {
return (await doGetTexte(params.id, url)) ?? {}
}

View file

@ -0,0 +1,55 @@
<script lang="ts">
import { TreeView, SummaryView } from "augmented-data-viewer"
import IdPagesSwitcher from "$lib/components/IdPagesSwitcher.svelte"
import TexteVersionView from "$lib/components/TexteVersionView.svelte"
import { summarizeLegalObject } from "$lib/summaries"
import type { PageData } from "./$types"
export let data: PageData
const id = data.id!
const texteVersion = data.texte_version![id]
let showArticles = false
let showRawData = false
$: summary = summarizeLegalObject(
{ key: "texte_version" },
"texte_version",
texteVersion,
)
</script>
<IdPagesSwitcher {id} />
<header class="prose my-6 max-w-full">
<h2>Codes, lois et règlements</h2>
{#if summary !== undefined}
<h1>
<SummaryView {summary} />
</h1>
{/if}
</header>
<form class="max-w-xs" on:submit|preventDefault>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Données brutes</span>
<input class="toggle" type="checkbox" bind:checked={showRawData} />
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Montrer les articles</span>
<input class="toggle" type="checkbox" bind:checked={showArticles} />
</label>
</div>
</form>
{#if showRawData}
<TreeView frame={false} open value={data} />
{:else}
<TexteVersionView {data} {showArticles} {texteVersion} />
{/if}