Skip to content

Commit

Permalink
introducing QueryTokenString<T>
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Dec 20, 2018
1 parent 60901c9 commit 10d2421
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 45 deletions.
12 changes: 6 additions & 6 deletions Signum.React/Scripts/FindOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypeReference, PseudoType, QueryKey } from './Reflection';
import { TypeReference, PseudoType, QueryKey, getLambdaMembers, QueryTokenString } from './Reflection';
import { Lite, Entity } from './Signum.Entities';
import { PaginationMode, OrderType, FilterOperation, FilterType, ColumnOptionsMode, UniqueType, SystemTimeMode, FilterGroupOperation } from './Signum.Entities.DynamicQuery';
import { SearchControlProps } from "./Search";
Expand All @@ -25,7 +25,7 @@ export interface ModalFindOptions {
export interface FindOptions {
queryName: PseudoType | QueryKey;
groupResults?: boolean;
parentToken?: string;
parentToken?: string | QueryTokenString<any>;
parentValue?: any;

filterOptions?: FilterOption[];
Expand Down Expand Up @@ -54,14 +54,14 @@ export function isFilterGroupOption(fo: FilterOption): fo is FilterGroupOption {
}

export interface FilterConditionOption {
token: string;
token: string | QueryTokenString<any>;
frozen?: boolean;
operation?: FilterOperation;
value: any;
}

export interface FilterGroupOption {
token?: string;
token?: string | QueryTokenString<any>;
groupOperation: FilterGroupOperation;
filters: FilterOption[];
}
Expand All @@ -87,7 +87,7 @@ export interface FilterGroupOptionParsed {
}

export interface OrderOption {
token: string;
token: string | QueryTokenString<any>;
orderType: OrderType;
}

Expand All @@ -97,7 +97,7 @@ export interface OrderOptionParsed {
}

export interface ColumnOption {
token: string;
token: string | QueryTokenString<any>;
displayName?: string;
}

Expand Down
30 changes: 15 additions & 15 deletions Signum.React/Scripts/Finder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,11 @@ export function parseOrderOptions(orderOptions: OrderOption[], groupResults: boo

const completer = new TokenCompleter(qd);
var sto = SubTokensOptions.CanElement | (groupResults ? SubTokensOptions.CanAggregate : 0);
orderOptions.forEach(a => completer.request(a.token, sto));
orderOptions.forEach(a => completer.request(a.token.toString(), sto));

return completer.finished()
.then(() => orderOptions.map(oo => ({
token: completer.get(oo.token),
token: completer.get(oo.token.toString()),
orderType: oo.orderType || "Ascending",
}) as OrderOptionParsed));
}
Expand All @@ -340,12 +340,12 @@ export function parseColumnOptions(columnOptions: ColumnOption[], groupResults:

const completer = new TokenCompleter(qd);
var sto = SubTokensOptions.CanElement | (groupResults ? SubTokensOptions.CanAggregate : 0);
columnOptions.forEach(a => completer.request(a.token, sto));
columnOptions.forEach(a => completer.request(a.token.toString(), sto));

return completer.finished()
.then(() => columnOptions.map(co => ({
token: completer.get(co.token),
displayName: co.displayName || completer.get(co.token).niceName,
token: completer.get(co.token.toString()),
displayName: co.displayName || completer.get(co.token.toString()).niceName,
}) as ColumnOptionParsed));
}

Expand Down Expand Up @@ -496,10 +496,10 @@ export function parseFindOptions(findOptions: FindOptions, qd: QueryDescription)
fo.filterOptions.forEach(fo => completer.requestFilter(fo, SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll | canAggregate));

if (fo.orderOptions)
fo.orderOptions.forEach(oo => completer.request(oo.token, SubTokensOptions.CanElement | canAggregate));
fo.orderOptions.forEach(oo => completer.request(oo.token.toString(), SubTokensOptions.CanElement | canAggregate));

if (fo.columnOptions)
fo.columnOptions.forEach(co => completer.request(co.token, SubTokensOptions.CanElement | canAggregate));
fo.columnOptions.forEach(co => completer.request(co.token.toString(), SubTokensOptions.CanElement | canAggregate));

return completer.finished().then(() => {

Expand All @@ -510,12 +510,12 @@ export function parseFindOptions(findOptions: FindOptions, qd: QueryDescription)
systemTime: fo.systemTime,

columnOptions: (fo.columnOptions || []).map(co => ({
token: completer.get(co.token),
displayName: co.displayName || completer.get(co.token).niceName
token: completer.get(co.token.toString()),
displayName: co.displayName || completer.get(co.token.toString()).niceName
}) as ColumnOptionParsed),

orderOptions: (fo.orderOptions || []).map(oo => ({
token: completer.get(oo.token),
token: completer.get(oo.token.toString()),
orderType: oo.orderType,
}) as OrderOptionParsed),

Expand Down Expand Up @@ -642,7 +642,7 @@ export function expandParentColumn(fo: FindOptions): FindOptions {
...(fo.filterOptions || [])
];

if (!fo.parentToken.contains(".") && (fo.columnOptionsMode == undefined || fo.columnOptionsMode == "Remove")) {
if (!fo.parentToken.toString().contains(".") && (fo.columnOptionsMode == undefined || fo.columnOptionsMode == "Remove")) {
fo.columnOptions = [
{ token: fo.parentToken },
...(fo.columnOptions || [])
Expand Down Expand Up @@ -680,12 +680,12 @@ export class TokenCompleter {
requestFilter(fo: FilterOption, options: SubTokensOptions) {

if (isFilterGroupOption(fo)) {
fo.token && this.request(fo.token, options);
fo.token && this.request(fo.token.toString(), options);

fo.filters.forEach(f => this.requestFilter(f, options));
} else {

this.request(fo.token, options);
this.request(fo.token.toString(), options);
}
}

Expand Down Expand Up @@ -735,13 +735,13 @@ export class TokenCompleter {
toFilterOptionParsed(fo: FilterOption): FilterOptionParsed {
if (isFilterGroupOption(fo))
return ({
token: fo.token && this.get(fo.token),
token: fo.token && this.get(fo.token.toString()),
groupOperation: fo.groupOperation,
filters: fo.filters.map(f => this.toFilterOptionParsed(f))
} as FilterGroupOptionParsed);
else
return ({
token: this.get(fo.token),
token: this.get(fo.token.toString()),
operation: fo.operation || "EqualTo",
value: fo.value,
frozen: fo.frozen || false,
Expand Down
4 changes: 2 additions & 2 deletions Signum.React/Scripts/Lines/AutoCompleteConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import * as Finder from '../Finder'
import { AbortableRequest } from '../Services'
import { FindOptions, FilterOptionParsed, OrderOptionParsed, OrderRequest, ResultRow, ColumnOptionParsed, ColumnRequest } from '../FindOptions'
import { getTypeInfo, getQueryKey } from '../Reflection'
import { getTypeInfo, getQueryKey, QueryTokenString } from '../Reflection'
import { ModifiableEntity, Lite, Entity, toLite, is, isLite, isEntity, getToString } from '../Signum.Entities'
import { Typeahead } from '../Components'
import { toFilterRequests } from '../Finder';
Expand Down Expand Up @@ -173,7 +173,7 @@ export class FindOptionsAutocompleteConfig implements AutocompleteConfig<ResultR
Finder.API.FindRowsLike({
queryKey: getQueryKey(this.findOptions.queryName),
columns: columns.map(c => ({ token: c.token!.fullKey, displayName: c.displayName }) as ColumnRequest),
filters: [{ token: "Entity.Id", operation: "EqualTo", value: lite.id }],
filters: [{ token: QueryTokenString.entity<Entity>().append(e => e.id).toString(), operation: "EqualTo", value: lite.id }],
orders: [],
count: 1,
subString: ""
Expand Down
4 changes: 2 additions & 2 deletions Signum.React/Scripts/Operations.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react"
import * as React from "react"
import { Dic } from './Globals'
import { ajaxPost } from './Services'
import {
Expand All @@ -25,7 +25,7 @@ export function start() {

QuickLinks.registerGlobalQuickLink(ctx => new QuickLinks.QuickLinkExplore({
queryName: OperationLogEntity,
parentToken: "Target",
parentToken: OperationLogEntity.token(e => e.target),
parentValue: ctx.lite
},
{
Expand Down
80 changes: 80 additions & 0 deletions Signum.React/Scripts/Reflection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dic } from './Globals';
import { ModifiableEntity, Entity, Lite, MListElement, ModelState, MixinEntity } from './Signum.Entities'; //ONLY TYPES!
import { ajaxGet } from './Services';
import { MList } from "./Signum.Entities";
import QueryTokenBuilder from './SearchControl/QueryTokenBuilder';

export function getEnumInfo(enumTypeName: string, enumId: number) {

Expand Down Expand Up @@ -865,6 +866,85 @@ export class Type<T extends ModifiableEntity> implements IType {
isLite(obj: any): obj is Lite<T & Entity> {
return obj && (obj as Lite<Entity>).EntityType == this.typeName;
}

/* Constructs a QueryToken compatible string like "Name" from a strongly typed lambda like a => a.name
* Note: The QueryToken language is quite different to javascript lambdas (Any, Lites, Nullable, etc) but this method works in the common simple cases*/
token(): QueryTokenString<T>;
token<S>(lambdaToColumn: (v: T) => S) : QueryTokenString<S>;
token(lambdaToColumn?: Function): QueryTokenString<any> {
if (lambdaToColumn == null)
return new QueryTokenString("");
else
return new QueryTokenString(getLambdaMembers(lambdaToColumn).map(a => a.name.firstUpper()).join("."));
}
}


/* Some examples being in ExceptionEntity:
* "User" -> ExceptionEntity.token().append(a => a.user)
* ExceptionEntity.token(a => a.user)
* "Entity.User" -> ExceptionEntity.token().entity(a => a.user)
* ExceptionEntity.token().entity().append(a=>a.user)
*
*/
export class QueryTokenString<T> {

token: string;
constructor(token: string) {
this.token = token;
}

toString() {
return this.token;
}

static entity<T extends Entity = Entity>() {
return new QueryTokenString<T>("Entity");
}

static count() {
return new QueryTokenString("Count");
}

systemValidFrom() {
return new QueryTokenString(this.token + ".SystemValidFrom");
}

systemValidTo() {
return new QueryTokenString(this.token + "Entity.SystemValidTo");
}

entity() : QueryTokenString<T>;
entity<S>(lambdaToProperty: (v: T) => S) : QueryTokenString<S>;
entity(lambdaToProperty?: Function): QueryTokenString<any> {
if (this.token != "")
throw new Error("entity is only meant to be used with an empty token");

if (lambdaToProperty == null)
return new QueryTokenString("Entity")
else
return new QueryTokenString("Entity." + getLambdaMembers(lambdaToProperty).map(a => a.name.firstUpper()).join("."));
}

cast<R extends Entity>(t: Type<R>): QueryTokenString<R> {
return new QueryTokenString<R>(this.token + ".(" + t.typeName + ")");
}

append<S>(lambdaToProperty: (v: T) => S): QueryTokenString<S> {
return new QueryTokenString<S>(this.token + "." + getLambdaMembers(lambdaToProperty).map(a => a.name.firstUpper()).join("."));
}

mixin<M extends MixinEntity>(t: Type<M>): QueryTokenString<M> {
return new QueryTokenString<M>(this.token);
}

implicit<S>(lambdaToProperty: (v: T) => S): QueryTokenString<S> {
return new QueryTokenString<S>(this.token);
}

expression<S>(expressionName: string): QueryTokenString<S> {
return new QueryTokenString<S>(this.token + "." + expressionName);
}
}

export class EnumType<T extends string> {
Expand Down
2 changes: 1 addition & 1 deletion Signum.React/Scripts/SearchControl/SearchControlLoaded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export default class SearchControlLoaded extends React.Component<SearchControlLo
groupResults: fo.groupResults,
filters: toFilterRequests(fo.filterOptions),
columns: fo.columnOptions.filter(a => a.token != undefined).map(co => ({ token: co.token!.fullKey, displayName: co.displayName! }))
.concat((!fo.groupResults && qs && qs.hiddenColumns || []).map(co => ({ token: co.token, displayName: "" }))),
.concat((!fo.groupResults && qs && qs.hiddenColumns || []).map(co => ({ token: co.token.toString(), displayName: "" }))),
orders: fo.orderOptions.filter(a => a.token != undefined).map(oo => ({ token: oo.token.fullKey, orderType: oo.orderType })),
pagination: fo.pagination,
systemTime: fo.systemTime,
Expand Down
29 changes: 17 additions & 12 deletions Signum.React/Scripts/SearchControl/SystemTimeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as moment from 'moment'
import * as moment from 'moment'
import * as React from 'react'
import * as Finder from '../Finder'
import { classes } from '../Globals';
import { SystemTime, FindOptionsParsed, QueryDescription } from '../FindOptions'
import { SystemTimeMode } from '../Signum.Entities.DynamicQuery'
import { JavascriptMessage } from '../Signum.Entities'
import { DateTimePicker } from 'react-widgets';
import { QueryTokenString } from '../Reflection';
import QueryTokenBuilder from './QueryTokenBuilder';
import { OperationLogEntity } from '../Signum.Entities.Basics';

interface SystemTimeEditorProps extends React.Props<SystemTime> {
findOptions: FindOptionsParsed;
Expand Down Expand Up @@ -33,15 +36,15 @@ export default class SystemTimeEditor extends React.Component<SystemTimeEditorPr
var fop = this.props.findOptions;
if (this.isPeriodChecked()) {
fop.columnOptions.extract(a => a.token != null && (
a.token.fullKey.startsWith("Entity.SystemValidFrom") ||
a.token.fullKey.startsWith("Entity.SystemValidTo")));
a.token.fullKey.startsWith(QueryTokenString.entity().systemValidFrom().toString()) ||
a.token.fullKey.startsWith(QueryTokenString.entity().systemValidTo().toString())));
this.props.onChanged();
}
else {

Finder.parseColumnOptions([
{ token: "Entity.SystemValidFrom" },
{ token: "Entity.SystemValidTo" }
{ token: QueryTokenString.entity().systemValidFrom() },
{ token: QueryTokenString.entity().systemValidTo() }
], fop.groupResults, this.props.queryDescription).then(cops => {
fop.columnOptions = [...cops, ...fop.columnOptions];
this.props.onChanged();
Expand All @@ -53,8 +56,8 @@ export default class SystemTimeEditor extends React.Component<SystemTimeEditorPr
var cos = this.props.findOptions.columnOptions;

return cos.some(a => a.token != null && (
a.token.fullKey.startsWith("Entity.SystemValidFrom") ||
a.token.fullKey.startsWith("Entity.SystemValidTo"))
a.token.fullKey.startsWith(QueryTokenString.entity().systemValidFrom().toString()) ||
a.token.fullKey.startsWith(QueryTokenString.entity().systemValidTo().toString()))
);
}

Expand All @@ -70,23 +73,25 @@ export default class SystemTimeEditor extends React.Component<SystemTimeEditorPr
}

handlePreviousOperationClicked = () => {

var prevLogToken = QueryTokenString.entity().expression<OperationLogEntity>("PreviousOperationLog");

var fop = this.props.findOptions;
if (this.isPreviousOperationChecked()) {
fop.columnOptions.extract(a => a.token != null && a.token.fullKey.startsWith("Entity.PreviousOperationLog"));
fop.columnOptions.extract(a => a.token != null && a.token.fullKey.startsWith(prevLogToken.toString()));
this.props.onChanged();
}
else {

Finder.parseColumnOptions([
{ token: "Entity.PreviousOperationLog.Start" },
{ token: "Entity.PreviousOperationLog.User" },
{ token: "Entity.PreviousOperationLog.Operation" },
{ token: prevLogToken.append(a => a.start) },
{ token: prevLogToken.append(a => a.user) },
{ token: prevLogToken.append(a => a.operation) },
], fop.groupResults, this.props.queryDescription).then(cops => {
fop.columnOptions = [...cops, ...fop.columnOptions];
this.props.onChanged();
}).done();
}

}

isPreviousOperationChecked() {
Expand Down
8 changes: 4 additions & 4 deletions Signum.React/Scripts/SearchControl/ValueSearchControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { classes } from '../Globals'
import * as Finder from '../Finder'
import { FindOptions, FindOptionsParsed, SubTokensOptions, QueryToken, QueryValueRequest } from '../FindOptions'
import { Lite, Entity, getToString, EmbeddedEntity } from '../Signum.Entities'
import { getQueryKey, toNumbroFormat, toMomentFormat, getEnumInfo } from '../Reflection'
import { getQueryKey, toNumbroFormat, toMomentFormat, getEnumInfo, QueryTokenString } from '../Reflection'
import { AbortableRequest } from "../Services";
import { SearchControlProps } from "./SearchControl";
import { BsColor } from '../Components';
import { toFilterRequests } from '../Finder';

export interface ValueSearchControlProps extends React.Props<ValueSearchControl> {
valueToken?: string;
valueToken?: string | QueryTokenString<any>;
findOptions: FindOptions;
isLink?: boolean;
isBadge?: boolean | "MoreThanZero";
Expand Down Expand Up @@ -53,7 +53,7 @@ export default class ValueSearchControl extends React.Component<ValueSearchContr
return {
queryKey: fo.queryKey,
filters: toFilterRequests(fo.filterOptions),
valueToken: this.props.valueToken,
valueToken: this.props.valueToken && this.props.valueToken.toString(),
systemTime: fo.systemTime && { ...fo.systemTime }
};
}
Expand Down Expand Up @@ -87,7 +87,7 @@ export default class ValueSearchControl extends React.Component<ValueSearchContr

this.setState({ token: undefined, value: undefined });
if (props.valueToken)
Finder.parseSingleToken(props.findOptions.queryName, props.valueToken, SubTokensOptions.CanAggregate | SubTokensOptions.CanAnyAll)
Finder.parseSingleToken(props.findOptions.queryName, props.valueToken.toString(), SubTokensOptions.CanAggregate | SubTokensOptions.CanAnyAll)
.then(st => {
this.setState({ token: st });
this.props.onTokenLoaded && this.props.onTokenLoaded();
Expand Down
Loading

2 comments on commit 10d2421

@olmobrutall
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryTokenString<T>: Against the last stringly-typed stronghold

When creating SearchControls, ValueSearchControls or anyting that requires a FindOptions we usually need to write QueryTokens. Till today they where error-prone strings like "Entity.Category".

This commit provides a new type-safe approach. QueryTokenString<T>, providing a fluent API to generate query token strings.

This is the API for now:

export class QueryTokenString<T> {

  token: string;
  constructor(token: string) {
    this.token = token;
  }

  toString() {
    return this.token;
  }

  static entity<T extends Entity = Entity>() {
    return new QueryTokenString<T>("Entity");
  }

  static count() {
    return new QueryTokenString("Count");
  }

  systemValidFrom() {
    return new QueryTokenString(this.token + ".SystemValidFrom");
  }

  systemValidTo() {
    return new QueryTokenString(this.token + "Entity.SystemValidTo");
  }

  entity() : QueryTokenString<T>;
  entity<S>(lambdaToProperty: (v: T) => S) : QueryTokenString<S>;
  entity(lambdaToProperty?: Function): QueryTokenString<any> {
    if (this.token != "")
      throw new Error("entity is only meant to be used with an empty token");

    if (lambdaToProperty == null)
      return new QueryTokenString("Entity")
    else
      return new QueryTokenString("Entity." + getLambdaMembers(lambdaToProperty).map(a => a.name.firstUpper()).join("."));
  }

  cast<R extends Entity>(t: Type<R>): QueryTokenString<R> {
    return new QueryTokenString<R>(this.token + ".(" + t.typeName + ")");
  }

  append<S>(lambdaToProperty: (v: T) => S): QueryTokenString<S> {
    return new QueryTokenString<S>(this.token + "." + getLambdaMembers(lambdaToProperty).map(a => a.name.firstUpper()).join("."));
  }

  mixin<M extends MixinEntity>(t: Type<M>): QueryTokenString<M> {
    return new QueryTokenString<M>(this.token);
  }

  implicit<S>(lambdaToProperty: (v: T) => S): QueryTokenString<S> {
    return new QueryTokenString<S>(this.token);
  }

  expression<S>(expressionName: string): QueryTokenString<S> {
    return new QueryTokenString<S>(this.token + "." + expressionName);
  }
}

And Type has a convinced methods to get one instance:

export class Type<T extends ModifiableEntity> implements IType {
  
  [...]

  token(): QueryTokenString<T>;
  token<S>(lambdaToColumn: (v: T) => S) : QueryTokenString<S>;
  token(lambdaToColumn?: Function): QueryTokenString<any> {
    if (lambdaToColumn == null)
      return new QueryTokenString("");
    else
      return new QueryTokenString(getLambdaMembers(lambdaToColumn).map(a => a.name.firstUpper()).join("."));
  }
}

How To use it

instead of writing:

Finder.find<WorkflowEntity>({
       queryName: WorkflowEntity,	      
       parentToken: "Entity.MainEntityType.CleanName",	      
       parentValue: this.props.typeName,	       
})

Now you write:

Finder.find<WorkflowEntity>({
       queryName: WorkflowEntity,	      
       parentToken: WorkflowEntity.token().entity(a => a.mainEntityType!.cleanName),	      
       parentValue: this.props.typeName,	       
})

There are many more examples here: signumsoftware/extensions@e010e76

Limitations

The QueryToken API is not a 1-to-1 mapping to C# or Typescript, making this API not as straightforward as it could be. This differences are there to simplify the UI to the end user.

  • Casing: In Typescript members are lowercase, but QueryTokenString will automatically make it uppercase.
  • Nullability: QueryTokens propagate nulls automatically, you'll need to add some ! here an there. They will be removed in javascript and also in the QueryToken.
  • Expressions: Registered expressions are not generated in TS, so there is no strongly-typed story for it. You'll need to use expression(string) method.
  • Lites: Following a lite entity is automatic in QueryToken language, in order to continue the method chain you need to use .implicit(a=>a.entity!)

How to upgrade:

FIND: token:\s*"Entity.([^"]+)"
REPL: token: MyEntity.token().entity(e => e.$1)

FIND: parentToken:\s*"Entity.([^"]+)"
REPL: parentToken: MyEntity.token().entity(e => e.$1)

FIND: token:\s*"([^"]+)"
REPL: token: MyEntity.token(e => e.$1)

FIND: parentToken:\s*"([^"]+)"
REPL: parentToken: MyEntity.token(e => e.$1)

Then you'll need to manually change MyEntity for the entity related to the queryName and fix the lambda ([Ctrl]+[Space]).

In some cases where queryName is dynamic you can use the static methods in QueryTokenString like QueryTokenString.entity()

Enjoy a type-safe world 👍 👮 🎈

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 10d2421 Dec 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

Please sign in to comment.