Improve search of 'textes' (allow by nature & full text search on title).

This commit is contained in:
Emmanuel 2022-08-26 14:42:09 +02:00
parent 6d9efd976f
commit a64aa82835
21 changed files with 512 additions and 115 deletions

View file

@ -1,4 +1,10 @@
import type { Article, SectionTa, Textelr, TexteVersion } from "$lib/legal"
import type {
Article,
SectionTa,
Textekali,
Textelr,
TexteVersion,
} from "$lib/legal"
export interface Aggregate {
article?: { [id: string]: Article }
@ -6,6 +12,7 @@ export interface Aggregate {
ids?: string[]
section_ta?: { [id: string]: SectionTa }
texte_version?: { [id: string]: TexteVersion }
textekali?: { [id: string]: Textekali }
textelr?: { [id: string]: Textelr }
}
@ -13,7 +20,7 @@ export type Follow = typeof allFollows[number]
export interface GetRechercheResult extends Aggregate {
follow: Follow[]
id: string
id?: string
q?: string
}
@ -26,6 +33,7 @@ export interface ListTextesResult extends Aggregate {
follow: Follow[]
ids: string[]
limit: number
nature?: string
offset: number
q?: string
}
@ -36,6 +44,7 @@ export const allFollows = [
"STRUCT.LIEN_SECTION_TA.@id",
"STRUCTURE_TA.LIEN_ART.@id",
"STRUCTURE_TA.LIEN_SECTION_TA.@id",
"TEXTEKALI",
"TEXTELR",
] as const
export const allFollowsMutable = [...allFollows]

View file

@ -33,6 +33,22 @@ export function auditFollowSearchParams(
)
}
export function auditFollowWithFalseSearchParams(
audit: Audit,
data: { [key: string]: unknown },
errors: { [key: string]: unknown },
remainingKeys: Set<string>,
): void {
audit.attribute(
data,
"follow",
true,
errors,
remainingKeys,
auditSearchParamsOptionsSet([...allFollowsMutable, "false"]),
)
}
export function auditLimitSearchParam(
audit: Audit,
data: { [key: string]: unknown },

View file

@ -4,6 +4,7 @@
import { page } from "$app/stores"
import { auditPaginationSearchParams } from "$lib/auditors/search_params"
import type { PaginationQuery } from "$lib/queries"
import { urlFromUrlAndQuery } from "$lib/urls"
export let count: number | undefined | null = undefined // Count is not always known.
export let currentPageCount: number
@ -47,7 +48,7 @@
| string
| undefined
| null
| Array<boolean | number | string | undefined | null>
| Iterable<boolean | number | string | undefined | null>
},
{ limit, offset }: { limit?: number; offset?: number } = {},
) {
@ -62,23 +63,7 @@
} else {
delete query.offset
}
const search = new URLSearchParams(
Object.entries(query).reduce((couples, [key, value]) => {
if (value != null) {
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()
return search ? `${pathname}?${search}` : pathname
return urlFromUrlAndQuery(pathname, query)
}
function pageNumber(limit: number, offset: number): number {

View file

@ -13,7 +13,7 @@
$: id = texteVersion.META.META_COMMUN.ID
$: textelr = data.textelr?.[id]
$: textelr = data.textekali?.[id] ?? data.textelr?.[id]
$: summary = summarizeLegalObject(
{ key: "texte_version" },

View file

@ -153,6 +153,10 @@ export interface LienSectionTa {
export interface MetaCommun {
ID: string
URL: string
NATURE: string
ORIGINE: string
ANCIEN_ID: string
}
export interface MetaTexteChronicle {
@ -180,15 +184,9 @@ export interface SectionTa {
}
}
export interface Textekali {
META: {
META_COMMUN: MetaCommun
META_SPEC: {
META_TEXTE_CHRONICLE: MetaTexteChronicle
}
}
}
export type Textekali = Textelr
/// Racine de l'arborescence d'un texte législatif ou règlementaire
export interface Textelr {
META: {
META_COMMUN: MetaCommun
@ -383,7 +381,9 @@ export function menuItemsFromLegalId(
return [
{ href: `/textes/${id}`, label: "Présentation" },
{ href: `/texte_version/${id}`, label: "TEXTE_VERSION" },
{ href: `/textelr/${id}`, label: "TEXTELR" },
id.startsWith("KALI")
? { href: `/textekali/${id}`, label: "TEXTEKALI" }
: { href: `/textelr/${id}`, label: "TEXTELR" },
]
default:
return undefined
@ -395,6 +395,9 @@ export function pathnameFromLegalId(id: string): string | undefined {
if (rootType === undefined) {
return undefined
}
if (["texte_version", "textekali", "textelr"].includes(rootType)) {
return `/textes/${id}`
}
return `/${rootType}/${id}`
}
@ -418,11 +421,14 @@ export function pathnameFromLegalObject(
case "section_ta":
return `/section_ta/${(object as SectionTa).ID}`
case "texte_version":
return `/texte_version/${(object as TexteVersion).META.META_COMMUN.ID}`
// return `/texte_version/${(object as TexteVersion).META.META_COMMUN.ID}`
return `/textes/${(object as TexteVersion).META.META_COMMUN.ID}`
case "textekali":
return `/textekali/${(object as Textekali).META.META_COMMUN.ID}`
// return `/textekali/${(object as Textekali).META.META_COMMUN.ID}`
return `/textes/${(object as TexteVersion).META.META_COMMUN.ID}`
case "textelr":
return `/textelr/${(object as Textelr).META.META_COMMUN.ID}`
// return `/textelr/${(object as Textelr).META.META_COMMUN.ID}`
return `/textes/${(object as TexteVersion).META.META_COMMUN.ID}`
case "versions":
return `/versions/${(object as VersionsWrapper).eli}`
default:
@ -449,11 +455,14 @@ export function pathnameFromLegalObjectId(
case "section_ta":
return `/section_ta/${id}`
case "texte_version":
return `/texte_version/${id}`
// return `/texte_version/${id}`
return `/textes/${id}`
case "textekali":
return `/textekali/${id}`
// return `/textekali/${id}`
return `/textes/${id}`
case "textelr":
return `/textelr/${id}`
// return `/textelr/${id}`
return `/textes/${id}`
case "versions":
// Here, id is an ELI.
return `/versions/${id}`

View file

@ -232,20 +232,34 @@ async function importJorf({ resume }: { resume?: string } = {}): Promise<void> {
break
}
case "TEXTE_VERSION": {
const version = element as TexteVersion
const texteVersion = element as TexteVersion
const textAFragments = [
texteVersion.META.META_SPEC.META_TEXTE_VERSION.TITRE,
texteVersion.META.META_SPEC.META_TEXTE_VERSION.TITREFULL,
]
await db`
INSERT INTO texte_version (
id,
data
data,
nature,
text_search
) VALUES (
${version.META.META_COMMUN.ID},
${db.json(version as unknown as JSONValue)}
${texteVersion.META.META_COMMUN.ID},
${db.json(texteVersion as unknown as JSONValue)},
${texteVersion.META.META_COMMUN.NATURE}
setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
)
ON CONFLICT (id)
DO UPDATE SET
data = ${db.json(version as unknown as JSONValue)}
data = ${db.json(texteVersion as unknown as JSONValue)},
nature = ${texteVersion.META.META_COMMUN.NATURE},
text_search = setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
`
texteVersionRemainingIds.delete(version.META.META_COMMUN.ID)
texteVersionRemainingIds.delete(texteVersion.META.META_COMMUN.ID)
break
}
case "TEXTELR": {

View file

@ -190,20 +190,34 @@ async function importKali({ resume }: { resume?: string } = {}): Promise<void> {
break
}
case "TEXTE_VERSION": {
const version = element as TexteVersion
const texteVersion = element as TexteVersion
const textAFragments = [
texteVersion.META.META_SPEC.META_TEXTE_VERSION.TITRE,
texteVersion.META.META_SPEC.META_TEXTE_VERSION.TITREFULL,
]
await db`
INSERT INTO texte_version (
id,
data
data,
nature,
text_search
) VALUES (
${version.META.META_COMMUN.ID},
${db.json(version as unknown as JSONValue)}
${texteVersion.META.META_COMMUN.ID},
${db.json(texteVersion as unknown as JSONValue)},
${texteVersion.META.META_COMMUN.NATURE}
setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
)
ON CONFLICT (id)
DO UPDATE SET
data = ${db.json(version as unknown as JSONValue)}
data = ${db.json(texteVersion as unknown as JSONValue)},
nature = ${texteVersion.META.META_COMMUN.NATURE},
text_search = setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
`
texteVersionRemainingIds.delete(version.META.META_COMMUN.ID)
texteVersionRemainingIds.delete(texteVersion.META.META_COMMUN.ID)
break
}
case "TEXTEKALI": {

View file

@ -77,7 +77,7 @@ async function importLegi({ resume }: { resume?: string } = {}): Promise<void> {
`
).map(({ id }) => id),
)
const texteVersionRemainingElis = new Set(
const texteVersionRemainingIds = new Set(
(
await db<{ id: string }[]>`
SELECT id
@ -198,20 +198,34 @@ async function importLegi({ resume }: { resume?: string } = {}): Promise<void> {
break
}
case "TEXTE_VERSION": {
const version = element as TexteVersion
const texteVersion = element as TexteVersion
const textAFragments = [
texteVersion.META.META_SPEC.META_TEXTE_VERSION.TITRE,
texteVersion.META.META_SPEC.META_TEXTE_VERSION.TITREFULL,
]
await db`
INSERT INTO texte_version (
id,
data
data,
nature,
text_search
) VALUES (
${version.META.META_COMMUN.ID},
${db.json(version as unknown as JSONValue)}
${texteVersion.META.META_COMMUN.ID},
${db.json(texteVersion as unknown as JSONValue)},
${texteVersion.META.META_COMMUN.NATURE}
setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
)
ON CONFLICT (id)
DO UPDATE SET
data = ${db.json(version as unknown as JSONValue)}
data = ${db.json(texteVersion as unknown as JSONValue)},
nature = ${texteVersion.META.META_COMMUN.NATURE},
text_search = setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
`
texteVersionRemainingElis.delete(version.META.META_COMMUN.ID)
texteVersionRemainingIds.delete(texteVersion.META.META_COMMUN.ID)
break
}
case "TEXTELR": {
@ -310,7 +324,7 @@ async function importLegi({ resume }: { resume?: string } = {}): Promise<void> {
WHERE id = ${id}
`
}
for (const id of texteVersionRemainingElis) {
for (const id of texteVersionRemainingIds) {
console.log(`Deleting TEXTE_VERSION ${id}`)
await db`
DELETE FROM texte_version

View file

@ -6,6 +6,7 @@ import {
type Article,
type LegalObjectType,
type SectionTa,
type Textekali,
type Textelr,
type TexteVersion,
} from "$lib/legal"
@ -17,6 +18,7 @@ export class Aggregator {
requestedTypeAndIds: Set<TypeAndId> = new Set()
section_ta: { [id: string]: SectionTa } = {}
texte_version: { [id: string]: TexteVersion } = {}
textekali: { [id: string]: Textekali } = {}
textelr: { [id: string]: Textelr } = {}
visitedTypeAndIds: Set<TypeAndId> = new Set()
@ -58,6 +60,25 @@ export class Aggregator {
}
}
addTextekali(textekali: Textekali): void {
const id = textekali.META.META_COMMUN.ID
this.textekali[id] = textekali
if (this.follow.has("STRUCT.LIEN_ART.@id")) {
for (const lien of iterArrayOrSingleton(textekali.STRUCT.LIEN_ART)) {
this.requestId(lien["@id"])
}
}
if (this.follow.has("STRUCT.LIEN_SECTION_TA.@id")) {
for (const lien of iterArrayOrSingleton(
textekali.STRUCT.LIEN_SECTION_TA,
)) {
this.requestId(lien["@id"])
}
}
}
addTextelr(textelr: Textelr): void {
const id = textelr.META.META_COMMUN.ID
this.textelr[id] = textelr
@ -79,6 +100,9 @@ export class Aggregator {
const id = texteVersion.META.META_COMMUN.ID
this.texte_version[id] = texteVersion
if (this.follow.has("TEXTEKALI")) {
this.requestTypeAndId(["textekali", id])
}
if (this.follow.has("TEXTELR")) {
this.requestTypeAndId(["textelr", id])
}
@ -97,12 +121,9 @@ export class Aggregator {
}
async getAll(): Promise<void> {
while (true) {
if (this.requestedTypeAndIds.size === 0) {
break
}
while (this.requestedTypeAndIds.size > 0) {
console.log(
"this.requestedTypeAndIds.size",
"Aggregator.getAll: this.requestedTypeAndIds.size =",
this.requestedTypeAndIds.size,
)
@ -138,6 +159,22 @@ export class Aggregator {
}
}
{
const textekaliTypeAndIds = [...this.requestedTypeAndIds].filter(
([type]) => type === "textekali",
)
if (textekaliTypeAndIds.length > 0) {
this.deleteFromRequestedTypeAndIds(textekaliTypeAndIds)
for (const { data } of await db<{ id: string; data: Textekali }[]>`
SELECT id, data FROM textekali
WHERE id IN ${db(textekaliTypeAndIds.map(([, id]) => id))}
`) {
this.addTextekali(data)
}
this.addToVisitedTypeAndIds(textekaliTypeAndIds)
}
}
{
const textelrTypeAndIds = [...this.requestedTypeAndIds].filter(
([type]) => type === "textelr",
@ -180,6 +217,9 @@ export class Aggregator {
if (Object.keys(this.texte_version).length > 0) {
json.texte_version = this.texte_version
}
if (Object.keys(this.textekali).length > 0) {
json.textekali = this.textekali
}
if (Object.keys(this.textelr).length > 0) {
json.textelr = this.textelr
}

View file

@ -1,5 +1,7 @@
import assert from "assert"
import type { SerializableParameter } from "postgres"
import type { TexteVersion } from "$lib/legal"
import { db, type Version, versionNumber } from "$lib/server/database"
export async function configureDatabase() {
@ -42,6 +44,17 @@ export async function configureDatabase() {
await db`ALTER TABLE IF EXISTS textes RENAME TO texte_version`
}
if (version.number < 3) {
await db`
ALTER TABLE IF EXISTS texte_version
ADD COLUMN IF NOT EXISTS nature text
`
await db`
ALTER TABLE IF EXISTS texte_version
ADD COLUMN IF NOT EXISTS text_search tsvector
`
}
// Types
// Tables
@ -108,7 +121,9 @@ export async function configureDatabase() {
await db`
CREATE TABLE IF NOT EXISTS texte_version (
id char(20) PRIMARY KEY,
data jsonb NOT NULL
data jsonb NOT NULL,
nature text NOT NULL,
text_search tsvector NOT NULL
)
`
@ -139,8 +154,62 @@ export async function configureDatabase() {
// Apply patches that must be executed after every table is created.
if (version.number < 3) {
// Fill "nature" column of table texte_version.
for await (const rows of db<
Array<{ data: TexteVersion; id: string; nature: string }>
>`SELECT data, id, nature FROM texte_version`.cursor(100)) {
for (const { data, id, nature } of rows) {
if (data.META.META_COMMUN.NATURE !== nature) {
await db`
UPDATE texte_version
SET nature = ${data.META.META_COMMUN.NATURE}
WHERE id = ${id}
`
}
}
}
await db`
ALTER TABLE texte_version
ALTER COLUMN nature SET NOT NULL
`
// Fill "text_search" column of table texte_version.
for await (const rows of db<
Array<{ data: TexteVersion; id: string }>
>`SELECT data, id FROM texte_version`.cursor(100)) {
for (const { data, id } of rows) {
const textAFragments = [
data.META.META_SPEC.META_TEXTE_VERSION.TITRE,
data.META.META_SPEC.META_TEXTE_VERSION.TITREFULL,
]
// const vector = (await db<{vector: SerializableParameter}[]>`
// SELECT
// setweight(to_tsvector('french', ${textAFragments.join(" ")}), 'A')
// AS vector
// `)[0].vector
await db`
UPDATE texte_version
SET text_search = setweight(to_tsvector('french', ${textAFragments.join(
" ",
)}), 'A')
WHERE id = ${id}
`
}
}
await db`
ALTER TABLE texte_version
ALTER COLUMN text_search SET NOT NULL
`
}
// Add indexes once every table and column exists.
await db`
CREATE INDEX IF NOT EXISTS texte_version_nature_key
ON texte_version (nature)
`
// await db`
// CREATE INDEX IF NOT EXISTS article_autocompletions_trigrams_idx
// ON article_autocompletions

View file

@ -14,7 +14,7 @@ export const db = postgres({
port: config.db.port,
user: config.db.user,
})
export const versionNumber = 2
export const versionNumber = 3
/// Check that database exists and is up to date.
export async function checkDb(): Promise<void> {

13
src/lib/server/sql.ts Normal file
View file

@ -0,0 +1,13 @@
import type { PendingQuery, Row } from "postgres"
import { db } from "$lib/server/database"
export function joinSqlClauses(
joiner: PendingQuery<Row[]>,
clauses: Array<PendingQuery<Row[]>>,
): PendingQuery<Row[]> {
return clauses.reduce(
(acc, clause, index) => db`${acc} ${index > 0 ? joiner : db``} ${clause}`,
db``,
)
}

37
src/lib/urls.ts Normal file
View file

@ -0,0 +1,37 @@
export function urlFromUrlAndQuery(
urlOrPathname: string,
query: {
[key: string]:
| boolean
| number
| string
| undefined
| null
| Iterable<boolean | number | string | undefined | null>
},
) {
const search = new URLSearchParams(
Object.entries(query).reduce((couples, [key, value]) => {
if (value != null) {
if (
typeof value !== "string" &&
typeof (
value as Iterable<boolean | number | string | undefined | null>
)[Symbol.iterator] === "function"
) {
for (const item of value as Iterable<
boolean | number | string | undefined | null
>) {
if (item != null) {
couples.push([key, item.toString()])
}
}
} else {
couples.push([key, value.toString()])
}
}
return couples
}, [] as Array<[string, string]>),
).toString()
return search ? `${urlOrPathname}?${search}` : urlOrPathname
}

View file

@ -1,5 +1,11 @@
import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core"
import {
type Audit,
auditSetNullish,
auditTrimString,
cleanAudit,
} from "@auditors/core"
import { error } from "@sveltejs/kit"
import type { PendingQuery, Row } from "postgres"
import type { Follow } from "$lib/aggregates"
import {
@ -7,10 +13,12 @@ import {
auditLimitSearchParam,
auditOffsetSearchParam,
auditQSearchParam,
auditSingleton,
} from "$lib/auditors/search_params"
import type { TexteVersion } from "$lib/legal"
import { Aggregator } from "$lib/server/aggregates"
import { db } from "$lib/server/database"
import { joinSqlClauses } from "$lib/server/sql"
import type { RequestHandler } from "./$types"
@ -38,6 +46,14 @@ export function auditSearchParams(
auditFollowSearchParams(audit, data, errors, remainingKeys)
auditLimitSearchParam(audit, data, errors, remainingKeys)
audit.attribute(
data,
"nature",
true,
errors,
remainingKeys,
auditSingleton(auditTrimString),
)
auditOffsetSearchParam(audit, data, errors, remainingKeys)
auditQSearchParam(audit, data, errors, remainingKeys)
@ -54,6 +70,7 @@ export const GET: RequestHandler = async ({ url }) => {
limit: number
offset: number
q?: string
nature?: string
},
unknown,
]
@ -67,10 +84,44 @@ export const GET: RequestHandler = async ({ url }) => {
)
throw error(400, JSON.stringify(queryError, null, 2))
}
const { follow, limit, offset, q } = query
const { follow, limit, nature, offset, q } = query
const joinClauses: Array<PendingQuery<Row[]>> = []
const orderByClauses: Array<PendingQuery<Row[]>> = []
const selectClauses: Array<PendingQuery<Row[]>> = [db`data`]
const whereClauses: Array<PendingQuery<Row[]>> = []
if (nature !== undefined) {
whereClauses.push(db`nature = ${nature}`)
}
if (q !== undefined) {
joinClauses.push(db`
CROSS JOIN (
SELECT plainto_tsquery('french', ${q}) AS query
) AS constants
`)
orderByClauses.push(db`rank DESC`)
selectClauses.push(db`ts_rank_cd(text_search, query) AS rank`)
whereClauses.push(db`query @@ text_search`)
}
const joinClause = joinSqlClauses(db``, joinClauses)
const orderByClause =
orderByClauses.length > 0
? db`ORDER BY ${joinSqlClauses(db`,`, orderByClauses)}`
: db``
const selectClause = joinSqlClauses(db`,`, selectClauses)
const whereClause =
whereClauses.length > 0
? db`WHERE ${joinSqlClauses(db`AND`, whereClauses)}`
: db``
const texteVersionArray = (
await db<{ data: TexteVersion }[]>`
SELECT data FROM texte_version
SELECT ${db`${selectClause}`} FROM texte_version
${db`${joinClause}`}
${db`${whereClause}`}
${db`${orderByClause}`}
OFFSET ${offset}
LIMIT ${limit}
`
@ -91,6 +142,7 @@ export const GET: RequestHandler = async ({ url }) => {
(texteVersion) => texteVersion.META.META_COMMUN.ID,
),
limit,
nature,
offset,
q,
},

View file

@ -0,0 +1,25 @@
import { db } from "$lib/server/database"
import type { RequestHandler } from "./$types"
export const GET: RequestHandler = async ({ url }) => {
const natures = (
await db<{ nature: string }[]>`
SELECT distinct nature FROM texte_version
`
).map(({ nature }) => nature)
return new Response(
JSON.stringify(
{
natures,
},
null,
2,
),
{
headers: {
"Content-Type": "application/json; charset=utf-8",
},
},
)
}

View file

@ -4,13 +4,16 @@
import { TreeView } from "augmented-data-viewer"
import { page } from "$app/stores"
import Pagination from "$lib/components/Pagination.svelte"
// import Pagination from "$lib/components/Pagination.svelte"
import { summarizeArticleProperties } from "$lib/summaries"
import type { PageData } from "./$types"
export let data: PageData
let { articles, q } = data
$: id = data.id
$: q = data.q
$: article = id === undefined ? undefined : data.article?.[id]
</script>
<header class="prose my-6 max-w-full">
@ -34,7 +37,7 @@
</form>
{#if q !== undefined}
{#if articles === undefined}
{#if article === undefined}
<p>Aucun article trouvé</p>
{:else}
<TreeView
@ -42,9 +45,9 @@
frame={false}
open
summarize={summarizeArticleProperties}
value={articles}
value={article}
/>
<Pagination currentPageCount={articles.length ?? 0} />
<!-- <Pagination currentPageCount={article.length ?? 0} /> -->
{/if}
{/if}

View file

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

View file

@ -3,7 +3,7 @@
// import searchIcon from "@iconify-icons/codicon/search"
import { TreeView } from "augmented-data-viewer"
// import { page } from "$app/stores"
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"
@ -12,51 +12,80 @@
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(
let nature = data.nature ?? ""
const naturesPromise = (async (): Promise<string[] | undefined> => {
const apiUrl = "/api/textes/natures"
const response = await fetch(apiUrl, {
headers: { Accept: "application/json" },
})
if (!response.ok) {
const text = await response.text()
console.error(
`Error in ${$page.url.pathname} while calling ${apiUrl}:\n${response.status} ${response.statusText}\n\n${text}`,
)
return undefined
}
return (await response.json()).natures
})()
let q = data.q
$: texteVersionById = data.texte_version as { [id: string]: TexteVersion }
$: 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">
<form action={$page.url.pathname} class="mx-auto max-w-sm" method="get">
<div class="form-control">
<div class="input-group">
<label class="label" for="q">
<span class="label-text">Intitulé</span>
</label>
<input
class="input input-bordered"
id="q"
name="q"
placeholder="Code civil…"
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>
<label class="label" for="nature">
<span class="label-text">Nature</span>
</label>
<select
class="select select-bordered"
name="legislature"
bind:value={legislature}
id="nature"
name="nature"
bind:value={nature}
>
<option value="">Toutes</option>
{#each Object.entries(Legislature) as [key, value]}
{#if value !== "*"}
<option {value}>{key}</option>
{#await naturesPromise}
{#if nature !== undefined}
<option selected value={nature}>{nature}</option>
{/if}
{:then natures}
{#if natures !== undefined}
{#each natures as nature1}
{#if nature1}
<option selected={nature1 === nature} value={nature1}
>{nature1}</option
>
{/if}
{/each}
{/if}
{/await}
</select>
</label>
</div>
</form> -->
<button class="btn my-4" type="submit">Rechercher</button>
</form>
<TreeView
access={{ key: "texte_version" }}

View file

@ -5,7 +5,7 @@ import type { ListTextesResult } from "$lib/aggregates"
import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ fetch, url }) => {
const apiUrl = `/api${url.pathname}`
const apiUrl = `/api${url.pathname}${url.search}`
const response = await fetch(apiUrl, {
headers: { Accept: "application/json" },
})

View file

@ -9,12 +9,12 @@
export let data: PageData
const id = data.id!
const texteVersion = data.texte_version![id]
let showArticles = false
let showRawData = false
$: id = data.id!
$: texteVersion = data.texte_version![id]
$: summary = summarizeLegalObject(
{ key: "texte_version" },
"texte_version",

View file

@ -1,11 +1,76 @@
import { error } from "@sveltejs/kit"
import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core"
import type { GetTexteResult } from "$lib/aggregates"
import type { Follow, GetTexteResult } from "$lib/aggregates"
import { auditFollowWithFalseSearchParams } from "$lib/auditors/search_params"
import { urlFromUrlAndQuery } from "$lib/urls"
import type { PageLoad } from "./$types"
export function auditSearchParams(
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))
auditFollowWithFalseSearchParams(audit, data, errors, remainingKeys)
return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({}))
}
export const load: PageLoad = async ({ fetch, url }) => {
const apiUrl = `/api${url.pathname}`
const [query, queryError] = auditSearchParams(
cleanAudit,
url.searchParams,
) as [
{
follow: Set<Follow | "false">
},
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: requestedFollow } = query
let follow = new Set(requestedFollow)
const followFalse = follow.delete("false")
if (follow.size === 0 && !followFalse) {
// Set default follow
follow = new Set([
// "STRUCT.LIEN_ART.@id",
"STRUCT.LIEN_SECTION_TA.@id",
// "STRUCTURE_TA.LIEN_ART.@id",
"STRUCTURE_TA.LIEN_SECTION_TA.@id",
"TEXTEKALI",
"TEXTELR",
])
}
const apiUrl = urlFromUrlAndQuery(`/api${url.pathname}`, { follow })
const response = await fetch(apiUrl, {
headers: { Accept: "application/json" },
})