From 0d43fd7918d7df324e0d5227d5657b2334c5468f Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Dec 2024 09:07:10 +0100 Subject: [PATCH 1/8] Create API type, move Query types --- app/scripts/backend/backend.ts | 23 ++-- app/scripts/backend/common.ts | 15 +++ app/scripts/backend/graph-proxy.ts | 7 +- app/scripts/backend/kwic-proxy.ts | 112 +++--------------- app/scripts/backend/lemgram-proxy.ts | 7 +- app/scripts/backend/stats-proxy.ts | 7 +- app/scripts/backend/struct-service.ts | 4 +- app/scripts/backend/time-proxy.ts | 5 +- .../backend/{types.ts => types/common.ts} | 46 ++++++- app/scripts/backend/types/index.ts | 12 ++ app/scripts/backend/types/query.ts | 48 ++++++++ .../components/deptree/deptree-util.ts | 2 +- app/scripts/components/deptree/deptree.ts | 2 +- app/scripts/components/kwic-word.ts | 2 +- app/scripts/components/kwic.ts | 3 +- app/scripts/components/sidebar.ts | 2 +- app/scripts/components/trend-diagram.ts | 4 +- app/scripts/controllers/example_controller.ts | 8 +- app/scripts/controllers/kwic_controller.ts | 15 +-- .../controllers/text_reader_controller.ts | 2 +- .../controllers/word_picture_controller.ts | 4 +- app/scripts/jquery.types.ts | 12 +- app/scripts/kwic_download.ts | 9 +- app/scripts/statemachine/types.ts | 2 +- app/scripts/util.ts | 6 +- 25 files changed, 200 insertions(+), 159 deletions(-) create mode 100644 app/scripts/backend/common.ts rename app/scripts/backend/{types.ts => types/common.ts} (60%) create mode 100644 app/scripts/backend/types/index.ts create mode 100644 app/scripts/backend/types/query.ts diff --git a/app/scripts/backend/backend.ts b/app/scripts/backend/backend.ts index 0c0578114..b26a7424c 100644 --- a/app/scripts/backend/backend.ts +++ b/app/scripts/backend/backend.ts @@ -1,13 +1,14 @@ /** @format */ import _ from "lodash" import { getAuthorizationHeader } from "@/components/auth/auth" -import { KorpResponse, WithinParameters } from "@/backend/types" import { SavedSearch } from "@/local-storage" import settings from "@/settings" import { httpConfAddMethodFetch } from "@/util" import { KorpStatsParams, KorpStatsResponse, normalizeStatsData } from "@/backend/stats-proxy" import { MapResult, parseMapData } from "@/map_services" -import { KorpQueryResponse } from "@/backend/kwic-proxy" +import { korpRequest } from "./common" +import { Response, WithinParameters } from "./types" +import { QueryResponse } from "./types/query" type KorpLoglikeResponse = { /** Log-likelihood average. */ @@ -52,14 +53,15 @@ export type MapRequestResult = { type MapAttribute = { label: string; corpora: string[] } -async function korpRequest = {}, P extends Record = {}>( +// TODO Remove in favor of `korpRequest` +async function korpRequestAny = {}, P extends Record = {}>( endpoint: string, params: P -): Promise> { +): Promise> { const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/" + endpoint, params) request.headers = { ...request.headers, ...getAuthorizationHeader() } const response = await fetch(url, request) - return (await response.json()) as KorpResponse + return (await response.json()) as Response } /** Note: since this is using native Promise, we must use it with something like $q or $scope.$apply for AngularJS to react when they resolve. */ @@ -91,7 +93,7 @@ export async function requestCompare( top, } - const data = await korpRequest("loglike", params) + const data = await korpRequestAny("loglike", params) if ("ERROR" in data) { // TODO Create a KorpBackendError which could be displayed nicely @@ -146,7 +148,7 @@ export async function requestMapData( Object.keys(cqpExprs).map((cqp, i) => (params[`subcqp${i}`] = cqp)) - const data = await korpRequest("count", params) + const data = await korpRequestAny("count", params) if ("ERROR" in data) { // TODO Create a KorpBackendError which could be displayed nicely @@ -158,10 +160,7 @@ export async function requestMapData( return { corpora: attribute.corpora, cqp, within, data: result, attribute } } -export async function getDataForReadingMode( - inputCorpus: string, - textId: string -): Promise> { +export async function getDataForReadingMode(inputCorpus: string, textId: string): Promise> { const corpus = inputCorpus.toUpperCase() const corpusSettings = settings.corpusListing.get(inputCorpus) @@ -184,5 +183,5 @@ export async function getDataForReadingMode( end: 0, } - return korpRequest("query", params) + return korpRequest("query", params) } diff --git a/app/scripts/backend/common.ts b/app/scripts/backend/common.ts new file mode 100644 index 000000000..bddca468d --- /dev/null +++ b/app/scripts/backend/common.ts @@ -0,0 +1,15 @@ +/** @format */ +import { httpConfAddMethodFetch } from "@/util" +import { getAuthorizationHeader } from "@/components/auth/auth" +import settings from "@/settings" +import { API, Response } from "./types" + +export async function korpRequest( + endpoint: K, + params: API[K]["params"] +): Promise> { + const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/" + endpoint, params) + request.headers = { ...request.headers, ...getAuthorizationHeader() } + const response = await fetch(url, request) + return (await response.json()) as Response +} diff --git a/app/scripts/backend/graph-proxy.ts b/app/scripts/backend/graph-proxy.ts index 27af43377..c778e3771 100644 --- a/app/scripts/backend/graph-proxy.ts +++ b/app/scripts/backend/graph-proxy.ts @@ -2,9 +2,10 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import { AjaxSettings, Granularity, Histogram, KorpResponse, NumericString } from "@/backend/types" +import { Granularity, Histogram, Response, NumericString } from "@/backend/types" import { AbsRelTuple } from "@/statistics.types" import { Factory, httpConfAddMethod } from "@/util" +import { AjaxSettings } from "@/jquery.types" export class GraphProxy extends BaseProxy { granularity: Granularity @@ -33,7 +34,7 @@ export class GraphProxy extends BaseProxy { corpora: string, from: NumericString, to: NumericString - ): JQuery.Promise> { + ): JQuery.Promise> { this.resetRequest() const self = this const params: KorpCountTimeParams = { @@ -77,7 +78,7 @@ export class GraphProxy extends BaseProxy { error(jqXHR, textStatus, errorThrown) { def.reject(textStatus) }, - success(data: KorpResponse) { + success(data: Response) { def.resolve(data) self.cleanup() }, diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index a414dcfe3..c057eb99b 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -2,12 +2,14 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import type { AjaxSettings, KorpResponse, ProgressReport } from "@/backend/types" import { locationSearchGet, httpConfAddMethod, Factory } from "@/util" +import { ProgressReport, Response } from "./types" +import { QueryParams, QueryResponse } from "./types/query" +import { AjaxSettings } from "@/jquery.types" -export class KwicProxy extends BaseProxy { +export class KwicProxy extends BaseProxy { prevCQP?: string - prevParams: KorpQueryParams | null + prevParams: QueryParams | null prevRequest: AjaxSettings | null // Used for download prevUrl?: string queryData?: string @@ -22,9 +24,9 @@ export class KwicProxy extends BaseProxy { makeRequest( options: KorpQueryRequestOptions, page: number | undefined, - progressCallback: (data: ProgressReport) => void, - kwicCallback: (data: KorpResponse) => void - ): JQuery.jqXHR> { + progressCallback: (data: ProgressReport) => void, + kwicCallback: (data: Response) => void + ): JQuery.jqXHR> { const self = this this.resetRequest() if (!kwicCallback) { @@ -36,7 +38,7 @@ export class KwicProxy extends BaseProxy { _.extend(options.ajaxParams, settings.corpusListing.getWithinParameters()) } - function getPageInterval(): Interval { + function getPageInterval(): { start: number; end: number } { const hpp = locationSearchGet("hpp") const itemsPerPage = Number(hpp) || settings.hits_per_page_default const start = (page || 0) * itemsPerPage @@ -46,7 +48,7 @@ export class KwicProxy extends BaseProxy { const command = options.ajaxParams.command || "query" - const data: KorpQueryParams = { + const data: QueryParams = { default_context: settings.default_overview_context, ...getPageInterval(), ...options.ajaxParams, @@ -99,7 +101,7 @@ export class KwicProxy extends BaseProxy { self.prevUrl = self.makeUrlWithParams(this.url, data) }, - success(data: KorpQueryResponse, status, jqxhr) { + success(data: QueryResponse, status, jqxhr) { self.queryData = data.query_data self.cleanup() // Run the callback to show results, if not already done by the progress handler @@ -115,12 +117,12 @@ export class KwicProxy extends BaseProxy { // The request may continue to count hits in the background if ("kwic" in progressObj.struct) { this.foundKwic = true - kwicCallback(progressObj.struct as KorpQueryResponse) + kwicCallback(progressObj.struct as QueryResponse) } }, } - const def = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR> + const def = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR> this.pendingRequests.push(def) return def } @@ -129,97 +131,11 @@ export class KwicProxy extends BaseProxy { const kwicProxyFactory = new Factory(KwicProxy) export default kwicProxyFactory -/** - * The `query` and `relations_sentences` endpoints are combined here. - * @see https://ws.spraakbanken.gu.se/docs/korp#tag/Concordance/paths/~1query/get - * @see https://ws.spraakbanken.gu.se/docs/korp#tag/Word-Picture/paths/~1relations_sentences/get - */ -export type KorpQueryParams = { - /* Required for `query` */ - corpus?: string - /* Required for `query` */ - cqp?: string - start?: number - end?: number - default_context?: string - context?: string - show?: string - show_struct?: string - /** Required for `relations_sentences` */ - source?: string - default_within?: string - within?: string - in_order?: boolean - sort?: string - random_seed?: number - cut?: number - [cqpn: `cqp${number}`]: string - expand_prequeries?: boolean - incremental?: boolean - query_data?: string -} - export type KorpQueryRequestOptions = { // TODO Should start,end really exist here as well as under ajaxParams? start?: number end?: number - ajaxParams: KorpQueryParams & { + ajaxParams: QueryParams & { command?: "query" | "relations_sentences" } } - -type Interval = { start: number; end: number } - -/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Concordance/paths/~1query/get */ -export type KorpQueryResponse = { - /** Search hits */ - kwic: ApiKwic[] - /** Total number of hits */ - hits: number - /** Number of hits for each corpus */ - corpus_hits: Record - /** Order of corpora in result */ - corpus_order: string[] - /** Execution time in seconds */ - time: number - /** A hash of this query */ - query_data: string -} - -/** Search hits */ -export type ApiKwic = { - corpus: string - /** An object for each token in the context, with attribute values for that token */ - tokens: Token[] - /** Attribute values for the context (e.g. sentence) */ - structs: Record - /** Specifies the position of the match in the context. If `in_order` is false, `match` will consist of a list of match objects, one per highlighted word */ - match: KwicMatch | KwicMatch[] - /** Hits from aligned corpora if available, otherwise omitted */ - aligned: { - [linkedCorpusId: `${string}-${string}`]: Token[] - } -} - -/** Specifies the position of a match in a context */ -type KwicMatch = { - /** Start position of the match within the context */ - start: number - /** End position of the match within the context */ - end: number - /** Global corpus position of the match */ - position: number -} - -export type Token = { - word: string - structs?: { - open?: { - [element: string]: { - [attr: string]: string - } - }[] - close?: string[] - } - [attr: string]: any -} diff --git a/app/scripts/backend/lemgram-proxy.ts b/app/scripts/backend/lemgram-proxy.ts index 4fc7dc8d9..ae500fad1 100644 --- a/app/scripts/backend/lemgram-proxy.ts +++ b/app/scripts/backend/lemgram-proxy.ts @@ -1,8 +1,9 @@ /** @format */ import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import type { AjaxSettings, KorpResponse, ProgressReport } from "@/backend/types" +import type { Response, ProgressReport } from "@/backend/types" import { Factory, httpConfAddMethod } from "@/util" +import { AjaxSettings } from "@/jquery.types" export class LemgramProxy extends BaseProxy { prevParams?: KorpRelationsParams @@ -13,7 +14,7 @@ export class LemgramProxy extends BaseProxy { word: string, type: string, callback: (data: ProgressReport) => void - ): JQuery.jqXHR> { + ): JQuery.jqXHR> { this.resetRequest() const self = this @@ -50,7 +51,7 @@ export class LemgramProxy extends BaseProxy { }, } - const def = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR> + const def = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR> this.pendingRequests.push(def) return def } diff --git a/app/scripts/backend/stats-proxy.ts b/app/scripts/backend/stats-proxy.ts index 86d199c7d..8f30414ef 100644 --- a/app/scripts/backend/stats-proxy.ts +++ b/app/scripts/backend/stats-proxy.ts @@ -2,10 +2,11 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import type { AjaxSettings, KorpResponse, ProgressResponse, ProgressReport } from "@/backend/types" +import type { Response, ProgressResponse, ProgressReport } from "@/backend/types" import { StatsNormalized, StatsColumn, StatisticsWorkerResult } from "@/statistics.types" import { locationSearchGet, httpConfAddMethod, Factory } from "@/util" import { statisticsService } from "@/statistics" +import { AjaxSettings } from "@/jquery.types" /** * Stats in the response can be split by subqueries if the `subcqp#` param is used, but otherwise not. @@ -113,7 +114,7 @@ export class StatsProxy extends BaseProxy { const def: JQuery.Deferred = $.Deferred() const url = settings.korp_backend_url + "/count" - const ajaxSettings: AjaxSettings> = { + const ajaxSettings: AjaxSettings> = { url, data, beforeSend(req, settings) { @@ -137,7 +138,7 @@ export class StatsProxy extends BaseProxy { } }, - success: (data: KorpResponse) => { + success: (data: Response) => { self.cleanup() if ("ERROR" in data) { console.log("gettings stats failed with error", data.ERROR) diff --git a/app/scripts/backend/struct-service.ts b/app/scripts/backend/struct-service.ts index 158a62a1b..18eafda1b 100644 --- a/app/scripts/backend/struct-service.ts +++ b/app/scripts/backend/struct-service.ts @@ -4,7 +4,7 @@ import angular, { IHttpService, IPromise } from "angular" import settings from "@/settings" import { getAuthorizationHeader } from "@/components/auth/auth" import { httpConfAddMethod } from "@/util" -import { KorpResponse } from "./types" +import { Response } from "./types" export type StructService = { /** Find which unique values occur and count them. */ @@ -70,7 +70,7 @@ angular.module("korpApp").factory("structService", [ const conf = httpConfAddMethod({ url, method: "GET", params, headers }) - const { data } = await $http>(conf) + const { data } = await $http>(conf) if ("ERROR" in data) throw new Error(data.ERROR.value) return data } diff --git a/app/scripts/backend/time-proxy.ts b/app/scripts/backend/time-proxy.ts index 3a83ea283..ced7789de 100644 --- a/app/scripts/backend/time-proxy.ts +++ b/app/scripts/backend/time-proxy.ts @@ -2,8 +2,9 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import type { AjaxSettings, Granularity, Histogram, KorpResponse, NumericString } from "@/backend/types" +import type { Granularity, Histogram, Response, NumericString } from "@/backend/types" import { Factory, httpConfAddMethod } from "@/util" +import { AjaxSettings } from "@/jquery.types" export class TimeProxy extends BaseProxy { makeRequest(): JQueryDeferred { @@ -100,7 +101,7 @@ type KorpTimespanParams = { } /** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Statistics/paths/~1timespan/get */ -type KorpTimespanResponse = KorpResponse<{ +type KorpTimespanResponse = Response<{ /** An object with corpus names as keys and time statistics objects as values */ corpora: Record /** Number of tokens per time period */ diff --git a/app/scripts/backend/types.ts b/app/scripts/backend/types/common.ts similarity index 60% rename from app/scripts/backend/types.ts rename to app/scripts/backend/types/common.ts index 833662071..ec4e809c5 100644 --- a/app/scripts/backend/types.ts +++ b/app/scripts/backend/types/common.ts @@ -1,7 +1,7 @@ /** @format */ /** A Korp response is either successful or has error info */ -export type KorpResponse = ResponseBase & (R | Error) +export type Response = ResponseBase & (R | Error) /** All responses have time info. */ export type ResponseBase = { @@ -31,11 +31,6 @@ export type ProgressResponse = { [progress_n: `progress_${number}`]: string | { corpus: string; hits: number } } -/** Extends JQuery `jaxSettings` with stuff we use. */ -export type AjaxSettings = JQuery.AjaxSettings & { - progress?: (this: TContext, data: any, e: any) => void -} - export type ProgressReport = { /** Response data */ struct: ResponseBase & ProgressResponse & Partial @@ -63,3 +58,42 @@ export type WithinParameters = { default_within: string within: string } + +/** Search hit */ +export type ApiKwic = { + corpus: string + /** An object for each token in the context, with attribute values for that token */ + tokens: Token[] + /** Attribute values for the context (e.g. sentence) */ + structs: Record + /** Specifies the position of the match in the context. If `in_order` is false, `match` will consist of a list of match objects, one per highlighted word */ + match: KwicMatch | KwicMatch[] + /** Hits from aligned corpora if available, otherwise omitted */ + aligned: { + [linkedCorpusId: `${string}-${string}`]: Token[] + } +} + +/** Specifies the position of a match in a context */ +type KwicMatch = { + /** Start position of the match within the context */ + start: number + /** End position of the match within the context */ + end: number + /** Global corpus position of the match */ + position: number +} + +export type Token = { + word: string + /** Start/end tags */ + structs?: { + open?: { + [element: string]: { + [attr: string]: string + } + }[] + close?: string[] + } + [attr: string]: any +} diff --git a/app/scripts/backend/types/index.ts b/app/scripts/backend/types/index.ts new file mode 100644 index 000000000..29f0a9e72 --- /dev/null +++ b/app/scripts/backend/types/index.ts @@ -0,0 +1,12 @@ +/** @format */ + +import { QueryParams, QueryResponse } from "./query" +export * from "./common" + +/** Maps a Korp backend endpoint name to the expected parameters and response */ +export type API = { + query: { + params: QueryParams + response: QueryResponse + } +} diff --git a/app/scripts/backend/types/query.ts b/app/scripts/backend/types/query.ts new file mode 100644 index 000000000..bd28d0fa7 --- /dev/null +++ b/app/scripts/backend/types/query.ts @@ -0,0 +1,48 @@ +/** @format */ +import { ApiKwic } from "./common" + +/** + * The `query` and `relations_sentences` endpoints are combined here. + * @see https://ws.spraakbanken.gu.se/docs/korp#tag/Concordance/paths/~1query/get + * @see https://ws.spraakbanken.gu.se/docs/korp#tag/Word-Picture/paths/~1relations_sentences/get + */ +export type QueryParams = { + /* Required for `query` */ + corpus?: string + /* Required for `query` */ + cqp?: string + start?: number + end?: number + default_context?: string + context?: string + show?: string + show_struct?: string + /** Required for `relations_sentences` */ + source?: string + default_within?: string + within?: string + in_order?: boolean + sort?: string + random_seed?: number + cut?: number + [cqpn: `cqp${number}`]: string + expand_prequeries?: boolean + incremental?: boolean + query_data?: string +} + +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Concordance/paths/~1query/get */ +export type QueryResponse = { + /** Search hits */ + kwic: ApiKwic[] + /** Total number of hits */ + hits: number + /** Number of hits for each corpus */ + corpus_hits: Record + /** Order of corpora in result */ + corpus_order: string[] + /** Execution time in seconds */ + time: number + /** A hash of this query */ + query_data: string +} diff --git a/app/scripts/components/deptree/deptree-util.ts b/app/scripts/components/deptree/deptree-util.ts index 42fa6f4ac..f1387268e 100644 --- a/app/scripts/components/deptree/deptree-util.ts +++ b/app/scripts/components/deptree/deptree-util.ts @@ -1,6 +1,6 @@ /** @format */ -import { Token } from "@/backend/kwic-proxy" +import { Token } from "@/backend/types" import type { BratEntity, BratRelation, BratType, BratVisualizer } from "./deptree_deps" type HoverFunction = (data: Record) => void diff --git a/app/scripts/components/deptree/deptree.ts b/app/scripts/components/deptree/deptree.ts index 80bfe1cb1..7e9a1031b 100644 --- a/app/scripts/components/deptree/deptree.ts +++ b/app/scripts/components/deptree/deptree.ts @@ -3,9 +3,9 @@ import angular, { IController, IScope, ITimeoutService, ui } from "angular" import _ from "lodash" import { html } from "@/util" import { locObj } from "@/i18n" -import { Token } from "@/backend/kwic-proxy" import { CorpusTransformed } from "@/settings/config-transformed.types" import { drawBratTree } from "./deptree-util" +import { Token } from "@/backend/types" type DeptreeController = IController & { tokens: Token[] diff --git a/app/scripts/components/kwic-word.ts b/app/scripts/components/kwic-word.ts index f58e039e4..8f2df5e5c 100644 --- a/app/scripts/components/kwic-word.ts +++ b/app/scripts/components/kwic-word.ts @@ -1,7 +1,7 @@ /** @format */ import angular, { IController, IScope } from "angular" import { html } from "@/util" -import { ApiKwic, Token } from "@/backend/kwic-proxy" +import { ApiKwic, Token } from "@/backend/types" type KwicWordController = IController & { word: Token diff --git a/app/scripts/components/kwic.ts b/app/scripts/components/kwic.ts index e072135ee..c22dcbcfa 100644 --- a/app/scripts/components/kwic.ts +++ b/app/scripts/components/kwic.ts @@ -8,10 +8,10 @@ import { SelectionManager, html, setDownloadLinks } from "@/util" import "@/components/kwic-pager" import "@/components/kwic-word" import { LocationService } from "@/urlparams" -import { ApiKwic, Token } from "@/backend/kwic-proxy" import { LangString } from "@/i18n/types" import { KwicWordScope } from "@/components/kwic-word" import { SelectWordEvent } from "@/statemachine/types" +import { ApiKwic, Token } from "@/backend/types" export type Row = ApiKwic | LinkedKwic | CorpusHeading @@ -388,6 +388,7 @@ angular.module("korpApp").component("kwic", { return sentence.tokens.slice(from, len) } + // TODO Create new tokens instead of modifying the existing ones function massageData(hitArray: ApiKwic[]): Row[] { const punctArray = [",", ".", ";", ":", "!", "?", "..."] diff --git a/app/scripts/components/sidebar.ts b/app/scripts/components/sidebar.ts index 1710e6071..ea0f9958c 100644 --- a/app/scripts/components/sidebar.ts +++ b/app/scripts/components/sidebar.ts @@ -12,10 +12,10 @@ import "@/components/deptree/deptree" import "@/video-controller" // May be used by custom code import { RootScope } from "@/root-scope.types" import { CqpSearchEvent, SelectWordEvent } from "@/statemachine/types" -import { Token } from "@/backend/kwic-proxy" import { CorpusTransformed } from "@/settings/config-transformed.types" import { Attribute, CustomAttribute, MaybeConfigurable } from "@/settings/config.types" import { JQueryExtended } from "@/jquery.types" +import { Token } from "@/backend/types" export type SidebarComponentDefinition = MaybeConfigurable export type SidebarComponent = { diff --git a/app/scripts/components/trend-diagram.ts b/app/scripts/components/trend-diagram.ts index 1723952a4..d15632e26 100644 --- a/app/scripts/components/trend-diagram.ts +++ b/app/scripts/components/trend-diagram.ts @@ -16,7 +16,7 @@ import { loc } from "@/i18n" import { formatUnixDate, getTimeCqp, GRANULARITIES, parseDate, LEVELS, FORMATS, Level } from "@/trend-diagram/util" import "@/components/korp-error" import { GraphTab, RootScope } from "@/root-scope.types" -import { Histogram, KorpResponse, ProgressReport } from "@/backend/types" +import { Histogram, Response, ProgressReport } from "@/backend/types" import { JQueryExtended } from "@/jquery.types" import { CorpusListing } from "@/corpus_listing" @@ -570,7 +570,7 @@ angular.module("korpApp").component("trendDiagram", { function renderGraph( Rickshaw: any, - data: KorpResponse, + data: Response, cqp: string, currentZoom: Level, showTotal?: boolean diff --git a/app/scripts/controllers/example_controller.ts b/app/scripts/controllers/example_controller.ts index 62cf9be32..e0fc4873d 100644 --- a/app/scripts/controllers/example_controller.ts +++ b/app/scripts/controllers/example_controller.ts @@ -5,8 +5,8 @@ import settings from "@/settings" import { KwicCtrl, KwicCtrlScope } from "./kwic_controller" import { LocationService } from "@/urlparams" import { KwicTab, RootScope } from "@/root-scope.types" -import { KorpResponse, ProgressReport } from "@/backend/types" -import { KorpQueryResponse } from "@/backend/kwic-proxy" +import { Response, ProgressReport } from "@/backend/types" +import { QueryResponse } from "@/backend/types/query" import { UtilsService } from "@/services/utils" import "@/services/utils" import { TabHashScope } from "@/directives/tab-hash" @@ -21,10 +21,10 @@ type ExampleCtrlScope = ScopeBase & { hitsPictureData?: any hitspictureClick?: (page: number) => void kwicTab: KwicTab - makeRequest: (isPaging?: boolean) => JQuery.jqXHR> + makeRequest: (isPaging?: boolean) => JQuery.jqXHR> onExampleProgress: (progressObj: ProgressReport, isPaging?: boolean) => void setupReadingWatch: () => void - superRenderResult: (data: KorpResponse) => void + superRenderResult: (data: Response) => void } class ExampleCtrl extends KwicCtrl { diff --git a/app/scripts/controllers/kwic_controller.ts b/app/scripts/controllers/kwic_controller.ts index b4beb0615..097d58568 100644 --- a/app/scripts/controllers/kwic_controller.ts +++ b/app/scripts/controllers/kwic_controller.ts @@ -2,10 +2,11 @@ import angular, { IController, ITimeoutService } from "angular" import _ from "lodash" import settings from "@/settings" -import kwicProxyFactory, { ApiKwic, KorpQueryParams, KorpQueryResponse, type KwicProxy } from "@/backend/kwic-proxy" +import kwicProxyFactory, { type KwicProxy } from "@/backend/kwic-proxy" +import { ApiKwic, Response, ProgressReport } from "@/backend/types" +import { QueryParams, QueryResponse } from "@/backend/types/query" import { RootScope } from "@/root-scope.types" import { LocationService } from "@/urlparams" -import { KorpResponse, ProgressReport } from "@/backend/types" import { UtilsService } from "@/services/utils" import "@/services/utils" import { TabHashScope } from "@/directives/tab-hash" @@ -15,7 +16,7 @@ angular.module("korpApp").directive("kwicCtrl", () => ({ controller: KwicCtrl }) export type KwicCtrlScope = TabHashScope & { active?: boolean aborted?: boolean - buildQueryOptions: (isPaging: boolean) => KorpQueryParams + buildQueryOptions: (isPaging: boolean) => QueryParams corpusHits?: Record countCorpora?: () => number | null corpusOrder?: string[] @@ -44,8 +45,8 @@ export type KwicCtrlScope = TabHashScope & { randomSeed?: number reading_mode?: boolean readingChange: () => void - renderCompleteResult: (data: KorpResponse, isPaging?: boolean) => void - renderResult: (data: KorpResponse) => void + renderCompleteResult: (data: Response, isPaging?: boolean) => void + renderResult: (data: Response) => void tabindex?: number toggleReading: () => void } @@ -181,7 +182,7 @@ export class KwicCtrl implements IController { const cqp = s.cqp || s.proxy.prevCQP if (!cqp) throw new Error("cqp missing") - const params: KorpQueryParams = { + const params: QueryParams = { corpus: settings.corpusListing.stringifySelected(), cqp, query_data: s.proxy.queryData, @@ -219,7 +220,7 @@ export class KwicCtrl implements IController { (progressObj) => $timeout(() => s.onProgress(progressObj, isPaging)), (data) => $timeout(() => s.renderResult(data)) ) - req.done((data: KorpResponse) => { + req.done((data: Response) => { $timeout(() => { s.loading = false s.renderCompleteResult(data, isPaging) diff --git a/app/scripts/controllers/text_reader_controller.ts b/app/scripts/controllers/text_reader_controller.ts index fef5dda38..7ab124b86 100644 --- a/app/scripts/controllers/text_reader_controller.ts +++ b/app/scripts/controllers/text_reader_controller.ts @@ -8,9 +8,9 @@ import "@/components/readingmode" import { RootScope, TextTab } from "@/root-scope.types" import { CorpusTransformed } from "@/settings/config-transformed.types" import { kebabize } from "@/util" -import { ApiKwic, Token } from "@/backend/kwic-proxy" import { TabHashScope } from "@/directives/tab-hash" import { getDataForReadingMode } from "@/backend/backend" +import { ApiKwic, Token } from "@/backend/types" type TextReaderControllerScope = TabHashScope & { loading: boolean diff --git a/app/scripts/controllers/word_picture_controller.ts b/app/scripts/controllers/word_picture_controller.ts index afc34a110..9250ad3c6 100644 --- a/app/scripts/controllers/word_picture_controller.ts +++ b/app/scripts/controllers/word_picture_controller.ts @@ -6,7 +6,7 @@ import lemgramProxyFactory, { ApiRelation, KorpRelationsResponse, LemgramProxy } import { isLemgram, lemgramToString, unregescape } from "@/util" import { RootScope } from "@/root-scope.types" import { LocationService } from "@/urlparams" -import { KorpResponse, ProgressReport } from "@/backend/types" +import { ProgressReport, Response } from "@/backend/types" import { WordPictureDefItem } from "@/settings/app-settings.types" import { TabHashScope } from "@/directives/tab-hash" @@ -30,7 +30,7 @@ type WordpicCtrlScope = TabHashScope & { onProgress: (progressObj: ProgressReport) => void progress: number proxy: LemgramProxy - renderResult: (data: KorpResponse, word: string) => void + renderResult: (data: Response, word: string) => void renderTables: (query: string, data: ApiRelation[]) => void renderWordTables: (query: string, data: ApiRelation[]) => void resetView: () => void diff --git a/app/scripts/jquery.types.ts b/app/scripts/jquery.types.ts index ed8bf3437..acb2287ad 100644 --- a/app/scripts/jquery.types.ts +++ b/app/scripts/jquery.types.ts @@ -1,9 +1,19 @@ +/** @format */ + export type JQueryExtended = JQuery & { + // Defined in /app/lib/jquery.localize.js, a modified version of jquery-localize localize: () => JQueryExtended + // Defined in jq_extensions outerHTML: () => JQueryExtended localeKey: (key: string) => JQueryExtended } export type JQueryStaticExtended = JQueryStatic & { + // Defined in jq_extensions generateFile: (url: string, params: Record) => {} -} \ No newline at end of file +} + +export type AjaxSettings = JQuery.AjaxSettings & { + // Defined in jq_extensions + progress?: (this: TContext, data: any, e: any) => void +} diff --git a/app/scripts/kwic_download.ts b/app/scripts/kwic_download.ts index 02b229b36..351ccccbb 100644 --- a/app/scripts/kwic_download.ts +++ b/app/scripts/kwic_download.ts @@ -4,7 +4,8 @@ import moment from "moment" import CSV from "comma-separated-values/csv" import { locObj } from "@/i18n" import { CorpusHeading, isCorpusHeading, isKwic, Row } from "./components/kwic" -import { type ApiKwic, type KorpQueryParams } from "@/backend/kwic-proxy" +import { ApiKwic } from "./backend/types" +import { QueryParams } from "./backend/types/query" // The annotations option is not available for parallel type AnnotationsRow = ApiKwic | CorpusHeading @@ -22,7 +23,7 @@ function createFile(dataType: string, fileType: string, content: string) { return [filename, blobURL] } -function createSearchInfo(requestInfo: KorpQueryParams, totalHits: number) { +function createSearchInfo(requestInfo: QueryParams, totalHits: number) { return [ `## CQP query: ${requestInfo.cqp}`, `## context: ${requestInfo.default_context}`, @@ -148,7 +149,7 @@ function transformDataToKWIC(data: Row[], searchInfo: string[]) { return res } -function transformData(dataType: "annotations" | "kwic", data: Row[], requestInfo: KorpQueryParams, totalHits: number) { +function transformData(dataType: "annotations" | "kwic", data: Row[], requestInfo: QueryParams, totalHits: number) { const searchInfo = createSearchInfo(requestInfo, totalHits) if (dataType === "annotations") { return transformDataToAnnotations(data as AnnotationsRow[], searchInfo) @@ -172,7 +173,7 @@ export function makeDownload( dataType: "annotations" | "kwic", fileType: "csv" | "tsv", data: Row[], - requestInfo: KorpQueryParams, + requestInfo: QueryParams, totalHits: number ) { const table = transformData(dataType, data, requestInfo, totalHits) diff --git a/app/scripts/statemachine/types.ts b/app/scripts/statemachine/types.ts index 3d84c2bcf..459fa9bb2 100644 --- a/app/scripts/statemachine/types.ts +++ b/app/scripts/statemachine/types.ts @@ -1,5 +1,5 @@ /** @format */ -import { Token } from "@/backend/kwic-proxy" +import { Token } from "@/backend/types" import { CorpusTransformed } from "@/settings/config-transformed.types" /** Mapping from event names to the type of the associated payload. */ diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 11c5a8e4d..ec12d7eb5 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -9,9 +9,9 @@ import { JQueryExtended, JQueryStaticExtended } from "./jquery.types" import { HashParams, LocationService, UrlParams } from "./urlparams" import { AttributeOption } from "./corpus_listing" import { MaybeWithOptions, MaybeConfigurable } from "./settings/config.types" -import { ApiKwic, KorpQueryResponse } from "./backend/kwic-proxy" import { CorpusTransformed } from "./settings/config-transformed.types" -import { isCorpusHeading, LinkedKwic, Row } from "./components/kwic" +import { LinkedKwic, Row } from "./components/kwic" +import { ApiKwic } from "./backend/types" /** Use html`
html here
` to enable formatting template strings with Prettier. */ export const html = String.raw @@ -484,7 +484,7 @@ export function httpConfAddMethodAngular + params: Record ): { url: string; request: RequestInit } { if (calcUrlLength(url, params) > settings.backendURLMaxLength) { const body = new FormData() From b0155c167016e0132db0d9f0356b865641bdbcbc Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Dec 2024 09:57:24 +0100 Subject: [PATCH 2/8] Move Count types --- app/scripts/backend/backend.ts | 7 ++-- app/scripts/backend/graph-proxy.ts | 3 +- app/scripts/backend/stats-proxy.ts | 58 +++++---------------------- app/scripts/backend/types/common.ts | 3 ++ app/scripts/backend/types/count.ts | 60 ++++++++++++++++++++++++++++ app/scripts/backend/types/index.ts | 5 +++ app/scripts/components/statistics.ts | 4 +- app/scripts/statistics.ts | 2 +- app/scripts/statistics.types.ts | 24 +---------- app/scripts/statistics_worker.ts | 11 +---- 10 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 app/scripts/backend/types/count.ts diff --git a/app/scripts/backend/backend.ts b/app/scripts/backend/backend.ts index b26a7424c..d7c05d87a 100644 --- a/app/scripts/backend/backend.ts +++ b/app/scripts/backend/backend.ts @@ -4,11 +4,12 @@ import { getAuthorizationHeader } from "@/components/auth/auth" import { SavedSearch } from "@/local-storage" import settings from "@/settings" import { httpConfAddMethodFetch } from "@/util" -import { KorpStatsParams, KorpStatsResponse, normalizeStatsData } from "@/backend/stats-proxy" +import { normalizeStatsData } from "@/backend/stats-proxy" import { MapResult, parseMapData } from "@/map_services" import { korpRequest } from "./common" import { Response, WithinParameters } from "./types" import { QueryResponse } from "./types/query" +import { CountParams, CountResponse } from "./types/count" type KorpLoglikeResponse = { /** Log-likelihood average. */ @@ -136,7 +137,7 @@ export async function requestMapData( attribute: MapAttribute, relative?: boolean ): Promise { - const params: KorpStatsParams = { + const params: CountParams = { group_by_struct: attribute.label, cqp, corpus: attribute.corpora.join(","), @@ -148,7 +149,7 @@ export async function requestMapData( Object.keys(cqpExprs).map((cqp, i) => (params[`subcqp${i}`] = cqp)) - const data = await korpRequestAny("count", params) + const data = await korpRequest("count", params) if ("ERROR" in data) { // TODO Create a KorpBackendError which could be displayed nicely diff --git a/app/scripts/backend/graph-proxy.ts b/app/scripts/backend/graph-proxy.ts index c778e3771..c62b77952 100644 --- a/app/scripts/backend/graph-proxy.ts +++ b/app/scripts/backend/graph-proxy.ts @@ -2,8 +2,7 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import { Granularity, Histogram, Response, NumericString } from "@/backend/types" -import { AbsRelTuple } from "@/statistics.types" +import { AbsRelTuple, Granularity, Histogram, Response, NumericString } from "@/backend/types" import { Factory, httpConfAddMethod } from "@/util" import { AjaxSettings } from "@/jquery.types" diff --git a/app/scripts/backend/stats-proxy.ts b/app/scripts/backend/stats-proxy.ts index 8f30414ef..f687400e9 100644 --- a/app/scripts/backend/stats-proxy.ts +++ b/app/scripts/backend/stats-proxy.ts @@ -3,17 +3,18 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" import type { Response, ProgressResponse, ProgressReport } from "@/backend/types" -import { StatsNormalized, StatsColumn, StatisticsWorkerResult } from "@/statistics.types" +import { StatisticsWorkerResult } from "@/statistics.types" import { locationSearchGet, httpConfAddMethod, Factory } from "@/util" import { statisticsService } from "@/statistics" import { AjaxSettings } from "@/jquery.types" +import { CountParams, CountResponse, StatsColumn, StatsNormalized } from "./types/count" /** * Stats in the response can be split by subqueries if the `subcqp#` param is used, but otherwise not. * * This function adds a split (converts non-arrays to single-element arrays) if not, so higher code can assume the same shape regardless. */ -export function normalizeStatsData(data: KorpStatsResponse): StatsNormalized { +export function normalizeStatsData(data: CountResponse): StatsNormalized { const combined = !Array.isArray(data.combined) ? [data.combined] : data.combined const corpora: Record = {} @@ -26,8 +27,8 @@ export function normalizeStatsData(data: KorpStatsResponse): StatsNormalized { return { ...data, combined, corpora } } -export class StatsProxy extends BaseProxy { - prevParams: KorpStatsParams | null +export class StatsProxy extends BaseProxy { + prevParams: CountParams | null prevRequest: AjaxSettings | null prevUrl?: string @@ -37,7 +38,7 @@ export class StatsProxy extends BaseProxy { this.prevParams = null } - makeParameters(reduceVals: string[], cqp: string, ignoreCase: boolean): KorpStatsParams { + makeParameters(reduceVals: string[], cqp: string, ignoreCase: boolean): CountParams { const structAttrs = settings.corpusListing.getStructAttrs(settings.corpusListing.getReduceLang()) const groupBy: string[] = [] const groupByStruct: string[] = [] @@ -67,7 +68,7 @@ export class StatsProxy extends BaseProxy { makeRequest( cqp: string, - callback: (data: ProgressReport) => void + callback: (data: ProgressReport) => void ): JQuery.Promise { const self = this this.resetRequest() @@ -114,7 +115,7 @@ export class StatsProxy extends BaseProxy { const def: JQuery.Deferred = $.Deferred() const url = settings.korp_backend_url + "/count" - const ajaxSettings: AjaxSettings> = { + const ajaxSettings: AjaxSettings> = { url, data, beforeSend(req, settings) { @@ -138,7 +139,7 @@ export class StatsProxy extends BaseProxy { } }, - success: (data: Response) => { + success: (data: Response) => { self.cleanup() if ("ERROR" in data) { console.log("gettings stats failed with error", data.ERROR) @@ -157,7 +158,7 @@ export class StatsProxy extends BaseProxy { ) }, } - this.pendingRequests.push($.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR) + this.pendingRequests.push($.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR) return def.promise() } @@ -165,42 +166,3 @@ export class StatsProxy extends BaseProxy { const statsProxyFactory = new Factory(StatsProxy) export default statsProxyFactory - -/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Statistics/paths/~1count/get */ -export type KorpStatsParams = { - /** Corpus names, separated by comma */ - corpus: string - /** CQP query */ - cqp: string - /** Positional attribute by which the hits should be grouped. Defaults to "word" if neither `group_by` nor `group_by_struct` is defined */ - group_by?: string - /** Structural attribute by which the hits should be grouped. The value for the first token of the hit will be used */ - group_by_struct?: string - /** Prevent search from crossing boundaries of the given structural attribute, e.g. 'sentence'. */ - default_within?: string - /** Like default_within, but for specific corpora, overriding the default. Specified using the format 'corpus:attribute' */ - within?: string - ignore_case?: string - relative_to_struct?: string - split?: string - top?: string - [cqpn: `cqp${number}`]: string - expand_prequeries?: boolean - [subcqpn: `subcqp${number}`]: string - start?: number - end?: number - /** Incrementally return progress updates when the calculation for each corpus is finished */ - incremental?: boolean -} - -/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Statistics/paths/~1count/get */ -export type KorpStatsResponse = { - corpora: { - [name: string]: StatsColumn | StatsColumn[] - } - combined: StatsColumn | StatsColumn[] - /** Total number of different values */ - count: number - /** Execution time in seconds */ - time: number -} diff --git a/app/scripts/backend/types/common.ts b/app/scripts/backend/types/common.ts index ec4e809c5..f5bb72cfe 100644 --- a/app/scripts/backend/types/common.ts +++ b/app/scripts/backend/types/common.ts @@ -40,6 +40,9 @@ export type ProgressReport = { total_results: number | null } +/** Frequency count as absolute and relative (to some total size). */ +export type AbsRelTuple = { absolute: number; relative: number } + /** A string consisting of numbers. */ export type NumericString = `${number}` diff --git a/app/scripts/backend/types/count.ts b/app/scripts/backend/types/count.ts new file mode 100644 index 000000000..3db4708a6 --- /dev/null +++ b/app/scripts/backend/types/count.ts @@ -0,0 +1,60 @@ +/** @format */ +import { AbsRelTuple } from "./common" + +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Statistics/paths/~1count/get */ +export type CountParams = { + /** Corpus names, separated by comma */ + corpus: string + /** CQP query */ + cqp: string + /** Positional attribute by which the hits should be grouped. Defaults to "word" if neither `group_by` nor `group_by_struct` is defined */ + group_by?: string + /** Structural attribute by which the hits should be grouped. The value for the first token of the hit will be used */ + group_by_struct?: string + /** Prevent search from crossing boundaries of the given structural attribute, e.g. 'sentence'. */ + default_within?: string + /** Like default_within, but for specific corpora, overriding the default. Specified using the format 'corpus:attribute' */ + within?: string + ignore_case?: string + relative_to_struct?: string + split?: string + top?: string + [cqpn: `cqp${number}`]: string + expand_prequeries?: boolean + [subcqpn: `subcqp${number}`]: string + start?: number + end?: number + /** Incrementally return progress updates when the calculation for each corpus is finished */ + incremental?: boolean +} + +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Statistics/paths/~1count/get */ +export type CountResponse = { + corpora: { + [name: string]: StatsColumn | StatsColumn[] + } + combined: StatsColumn | StatsColumn[] + /** Total number of different values */ + count: number + /** Execution time in seconds */ + time: number +} + +/** Like `CountResponse` but the stats are necessarily arrays. */ +export type StatsNormalized = { + corpora: { + [name: string]: StatsColumn[] + } + combined: StatsColumn[] + count: number + time: number +} + +export type StatsColumn = { + sums: AbsRelTuple + rows: StatsRow[] +} + +export type StatsRow = AbsRelTuple & { + value: Record +} diff --git a/app/scripts/backend/types/index.ts b/app/scripts/backend/types/index.ts index 29f0a9e72..a1788587a 100644 --- a/app/scripts/backend/types/index.ts +++ b/app/scripts/backend/types/index.ts @@ -1,10 +1,15 @@ /** @format */ +import { CountParams, CountResponse } from "./count" import { QueryParams, QueryResponse } from "./query" export * from "./common" /** Maps a Korp backend endpoint name to the expected parameters and response */ export type API = { + count: { + params: CountParams + response: CountResponse + } query: { params: QueryParams response: QueryResponse diff --git a/app/scripts/components/statistics.ts b/app/scripts/components/statistics.ts index ed72769c5..8a2c1f2df 100644 --- a/app/scripts/components/statistics.ts +++ b/app/scripts/components/statistics.ts @@ -8,7 +8,6 @@ import { loc, locObj } from "@/i18n" import { getCqp } from "../../config/statistics_config" import { expandOperators } from "@/cqp_parser/cqp" import { requestMapData } from "@/backend/backend" -import { KorpStatsParams } from "@/backend/stats-proxy" import "@/backend/backend" import "@/services/searches" import "@/components/corpus-distribution-chart" @@ -16,6 +15,7 @@ import { SearchesService } from "@/services/searches" import { RootScope } from "@/root-scope.types" import { JQueryExtended } from "@/jquery.types" import { AbsRelSeq, Dataset, isTotalRow, Row, SearchParams, SingleRow, SlickgridColumn } from "@/statistics.types" +import { CountParams } from "@/backend/types/count" type StatisticsScope = IScope & { rowData: { title: string; values: AbsRelSeq }[] @@ -33,7 +33,7 @@ type StatisticsController = IController & { inOrder: boolean loading: boolean noHits: boolean - prevParams: KorpStatsParams + prevParams: CountParams searchParams: SearchParams showStatistics: boolean sortColumn?: string diff --git a/app/scripts/statistics.ts b/app/scripts/statistics.ts index 6456464cd..d647235be 100644 --- a/app/scripts/statistics.ts +++ b/app/scripts/statistics.ts @@ -2,11 +2,11 @@ import _ from "lodash" import settings from "@/settings" import { reduceStringify } from "../config/statistics_config" +import { StatsNormalized } from "@/backend/types/count" import { Dataset, isTotalRow, Row, - StatsNormalized, StatisticsWorkerMessage, StatisticsWorkerResult, SearchParams, diff --git a/app/scripts/statistics.types.ts b/app/scripts/statistics.types.ts index 06c4f558a..44720a22d 100644 --- a/app/scripts/statistics.types.ts +++ b/app/scripts/statistics.types.ts @@ -1,6 +1,6 @@ /** @format*/ // TODO: Merge with @/interfaces/stats.ts - +import { StatsNormalized } from "./backend/types/count" import { LangString } from "./i18n/types" export type StatisticsWorkerMessage = { @@ -16,28 +16,6 @@ export type SlickgridColumn = Slick.Column & { translation?: LangString } -/** Like `KorpStatsResponse` but the stats are necessarily arrays. */ -export type StatsNormalized = { - corpora: { - [name: string]: StatsColumn[] - } - combined: StatsColumn[] - count: number - time: number -} - -export type StatsColumn = { - sums: AbsRelTuple - rows: StatsRow[] -} - -/** Frequency count as absolute and relative (to some total size). */ -export type AbsRelTuple = { absolute: number; relative: number } - -export type StatsRow = AbsRelTuple & { - value: Record -} - export type SearchParams = { reduceVals: string[] ignoreCase: boolean diff --git a/app/scripts/statistics_worker.ts b/app/scripts/statistics_worker.ts index 8187111a9..d2d42fdc2 100644 --- a/app/scripts/statistics_worker.ts +++ b/app/scripts/statistics_worker.ts @@ -6,15 +6,8 @@ import isArray from "lodash/isArray" import keys from "lodash/keys" import { RowsEntity } from "./interfaces/stats" -import { - AbsRelSeq, - Dataset, - SingleRow, - TotalRow, - StatisticsWorkerMessage, - StatsNormalized, - StatsRow, -} from "./statistics.types" +import { StatsNormalized, StatsRow } from "./backend/types/count" +import { AbsRelSeq, Dataset, SingleRow, TotalRow, StatisticsWorkerMessage } from "./statistics.types" /* This is optimized code for transforming the statistics data. From fc7130f2ebf1d8628c8e996d07646f73d2bf0c4d Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Dec 2024 10:22:51 +0100 Subject: [PATCH 3/8] Move Loglike types --- app/scripts/backend/backend.ts | 30 +++------------------------- app/scripts/backend/types/index.ts | 6 ++++++ app/scripts/backend/types/loglike.ts | 27 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 app/scripts/backend/types/loglike.ts diff --git a/app/scripts/backend/backend.ts b/app/scripts/backend/backend.ts index d7c05d87a..d09282caf 100644 --- a/app/scripts/backend/backend.ts +++ b/app/scripts/backend/backend.ts @@ -1,26 +1,13 @@ /** @format */ import _ from "lodash" -import { getAuthorizationHeader } from "@/components/auth/auth" import { SavedSearch } from "@/local-storage" import settings from "@/settings" -import { httpConfAddMethodFetch } from "@/util" import { normalizeStatsData } from "@/backend/stats-proxy" import { MapResult, parseMapData } from "@/map_services" import { korpRequest } from "./common" import { Response, WithinParameters } from "./types" import { QueryResponse } from "./types/query" -import { CountParams, CountResponse } from "./types/count" - -type KorpLoglikeResponse = { - /** Log-likelihood average. */ - average: number - /** Log-likelihood values. */ - loglike: Record - /** Absolute frequency for the values in set 1. */ - set1: Record - /** Absolute frequency for the values in set 2. */ - set2: Record -} +import { CountParams } from "./types/count" export type CompareResult = [CompareTables, number, SavedSearch, SavedSearch, string[]] @@ -54,17 +41,6 @@ export type MapRequestResult = { type MapAttribute = { label: string; corpora: string[] } -// TODO Remove in favor of `korpRequest` -async function korpRequestAny = {}, P extends Record = {}>( - endpoint: string, - params: P -): Promise> { - const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/" + endpoint, params) - request.headers = { ...request.headers, ...getAuthorizationHeader() } - const response = await fetch(url, request) - return (await response.json()) as Response -} - /** Note: since this is using native Promise, we must use it with something like $q or $scope.$apply for AngularJS to react when they resolve. */ export async function requestCompare( cmpObj1: SavedSearch, @@ -89,12 +65,12 @@ export async function requestCompare( set1_cqp: cmpObj1.cqp, set2_corpus: corpora2.join(",").toUpperCase(), set2_cqp: cmpObj2.cqp, - max: "50", + max: 50, split, top, } - const data = await korpRequestAny("loglike", params) + const data = await korpRequest("loglike", params) if ("ERROR" in data) { // TODO Create a KorpBackendError which could be displayed nicely diff --git a/app/scripts/backend/types/index.ts b/app/scripts/backend/types/index.ts index a1788587a..0266a6441 100644 --- a/app/scripts/backend/types/index.ts +++ b/app/scripts/backend/types/index.ts @@ -1,7 +1,9 @@ /** @format */ import { CountParams, CountResponse } from "./count" +import { LoglikeParams, LoglikeResponse } from "./loglike" import { QueryParams, QueryResponse } from "./query" + export * from "./common" /** Maps a Korp backend endpoint name to the expected parameters and response */ @@ -10,6 +12,10 @@ export type API = { params: CountParams response: CountResponse } + loglike: { + params: LoglikeParams + response: LoglikeResponse + } query: { params: QueryParams response: QueryResponse diff --git a/app/scripts/backend/types/loglike.ts b/app/scripts/backend/types/loglike.ts new file mode 100644 index 000000000..d84d1ad62 --- /dev/null +++ b/app/scripts/backend/types/loglike.ts @@ -0,0 +1,27 @@ +/** @format */ + +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Misc/paths/~1loglike/get */ +export type LoglikeParams = { + set1_corpus: string + set1_cqp: string + set2_corpus: string + set2_cqp: string + group_by?: string + group_by_struct?: string + ignore_case?: boolean + max: number + split: string + top: string +} + +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Misc/paths/~1loglike/get */ +export type LoglikeResponse = { + /** Log-likelihood average. */ + average: number + /** Log-likelihood values. */ + loglike: Record + /** Absolute frequency for the values in set 1. */ + set1: Record + /** Absolute frequency for the values in set 2. */ + set2: Record +} From 3170cf71d6b14ffa808afabced31f4eb6e9c0b8d Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Dec 2024 11:20:31 +0100 Subject: [PATCH 4/8] refactor: remove prevRequest, use prevUrl That means less dependency on JQuery and aligns with CSCFi modifications of setDownloadLinks --- app/scripts/backend/base-proxy.ts | 27 --------------------------- app/scripts/backend/graph-proxy.ts | 4 +--- app/scripts/backend/kwic-proxy.ts | 7 ++----- app/scripts/backend/lemgram-proxy.ts | 5 +---- app/scripts/backend/stats-proxy.ts | 5 +---- app/scripts/components/kwic.ts | 8 ++++---- app/scripts/components/results.ts | 4 ++-- app/scripts/util.ts | 13 +++++-------- 8 files changed, 16 insertions(+), 57 deletions(-) diff --git a/app/scripts/backend/base-proxy.ts b/app/scripts/backend/base-proxy.ts index 7a7761454..f3d21450a 100644 --- a/app/scripts/backend/base-proxy.ts +++ b/app/scripts/backend/base-proxy.ts @@ -41,33 +41,6 @@ export default abstract class BaseProxy { this.total = null } - /** - * Return a URL with baseUrl base and data encoded as URL parameters. - * If baseUrl already contains URL parameters, return it as is. - * - * Note that this function is now largely redundant: when called - * for GET URLs already containing URL parameters, it does - * nothing, whereas the GET URL returned by it for a POST URL - * typically results in an "URI too long" error, if - * settings.backendURLMaxLength is configured appropriately for - * the Web server on which the backend runs. - */ - makeUrlWithParams(baseUrl: string, data: Record): string { - if (baseUrl.indexOf("?") != -1) { - return baseUrl - } - return ( - baseUrl + - "?" + - _.toPairs(data) - .map(function ([key, val]) { - val = encodeURIComponent(val) - return key + "=" + val - }) - .join("&") - ) - } - abort(): void { this.pendingRequests.forEach((req) => req.abort()) this.cleanup() diff --git a/app/scripts/backend/graph-proxy.ts b/app/scripts/backend/graph-proxy.ts index 27af43377..a0c3b7103 100644 --- a/app/scripts/backend/graph-proxy.ts +++ b/app/scripts/backend/graph-proxy.ts @@ -9,7 +9,6 @@ import { Factory, httpConfAddMethod } from "@/util" export class GraphProxy extends BaseProxy { granularity: Granularity prevParams: KorpCountTimeParams | null - prevRequest: AjaxSettings constructor() { super() @@ -61,8 +60,7 @@ export class GraphProxy extends BaseProxy { dataType: "json", data: params, - beforeSend: (req, settings) => { - this.prevRequest = settings + beforeSend: (req) => { this.addAuthorizationHeader(req) }, diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index a414dcfe3..dadf3e91e 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -8,13 +8,11 @@ import { locationSearchGet, httpConfAddMethod, Factory } from "@/util" export class KwicProxy extends BaseProxy { prevCQP?: string prevParams: KorpQueryParams | null - prevRequest: AjaxSettings | null // Used for download - prevUrl?: string + prevUrl?: string // Used for download queryData?: string constructor() { super() - this.prevRequest = null this.queryData = undefined this.prevParams = null } @@ -94,9 +92,8 @@ export class KwicProxy extends BaseProxy { url: settings.korp_backend_url + "/" + command, data: data, beforeSend(req, settings) { - self.prevRequest = settings self.addAuthorizationHeader(req) - self.prevUrl = self.makeUrlWithParams(this.url, data) + self.prevUrl = settings.url }, success(data: KorpQueryResponse, status, jqxhr) { diff --git a/app/scripts/backend/lemgram-proxy.ts b/app/scripts/backend/lemgram-proxy.ts index 4fc7dc8d9..80165ed1d 100644 --- a/app/scripts/backend/lemgram-proxy.ts +++ b/app/scripts/backend/lemgram-proxy.ts @@ -6,7 +6,6 @@ import { Factory, httpConfAddMethod } from "@/util" export class LemgramProxy extends BaseProxy { prevParams?: KorpRelationsParams - prevRequest?: AjaxSettings prevUrl?: string makeRequest( @@ -31,7 +30,6 @@ export class LemgramProxy extends BaseProxy { data: params, success() { - self.prevRequest = params self.cleanup() }, @@ -44,9 +42,8 @@ export class LemgramProxy extends BaseProxy { }, beforeSend(req, settings) { - self.prevRequest = settings self.addAuthorizationHeader(req) - self.prevUrl = self.makeUrlWithParams(this.url, params) + self.prevUrl = settings.url }, } diff --git a/app/scripts/backend/stats-proxy.ts b/app/scripts/backend/stats-proxy.ts index 86d199c7d..43fce2601 100644 --- a/app/scripts/backend/stats-proxy.ts +++ b/app/scripts/backend/stats-proxy.ts @@ -27,12 +27,10 @@ export function normalizeStatsData(data: KorpStatsResponse): StatsNormalized { export class StatsProxy extends BaseProxy { prevParams: KorpStatsParams | null - prevRequest: AjaxSettings | null prevUrl?: string constructor() { super() - this.prevRequest = null this.prevParams = null } @@ -117,9 +115,8 @@ export class StatsProxy extends BaseProxy { url, data, beforeSend(req, settings) { - self.prevRequest = settings self.addAuthorizationHeader(req) - self.prevUrl = self.makeUrlWithParams(url, data) + self.prevUrl = settings.url }, error(jqXHR, textStatus, errorThrown) { diff --git a/app/scripts/components/kwic.ts b/app/scripts/components/kwic.ts index e072135ee..028c5c4c4 100644 --- a/app/scripts/components/kwic.ts +++ b/app/scripts/components/kwic.ts @@ -45,7 +45,7 @@ type KwicController = IController & { contextChangeEvent: () => void hitsPerPage: number prevParams: any - prevRequest: any + prevUrl?: string corpusOrder: string[] /** Current page of results. */ kwicInput: ApiKwic[] @@ -230,7 +230,7 @@ angular.module("korpApp").component("kwic", { contextChangeEvent: "<", hitsPerPage: "<", prevParams: "<", - prevRequest: "<", + prevUrl: "<", corpusOrder: "<", kwicInput: "<", corpusHits: "<", @@ -259,8 +259,8 @@ angular.module("korpApp").component("kwic", { }) } - if (settings["enable_backend_kwic_download"]) { - setDownloadLinks($ctrl.prevRequest, { + if (settings["enable_backend_kwic_download"] && $ctrl.prevUrl) { + setDownloadLinks($ctrl.prevUrl, { kwic: $ctrl.kwic, corpus_order: $ctrl.corpusOrder, }) diff --git a/app/scripts/components/results.ts b/app/scripts/components/results.ts index f58dade98..ce1c80101 100644 --- a/app/scripts/components/results.ts +++ b/app/scripts/components/results.ts @@ -60,7 +60,7 @@ angular.module("korpApp").component("results", { context-change-event="toggleReading" hits-per-page="hitsPerPage" prev-params="proxy.prevParams" - prev-request="proxy.prevRequest" + prev-url="proxy.prevUrl" corpus-order="corpusOrder" > @@ -153,7 +153,7 @@ angular.module("korpApp").component("results", { context-change-event="toggleReading" hits-per-page="hitsPerPage" prev-params="proxy.prevParams" - prev-request="proxy.prevRequest" + prev-url="proxy.prevUrl" corpus-order="corpusOrder" > diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 11c5a8e4d..3372e5cbd 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -312,15 +312,10 @@ function numberToSuperscript(number: string | number): string { // settings["download_formats"] (Jyrki Niemi // 2014-02-26/04-30) -export function setDownloadLinks( - xhr_settings: JQuery.AjaxSettings, - result_data: { kwic: Row[]; corpus_order: string[] } -): void { +export function setDownloadLinks(query_url: string, result_data: { kwic: Row[]; corpus_order: string[] }): void { // If some of the required parameters are null, return without // adding the download links. - if ( - !(xhr_settings != null && result_data != null && result_data.corpus_order != null && result_data.kwic != null) - ) { + if (!(query_url != null && result_data != null && result_data.corpus_order != null && result_data.kwic != null)) { console.log("failed to do setDownloadLinks") return } @@ -377,8 +372,10 @@ export function setDownloadLinks( class="download_link">${format.toUpperCase()}\ `) + const query_params = JSON.stringify(Object.fromEntries(new URL(query_url).searchParams)) + const download_params = { - query_params: xhr_settings.url, + query_params, format, korp_url: window.location.href, korp_server_url: settings.korp_backend_url, From 968ef66dc47a7fd89da4d8d6b12f5a96749e8917 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Dec 2024 15:45:32 +0100 Subject: [PATCH 5/8] refactor: removed httpConfAddMethodAngular --- CHANGELOG.md | 4 ++++ app/scripts/util.ts | 25 ------------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb499257c..8467fbba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Removed `httpConfAddMethodAngular` + ## [9.7.2] - 2024-12-09 ### Added diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 96652494c..8eef39c33 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -449,31 +449,6 @@ export function httpConfAddMethod(conf: T & { url: string }): T { - const fixedConf = httpConfAddMethod(conf) - - if (fixedConf.method == "POST") { - const formDataParams = new FormData() - for (var key in fixedConf.data) { - formDataParams.append(key, fixedConf.data[key]) - } - fixedConf.data = formDataParams - - if (!fixedConf.headers) { - fixedConf.headers = {} - } - // will be set correct automatically by Angular - fixedConf.headers["Content-Type"] = undefined - } - - return fixedConf -} - /** * Like `httpConfAddMethod`, but for use with native `fetch()`. * @param conf A $http or jQuery.ajax configuration object. From 2476a71434863d2eeab0dceb698d079d219f1663 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Dec 2024 16:38:59 +0100 Subject: [PATCH 6/8] refactor: httpConfAddMethod* util functions --- CHANGELOG.md | 5 +- app/scripts/backend/common.ts | 4 +- app/scripts/backend/graph-proxy.ts | 8 +-- app/scripts/backend/kwic-proxy.ts | 8 +-- app/scripts/backend/lemgram-proxy.ts | 8 +-- app/scripts/backend/stats-proxy.ts | 8 +-- app/scripts/backend/time-proxy.ts | 8 +-- app/scripts/data_init.ts | 4 +- app/scripts/jquery.types.ts | 5 +- app/scripts/karp.ts | 4 +- app/scripts/util.ts | 77 +++++++++++++++------------- 11 files changed, 74 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8467fbba7..47f610a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ ### Changed -- Removed `httpConfAddMethodAngular` +- The `httpConfAddMethod*` util functions were refactored: + - The `$.ajax` case of `httpConfAddMethod` was extracted into `ajaxConfAddMethod` + - `httpConfAddMethodFetch` was renamed to `fetchConfAddMethod` + - `httpConfAddMethodAngular` was removed ## [9.7.2] - 2024-12-09 diff --git a/app/scripts/backend/common.ts b/app/scripts/backend/common.ts index bddca468d..d1953544f 100644 --- a/app/scripts/backend/common.ts +++ b/app/scripts/backend/common.ts @@ -1,5 +1,5 @@ /** @format */ -import { httpConfAddMethodFetch } from "@/util" +import { fetchConfAddMethod } from "@/util" import { getAuthorizationHeader } from "@/components/auth/auth" import settings from "@/settings" import { API, Response } from "./types" @@ -8,7 +8,7 @@ export async function korpRequest( endpoint: K, params: API[K]["params"] ): Promise> { - const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/" + endpoint, params) + const { url, request } = fetchConfAddMethod(settings.korp_backend_url + "/" + endpoint, params) request.headers = { ...request.headers, ...getAuthorizationHeader() } const response = await fetch(url, request) return (await response.json()) as Response diff --git a/app/scripts/backend/graph-proxy.ts b/app/scripts/backend/graph-proxy.ts index efd5a081b..d1ac13354 100644 --- a/app/scripts/backend/graph-proxy.ts +++ b/app/scripts/backend/graph-proxy.ts @@ -3,7 +3,7 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" import { AbsRelTuple, Granularity, Histogram, Response, NumericString } from "@/backend/types" -import { Factory, httpConfAddMethod } from "@/util" +import { ajaxConfAddMethod, Factory } from "@/util" import { AjaxSettings } from "@/jquery.types" export class GraphProxy extends BaseProxy { @@ -55,7 +55,7 @@ export class GraphProxy extends BaseProxy { this.prevParams = params const def = $.Deferred() - const ajaxSettings: AjaxSettings = { + const ajaxSettings = { url: settings.korp_backend_url + "/count_time", dataType: "json", data: params, @@ -79,9 +79,9 @@ export class GraphProxy extends BaseProxy { def.resolve(data) self.cleanup() }, - } + } satisfies AjaxSettings - $.ajax(httpConfAddMethod(ajaxSettings)) + $.ajax(ajaxConfAddMethod(ajaxSettings)) return def.promise() } diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index 22a19c88f..03fcdef0b 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -2,7 +2,7 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" -import { locationSearchGet, httpConfAddMethod, Factory } from "@/util" +import { locationSearchGet, Factory, ajaxConfAddMethod } from "@/util" import { ProgressReport, Response } from "./types" import { QueryParams, QueryResponse } from "./types/query" import { AjaxSettings } from "@/jquery.types" @@ -90,7 +90,7 @@ export class KwicProxy extends BaseProxy { } this.prevParams = data - const ajaxSettings: AjaxSettings = { + const ajaxSettings = { url: settings.korp_backend_url + "/" + command, data: data, beforeSend(req, settings) { @@ -117,9 +117,9 @@ export class KwicProxy extends BaseProxy { kwicCallback(progressObj.struct as QueryResponse) } }, - } + } satisfies AjaxSettings - const def = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR> + const def = $.ajax(ajaxConfAddMethod(ajaxSettings)) as JQuery.jqXHR> this.pendingRequests.push(def) return def } diff --git a/app/scripts/backend/lemgram-proxy.ts b/app/scripts/backend/lemgram-proxy.ts index 3086e56bd..79883c5b3 100644 --- a/app/scripts/backend/lemgram-proxy.ts +++ b/app/scripts/backend/lemgram-proxy.ts @@ -2,7 +2,7 @@ import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" import type { Response, ProgressReport } from "@/backend/types" -import { Factory, httpConfAddMethod } from "@/util" +import { ajaxConfAddMethod, Factory } from "@/util" import { AjaxSettings } from "@/jquery.types" export class LemgramProxy extends BaseProxy { @@ -26,7 +26,7 @@ export class LemgramProxy extends BaseProxy { } this.prevParams = params - const ajaxSettings: AjaxSettings = { + const ajaxSettings = { url: settings.korp_backend_url + "/relations", data: params, @@ -46,9 +46,9 @@ export class LemgramProxy extends BaseProxy { self.addAuthorizationHeader(req) self.prevUrl = settings.url }, - } + } satisfies AjaxSettings - const def = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR> + const def = $.ajax(ajaxConfAddMethod(ajaxSettings)) as JQuery.jqXHR> this.pendingRequests.push(def) return def } diff --git a/app/scripts/backend/stats-proxy.ts b/app/scripts/backend/stats-proxy.ts index 7a48dc0b4..0065f39eb 100644 --- a/app/scripts/backend/stats-proxy.ts +++ b/app/scripts/backend/stats-proxy.ts @@ -4,7 +4,7 @@ import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" import type { Response, ProgressResponse, ProgressReport } from "@/backend/types" import { StatisticsWorkerResult } from "@/statistics.types" -import { locationSearchGet, httpConfAddMethod, Factory } from "@/util" +import { locationSearchGet, Factory, ajaxConfAddMethod } from "@/util" import { statisticsService } from "@/statistics" import { AjaxSettings } from "@/jquery.types" import { CountParams, CountResponse, StatsColumn, StatsNormalized } from "./types/count" @@ -113,7 +113,7 @@ export class StatsProxy extends BaseProxy { const def: JQuery.Deferred = $.Deferred() const url = settings.korp_backend_url + "/count" - const ajaxSettings: AjaxSettings> = { + const ajaxSettings = { url, data, beforeSend(req, settings) { @@ -154,8 +154,8 @@ export class StatsProxy extends BaseProxy { cqp ) }, - } - this.pendingRequests.push($.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR) + } satisfies AjaxSettings + this.pendingRequests.push($.ajax(ajaxConfAddMethod(ajaxSettings)) as JQuery.jqXHR) return def.promise() } diff --git a/app/scripts/backend/time-proxy.ts b/app/scripts/backend/time-proxy.ts index ced7789de..14896351c 100644 --- a/app/scripts/backend/time-proxy.ts +++ b/app/scripts/backend/time-proxy.ts @@ -3,7 +3,7 @@ import _ from "lodash" import settings from "@/settings" import BaseProxy from "@/backend/base-proxy" import type { Granularity, Histogram, Response, NumericString } from "@/backend/types" -import { Factory, httpConfAddMethod } from "@/util" +import { ajaxConfAddMethod, Factory } from "@/util" import { AjaxSettings } from "@/jquery.types" export class TimeProxy extends BaseProxy { @@ -14,11 +14,11 @@ export class TimeProxy extends BaseProxy { } const dfd = $.Deferred() - const ajaxSettings: AjaxSettings = { + const ajaxSettings = { url: settings.korp_backend_url + "/timespan", data, - } - const xhr = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR + } satisfies AjaxSettings + const xhr = $.ajax(ajaxConfAddMethod(ajaxSettings)) as JQuery.jqXHR xhr.done((data) => { if ("ERROR" in data) { diff --git a/app/scripts/data_init.ts b/app/scripts/data_init.ts index bf3dd9c45..c2d966e07 100644 --- a/app/scripts/data_init.ts +++ b/app/scripts/data_init.ts @@ -7,7 +7,7 @@ import timeProxyFactory from "@/backend/time-proxy" import { getAllCorporaInFolders } from "./components/corpus-chooser/util" import { CorpusListing } from "./corpus_listing" import { ParallelCorpusListing } from "./parallel/corpus_listing" -import { fromKeys, httpConfAddMethodFetch } from "@/util" +import { fromKeys, fetchConfAddMethod } from "@/util" import { Labeled, LangLocMap, LocMap } from "./i18n/types" import { CorpusInfoResponse } from "./settings/corpus-info.types" import { Attribute, Config, Corpus, CorpusParallel, CustomAttribute } from "./settings/config.types" @@ -47,7 +47,7 @@ type InfoData = Record { const params = { corpus: corpusIds.map((id) => id.toUpperCase()).join(",") } - const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/corpus_info", params) + const { url, request } = fetchConfAddMethod(settings.korp_backend_url + "/corpus_info", params) const response = await fetch(url, request) const data = (await response.json()) as CorpusInfoResponse diff --git a/app/scripts/jquery.types.ts b/app/scripts/jquery.types.ts index acb2287ad..bb0b63d54 100644 --- a/app/scripts/jquery.types.ts +++ b/app/scripts/jquery.types.ts @@ -13,7 +13,8 @@ export type JQueryStaticExtended = JQueryStatic & { generateFile: (url: string, params: Record) => {} } -export type AjaxSettings = JQuery.AjaxSettings & { +export type AjaxSettings

> = JQuery.AjaxSettings & { + data?: P // Defined in jq_extensions - progress?: (this: TContext, data: any, e: any) => void + progress?: (this: any, data: any, e: any) => void } diff --git a/app/scripts/karp.ts b/app/scripts/karp.ts index 3dc1c633a..fbd55316b 100644 --- a/app/scripts/karp.ts +++ b/app/scripts/karp.ts @@ -1,4 +1,6 @@ /** @format */ +import { buildUrl } from "./util" + const karpURL = "https://spraakbanken4.it.gu.se/karp/v7" export type KarpResponse = { @@ -30,7 +32,7 @@ const equals = (field: string, values: string[]) => /** Query lexicons in the Karp API */ async function query(lexicons: string[], q: string, path: string, params?: object) { const url = `${karpURL}/query/${lexicons.join(",")}` - const response = await fetch(url + "?" + new URLSearchParams({ q, path, ...params })) + const response = await fetch(buildUrl(url, { q, path, ...params })) return (await response.json()) as KarpResponse } diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 8eef39c33..5ce531d87 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -5,7 +5,7 @@ import settings from "@/settings" import { getLang, loc, locObj } from "@/i18n" import { LangString } from "./i18n/types" import { RootScope } from "./root-scope.types" -import { JQueryExtended, JQueryStaticExtended } from "./jquery.types" +import { AjaxSettings, JQueryExtended, JQueryStaticExtended } from "./jquery.types" import { HashParams, LocationService, UrlParams } from "./urlparams" import { AttributeOption } from "./corpus_listing" import { MaybeWithOptions, MaybeConfigurable } from "./settings/config.types" @@ -421,52 +421,55 @@ export const regescape = (s: string): string => s.replace(/[.|?|+|*||'|()^$\\]/g /** Unescape special characters in a regular expression – remove single backslashes and replace double with single. */ export const unregescape = (s: string): string => s.replace(/\\\\|\\/g, (match) => (match === "\\\\" ? "\\" : "")) -/** Return the length of baseUrl with params added. */ -const calcUrlLength = (baseUrl: string, params: any): number => - baseUrl.length + new URLSearchParams(params).toString().length + 1 - /** - * Add HTTP method to the HTTP configuration object conf for jQuery.ajax or AngularJS $http call: - * if the result URL would be longer than settings.backendURLMaxLength, use POST, otherwise GET. - * @param conf A $http or jQuery.ajax configuration object. - * For $http, the request parameters should be in `params` (moved to `data` for POST), - * and for jQuery.ajax, they should be in `data`. - * @returns The same object, possibly modified in-place + * Select GET or POST depending on url length, modifies conf in-place. */ -export function httpConfAddMethod(conf: T): T { - // The property to use for GET: AngularJS $http uses params for - // GET and data for POST, whereas jQuery.ajax uses data for both - const data = "params" in conf ? conf.params : conf.data - if (calcUrlLength(conf.url!, data) > settings.backendURLMaxLength) { - conf.method = "POST" - conf.data = data - if ("params" in conf) delete conf.params - } else { - conf.method = "GET" - if ("params" in conf) conf.params = data - else conf.data = data +export function httpConfAddMethod(conf: IRequestConfig & { url: string }): IRequestConfig { + // $http uses `data` for POST but `params` for GET. + conf.method = selectHttpMethod(conf.url, conf.params) + if (conf.method == "POST") { + conf.data = conf.params + delete conf.params } return conf } +/** + * Select GET or POST depending on url length, modifies conf in-place. + */ +export function ajaxConfAddMethod(conf: T): T { + // $.ajax uses `data` for both GET and POST. + conf.method = selectHttpMethod(conf.url!, conf.data || {}) + return conf +} + /** * Like `httpConfAddMethod`, but for use with native `fetch()`. - * @param conf A $http or jQuery.ajax configuration object. - * @returns The same object, possibly modified in-place */ -export function httpConfAddMethodFetch( - url: string, - params: Record -): { url: string; request: RequestInit } { - if (calcUrlLength(url, params) > settings.backendURLMaxLength) { - const body = new FormData() - for (const key in params) { - body.append(key, params[key]) - } - return { url, request: { method: "POST", body } } - } else { - return { url: url + "?" + new URLSearchParams(params), request: {} } +export function fetchConfAddMethod(url: string, params: Record): { url: string; request: RequestInit } { + if (selectHttpMethod(url, params) == "POST") { + return { url, request: { method: "POST", body: toFormData(params) } } } + return { url: buildUrl(url, params), request: {} } +} + +/** Check url length */ +function selectHttpMethod(url: string, params: Record): "GET" | "POST" { + return buildUrl(url, params).length > settings.backendURLMaxLength ? "POST" : "GET" +} + +/** Convert object to FormData */ +function toFormData(obj: Record): FormData { + const formData = new FormData() + Object.entries(obj).forEach(([key, value]) => formData.append(key, value)) + return formData +} + +/** Append search params to url */ +export function buildUrl(base: string, params: Record): string { + const url = new URL(base) + Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value)) + return url.toString() } /** From f63f041122566a40a46cebdda58b75c556fdd601 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 12 Dec 2024 09:28:39 +0100 Subject: [PATCH 7/8] korpRequest for corpus_info --- .../types/corpus-info.ts} | 9 +++++---- app/scripts/backend/types/index.ts | 5 +++++ app/scripts/data_init.ts | 11 ++++++----- app/scripts/settings/config-transformed.types.ts | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) rename app/scripts/{settings/corpus-info.types.ts => backend/types/corpus-info.ts} (87%) diff --git a/app/scripts/settings/corpus-info.types.ts b/app/scripts/backend/types/corpus-info.ts similarity index 87% rename from app/scripts/settings/corpus-info.types.ts rename to app/scripts/backend/types/corpus-info.ts index 80590f1ca..cc65d80bc 100644 --- a/app/scripts/settings/corpus-info.types.ts +++ b/app/scripts/backend/types/corpus-info.ts @@ -1,9 +1,10 @@ /** @format */ +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_info/get */ + +export type CorpusInfoParams = { + corpus: string +} -/** - * Response from the `/corpus_info` API. - * See https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_info/get - */ export type CorpusInfoResponse = { corpora: Record total_sentences: number diff --git a/app/scripts/backend/types/index.ts b/app/scripts/backend/types/index.ts index 0266a6441..ba575e7eb 100644 --- a/app/scripts/backend/types/index.ts +++ b/app/scripts/backend/types/index.ts @@ -1,5 +1,6 @@ /** @format */ +import { CorpusInfoParams, CorpusInfoResponse } from "./corpus-info" import { CountParams, CountResponse } from "./count" import { LoglikeParams, LoglikeResponse } from "./loglike" import { QueryParams, QueryResponse } from "./query" @@ -8,6 +9,10 @@ export * from "./common" /** Maps a Korp backend endpoint name to the expected parameters and response */ export type API = { + corpus_info: { + params: CorpusInfoParams + response: CorpusInfoResponse + } count: { params: CountParams response: CountResponse diff --git a/app/scripts/data_init.ts b/app/scripts/data_init.ts index c2d966e07..2c654734b 100644 --- a/app/scripts/data_init.ts +++ b/app/scripts/data_init.ts @@ -7,11 +7,11 @@ import timeProxyFactory from "@/backend/time-proxy" import { getAllCorporaInFolders } from "./components/corpus-chooser/util" import { CorpusListing } from "./corpus_listing" import { ParallelCorpusListing } from "./parallel/corpus_listing" -import { fromKeys, fetchConfAddMethod } from "@/util" +import { fromKeys } from "@/util" import { Labeled, LangLocMap, LocMap } from "./i18n/types" -import { CorpusInfoResponse } from "./settings/corpus-info.types" import { Attribute, Config, Corpus, CorpusParallel, CustomAttribute } from "./settings/config.types" import { ConfigTransformed, CorpusTransformed } from "./settings/config-transformed.types" +import { korpRequest } from "./backend/common" // Using memoize, this will only fetch once and then return the same promise when called again. // TODO it would be better only to load additional languages when there is a language change @@ -47,9 +47,10 @@ type InfoData = Record { const params = { corpus: corpusIds.map((id) => id.toUpperCase()).join(",") } - const { url, request } = fetchConfAddMethod(settings.korp_backend_url + "/corpus_info", params) - const response = await fetch(url, request) - const data = (await response.json()) as CorpusInfoResponse + const data = await korpRequest("corpus_info", params) + if ("ERROR" in data) { + throw new Error(data.ERROR.value) + } return fromKeys(corpusIds, (corpusId) => ({ info: data.corpora[corpusId.toUpperCase()].info, diff --git a/app/scripts/settings/config-transformed.types.ts b/app/scripts/settings/config-transformed.types.ts index 6937d7f2e..1dcc06ade 100644 --- a/app/scripts/settings/config-transformed.types.ts +++ b/app/scripts/settings/config-transformed.types.ts @@ -5,7 +5,7 @@ import { LangString } from "@/i18n/types" import { Attribute, Config, Corpus, CustomAttribute, Folder } from "./config.types" -import { CorpusInfoInfo } from "./corpus-info.types" +import { CorpusInfoInfo } from "@/backend/types/corpus-info" export type ConfigTransformed = Omit & { corpora: Record From ce741d8fd18ba5fc6e6ebca418868f1fa2ea9c60 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 12 Dec 2024 11:09:58 +0100 Subject: [PATCH 8/8] korpRequest for corpus_config --- CHANGELOG.md | 1 + app/scripts/backend/common.ts | 2 ++ app/scripts/backend/types/corpus-config.ts | 11 ++++++++ app/scripts/backend/types/index.ts | 5 ++++ app/scripts/data_init.ts | 30 +++++++++------------- app/scripts/settings/app-settings.types.ts | 2 +- doc/frontend_devel.md | 6 +++-- 7 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 app/scripts/backend/types/corpus-config.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f610a25..007ceeba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed +- The `corpus_config_url` setting is replaced by `get_corpus_ids`, see [doc/frontend_devel.md](./doc/frontend_devel.md) - The `httpConfAddMethod*` util functions were refactored: - The `$.ajax` case of `httpConfAddMethod` was extracted into `ajaxConfAddMethod` - `httpConfAddMethodFetch` was renamed to `fetchConfAddMethod` diff --git a/app/scripts/backend/common.ts b/app/scripts/backend/common.ts index d1953544f..70ab25c8a 100644 --- a/app/scripts/backend/common.ts +++ b/app/scripts/backend/common.ts @@ -3,11 +3,13 @@ import { fetchConfAddMethod } from "@/util" import { getAuthorizationHeader } from "@/components/auth/auth" import settings from "@/settings" import { API, Response } from "./types" +import { omitBy } from "lodash" export async function korpRequest( endpoint: K, params: API[K]["params"] ): Promise> { + params = omitBy(params, (value) => value == null) as API[K]["params"] const { url, request } = fetchConfAddMethod(settings.korp_backend_url + "/" + endpoint, params) request.headers = { ...request.headers, ...getAuthorizationHeader() } const response = await fetch(url, request) diff --git a/app/scripts/backend/types/corpus-config.ts b/app/scripts/backend/types/corpus-config.ts new file mode 100644 index 000000000..de210be2c --- /dev/null +++ b/app/scripts/backend/types/corpus-config.ts @@ -0,0 +1,11 @@ +/** @format */ +/** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_config/get */ +import { Config } from "@/settings/config.types" + +export type CorpusConfigParams = { + mode: string + corpus?: string + include_lab?: string +} + +export type CorpusConfigResponse = Config diff --git a/app/scripts/backend/types/index.ts b/app/scripts/backend/types/index.ts index ba575e7eb..cf01b7f8c 100644 --- a/app/scripts/backend/types/index.ts +++ b/app/scripts/backend/types/index.ts @@ -1,5 +1,6 @@ /** @format */ +import { CorpusConfigParams, CorpusConfigResponse } from "./corpus-config" import { CorpusInfoParams, CorpusInfoResponse } from "./corpus-info" import { CountParams, CountResponse } from "./count" import { LoglikeParams, LoglikeResponse } from "./loglike" @@ -9,6 +10,10 @@ export * from "./common" /** Maps a Korp backend endpoint name to the expected parameters and response */ export type API = { + corpus_config: { + params: CorpusConfigParams + response: CorpusConfigResponse + } corpus_info: { params: CorpusInfoParams response: CorpusInfoResponse diff --git a/app/scripts/data_init.ts b/app/scripts/data_init.ts index 2c654734b..5991c8e89 100644 --- a/app/scripts/data_init.ts +++ b/app/scripts/data_init.ts @@ -46,6 +46,8 @@ type InfoData = Record { + if (!corpusIds.length) return {} + const params = { corpus: corpusIds.map((id) => id.toUpperCase()).join(",") } const data = await korpRequest("corpus_info", params) if ("ERROR" in data) { @@ -98,27 +100,19 @@ async function getConfig(): Promise { return corpusConfig } catch {} - let configUrl: string - // The corpora to include can be defined elsewhere can in a mode - if (settings.corpus_config_url) { - configUrl = await settings.corpus_config_url() - } else { - const labParam = process.env.ENVIRONMENT == "staging" ? "&include_lab" : "" - configUrl = `${settings.korp_backend_url}/corpus_config?mode=${currentMode}${labParam}` - } - let response: Response - try { - response = await fetch(configUrl) - } catch (error) { - throw Error("Config request failed") - } + // The corpora to include are normally given by the mode config, but allow defining it elsewhere (used by Mink) + const corpusIds = settings.get_corpus_ids ? await settings.get_corpus_ids() : undefined + + const config = await korpRequest("corpus_config", { + mode: currentMode, + corpus: corpusIds?.join(",") || undefined, + }) - if (!response.ok) { - console.error("Something wrong with corpus config", response.statusText) - throw Error("Something wrong with corpus config") + if ("ERROR" in config) { + throw Error(config.ERROR.value) } - return (await response.json()) as Config + return config } /** diff --git a/app/scripts/settings/app-settings.types.ts b/app/scripts/settings/app-settings.types.ts index a0855bd36..d2cb30285 100644 --- a/app/scripts/settings/app-settings.types.ts +++ b/app/scripts/settings/app-settings.types.ts @@ -15,7 +15,7 @@ export type AppSettings = { backendURLMaxLength: number common_struct_types?: Record config_dependent_on_authentication?: boolean - corpus_config_url?: () => Promise + get_corpus_ids?: () => Promise corpus_info_link?: { url_template: string label: LangString diff --git a/doc/frontend_devel.md b/doc/frontend_devel.md index e41259e0c..f5bbee901 100644 --- a/doc/frontend_devel.md +++ b/doc/frontend_devel.md @@ -109,7 +109,6 @@ settings that affect the frontend. that may be added automatically to a corpus. See [backend documentation](https://github.com/spraakbanken/korp-backend) for more information about how to define attributes. - __config_dependent_on_authentication__ - Boolean. If true, backend config will not be fetched until login check has finished. -- __corpus_config_url__ - Async function returning a url string. Configuration for the selected mode is fetched from here at app initialization. If not given, the default is `/corpus_config?mode=`, see the [`corpus_config`](https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_config/get) API. - __corpus_info_link__ - Object. Use this to render a link for each corpus in the corpus chooser. - __url_template__ - String or translation object. A URL containing a token "%s", which will be replaced with the corpus id. - __label__ - String or translation object. The label is the the same for all corpora. @@ -134,6 +133,7 @@ settings that affect the frontend. - __label__: String or translation object. - __params__: Object. This is translated to URL search params when the link is clicked. - __hint__: String or translation object. Can contain HTML. +- __get_corpus_ids__ - Async function returning a list of strings. The corpus ids are passed as the `corpus=` param to the `/corpus_config?mode=` call, see the [`corpus_config`](https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_config/get) API. - __group_statistics__ - List of attribute names. Attributes that either have a rank or a numbering used for multi-word units. For example, removing `:2` from `ta_bort..vbm.1:2`, to get the lemgram of this word: `ta_bort..vbm.1`. - __has_timespan__ - Boolean. If the backend supports the `timespan` call, used in corpus chooser for example. Default: `true` - __hits_per_page_values__ - Array of integer. The available page sizes. Default: `[25, 50, 75, 100]` @@ -228,12 +228,14 @@ If no mode is given, mode is `default`. It then looks for mode-specific code in `/modes/_mode.js`. Mode code may overwrite values from `config.yml` by altering the `settings` object imported from `@/settings`. -It then looks for settings for this specific mode, the **corpus config**. If it exists at `/modes/_corpus_config.json`, it will be loaded from there. Otherwise, it retrieves it from the url given by the `corpus_config_url` option, which defaults to: +It then looks for settings for this specific mode, the **corpus config**. If it exists at `/modes/_corpus_config.json`, it will be loaded from there. Otherwise, it retrieves it from the backend: ``` https:///corpus_config?mode= ``` +Normally, the mode param is enough for the backend to know what corpora to include. Alternatively, it is possible to specify corpus ids in the `corpus=` param, by assigning a function to the `get_corpus_ids` setting (in `_mode.js`). The function can be async and should return a list of corpus ids. + See the [`corpus_config`](https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_config/get) API for more information. ## Parallel corpora