From e416541f40b4782c72dd2785c71065cb0b52d480 Mon Sep 17 00:00:00 2001 From: Syndesi Date: Tue, 10 Feb 2026 21:33:06 +0100 Subject: [PATCH] add support for post search endpoint, closes #6 --- CHANGELOG.md | 2 + .../src/Endpoint/Search/PostSearchEndpoint.ts | 64 +++++++++++++++++++ core/src/Service/ApiWrapper.ts | 17 +++++ .../Search/Step/CypherPathSubsetStep.ts | 8 +++ .../Step/ElasticsearchQueryDSLMixinStep.ts | 15 +++++ .../Search/Step/ElementHydrationStep.ts | 11 ++++ core/src/Type/Definition/Search/Step/Step.ts | 7 ++ core/src/Type/Definition/Search/Step/index.ts | 4 ++ .../StepResult/CypherPathSubsetStepResult.ts | 10 +++ .../ElasticsearchQueryDSLMixinStepResult.ts | 15 +++++ .../StepResult/ElementHydrationStepResult.ts | 6 ++ .../Search/StepResult/StepResult.ts | 7 ++ .../Definition/Search/StepResult/index.ts | 4 ++ core/src/Type/Definition/Search/index.ts | 2 + core/src/Type/Definition/index.ts | 1 + core/src/Type/Enum/ServiceIdentifier.ts | 3 + core/src/init.ts | 4 ++ .../Service/ApiWrapper/ApiWrapper.test.ts | 2 + .../Unit/Service/ApiWrapper/ApiWrapper.ts | 3 + 19 files changed, 185 insertions(+) create mode 100644 core/src/Endpoint/Search/PostSearchEndpoint.ts create mode 100644 core/src/Type/Definition/Search/Step/CypherPathSubsetStep.ts create mode 100644 core/src/Type/Definition/Search/Step/ElasticsearchQueryDSLMixinStep.ts create mode 100644 core/src/Type/Definition/Search/Step/ElementHydrationStep.ts create mode 100644 core/src/Type/Definition/Search/Step/Step.ts create mode 100644 core/src/Type/Definition/Search/Step/index.ts create mode 100644 core/src/Type/Definition/Search/StepResult/CypherPathSubsetStepResult.ts create mode 100644 core/src/Type/Definition/Search/StepResult/ElasticsearchQueryDSLMixinStepResult.ts create mode 100644 core/src/Type/Definition/Search/StepResult/ElementHydrationStepResult.ts create mode 100644 core/src/Type/Definition/Search/StepResult/StepResult.ts create mode 100644 core/src/Type/Definition/Search/StepResult/index.ts create mode 100644 core/src/Type/Definition/Search/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e15a301..6671059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Add support for post search endpoint, closes #6. ## 0.1.0 - 2026-02-01 diff --git a/core/src/Endpoint/Search/PostSearchEndpoint.ts b/core/src/Endpoint/Search/PostSearchEndpoint.ts new file mode 100644 index 0000000..bdac2c9 --- /dev/null +++ b/core/src/Endpoint/Search/PostSearchEndpoint.ts @@ -0,0 +1,64 @@ +import { FetchHelper, ServiceResolver } from '../../Service/index.js'; +import { LoggerInterface } from '../../Type/Definition/index.js'; +import { ParsedResponse } from '../../Type/Definition/Response/index.js'; +import { Step } from '../../Type/Definition/Search/Step/index.js'; +import { StepResult } from '../../Type/Definition/Search/StepResult/index.js'; +import { ServiceIdentifier } from '../../Type/Enum/index.js'; + +/** + * The post search endpoint executes a search query and returns results. + * + * @see [Ember Nexus API: Search Endpoint](https://ember-nexus.github.io/api/#/api-endpoints/search/post-search) + * @experimental + */ +class PostSearchEndpoint { + static identifier: ServiceIdentifier = ServiceIdentifier.endpointSearchPostSearchEndpoint; + constructor( + private logger: LoggerInterface, + private fetchHelper: FetchHelper, + ) {} + + static constructFromServiceResolver(serviceResolver: ServiceResolver): PostSearchEndpoint { + return new PostSearchEndpoint( + serviceResolver.getServiceOrFail(ServiceIdentifier.logger), + serviceResolver.getServiceOrFail(ServiceIdentifier.serviceFetchHelper), + ); + } + + async postSearch( + steps: Step[], + parameters: null | Record = null, + debug: boolean = false, + ): Promise> { + try { + const url = this.fetchHelper.buildUrl('/search'); + this.logger.debug(`Executing HTTP POST request against URL: ${url}`); + + const response = await fetch( + url, + this.fetchHelper.getDefaultPostOptions( + JSON.stringify({ + steps: steps, + parameters: parameters, + debug: debug, + }), + ), + ).catch((error) => this.fetchHelper.rethrowErrorAsNetworkError(error)); + + const rawData = await this.fetchHelper.parseJsonResponse(response); + + if (!('results' in rawData)) { + throw new Error("Search result did not contain property 'results'."); + } + + return { + data: rawData.results as StepResult[], + response: response, + }; + } catch (error) { + this.fetchHelper.logAndThrowError(error); + } + } +} + +export { PostSearchEndpoint }; diff --git a/core/src/Service/ApiWrapper.ts b/core/src/Service/ApiWrapper.ts index 3edcae7..92e9483 100644 --- a/core/src/Service/ApiWrapper.ts +++ b/core/src/Service/ApiWrapper.ts @@ -17,6 +17,7 @@ import { PostIndexEndpoint, PutElementEndpoint, } from '../Endpoint/Element/index.js'; +import { PostSearchEndpoint } from '../Endpoint/Search/PostSearchEndpoint.js'; import { DeleteTokenEndpoint, GetMeEndpoint, @@ -38,6 +39,8 @@ import { Uuid, } from '../Type/Definition/index.js'; import { ParsedResponse } from '../Type/Definition/Response/index.js'; +import { Step } from '../Type/Definition/Search/Step/index.js'; +import { StepResult } from '../Type/Definition/Search/StepResult/index.js'; import { ServiceIdentifier } from '../Type/Enum/index.js'; class ApiWrapper { @@ -60,6 +63,7 @@ class ApiWrapper { private postTokenEndpoint: PostTokenEndpoint, private getTokenEndpoint: GetTokenEndpoint, private deleteTokenEndpoint: DeleteTokenEndpoint, + private postSearchEndpoint: PostSearchEndpoint, private elementCache: ElementCache, private elementChildrenCache: ElementChildrenCache, private elementParentsCache: ElementParentsCache, @@ -93,6 +97,7 @@ class ApiWrapper { serviceResolver.getServiceOrFail(ServiceIdentifier.endpointUserPostTokenEndpoint), serviceResolver.getServiceOrFail(ServiceIdentifier.endpointUserGetTokenEndpoint), serviceResolver.getServiceOrFail(ServiceIdentifier.endpointUserDeleteTokenEndpoint), + serviceResolver.getServiceOrFail(ServiceIdentifier.endpointSearchPostSearchEndpoint), serviceResolver.getServiceOrFail(ServiceIdentifier.cacheElement), serviceResolver.getServiceOrFail(ServiceIdentifier.cacheElementChildren), serviceResolver.getServiceOrFail(ServiceIdentifier.cacheElementParents), @@ -537,6 +542,18 @@ class ApiWrapper { public async deleteToken(): Promise { await this.deleteTokenEndpoint.deleteToken(); } + + /** + * @experimental + */ + public async postSearch( + steps: Step[], + parameters: null | Record = null, + debug: boolean = false, + ): Promise { + const parsedResponse = await this.postSearchEndpoint.postSearch(steps, parameters, debug); + return parsedResponse.data; + } } export { ApiWrapper }; diff --git a/core/src/Type/Definition/Search/Step/CypherPathSubsetStep.ts b/core/src/Type/Definition/Search/Step/CypherPathSubsetStep.ts new file mode 100644 index 0000000..6badce0 --- /dev/null +++ b/core/src/Type/Definition/Search/Step/CypherPathSubsetStep.ts @@ -0,0 +1,8 @@ +import { Step } from './Step.js'; + +interface CypherPathSubsetStep extends Step { + type: 'cypher-path-subset'; + query: string; +} + +export { CypherPathSubsetStep }; diff --git a/core/src/Type/Definition/Search/Step/ElasticsearchQueryDSLMixinStep.ts b/core/src/Type/Definition/Search/Step/ElasticsearchQueryDSLMixinStep.ts new file mode 100644 index 0000000..0c9f870 --- /dev/null +++ b/core/src/Type/Definition/Search/Step/ElasticsearchQueryDSLMixinStep.ts @@ -0,0 +1,15 @@ +import { Step } from './Step.js'; + +interface ElasticsearchQueryDSLMixinStep extends Step { + type: 'elasticsearch-query-dsl-mixin'; + query: Record; + parameters?: { + nodeTypes?: string[]; + relationTypes?: string[]; + page?: number; + pageSize?: number; + minScore?: null | number; + }; +} + +export { ElasticsearchQueryDSLMixinStep }; diff --git a/core/src/Type/Definition/Search/Step/ElementHydrationStep.ts b/core/src/Type/Definition/Search/Step/ElementHydrationStep.ts new file mode 100644 index 0000000..a64fc80 --- /dev/null +++ b/core/src/Type/Definition/Search/Step/ElementHydrationStep.ts @@ -0,0 +1,11 @@ +import { Step } from './Step.js'; +import { Uuid } from '../../Uuid.js'; + +interface ElementHydrationStep extends Step { + type: 'element-hydration'; + query: { + elementIds: Uuid[] | string; + }; +} + +export { ElementHydrationStep }; diff --git a/core/src/Type/Definition/Search/Step/Step.ts b/core/src/Type/Definition/Search/Step/Step.ts new file mode 100644 index 0000000..e9d3b95 --- /dev/null +++ b/core/src/Type/Definition/Search/Step/Step.ts @@ -0,0 +1,7 @@ +interface Step { + type: string; + query?: null | string | Record; + parameters?: Record; +} + +export { Step }; diff --git a/core/src/Type/Definition/Search/Step/index.ts b/core/src/Type/Definition/Search/Step/index.ts new file mode 100644 index 0000000..1a22f88 --- /dev/null +++ b/core/src/Type/Definition/Search/Step/index.ts @@ -0,0 +1,4 @@ +export * from './CypherPathSubsetStep.js'; +export * from './ElasticsearchQueryDSLMixinStep.js'; +export * from './ElementHydrationStep.js'; +export * from './Step.js'; diff --git a/core/src/Type/Definition/Search/StepResult/CypherPathSubsetStepResult.ts b/core/src/Type/Definition/Search/StepResult/CypherPathSubsetStepResult.ts new file mode 100644 index 0000000..52acb3a --- /dev/null +++ b/core/src/Type/Definition/Search/StepResult/CypherPathSubsetStepResult.ts @@ -0,0 +1,10 @@ +import { Uuid } from '../../Uuid.js'; + +interface CypherPathSubsetStepResult { + paths: { + nodeIds: Uuid[]; + relationIds: Uuid[]; + }; +} + +export { CypherPathSubsetStepResult }; diff --git a/core/src/Type/Definition/Search/StepResult/ElasticsearchQueryDSLMixinStepResult.ts b/core/src/Type/Definition/Search/StepResult/ElasticsearchQueryDSLMixinStepResult.ts new file mode 100644 index 0000000..35d7205 --- /dev/null +++ b/core/src/Type/Definition/Search/StepResult/ElasticsearchQueryDSLMixinStepResult.ts @@ -0,0 +1,15 @@ +import { Uuid } from '../../Uuid.js'; + +interface ElasticsearchQueryDSLMixinStepResult { + elements: { + id: Uuid; + type: string; + metadata: { + score: number; + }; + }[]; + totalHits: number; + maxScore: number; +} + +export { ElasticsearchQueryDSLMixinStepResult }; diff --git a/core/src/Type/Definition/Search/StepResult/ElementHydrationStepResult.ts b/core/src/Type/Definition/Search/StepResult/ElementHydrationStepResult.ts new file mode 100644 index 0000000..628dec1 --- /dev/null +++ b/core/src/Type/Definition/Search/StepResult/ElementHydrationStepResult.ts @@ -0,0 +1,6 @@ +import { Node } from '../../Node.js'; +import { Relation } from '../../Relation.js'; + +type ElementHydrationStepResult = [Node, Relation][]; + +export { ElementHydrationStepResult }; diff --git a/core/src/Type/Definition/Search/StepResult/StepResult.ts b/core/src/Type/Definition/Search/StepResult/StepResult.ts new file mode 100644 index 0000000..4ef3074 --- /dev/null +++ b/core/src/Type/Definition/Search/StepResult/StepResult.ts @@ -0,0 +1,7 @@ +import { CypherPathSubsetStepResult } from './CypherPathSubsetStepResult.js'; +import { ElasticsearchQueryDSLMixinStepResult } from './ElasticsearchQueryDSLMixinStepResult.js'; +import { ElementHydrationStepResult } from './ElementHydrationStepResult.js'; + +type StepResult = CypherPathSubsetStepResult | ElasticsearchQueryDSLMixinStepResult | ElementHydrationStepResult; + +export { StepResult }; diff --git a/core/src/Type/Definition/Search/StepResult/index.ts b/core/src/Type/Definition/Search/StepResult/index.ts new file mode 100644 index 0000000..028649d --- /dev/null +++ b/core/src/Type/Definition/Search/StepResult/index.ts @@ -0,0 +1,4 @@ +export * from './CypherPathSubsetStepResult.js'; +export * from './ElasticsearchQueryDSLMixinStepResult.js'; +export * from './ElementHydrationStepResult.js'; +export * from './StepResult.js'; diff --git a/core/src/Type/Definition/Search/index.ts b/core/src/Type/Definition/Search/index.ts new file mode 100644 index 0000000..188849d --- /dev/null +++ b/core/src/Type/Definition/Search/index.ts @@ -0,0 +1,2 @@ +export * as Step from './Step/index.js'; +export * as StepResult from './StepResult/index.js'; diff --git a/core/src/Type/Definition/index.ts b/core/src/Type/Definition/index.ts index 059925d..96b628b 100644 --- a/core/src/Type/Definition/index.ts +++ b/core/src/Type/Definition/index.ts @@ -28,6 +28,7 @@ export * from './RouteGuardFunction.js'; export * from './RouteIdentifier.js'; export * from './RouteNode.js'; export * from './RouteToWebComponentFunction.js'; +export * as Search from './Search/index.js'; export * from './ServiceIdentifier.js'; export * from './Token.js'; export * from './UniqueUserIdentifier.js'; diff --git a/core/src/Type/Enum/ServiceIdentifier.ts b/core/src/Type/Enum/ServiceIdentifier.ts index 5e2bcf9..ac71124 100644 --- a/core/src/Type/Enum/ServiceIdentifier.ts +++ b/core/src/Type/Enum/ServiceIdentifier.ts @@ -38,6 +38,9 @@ enum ServiceIdentifier { endpointUserPostRegisterEndpoint = 'ember-nexus.app-core.endpoint.user.post-register-endpoint', endpointUserPostTokenEndpoint = 'ember-nexus.app-core.endpoint.user.post-token-endpoint', + // endpoints: search + endpointSearchPostSearchEndpoint = 'ember-nexus.app-core.endpoint.search.post-search-endpoint', + // cache cacheElement = 'ember-nexus.app-core.cache.element-cache', cacheElementChildren = 'ember-nexus.app-core.cache.element-children-cache', diff --git a/core/src/init.ts b/core/src/init.ts index 99fcb49..20334e2 100644 --- a/core/src/init.ts +++ b/core/src/init.ts @@ -19,6 +19,7 @@ import { PostIndexEndpoint, PutElementEndpoint, } from './Endpoint/Element/index.js'; +import { PostSearchEndpoint } from './Endpoint/Search/PostSearchEndpoint.js'; import { DeleteTokenEndpoint, GetMeEndpoint, @@ -83,6 +84,9 @@ const init: pluginInit = (serviceResolver: ServiceResolver) => { PostRegisterEndpoint, PostTokenEndpoint, + // search endpoints + PostSearchEndpoint, + // caches ElementCache, ElementChildrenCache, diff --git a/core/test/Unit/Service/ApiWrapper/ApiWrapper.test.ts b/core/test/Unit/Service/ApiWrapper/ApiWrapper.test.ts index a9570fc..610a540 100644 --- a/core/test/Unit/Service/ApiWrapper/ApiWrapper.test.ts +++ b/core/test/Unit/Service/ApiWrapper/ApiWrapper.test.ts @@ -20,6 +20,7 @@ import { PostIndexEndpoint, PutElementEndpoint, } from '../../../../src/Endpoint/Element/index.js'; +import { PostSearchEndpoint } from '../../../../src/Endpoint/Search/PostSearchEndpoint.js'; import { DeleteTokenEndpoint, GetMeEndpoint, @@ -50,6 +51,7 @@ test('create ApiWrapper from ServiceResolver', () => { serviceResolver.setService(PostTokenEndpoint.identifier, mock()); serviceResolver.setService(GetTokenEndpoint.identifier, mock()); serviceResolver.setService(DeleteTokenEndpoint.identifier, mock()); + serviceResolver.setService(PostSearchEndpoint.identifier, mock()); serviceResolver.setService(ElementCache.identifier, mock()); serviceResolver.setService(ElementChildrenCache.identifier, mock()); serviceResolver.setService(ElementParentsCache.identifier, mock()); diff --git a/core/test/Unit/Service/ApiWrapper/ApiWrapper.ts b/core/test/Unit/Service/ApiWrapper/ApiWrapper.ts index c0e3c4d..db3f963 100644 --- a/core/test/Unit/Service/ApiWrapper/ApiWrapper.ts +++ b/core/test/Unit/Service/ApiWrapper/ApiWrapper.ts @@ -19,6 +19,7 @@ import { PostIndexEndpoint, PutElementEndpoint, } from '../../../../src/Endpoint/Element/index.js'; +import { PostSearchEndpoint } from '../../../../src/Endpoint/Search/PostSearchEndpoint.js'; import { DeleteTokenEndpoint, GetMeEndpoint, @@ -46,6 +47,7 @@ function createApiWrapper(services: { postTokenEndpoint?: PostTokenEndpoint; getTokenEndpoint?: GetTokenEndpoint; deleteTokenEndpoint?: DeleteTokenEndpoint; + postSearchEndpoint?: PostSearchEndpoint; elementCache?: ElementCache; elementChildrenCache?: ElementChildrenCache; elementParentsCache?: ElementParentsCache; @@ -69,6 +71,7 @@ function createApiWrapper(services: { services.postTokenEndpoint ?? mock(), services.getTokenEndpoint ?? mock(), services.deleteTokenEndpoint ?? mock(), + services.postSearchEndpoint ?? mock(), services.elementCache ?? mock(), services.elementChildrenCache ?? mock(), services.elementParentsCache ?? mock(),