diff --git a/package-lock.json b/package-lock.json index 7798370..7e32f25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "@iconify/svelte": "^2.2.1", "@playwright/test": "^1.22.2", "@sveltejs/adapter-node": "^1.0.0-next.86", - "@sveltejs/kit": "^1.0.0-next.428", + "@sveltejs/kit": "^1.0.0-next.430", "@sveltejs/package": "^1.0.0-next.1", "@tailwindcss/typography": "^0.5.3", - "@tricoteuses/explorer-tools": "^0.1.9", + "@tricoteuses/explorer-tools": "^0.1.12", "@types/fs-extra": "^9.0.13", "@types/he": "^1.1.2", "@typescript-eslint/eslint-plugin": "^5.27.0", @@ -32,6 +32,7 @@ "fast-xml-parser": "^4.0.9", "fs-extra": "^10.1.0", "he": "^1.2.0", + "morgan": "^1.10.0", "postcss": "^8.4.14", "postcss-load-config": "^4.0.1", "postgres": "^3.2.4", @@ -250,9 +251,9 @@ "dev": true }, "node_modules/@sveltejs/kit": { - "version": "1.0.0-next.428", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.428.tgz", - "integrity": "sha512-CVp7zltQ+3VOII1s7KuvgvGEFwD0PSqts9R3q4eaj0CNoC3gpdzcbEZayyQwXX/3lI1HdSNSF8gxbX0TRUoFuA==", + "version": "1.0.0-next.430", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.430.tgz", + "integrity": "sha512-0NL6DfFWASDkvjL6iCr3pO/u1oqvoF75lvnO6QTB0rIHMYOL+l9BAkxVETnNxzvgLRFCECi6NSh2C0bdaYaM1g==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -343,9 +344,9 @@ } }, "node_modules/@tricoteuses/explorer-tools": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.9.tgz", - "integrity": "sha512-qMeJdl4GXNShCHnCRZGLogmAie/TFEEC6BiKBasK4NqewWIyFSCmO8tWKRaDdXU7US641lS0ZRHFZwcVWkO6mA==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.12.tgz", + "integrity": "sha512-Aachs9YOz1R624H3gl1WuMkdbqhBPmjKIogfO3iukjVH4MBZ7g1jN2a8q4mRj0c9v82fwiKKBdcCz0FLi4S8Cw==", "dev": true, "dependencies": { "@iconify-icons/codicon": "^1.2.15", @@ -755,6 +756,18 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1073,6 +1086,15 @@ "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", "dev": true }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -1150,6 +1172,12 @@ "node": ">=12" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.225", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.225.tgz", @@ -2444,6 +2472,37 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2566,6 +2625,27 @@ "node": ">= 6" } }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3083,6 +3163,12 @@ "node": ">=6" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -3926,9 +4012,9 @@ "dev": true }, "@sveltejs/kit": { - "version": "1.0.0-next.428", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.428.tgz", - "integrity": "sha512-CVp7zltQ+3VOII1s7KuvgvGEFwD0PSqts9R3q4eaj0CNoC3gpdzcbEZayyQwXX/3lI1HdSNSF8gxbX0TRUoFuA==", + "version": "1.0.0-next.430", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.430.tgz", + "integrity": "sha512-0NL6DfFWASDkvjL6iCr3pO/u1oqvoF75lvnO6QTB0rIHMYOL+l9BAkxVETnNxzvgLRFCECi6NSh2C0bdaYaM1g==", "dev": true, "requires": { "@sveltejs/vite-plugin-svelte": "^1.0.1", @@ -3983,9 +4069,9 @@ } }, "@tricoteuses/explorer-tools": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.9.tgz", - "integrity": "sha512-qMeJdl4GXNShCHnCRZGLogmAie/TFEEC6BiKBasK4NqewWIyFSCmO8tWKRaDdXU7US641lS0ZRHFZwcVWkO6mA==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@tricoteuses/explorer-tools/-/explorer-tools-0.1.12.tgz", + "integrity": "sha512-Aachs9YOz1R624H3gl1WuMkdbqhBPmjKIogfO3iukjVH4MBZ7g1jN2a8q4mRj0c9v82fwiKKBdcCz0FLi4S8Cw==", "dev": true, "requires": { "@iconify-icons/codicon": "^1.2.15", @@ -4253,6 +4339,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4476,6 +4571,12 @@ "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, "detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -4535,6 +4636,12 @@ "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", "dev": true }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "electron-to-chromium": { "version": "1.4.225", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.225.tgz", @@ -5413,6 +5520,36 @@ "minimist": "^1.2.6" } }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -5494,6 +5631,21 @@ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5807,6 +5959,12 @@ "mri": "^1.1.0" } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", diff --git a/package.json b/package.json index a5dd433..036b64d 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "@iconify/svelte": "^2.2.1", "@playwright/test": "^1.22.2", "@sveltejs/adapter-node": "^1.0.0-next.86", - "@sveltejs/kit": "^1.0.0-next.428", + "@sveltejs/kit": "^1.0.0-next.430", "@sveltejs/package": "^1.0.0-next.1", "@tailwindcss/typography": "^0.5.3", - "@tricoteuses/explorer-tools": "^0.1.9", + "@tricoteuses/explorer-tools": "^0.1.12", "@types/fs-extra": "^9.0.13", "@types/he": "^1.1.2", "@typescript-eslint/eslint-plugin": "^5.27.0", @@ -45,6 +45,7 @@ "fast-xml-parser": "^4.0.9", "fs-extra": "^10.1.0", "he": "^1.2.0", + "morgan": "^1.10.0", "postcss": "^8.4.14", "postcss-load-config": "^4.0.1", "postgres": "^3.2.4", diff --git a/src/lib/aggregates.ts b/src/lib/aggregates.ts new file mode 100644 index 0000000..00e8a4b --- /dev/null +++ b/src/lib/aggregates.ts @@ -0,0 +1,6 @@ +export type Follow = typeof allFollows[number] + +export const allFollows = [ + "LIENS.LIEN[@sens=cible,@typelien=CREATION].@id", +] as const +export const allFollowsMutable = [...allFollows] diff --git a/src/lib/auditors/queries.ts b/src/lib/auditors/search_params.ts similarity index 54% rename from src/lib/auditors/queries.ts rename to src/lib/auditors/search_params.ts index b7059e2..1bf9570 100644 --- a/src/lib/auditors/queries.ts +++ b/src/lib/auditors/search_params.ts @@ -2,43 +2,50 @@ import type { Audit, Auditor } from "@auditors/core" import { auditArray, auditChain, + auditCleanArray, auditFunction, auditInteger, + auditNullish, + auditOptions, auditSetNullish, auditStringToNumber, + auditSwitch, auditTest, auditTrimString, + auditUnique, } from "@auditors/core" -export function auditPaginationQuery( +import { allFollowsMutable } from "$lib/aggregates" + +export function auditFollowSearchParams( 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)) + data: { [key: string]: unknown }, + errors: { [key: string]: unknown }, + remainingKeys: Set, +): void { + audit.attribute( + data, + "follow", + true, + errors, + remainingKeys, + auditSearchParamsOptionsSet(allFollowsMutable), + ) +} +export function auditLimitSearchParam( + audit: Audit, + data: { [key: string]: unknown }, + errors: { [key: string]: unknown }, + remainingKeys: Set, +): void { audit.attribute( data, "limit", true, errors, remainingKeys, - auditQuerySingleton( + auditSingleton( auditTrimString, auditStringToNumber, auditInteger, @@ -53,13 +60,21 @@ export function auditPaginationQuery( auditSetNullish(10), ), ) +} + +export function auditOffsetSearchParam( + audit: Audit, + data: { [key: string]: unknown }, + errors: { [key: string]: unknown }, + remainingKeys: Set, +): void { audit.attribute( data, "offset", true, errors, remainingKeys, - auditQuerySingleton( + auditSingleton( auditTrimString, auditStringToNumber, auditInteger, @@ -70,26 +85,9 @@ export function auditPaginationQuery( auditSetNullish(0), ), ) - - // Keep the remaining keys as is. - remainingKeys.clear() - - return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } -export function auditQuerySingleton(...auditors: Auditor[]) { - return auditChain( - auditArray(), - auditTest( - (values) => values.length <= 1, - "Parameter must be present only once in query", - ), - auditFunction((value) => value[0]), - ...auditors, - ) -} - -export function auditSearchQuery( +export function auditPaginationSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -111,61 +109,110 @@ export function auditSearchQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditLimitSearchParam(audit, data, errors, remainingKeys) + auditOffsetSearchParam(audit, data, errors, remainingKeys) + + // Keep the remaining keys as is. + remainingKeys.clear() return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } -export function auditSearchQueryContent( +export function auditQSearchParam( audit: Audit, data: { [key: string]: unknown }, errors: { [key: string]: unknown }, remainingKeys: Set, -) { - audit.attribute( - data, - "limit", - true, - errors, - remainingKeys, - auditQuerySingleton( - auditTrimString, - auditStringToNumber, - auditInteger, - auditTest( - (value: number) => value >= 1, - "Value must be greater than or equal to 1", - ), - auditTest( - (value: number) => value <= 1000, - "Value must be less than or equal to 1000", - ), - auditSetNullish(20), - ), - ) - audit.attribute( - data, - "offset", - true, - errors, - remainingKeys, - auditQuerySingleton( - auditTrimString, - auditStringToNumber, - auditInteger, - auditTest( - (value: number) => value >= 0, - "Value must be greater than or equal to 0", - ), - auditSetNullish(0), - ), - ) +): void { audit.attribute( data, "q", true, errors, remainingKeys, - auditQuerySingleton(auditTrimString), + auditSingleton(auditTrimString), + ) +} +export const auditSearchParamsArray = auditChain( + auditSwitch( + auditNullish, + [auditTrimString, auditFunction((value) => [value])], + auditCleanArray(auditTrimString), + ), + auditSetNullish([]), +) + +export function auditSearchParamsOptionsArray( + possibleValues: string[], +): Auditor { + return auditChain( + auditSearchParamsArray, + auditArray(auditOptions(possibleValues)), + auditUnique, + auditSetNullish([]), + ) +} + +export function auditSearchParamsOptionsSet(possibleValues: string[]): Auditor { + return auditChain( + auditSearchParamsOptionsArray(possibleValues), + auditFunction((values) => new Set(values)), + auditSetNullish(new Set()), + ) +} + +export const auditSearchParamsSet = auditChain( + auditSearchParamsArray, + auditFunction((values) => new Set(values)), + auditSetNullish(new Set()), +) + +export function auditSearchSearchParams( + 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)) + + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) + + return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) +} + +export function auditSearchSearchParamsContent( + audit: Audit, + data: { [key: string]: unknown }, + errors: { [key: string]: unknown }, + remainingKeys: Set, +): void { + auditLimitSearchParam(audit, data, errors, remainingKeys) + auditOffsetSearchParam(audit, data, errors, remainingKeys) + auditQSearchParam(audit, data, errors, remainingKeys) +} + +export function auditSingleton(...auditors: Auditor[]): Auditor { + return auditChain( + auditArray(), + auditTest( + (values) => values.length <= 1, + "Parameter must be present only once in query", + ), + auditFunction((value) => value[0]), + ...auditors, ) } diff --git a/src/lib/components/Pagination.svelte b/src/lib/components/Pagination.svelte index d575840..8aaac44 100644 --- a/src/lib/components/Pagination.svelte +++ b/src/lib/components/Pagination.svelte @@ -2,7 +2,7 @@ import { cleanAudit } from "@auditors/core" import { page } from "$app/stores" - import { auditPaginationQuery } from "$lib/auditors/queries" + import { auditPaginationSearchParams } from "$lib/auditors/search_params" import type { PaginationQuery } from "$lib/queries" export let count: number | undefined | null = undefined // Count is not always known. @@ -19,7 +19,7 @@ $: pathname = url.pathname function ensureValidQuery(query: URLSearchParams): PaginationQuery { - const [validQuery, queryError] = auditPaginationQuery( + const [validQuery, queryError] = auditPaginationSearchParams( cleanAudit, query, ) as [PaginationQuery, unknown] diff --git a/src/lib/legal.ts b/src/lib/legal.ts index e4876f2..1b4a565 100644 --- a/src/lib/legal.ts +++ b/src/lib/legal.ts @@ -13,20 +13,37 @@ export interface Article { } } } + NOTA: { + CONTENU: string + } + LIENS?: { + LIEN: Lien | Lien + } CONTEXTE: Contexte VERSIONS: { - VERSION: Array<{ - "@etat": Etat - LIEN_ART: LienArt - }> + VERSION: ArticleVersion | ArticleVersion[] } BLOC_TEXTUEL: { CONTENU: string // HTML } } +export interface ArticleVersion { + "@etat": Etat + LIEN_ART: LienArt +} + export interface Contexte { TEXTE: { + "@cid": string + "@nor": string + "@num": string + "@nature": string + "@autorite": string + "@ministere": string + "@date_publi": string + "@date_signature": string + "@num_parution_jo": string TITRE_TXT: TitreTxt | TitreTxt[] } } @@ -40,7 +57,7 @@ export interface DossierLegislatif { } } -export type Etat = "MODIFIE" | "PERIME" | "VIGUEUR" +export type Etat = "" | "MODIFIE" | "PERIME" | "VIGUEUR" export interface Idcc { META: { @@ -72,6 +89,9 @@ export interface Jo { } META_COMMUN: MetaCommun } + STRUCTURE_TXT: { + TM: Tm + } } export type LegalObject = @@ -98,6 +118,19 @@ export type LegalObjectType = | "textelr" | "versions" +export interface Lien { + "@id": string + "@num": string + "@sens": "cible" | "source" + "@cidtexte": string + "@nortexte": string + "@numtexte": string + "@typelien": "CITATION" | "CODIFICATION" | "CREATION" | "SPEC_APPLI" + "@naturetexte": string + "@datesignatexte": string + "#text": string +} + export interface LienArt { "@id": string "@fin": string @@ -111,30 +144,35 @@ export interface MetaCommun { ID: string } +export interface MetaTexteChronicle { + CID: string + NUM: string + NUM_PARUTION: string + NUM_SEQUENCE: number + NOR: string + DATE_PUBLI: string + DATE_TEXTE: string + DERNIERE_MODIFICATION: string + ORIGINE_PUBLI: string + PAGE_DEB_PUBLI: number + PAGE_FIN_PUBLI: number + VERSIONS_A_VENIR?: string +} + export interface SectionTa { ID: string CONTEXTE: Contexte TITRE_TA: string - STRUCTURE_TA: { LIEN_ART?: LienArt | LienArt[] } + STRUCTURE_TA: { + LIEN_ART?: LienArt | LienArt[] + } } export interface Textekali { META: { META_COMMUN: MetaCommun META_SPEC: { - META_TEXTE_CHRONICLE: { - CID: string - NUM: string - NUM_PARUTION: string - NUM_SEQUENCE: string - NOR: string - DATE_PUBLI: string - DATE_TEXTE: string - DERNIERE_MODIFICATION: string - ORIGINE_PUBLI: string - PAGE_DEB_PUBLI: string - PAGE_FIN_PUBLI: string - } + META_TEXTE_CHRONICLE: MetaTexteChronicle } } } @@ -142,12 +180,37 @@ export interface Textekali { export interface Textelr { META: { META_COMMUN: MetaCommun + META_SPEC: { + META_TEXTE_CHRONICLE: MetaTexteChronicle + } + } + STRUCT: { + LIEN_ART?: LienArt | LienArt[] + } + VERSIONS: { + VERSION: TextelrVersion | TextelrVersion[] } } +export interface TextelrVersion { + "@etat": Etat + LIEN_TXT: TextelrVersionLienTxt +} + +export interface TextelrVersionLienTxt { + "@id": string + "@fin": string + "@num": string + "@debut": string +} + export interface TexteVersion { + ABRO?: { + CONTENU: string + } META: { META_SPEC: { + META_TEXTE_CHRONICLE: MetaTexteChronicle META_TEXTE_VERSION: { TITRE: string TITREFULL: string @@ -155,6 +218,34 @@ export interface TexteVersion { } META_COMMUN: MetaCommun } + NOTA?: { + CONTENU: string + } + NOTICE?: { + CONTENU: string + } + RECT?: { + CONTENU: string + } + SIGNATAIRES?: { + CONTENU: string + } + SM?: { + CONTENU: string + } + TP?: { + CONTENU: string + } + VISAS?: { + CONTENU: string + } +} + +export interface TitreTm { + "@id": string + "@fin": string + "#text": string + "@debut": string } export interface TitreTxt { @@ -165,6 +256,19 @@ export interface TitreTxt { "#text": string } +/// Table des matières (TM) +export interface Tm { + "@niv": string // 1, 2, 3… + LIEN_TXT?: TmLienTxt | TmLienTxt[] + TITRE_TM: string + TM?: Tm | Tm[] +} + +export interface TmLienTxt { + "@idtxt": string + "@titretxt": string +} + export interface Versions { VERSION: { "@id": string @@ -186,6 +290,15 @@ export interface XmlHeader { export const appMenu: MenuItem[] = [ { href: "/recherche", label: "Recherche" }, + { + items: [ + { href: "/idcc", label: "Accords de branche et conventions collectives" }, + { href: "/texte_version", label: "Codes, lois et règlements" }, + { href: "/dossier_legislatif", label: "Dossiers législatifs" }, + { href: "/jo", label: "Journal officiel" }, + ], + label: "Fonds", + }, { items: [ { href: "/article", label: "ARTICLE" }, @@ -242,6 +355,38 @@ export function bestItemForDate( return items[0] } +export function pathnameFromLegalId(id: string): string | undefined { + if (id.match(/^[A-Z]{4}ARTI/) !== null) { + return `/article/${id}` + } + if (id.match(/^[A-Z]{4}DOLE/) !== null) { + 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( type: LegalObjectType, object: LegalObject, diff --git a/src/lib/scripts/import_jorf.ts b/src/lib/scripts/import_jorf.ts index fcd69ea..16f1b8f 100644 --- a/src/lib/scripts/import_jorf.ts +++ b/src/lib/scripts/import_jorf.ts @@ -23,7 +23,19 @@ import { walkDir } from "$lib/server/file_systems" const xmlParser = new XMLParser({ attributeNamePrefix: "@", ignoreAttributes: false, - stopNodes: ["ARTICLE.BLOC_TEXTUEL.CONTENU", "ARTICLE.SM.CONTENU"], + stopNodes: [ + "ARTICLE.BLOC_TEXTUEL.CONTENU", + "ARTICLE.NOTA.CONTENU", + "ARTICLE.SM.CONTENU", + "TEXTE_VERSION.ABRO.CONTENU", + "TEXTE_VERSION.NOTA.CONTENU", + "TEXTE_VERSION.NOTICE.CONTENU", + "TEXTE_VERSION.RECT.CONTENU", + "TEXTE_VERSION.SIGNATAIRES.CONTENU", + "TEXTE_VERSION.SM.CONTENU", + "TEXTE_VERSION.TP.CONTENU", + "TEXTE_VERSION.VISAS.CONTENU", + ], tagValueProcessor: (_tagName, tagValue) => he.decode(tagValue), }) diff --git a/src/lib/scripts/import_kali.ts b/src/lib/scripts/import_kali.ts index ed42e2a..62df378 100644 --- a/src/lib/scripts/import_kali.ts +++ b/src/lib/scripts/import_kali.ts @@ -22,7 +22,19 @@ import { walkDir } from "$lib/server/file_systems" const xmlParser = new XMLParser({ attributeNamePrefix: "@", ignoreAttributes: false, - stopNodes: ["ARTICLE.BLOC_TEXTUEL.CONTENU", "ARTICLE.SM.CONTENU"], + stopNodes: [ + "ARTICLE.BLOC_TEXTUEL.CONTENU", + "ARTICLE.NOTA.CONTENU", + "ARTICLE.SM.CONTENU", + "TEXTE_VERSION.ABRO.CONTENU", + "TEXTE_VERSION.NOTA.CONTENU", + "TEXTE_VERSION.NOTICE.CONTENU", + "TEXTE_VERSION.RECT.CONTENU", + "TEXTE_VERSION.SIGNATAIRES.CONTENU", + "TEXTE_VERSION.SM.CONTENU", + "TEXTE_VERSION.TP.CONTENU", + "TEXTE_VERSION.VISAS.CONTENU", + ], tagValueProcessor: (_tagName, tagValue) => he.decode(tagValue), }) diff --git a/src/lib/scripts/import_legi.ts b/src/lib/scripts/import_legi.ts index 8007023..0b8bf0a 100644 --- a/src/lib/scripts/import_legi.ts +++ b/src/lib/scripts/import_legi.ts @@ -24,11 +24,14 @@ const xmlParser = new XMLParser({ ignoreAttributes: false, stopNodes: [ "ARTICLE.BLOC_TEXTUEL.CONTENU", + "ARTICLE.NOTA.CONTENU", + "ARTICLE.SM.CONTENU", "TEXTE_VERSION.ABRO.CONTENU", - "TEXTE_VERSION.CONTENU", "TEXTE_VERSION.NOTA.CONTENU", + "TEXTE_VERSION.NOTICE.CONTENU", "TEXTE_VERSION.RECT.CONTENU", "TEXTE_VERSION.SIGNATAIRES.CONTENU", + "TEXTE_VERSION.SM.CONTENU", "TEXTE_VERSION.TP.CONTENU", "TEXTE_VERSION.VISAS.CONTENU", ], diff --git a/src/lib/server/aggregates.ts b/src/lib/server/aggregates.ts new file mode 100644 index 0000000..bb7bf63 --- /dev/null +++ b/src/lib/server/aggregates.ts @@ -0,0 +1,86 @@ +import { iterArrayOrSingleton } from "@tricoteuses/explorer-tools" + +import type { Follow } from "$lib/aggregates" +import type { Article, Lien } from "$lib/legal" +import { db } from "$lib/server/database" + +export interface Aggregate { + article?: { [id: string]: Article } + id?: string + ids?: string[] +} + +export class Aggregator { + article: { [id: string]: Article } = {} + follow: Set + requestedIds: Set = new Set() + visitedIds: Set = new Set() + + constructor(follow: Iterable) { + this.follow = new Set(follow) + } + + addArticle(article: Article): void { + const id = article.META.META_COMMUN.ID + this.article[id] = article + + if (this.follow.has("LIENS.LIEN[@sens=cible,@typelien=CREATION].@id")) { + for (const lien of iterArrayOrSingleton(article.LIENS?.LIEN)) { + if (lien["@sens"] === "cible" && lien["@typelien"] === "CREATION") { + this.requestId(lien["@id"]) + } + } + } + } + + addToVisitedIds(ids: string[]) { + for (const id of ids) { + this.visitedIds.add(id) + } + } + + deleteFromRequestedIds(ids: string[]) { + for (const id of ids) { + this.requestedIds.delete(id) + } + } + + async getAll(): Promise { + while (true) { + if (this.requestedIds.size === 0) { + break + } + console.log("this.requestedIds.size", this.requestedIds.size) + + { + const articleIds = [...this.requestedIds].filter( + (id) => id.match(/^[A-Z]{4}ARTI/) !== null, + ) + if (articleIds.length > 0) { + this.deleteFromRequestedIds(articleIds) + for (const { data } of await db<{ id: string; data: Article }[]>` + SELECT id, data FROM article + WHERE id IN ${db(articleIds)} + `) { + this.addArticle(data) + } + this.addToVisitedIds(articleIds) + } + } + } + } + + requestId(id: string): void { + if (!this.visitedIds.has(id)) { + this.requestedIds.add(id) + } + } + + toJson(): Aggregate { + const json: Aggregate = {} + if (Object.keys(this.article).length > 0) { + json.article = this.article + } + return json + } +} diff --git a/src/lib/server/doers/recherche.ts b/src/lib/server/doers/recherche.ts new file mode 100644 index 0000000..3711fa0 --- /dev/null +++ b/src/lib/server/doers/recherche.ts @@ -0,0 +1,91 @@ +import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" +import { error } from "@sveltejs/kit" + +import type { Follow } from "$lib/aggregates" +import { + auditFollowSearchParams, + auditQSearchParam, +} from "$lib/auditors/search_params" +import type { Article } from "$lib/legal" +import { type Aggregate, Aggregator } from "$lib/server/aggregates" +import { db } from "$lib/server/database" + +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)) + + auditFollowSearchParams(audit, data, errors, remainingKeys) + auditQSearchParam(audit, data, errors, remainingKeys) + + return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) +} + +export const doGetRecherche = async ( + url: URL, +): Promise< + | (Aggregate & { + q: string + }) + | null +> => { + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ follow: Follow[]; 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, q } = query + + if (q !== undefined) { + // 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/1984-12-30/ + // 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/id/LEGIARTI000006317314/1983-12-30 + const articleId = q.match(/LEGIARTI\d+/)?.[0] + if (articleId != null) { + const article = ( + await db<{ data: Article }[]>` + SELECT data FROM article + WHERE id = ${articleId} + LIMIT 1 + ` + ).map(({ data }) => data)[0] + if (article !== undefined) { + const aggregator = new Aggregator(follow) + aggregator.addArticle(article) + await aggregator.getAll() + + return { ...aggregator.toJson(), id: article.META.META_COMMUN.ID, q } + } + } + } + return null +} diff --git a/src/lib/summaries.ts b/src/lib/summaries.ts index 6661236..5972e8a 100644 --- a/src/lib/summaries.ts +++ b/src/lib/summaries.ts @@ -3,22 +3,30 @@ import type { Access, Summarizer, Summary } from "augmented-data-viewer" import { type Article, + type ArticleVersion, assertNeverLegalObjectType, bestItemForDate, + type DossierLegislatif, type Idcc, type IdWrapper, type Jo, type LegalObject, type LegalObjectType, + type Lien, + type LienArt, + pathnameFromLegalId, pathnameFromLegalObject, pathnameFromLegalObjectId, type SectionTa, type Textekali, type Textelr, + type TextelrVersionLienTxt, type TexteVersion, - type LienArt, + type TitreTxt, + type TitreTm, + type Tm, + type TmLienTxt, type VersionsWrapper, - type DossierLegislatif, } from "$lib/legal" export const summarizeArticleProperties: Summarizer = (access, value) => { @@ -29,6 +37,25 @@ export const summarizeArticleProperties: Summarizer = (access, value) => { return summarizeLegalObjectToLink(access, "article", value) } + if (access?.key === "@cid" && access?.access?.key === "TEXTE") { + return summarizeLegalIdToLink(access, value) + } + if ( + access?.key === "@cidtexte" && + (access?.access?.key === "LIEN" || + (typeof access?.access?.key === "number" && + access?.access?.access?.key === "LIEN")) + ) { + return summarizeLegalIdToLink(access, value) + } + if ( + access?.key === "@id" && + (access?.access?.key === "LIEN" || + (typeof access?.access?.key === "number" && + access?.access?.access?.key === "LIEN")) + ) { + return summarizeLienId(access.access, access.parent) + } if ( access?.key === "@id" && (access?.access?.key === "LIEN_ART" || @@ -37,17 +64,39 @@ export const summarizeArticleProperties: Summarizer = (access, value) => { ) { return summarizeLienArtId(access.access, access.parent) } + if ( + access?.key === "@id" && + (access?.access?.key === "TITRE_TM" || + (typeof access?.access?.key === "number" && + access?.access?.access?.key === "TITRE_TM")) + ) { + return summarizeTitreTmId(access.access, access.parent) + } + if ( + access?.key === "@id_txt" && + (access?.access?.key === "TITRE_TXT" || + (typeof access?.access?.key === "number" && + access?.access?.access?.key === "TITRE_TXT")) + ) { + return summarizeTitreTxtIdTxt(access.access, access.parent) + } if (access?.key === "CONTENU") { return { content: value as string, type: "html" } } - if (access?.key === "LIEN_ART" && !Array.isArray(value)) { - return summarizeLienArt(access, value) + if ( + (access?.key === "LIEN" && !Array.isArray(value)) || + (typeof access?.key === "number" && access?.access?.key === "LIEN") + ) { + return summarizeLien(access, value) } - if (typeof access?.key === "number" && access?.access?.key === "LIEN_ART") { + if ( + (access?.key === "LIEN_ART" && !Array.isArray(value)) || + (typeof access?.key === "number" && access?.access?.key === "LIEN_ART") + ) { return summarizeLienArt(access, value) } if (access?.access?.key === "VERSION") { - const version = value as Article["VERSIONS"]["VERSION"][0] + const version = value as ArticleVersion const lienArt = version.LIEN_ART return { content: { @@ -139,9 +188,61 @@ export const summarizeJoProperties: Summarizer = (access, value) => { return summarizeLegalObjectToLink(access, "jo", value) } + if ( + (access?.key === "LIEN_TXT" && !Array.isArray(value)) || + (typeof access?.key === "number" && access?.access?.key === "LIEN_TXT") + ) { + return summarizeTmLienTxt(access, value) + } + if ( + (access?.key === "TM" && !Array.isArray(value)) || + (typeof access?.key === "number" && access?.access?.key === "TM") + ) { + return summarizeTm(access, value) + } + return undefined } +export function summarizeLegalIdToLink( + access: Access | undefined, + value: unknown, +): Summary | undefined { + const id = value as string | undefined + if (id === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(id) + if (pathname === undefined) { + return undefined + } + return { + content: id, + href: pathname, + type: "link", + } +} + +export function summarizeLegalIdToValueArrowLink( + access: Access | undefined, + value: unknown, +): Summary | undefined { + const uidLinkSummary = summarizeLegalIdToLink(access, value) + return uidLinkSummary === undefined + ? undefined + : { + items: [ + { + content: JSON.stringify(value), + type: "raw_data", + }, + { class: "mx-1 mt-1", icon: arrowRight, inline: true, type: "icon" }, + uidLinkSummary, + ], + type: "concatenation", + } +} + export function summarizeLegalObject( access: Access | undefined, type: LegalObjectType, @@ -250,6 +351,22 @@ export function summarizeLegalObjectToLink( } } +export const summarizeLien: Summarizer = (access, value) => { + const lien = value as Lien | undefined + if (lien === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(lien["@id"]) + if (pathname === undefined) { + return undefined + } + return { + content: `${lien["@typelien"]} ${lien["@sens"]} : ${lien["#text"]}`, + href: pathname, + type: "link", + } +} + export const summarizeLienArt: Summarizer = (access, value) => { const lienArt = value as LienArt | undefined if (lienArt === undefined) { @@ -273,13 +390,39 @@ export const summarizeLienArtId: Summarizer = (access, value) => { content: JSON.stringify(lienArt["@id"]), type: "raw_data", }, - { class: "mx-1", icon: arrowRight, inline: true, type: "icon" }, + { class: "mt-1 mx-1", icon: arrowRight, inline: true, type: "icon" }, summarizeLienArt(access, lienArt)!, ], type: "concatenation", } } +export const summarizeLienId: Summarizer = (access, value) => { + const lien = value as Lien | undefined + if (lien === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(lien["@id"]) + if (pathname === undefined) { + return undefined + } + return { + items: [ + { + content: JSON.stringify(lien["@id"]), + type: "raw_data", + }, + { class: "mt-1 mx-1", icon: arrowRight, inline: true, type: "icon" }, + { + content: lien["#text"], + href: pathname, + type: "link", + }, + ], + type: "concatenation", + } +} + export const summarizeSectionTaProperties: Summarizer = (access, value) => { if (access?.key === "section_ta" && typeof value !== "number") { return summarizeLegalObject(access, "section_ta", value) @@ -288,6 +431,9 @@ export const summarizeSectionTaProperties: Summarizer = (access, value) => { return summarizeLegalObjectToLink(access, "section_ta", value) } + if (access?.key === "@cid" && access?.access?.key === "TEXTE") { + return summarizeLegalIdToLink(access, value) + } if ( access?.key === "@id" && (access?.access?.key === "LIEN_ART" || @@ -296,10 +442,10 @@ export const summarizeSectionTaProperties: Summarizer = (access, value) => { ) { return summarizeLienArtId(access.access, access.parent) } - if (access?.key === "LIEN_ART" && !Array.isArray(value)) { - return summarizeLienArt(access, value) - } - if (typeof access?.key === "number" && access?.access?.key === "LIEN_ART") { + if ( + (access?.key === "LIEN_ART" && !Array.isArray(value)) || + (typeof access?.key === "number" && access?.access?.key === "LIEN_ART") + ) { return summarizeLienArt(access, value) } return undefined @@ -313,9 +459,29 @@ export const summarizeTextekaliProperties: Summarizer = (access, value) => { return summarizeLegalObjectToLink(access, "textekali", value) } + if (access?.key === "CID") { + return summarizeLegalIdToLink(access, value) + } + return undefined } +export const summarizeTextelrVersionLienTxt: Summarizer = (access, value) => { + const lienTxt = value as TextelrVersionLienTxt | undefined + if (lienTxt === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(lienTxt["@id"]) + if (pathname === undefined) { + return undefined + } + return { + content: lienTxt["@id"], + href: pathname, + type: "link", + } +} + export const summarizeTextelrProperties: Summarizer = (access, value) => { if (access?.key === "textelr" && typeof value !== "number") { return summarizeLegalObject(access, "textelr", value) @@ -324,6 +490,27 @@ export const summarizeTextelrProperties: Summarizer = (access, value) => { return summarizeLegalObjectToLink(access, "textelr", value) } + if ( + access?.key === "@id" && + (access?.access?.key === "LIEN_ART" || + (typeof access?.access?.key === "number" && + access?.access?.access?.key === "LIEN_ART")) + ) { + return summarizeLienArtId(access.access, access.parent) + } + if (access?.key === "CID") { + return summarizeLegalIdToLink(access, value) + } + if ( + (access?.key === "LIEN_ART" && !Array.isArray(value)) || + (typeof access?.key === "number" && access?.access?.key === "LIEN_ART") + ) { + return summarizeLienArt(access, value) + } + if (access?.key === "LIEN_TXT") { + return summarizeTextelrVersionLienTxt(access, value) + } + return undefined } @@ -338,9 +525,95 @@ export const summarizeTexteVersionProperties: Summarizer = (access, value) => { return summarizeLegalObjectToLink(access, "texte_version", value) } + if (access?.key === "CID") { + return summarizeLegalIdToLink(access, value) + } + if (access?.key === "CONTENU") { + return { content: value as string, type: "html" } + } + return undefined } +export const summarizeTitreTmId: Summarizer = (access, value) => { + const titreTm = value as TitreTm | undefined + if (titreTm === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(titreTm["@id"]) + if (pathname === undefined) { + return undefined + } + return { + items: [ + { + content: JSON.stringify(titreTm["@id"]), + type: "raw_data", + }, + { class: "mt-1 mx-1", icon: arrowRight, inline: true, type: "icon" }, + { + content: titreTm["#text"], + href: pathname, + type: "link", + }, + ], + type: "concatenation", + } +} + +export const summarizeTitreTxtIdTxt: Summarizer = (access, value) => { + const titreTxt = value as TitreTxt | undefined + if (titreTxt === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(titreTxt["@id_txt"]) + if (pathname === undefined) { + return undefined + } + return { + items: [ + { + content: JSON.stringify(titreTxt["@id_txt"]), + type: "raw_data", + }, + { class: "mt-1 mx-1", icon: arrowRight, inline: true, type: "icon" }, + { + content: titreTxt["#text"], + href: pathname, + type: "link", + }, + ], + type: "concatenation", + } +} + +export function summarizeTm( + access: Access | undefined, + value: unknown, +): Summary | undefined { + const tm = value as Tm | undefined + if (tm === undefined) { + return undefined + } + return tm.TITRE_TM +} + +export const summarizeTmLienTxt: Summarizer = (access, value) => { + const lienTxt = value as TmLienTxt | undefined + if (lienTxt === undefined) { + return undefined + } + const pathname = pathnameFromLegalId(lienTxt["@idtxt"]) + if (pathname === undefined) { + return undefined + } + return { + content: lienTxt["@titretxt"], + href: pathname, + type: "link", + } +} + export const summarizeVersionsWrapperProperties: Summarizer = ( access, value, diff --git a/src/routes/api/recherche/+server.ts b/src/routes/api/recherche/+server.ts new file mode 100644 index 0000000..a0ed47e --- /dev/null +++ b/src/routes/api/recherche/+server.ts @@ -0,0 +1,14 @@ +import { doGetRecherche } from "$lib/server/doers/recherche" + +import type { RequestHandler } from "./$types" + +export const GET: RequestHandler = async ({ url }) => { + const result = await doGetRecherche(url) + return result === null + ? new Response(null, { status: 204 }) + : new Response(JSON.stringify(result, null, 2), { + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) +} diff --git a/src/routes/article/+page.server.ts b/src/routes/article/+page.server.ts index eceb122..99c86e0 100644 --- a/src/routes/article/+page.server.ts +++ b/src/routes/article/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { Article } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/dossier_legislatif/+page.server.ts b/src/routes/dossier_legislatif/+page.server.ts index c731eea..4d96a15 100644 --- a/src/routes/dossier_legislatif/+page.server.ts +++ b/src/routes/dossier_legislatif/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { DossierLegislatif } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/id/+page.server.ts b/src/routes/id/+page.server.ts index 4b00f28..3c1561a 100644 --- a/src/routes/id/+page.server.ts +++ b/src/routes/id/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { IdWrapper } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/idcc/+page.server.ts b/src/routes/idcc/+page.server.ts index 80e1011..9d72d23 100644 --- a/src/routes/idcc/+page.server.ts +++ b/src/routes/idcc/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { Idcc } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/jo/+page.server.ts b/src/routes/jo/+page.server.ts index 68e2f1c..3bb13cb 100644 --- a/src/routes/jo/+page.server.ts +++ b/src/routes/jo/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { Jo } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/recherche/+page.server.ts b/src/routes/recherche/+page.server.ts index fcd7067..ef82ac2 100644 --- a/src/routes/recherche/+page.server.ts +++ b/src/routes/recherche/+page.server.ts @@ -1,73 +1,7 @@ -import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" -import { error } from "@sveltejs/kit" - -import { auditSearchQueryContent } from "$lib/auditors/queries" -import type { Article } from "$lib/legal" -import { db } from "$lib/server/database" +import { doGetRecherche } from "$lib/server/doers/recherche" import type { PageServerLoad } from "./$types" -export function auditQuery( - 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)) - - auditSearchQueryContent(audit, data, errors, remainingKeys) - - return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) -} - export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { 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 { limit, offset, q } = query - - if (q !== undefined) { - // 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/1984-12-30/ - // 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/id/LEGIARTI000006317314/1983-12-30 - const articleId = q.match(/LEGIARTI\d+/)?.[0] - if (articleId != null) { - const articles = ( - await db<{ data: Article }[]>` - SELECT data FROM article - WHERE id = ${articleId} - ` - ).map(({ data }) => data) - return { articles, q } - } - } - return {} + return (await doGetRecherche(url)) ?? {} } diff --git a/src/routes/recherche/+page.svelte b/src/routes/recherche/+page.svelte index 7cd269f..0367d2a 100644 --- a/src/routes/recherche/+page.svelte +++ b/src/routes/recherche/+page.svelte @@ -10,8 +10,7 @@ import type { PageData } from "./$types" export let data: PageData - - $: ({ articles, q } = data) + let { articles, q } = data
diff --git a/src/routes/section_ta/+page.server.ts b/src/routes/section_ta/+page.server.ts index e115eea..5be0c91 100644 --- a/src/routes/section_ta/+page.server.ts +++ b/src/routes/section_ta/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { SectionTa } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/texte_version/+page.server.ts b/src/routes/texte_version/+page.server.ts index 0d5c29b..b2f207a 100644 --- a/src/routes/texte_version/+page.server.ts +++ b/src/routes/texte_version/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { TexteVersion } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/textekali/+page.server.ts b/src/routes/textekali/+page.server.ts index 2b1e66e..97fe5ed 100644 --- a/src/routes/textekali/+page.server.ts +++ b/src/routes/textekali/+page.server.ts @@ -1,13 +1,13 @@ import { type Audit, auditSetNullish, cleanAudit } from "@auditors/core" import { error } from "@sveltejs/kit" -import { auditSearchQueryContent } from "$lib/auditors/queries" +import { auditSearchSearchParamsContent } from "$lib/auditors/search_params" import type { Textekali } from "$lib/legal" import { db } from "$lib/server/database" import type { PageServerLoad } from "./$types" -export function auditQuery( +export function auditSearchParams( audit: Audit, query: URLSearchParams, ): [unknown, unknown] { @@ -29,16 +29,16 @@ export function auditQuery( const errors: { [key: string]: unknown } = {} const remainingKeys = new Set(Object.keys(data)) - auditSearchQueryContent(audit, data, errors, remainingKeys) + auditSearchSearchParamsContent(audit, data, errors, remainingKeys) return audit.reduceRemaining(data, errors, remainingKeys, auditSetNullish({})) } export const load: PageServerLoad = async ({ url }) => { - const [query, queryError] = auditQuery(cleanAudit, url.searchParams) as [ - { limit: number; offset: number; q?: string }, - unknown, - ] + const [query, queryError] = auditSearchParams( + cleanAudit, + url.searchParams, + ) as [{ limit: number; offset: number; q?: string }, unknown] if (queryError !== null) { console.error( `Error in ${url.pathname} query:\n${JSON.stringify( diff --git a/src/routes/textekali/+page.svelte b/src/routes/textekali/+page.svelte index 439fb02..701763e 100644 --- a/src/routes/textekali/+page.svelte +++ b/src/routes/textekali/+page.svelte @@ -1,8 +1,4 @@