Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 64 additions & 0 deletions core/src/Endpoint/Search/PostSearchEndpoint.ts
Original file line number Diff line number Diff line change
@@ -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<LoggerInterface>(ServiceIdentifier.logger),
serviceResolver.getServiceOrFail<FetchHelper>(ServiceIdentifier.serviceFetchHelper),
);
}

async postSearch(
steps: Step[],
parameters: null | Record<string, unknown> = null,
debug: boolean = false,
): Promise<ParsedResponse<StepResult[]>> {
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 };
17 changes: 17 additions & 0 deletions core/src/Service/ApiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PostIndexEndpoint,
PutElementEndpoint,
} from '../Endpoint/Element/index.js';
import { PostSearchEndpoint } from '../Endpoint/Search/PostSearchEndpoint.js';
import {
DeleteTokenEndpoint,
GetMeEndpoint,
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -93,6 +97,7 @@ class ApiWrapper {
serviceResolver.getServiceOrFail<PostTokenEndpoint>(ServiceIdentifier.endpointUserPostTokenEndpoint),
serviceResolver.getServiceOrFail<GetTokenEndpoint>(ServiceIdentifier.endpointUserGetTokenEndpoint),
serviceResolver.getServiceOrFail<DeleteTokenEndpoint>(ServiceIdentifier.endpointUserDeleteTokenEndpoint),
serviceResolver.getServiceOrFail<PostSearchEndpoint>(ServiceIdentifier.endpointSearchPostSearchEndpoint),
serviceResolver.getServiceOrFail<ElementCache>(ServiceIdentifier.cacheElement),
serviceResolver.getServiceOrFail<ElementChildrenCache>(ServiceIdentifier.cacheElementChildren),
serviceResolver.getServiceOrFail<ElementParentsCache>(ServiceIdentifier.cacheElementParents),
Expand Down Expand Up @@ -537,6 +542,18 @@ class ApiWrapper {
public async deleteToken(): Promise<void> {
await this.deleteTokenEndpoint.deleteToken();
}

/**
* @experimental
*/
public async postSearch(
steps: Step[],
parameters: null | Record<string, unknown> = null,
debug: boolean = false,
): Promise<StepResult[]> {
const parsedResponse = await this.postSearchEndpoint.postSearch(steps, parameters, debug);
return parsedResponse.data;
}
}

export { ApiWrapper };
8 changes: 8 additions & 0 deletions core/src/Type/Definition/Search/Step/CypherPathSubsetStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Step } from './Step.js';

interface CypherPathSubsetStep extends Step {
type: 'cypher-path-subset';
query: string;
}

export { CypherPathSubsetStep };
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Step } from './Step.js';

interface ElasticsearchQueryDSLMixinStep extends Step {
type: 'elasticsearch-query-dsl-mixin';
query: Record<string, unknown>;
parameters?: {
nodeTypes?: string[];
relationTypes?: string[];
page?: number;
pageSize?: number;
minScore?: null | number;
};
}

export { ElasticsearchQueryDSLMixinStep };
11 changes: 11 additions & 0 deletions core/src/Type/Definition/Search/Step/ElementHydrationStep.ts
Original file line number Diff line number Diff line change
@@ -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 };
7 changes: 7 additions & 0 deletions core/src/Type/Definition/Search/Step/Step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface Step {
type: string;
query?: null | string | Record<string, unknown>;
parameters?: Record<string, unknown>;
}

export { Step };
4 changes: 4 additions & 0 deletions core/src/Type/Definition/Search/Step/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './CypherPathSubsetStep.js';
export * from './ElasticsearchQueryDSLMixinStep.js';
export * from './ElementHydrationStep.js';
export * from './Step.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Uuid } from '../../Uuid.js';

interface CypherPathSubsetStepResult {
paths: {
nodeIds: Uuid[];
relationIds: Uuid[];
};
}

export { CypherPathSubsetStepResult };
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Node } from '../../Node.js';
import { Relation } from '../../Relation.js';

type ElementHydrationStepResult = [Node, Relation][];

export { ElementHydrationStepResult };
7 changes: 7 additions & 0 deletions core/src/Type/Definition/Search/StepResult/StepResult.ts
Original file line number Diff line number Diff line change
@@ -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 };
4 changes: 4 additions & 0 deletions core/src/Type/Definition/Search/StepResult/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './CypherPathSubsetStepResult.js';
export * from './ElasticsearchQueryDSLMixinStepResult.js';
export * from './ElementHydrationStepResult.js';
export * from './StepResult.js';
2 changes: 2 additions & 0 deletions core/src/Type/Definition/Search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as Step from './Step/index.js';
export * as StepResult from './StepResult/index.js';
1 change: 1 addition & 0 deletions core/src/Type/Definition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions core/src/Type/Enum/ServiceIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions core/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PostIndexEndpoint,
PutElementEndpoint,
} from './Endpoint/Element/index.js';
import { PostSearchEndpoint } from './Endpoint/Search/PostSearchEndpoint.js';
import {
DeleteTokenEndpoint,
GetMeEndpoint,
Expand Down Expand Up @@ -83,6 +84,9 @@ const init: pluginInit = (serviceResolver: ServiceResolver) => {
PostRegisterEndpoint,
PostTokenEndpoint,

// search endpoints
PostSearchEndpoint,

// caches
ElementCache,
ElementChildrenCache,
Expand Down
2 changes: 2 additions & 0 deletions core/test/Unit/Service/ApiWrapper/ApiWrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
PostIndexEndpoint,
PutElementEndpoint,
} from '../../../../src/Endpoint/Element/index.js';
import { PostSearchEndpoint } from '../../../../src/Endpoint/Search/PostSearchEndpoint.js';
import {
DeleteTokenEndpoint,
GetMeEndpoint,
Expand Down Expand Up @@ -50,6 +51,7 @@ test('create ApiWrapper from ServiceResolver', () => {
serviceResolver.setService(PostTokenEndpoint.identifier, mock<PostTokenEndpoint>());
serviceResolver.setService(GetTokenEndpoint.identifier, mock<GetTokenEndpoint>());
serviceResolver.setService(DeleteTokenEndpoint.identifier, mock<DeleteTokenEndpoint>());
serviceResolver.setService(PostSearchEndpoint.identifier, mock<PostSearchEndpoint>());
serviceResolver.setService(ElementCache.identifier, mock<ElementCache>());
serviceResolver.setService(ElementChildrenCache.identifier, mock<ElementChildrenCache>());
serviceResolver.setService(ElementParentsCache.identifier, mock<ElementParentsCache>());
Expand Down
3 changes: 3 additions & 0 deletions core/test/Unit/Service/ApiWrapper/ApiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PostIndexEndpoint,
PutElementEndpoint,
} from '../../../../src/Endpoint/Element/index.js';
import { PostSearchEndpoint } from '../../../../src/Endpoint/Search/PostSearchEndpoint.js';
import {
DeleteTokenEndpoint,
GetMeEndpoint,
Expand Down Expand Up @@ -46,6 +47,7 @@ function createApiWrapper(services: {
postTokenEndpoint?: PostTokenEndpoint;
getTokenEndpoint?: GetTokenEndpoint;
deleteTokenEndpoint?: DeleteTokenEndpoint;
postSearchEndpoint?: PostSearchEndpoint;
elementCache?: ElementCache;
elementChildrenCache?: ElementChildrenCache;
elementParentsCache?: ElementParentsCache;
Expand All @@ -69,6 +71,7 @@ function createApiWrapper(services: {
services.postTokenEndpoint ?? mock<PostTokenEndpoint>(),
services.getTokenEndpoint ?? mock<GetTokenEndpoint>(),
services.deleteTokenEndpoint ?? mock<DeleteTokenEndpoint>(),
services.postSearchEndpoint ?? mock<PostSearchEndpoint>(),
services.elementCache ?? mock<ElementCache>(),
services.elementChildrenCache ?? mock<ElementChildrenCache>(),
services.elementParentsCache ?? mock<ElementParentsCache>(),
Expand Down