From 9f9987b255a0fceb8a781f3ef735be6ab46fcc89 Mon Sep 17 00:00:00 2001 From: Beau Cameron Date: Sat, 19 Aug 2023 08:09:42 -0600 Subject: [PATCH 01/23] Initial Pass at Lambda --- packages/sp/spqueryable.ts | 137 +++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 4 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index e246c3d35..bb496d9c7 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -149,22 +149,151 @@ export class _SPQueryable extends Queryable { export interface ISPQueryable extends _SPQueryable { } export const SPQueryable = spInvokableFactory(_SPQueryable); + +/** + * Supported Odata Operators for SharePoint + * + */ +type FilterOperation = "eq" | "ne" | "gt" | "lt" | "startswith" | "endswith" | "substringof"; + +/** +* FilterField class for constructing OData filter operators +* +*/ +class FilterField { + constructor(private parent: FilterBuilder, private field: keyof any) {} + + public equals(value: string | number): FilterBuilder { + this.parent.addFilter(this.field as string, "eq", value); + return this.parent; + } + + public notEquals(value: string | number): FilterBuilder { + this.parent.addFilter(this.field, "ne", value); + return this.parent; + } + + public greaterThan(value: number|Date): FilterBuilder { + this.parent.addFilter(this.field, "gt", value); + return this.parent; + } + + public lessThan(value: number|Date): FilterBuilder { + this.parent.addFilter(this.field, "lt", value); + return this.parent; + } + + public startsWith(value: string): FilterBuilder { + this.parent.addFilter(this.field, "startswith", value); + return this.parent; + } + + public endsWith(value: string): FilterBuilder { + this.parent.addFilter(this.field, "endswith", value); + return this.parent; + } + public substringof(value: string): FilterBuilder { + this.parent.addFilter(this.field, "substringof", value); + return this.parent; + } +} + +/** + * FilterBuilder class for constructing OData filter queries + * + */ +export class FilterBuilder { + private condition = ""; + + public field(field: keyof any): FilterField { + return new FilterField(this, field); + } + + public and(filter: (builder: FilterBuilder) => void): FilterBuilder { + const previousCondition = this.condition; + filter(this); + const conditionInGroup = this.condition; + this.condition = `(${previousCondition} and ${conditionInGroup})`; + return this; + } + + public or(filter: (builder: FilterBuilder) => void): FilterBuilder { + const previousCondition = this.condition; + filter(this); + const conditionInGroup = this.condition; + this.condition = `(${previousCondition} or ${conditionInGroup})`; + return this; + } + + public addFilter(field: keyof GetType, operation: FilterOperation, value: string | number | Date): void { + switch(operation) { + case ("startswith" || "endswith"): + this.condition = `${operation}(${String(field)},${this.formatValue(value)})`; + break; + case "substringof": + this.condition = `${operation}(${this.formatValue(value)},${String(field)})}`; + break; + default: + this.condition = `${String(field)} ${operation} ${this.formatValue(value)}`; + } + } + + private formatValue(value: string | number | object): string { + switch(typeof value){ + case "string": + return `'${value}'`; + case "number": + return value.toString(); + case "object": + if(value instanceof Date){ + const isoDate = value.toISOString(); + return `datetime'${isoDate}'`; + } + break; + default: + return `${value}`; + } + } + + public build(): string { + return this.condition; + } +} + /** * Represents a REST collection which can be filtered, paged, and selected * */ export class _SPCollection extends _SPQueryable { - + private filterConditions: string[] = []; /** * Filters the returned collection (https://msdn.microsoft.com/en-us/library/office/fp142385.aspx#bk_supported) * - * @param filter The string representing the filter query + * @param filter The filter condition function */ - public filter(filter: string): this { - this.query.set("$filter", filter); + + public filter(filter: string | ((builder: FilterBuilder) => void)): this { + if (typeof filter === "string") { + this.query.set("$filter", filter); + } else { + const filterBuilder = new FilterBuilder(); + filter(filterBuilder); + this.query.set("$filter", filterBuilder.build()); + } return this; } + // don't really need this. + public getFilterQuery(): string { + if (this.filterConditions.length === 0) { + return ""; + } else if (this.filterConditions.length === 1) { + return `${this.filterConditions[0]}`; + } else { + return `${this.filterConditions.join(" and ")}`; + } + } + /** * Orders based on the supplied fields * From 4c43d58cc5da8c0277c2157f71e4c94835b9254a Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 20 Nov 2023 18:29:12 +0100 Subject: [PATCH 02/23] =?UTF-8?q?=F0=9F=94=A8=20-=20OData=20filter=20inita?= =?UTF-8?q?l=20prototype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 171 +++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index bb496d9c7..89894dfda 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -371,3 +371,174 @@ export interface IDeleteableWithETag { */ delete(eTag?: string): Promise; } + + + +export namespace OData { + enum FilterOperation { + Equals = "eq", + NotEquals = "ne", + GreaterThan = "gt", + GreaterThanOrEqualTo = "ge", + LessThan = "lt", + LessThanOrEqualTo = "le", + StartsWith = "startswith", + EndsWith = "endswith", + SubstringOf = "substringof" + } + + enum FilterJoinOperator { + And = "and", + AndWithSpace = " and ", + Or = "or", + OrWithSpace = " or " + } + + export const Where = () => new ODataFilterClass(); + + class BaseQueryable { + protected query: string[] = []; + + constructor(query: string[]) { + this.query = query; + } + } + + class WhereClause extends BaseQueryable { + constructor(q: string[]) { + super(q); + } + + public TextField(InternalName: keyof T): TextField { + return new TextField([...this.query, (InternalName as string)]); + } + + public NumberField(InternalName: keyof T): NumberField { + return new NumberField([...this.query, (InternalName as string)]); + } + + public DateField(InternalName: keyof T): DateField { + return new DateField([...this.query, (InternalName as string)]); + } + + public BooleanField(InternalName: keyof T): BooleanField { + return new BooleanField([...this.query, (InternalName as string)]); + } + } + + class ODataFilterClass extends WhereClause{ + constructor() { + super([]); + } + + public All(queries: BaseFilterCompareResult[]): BaseFilterCompareResult { + return new BaseFilterCompareResult(["(", queries.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace), ")"]); + } + + public Some(queries: BaseFilterCompareResult[]): BaseFilterCompareResult { + // This is pretty ugly, but avoids the space between the parenthesis and the first filter/last filter - I'm not sure if I like this, or the All one more, and then just living with the '( (' effect + return new BaseFilterCompareResult([...queries.map((filter, index, arr) => `${index == 0 ? "(" : ""}${filter.ToString()}${arr.length-1 == index ? ")" : ""}`).join(FilterJoinOperator.OrWithSpace)]); + } + } + + class BaseField extends BaseQueryable{ + constructor(q: string[]) { + super(q); + } + + protected ToODataValue(value: Tinput): string { + return `'${value}'`; + } + + public EqualTo(value: Tinput): BaseFilterCompareResult { + return new BaseFilterCompareResult([...this.query, FilterOperation.Equals, this.ToODataValue(value)]); + } + + public NotEqualTo(value: Tinput): BaseFilterCompareResult { + return new BaseFilterCompareResult([...this.query, FilterOperation.NotEquals, this.ToODataValue(value)]); + } + } + + class BaseComparableField extends BaseField{ + constructor(q: string[]) { + super(q); + } + + public GreaterThan(value: Ttype): BaseFilterCompareResult { + return new BaseFilterCompareResult([...this.query, FilterOperation.GreaterThan, this.ToODataValue(value)]); + } + + public GreaterThanOrEqualTo(value: Ttype): BaseFilterCompareResult { + return new BaseFilterCompareResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)]); + } + + public LessThan(value: Ttype): BaseFilterCompareResult { + return new BaseFilterCompareResult([...this.query, FilterOperation.LessThan, this.ToODataValue(value)]); + } + + public LessThanOrEqualTo(value: Ttype): BaseFilterCompareResult { + return new BaseFilterCompareResult([...this.query, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)]); + } + } + + class TextField extends BaseField{ + constructor(q: string[]) { + super(q); + } + } + + class NumberField extends BaseComparableField{ + constructor(q: string[]) { + super(q); + } + + protected override ToODataValue(value: number): string { + return `${value}`; + } + } + + class DateField extends BaseComparableField{ + constructor(q: string[]) { + super(q); + } + + protected override ToODataValue(value: Date): string { + return `'${value.toISOString()}'` + } + } + + class BooleanField extends BaseField{ + constructor(q: string[]) { + super(q); + } + + protected override ToODataValue(value: boolean): string { + return `${value == null ? null : value ? 1 : 0}`; + } + } + + + class BaseFilterCompareResult extends BaseQueryable{ + constructor(q: string[]) { + super(q); + } + + public Or(): FilterResult { + return new FilterResult(this.query, FilterJoinOperator.Or); + } + + public And(): FilterResult { + return new FilterResult(this.query, FilterJoinOperator.And); + } + + public ToString(): string { + return this.query.join(" "); + } + } + + class FilterResult extends WhereClause{ + constructor(currentQuery: string[], FilterJoinOperator: FilterJoinOperator) { + super([...currentQuery, FilterJoinOperator]); + } + } +} \ No newline at end of file From fbf1f568fbfa31a74fe88a22dbd7bfa932c928b9 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 11 Dec 2023 21:48:38 +0100 Subject: [PATCH 03/23] =?UTF-8?q?=F0=9F=8E=89=20-=20Initial=20work=20on=20?= =?UTF-8?q?filter=20queryable=20with=20lambda=20expressions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 478 +++++++++++++++++++------------------ 1 file changed, 245 insertions(+), 233 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 89894dfda..e058f5a6f 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -149,117 +149,6 @@ export class _SPQueryable extends Queryable { export interface ISPQueryable extends _SPQueryable { } export const SPQueryable = spInvokableFactory(_SPQueryable); - -/** - * Supported Odata Operators for SharePoint - * - */ -type FilterOperation = "eq" | "ne" | "gt" | "lt" | "startswith" | "endswith" | "substringof"; - -/** -* FilterField class for constructing OData filter operators -* -*/ -class FilterField { - constructor(private parent: FilterBuilder, private field: keyof any) {} - - public equals(value: string | number): FilterBuilder { - this.parent.addFilter(this.field as string, "eq", value); - return this.parent; - } - - public notEquals(value: string | number): FilterBuilder { - this.parent.addFilter(this.field, "ne", value); - return this.parent; - } - - public greaterThan(value: number|Date): FilterBuilder { - this.parent.addFilter(this.field, "gt", value); - return this.parent; - } - - public lessThan(value: number|Date): FilterBuilder { - this.parent.addFilter(this.field, "lt", value); - return this.parent; - } - - public startsWith(value: string): FilterBuilder { - this.parent.addFilter(this.field, "startswith", value); - return this.parent; - } - - public endsWith(value: string): FilterBuilder { - this.parent.addFilter(this.field, "endswith", value); - return this.parent; - } - public substringof(value: string): FilterBuilder { - this.parent.addFilter(this.field, "substringof", value); - return this.parent; - } -} - -/** - * FilterBuilder class for constructing OData filter queries - * - */ -export class FilterBuilder { - private condition = ""; - - public field(field: keyof any): FilterField { - return new FilterField(this, field); - } - - public and(filter: (builder: FilterBuilder) => void): FilterBuilder { - const previousCondition = this.condition; - filter(this); - const conditionInGroup = this.condition; - this.condition = `(${previousCondition} and ${conditionInGroup})`; - return this; - } - - public or(filter: (builder: FilterBuilder) => void): FilterBuilder { - const previousCondition = this.condition; - filter(this); - const conditionInGroup = this.condition; - this.condition = `(${previousCondition} or ${conditionInGroup})`; - return this; - } - - public addFilter(field: keyof GetType, operation: FilterOperation, value: string | number | Date): void { - switch(operation) { - case ("startswith" || "endswith"): - this.condition = `${operation}(${String(field)},${this.formatValue(value)})`; - break; - case "substringof": - this.condition = `${operation}(${this.formatValue(value)},${String(field)})}`; - break; - default: - this.condition = `${String(field)} ${operation} ${this.formatValue(value)}`; - } - } - - private formatValue(value: string | number | object): string { - switch(typeof value){ - case "string": - return `'${value}'`; - case "number": - return value.toString(); - case "object": - if(value instanceof Date){ - const isoDate = value.toISOString(); - return `datetime'${isoDate}'`; - } - break; - default: - return `${value}`; - } - } - - public build(): string { - return this.condition; - } -} - /** * Represents a REST collection which can be filtered, paged, and selected * @@ -272,13 +161,14 @@ export class _SPCollection extends _SPQueryable { * @param filter The filter condition function */ - public filter(filter: string | ((builder: FilterBuilder) => void)): this { + public filter(filter: string | ((builder: QueryableGroups) => ComparisonResult)): this { if (typeof filter === "string") { this.query.set("$filter", filter); } else { - const filterBuilder = new FilterBuilder(); - filter(filterBuilder); - this.query.set("$filter", filterBuilder.build()); + this.query.set("$filter", filter(SPOData.Where()).ToString()); + // const filterBuilder = new FilterBuilder(); + // filter(filterBuilder); + // this.query.set("$filter", filterBuilder.build()); } return this; } @@ -374,171 +264,293 @@ export interface IDeleteableWithETag { -export namespace OData { - enum FilterOperation { - Equals = "eq", - NotEquals = "ne", - GreaterThan = "gt", - GreaterThanOrEqualTo = "ge", - LessThan = "lt", - LessThanOrEqualTo = "le", - StartsWith = "startswith", - EndsWith = "endswith", - SubstringOf = "substringof" - } - enum FilterJoinOperator { - And = "and", - AndWithSpace = " and ", - Or = "or", - OrWithSpace = " or " + +type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T]; + +enum FilterOperation { + Equals = "eq", + NotEquals = "ne", + GreaterThan = "gt", + GreaterThanOrEqualTo = "ge", + LessThan = "lt", + LessThanOrEqualTo = "le", + StartsWith = "startswith", + SubstringOf = "substringof" +} + +enum FilterJoinOperator { + And = "and", + AndWithSpace = " and ", + Or = "or", + OrWithSpace = " or " +} + +export class SPOData { + static Where() { + return new QueryableGroups(); } +} - export const Where = () => new ODataFilterClass(); +class BaseQuery { + protected query: string[] = []; - class BaseQueryable { - protected query: string[] = []; + protected AddToQuery(InternalName: keyof TBaseInterface, Operation: FilterOperation, Value: string) { + this.query.push(`${InternalName as string} ${Operation} ${Value}`); + } - constructor(query: string[]) { - this.query = query; - } + protected AddQueryableToQuery(Queries: ComparisonResult) { + this.query.push(Queries.ToString()); } - class WhereClause extends BaseQueryable { - constructor(q: string[]) { - super(q); + constructor(BaseQuery?: BaseQuery) { + if (BaseQuery != null) { + this.query = BaseQuery.query; } + } +} - public TextField(InternalName: keyof T): TextField { - return new TextField([...this.query, (InternalName as string)]); - } - public NumberField(InternalName: keyof T): NumberField { - return new NumberField([...this.query, (InternalName as string)]); - } +class QueryableFields extends BaseQuery { + public TextField(InternalName: KeysMatching): TextField { + return new TextField(this, InternalName); + } - public DateField(InternalName: keyof T): DateField { - return new DateField([...this.query, (InternalName as string)]); - } + public ChoiceField(InternalName: KeysMatching): TextField { + return new TextField(this, InternalName); + } - public BooleanField(InternalName: keyof T): BooleanField { - return new BooleanField([...this.query, (InternalName as string)]); - } + public MultiChoiceField(InternalName: KeysMatching): TextField { + return new TextField(this, InternalName); } - class ODataFilterClass extends WhereClause{ - constructor() { - super([]); - } + public NumberField(InternalName: KeysMatching): NumberField { + return new NumberField(this, InternalName); + } - public All(queries: BaseFilterCompareResult[]): BaseFilterCompareResult { - return new BaseFilterCompareResult(["(", queries.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace), ")"]); - } + public DateField(InternalName: KeysMatching): DateField { + return new DateField(this, InternalName); + } - public Some(queries: BaseFilterCompareResult[]): BaseFilterCompareResult { - // This is pretty ugly, but avoids the space between the parenthesis and the first filter/last filter - I'm not sure if I like this, or the All one more, and then just living with the '( (' effect - return new BaseFilterCompareResult([...queries.map((filter, index, arr) => `${index == 0 ? "(" : ""}${filter.ToString()}${arr.length-1 == index ? ")" : ""}`).join(FilterJoinOperator.OrWithSpace)]); - } + public BooleanField(InternalName: KeysMatching): BooleanField { + return new BooleanField(this, InternalName); } - class BaseField extends BaseQueryable{ - constructor(q: string[]) { - super(q); - } + public LookupField>(InternalName: TKey): LookupQueryableFields { + return new LookupQueryableFields(this, InternalName as string); + } - protected ToODataValue(value: Tinput): string { - return `'${value}'`; - } + public LookupIdField>(InternalName: TKey): NumberField { + const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; + return new NumberField(this, col as any as keyof TBaseInterface); + } +} - public EqualTo(value: Tinput): BaseFilterCompareResult { - return new BaseFilterCompareResult([...this.query, FilterOperation.Equals, this.ToODataValue(value)]); - } +class LookupQueryableFields extends BaseQuery{ + private LookupField: string; + constructor(q: BaseQuery, LookupField: string) { + super(q); + this.LookupField = LookupField; + } - public NotEqualTo(value: Tinput): BaseFilterCompareResult { - return new BaseFilterCompareResult([...this.query, FilterOperation.NotEquals, this.ToODataValue(value)]); - } + public Id(Id: number): ComparisonResult { + this.AddToQuery(`${this.LookupField}Id` as keyof TBaseInterface, FilterOperation.Equals, Id.toString()); + return new ComparisonResult(this); } - class BaseComparableField extends BaseField{ - constructor(q: string[]) { - super(q); - } + public TextField(InternalName: KeysMatching): TextField { + return new TextField(this, `${this.LookupField}/${InternalName as string}` as any as keyof TBaseInterface); + } - public GreaterThan(value: Ttype): BaseFilterCompareResult { - return new BaseFilterCompareResult([...this.query, FilterOperation.GreaterThan, this.ToODataValue(value)]); - } + public NumberField(InternalName: KeysMatching): NumberField { + return new NumberField(this, `${this.LookupField}/${InternalName as string}` as any as keyof TBaseInterface); + } - public GreaterThanOrEqualTo(value: Ttype): BaseFilterCompareResult { - return new BaseFilterCompareResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)]); - } + // Support has been announced, but is not yet available in SharePoint Online + // https://www.microsoft.com/en-ww/microsoft-365/roadmap?filters=&searchterms=100503 + // public BooleanField(InternalName: KeysMatching): BooleanField { + // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); + // } +} - public LessThan(value: Ttype): BaseFilterCompareResult { - return new BaseFilterCompareResult([...this.query, FilterOperation.LessThan, this.ToODataValue(value)]); +class QueryableGroups extends QueryableFields{ + public And(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { + if (Array.isArray(queries) && queries.every(x => x instanceof ComparisonResult)) { + this.query.push(`(${queries.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); + } else { + const result = queries.map(x => x(SPOData.Where())); + this.query.push(`(${result.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); } + return new ComparisonResult(this); + } - public LessThanOrEqualTo(value: Ttype): BaseFilterCompareResult { - return new BaseFilterCompareResult([...this.query, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)]); + public Or(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { + if (Array.isArray(queries) && queries.every(x => x instanceof ComparisonResult)) { + this.query.push(`(${queries.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); + } else { + const result = queries.map(x => x(SPOData.Where())); + this.query.push(`(${result.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); } + return new ComparisonResult(this); } +} - class TextField extends BaseField{ - constructor(q: string[]) { - super(q); - } + + + + +class NullableField extends BaseQuery{ + protected InternalName: KeysMatching; + + constructor(base: BaseQuery, InternalName: keyof TBaseInterface) { + super(base); + this.InternalName = InternalName as any as KeysMatching; } - class NumberField extends BaseComparableField{ - constructor(q: string[]) { - super(q); - } + protected ToODataValue(value: TInputValueType): string { + return `'${value}'`; + } - protected override ToODataValue(value: number): string { - return `${value}`; - } + public IsNull(): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.Equals, "null"); + return new ComparisonResult(this); } - class DateField extends BaseComparableField{ - constructor(q: string[]) { - super(q); - } + public IsNotNull(): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.NotEquals, "null"); + return new ComparisonResult(this); + } +} - protected override ToODataValue(value: Date): string { - return `'${value.toISOString()}'` - } +class ComparableField extends NullableField{ + public EqualTo(value: TInputValueType): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.Equals, this.ToODataValue(value)); + return new ComparisonResult(this); } - class BooleanField extends BaseField{ - constructor(q: string[]) { - super(q); - } + public NotEqualTo(value: TInputValueType): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.NotEquals, this.ToODataValue(value)); + return new ComparisonResult(this); + } - protected override ToODataValue(value: boolean): string { - return `${value == null ? null : value ? 1 : 0}`; - } + public In(values: TInputValueType[]): ComparisonResult { + + const query = values.map(x => + `${this.InternalName as string} ${FilterOperation.Equals} ${this.ToODataValue(x)}` + ).join(FilterJoinOperator.OrWithSpace); + + this.query.push(`(${query})`); + return new ComparisonResult(this); } +} +class TextField extends ComparableField{ - class BaseFilterCompareResult extends BaseQueryable{ - constructor(q: string[]) { - super(q); - } + public StartsWith(value: string): ComparisonResult { + this.query.push(`${FilterOperation.StartsWith}(${this.InternalName as string}, ${this.ToODataValue(value)})`); + return new ComparisonResult(this); + } - public Or(): FilterResult { - return new FilterResult(this.query, FilterJoinOperator.Or); - } + public Contains(value: string): ComparisonResult { + this.query.push(`${FilterOperation.SubstringOf}(${this.ToODataValue(value)}, ${this.InternalName as string})`); + return new ComparisonResult(this); + } +} - public And(): FilterResult { - return new FilterResult(this.query, FilterJoinOperator.And); - } +class BooleanField extends NullableField{ - public ToString(): string { - return this.query.join(" "); - } + protected override ToODataValue(value: boolean | null): string { + return `${value == null ? "null" : value ? 1 : 0}`; } - class FilterResult extends WhereClause{ - constructor(currentQuery: string[], FilterJoinOperator: FilterJoinOperator) { - super([...currentQuery, FilterJoinOperator]); - } + public IsTrue(): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.Equals, this.ToODataValue(true)); + return new ComparisonResult(this); + } + + public IsFalse(): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.Equals, this.ToODataValue(false)); + return new ComparisonResult(this); + } + + public IsFalseOrNull(): ComparisonResult { + this.AddQueryableToQuery(SPOData.Where().Or([ + SPOData.Where().BooleanField(this.InternalName).IsFalse(), + SPOData.Where().BooleanField(this.InternalName).IsNull() + ])); + + return new ComparisonResult(this); + } +} + +class NumericField extends ComparableField{ + + public GreaterThan(value: TInputValueType): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.GreaterThan, this.ToODataValue(value)); + return new ComparisonResult(this); + } + + public GreaterThanOrEqualTo(value: TInputValueType): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)); + return new ComparisonResult(this); + } + + public LessThan(value: TInputValueType): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.LessThan, this.ToODataValue(value)); + return new ComparisonResult(this); + } + + public LessThanOrEqualTo(value: TInputValueType): ComparisonResult { + this.AddToQuery(this.InternalName, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)); + return new ComparisonResult(this); + } +} + + +class NumberField extends NumericField{ + protected override ToODataValue(value: number): string { + return `${value}`; + } +} + +class DateField extends NumericField{ + protected override ToODataValue(value: Date): string { + return `'${value.toISOString()}'` + } + + public IsBetween(startDate: Date, endDate: Date): ComparisonResult { + this.AddQueryableToQuery(SPOData.Where().And([ + SPOData.Where().DateField(this.InternalName as string).GreaterThanOrEqualTo(startDate), + SPOData.Where().DateField(this.InternalName as string).LessThanOrEqualTo(endDate) + ])); + + return new ComparisonResult(this); + } + + public IsToday(): ComparisonResult { + const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); + const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); + return this.IsBetween(StartToday, EndToday); + } +} + + + + + + +class ComparisonResult extends BaseQuery{ + public Or(): QueryableFields { + this.query.push(FilterJoinOperator.Or); + return new QueryableFields(this); + } + + public And(): QueryableFields { + this.query.push(FilterJoinOperator.And); + return new QueryableFields(this); + } + + public ToString(): string { + return this.query.join(" "); } } \ No newline at end of file From 430a3c0b1b20e8ac3933bc7919bdfe158803248c Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 12 Dec 2023 18:30:55 +0100 Subject: [PATCH 04/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Move=20away=20from=20t?= =?UTF-8?q?he=20"class=20based"=20model=20towards=20just=20a=20string=20ar?= =?UTF-8?q?ray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This solved the issue where if you reused the same variable name in your long queries it broke --- packages/sp/spqueryable.ts | 205 ++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 95 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index e058f5a6f..47d4abae0 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -90,7 +90,7 @@ export class _SPQueryable extends Queryable { // there could be spaces or not around the boundaries let url = this.toUrl().replace(/([( *| *, *| *= *])'!(@.*?)::(.*?)'([ *)| *, *])/ig, (match, frontBoundary, labelName, value, endBoundary) => { this.log(`Rewriting aliased parameter from match ${match} to label: ${labelName} value: ${value}`, 0); - aliasedParams.set(labelName,`'${value}'`); + aliasedParams.set(labelName, `'${value}'`); return `${frontBoundary}${labelName}${endBoundary}`; }); @@ -287,83 +287,86 @@ enum FilterJoinOperator { } export class SPOData { - static Where() { + + /** + * Generates a new instance of the SPOData query builder, with the type of T + */ + public static Where() { return new QueryableGroups(); } } +/** + * Base class for all OData builder queryables + */ class BaseQuery { protected query: string[] = []; - protected AddToQuery(InternalName: keyof TBaseInterface, Operation: FilterOperation, Value: string) { - this.query.push(`${InternalName as string} ${Operation} ${Value}`); - } - - protected AddQueryableToQuery(Queries: ComparisonResult) { - this.query.push(Queries.ToString()); - } - - constructor(BaseQuery?: BaseQuery) { - if (BaseQuery != null) { - this.query = BaseQuery.query; - } + constructor(query: string[]) { + this.query = query; } } +/** + * This class is used to build a query for a SharePoint list + */ class QueryableFields extends BaseQuery { + constructor(q: string[]) { + super(q); + } + public TextField(InternalName: KeysMatching): TextField { - return new TextField(this, InternalName); + return new TextField([...this.query, (InternalName as string)]); } public ChoiceField(InternalName: KeysMatching): TextField { - return new TextField(this, InternalName); + return new TextField([...this.query, (InternalName as string)]); } public MultiChoiceField(InternalName: KeysMatching): TextField { - return new TextField(this, InternalName); + return new TextField([...this.query, (InternalName as string)]); } public NumberField(InternalName: KeysMatching): NumberField { - return new NumberField(this, InternalName); + return new NumberField([...this.query, (InternalName as string)]); } public DateField(InternalName: KeysMatching): DateField { - return new DateField(this, InternalName); + return new DateField([...this.query, (InternalName as string)]); } public BooleanField(InternalName: KeysMatching): BooleanField { - return new BooleanField(this, InternalName); + return new BooleanField([...this.query, (InternalName as string)]); } public LookupField>(InternalName: TKey): LookupQueryableFields { - return new LookupQueryableFields(this, InternalName as string); + return new LookupQueryableFields([...this.query], InternalName as string); } public LookupIdField>(InternalName: TKey): NumberField { const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; - return new NumberField(this, col as any as keyof TBaseInterface); + return new NumberField([...this.query, col]); } } -class LookupQueryableFields extends BaseQuery{ +class LookupQueryableFields extends BaseQuery{ private LookupField: string; - constructor(q: BaseQuery, LookupField: string) { + constructor(q: string[], LookupField: string) { super(q); this.LookupField = LookupField; } public Id(Id: number): ComparisonResult { - this.AddToQuery(`${this.LookupField}Id` as keyof TBaseInterface, FilterOperation.Equals, Id.toString()); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, `${this.LookupField}/Id`, FilterOperation.Equals, Id.toString()]); } public TextField(InternalName: KeysMatching): TextField { - return new TextField(this, `${this.LookupField}/${InternalName as string}` as any as keyof TBaseInterface); + return new TextField([...this.query, `${this.LookupField}/${InternalName as string}`]); } public NumberField(InternalName: KeysMatching): NumberField { - return new NumberField(this, `${this.LookupField}/${InternalName as string}` as any as keyof TBaseInterface); + return new NumberField([...this.query, `${this.LookupField}/${InternalName as string}`]); } // Support has been announced, but is not yet available in SharePoint Online @@ -373,25 +376,35 @@ class LookupQueryableFields extends BaseQuery extends QueryableFields{ + constructor() { + super([]); + } + + /** + * @param queries An array of queries to be joined by AND + */ public And(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - if (Array.isArray(queries) && queries.every(x => x instanceof ComparisonResult)) { - this.query.push(`(${queries.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); + let result: string[] = []; + if (Array.isArray(queries) && queries[0] instanceof ComparisonResult) { + result = queries.map(x => x.ToString()); } else { - const result = queries.map(x => x(SPOData.Where())); - this.query.push(`(${result.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); + result = queries.map(x => x(SPOData.Where()).ToString()); } - return new ComparisonResult(this); + return new ComparisonResult([`(${result.join(FilterJoinOperator.AndWithSpace)})`]); } - + /** + * @param queries An array of queries to be joined by OR + */ public Or(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - if (Array.isArray(queries) && queries.every(x => x instanceof ComparisonResult)) { - this.query.push(`(${queries.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); + let result: string[] = []; + if (Array.isArray(queries) && queries[0] instanceof ComparisonResult) { + result = queries.map(x => x.ToString()); } else { - const result = queries.map(x => x(SPOData.Where())); - this.query.push(`(${result.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`); + result = queries.map(x => x(SPOData.Where()).ToString()); } - return new ComparisonResult(this); + return new ComparisonResult([`(${result.join(FilterJoinOperator.OrWithSpace)})`]); } } @@ -400,11 +413,13 @@ class QueryableGroups extends QueryableFields{ class NullableField extends BaseQuery{ - protected InternalName: KeysMatching; + protected LastIndex: number; + protected InternalName: string; - constructor(base: BaseQuery, InternalName: keyof TBaseInterface) { - super(base); - this.InternalName = InternalName as any as KeysMatching; + constructor(q: string[]) { + super(q); + this.LastIndex = q.length - 1; + this.InternalName = q[this.LastIndex]; } protected ToODataValue(value: TInputValueType): string { @@ -412,119 +427,120 @@ class NullableField extends BaseQuery { - this.AddToQuery(this.InternalName, FilterOperation.Equals, "null"); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.Equals, "null"]); } public IsNotNull(): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.NotEquals, "null"); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.NotEquals, "null"]); } } class ComparableField extends NullableField{ + constructor(q: string[]) { + super(q); + } + public EqualTo(value: TInputValueType): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.Equals, this.ToODataValue(value)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(value)]); } public NotEqualTo(value: TInputValueType): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.NotEquals, this.ToODataValue(value)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.NotEquals, this.ToODataValue(value)]); } public In(values: TInputValueType[]): ComparisonResult { - - const query = values.map(x => - `${this.InternalName as string} ${FilterOperation.Equals} ${this.ToODataValue(x)}` - ).join(FilterJoinOperator.OrWithSpace); - - this.query.push(`(${query})`); - return new ComparisonResult(this); + return SPOData.Where().Or(values.map(x => this.EqualTo(x))); } } class TextField extends ComparableField{ + constructor(q: string[]) { + super(q); + } public StartsWith(value: string): ComparisonResult { - this.query.push(`${FilterOperation.StartsWith}(${this.InternalName as string}, ${this.ToODataValue(value)})`); - return new ComparisonResult(this); + const filter = `${FilterOperation.StartsWith}(${this.InternalName}, ${this.ToODataValue(value)})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); } public Contains(value: string): ComparisonResult { - this.query.push(`${FilterOperation.SubstringOf}(${this.ToODataValue(value)}, ${this.InternalName as string})`); - return new ComparisonResult(this); + const filter = `${FilterOperation.SubstringOf}(${this.ToODataValue(value)}, ${this.InternalName})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); } } class BooleanField extends NullableField{ + constructor(q: string[]) { + super(q); + } protected override ToODataValue(value: boolean | null): string { return `${value == null ? "null" : value ? 1 : 0}`; } public IsTrue(): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.Equals, this.ToODataValue(true)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(true)]); } public IsFalse(): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.Equals, this.ToODataValue(false)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(false)]); } public IsFalseOrNull(): ComparisonResult { - this.AddQueryableToQuery(SPOData.Where().Or([ - SPOData.Where().BooleanField(this.InternalName).IsFalse(), - SPOData.Where().BooleanField(this.InternalName).IsNull() - ])); - - return new ComparisonResult(this); + const filter = `(${[this.InternalName, FilterOperation.Equals, this.ToODataValue(null), FilterJoinOperator.Or, this.InternalName, FilterOperation.Equals, this.ToODataValue(false)].join(" ")})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); } } class NumericField extends ComparableField{ + constructor(q: string[]) { + super(q); + } public GreaterThan(value: TInputValueType): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.GreaterThan, this.ToODataValue(value)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.GreaterThan, this.ToODataValue(value)]); } public GreaterThanOrEqualTo(value: TInputValueType): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)]); } public LessThan(value: TInputValueType): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.LessThan, this.ToODataValue(value)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.LessThan, this.ToODataValue(value)]); } public LessThanOrEqualTo(value: TInputValueType): ComparisonResult { - this.AddToQuery(this.InternalName, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)); - return new ComparisonResult(this); + return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)]); } } class NumberField extends NumericField{ + constructor(q: string[]) { + super(q); + } + protected override ToODataValue(value: number): string { return `${value}`; } } class DateField extends NumericField{ + constructor(q: string[]) { + super(q); + } + protected override ToODataValue(value: Date): string { - return `'${value.toISOString()}'` + return `'${value.toISOString()}'`; } public IsBetween(startDate: Date, endDate: Date): ComparisonResult { - this.AddQueryableToQuery(SPOData.Where().And([ - SPOData.Where().DateField(this.InternalName as string).GreaterThanOrEqualTo(startDate), - SPOData.Where().DateField(this.InternalName as string).LessThanOrEqualTo(endDate) - ])); - - return new ComparisonResult(this); + const filter = `(${[this.InternalName, FilterOperation.GreaterThan, this.ToODataValue(startDate), FilterJoinOperator.And, this.InternalName, FilterOperation.LessThan, this.ToODataValue(endDate)].join(" ")})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); } public IsToday(): ComparisonResult { @@ -536,21 +552,20 @@ class DateField extends NumericField{ - - - class ComparisonResult extends BaseQuery{ + constructor(q: string[]) { + super(q); + } + public Or(): QueryableFields { - this.query.push(FilterJoinOperator.Or); - return new QueryableFields(this); + return new QueryableFields([...this.query, FilterJoinOperator.Or]); } public And(): QueryableFields { - this.query.push(FilterJoinOperator.And); - return new QueryableFields(this); + return new QueryableFields([...this.query, FilterJoinOperator.And]); } public ToString(): string { return this.query.join(" "); } -} \ No newline at end of file +} From 181de6e1a43e44539dba3d545163fbd458d0f53b Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 12 Dec 2023 19:00:05 +0100 Subject: [PATCH 05/23] =?UTF-8?q?=F0=9F=94=92=20-=20Updated=20package-lock?= =?UTF-8?q?=20(version:=204.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e1fa8c0a..94936a2f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pnp/monorepo", - "version": "3.17.0", + "version": "4.0.0-alpha0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@pnp/monorepo", - "version": "3.17.0", + "version": "4.0.0-alpha0", "license": "MIT", "devDependencies": { "@azure/identity": "3.2.4", From c955e880b8bbb0842c7a1719498242036b4a0341 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 18 Dec 2023 18:27:48 +0100 Subject: [PATCH 06/23] =?UTF-8?q?=F0=9F=94=A8=20-=20SPText=20filters=20wor?= =?UTF-8?q?king=20-=20not=20auto=20detecting=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 50 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 47d4abae0..b709e17d4 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -161,11 +161,11 @@ export class _SPCollection extends _SPQueryable { * @param filter The filter condition function */ - public filter(filter: string | ((builder: QueryableGroups) => ComparisonResult)): this { + public filter(filter: string | ComparisonResult): this { if (typeof filter === "string") { this.query.set("$filter", filter); } else { - this.query.set("$filter", filter(SPOData.Where()).ToString()); + this.query.set("$filter", filter.ToString()); // const filterBuilder = new FilterBuilder(); // filter(filterBuilder); // this.query.set("$filter", filterBuilder.build()); @@ -308,6 +308,46 @@ class BaseQuery { } +export const SPText = (InternalName: KeysMatching) => { + return new QueryableGroups().TextField(InternalName); +} + +export const SPChoice = (InternalName: KeysMatching) => { + return new QueryableGroups().TextField(InternalName); +} + +export const SPMultiChoice = (InternalName: KeysMatching) => { + return new QueryableGroups().TextField(InternalName as any as KeysMatching); +} + +export const SPNumber = (InternalName: KeysMatching) => { + return new QueryableGroups().NumberField(InternalName); +} + +export const SPDate = (InternalName: KeysMatching) => { + return new QueryableGroups().DateField(InternalName); +} + +export const SPBoolean = (InternalName: KeysMatching) => { + return new QueryableGroups().BooleanField(InternalName); +} + +export const SPLookup = >(InternalName: TKey) => { + return new QueryableGroups().LookupField(InternalName); +} + +export const SPLookupId = >(InternalName: TKey) => { + return new QueryableGroups().LookupIdField(InternalName); +} + +export const SPAnd = (queries: ComparisonResult[]) => { + return new QueryableGroups().And(queries); +} + +export const SPOr = (queries: ComparisonResult[]) => { + return new QueryableGroups().Or(queries); +} + /** * This class is used to build a query for a SharePoint list */ @@ -440,16 +480,16 @@ class ComparableField extends NullableField { + public Equals(value: TInputValueType): ComparisonResult { return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(value)]); } - public NotEqualTo(value: TInputValueType): ComparisonResult { + public NotEquals(value: TInputValueType): ComparisonResult { return new ComparisonResult([...this.query, FilterOperation.NotEquals, this.ToODataValue(value)]); } public In(values: TInputValueType[]): ComparisonResult { - return SPOData.Where().Or(values.map(x => this.EqualTo(x))); + return SPOData.Where().Or(values.map(x => this.Equals(x))); } } From ed467623bc3dd6c33f9fda2365a8e4c4ea1bb537 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 18 Dec 2023 19:48:47 +0100 Subject: [PATCH 07/23] =?UTF-8?q?=F0=9F=94=A8=20-=20switch=20to=20f=20=3D>?= =?UTF-8?q?=20f.Text('x').Equals('y')?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 71 ++++++++++++-------------------------- 1 file changed, 23 insertions(+), 48 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index b709e17d4..99b9bb7df 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -154,36 +154,21 @@ export const SPQueryable = spInvokableFactory(_SPQueryable); * */ export class _SPCollection extends _SPQueryable { - private filterConditions: string[] = []; /** * Filters the returned collection (https://msdn.microsoft.com/en-us/library/office/fp142385.aspx#bk_supported) * * @param filter The filter condition function */ - public filter(filter: string | ComparisonResult): this { + public filter(filter: string | ((builder: QueryableGroups) => ComparisonResult)): this { if (typeof filter === "string") { this.query.set("$filter", filter); } else { - this.query.set("$filter", filter.ToString()); - // const filterBuilder = new FilterBuilder(); - // filter(filterBuilder); - // this.query.set("$filter", filterBuilder.build()); + this.query.set("$filter", filter(new QueryableGroups()).ToString()); } return this; } - // don't really need this. - public getFilterQuery(): string { - if (this.filterConditions.length === 0) { - return ""; - } else if (this.filterConditions.length === 1) { - return `${this.filterConditions[0]}`; - } else { - return `${this.filterConditions.join(" and ")}`; - } - } - /** * Orders based on the supplied fields * @@ -307,37 +292,36 @@ class BaseQuery { } } - export const SPText = (InternalName: KeysMatching) => { - return new QueryableGroups().TextField(InternalName); + return new QueryableGroups().Text(InternalName); } export const SPChoice = (InternalName: KeysMatching) => { - return new QueryableGroups().TextField(InternalName); + return new QueryableGroups().Text(InternalName); } export const SPMultiChoice = (InternalName: KeysMatching) => { - return new QueryableGroups().TextField(InternalName as any as KeysMatching); + return new QueryableGroups().Text(InternalName as any as KeysMatching); } export const SPNumber = (InternalName: KeysMatching) => { - return new QueryableGroups().NumberField(InternalName); + return new QueryableGroups().Number(InternalName); } export const SPDate = (InternalName: KeysMatching) => { - return new QueryableGroups().DateField(InternalName); + return new QueryableGroups().Date(InternalName); } export const SPBoolean = (InternalName: KeysMatching) => { - return new QueryableGroups().BooleanField(InternalName); + return new QueryableGroups().Boolean(InternalName); } export const SPLookup = >(InternalName: TKey) => { - return new QueryableGroups().LookupField(InternalName); + return new QueryableGroups().Lookup(InternalName); } -export const SPLookupId = >(InternalName: TKey) => { - return new QueryableGroups().LookupIdField(InternalName); +export const SPLookupId = >(InternalName: TKey) => { + return new QueryableGroups().LookupId(InternalName); } export const SPAnd = (queries: ComparisonResult[]) => { @@ -356,35 +340,35 @@ class QueryableFields extends BaseQuery { super(q); } - public TextField(InternalName: KeysMatching): TextField { + public Text(InternalName: KeysMatching): TextField { return new TextField([...this.query, (InternalName as string)]); } - public ChoiceField(InternalName: KeysMatching): TextField { + public Choice(InternalName: KeysMatching): TextField { return new TextField([...this.query, (InternalName as string)]); } - public MultiChoiceField(InternalName: KeysMatching): TextField { + public MultiChoice(InternalName: KeysMatching): TextField { return new TextField([...this.query, (InternalName as string)]); } - public NumberField(InternalName: KeysMatching): NumberField { + public Number(InternalName: KeysMatching): NumberField { return new NumberField([...this.query, (InternalName as string)]); } - public DateField(InternalName: KeysMatching): DateField { + public Date(InternalName: KeysMatching): DateField { return new DateField([...this.query, (InternalName as string)]); } - public BooleanField(InternalName: KeysMatching): BooleanField { + public Boolean(InternalName: KeysMatching): BooleanField { return new BooleanField([...this.query, (InternalName as string)]); } - public LookupField>(InternalName: TKey): LookupQueryableFields { + public Lookup>(InternalName: TKey): LookupQueryableFields { return new LookupQueryableFields([...this.query], InternalName as string); } - public LookupIdField>(InternalName: TKey): NumberField { + public LookupId>(InternalName: TKey): NumberField { const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; return new NumberField([...this.query, col]); } @@ -425,25 +409,16 @@ class QueryableGroups extends QueryableFields{ /** * @param queries An array of queries to be joined by AND */ - public And(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - let result: string[] = []; - if (Array.isArray(queries) && queries[0] instanceof ComparisonResult) { - result = queries.map(x => x.ToString()); - } else { - result = queries.map(x => x(SPOData.Where()).ToString()); - } + public And(queries: ComparisonResult[]): ComparisonResult { + let result: string[] = queries.map(x => x.ToString()); return new ComparisonResult([`(${result.join(FilterJoinOperator.AndWithSpace)})`]); } + /** * @param queries An array of queries to be joined by OR */ public Or(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - let result: string[] = []; - if (Array.isArray(queries) && queries[0] instanceof ComparisonResult) { - result = queries.map(x => x.ToString()); - } else { - result = queries.map(x => x(SPOData.Where()).ToString()); - } + let result: string[] = queries.map(x => x.ToString()); return new ComparisonResult([`(${result.join(FilterJoinOperator.OrWithSpace)})`]); } } From fc6fd401bca8ce9afad66b517d8251c296d65a4c Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 18 Dec 2023 21:56:59 +0100 Subject: [PATCH 08/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Remove=20the=20SP-pref?= =?UTF-8?q?ixed=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 47 +++----------------------------------- 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 99b9bb7df..0223273f4 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -271,8 +271,7 @@ enum FilterJoinOperator { OrWithSpace = " or " } -export class SPOData { - +class SPOData { /** * Generates a new instance of the SPOData query builder, with the type of T */ @@ -292,46 +291,6 @@ class BaseQuery { } } -export const SPText = (InternalName: KeysMatching) => { - return new QueryableGroups().Text(InternalName); -} - -export const SPChoice = (InternalName: KeysMatching) => { - return new QueryableGroups().Text(InternalName); -} - -export const SPMultiChoice = (InternalName: KeysMatching) => { - return new QueryableGroups().Text(InternalName as any as KeysMatching); -} - -export const SPNumber = (InternalName: KeysMatching) => { - return new QueryableGroups().Number(InternalName); -} - -export const SPDate = (InternalName: KeysMatching) => { - return new QueryableGroups().Date(InternalName); -} - -export const SPBoolean = (InternalName: KeysMatching) => { - return new QueryableGroups().Boolean(InternalName); -} - -export const SPLookup = >(InternalName: TKey) => { - return new QueryableGroups().Lookup(InternalName); -} - -export const SPLookupId = >(InternalName: TKey) => { - return new QueryableGroups().LookupId(InternalName); -} - -export const SPAnd = (queries: ComparisonResult[]) => { - return new QueryableGroups().And(queries); -} - -export const SPOr = (queries: ComparisonResult[]) => { - return new QueryableGroups().Or(queries); -} - /** * This class is used to build a query for a SharePoint list */ @@ -410,7 +369,7 @@ class QueryableGroups extends QueryableFields{ * @param queries An array of queries to be joined by AND */ public And(queries: ComparisonResult[]): ComparisonResult { - let result: string[] = queries.map(x => x.ToString()); + const result: string[] = queries.map(x => x.ToString()); return new ComparisonResult([`(${result.join(FilterJoinOperator.AndWithSpace)})`]); } @@ -418,7 +377,7 @@ class QueryableGroups extends QueryableFields{ * @param queries An array of queries to be joined by OR */ public Or(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - let result: string[] = queries.map(x => x.ToString()); + const result: string[] = queries.map(x => x.ToString()); return new ComparisonResult([`(${result.join(FilterJoinOperator.OrWithSpace)})`]); } } From ca0b26ff0cdb2bb4df25fd2fef0110a1ab8ee319 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 19 Dec 2023 18:30:16 +0100 Subject: [PATCH 09/23] =?UTF-8?q?Revert=20"=F0=9F=94=A8=20-=20Remove=20the?= =?UTF-8?q?=20SP-prefixed=20options"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit fc6fd401bca8ce9afad66b517d8251c296d65a4c. --- packages/sp/spqueryable.ts | 47 +++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 0223273f4..99b9bb7df 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -271,7 +271,8 @@ enum FilterJoinOperator { OrWithSpace = " or " } -class SPOData { +export class SPOData { + /** * Generates a new instance of the SPOData query builder, with the type of T */ @@ -291,6 +292,46 @@ class BaseQuery { } } +export const SPText = (InternalName: KeysMatching) => { + return new QueryableGroups().Text(InternalName); +} + +export const SPChoice = (InternalName: KeysMatching) => { + return new QueryableGroups().Text(InternalName); +} + +export const SPMultiChoice = (InternalName: KeysMatching) => { + return new QueryableGroups().Text(InternalName as any as KeysMatching); +} + +export const SPNumber = (InternalName: KeysMatching) => { + return new QueryableGroups().Number(InternalName); +} + +export const SPDate = (InternalName: KeysMatching) => { + return new QueryableGroups().Date(InternalName); +} + +export const SPBoolean = (InternalName: KeysMatching) => { + return new QueryableGroups().Boolean(InternalName); +} + +export const SPLookup = >(InternalName: TKey) => { + return new QueryableGroups().Lookup(InternalName); +} + +export const SPLookupId = >(InternalName: TKey) => { + return new QueryableGroups().LookupId(InternalName); +} + +export const SPAnd = (queries: ComparisonResult[]) => { + return new QueryableGroups().And(queries); +} + +export const SPOr = (queries: ComparisonResult[]) => { + return new QueryableGroups().Or(queries); +} + /** * This class is used to build a query for a SharePoint list */ @@ -369,7 +410,7 @@ class QueryableGroups extends QueryableFields{ * @param queries An array of queries to be joined by AND */ public And(queries: ComparisonResult[]): ComparisonResult { - const result: string[] = queries.map(x => x.ToString()); + let result: string[] = queries.map(x => x.ToString()); return new ComparisonResult([`(${result.join(FilterJoinOperator.AndWithSpace)})`]); } @@ -377,7 +418,7 @@ class QueryableGroups extends QueryableFields{ * @param queries An array of queries to be joined by OR */ public Or(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - const result: string[] = queries.map(x => x.ToString()); + let result: string[] = queries.map(x => x.ToString()); return new ComparisonResult([`(${result.join(FilterJoinOperator.OrWithSpace)})`]); } } From 7d0462b8a3a4ef6f7778a570c172fe0b4ecd9ba5 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 19 Dec 2023 20:19:58 +0100 Subject: [PATCH 10/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Inital=20work=20on=20r?= =?UTF-8?q?ewrite=20inspired=20by=20Beau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 400 +++++++++++-------------------------- 1 file changed, 121 insertions(+), 279 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 99b9bb7df..0893a41c6 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -1,6 +1,8 @@ import { combine, isUrlAbsolute, isArray, objectDefinedNotNull, stringIsNullOrEmpty } from "@pnp/core"; import { IInvokable, Queryable, queryableFactory } from "@pnp/queryable"; import { spPostDelete, spPostDeleteETag } from "./operations.js"; +import { IField } from "./fields/types.js"; +import { filter } from "core-js/core/array"; export type SPInit = string | ISPQueryable | [ISPQueryable, string]; @@ -157,15 +159,10 @@ export class _SPCollection extends _SPQueryable { /** * Filters the returned collection (https://msdn.microsoft.com/en-us/library/office/fp142385.aspx#bk_supported) * - * @param filter The filter condition function + * @param filter The string representing the filter query */ - - public filter(filter: string | ((builder: QueryableGroups) => ComparisonResult)): this { - if (typeof filter === "string") { - this.query.set("$filter", filter); - } else { - this.query.set("$filter", filter(new QueryableGroups()).ToString()); - } + public filter(filter: string | ICondition | IFieldCondition): this { + this.query.set("$filter", typeof filter === "string" ? filter : filter.toQuery()); return this; } @@ -247,10 +244,6 @@ export interface IDeleteableWithETag { delete(eTag?: string): Promise; } - - - - type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T]; enum FilterOperation { @@ -266,321 +259,170 @@ enum FilterOperation { enum FilterJoinOperator { And = "and", - AndWithSpace = " and ", - Or = "or", - OrWithSpace = " or " + Or = "or" } -export class SPOData { - /** - * Generates a new instance of the SPOData query builder, with the type of T - */ - public static Where() { - return new QueryableGroups(); - } +export interface IFieldCondition { + toQuery(): string; } -/** - * Base class for all OData builder queryables - */ -class BaseQuery { - protected query: string[] = []; - - constructor(query: string[]) { - this.query = query; - } +export interface ICondition { + toQuery(): string; } -export const SPText = (InternalName: KeysMatching) => { - return new QueryableGroups().Text(InternalName); +export interface INullableFieldBuilder { + toQuery(): string; + toODataValue(value: TObjectType): string; + Equals(value: TObjectType): IFieldCondition; + NotEquals(value: TObjectType): IFieldCondition; + IsNull(): IFieldCondition; } -export const SPChoice = (InternalName: KeysMatching) => { - return new QueryableGroups().Text(InternalName); +function BaseNullableField(field: KeysMatching): INullableFieldBuilder { + return { + toQuery: () => "", + toODataValue: val => `'${val}'`, + Equals(value: TType): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(value)}` }; + }, + NotEquals(value: TType): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.NotEquals} ${this.toODataValue(value)}` }; + }, + IsNull(): IFieldCondition { + return { toQuery: () => `${field as string} eq null` }; + } + }; } -export const SPMultiChoice = (InternalName: KeysMatching) => { - return new QueryableGroups().Text(InternalName as any as KeysMatching); +export interface ITextFieldBuilder extends INullableFieldBuilder { + StartsWith(value: string): IFieldCondition; + Contains(value: string): IFieldCondition; } -export const SPNumber = (InternalName: KeysMatching) => { - return new QueryableGroups().Number(InternalName); +function BaseTextField(field: KeysMatching): ITextFieldBuilder { + return { + ...BaseNullableField(field), + StartsWith(value: string): IFieldCondition { + return { toQuery: () => `${FilterOperation.StartsWith}(${field as string}, ${this.toODataValue(value)})` }; + }, + Contains(value: string): IFieldCondition { + return { toQuery: () => `${FilterOperation.SubstringOf}(${this.toODataValue(value)}, ${field as string})` }; + } + }; } -export const SPDate = (InternalName: KeysMatching) => { - return new QueryableGroups().Date(InternalName); +export function TextField(field: KeysMatching): ITextFieldBuilder { + return BaseTextField(field); } -export const SPBoolean = (InternalName: KeysMatching) => { - return new QueryableGroups().Boolean(InternalName); +export function ChoiceField(field: KeysMatching): ITextFieldBuilder { + return BaseTextField(field); } -export const SPLookup = >(InternalName: TKey) => { - return new QueryableGroups().Lookup(InternalName); +export function MultiChoiceField(field: KeysMatching): ITextFieldBuilder { + return BaseTextField(field); } -export const SPLookupId = >(InternalName: TKey) => { - return new QueryableGroups().LookupId(InternalName); -} -export const SPAnd = (queries: ComparisonResult[]) => { - return new QueryableGroups().And(queries); -} -export const SPOr = (queries: ComparisonResult[]) => { - return new QueryableGroups().Or(queries); +interface INumericField extends INullableFieldBuilder { + Equals(value: TType): IFieldCondition; + GreaterThan(value: TType): IFieldCondition; + GreaterThanOrEquals(value: TType): IFieldCondition; + LessThan(value: TType): IFieldCondition; + LessThanOrEquals(value: TType): IFieldCondition; } -/** - * This class is used to build a query for a SharePoint list - */ -class QueryableFields extends BaseQuery { - constructor(q: string[]) { - super(q); - } - - public Text(InternalName: KeysMatching): TextField { - return new TextField([...this.query, (InternalName as string)]); - } - - public Choice(InternalName: KeysMatching): TextField { - return new TextField([...this.query, (InternalName as string)]); - } - - public MultiChoice(InternalName: KeysMatching): TextField { - return new TextField([...this.query, (InternalName as string)]); - } - - public Number(InternalName: KeysMatching): NumberField { - return new NumberField([...this.query, (InternalName as string)]); - } - public Date(InternalName: KeysMatching): DateField { - return new DateField([...this.query, (InternalName as string)]); - } - - public Boolean(InternalName: KeysMatching): BooleanField { - return new BooleanField([...this.query, (InternalName as string)]); - } - - public Lookup>(InternalName: TKey): LookupQueryableFields { - return new LookupQueryableFields([...this.query], InternalName as string); - } - - public LookupId>(InternalName: TKey): NumberField { - const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; - return new NumberField([...this.query, col]); - } +function BaseNumericField(field: KeysMatching): INumericField { + return { + ...BaseNullableField(field), + GreaterThan(value: TType): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.GreaterThan} ${this.toODataValue(value)}` }; + }, + GreaterThanOrEquals(value: TType): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.GreaterThanOrEqualTo} ${this.toODataValue(value)}` }; + }, + LessThan(value: TType): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.LessThan} ${this.toODataValue(value)}` }; + }, + LessThanOrEquals(value: TType): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.LessThanOrEqualTo} ${this.toODataValue(value)}` }; + } + }; } -class LookupQueryableFields extends BaseQuery{ - private LookupField: string; - constructor(q: string[], LookupField: string) { - super(q); - this.LookupField = LookupField; - } - - public Id(Id: number): ComparisonResult { - return new ComparisonResult([...this.query, `${this.LookupField}/Id`, FilterOperation.Equals, Id.toString()]); - } - - public TextField(InternalName: KeysMatching): TextField { - return new TextField([...this.query, `${this.LookupField}/${InternalName as string}`]); - } - - public NumberField(InternalName: KeysMatching): NumberField { - return new NumberField([...this.query, `${this.LookupField}/${InternalName as string}`]); - } - - // Support has been announced, but is not yet available in SharePoint Online - // https://www.microsoft.com/en-ww/microsoft-365/roadmap?filters=&searchterms=100503 - // public BooleanField(InternalName: KeysMatching): BooleanField { - // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); - // } +export function NumberField(field: KeysMatching): INumericField { + return { + ...BaseNumericField(field), + toODataValue: val => `${val}` + }; } - -class QueryableGroups extends QueryableFields{ - constructor() { - super([]); - } - - /** - * @param queries An array of queries to be joined by AND - */ - public And(queries: ComparisonResult[]): ComparisonResult { - let result: string[] = queries.map(x => x.ToString()); - return new ComparisonResult([`(${result.join(FilterJoinOperator.AndWithSpace)})`]); - } - - /** - * @param queries An array of queries to be joined by OR - */ - public Or(queries: ComparisonResult[] | ((builder: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - let result: string[] = queries.map(x => x.ToString()); - return new ComparisonResult([`(${result.join(FilterJoinOperator.OrWithSpace)})`]); - } +export interface IDateFieldBuilder extends INumericField { + IsToday(): IFieldCondition; + IsBetween(start: Date, end: Date): IFieldCondition; } - - - - -class NullableField extends BaseQuery{ - protected LastIndex: number; - protected InternalName: string; - - constructor(q: string[]) { - super(q); - this.LastIndex = q.length - 1; - this.InternalName = q[this.LastIndex]; - } - - protected ToODataValue(value: TInputValueType): string { - return `'${value}'`; - } - - public IsNull(): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.Equals, "null"]); +export function DateField(field: KeysMatching): IDateFieldBuilder { + return { + ...BaseNumericField(field), + toODataValue: val => `datetime'${val.toISOString()}'`, + IsBetween(startDate: Date, endDate: Date): IFieldCondition { + return { toQuery: () => `(${field as string} ${FilterOperation.GreaterThanOrEqualTo} ${this.toODataValue(startDate)} ${FilterJoinOperator.And} ${field as string} ${FilterOperation.LessThan} ${this.toODataValue(endDate)})` }; + }, + IsToday(): IFieldCondition { + const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); + const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); + return this.IsBetween(StartToday, EndToday); + } } - public IsNotNull(): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.NotEquals, "null"]); - } } -class ComparableField extends NullableField{ - constructor(q: string[]) { - super(q); - } - - public Equals(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(value)]); - } - public NotEquals(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.NotEquals, this.ToODataValue(value)]); - } - - public In(values: TInputValueType[]): ComparisonResult { - return SPOData.Where().Or(values.map(x => this.Equals(x))); - } -} -class TextField extends ComparableField{ - constructor(q: string[]) { - super(q); - } - public StartsWith(value: string): ComparisonResult { - const filter = `${FilterOperation.StartsWith}(${this.InternalName}, ${this.ToODataValue(value)})`; - this.query[this.LastIndex] = filter; - return new ComparisonResult([...this.query]); - } - public Contains(value: string): ComparisonResult { - const filter = `${FilterOperation.SubstringOf}(${this.ToODataValue(value)}, ${this.InternalName})`; - this.query[this.LastIndex] = filter; - return new ComparisonResult([...this.query]); - } +export interface IBooleanFieldBuilder extends INullableFieldBuilder { + IsTrue(): IFieldCondition; + IsFalse(): IFieldCondition; + IsFalseOrNull(): IFieldCondition; } -class BooleanField extends NullableField{ - constructor(q: string[]) { - super(q); - } - - protected override ToODataValue(value: boolean | null): string { - return `${value == null ? "null" : value ? 1 : 0}`; - } - - public IsTrue(): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(true)]); - } - - public IsFalse(): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(false)]); - } - - public IsFalseOrNull(): ComparisonResult { - const filter = `(${[this.InternalName, FilterOperation.Equals, this.ToODataValue(null), FilterJoinOperator.Or, this.InternalName, FilterOperation.Equals, this.ToODataValue(false)].join(" ")})`; - this.query[this.LastIndex] = filter; - return new ComparisonResult([...this.query]); - } +export function BooleanField(field: KeysMatching): IBooleanFieldBuilder { + return { + ...BaseNullableField(field), + toODataValue: val => `${val}`, + IsTrue(): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(true)}` }; + }, + IsFalse(): IFieldCondition { + return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)}` }; + }, + IsFalseOrNull(): IFieldCondition { + return { toQuery: () => `(${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)} ${FilterJoinOperator.Or} ${field as string} eq ${this.toODataValue(null)})` }; + } + }; } -class NumericField extends ComparableField{ - constructor(q: string[]) { - super(q); - } - - public GreaterThan(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.GreaterThan, this.ToODataValue(value)]); - } - public GreaterThanOrEqualTo(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)]); - } - public LessThan(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.LessThan, this.ToODataValue(value)]); - } - public LessThanOrEqualTo(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)]); - } +export function Or(...conditions: Array | ICondition>): ICondition { + return buildCondition(FilterJoinOperator.Or, ...conditions); } - -class NumberField extends NumericField{ - constructor(q: string[]) { - super(q); - } - - protected override ToODataValue(value: number): string { - return `${value}`; - } -} - -class DateField extends NumericField{ - constructor(q: string[]) { - super(q); - } - - protected override ToODataValue(value: Date): string { - return `'${value.toISOString()}'`; - } - - public IsBetween(startDate: Date, endDate: Date): ComparisonResult { - const filter = `(${[this.InternalName, FilterOperation.GreaterThan, this.ToODataValue(startDate), FilterJoinOperator.And, this.InternalName, FilterOperation.LessThan, this.ToODataValue(endDate)].join(" ")})`; - this.query[this.LastIndex] = filter; - return new ComparisonResult([...this.query]); - } - - public IsToday(): ComparisonResult { - const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); - const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); - return this.IsBetween(StartToday, EndToday); - } +export function And(...conditions: Array | ICondition>): ICondition { + return buildCondition(FilterJoinOperator.Or, ...conditions); } - - -class ComparisonResult extends BaseQuery{ - constructor(q: string[]) { - super(q); - } - - public Or(): QueryableFields { - return new QueryableFields([...this.query, FilterJoinOperator.Or]); - } - - public And(): QueryableFields { - return new QueryableFields([...this.query, FilterJoinOperator.And]); - } - - public ToString(): string { - return this.query.join(" "); - } -} +function buildCondition(operator: FilterJoinOperator, ...conditions: Array | ICondition>): ICondition { + return { + toQuery(): string { + ; + return `(${conditions.map(c => c.toQuery()).join(` ${operator} `)})`; + }, + }; +} \ No newline at end of file From 8efc41566bf18bba1748ab5672cfe5886cd42554 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 19 Dec 2023 20:25:17 +0100 Subject: [PATCH 11/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Added=20In=20operator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 0893a41c6..86ad3fd05 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -244,6 +244,19 @@ export interface IDeleteableWithETag { delete(eTag?: string): Promise; } + + + + + + + + + + + + + type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T]; enum FilterOperation { @@ -254,7 +267,8 @@ enum FilterOperation { LessThan = "lt", LessThanOrEqualTo = "le", StartsWith = "startswith", - SubstringOf = "substringof" + SubstringOf = "substringof", + In = "in" } enum FilterJoinOperator { @@ -298,6 +312,7 @@ function BaseNullableField(field: KeysMatching): INullableFi export interface ITextFieldBuilder extends INullableFieldBuilder { StartsWith(value: string): IFieldCondition; Contains(value: string): IFieldCondition; + In(values: string[]): IFieldCondition; } function BaseTextField(field: KeysMatching): ITextFieldBuilder { @@ -308,6 +323,9 @@ function BaseTextField(field: KeysMatching): ITextFieldBuilder }, Contains(value: string): IFieldCondition { return { toQuery: () => `${FilterOperation.SubstringOf}(${this.toODataValue(value)}, ${field as string})` }; + }, + In(values: string[]): IFieldCondition { + return Or(...values.map(v => this.Equals(v))); } }; } From 22360bafb7394fdd351d796864af577377decf8e Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 19 Dec 2023 21:00:45 +0100 Subject: [PATCH 12/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Lookup=20field=20suppo?= =?UTF-8?q?rt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 137 +++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 86ad3fd05..8102c3039 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -277,85 +277,94 @@ enum FilterJoinOperator { } -export interface IFieldCondition { +export interface IFieldCondition { toQuery(): string; } -export interface ICondition { +export interface ICondition { toQuery(): string; } -export interface INullableFieldBuilder { +export interface INullableFieldBuilder { toQuery(): string; toODataValue(value: TObjectType): string; - Equals(value: TObjectType): IFieldCondition; - NotEquals(value: TObjectType): IFieldCondition; - IsNull(): IFieldCondition; + IsNull(): IFieldCondition; } -function BaseNullableField(field: KeysMatching): INullableFieldBuilder { +function BaseNullableField(field: KeysMatching): INullableFieldBuilder { return { toQuery: () => "", toODataValue: val => `'${val}'`, - Equals(value: TType): IFieldCondition { + IsNull(): IFieldCondition { + return { toQuery: () => `${field as string} eq null` }; + } + }; +} + +interface IComperableField extends INullableFieldBuilder { + Equals(value: TObjectType): IFieldCondition; + NotEquals(value: TObjectType): IFieldCondition; +} + +function BaseComperableField(field: KeysMatching): IComperableField { + return { + ...BaseNullableField(field), + Equals(value: TType): IFieldCondition { return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(value)}` }; }, - NotEquals(value: TType): IFieldCondition { + NotEquals(value: TType): IFieldCondition { return { toQuery: () => `${field as string} ${FilterOperation.NotEquals} ${this.toODataValue(value)}` }; }, - IsNull(): IFieldCondition { - return { toQuery: () => `${field as string} eq null` }; - } }; } -export interface ITextFieldBuilder extends INullableFieldBuilder { - StartsWith(value: string): IFieldCondition; - Contains(value: string): IFieldCondition; - In(values: string[]): IFieldCondition; +export interface ITextFieldBuilder extends IComperableField { + StartsWith(value: string): IFieldCondition; + Contains(value: string): IFieldCondition; + In(...values: string[]): IFieldCondition; } -function BaseTextField(field: KeysMatching): ITextFieldBuilder { +function BaseTextField(field: KeysMatching): ITextFieldBuilder { return { - ...BaseNullableField(field), - StartsWith(value: string): IFieldCondition { + ...BaseComperableField(field), + StartsWith(value: string): IFieldCondition { return { toQuery: () => `${FilterOperation.StartsWith}(${field as string}, ${this.toODataValue(value)})` }; }, - Contains(value: string): IFieldCondition { + Contains(value: string): IFieldCondition { return { toQuery: () => `${FilterOperation.SubstringOf}(${this.toODataValue(value)}, ${field as string})` }; }, - In(values: string[]): IFieldCondition { + In(...values: string[]): IFieldCondition { return Or(...values.map(v => this.Equals(v))); } }; } -export function TextField(field: KeysMatching): ITextFieldBuilder { - return BaseTextField(field); +export function TextField(field: KeysMatching): ITextFieldBuilder { + return BaseTextField(field); } -export function ChoiceField(field: KeysMatching): ITextFieldBuilder { - return BaseTextField(field); +export function ChoiceField(field: KeysMatching): ITextFieldBuilder { + return BaseTextField(field); } -export function MultiChoiceField(field: KeysMatching): ITextFieldBuilder { - return BaseTextField(field); +export function MultiChoiceField(field: KeysMatching): ITextFieldBuilder { + return BaseTextField(field); } -interface INumericField extends INullableFieldBuilder { - Equals(value: TType): IFieldCondition; - GreaterThan(value: TType): IFieldCondition; - GreaterThanOrEquals(value: TType): IFieldCondition; - LessThan(value: TType): IFieldCondition; - LessThanOrEquals(value: TType): IFieldCondition; +interface INumericField extends INullableFieldBuilder { + Equals(value: TType): IFieldCondition; + GreaterThan(value: TType): IFieldCondition; + GreaterThanOrEquals(value: TType): IFieldCondition; + LessThan(value: TType): IFieldCondition; + LessThanOrEquals(value: TType): IFieldCondition; } function BaseNumericField(field: KeysMatching): INumericField { return { - ...BaseNullableField(field), + ...BaseComperableField(field), GreaterThan(value: TType): IFieldCondition { return { toQuery: () => `${field as string} ${FilterOperation.GreaterThan} ${this.toODataValue(value)}` }; }, @@ -378,19 +387,19 @@ export function NumberField(field: KeysMatching): INumericField extends INumericField { - IsToday(): IFieldCondition; - IsBetween(start: Date, end: Date): IFieldCondition; +export interface IDateFieldBuilder extends INumericField { + IsToday(): IFieldCondition; + IsBetween(start: Date, end: Date): IFieldCondition; } -export function DateField(field: KeysMatching): IDateFieldBuilder { +export function DateField(field: KeysMatching): IDateFieldBuilder { return { - ...BaseNumericField(field), + ...BaseNumericField(field), toODataValue: val => `datetime'${val.toISOString()}'`, - IsBetween(startDate: Date, endDate: Date): IFieldCondition { + IsBetween(startDate: Date, endDate: Date): IFieldCondition { return { toQuery: () => `(${field as string} ${FilterOperation.GreaterThanOrEqualTo} ${this.toODataValue(startDate)} ${FilterJoinOperator.And} ${field as string} ${FilterOperation.LessThan} ${this.toODataValue(endDate)})` }; }, - IsToday(): IFieldCondition { + IsToday(): IFieldCondition { const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); return this.IsBetween(StartToday, EndToday); @@ -403,28 +412,52 @@ export function DateField(field: KeysMatching): IDateFieldBuilder -export interface IBooleanFieldBuilder extends INullableFieldBuilder { - IsTrue(): IFieldCondition; - IsFalse(): IFieldCondition; - IsFalseOrNull(): IFieldCondition; +export interface IBooleanFieldBuilder extends INullableFieldBuilder { + IsTrue(): IFieldCondition; + IsFalse(): IFieldCondition; + IsFalseOrNull(): IFieldCondition; } -export function BooleanField(field: KeysMatching): IBooleanFieldBuilder { +export function BooleanField(field: KeysMatching): IBooleanFieldBuilder { return { - ...BaseNullableField(field), - toODataValue: val => `${val}`, - IsTrue(): IFieldCondition { + ...BaseNullableField(field), + toODataValue: val => `${val === null ? null : val ? 1 : 0}`, + IsTrue(): IFieldCondition { return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(true)}` }; }, - IsFalse(): IFieldCondition { + IsFalse(): IFieldCondition { return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)}` }; }, - IsFalseOrNull(): IFieldCondition { + IsFalseOrNull(): IFieldCondition { return { toQuery: () => `(${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)} ${FilterJoinOperator.Or} ${field as string} eq ${this.toODataValue(null)})` }; } }; } +export function LookupFieldId(field: KeysMatching): INumericField { + const col: string = (field as string).endsWith("Id") ? field as string : `${field as string}Id`; + return BaseNumericField(col as any as KeysMatching); +} + + + +interface ILookupValueFieldBuilder extends INullableFieldBuilder { + Id: (Id: number) => IFieldCondition; + TextField: (Field: KeysMatching) => ITextFieldBuilder; + NumberField: (Field: KeysMatching) => INumericField; +} + +export function LookupField(field: KeysMatching): ILookupValueFieldBuilder { + return { + toQuery: () => "", + toODataValue: val => `${val}`, + IsNull: () => ({ toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(null)}` }), + Id: Id => NumberField(`${field as string}Id` as any as KeysMatching).Equals(Id), + TextField: lookupField => TextField(`${field as string}/${lookupField as string}` as any as KeysMatching), + NumberField: lookupField => NumberField(`${field as string}/${lookupField as string}` as any as KeysMatching), + + }; +} @@ -433,7 +466,7 @@ export function Or(...conditions: Array | ICond } export function And(...conditions: Array | ICondition>): ICondition { - return buildCondition(FilterJoinOperator.Or, ...conditions); + return buildCondition(FilterJoinOperator.And, ...conditions); } function buildCondition(operator: FilterJoinOperator, ...conditions: Array | ICondition>): ICondition { From 44b0795e0788d7153166975189e83b7ae4080e90 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 19 Dec 2023 21:44:53 +0100 Subject: [PATCH 13/23] =?UTF-8?q?=F0=9F=94=A8=20-=20a=20bit=20of=20lint=20?= =?UTF-8?q?cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 8102c3039..11f40b667 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -1,8 +1,6 @@ import { combine, isUrlAbsolute, isArray, objectDefinedNotNull, stringIsNullOrEmpty } from "@pnp/core"; import { IInvokable, Queryable, queryableFactory } from "@pnp/queryable"; import { spPostDelete, spPostDeleteETag } from "./operations.js"; -import { IField } from "./fields/types.js"; -import { filter } from "core-js/core/array"; export type SPInit = string | ISPQueryable | [ISPQueryable, string]; @@ -161,7 +159,7 @@ export class _SPCollection extends _SPQueryable { * * @param filter The string representing the filter query */ - public filter(filter: string | ICondition | IFieldCondition): this { + public filter(filter: string | ICondition | IFieldCondition): this { this.query.set("$filter", typeof filter === "string" ? filter : filter.toQuery()); return this; } @@ -297,7 +295,7 @@ function BaseNullableField(field: KeysMatching `'${val}'`, IsNull(): IFieldCondition { return { toQuery: () => `${field as string} eq null` }; - } + }, }; } @@ -321,7 +319,7 @@ function BaseComperableField(field: KeysMatching extends IComperableField { StartsWith(value: string): IFieldCondition; Contains(value: string): IFieldCondition; - In(...values: string[]): IFieldCondition; + In(values: string[]): IFieldCondition; } function BaseTextField(field: KeysMatching): ITextFieldBuilder { @@ -333,9 +331,9 @@ function BaseTextField(field: KeysMatching { return { toQuery: () => `${FilterOperation.SubstringOf}(${this.toODataValue(value)}, ${field as string})` }; }, - In(...values: string[]): IFieldCondition { + In(values: string[]): IFieldCondition { return Or(...values.map(v => this.Equals(v))); - } + }, }; } @@ -376,14 +374,14 @@ function BaseNumericField(field: KeysMatching): INumericFiel }, LessThanOrEquals(value: TType): IFieldCondition { return { toQuery: () => `${field as string} ${FilterOperation.LessThanOrEqualTo} ${this.toODataValue(value)}` }; - } + }, }; } export function NumberField(field: KeysMatching): INumericField { return { ...BaseNumericField(field), - toODataValue: val => `${val}` + toODataValue: val => `${val}`, }; } @@ -397,15 +395,14 @@ export function DateField(field: KeysMatching(field), toODataValue: val => `datetime'${val.toISOString()}'`, IsBetween(startDate: Date, endDate: Date): IFieldCondition { - return { toQuery: () => `(${field as string} ${FilterOperation.GreaterThanOrEqualTo} ${this.toODataValue(startDate)} ${FilterJoinOperator.And} ${field as string} ${FilterOperation.LessThan} ${this.toODataValue(endDate)})` }; + return { toQuery: () => And(DateField(field).GreaterThanOrEquals(startDate), DateField(field).LessThan(endDate)).toQuery() }; }, IsToday(): IFieldCondition { const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); return this.IsBetween(StartToday, EndToday); - } - } - + }, + }; } @@ -429,8 +426,8 @@ export function BooleanField(field: KeysMatching `${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)}` }; }, IsFalseOrNull(): IFieldCondition { - return { toQuery: () => `(${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)} ${FilterJoinOperator.Or} ${field as string} eq ${this.toODataValue(null)})` }; - } + return { toQuery: () => Or(BooleanField(field).IsNull(), BooleanField(field).IsFalse()).toQuery() }; + }, }; } @@ -453,7 +450,7 @@ export function LookupField(field: KeysMatching `${val}`, IsNull: () => ({ toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(null)}` }), Id: Id => NumberField(`${field as string}Id` as any as KeysMatching).Equals(Id), - TextField: lookupField => TextField(`${field as string}/${lookupField as string}` as any as KeysMatching), + TextField: lookupField => TextField(`${field as string}/${lookupField as string}` as any as KeysMatching), NumberField: lookupField => NumberField(`${field as string}/${lookupField as string}` as any as KeysMatching), }; @@ -461,19 +458,18 @@ export function LookupField(field: KeysMatching(...conditions: Array | ICondition>): ICondition { +export function Or(...conditions: Array | ICondition>): ICondition { return buildCondition(FilterJoinOperator.Or, ...conditions); } -export function And(...conditions: Array | ICondition>): ICondition { +export function And(...conditions: Array | ICondition>): ICondition { return buildCondition(FilterJoinOperator.And, ...conditions); } -function buildCondition(operator: FilterJoinOperator, ...conditions: Array | ICondition>): ICondition { +function buildCondition(operator: FilterJoinOperator, ...conditions: Array | ICondition>): ICondition { return { toQuery(): string { - ; return `(${conditions.map(c => c.toQuery()).join(` ${operator} `)})`; }, }; -} \ No newline at end of file +} From 0df4ec6025c0868441bb3bb3b503c9266a7c2ac3 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Tue, 19 Dec 2023 22:26:11 +0100 Subject: [PATCH 14/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Support=20for=20quick?= =?UTF-8?q?=20inline=20field=20edits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 11f40b667..090bf8e8b 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -159,7 +159,7 @@ export class _SPCollection extends _SPQueryable { * * @param filter The string representing the filter query */ - public filter(filter: string | ICondition | IFieldCondition): this { + public filter(filter: string | ICondition | IFieldCondition | INullableFieldBuilder): this { this.query.set("$filter", typeof filter === "string" ? filter : filter.toQuery()); return this; } @@ -284,14 +284,13 @@ export interface ICondition { } export interface INullableFieldBuilder { - toQuery(): string; + toQuery?(): string; toODataValue(value: TObjectType): string; IsNull(): IFieldCondition; } function BaseNullableField(field: KeysMatching): INullableFieldBuilder { return { - toQuery: () => "", toODataValue: val => `'${val}'`, IsNull(): IFieldCondition { return { toQuery: () => `${field as string} eq null` }; @@ -446,7 +445,6 @@ interface ILookupValueFieldBuilder extends INulla export function LookupField(field: KeysMatching): ILookupValueFieldBuilder { return { - toQuery: () => "", toODataValue: val => `${val}`, IsNull: () => ({ toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(null)}` }), Id: Id => NumberField(`${field as string}Id` as any as KeysMatching).Equals(Id), @@ -469,7 +467,7 @@ export function And(...conditions: Array | ICondi function buildCondition(operator: FilterJoinOperator, ...conditions: Array | ICondition>): ICondition { return { toQuery(): string { - return `(${conditions.map(c => c.toQuery()).join(` ${operator} `)})`; + return `(${conditions.filter(c => c.toQuery != null).map(c => c.toQuery()).join(` ${operator} `)})`; }, }; } From 9019579f87d6d39b691ec81b67c840ffe2d77616 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Fri, 18 Oct 2024 21:32:55 +0200 Subject: [PATCH 15/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Move=20to=20arrow=20fu?= =?UTF-8?q?nction=20syntax=20in=20filters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 398 ++++++++++++++++++++++--------------- 1 file changed, 243 insertions(+), 155 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 090bf8e8b..12af1d675 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -159,8 +159,16 @@ export class _SPCollection extends _SPQueryable { * * @param filter The string representing the filter query */ - public filter(filter: string | ICondition | IFieldCondition | INullableFieldBuilder): this { - this.query.set("$filter", typeof filter === "string" ? filter : filter.toQuery()); + public filter(filter: string | ComparisonResult | ((f: QueryableGroups) => ComparisonResult)): this { + if (typeof filter === "object") { + this.query.set("$filter", filter.ToString()); + return this; + } + if (typeof filter === "function") { + this.query.set("$filter", filter(SPOData.Where()).ToString()); + return this; + } + this.query.set("$filter", filter.toString()); return this; } @@ -265,209 +273,289 @@ enum FilterOperation { LessThan = "lt", LessThanOrEqualTo = "le", StartsWith = "startswith", - SubstringOf = "substringof", - In = "in" + SubstringOf = "substringof" } enum FilterJoinOperator { And = "and", - Or = "or" + AndWithSpace = " and ", + Or = "or", + OrWithSpace = " or " } - -export interface IFieldCondition { - toQuery(): string; +export class SPOData { + static Where() { + return new QueryableGroups(); + } } -export interface ICondition { - toQuery(): string; -} +class BaseQuery { + protected query: string[] = []; -export interface INullableFieldBuilder { - toQuery?(): string; - toODataValue(value: TObjectType): string; - IsNull(): IFieldCondition; + constructor(query: string[]) { + this.query = query; + } } -function BaseNullableField(field: KeysMatching): INullableFieldBuilder { - return { - toODataValue: val => `'${val}'`, - IsNull(): IFieldCondition { - return { toQuery: () => `${field as string} eq null` }; - }, - }; -} -interface IComperableField extends INullableFieldBuilder { - Equals(value: TObjectType): IFieldCondition; - NotEquals(value: TObjectType): IFieldCondition; -} +class QueryableFields extends BaseQuery { + constructor(q: string[]) { + super(q); + } -function BaseComperableField(field: KeysMatching): IComperableField { - return { - ...BaseNullableField(field), - Equals(value: TType): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(value)}` }; - }, - NotEquals(value: TType): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.NotEquals} ${this.toODataValue(value)}` }; - }, - }; -} + public TextField(InternalName: KeysMatching): TextField { + return new TextField([...this.query, (InternalName as string)]); + } -export interface ITextFieldBuilder extends IComperableField { - StartsWith(value: string): IFieldCondition; - Contains(value: string): IFieldCondition; - In(values: string[]): IFieldCondition; -} + public ChoiceField(InternalName: KeysMatching): TextField { + return new TextField([...this.query, (InternalName as string)]); + } -function BaseTextField(field: KeysMatching): ITextFieldBuilder { - return { - ...BaseComperableField(field), - StartsWith(value: string): IFieldCondition { - return { toQuery: () => `${FilterOperation.StartsWith}(${field as string}, ${this.toODataValue(value)})` }; - }, - Contains(value: string): IFieldCondition { - return { toQuery: () => `${FilterOperation.SubstringOf}(${this.toODataValue(value)}, ${field as string})` }; - }, - In(values: string[]): IFieldCondition { - return Or(...values.map(v => this.Equals(v))); - }, - }; -} + public MultiChoiceField(InternalName: KeysMatching): TextField { + return new TextField([...this.query, (InternalName as string)]); + } -export function TextField(field: KeysMatching): ITextFieldBuilder { - return BaseTextField(field); -} + public NumberField(InternalName: KeysMatching): NumberField { + return new NumberField([...this.query, (InternalName as string)]); + } -export function ChoiceField(field: KeysMatching): ITextFieldBuilder { - return BaseTextField(field); -} + public DateField(InternalName: KeysMatching): DateField { + return new DateField([...this.query, (InternalName as string)]); + } + + public BooleanField(InternalName: KeysMatching): BooleanField { + return new BooleanField([...this.query, (InternalName as string)]); + } -export function MultiChoiceField(field: KeysMatching): ITextFieldBuilder { - return BaseTextField(field); + public LookupField>(InternalName: TKey): LookupQueryableFields { + return new LookupQueryableFields([...this.query], InternalName as string); + } + + public LookupIdField>(InternalName: TKey): NumberField { + const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; + return new NumberField([...this.query, col]); + } } +class LookupQueryableFields extends BaseQuery { + private LookupField: string; + constructor(q: string[], LookupField: string) { + super(q); + this.LookupField = LookupField; + } + public Id(Id: number): ComparisonResult { + return new ComparisonResult([...this.query, `${this.LookupField}/Id`, FilterOperation.Equals, Id.toString()]); + } -interface INumericField extends INullableFieldBuilder { - Equals(value: TType): IFieldCondition; - GreaterThan(value: TType): IFieldCondition; - GreaterThanOrEquals(value: TType): IFieldCondition; - LessThan(value: TType): IFieldCondition; - LessThanOrEquals(value: TType): IFieldCondition; -} + public TextField(InternalName: KeysMatching): TextField { + return new TextField([...this.query, `${this.LookupField}/${InternalName as string}`]); + } + public NumberField(InternalName: KeysMatching): NumberField { + return new NumberField([...this.query, `${this.LookupField}/${InternalName as string}`]); + } -function BaseNumericField(field: KeysMatching): INumericField { - return { - ...BaseComperableField(field), - GreaterThan(value: TType): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.GreaterThan} ${this.toODataValue(value)}` }; - }, - GreaterThanOrEquals(value: TType): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.GreaterThanOrEqualTo} ${this.toODataValue(value)}` }; - }, - LessThan(value: TType): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.LessThan} ${this.toODataValue(value)}` }; - }, - LessThanOrEquals(value: TType): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.LessThanOrEqualTo} ${this.toODataValue(value)}` }; - }, - }; + // Support has been announced, but is not yet available in SharePoint Online + // https://www.microsoft.com/en-ww/microsoft-365/roadmap?filters=&searchterms=100503 + // public BooleanField(InternalName: KeysMatching): BooleanField { + // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); + // } } -export function NumberField(field: KeysMatching): INumericField { - return { - ...BaseNumericField(field), - toODataValue: val => `${val}`, - }; -} +class QueryableGroups extends QueryableFields { + constructor() { + super([]); + } -export interface IDateFieldBuilder extends INumericField { - IsToday(): IFieldCondition; - IsBetween(start: Date, end: Date): IFieldCondition; -} + public All(queries: ComparisonResult[] | ((f: QueryableGroups) => ComparisonResult)[]): ComparisonResult { + let query: ComparisonResult[] = []; -export function DateField(field: KeysMatching): IDateFieldBuilder { - return { - ...BaseNumericField(field), - toODataValue: val => `datetime'${val.toISOString()}'`, - IsBetween(startDate: Date, endDate: Date): IFieldCondition { - return { toQuery: () => And(DateField(field).GreaterThanOrEquals(startDate), DateField(field).LessThan(endDate)).toQuery() }; - }, - IsToday(): IFieldCondition { - const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); - const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); - return this.IsBetween(StartToday, EndToday); - }, - }; + for (const q of queries) { + if (typeof q === "function") { + query.push(q(SPOData.Where())); + } else { + query.push(q); + } + } + return new ComparisonResult([`(${query.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`]); + } + + public Some(queries: ComparisonResult[] | ((f: QueryableGroups) => ComparisonResult)[]): ComparisonResult { + let query: ComparisonResult[] = []; + + for (const q of queries) { + if (typeof q === "function") { + query.push(q(SPOData.Where())); + } else { + query.push(q); + } + } + return new ComparisonResult([`(${query.map(x => x.ToString()).join(FilterJoinOperator.OrWithSpace)})`]); + } } -export interface IBooleanFieldBuilder extends INullableFieldBuilder { - IsTrue(): IFieldCondition; - IsFalse(): IFieldCondition; - IsFalseOrNull(): IFieldCondition; -} +class NullableField extends BaseQuery { + protected LastIndex: number; + protected InternalName: string; -export function BooleanField(field: KeysMatching): IBooleanFieldBuilder { - return { - ...BaseNullableField(field), - toODataValue: val => `${val === null ? null : val ? 1 : 0}`, - IsTrue(): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(true)}` }; - }, - IsFalse(): IFieldCondition { - return { toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(false)}` }; - }, - IsFalseOrNull(): IFieldCondition { - return { toQuery: () => Or(BooleanField(field).IsNull(), BooleanField(field).IsFalse()).toQuery() }; - }, - }; + constructor(q: string[]) { + super(q); + this.LastIndex = q.length - 1 + this.InternalName = q[this.LastIndex]; + } + + protected ToODataValue(value: TInputValueType): string { + return `'${value}'`; + } + + public IsNull(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, "null"]); + } + + public IsNotNull(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.NotEquals, "null"]); + } } -export function LookupFieldId(field: KeysMatching): INumericField { - const col: string = (field as string).endsWith("Id") ? field as string : `${field as string}Id`; - return BaseNumericField(col as any as KeysMatching); +class ComparableField extends NullableField { + constructor(q: string[]) { + super(q); + } + + public EqualTo(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(value)]); + } + + public NotEqualTo(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.NotEquals, this.ToODataValue(value)]); + } + + public In(values: TInputValueType[]): ComparisonResult { + return SPOData.Where().Some(values.map(x => this.EqualTo(x))); + } } +class TextField extends ComparableField { + constructor(q: string[]) { + super(q); + } + public StartsWith(value: string): ComparisonResult { + const filter = `${FilterOperation.StartsWith}(${this.InternalName}, ${this.ToODataValue(value)})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } -interface ILookupValueFieldBuilder extends INullableFieldBuilder { - Id: (Id: number) => IFieldCondition; - TextField: (Field: KeysMatching) => ITextFieldBuilder; - NumberField: (Field: KeysMatching) => INumericField; + public Contains(value: string): ComparisonResult { + const filter = `${FilterOperation.SubstringOf}(${this.ToODataValue(value)}, ${this.InternalName})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } } -export function LookupField(field: KeysMatching): ILookupValueFieldBuilder { - return { - toODataValue: val => `${val}`, - IsNull: () => ({ toQuery: () => `${field as string} ${FilterOperation.Equals} ${this.toODataValue(null)}` }), - Id: Id => NumberField(`${field as string}Id` as any as KeysMatching).Equals(Id), - TextField: lookupField => TextField(`${field as string}/${lookupField as string}` as any as KeysMatching), - NumberField: lookupField => NumberField(`${field as string}/${lookupField as string}` as any as KeysMatching), +class BooleanField extends NullableField { + constructor(q: string[]) { + super(q); + } - }; + protected override ToODataValue(value: boolean | null): string { + return `${value == null ? "null" : value ? 1 : 0}`; + } + + public IsTrue(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(true)]); + } + + public IsFalse(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, this.ToODataValue(false)]); + } + + public IsFalseOrNull(): ComparisonResult { + const filter = `(${[this.InternalName, FilterOperation.Equals, this.ToODataValue(null), FilterJoinOperator.Or, this.InternalName, FilterOperation.Equals, this.ToODataValue(false)].join(" ")})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } } +class NumericField extends ComparableField { + constructor(q: string[]) { + super(q); + } + + public GreaterThan(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.GreaterThan, this.ToODataValue(value)]); + } + + public GreaterThanOrEqualTo(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.ToODataValue(value)]); + } + public LessThan(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.LessThan, this.ToODataValue(value)]); + } -export function Or(...conditions: Array | ICondition>): ICondition { - return buildCondition(FilterJoinOperator.Or, ...conditions); + public LessThanOrEqualTo(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.ToODataValue(value)]); + } } -export function And(...conditions: Array | ICondition>): ICondition { - return buildCondition(FilterJoinOperator.And, ...conditions); + +class NumberField extends NumericField { + constructor(q: string[]) { + super(q); + } + + protected override ToODataValue(value: number): string { + return `${value}`; + } } -function buildCondition(operator: FilterJoinOperator, ...conditions: Array | ICondition>): ICondition { - return { - toQuery(): string { - return `(${conditions.filter(c => c.toQuery != null).map(c => c.toQuery()).join(` ${operator} `)})`; - }, - }; +class DateField extends NumericField { + constructor(q: string[]) { + super(q); + } + + protected override ToODataValue(value: Date): string { + return `'${value.toISOString()}'` + } + + public IsBetween(startDate: Date, endDate: Date): ComparisonResult { + const filter = `(${[this.InternalName, FilterOperation.GreaterThan, this.ToODataValue(startDate), FilterJoinOperator.And, this.InternalName, FilterOperation.LessThan, this.ToODataValue(endDate)].join(" ")})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } + + public IsToday(): ComparisonResult { + const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); + const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); + return this.IsBetween(StartToday, EndToday); + } } + + + + + + +class ComparisonResult extends BaseQuery { + constructor(q: string[]) { + super(q); + } + + public Or(): QueryableFields { + return new QueryableFields([...this.query, FilterJoinOperator.Or]); + } + + public And(): QueryableFields { + return new QueryableFields([...this.query, FilterJoinOperator.And]); + } + + public ToString(): string { + return this.query.join(" "); + } +} \ No newline at end of file From 55822719c6270297b473884bbe1ac102d5d0e4f5 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Fri, 18 Oct 2024 22:28:33 +0200 Subject: [PATCH 16/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Linting=20|=20fixed=20?= =?UTF-8?q?Dates=20being=20suggested=20as=20internal=20name=20in=20lookup?= =?UTF-8?q?=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 46 ++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 12af1d675..3390f1421 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -263,7 +263,8 @@ export interface IDeleteableWithETag { -type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T]; +type KeysMatching = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]; +type KeysMatchingObjects = { [K in keyof T]: T[K] extends object ? (T[K] extends Date ? never : K) : never }[keyof T]; enum FilterOperation { Equals = "eq", @@ -284,11 +285,12 @@ enum FilterJoinOperator { } export class SPOData { - static Where() { + public static Where() { return new QueryableGroups(); } } +// Linting complains that TBaseInterface is unused, but without it all the intellisense is lost since it's carrying it through the chain class BaseQuery { protected query: string[] = []; @@ -327,14 +329,18 @@ class QueryableFields extends BaseQuery { return new BooleanField([...this.query, (InternalName as string)]); } - public LookupField>(InternalName: TKey): LookupQueryableFields { - return new LookupQueryableFields([...this.query], InternalName as string); - } + // public LookupField>>(InternalName: TKey): LookupQueryableFields { + // return new LookupQueryableFields([...this.query], InternalName as string); + // } public LookupIdField>(InternalName: TKey): NumberField { const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; return new NumberField([...this.query, col]); } + + public LookupField>(InternalName: TKey): LookupQueryableFields { + return new LookupQueryableFields([...this.query], InternalName as string); + } } class LookupQueryableFields extends BaseQuery { @@ -369,7 +375,7 @@ class QueryableGroups extends QueryableFields { } public All(queries: ComparisonResult[] | ((f: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - let query: ComparisonResult[] = []; + const query: ComparisonResult[] = []; for (const q of queries) { if (typeof q === "function") { @@ -382,7 +388,7 @@ class QueryableGroups extends QueryableFields { } public Some(queries: ComparisonResult[] | ((f: QueryableGroups) => ComparisonResult)[]): ComparisonResult { - let query: ComparisonResult[] = []; + const query: ComparisonResult[] = []; for (const q of queries) { if (typeof q === "function") { @@ -405,7 +411,7 @@ class NullableField extends BaseQuery extends NullableField { - const filter = `(${[this.InternalName, FilterOperation.Equals, this.ToODataValue(null), FilterJoinOperator.Or, this.InternalName, FilterOperation.Equals, this.ToODataValue(false)].join(" ")})`; + const filter = `(${[ + this.InternalName, + FilterOperation.Equals, + this.ToODataValue(null), + FilterJoinOperator.Or, + this.InternalName, + FilterOperation.Equals, + this.ToODataValue(false), + ].join(" ")})`; this.query[this.LastIndex] = filter; return new ComparisonResult([...this.query]); } @@ -521,11 +535,19 @@ class DateField extends NumericField { } protected override ToODataValue(value: Date): string { - return `'${value.toISOString()}'` + return `'${value.toISOString()}'`; } public IsBetween(startDate: Date, endDate: Date): ComparisonResult { - const filter = `(${[this.InternalName, FilterOperation.GreaterThan, this.ToODataValue(startDate), FilterJoinOperator.And, this.InternalName, FilterOperation.LessThan, this.ToODataValue(endDate)].join(" ")})`; + const filter = `(${[ + this.InternalName, + FilterOperation.GreaterThan, + this.ToODataValue(startDate), + FilterJoinOperator.And, + this.InternalName, + FilterOperation.LessThan, + this.ToODataValue(endDate), + ].join(" ")})`; this.query[this.LastIndex] = filter; return new ComparisonResult([...this.query]); } @@ -558,4 +580,4 @@ class ComparisonResult extends BaseQuery { public ToString(): string { return this.query.join(" "); } -} \ No newline at end of file +} From 5d7ea44c3a889846e78c278994d8244ffef80600 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Fri, 18 Oct 2024 22:36:30 +0200 Subject: [PATCH 17/23] =?UTF-8?q?=F0=9F=94=A8=20Slightly=20more=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit down to one warning which I can't resolve --- packages/sp/spqueryable.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 3390f1421..025f4bc37 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -329,18 +329,14 @@ class QueryableFields extends BaseQuery { return new BooleanField([...this.query, (InternalName as string)]); } - // public LookupField>>(InternalName: TKey): LookupQueryableFields { - // return new LookupQueryableFields([...this.query], InternalName as string); - // } + public LookupField>(InternalName: TKey): LookupQueryableFields { + return new LookupQueryableFields([...this.query], InternalName as string); + } public LookupIdField>(InternalName: TKey): NumberField { const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; return new NumberField([...this.query, col]); } - - public LookupField>(InternalName: TKey): LookupQueryableFields { - return new LookupQueryableFields([...this.query], InternalName as string); - } } class LookupQueryableFields extends BaseQuery { @@ -364,8 +360,8 @@ class LookupQueryableFields extends BaseQuery): BooleanField { - // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); + // public BooleanField(InternalName: KeysMatching): BooleanField { + // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); // } } From f92c6ba6cee190b3a94ba5b37ded9fdfac8671bc Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Sat, 19 Oct 2024 00:02:41 +0200 Subject: [PATCH 18/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Allowed=20for=20mixed?= =?UTF-8?q?=20queries=20in=20.some=20and=20.all=20combiners?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 025f4bc37..50da723b0 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -370,7 +370,7 @@ class QueryableGroups extends QueryableFields { super([]); } - public All(queries: ComparisonResult[] | ((f: QueryableGroups) => ComparisonResult)[]): ComparisonResult { + public All(queries: (ComparisonResult | ((f: QueryableGroups) => ComparisonResult))[]): ComparisonResult { const query: ComparisonResult[] = []; for (const q of queries) { @@ -383,7 +383,7 @@ class QueryableGroups extends QueryableFields { return new ComparisonResult([`(${query.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`]); } - public Some(queries: ComparisonResult[] | ((f: QueryableGroups) => ComparisonResult)[]): ComparisonResult { + public Some(queries: (ComparisonResult | ((f: QueryableGroups) => ComparisonResult))[]): ComparisonResult { const query: ComparisonResult[] = []; for (const q of queries) { From 5be7693d176ba09ed8d22d7bf0b12df3b08970a5 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Sat, 19 Oct 2024 16:55:31 +0200 Subject: [PATCH 19/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Move=20around=20the=20?= =?UTF-8?q?code=20to=20allow=20.All=20and=20.Some,=20not=20only=20on=20"to?= =?UTF-8?q?p=20level"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 66 ++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 50da723b0..023178d6e 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -159,7 +159,7 @@ export class _SPCollection extends _SPQueryable { * * @param filter The string representing the filter query */ - public filter(filter: string | ComparisonResult | ((f: QueryableGroups) => ComparisonResult)): this { + public filter(filter: string | ComparisonResult | ((f: QueryableFields) => ComparisonResult)): this { if (typeof filter === "object") { this.query.set("$filter", filter.ToString()); return this; @@ -286,7 +286,7 @@ enum FilterJoinOperator { export class SPOData { public static Where() { - return new QueryableGroups(); + return new QueryableFields([]); } } @@ -337,40 +337,8 @@ class QueryableFields extends BaseQuery { const col: string = (InternalName as string).endsWith("Id") ? InternalName as string : `${InternalName as string}Id`; return new NumberField([...this.query, col]); } -} - -class LookupQueryableFields extends BaseQuery { - private LookupField: string; - constructor(q: string[], LookupField: string) { - super(q); - this.LookupField = LookupField; - } - - public Id(Id: number): ComparisonResult { - return new ComparisonResult([...this.query, `${this.LookupField}/Id`, FilterOperation.Equals, Id.toString()]); - } - - public TextField(InternalName: KeysMatching): TextField { - return new TextField([...this.query, `${this.LookupField}/${InternalName as string}`]); - } - - public NumberField(InternalName: KeysMatching): NumberField { - return new NumberField([...this.query, `${this.LookupField}/${InternalName as string}`]); - } - // Support has been announced, but is not yet available in SharePoint Online - // https://www.microsoft.com/en-ww/microsoft-365/roadmap?filters=&searchterms=100503 - // public BooleanField(InternalName: KeysMatching): BooleanField { - // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); - // } -} - -class QueryableGroups extends QueryableFields { - constructor() { - super([]); - } - - public All(queries: (ComparisonResult | ((f: QueryableGroups) => ComparisonResult))[]): ComparisonResult { + public All(queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { const query: ComparisonResult[] = []; for (const q of queries) { @@ -380,10 +348,10 @@ class QueryableGroups extends QueryableFields { query.push(q); } } - return new ComparisonResult([`(${query.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`]); + return new ComparisonResult([...this.query, `(${query.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`]); } - public Some(queries: (ComparisonResult | ((f: QueryableGroups) => ComparisonResult))[]): ComparisonResult { + public Some(queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { const query: ComparisonResult[] = []; for (const q of queries) { @@ -393,13 +361,35 @@ class QueryableGroups extends QueryableFields { query.push(q); } } - return new ComparisonResult([`(${query.map(x => x.ToString()).join(FilterJoinOperator.OrWithSpace)})`]); + return new ComparisonResult([...this.query, `(${query.map(x => x.ToString()).join(FilterJoinOperator.OrWithSpace)})`]); } } +class LookupQueryableFields extends BaseQuery { + private LookupField: string; + constructor(q: string[], LookupField: string) { + super(q); + this.LookupField = LookupField; + } + public Id(Id: number): ComparisonResult { + return new ComparisonResult([...this.query, `${this.LookupField}/Id`, FilterOperation.Equals, Id.toString()]); + } + public TextField(InternalName: KeysMatching): TextField { + return new TextField([...this.query, `${this.LookupField}/${InternalName as string}`]); + } + + public NumberField(InternalName: KeysMatching): NumberField { + return new NumberField([...this.query, `${this.LookupField}/${InternalName as string}`]); + } + // Support has been announced, but is not yet available in SharePoint Online + // https://www.microsoft.com/en-ww/microsoft-365/roadmap?filters=&searchterms=100503 + // public BooleanField(InternalName: KeysMatching): BooleanField { + // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); + // } +} class NullableField extends BaseQuery { protected LastIndex: number; From f1c3663e524482e4a14ca1f357ea7be504d8ab08 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Sat, 19 Oct 2024 17:18:19 +0200 Subject: [PATCH 20/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Change=20from=20taking?= =?UTF-8?q?=20an=20array=20of=20queries=20to=20using=20spread=20operator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sp/spqueryable.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 023178d6e..49494ce1b 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -338,7 +338,7 @@ class QueryableFields extends BaseQuery { return new NumberField([...this.query, col]); } - public All(queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { + public All(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { const query: ComparisonResult[] = []; for (const q of queries) { @@ -351,7 +351,7 @@ class QueryableFields extends BaseQuery { return new ComparisonResult([...this.query, `(${query.map(x => x.ToString()).join(FilterJoinOperator.AndWithSpace)})`]); } - public Some(queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { + public Some(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { const query: ComparisonResult[] = []; for (const q of queries) { @@ -428,7 +428,7 @@ class ComparableField extends NullableField { - return SPOData.Where().Some(values.map(x => this.EqualTo(x))); + return SPOData.Where().Some(...values.map(x => this.EqualTo(x))); } } From c77784414928642d5590e46de496c90573643c42 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Sun, 20 Oct 2024 16:34:25 +0200 Subject: [PATCH 21/23] =?UTF-8?q?=F0=9F=94=A8=20-=20Changed=20based=20on?= =?UTF-8?q?=20feedback=20in=20initial=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We like "method 1", seems simpler Drop "Field" from all the names so Lookup() vs LookupField(). switch to camel case so Lookup becomes lookup() could simplify the names, equal instead of EqualsTo and notEqual vs NotEqualTo Swap and/or for All/Some, just seems clearer to someone showing up with no knowledge of the filter methods pass in multiple objects you can use ...[] to gather them into an array within the method. this should be able to support all the filter cases, another reason to drop the "Field" name from the builder functions. Like for a site if you give it ISiteInfo you could filter on Text("Title").EqualsTo("blah"); --- packages/sp/spqueryable.ts | 305 ++++++++++++++++++++++++++++++++++++- 1 file changed, 303 insertions(+), 2 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 0eaa0669a..f63d3ff03 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -141,8 +141,16 @@ export class _SPCollection extends _SPQueryable { * * @param filter The string representing the filter query */ - public filter(filter: string): this { - this.query.set("$filter", filter); + public filter>(filter: string | ComparisonResult | ((f: InitialFieldQuery) => ComparisonResult)): this { + if (typeof filter === "object") { + this.query.set("$filter", filter.toString()); + return this; + } + if (typeof filter === "function") { + this.query.set("$filter", filter(SPOData.Where()).toString()); + return this; + } + this.query.set("$filter", filter.toString()); return this; } @@ -254,3 +262,296 @@ export const spPostDeleteETag = (o: ISPQueryable, init?: RequestIn export const spDelete = (o: ISPQueryable, init?: RequestInit): Promise => op(o, del, init); export const spPatch = (o: ISPQueryable, init?: RequestInit): Promise => op(o, patch, init); + + + +type KeysMatching = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]; +type KeysMatchingObjects = { [K in keyof T]: T[K] extends object ? (T[K] extends Date ? never : K) : never }[keyof T]; +type UnwrapArray = T extends (infer U)[] ? U : T; + +enum FilterOperation { + Equals = "eq", + NotEquals = "ne", + GreaterThan = "gt", + GreaterThanOrEqualTo = "ge", + LessThan = "lt", + LessThanOrEqualTo = "le", + StartsWith = "startswith", + SubstringOf = "substringof" +} + +enum FilterJoinOperator { + And = "and", + AndWithSpace = " and ", + Or = "or", + OrWithSpace = " or " +} + +class SPOData { + public static Where() { + return new InitialFieldQuery([]); + } +} + +// Linting complains that TBaseInterface is unused, but without it all the intellisense is lost since it's carrying it through the chain +class BaseQuery { + protected query: string[] = []; + + constructor(query: string[]) { + this.query = query; + } +} + + +class QueryableFields extends BaseQuery { + constructor(q: string[]) { + super(q); + } + + public text(internalName: KeysMatching): TextField { + return new TextField([...this.query, (internalName as string)]); + } + + public choice(internalName: KeysMatching): TextField { + return new TextField([...this.query, (internalName as string)]); + } + + public multiChoice(internalName: KeysMatching): TextField { + return new TextField([...this.query, (internalName as string)]); + } + + public number(internalName: KeysMatching): NumberField { + return new NumberField([...this.query, (internalName as string)]); + } + + public date(internalName: KeysMatching): DateField { + return new DateField([...this.query, (internalName as string)]); + } + + public boolean(internalName: KeysMatching): BooleanField { + return new BooleanField([...this.query, (internalName as string)]); + } + + public lookup>(internalName: TKey): LookupQueryableFields { + return new LookupQueryableFields([...this.query], internalName as string); + } + + public lookupId>(internalName: TKey): NumberField { + const col: string = (internalName as string).endsWith("Id") ? internalName as string : `${internalName as string}Id`; + return new NumberField([...this.query, col]); + } +} + +class QueryableAndResult extends QueryableFields { + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { + return new ComparisonResult([...this.query, `(${queries.map(x => x.toString()).join(FilterJoinOperator.OrWithSpace)})`]); + } +} + +class QueryableOrResult extends QueryableFields { + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult { + return new ComparisonResult([...this.query, `(${queries.map(x => x.toString()).join(FilterJoinOperator.AndWithSpace)})`]); + } +} + +class InitialFieldQuery extends QueryableFields { + public or(): QueryableFields; + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult; + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableFields) { + if (queries == null || queries.length == 0) + return new QueryableFields([...this.query, FilterJoinOperator.Or]); + return new ComparisonResult([...this.query, `(${queries.map(x => x.toString()).join(FilterJoinOperator.OrWithSpace)})`]); + } + + public and(): QueryableFields; + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableFields) { + if (queries == null || queries.length == 0) + return new QueryableFields([...this.query, FilterJoinOperator.And]); + return new ComparisonResult([...this.query, `(${queries.map(x => x.toString()).join(FilterJoinOperator.AndWithSpace)})`]); + } +} + + + +class LookupQueryableFields extends BaseQuery { + private LookupField: string; + constructor(q: string[], LookupField: string) { + super(q); + this.LookupField = LookupField; + } + + public Id(id: number): ComparisonResult { + return new ComparisonResult([...this.query, `${this.LookupField}/Id`, FilterOperation.Equals, id.toString()]); + } + + public text(internalName: KeysMatching): TextField { + return new TextField([...this.query, `${this.LookupField}/${internalName as string}`]); + } + + public number(internalName: KeysMatching): NumberField { + return new NumberField([...this.query, `${this.LookupField}/${internalName as string}`]); + } + + // Support has been announced, but is not yet available in SharePoint Online + // https://www.microsoft.com/en-ww/microsoft-365/roadmap?filters=&searchterms=100503 + // public boolean(InternalName: KeysMatching): BooleanField { + // return new BooleanField([...this.query, `${this.LookupField}/${InternalName as string}`]); + // } +} + +class NullableField extends BaseQuery { + protected LastIndex: number; + protected InternalName: string; + + constructor(q: string[]) { + super(q); + this.LastIndex = q.length - 1; + this.InternalName = q[this.LastIndex]; + } + + protected toODataValue(value: TInputValueType): string { + return `'${value}'`; + } + + public isNull(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, "null"]); + } + + public isNotNull(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.NotEquals, "null"]); + } +} + +class ComparableField extends NullableField { + public equal(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, this.toODataValue(value)]); + } + + public notEqual(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.NotEquals, this.toODataValue(value)]); + } + + public in(...values: TInputValueType[]): ComparisonResult { + return SPOData.Where().or(...values.map(x => this.equal(x))); + } + + public notIn(...values: TInputValueType[]): ComparisonResult { + return SPOData.Where().and(...values.map(x => this.notEqual(x))); + } +} + +class TextField extends ComparableField { + public startsWith(value: string): ComparisonResult { + const filter = `${FilterOperation.StartsWith}(${this.InternalName}, ${this.toODataValue(value)})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } + + public contains(value: string): ComparisonResult { + const filter = `${FilterOperation.SubstringOf}(${this.toODataValue(value)}, ${this.InternalName})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } +} + +class BooleanField extends NullableField { + protected override toODataValue(value: boolean | null): string { + return `${value == null ? "null" : value ? 1 : 0}`; + } + + public isTrue(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, this.toODataValue(true)]); + } + + public isFalse(): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.Equals, this.toODataValue(false)]); + } + + public isFalseOrNull(): ComparisonResult { + const filter = `(${[ + this.InternalName, + FilterOperation.Equals, + this.toODataValue(null), + FilterJoinOperator.Or, + this.InternalName, + FilterOperation.Equals, + this.toODataValue(false), + ].join(" ")})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } +} + +class NumericField extends ComparableField { + public greaterThan(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.GreaterThan, this.toODataValue(value)]); + } + + public greaterThanOrEqual(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.toODataValue(value)]); + } + + public lessThan(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.LessThan, this.toODataValue(value)]); + } + + public lessThanOrEqual(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.toODataValue(value)]); + } +} + + +class NumberField extends NumericField { + protected override toODataValue(value: number): string { + return `${value}`; + } +} + +class DateField extends NumericField { + protected override toODataValue(value: Date): string { + return `'${value.toISOString()}'`; + } + + public isBetween(startDate: Date, endDate: Date): ComparisonResult { + const filter = `(${[ + this.InternalName, + FilterOperation.GreaterThan, + this.toODataValue(startDate), + FilterJoinOperator.And, + this.InternalName, + FilterOperation.LessThan, + this.toODataValue(endDate), + ].join(" ")})`; + this.query[this.LastIndex] = filter; + return new ComparisonResult([...this.query]); + } + + public isToday(): ComparisonResult { + const StartToday = new Date(); StartToday.setHours(0, 0, 0, 0); + const EndToday = new Date(); EndToday.setHours(23, 59, 59, 999); + return this.isBetween(StartToday, EndToday); + } +} + +class ComparisonResult extends BaseQuery { + public and(): QueryableAndResult; + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableAndResult) { + if (queries == null || queries.length == 0) + return new QueryableAndResult([...this.query, FilterJoinOperator.And]); + return new ComparisonResult([...this.query, FilterJoinOperator.And, `(${queries.map(x => x.toString()).join(FilterJoinOperator.AndWithSpace)})`]); + } + + public or(): QueryableOrResult; + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult; + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableOrResult) { + if (queries == null || queries.length == 0) + return new QueryableOrResult([...this.query, FilterJoinOperator.Or]); + return new ComparisonResult([...this.query, FilterJoinOperator.Or, `(${queries.map(x => x.toString()).join(FilterJoinOperator.OrWithSpace)})`]); + } + + public toString(): string { + return this.query.join(" "); + } +} \ No newline at end of file From 76dca8709583f6b5938f28b8140181c15e9b452f Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Thu, 24 Oct 2024 00:53:45 +0200 Subject: [PATCH 22/23] =?UTF-8?q?=F0=9F=94=A8-=20First=20draft=20of=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sp/items.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/sp/webs.md | 15 ++++++-- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/docs/sp/items.md b/docs/sp/items.md index b0784e5da..6eea372b8 100644 --- a/docs/sp/items.md +++ b/docs/sp/items.md @@ -113,6 +113,89 @@ const r = await sp.web.lists.getByTitle("TaxonomyList").getItemsByCAMLQuery({ }); ``` +### Filter using fluent filter + +>Note: This feature is currently in preview and may not work as expected. + +PnPjs supports a fluent filter for all OData endpoints, including the items endpoint. this allows you to write a strongly fluent filter that will be parsed into an OData filter. + +```TypeScript +import { spfi } from "@pnp/sp"; +import "@pnp/sp/webs"; +import "@pnp/sp/lists"; + +const sp = spfi(...); + +const r = await sp.web.lists.filter(l => l.number("ItemCount").greaterThan(5000))(); +``` + +The following field types are supported in the fluent filter: + +- Text +- Choice +- MultiChoice +- Number +- Date +- Boolean +- Lookup +- LookupId + +The following operations are supported in the fluent filter: + +| Field Type | Operators/Values | +| -------------------- | ------------------------------------------------------------------------------------------ | +| All field types | `equal`, `notEqual`, `in`, `notIn` | +| Text & choice fields | `startsWith`, `contains` | +| Numeric fields | `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual` | +| Date fields | `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `isBetween`, `isToday` | +| Boolean fields | `isTrue`, `isFalse`, `isFalseOrNull` | +| Lookup | `id`, Text, Number | + +#### Complex Filter + +For all the regular endpoints, the fluent filter will infer the type automatically, but for the list items filter, you'll need to provide your own types to make the parser work. + +You can use the `and` and `or` operators to create complex filters that nest different grouping. + +```TypeScript +import { spfi } from "@pnp/sp"; +import "@pnp/sp/webs"; +import "@pnp/sp/lists"; +import "@pnp/sp/items"; + +const sp = spfi(...); + +interface ListItem extends IListItem { + FirstName: string; + LastName: string; + Age: number; + Manager: IListItem; + StartDate: Date; +} + + +// Get all employees named John +const r = await sp.web.lists.getByTitle("ListName").items.filter(f => f.text("FirstName").equal("John"))(); + +// Get all employees not named John who are over 30 +const r1 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.text("FirstName").notEqual("John").and().number("Age").greaterThan(30))(); + +// Get all employees that are named John Doe or Jane Doe +const r2 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.or( + f.and( + f.text("FirstName").equal("John"), + f.text("LastName").equal("Doe") + ), + f.and( + f.text("FirstName").equal("Jane"), + f.text("LastName").equal("Doe") + ) +))(); + +// Get all employees who are managed by John and start today +const r3 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.lookup("Manager").text("FirstName").equal("John").and().date("StartDate").isToday())(); +``` + ### Retrieving PublishingPageImage The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in [this thread](https://github.com/SharePoint/PnP-JS-Core/issues/178). Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. @@ -326,6 +409,8 @@ const sp = spfi(...); // you are getting back a collection here const items: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'")(); +// Using fluent filter +const items1: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter(f => f.text("Title").equal("A Title"))(); // see if we got something if (items.length > 0) { @@ -425,6 +510,9 @@ const sp = spfi(...); // first we need to get the hidden field's internal name. // The Title of that hidden field is, in my case and in the linked article just the visible field name with "_0" appended. const fields = await sp.web.lists.getByTitle("TestList").fields.filter("Title eq 'MultiMetaData_0'").select("Title", "InternalName")(); +// Using fluent filter +const fields1 = await sp.web.lists.getByTitle("TestList").fields.filter(f => f.text("Title").equal("MultiMetaData_0")).select("Title", "InternalName")(); + // get an item to update, here we just create one for testing const newItem = await sp.web.lists.getByTitle("TestList").items.add({ Title: "Testing", @@ -593,6 +681,15 @@ const response = .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) (); +// Using fluent filter +const response1 = + await sp.web.lists + .getByTitle('[Lists_Title]') + .fields + .select('Title, EntityPropertyName') + .filter(l => l.boolean("Hidden").isFalse().and().text("Title").equal("[Field's_Display_Name]")) + (); + console.log(response.map(field => { return { Title: field.Title, diff --git a/docs/sp/webs.md b/docs/sp/webs.md index c9e6e1b40..54968c7a9 100644 --- a/docs/sp/webs.md +++ b/docs/sp/webs.md @@ -254,12 +254,15 @@ const infos2 = await web.webinfos.select("Title", "Description")(); // or filter const infos3 = await web.webinfos.filter("Title eq 'MyWebTitle'")(); +// Using fluent filter +const infos4 = await web.webinfos.filter(w => w.text("Title").equal('MyWebTitle'))(); + // or both -const infos4 = await web.webinfos.select("Title", "Description").filter("Title eq 'MyWebTitle'")(); +const infos5 = await web.webinfos.select("Title", "Description").filter(w => w.text("Title").equal('MyWebTitle'))(); // get the top 4 ordered by Title -const infos5 = await web.webinfos.top(4).orderBy("Title")(); +const infos6 = await web.webinfos.top(4).orderBy("Title")(); ``` > Note: webinfos returns [IWebInfosData](#IWebInfosData) which is a subset of all the available fields on IWebInfo. @@ -537,9 +540,12 @@ const folders = await sp.web.folders(); // you can also filter and select as with any collection const folders2 = await sp.web.folders.select("ServerRelativeUrl", "TimeLastModified").filter("ItemCount gt 0")(); +// Using fluent filter +const folders3 = await sp.web.folders.select("ServerRelativeUrl", "TimeLastModified").filter(f => f.number("ItemCount").greaterThan(0))(); + // or get the most recently modified folder -const folders2 = await sp.web.folders.orderBy("TimeLastModified").top(1)(); +const folders4 = await sp.web.folders.orderBy("TimeLastModified").top(1)(); ``` ### rootFolder @@ -856,6 +862,9 @@ const users = await sp.web.siteUsers(); const users2 = await sp.web.siteUsers.top(5)(); const users3 = await sp.web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent("i:0#.f|m")}')`)(); +// Using fluent filter +const user4 = await sp.web.siteUsers.filter(u => u.text("LoginName").startsWith(encodeURIComponent("i:0#.f|m")))(); + ``` ### currentUser From 759bbcf5fddee15191d2b9b592445b8f5e7253fa Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 28 Oct 2024 23:06:09 +0100 Subject: [PATCH 23/23] =?UTF-8?q?=F0=9F=8E=A8=20-=20rename=20to=20equals?= =?UTF-8?q?=20notEquals=20greaterThanOrEquals=20lessThanOrEquals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sp/items.md | 34 +++++++++++++++++----------------- docs/sp/webs.md | 4 ++-- packages/sp/spqueryable.ts | 12 ++++++------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/sp/items.md b/docs/sp/items.md index 6eea372b8..126024646 100644 --- a/docs/sp/items.md +++ b/docs/sp/items.md @@ -142,14 +142,14 @@ The following field types are supported in the fluent filter: The following operations are supported in the fluent filter: -| Field Type | Operators/Values | -| -------------------- | ------------------------------------------------------------------------------------------ | -| All field types | `equal`, `notEqual`, `in`, `notIn` | -| Text & choice fields | `startsWith`, `contains` | -| Numeric fields | `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual` | -| Date fields | `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `isBetween`, `isToday` | -| Boolean fields | `isTrue`, `isFalse`, `isFalseOrNull` | -| Lookup | `id`, Text, Number | +| Field Type | Operators/Values | +| -------------------- | -------------------------------------------------------------------------------------------- | +| All field types | `equals`, `notEquals`, `in`, `notIn` | +| Text & choice fields | `startsWith`, `contains` | +| Numeric fields | `greaterThan`, `greaterThanOrEquals`, `lessThan`, `lessThanOrEquals` | +| Date fields | `greaterThan`, `greaterThanOrEquals`, `lessThan`, `lessThanOrEquals`, `isBetween`, `isToday` | +| Boolean fields | `isTrue`, `isFalse`, `isFalseOrNull` | +| Lookup | `id`, Text and Number field types | #### Complex Filter @@ -178,22 +178,22 @@ interface ListItem extends IListItem { const r = await sp.web.lists.getByTitle("ListName").items.filter(f => f.text("FirstName").equal("John"))(); // Get all employees not named John who are over 30 -const r1 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.text("FirstName").notEqual("John").and().number("Age").greaterThan(30))(); +const r1 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.text("FirstName").notEquals("John").and().number("Age").greaterThan(30))(); // Get all employees that are named John Doe or Jane Doe const r2 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.or( f.and( - f.text("FirstName").equal("John"), - f.text("LastName").equal("Doe") + f.text("FirstName").equals("John"), + f.text("LastName").equals("Doe") ), f.and( - f.text("FirstName").equal("Jane"), - f.text("LastName").equal("Doe") + f.text("FirstName").equals("Jane"), + f.text("LastName").equals("Doe") ) ))(); // Get all employees who are managed by John and start today -const r3 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.lookup("Manager").text("FirstName").equal("John").and().date("StartDate").isToday())(); +const r3 = await sp.web.lists.getByTitle("ListName").items.filter(f => f.lookup("Manager").text("FirstName").equals("John").and().date("StartDate").isToday())(); ``` ### Retrieving PublishingPageImage @@ -410,7 +410,7 @@ const sp = spfi(...); // you are getting back a collection here const items: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'")(); // Using fluent filter -const items1: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter(f => f.text("Title").equal("A Title"))(); +const items1: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter(f => f.text("Title").equals("A Title"))(); // see if we got something if (items.length > 0) { @@ -511,7 +511,7 @@ const sp = spfi(...); // The Title of that hidden field is, in my case and in the linked article just the visible field name with "_0" appended. const fields = await sp.web.lists.getByTitle("TestList").fields.filter("Title eq 'MultiMetaData_0'").select("Title", "InternalName")(); // Using fluent filter -const fields1 = await sp.web.lists.getByTitle("TestList").fields.filter(f => f.text("Title").equal("MultiMetaData_0")).select("Title", "InternalName")(); +const fields1 = await sp.web.lists.getByTitle("TestList").fields.filter(f => f.text("Title").equals("MultiMetaData_0")).select("Title", "InternalName")(); // get an item to update, here we just create one for testing const newItem = await sp.web.lists.getByTitle("TestList").items.add({ @@ -687,7 +687,7 @@ const response1 = .getByTitle('[Lists_Title]') .fields .select('Title, EntityPropertyName') - .filter(l => l.boolean("Hidden").isFalse().and().text("Title").equal("[Field's_Display_Name]")) + .filter(l => l.boolean("Hidden").isFalse().and().text("Title").equals("[Field's_Display_Name]")) (); console.log(response.map(field => { diff --git a/docs/sp/webs.md b/docs/sp/webs.md index 54968c7a9..b57627542 100644 --- a/docs/sp/webs.md +++ b/docs/sp/webs.md @@ -255,11 +255,11 @@ const infos2 = await web.webinfos.select("Title", "Description")(); // or filter const infos3 = await web.webinfos.filter("Title eq 'MyWebTitle'")(); // Using fluent filter -const infos4 = await web.webinfos.filter(w => w.text("Title").equal('MyWebTitle'))(); +const infos4 = await web.webinfos.filter(w => w.text("Title").equals('MyWebTitle'))(); // or both -const infos5 = await web.webinfos.select("Title", "Description").filter(w => w.text("Title").equal('MyWebTitle'))(); +const infos5 = await web.webinfos.select("Title", "Description").filter(w => w.text("Title").equals('MyWebTitle'))(); // get the top 4 ordered by Title const infos6 = await web.webinfos.top(4).orderBy("Title")(); diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index f63d3ff03..f6733b92a 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -424,20 +424,20 @@ class NullableField extends BaseQuery extends NullableField { - public equal(value: TInputValueType): ComparisonResult { + public equals(value: TInputValueType): ComparisonResult { return new ComparisonResult([...this.query, FilterOperation.Equals, this.toODataValue(value)]); } - public notEqual(value: TInputValueType): ComparisonResult { + public notEquals(value: TInputValueType): ComparisonResult { return new ComparisonResult([...this.query, FilterOperation.NotEquals, this.toODataValue(value)]); } public in(...values: TInputValueType[]): ComparisonResult { - return SPOData.Where().or(...values.map(x => this.equal(x))); + return SPOData.Where().or(...values.map(x => this.equals(x))); } public notIn(...values: TInputValueType[]): ComparisonResult { - return SPOData.Where().and(...values.map(x => this.notEqual(x))); + return SPOData.Where().and(...values.map(x => this.notEquals(x))); } } @@ -488,7 +488,7 @@ class NumericField extends ComparableField([...this.query, FilterOperation.GreaterThan, this.toODataValue(value)]); } - public greaterThanOrEqual(value: TInputValueType): ComparisonResult { + public greaterThanOrEquals(value: TInputValueType): ComparisonResult { return new ComparisonResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.toODataValue(value)]); } @@ -496,7 +496,7 @@ class NumericField extends ComparableField([...this.query, FilterOperation.LessThan, this.toODataValue(value)]); } - public lessThanOrEqual(value: TInputValueType): ComparisonResult { + public lessThanOrEquals(value: TInputValueType): ComparisonResult { return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.toODataValue(value)]); } }