@tricoteuses/senat 3.1.4 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/lib/src/config.d.ts +43 -0
  2. package/lib/src/config.js +37 -0
  3. package/lib/src/conversion_textes.d.ts +11 -0
  4. package/lib/src/conversion_textes.js +320 -0
  5. package/lib/src/databases_postgres.d.ts +4 -0
  6. package/lib/src/databases_postgres.js +23 -0
  7. package/lib/src/datasets.d.ts +38 -0
  8. package/lib/src/datasets.js +247 -0
  9. package/lib/src/git.d.ts +27 -0
  10. package/lib/src/git.js +251 -0
  11. package/lib/src/loaders.d.ts +52 -0
  12. package/lib/src/loaders.js +260 -0
  13. package/lib/src/model/agenda.d.ts +6 -0
  14. package/lib/src/model/agenda.js +148 -0
  15. package/lib/src/model/ameli.d.ts +67 -0
  16. package/lib/src/model/ameli.js +150 -0
  17. package/lib/src/model/commission.d.ts +19 -0
  18. package/lib/src/model/commission.js +269 -0
  19. package/lib/src/model/debats.d.ts +39 -0
  20. package/lib/src/model/debats.js +112 -0
  21. package/lib/src/model/documents.d.ts +32 -0
  22. package/lib/src/model/documents.js +182 -0
  23. package/lib/src/model/dosleg.d.ts +144 -0
  24. package/lib/src/model/dosleg.js +468 -0
  25. package/lib/src/model/index.d.ts +7 -0
  26. package/lib/src/model/index.js +7 -0
  27. package/lib/src/model/questions.d.ts +54 -0
  28. package/lib/src/model/questions.js +91 -0
  29. package/lib/src/model/scrutins.d.ts +48 -0
  30. package/lib/src/model/scrutins.js +121 -0
  31. package/lib/src/model/seance.d.ts +3 -0
  32. package/lib/src/model/seance.js +267 -0
  33. package/lib/src/model/sens.d.ts +112 -0
  34. package/lib/src/model/sens.js +385 -0
  35. package/lib/src/model/util.d.ts +1 -0
  36. package/lib/src/model/util.js +15 -0
  37. package/lib/src/raw_types/ameli.d.ts +1762 -0
  38. package/lib/src/raw_types/ameli.js +1074 -0
  39. package/lib/src/raw_types/debats.d.ts +380 -0
  40. package/lib/src/raw_types/debats.js +266 -0
  41. package/lib/src/raw_types/dosleg.d.ts +2954 -0
  42. package/lib/src/raw_types/dosleg.js +2005 -0
  43. package/lib/src/raw_types/questions.d.ts +699 -0
  44. package/lib/src/raw_types/questions.js +493 -0
  45. package/lib/src/raw_types/sens.d.ts +7843 -0
  46. package/lib/src/raw_types/sens.js +4691 -0
  47. package/lib/src/raw_types_schemats/ameli.d.ts +541 -0
  48. package/lib/src/raw_types_schemats/ameli.js +2 -0
  49. package/lib/src/raw_types_schemats/debats.d.ts +127 -0
  50. package/lib/src/raw_types_schemats/debats.js +2 -0
  51. package/lib/src/raw_types_schemats/dosleg.d.ts +977 -0
  52. package/lib/src/raw_types_schemats/dosleg.js +2 -0
  53. package/lib/src/raw_types_schemats/questions.d.ts +237 -0
  54. package/lib/src/raw_types_schemats/questions.js +2 -0
  55. package/lib/src/raw_types_schemats/sens.d.ts +2709 -0
  56. package/lib/src/raw_types_schemats/sens.js +2 -0
  57. package/lib/src/scripts/debug_dosleg_query.d.ts +6 -0
  58. package/lib/src/scripts/debug_dosleg_query.js +50 -0
  59. package/lib/src/scripts/retrieve_agenda.js +2 -4
  60. package/lib/src/types/agenda.d.ts +45 -0
  61. package/lib/src/types/agenda.js +1 -0
  62. package/lib/src/types/ameli.d.ts +5 -0
  63. package/lib/src/types/ameli.js +1 -0
  64. package/lib/src/types/compte_rendu.d.ts +83 -0
  65. package/lib/src/types/compte_rendu.js +1 -0
  66. package/lib/src/types/debats.d.ts +2 -0
  67. package/lib/src/types/debats.js +1 -0
  68. package/lib/src/types/dosleg.d.ts +70 -0
  69. package/lib/src/types/dosleg.js +1 -0
  70. package/lib/src/types/questions.d.ts +2 -0
  71. package/lib/src/types/questions.js +1 -0
  72. package/lib/src/types/sens.d.ts +8 -0
  73. package/lib/src/types/sens.js +1 -0
  74. package/lib/src/types/sessions.d.ts +6 -0
  75. package/lib/src/types/sessions.js +19 -0
  76. package/lib/src/types/texte.d.ts +72 -0
  77. package/lib/src/types/texte.js +15 -0
  78. package/lib/src/validators/config.d.ts +9 -0
  79. package/lib/src/validators/config.js +10 -0
  80. package/package.json +1 -1
@@ -0,0 +1,269 @@
1
+ import * as cheerio from "cheerio";
2
+ import path from "path";
3
+ import { makeReunionUid } from "../utils/reunion_parsing.js";
4
+ import { norm } from "../utils/string_cleaning.js";
5
+ import { frDateToISO, hourShortToStartTime } from "../utils/date.js";
6
+ import { toCRDate } from "./util.js";
7
+ const PARA_h3_SEL = "p.sh_justify, p.sh_center, p.sh_marge, p[align], li, h3";
8
+ function findDayRoot($, targetISO) {
9
+ let $root = $();
10
+ $("h2").each((_, el) => {
11
+ const txt = norm($(el).text());
12
+ const m = txt.match(/(?:Lundi|Mardi|Mercredi|Jeudi|Vendredi|Samedi|Dimanche)\s+(.+)$/i);
13
+ const iso = m ? frDateToISO(m[1]) : undefined;
14
+ if (iso === targetISO && $root.length === 0)
15
+ $root = $(el);
16
+ });
17
+ return $root;
18
+ }
19
+ function normalizeSpaces(s) {
20
+ return s.replace(/[\u00A0\u202F\u2009]/g, " ");
21
+ }
22
+ function stripIntroPunct(s) {
23
+ return s.replace(/^[\s]*[.:;]?\s*(?:[–—-]\s*)+/u, "");
24
+ }
25
+ function collectLeadingHeaderStrongEls($, $clone) {
26
+ const els = [];
27
+ const nodes = $clone.contents().toArray();
28
+ for (const node of nodes) {
29
+ if (node.type === "text") {
30
+ if (norm(node.data || ""))
31
+ break;
32
+ continue;
33
+ }
34
+ if (node.type === "tag") {
35
+ const $n = $(node);
36
+ if ($n.is("strong, b")) {
37
+ els.push(node);
38
+ continue;
39
+ }
40
+ if ($n.is("a") && $n.children("strong, b").length) {
41
+ $n.children("strong, b").each((_, el) => {
42
+ els.push(el);
43
+ });
44
+ continue;
45
+ }
46
+ break;
47
+ }
48
+ }
49
+ return els;
50
+ }
51
+ // Remove orateur's name from text and clean intro punct
52
+ export function getRemainingTextAfterSpeakerHeader($, $p) {
53
+ const $clone = $p.clone();
54
+ // 1) Remove <strong> at start
55
+ const headerStrongEls = collectLeadingHeaderStrongEls($, $clone);
56
+ for (const el of headerStrongEls)
57
+ $(el).remove();
58
+ // 2) normalize + clean intro punct
59
+ let remainingHtml = $clone.html() || "";
60
+ remainingHtml = normalizeSpaces(cheerio.load(remainingHtml).text());
61
+ remainingHtml = stripIntroPunct(remainingHtml);
62
+ const remainingText = norm(remainingHtml || "");
63
+ return remainingText;
64
+ }
65
+ function buildPointsFromParagraphs($, paras) {
66
+ const points = [];
67
+ let ordreAbsoluSeance = 0;
68
+ const normSpeaker = (s) => s
69
+ .normalize("NFKC")
70
+ .replace(/\s+/g, " ")
71
+ .replace(/[.:]\s*$/, "")
72
+ .trim();
73
+ const normQual = (s) => s
74
+ .normalize("NFKC")
75
+ .replace(/\s+/g, " ")
76
+ .replace(/^\s*,\s*|\s+$/g, "")
77
+ .replace(/[\s\u00A0]*[.,;:–—-]+$/u, "")
78
+ .trim();
79
+ let currentOrateur = null;
80
+ let currentQualite = "";
81
+ let currentTexte = "";
82
+ function isPresidentQual(qual) {
83
+ return /\bprésident(e)?\b/i.test(qual);
84
+ }
85
+ // Flush the buffered speaker’s text into points[] if any.
86
+ function flush() {
87
+ if (!currentOrateur || !currentTexte.trim())
88
+ return;
89
+ ordreAbsoluSeance++;
90
+ points.push({
91
+ code_grammaire: "PAROLE_GENERIQUE",
92
+ roledebat: isPresidentQual(currentQualite) ? "président" : "",
93
+ ordre_absolu_seance: String(ordreAbsoluSeance),
94
+ orateurs: { orateur: { nom: currentOrateur, id: "", qualite: currentQualite || "" } },
95
+ texte: { _: currentTexte.trim() },
96
+ });
97
+ currentOrateur = null;
98
+ currentQualite = "";
99
+ currentTexte = "";
100
+ }
101
+ function addPoint(payload) {
102
+ ordreAbsoluSeance++;
103
+ points.push({ ...payload, ordre_absolu_seance: String(ordreAbsoluSeance) });
104
+ }
105
+ for (const $p of paras) {
106
+ if ($p.closest("table").length)
107
+ continue;
108
+ const tagName = ($p.prop("tagName") || "").toString().toLowerCase();
109
+ const rawText = ($p.text() || "").replace(/\u00a0/g, " ").trim();
110
+ const text = norm(rawText);
111
+ if (!text || text.length <= 3)
112
+ continue;
113
+ const html = ($p.html() || "").trim();
114
+ const italicSpans = $p.find("i, em, span[style*='italic']");
115
+ const firstItalicOuter = italicSpans.length ? $(italicSpans[0]).prop("outerHTML") || "" : "";
116
+ const htmlBeforeFirstItalic = firstItalicOuter ? html.split(firstItalicOuter)[0].trim() : "";
117
+ const isPureItalic = italicSpans.length > 0 && italicSpans.length === $p.find("span,i,em").length && htmlBeforeFirstItalic === "";
118
+ if (tagName === "h3") {
119
+ flush();
120
+ addPoint({
121
+ code_style: "Titre",
122
+ code_grammaire: "TITRE_TEXTE_DISCUSSION",
123
+ texte: { _: text },
124
+ });
125
+ continue;
126
+ }
127
+ const boldSpans = $p.find("strong, b");
128
+ const joinedBold = norm(boldSpans
129
+ .map((_, el) => $(el).text() || "")
130
+ .get()
131
+ .join(""));
132
+ const [namePartRaw, qualPartRaw] = joinedBold.split(/\s*,\s+/, 2);
133
+ const namePart = namePartRaw ? normSpeaker(namePartRaw) : "";
134
+ const qualPart = qualPartRaw ? normQual(qualPartRaw) : "";
135
+ const looksLikeName = namePart.length > 3 && /^(M\.|Mme)[\s\u00A0\u202F]+/i.test(namePart);
136
+ const startsWithName = namePart && text.startsWith(namePart);
137
+ const isNewSpeaker = looksLikeName && startsWithName && namePart !== currentOrateur;
138
+ if (isNewSpeaker) {
139
+ flush();
140
+ currentOrateur = namePart;
141
+ currentQualite = qualPart;
142
+ const remainingText = getRemainingTextAfterSpeakerHeader($, $p);
143
+ currentTexte = remainingText;
144
+ continue;
145
+ }
146
+ if (isPureItalic || (!joinedBold && !currentOrateur && text)) {
147
+ flush();
148
+ addPoint({
149
+ code_style: "Info Italiques",
150
+ code_grammaire: "PAROLE_GENERIQUE",
151
+ texte: { _: "<i>" + text + "</i>" },
152
+ });
153
+ continue;
154
+ }
155
+ // concat text because same orateur
156
+ if (currentOrateur) {
157
+ const removeOrateurFromText = getRemainingTextAfterSpeakerHeader($, $p);
158
+ currentTexte += (currentTexte ? "<br/><br/>" : "") + removeOrateurFromText;
159
+ continue;
160
+ }
161
+ }
162
+ flush();
163
+ return points;
164
+ }
165
+ const TIME_RE = /(?:\b[àa]\s*)?(\d{1,2})\s*(?:h|heures?)\s*(?:([0-5]\d))?/i;
166
+ export function cleanTitle(t) {
167
+ return (t || "").replace(/\s+/g, " ").trim();
168
+ }
169
+ function parseTimeToHHmm(text) {
170
+ const m = normalizeSpaces(text).match(TIME_RE);
171
+ if (!m)
172
+ return undefined;
173
+ const hh = m[1]?.padStart(2, "0");
174
+ const mm = (m[2] ?? "00").padStart(2, "0");
175
+ const h = Number(hh);
176
+ if (h >= 0 && h <= 23)
177
+ return `${hh}:${mm}`;
178
+ return undefined;
179
+ }
180
+ function findNearbyTime($, $h3) {
181
+ let cur = $h3.prev();
182
+ for (let i = 0; i < 3 && cur.length; i++, cur = cur.prev()) {
183
+ const direct = parseTimeToHHmm(cur.text());
184
+ if (direct)
185
+ return direct;
186
+ const italic = parseTimeToHHmm(cur.find("i, em").first().text());
187
+ if (italic)
188
+ return italic;
189
+ }
190
+ return undefined;
191
+ }
192
+ export function extractDayH3Sections($, dateISO) {
193
+ const sections = [];
194
+ const $dayRoot = findDayRoot($, dateISO);
195
+ if ($dayRoot.length === 0)
196
+ return sections;
197
+ const $range = $dayRoot.nextUntil("h2");
198
+ const $h3s = $range.filter("h3").add($range.find("h3"));
199
+ $h3s.each((_, el) => {
200
+ const $h3 = $(el);
201
+ const title = cleanTitle($h3.text());
202
+ if (!title)
203
+ return;
204
+ const time = findNearbyTime($, $h3);
205
+ sections.push({ title, $start: $h3, time });
206
+ });
207
+ return sections;
208
+ }
209
+ export function parseCommissionCRSectionFromDom($, htmlFilePath, opts) {
210
+ try {
211
+ const { dateISO, hourShort, organe, section, matched } = opts;
212
+ const seanceRef = matched?.uid ?? makeReunionUid(dateISO, "COM", matched?.events[0].id ?? hourShort ?? "", organe ?? undefined);
213
+ const uid = seanceRef.replace(/^RU/, "CRC");
214
+ const dateSeance = toCRDate(dateISO, matched?.startTime ?? hourShortToStartTime(hourShort));
215
+ const $dayRoot = findDayRoot($, dateISO);
216
+ if ($dayRoot.length === 0) {
217
+ console.warn(`[COM-CR][parse] day root not found for ${dateISO} in ${path.basename(htmlFilePath)}`);
218
+ return null;
219
+ }
220
+ const paras = [];
221
+ let $cursor = section.$start;
222
+ // Jump title if we do not want to add it to paragraphes
223
+ $cursor = $cursor.next();
224
+ while ($cursor.length && !$cursor.is("h2") && !$cursor.is("h3")) {
225
+ if ($cursor.is(PARA_h3_SEL)) {
226
+ paras.push($cursor);
227
+ }
228
+ else {
229
+ const $ps = $cursor.find(PARA_h3_SEL);
230
+ if ($ps.length)
231
+ $ps.each((_, p) => {
232
+ paras.push($(p));
233
+ });
234
+ }
235
+ $cursor = $cursor.next();
236
+ }
237
+ const points = buildPointsFromParagraphs($, paras);
238
+ if (points.length < 4 || !points.some((pt) => pt.code_grammaire === "PAROLE_GENERIQUE" && pt.orateurs)) {
239
+ console.warn(`[COM-CR][parse] Insufficient points or no interventions found for a section in ${path.basename(htmlFilePath)}`);
240
+ return null;
241
+ }
242
+ const session = dateISO.slice(5, 7) >= "10" ? `${dateISO.slice(0, 4)}` : `${Number(dateISO.slice(0, 4)) - 1}`;
243
+ const contenu = {
244
+ quantiemes: { journee: dateISO, session },
245
+ point: points,
246
+ };
247
+ const metadonnees = {
248
+ dateSeance,
249
+ dateSeanceJour: dateISO,
250
+ numSeanceJour: "",
251
+ numSeance: "",
252
+ typeAssemblee: "SN",
253
+ legislature: "",
254
+ session,
255
+ nomFichierJo: path.basename(htmlFilePath),
256
+ validite: "non-certifie",
257
+ etat: "definitif",
258
+ diffusion: "publique",
259
+ version: "1",
260
+ environnement: "prod",
261
+ heureGeneration: new Date(),
262
+ };
263
+ return { uid, seanceRef, sessionRef: session, metadonnees, contenu };
264
+ }
265
+ catch (e) {
266
+ console.error(`[COM-CR][parse] error section file=${path.basename(htmlFilePath)}:`, e);
267
+ return null;
268
+ }
269
+ }
@@ -0,0 +1,39 @@
1
+ export interface DebatAuteurRow {
2
+ code: string | null;
3
+ matricule: string | null;
4
+ nom: string | null;
5
+ prenom: string | null;
6
+ }
7
+ export interface DebatInterventionRow {
8
+ analyse: string | null;
9
+ auteur: DebatAuteurRow | null;
10
+ auteur_code: string;
11
+ fonction_intervenant: string | null;
12
+ id: string | null;
13
+ url: string | null;
14
+ }
15
+ export interface DebatSectionRow {
16
+ categorie: string | null;
17
+ id?: string | null;
18
+ interventions: DebatInterventionRow[];
19
+ lecture_id?: string | null;
20
+ libelle?: string | null;
21
+ numero?: string | null;
22
+ objet: string | null;
23
+ type: string | null;
24
+ url?: string | null;
25
+ }
26
+ export interface DebatLectureRow {
27
+ id: string;
28
+ }
29
+ export interface DebatResult {
30
+ date_seance: string | null;
31
+ etat_synchronisation: string | null;
32
+ id: string | null;
33
+ lectures: DebatLectureRow[];
34
+ numero: string | null;
35
+ sections: DebatSectionRow[];
36
+ sections_divers: DebatSectionRow[];
37
+ url: string | null;
38
+ }
39
+ export declare function findAll(): AsyncGenerator<DebatResult, void, unknown>;
@@ -0,0 +1,112 @@
1
+ import { streamUnsafeQuery } from "../databases_postgres.js";
2
+ function buildFindAllDebatsQuery() {
3
+ return {
4
+ params: [],
5
+ query: `
6
+ select
7
+ to_char(debats.datsea, 'YYYYMMDD') as id,
8
+ to_char(debats.datsea, 'YYYY-MM-DD') as date_seance,
9
+ debats.numero::text as numero,
10
+ debats.deburl as url,
11
+ debats.debsyn as etat_synchronisation,
12
+ (
13
+ select coalesce(json_agg(section_rows order by section_rows.section_order nulls last), '[]'::json)
14
+ from (
15
+ select
16
+ secdis.secdisordid::text as id,
17
+ secdis.secdisnum as numero,
18
+ secdis.secdisobj as objet,
19
+ secdis.secdisurl as url,
20
+ typsec.typseclib as type,
21
+ typsec.typseccat as categorie,
22
+ secdis.lecassidt as lecture_id,
23
+ secdis.secdisordid as section_order,
24
+ (
25
+ select coalesce(
26
+ json_agg(intervention_rows order by intervention_rows.intervention_order nulls last),
27
+ '[]'::json
28
+ )
29
+ from (
30
+ select
31
+ intpjl.intordid::text as id,
32
+ intpjl.autcod as auteur_code,
33
+ intpjl.intfon as fonction_intervenant,
34
+ intpjl.inturl as url,
35
+ intpjl.intana as analyse,
36
+ json_build_object(
37
+ 'code', auteur.autcod,
38
+ 'nom', auteur.nomuse,
39
+ 'prenom', auteur.prenom,
40
+ 'matricule', auteur.autmat
41
+ ) as auteur,
42
+ intpjl.intordid as intervention_order
43
+ from senat.debats_intpjl as intpjl
44
+ left join senat.dosleg_auteur as auteur on intpjl.autcod = auteur.autcod
45
+ where intpjl.secdiscle = secdis.secdiscle
46
+ ) as intervention_rows
47
+ ) as interventions
48
+ from senat.debats_secdis as secdis
49
+ left join senat.debats_typsec as typsec on secdis.typseccod = typsec.typseccod
50
+ where secdis.datsea = debats.datsea
51
+ ) as section_rows
52
+ ) as sections,
53
+ (
54
+ select coalesce(json_agg(section_rows), '[]'::json)
55
+ from (
56
+ select
57
+ secdivers.secdiverslibelle as libelle,
58
+ secdivers.secdiversobj as objet,
59
+ typsec.typseclib as type,
60
+ typsec.typseccat as categorie,
61
+ (
62
+ select coalesce(
63
+ json_agg(intervention_rows order by intervention_rows.intervention_order nulls last),
64
+ '[]'::json
65
+ )
66
+ from (
67
+ select
68
+ intdivers.intdiversordid::text as id,
69
+ intdivers.autcod as auteur_code,
70
+ intdivers.intfon as fonction_intervenant,
71
+ intdivers.inturl as url,
72
+ intdivers.intana as analyse,
73
+ json_build_object(
74
+ 'code', auteur.autcod,
75
+ 'nom', auteur.nomuse,
76
+ 'prenom', auteur.prenom,
77
+ 'matricule', auteur.autmat
78
+ ) as auteur,
79
+ intdivers.intdiversordid as intervention_order
80
+ from senat.debats_intdivers as intdivers
81
+ left join senat.dosleg_auteur as auteur on intdivers.autcod = auteur.autcod
82
+ where intdivers.secdiverscle = secdivers.secdiverscle
83
+ ) as intervention_rows
84
+ ) as interventions
85
+ from senat.debats_secdivers as secdivers
86
+ left join senat.debats_typsec as typsec on secdivers.typseccod = typsec.typseccod
87
+ where secdivers.datsea = debats.datsea
88
+ ) as section_rows
89
+ ) as sections_divers,
90
+ (
91
+ select coalesce(json_agg(lecture_rows), '[]'::json)
92
+ from (
93
+ select lecassdeb.lecassidt as id
94
+ from senat.debats_lecassdeb as lecassdeb
95
+ where lecassdeb.datsea = debats.datsea
96
+ ) as lecture_rows
97
+ ) as lectures
98
+ from senat.debats_debats as debats
99
+ `,
100
+ };
101
+ }
102
+ export async function* findAll() {
103
+ const { query, params } = buildFindAllDebatsQuery();
104
+ for await (const row of streamUnsafeQuery(query, params)) {
105
+ yield {
106
+ ...row,
107
+ lectures: row.lectures ?? [],
108
+ sections: row.sections ?? [],
109
+ sections_divers: row.sections_divers ?? [],
110
+ };
111
+ }
112
+ }
@@ -0,0 +1,32 @@
1
+ export interface AuteurDocumentRow {
2
+ matricule: string | null;
3
+ nom_usuel: string;
4
+ ordre: string | null;
5
+ prenom: string | null;
6
+ qualite: string | null;
7
+ role: string | null;
8
+ }
9
+ export interface DocumentAnnexeRow {
10
+ type_document: string;
11
+ url: string | null;
12
+ }
13
+ export interface DocumentResult {
14
+ auteurs: AuteurDocumentRow[];
15
+ code_adoption?: string | null;
16
+ code_organisme: string | null;
17
+ date: string;
18
+ documents_annexes?: DocumentAnnexeRow[];
19
+ id: string | null;
20
+ modification?: string | null;
21
+ numero: number | string | null;
22
+ ordre_origine?: string | null;
23
+ origine?: string | null;
24
+ session: number | null;
25
+ signet_dossier?: string | null;
26
+ sous_titre?: string | null;
27
+ titre: string | null;
28
+ type: string | null;
29
+ url: string | null;
30
+ }
31
+ export declare function findAllTextes(): AsyncGenerator<DocumentResult, void, unknown>;
32
+ export declare function findAllRapports(): AsyncGenerator<DocumentResult, void, unknown>;
@@ -0,0 +1,182 @@
1
+ import { streamUnsafeQuery } from "../databases_postgres.js";
2
+ function buildAuteursRapportSql(rapportIdSql) {
3
+ return `
4
+ (
5
+ select coalesce(json_agg(author_rows order by author_rows.ordre_num nulls last), '[]'::json)
6
+ from (
7
+ select
8
+ auteur.prenom as prenom,
9
+ auteur.nomuse as nom_usuel,
10
+ auteur.autmat as matricule,
11
+ ecr.ecrnumtri::text as ordre,
12
+ ecr.ecrnumtri as ordre_num,
13
+ rolsig.rolsiglib as role,
14
+ ecr.ecrqua as qualite
15
+ from senat.dosleg_auteur as auteur
16
+ left join senat.dosleg_ecr as ecr on ecr.autcod = auteur.autcod
17
+ left join senat.dosleg_rolsig as rolsig on rolsig.signataire = ecr.signataire
18
+ where ecr.rapcod = ${rapportIdSql}
19
+ ) as author_rows
20
+ )
21
+ `;
22
+ }
23
+ function buildDocumentsAttachesSql(rapportIdSql) {
24
+ return `
25
+ (
26
+ select coalesce(json_agg(attached_rows), '[]'::json)
27
+ from (
28
+ select
29
+ docatt.docatturl as url,
30
+ typatt.typattlib as type_document
31
+ from senat.dosleg_docatt as docatt
32
+ left join senat.dosleg_typatt as typatt on docatt.typattcod = typatt.typattcod
33
+ where docatt.rapcod = ${rapportIdSql}
34
+ ) as attached_rows
35
+ )
36
+ `;
37
+ }
38
+ function buildAuteursTexteSql(texteIdSql) {
39
+ return `
40
+ (
41
+ select coalesce(json_agg(author_rows order by author_rows.ordre_num nulls last), '[]'::json)
42
+ from (
43
+ select
44
+ auteur.prenom as prenom,
45
+ auteur.nomuse as nom_usuel,
46
+ auteur.autmat as matricule,
47
+ ecr.ecrnumtri::text as ordre,
48
+ ecr.ecrnumtri as ordre_num,
49
+ rolsig.rolsiglib as role,
50
+ ecr.ecrqua as qualite
51
+ from senat.dosleg_auteur as auteur
52
+ left join senat.dosleg_ecr as ecr on ecr.autcod = auteur.autcod
53
+ left join senat.dosleg_rolsig as rolsig on rolsig.signataire = ecr.signataire
54
+ where ecr.texcod = ${texteIdSql}
55
+ ) as author_rows
56
+ )
57
+ `;
58
+ }
59
+ function stripTrailingHashesSql(expr) {
60
+ return `regexp_replace(${expr}, '#+$', '')`;
61
+ }
62
+ function rapportIdSql() {
63
+ return `
64
+ case
65
+ when rap.rapurl is not null then
66
+ ${stripTrailingHashesSql("regexp_replace(trim(rap.rapurl), '^(.*/)?(.*?)(\\.html)?$', '\\2')")}
67
+ else null
68
+ end
69
+ `;
70
+ }
71
+ function rapportUrlSql() {
72
+ return `
73
+ case
74
+ when rap.typurl = 'I' then ${stripTrailingHashesSql("'https://www.senat.fr/rap/' || rtrim(rap.rapurl)")}
75
+ else ${stripTrailingHashesSql("rtrim(rap.rapurl)")}
76
+ end
77
+ `;
78
+ }
79
+ function texteIdSql() {
80
+ return `
81
+ case
82
+ when texte.texurl is not null then
83
+ ${stripTrailingHashesSql("regexp_replace(trim(texte.texurl), '^(.*/)?(.*?)(\\.html)?$', '\\2')")}
84
+ else null
85
+ end
86
+ `;
87
+ }
88
+ function texteUrlSql() {
89
+ return `
90
+ case
91
+ when texte.typurl = 'I' then ${stripTrailingHashesSql("'https://www.senat.fr/leg/' || rtrim(texte.texurl)")}
92
+ else ${stripTrailingHashesSql("rtrim(texte.texurl)")}
93
+ end
94
+ `;
95
+ }
96
+ function baseRapportsSelect(includeSignetDossier) {
97
+ const signet = includeSignetDossier ? "loi.signet as signet_dossier," : "";
98
+ return `
99
+ select
100
+ ${signet}
101
+ rap.rapnum as numero,
102
+ raporg.orgcod as code_organisme,
103
+ ${rapportIdSql()} as id,
104
+ ${rapportUrlSql()} as url,
105
+ rtrim(denrap.libdenrap) as type,
106
+ rtrim(rtrim(rap.raptil)) as titre,
107
+ rtrim(rtrim(rap.rapsoustit)) as sous_titre,
108
+ to_char(rap.date_depot, 'YYYY-MM-DD') as date,
109
+ rap.sesann::int as session,
110
+ ${buildAuteursRapportSql("rap.rapcod")} as auteurs,
111
+ ${buildDocumentsAttachesSql("rap.rapcod")} as documents_annexes
112
+ `;
113
+ }
114
+ function baseTextesSelect(includeSignetDossier) {
115
+ const signet = includeSignetDossier ? "loi.signet as signet_dossier," : "";
116
+ return `
117
+ select
118
+ ${signet}
119
+ texte.texnum as numero,
120
+ texte.orgcod as code_organisme,
121
+ ${texteIdSql()} as id,
122
+ ${texteUrlSql()} as url,
123
+ rtrim(oritxt.oritxtlib) as origine,
124
+ oritxt.oriordre as ordre_origine,
125
+ oritxt.oritxtado as code_adoption,
126
+ oritxt.oritxtmod as modification,
127
+ rtrim(typtxt.typtxtlib) as type,
128
+ to_char(texte.txtoritxtdat, 'YYYY-MM-DD') as date,
129
+ texte.sesann::int as session,
130
+ ${buildAuteursTexteSql("texte.texcod")} as auteurs,
131
+ null::text as titre
132
+ `;
133
+ }
134
+ function buildFindAllRapportsQuery() {
135
+ return {
136
+ params: [],
137
+ query: `
138
+ ${baseRapportsSelect(true)}
139
+ from senat.dosleg_rap as rap
140
+ left join senat.dosleg_raporg as raporg on raporg.rapcod = rap.rapcod
141
+ left join senat.dosleg_denrap as denrap on denrap.coddenrap = rap.coddenrap
142
+ left join senat.dosleg_lecassrap as lecassrap on lecassrap.rapcod = rap.rapcod
143
+ left join senat.dosleg_lecass as lecass on lecass.lecassidt = lecassrap.lecassidt
144
+ left join senat.dosleg_lecture as lecture on lecture.lecidt = lecass.lecidt
145
+ left join senat.dosleg_loi as loi on loi.loicod = lecture.loicod
146
+ `,
147
+ };
148
+ }
149
+ function buildFindAllTextesQuery() {
150
+ return {
151
+ params: [],
152
+ query: `
153
+ ${baseTextesSelect(true)}
154
+ from senat.dosleg_texte as texte
155
+ left join senat.dosleg_oritxt as oritxt on oritxt.oritxtcod = texte.oritxtcod
156
+ left join senat.dosleg_typtxt as typtxt on typtxt.typtxtcod = texte.typtxtcod
157
+ left join senat.dosleg_lecass as lecass on lecass.lecassidt = texte.lecassidt
158
+ left join senat.dosleg_lecture as lecture on lecture.lecidt = lecass.lecidt
159
+ left join senat.dosleg_loi as loi on loi.loicod = lecture.loicod
160
+ order by array_position(array['0','2','1'], oritxt.oriordre)
161
+ `,
162
+ };
163
+ }
164
+ export async function* findAllTextes() {
165
+ const { query, params } = buildFindAllTextesQuery();
166
+ for await (const row of streamUnsafeQuery(query, params)) {
167
+ yield {
168
+ ...row,
169
+ auteurs: row.auteurs ?? [],
170
+ };
171
+ }
172
+ }
173
+ export async function* findAllRapports() {
174
+ const { query, params } = buildFindAllRapportsQuery();
175
+ for await (const row of streamUnsafeQuery(query, params)) {
176
+ yield {
177
+ ...row,
178
+ auteurs: row.auteurs ?? [],
179
+ documents_annexes: row.documents_annexes ?? [],
180
+ };
181
+ }
182
+ }