Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define ODataExpression for getting object with ODataV4QuerySegments #28

Merged
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ const query = ODataQuery.forV4<User>(`http://domain.example/path/to/endpoint/fun
`query` can now be used like any other OData Query (e.g., `filter`, `select`, `top`, etc.).


## Query Builder

This library also provides a query builder option that returns only the plain filter and query object, ensuring type safety. Utilizing the same syntax as `ODataQuery`, you can create precise filter expressions and query objects, For example:

```ts
// Create a type-safe filter expression
const result = ODataExpression.forV4<User>()
.filter((p) => p.firstName.$equals("john"))
.build();

console.log(results);
// Output: { "$filter": "firstName eq 'john'", ... }
```

By employing the query builder, you can adhere to the familiar syntax of `ODataQuery` while obtaining a streamlined result containing only the essential filter and query information.

## Upgrading from v1.x to 2.x
2.0 introduces a number of breaking changes. The primary breaking changes are with the `filter`, `orderBy` and `orderByDescending` methods on the `ODataQuery` type.
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { ODataContext } from "./lib/ODataContext";
export { ODataQuery } from "./lib/ODataQuery";
export { ODataExpression } from "./lib/ODataExpression";
export * from "./v4";
export * from "./lib/ProxyFilterTypes";
19 changes: 19 additions & 0 deletions src/lib/ODataExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ExcludeProperties } from "./ExcludeProperties";
import { ODataQueryBase } from "./ODataQueryBase";
import { ODataV4QueryProvider } from "./ODataV4QueryProvider";

/**
* Represents a query expression builder only, with empty endpoiont.
*/

export class ODataExpression<T, U = ExcludeProperties<T, unknown[]>> {
oDataQuery: ODataQueryBase<T, U>;

static forV4<T>() {
return new ODataExpression<T>().oDataQuery;
}

constructor() {
this.oDataQuery = new ODataQueryBase<T, U>(new ODataV4QueryProvider(""));
}
}
155 changes: 6 additions & 149 deletions src/lib/ODataQuery.ts
Original file line number Diff line number Diff line change
@@ -1,155 +1,27 @@
import type { ODataQueryProvider } from "./ODataQueryProvider";
import { FieldReference } from "./FieldReference";
import { Expression } from "./Expression";
import type { ODataQueryResponse, ODataQueryResponseWithCount, ODataResponse } from "./ODataResponse";
import type { BooleanPredicateBuilder } from "./BooleanPredicateBuilder";
import { ExpressionOperator } from "./ExpressionOperator";
import type { ExcludeProperties } from "./ExcludeProperties";
import type { ODataV4Options } from "./ODataV4QueryProvider";
import { ODataV4QueryProvider } from "./ODataV4QueryProvider";
import { FilterAccessoryFunctions } from "./FilterAccessoryFunctions";
import type { ReplaceDateWithString, ProjectorType } from "./ProxyFilterTypes";
import { createProxiedEntity, resolveQuery } from "./ProxyFilterTypes";
import type { EntityProxy, PropertyProxy } from "./ProxyTypes";
import { propertyPath, proxyProperties } from "./ProxyTypes";
import type { FieldsFor } from "./FieldsForType";
import type { JsonPrimitiveValueTypes } from "./JsonPrimitiveTypes";
import { ODataQueryBase } from "./ODataQueryBase";
import { ExpressionOperator } from "./ExpressionOperator";
import type { ODataQueryResponse, ODataQueryResponseWithCount, ODataResponse } from "./ODataResponse";
import { resolveQuery, type ReplaceDateWithString } from "./ProxyFilterTypes";

/**
* Represents a query against an OData source.
* This query is agnostic of the version of OData supported by the server (the provided @type {ODataQueryProvider} is responsible for translating the query into the correct syntax for the desired OData version supported by the endpoint).
*/
export class ODataQuery<T, U = ExcludeProperties<T, unknown[]>> {
export class ODataQuery<T, U = ExcludeProperties<T, unknown[]>> extends ODataQueryBase<T, U> {
static forV4<T>(endpoint: string, options?: Partial<ODataV4Options>) {
return new ODataQuery<T>(new ODataV4QueryProvider(endpoint, options));
}

constructor(
public readonly provider: ODataQueryProvider,
public readonly expression?: Expression,
) {}

/**
* Limits the fields that are returned; the most recent call to select() will be used.
* @param fields
*/
public select<U extends FieldsFor<T>>(...fields: U[]): ODataQuery<T, U>;
public select<U extends ProjectorType>(projector: (proxy: T) => U): ODataQuery<T, U>;
public select<U>(...args: [(proxy: T) => U | FieldsFor<T>, ...FieldsFor<T>[]]) {
if (args.length === 0) throw new Error("Parameters are requird");

const firstArg = args[0];
if (typeof firstArg === "function") {
const proxy = this.provider[createProxiedEntity]();
firstArg(proxy as unknown as T);
const expression = new Expression(
ExpressionOperator.Select,
[firstArg, ...getUsedPropertyPaths(proxy)],
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

const expression = new Expression(
ExpressionOperator.Select,
(args as FieldsFor<T>[]).map((v) => new FieldReference<T>(v)),
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

/**
* Returns the top n records; the most recent call to top() will be used.
* @param n
*/
public top(n: number) {
const expression = new Expression(ExpressionOperator.Top, [n], this.expression);
return this.provider.createQuery<T, U>(expression);
}

/**
* Omits the first n records from appear in the returned records; the most recent call to skip() will be used.
* @param n
*/
public skip(n: number) {
const expression = new Expression(ExpressionOperator.Skip, [n], this.expression);
return this.provider.createQuery<T, U>(expression);
}

/**
* Determines the sort order (ascending) of the records; calls or orderBy() and orderByDescending() are cumulative.
* @param fields
*/
public orderBy(fields: (entity: EntityProxy<T>) => PropertyProxy<unknown> | Array<PropertyProxy<unknown>>) {
const proxy = this.provider[createProxiedEntity]<T>();
const properties = [fields(proxy)].flat();
const expression = new Expression(
ExpressionOperator.OrderBy,
properties.map((f) => new FieldReference(f[propertyPath].join("/") as unknown as FieldsFor<unknown>)),
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

/**
* Determines the sort order (descending) of the records; calls to orderBy() and orderByDescending() are cumulative.
* @param fields
*/
public orderByDescending(fields: (entity: EntityProxy<T>) => PropertyProxy<unknown> | Array<PropertyProxy<unknown>>) {
const proxy = this.provider[createProxiedEntity]<T>();
const properties = [fields(proxy)].flat();
const expression = new Expression(
ExpressionOperator.OrderByDescending,
properties.map((f) => new FieldReference(f[propertyPath].join("/") as unknown as FieldsFor<unknown>)),
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

/**
* Filters the records based on the provided expression; multiple calls to filter() are cumulative (as well as UNIONed (AND))
* @param predicate A function that takes in an entity proxy and returns a BooleanPredicateBuilder.
*/
public filter(
predicate:
| BooleanPredicateBuilder<T>
| ((builder: EntityProxy<T, true>, functions: FilterAccessoryFunctions<T>) => BooleanPredicateBuilder<T>),
) {
if (typeof predicate === "function")
predicate = predicate(
this.provider[createProxiedEntity]() as unknown as EntityProxy<T, true>,
new FilterAccessoryFunctions<T>(),
);

const expression = new Expression(ExpressionOperator.Predicate, [predicate], this.expression);
return this.provider.createQuery<T, U>(expression);
}

/**
* Includes the indicated arrays are to be returned as part of the query results.
* @param fields
*/
public expand<
K extends keyof ExcludeProperties<
T,
JsonPrimitiveValueTypes | ArrayLike<JsonPrimitiveValueTypes> | Date | ArrayLike<Date>
>,
>(...fields: K[]) {
const expression = new Expression(
ExpressionOperator.Expand,
fields.map((f) => new FieldReference<T>(f as unknown as FieldsFor<T>)),
this.expression,
);
return this.provider.createQuery<T, U & Pick<T, K>>(expression);
}

/**
* Includes all arrays as part of the query results.
* @param fields
*/
public expandAll() {
const expression = new Expression(ExpressionOperator.ExpandAll, [], this.expression);
return this.provider.createQuery<T, U>(expression);
super(provider, expression);
}

/**
Expand Down Expand Up @@ -206,21 +78,6 @@ export class ODataQuery<T, U = ExcludeProperties<T, unknown[]>> {
}
}

/**
* Function that returns all OData paths that were used by the proxy.
* @param projectTarget
* @returns An array of paths found within the object (if the same path is used more than once, the duplicates are removed)
*/
function getUsedPropertyPaths(proxy: EntityProxy<unknown>): string[] {
const paths: string[] = [];
for (const p of proxy[proxyProperties]) {
if (p[proxyProperties].length === 0) paths.push(p[propertyPath].join("/"));
else paths.push(...getUsedPropertyPaths(p));
}

return Array.from(new Set(paths.flat()));
}

function getSelectMap<T, U>(expression?: Expression): ((entity: T) => U) | undefined {
while (expression != null) {
if (expression.operator === ExpressionOperator.Select) {
Expand Down
166 changes: 166 additions & 0 deletions src/lib/ODataQueryBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { ODataQueryProvider } from "./ODataQueryProvider";
import { FieldReference } from "./FieldReference";
import { Expression } from "./Expression";
import type { BooleanPredicateBuilder } from "./BooleanPredicateBuilder";
import { ExpressionOperator } from "./ExpressionOperator";
import type { ExcludeProperties } from "./ExcludeProperties";
import { FilterAccessoryFunctions } from "./FilterAccessoryFunctions";
import type { ProjectorType } from "./ProxyFilterTypes";
import { createProxiedEntity } from "./ProxyFilterTypes";
import type { EntityProxy, PropertyProxy } from "./ProxyTypes";
import { propertyPath, proxyProperties } from "./ProxyTypes";
import type { FieldsFor } from "./FieldsForType";
import type { JsonPrimitiveValueTypes } from "./JsonPrimitiveTypes";

/**
* Represents a query against an OData source.
* This query is agnostic of the version of OData supported by the server (the provided @type {ODataQueryProvider} is responsible for translating the query into the correct syntax for the desired OData version supported by the endpoint).
*/
export class ODataQueryBase<T, U = ExcludeProperties<T, unknown[]>> {
constructor(
public readonly provider: ODataQueryProvider,
public readonly expression?: Expression,
) {}

/**
* Limits the fields that are returned; the most recent call to select() will be used.
* @param fields
*/
public select<U extends FieldsFor<T>>(...fields: U[]): ODataQueryBase<T, U>;
public select<U extends ProjectorType>(projector: (proxy: T) => U): ODataQueryBase<T, U>;
public select<U>(...args: [(proxy: T) => U | FieldsFor<T>, ...FieldsFor<T>[]]) {
if (args.length === 0) throw new Error("Parameters are requird");

const firstArg = args[0];
if (typeof firstArg === "function") {
const proxy = this.provider[createProxiedEntity]();
firstArg(proxy as unknown as T);
const expression = new Expression(
ExpressionOperator.Select,
[firstArg, ...getUsedPropertyPaths(proxy)],
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

const expression = new Expression(
ExpressionOperator.Select,
(args as FieldsFor<T>[]).map((v) => new FieldReference<T>(v)),
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

/**
* Returns the top n records; the most recent call to top() will be used.
* @param n
*/
public top(n: number) {
const expression = new Expression(ExpressionOperator.Top, [n], this.expression);
return this.provider.createQuery<T, U>(expression);
}

/**
* Omits the first n records from appear in the returned records; the most recent call to skip() will be used.
* @param n
*/
public skip(n: number) {
const expression = new Expression(ExpressionOperator.Skip, [n], this.expression);
return this.provider.createQuery<T, U>(expression);
}

/**
* Determines the sort order (ascending) of the records; calls or orderBy() and orderByDescending() are cumulative.
* @param fields
*/
public orderBy(fields: (entity: EntityProxy<T>) => PropertyProxy<unknown> | Array<PropertyProxy<unknown>>) {
const proxy = this.provider[createProxiedEntity]<T>();
const properties = [fields(proxy)].flat();
const expression = new Expression(
ExpressionOperator.OrderBy,
properties.map((f) => new FieldReference(f[propertyPath].join("/") as unknown as FieldsFor<unknown>)),
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

/**
* Determines the sort order (descending) of the records; calls to orderBy() and orderByDescending() are cumulative.
* @param fields
*/
public orderByDescending(fields: (entity: EntityProxy<T>) => PropertyProxy<unknown> | Array<PropertyProxy<unknown>>) {
const proxy = this.provider[createProxiedEntity]<T>();
const properties = [fields(proxy)].flat();
const expression = new Expression(
ExpressionOperator.OrderByDescending,
properties.map((f) => new FieldReference(f[propertyPath].join("/") as unknown as FieldsFor<unknown>)),
this.expression,
);
return this.provider.createQuery<T, U>(expression);
}

/**
* Filters the records based on the provided expression; multiple calls to filter() are cumulative (as well as UNIONed (AND))
* @param predicate A function that takes in an entity proxy and returns a BooleanPredicateBuilder.
*/
public filter(
predicate:
| BooleanPredicateBuilder<T>
| ((builder: EntityProxy<T, true>, functions: FilterAccessoryFunctions<T>) => BooleanPredicateBuilder<T>),
) {
if (typeof predicate === "function")
predicate = predicate(
this.provider[createProxiedEntity]() as unknown as EntityProxy<T, true>,
new FilterAccessoryFunctions<T>(),
);

const expression = new Expression(ExpressionOperator.Predicate, [predicate], this.expression);
return this.provider.createQuery<T, U>(expression);
}

/**
* Includes the indicated arrays are to be returned as part of the query results.
* @param fields
*/
public expand<
K extends keyof ExcludeProperties<
T,
JsonPrimitiveValueTypes | ArrayLike<JsonPrimitiveValueTypes> | Date | ArrayLike<Date>
>,
>(...fields: K[]) {
const expression = new Expression(
ExpressionOperator.Expand,
fields.map((f) => new FieldReference<T>(f as unknown as FieldsFor<T>)),
this.expression,
);
return this.provider.createQuery<T, U & Pick<T, K>>(expression);
}

/**
* Includes all arrays as part of the query results.
* @param fields
*/
public expandAll() {
const expression = new Expression(ExpressionOperator.ExpandAll, [], this.expression);
return this.provider.createQuery<T, U>(expression);
}

build() {
return this.provider.build(this.expression);
}
}

/**
* Function that returns all OData paths that were used by the proxy.
* @param projectTarget
* @returns An array of paths found within the object (if the same path is used more than once, the duplicates are removed)
*/
function getUsedPropertyPaths(proxy: EntityProxy<unknown>): string[] {
const paths: string[] = [];
for (const p of proxy[proxyProperties]) {
if (p[proxyProperties].length === 0) paths.push(p[propertyPath].join("/"));
else paths.push(...getUsedPropertyPaths(p));
}

return Array.from(new Set(paths.flat()));
}
Loading
Loading