From 9f9987b255a0fceb8a781f3ef735be6ab46fcc89 Mon Sep 17 00:00:00 2001 From: Beau Cameron Date: Sat, 19 Aug 2023 08:09:42 -0600 Subject: [PATCH 01/31] 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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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/31] =?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 17daee50d805cfdf658d715a2e179136f6d5095f Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Tue, 15 Oct 2024 17:12:27 -0400 Subject: [PATCH 15/31] WIP graph pages, graph site open extenions and DebugHeaders --- debug/launch/setup.ts | 30 ++-- docs/graph/site-openextensions.md | 3 + docs/queryable/behaviors.md | 37 +++++ mkdocs.yml | 1 + packages/graph/decorators.ts | 4 +- packages/graph/graphqueryable.ts | 15 +- packages/graph/open-extensions/index.ts | 9 + packages/graph/open-extensions/site.ts | 13 ++ packages/graph/open-extensions/types.ts | 40 +++++ packages/graph/pages/index.ts | 8 + packages/graph/pages/site.ts | 14 ++ packages/graph/pages/types.ts | 154 ++++++++++++++++++ packages/queryable/behaviors/debug-headers.ts | 27 +++ packages/queryable/behaviors/parsers.ts | 2 +- packages/queryable/index.ts | 1 + 15 files changed, 332 insertions(+), 26 deletions(-) create mode 100644 docs/graph/site-openextensions.md create mode 100644 packages/graph/open-extensions/index.ts create mode 100644 packages/graph/open-extensions/site.ts create mode 100644 packages/graph/open-extensions/types.ts create mode 100644 packages/graph/pages/index.ts create mode 100644 packages/graph/pages/site.ts create mode 100644 packages/graph/pages/types.ts create mode 100644 packages/queryable/behaviors/debug-headers.ts diff --git a/debug/launch/setup.ts b/debug/launch/setup.ts index 472041b8f..093deeede 100644 --- a/debug/launch/setup.ts +++ b/debug/launch/setup.ts @@ -3,17 +3,19 @@ import { SPDefault, GraphDefault } from "@pnp/nodejs"; import { spfi, SPFI } from "@pnp/sp"; import { GraphFI, graphfi } from "@pnp/graph"; import { LogLevel, PnPLogging } from "@pnp/logging"; -import { Queryable } from "@pnp/queryable"; +import { Queryable, DebugHeaders } from "@pnp/queryable"; export function spSetup(settings: ITestingSettings): SPFI { - const sp = spfi(settings.testing.sp.url).using(SPDefault({ - msal: { - config: settings.testing.sp.msal.init, - scopes: settings.testing.sp.msal.scopes, - }, - })).using( + const sp = spfi(settings.testing.sp.url).using( + SPDefault({ + msal: { + config: settings.testing.sp.msal.init, + scopes: settings.testing.sp.msal.scopes, + }, + }), PnPLogging(LogLevel.Verbose), + DebugHeaders(), function (instance: Queryable) { instance.on.pre(async (url, init, result) => { @@ -29,13 +31,15 @@ export function spSetup(settings: ITestingSettings): SPFI { export function graphSetup(settings: ITestingSettings): GraphFI { - const graph = graphfi().using(GraphDefault({ - msal: { - config: settings.testing.graph.msal.init, - scopes: settings.testing.graph.msal.scopes, - }, - })).using( + const graph = graphfi().using( + GraphDefault({ + msal: { + config: settings.testing.graph.msal.init, + scopes: settings.testing.graph.msal.scopes, + }, + }), PnPLogging(LogLevel.Verbose), + DebugHeaders(), function (instance: Queryable) { instance.on.pre(async (url, init, result) => { diff --git a/docs/graph/site-openextensions.md b/docs/graph/site-openextensions.md new file mode 100644 index 000000000..fe8ff0c99 --- /dev/null +++ b/docs/graph/site-openextensions.md @@ -0,0 +1,3 @@ +# Site Open Extensions + +// TODO \ No newline at end of file diff --git a/docs/queryable/behaviors.md b/docs/queryable/behaviors.md index 5bc94e91f..e9b0962fa 100644 --- a/docs/queryable/behaviors.md +++ b/docs/queryable/behaviors.md @@ -491,3 +491,40 @@ setTimeout(() => { // this is awaiting the results of the request await p; ``` + +### DebugHeaders + +Adds logging for the request id and timestamp of the request, helpful when contacting Microsoft Support. It works for both Graph and SP libraries. + +```TypeScript +import { DebugHeaders } from "@pnp/queryable"; +import { spfi } from "@pnp/sp"; + +const sp = spfi().using(DebugHeaders()); + +sp.some_action(); + +// output to log: +// Server Request Id: {guid} +// Server Date: {date} +``` + +You can also supply additional headers to log from the response: + + +```TypeScript +import { DebugHeaders } from "@pnp/queryable"; +import { spfi } from "@pnp/sp"; + +const sp = spfi().using(DebugHeaders(["X-MyHeader", "client-request-id"])); + +sp.some_action(); + +// output to log: +// Server Request Id: {guid} +// Server Date: {date} +// X-MyHeader: {value} +// client-request-id: {guid} +``` + + diff --git a/mkdocs.yml b/mkdocs.yml index 1ee2e9f3a..7fd3b5115 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,6 +78,7 @@ nav: - search: 'graph/search.md' - shares: 'graph/shares.md' - sites: 'graph/sites.md' + - 'site openextensions': 'graph/site-openextensions.md' - subscriptions: 'graph/subscriptions.md' - taxonomy: 'graph/taxonomy.md' - teams: 'graph/teams.md' diff --git a/packages/graph/decorators.ts b/packages/graph/decorators.ts index 53ec5b0be..158463061 100644 --- a/packages/graph/decorators.ts +++ b/packages/graph/decorators.ts @@ -147,8 +147,8 @@ export function getById(factory: (...args: any[]) => R) { return function (target: T) { return class extends target { - public getById(this: IGraphQueryable, id: string): R { - return factory(this, id); + public getById(this: IGraphQueryable, id: any): R { + return factory(this, `${id}`); } }; }; diff --git a/packages/graph/graphqueryable.ts b/packages/graph/graphqueryable.ts index 324640e83..a05de634a 100644 --- a/packages/graph/graphqueryable.ts +++ b/packages/graph/graphqueryable.ts @@ -80,17 +80,12 @@ export class _GraphQueryable extends Queryable { * * @param factory The contructor for the class to create */ - protected getParent( - factory: IGraphConstructor, - base: GraphInit = this.parentUrl, - path?: string): T { + protected getParent( + factory: IGraphInvokableFactory, + path?: string, + base: string = this.parentUrl): T { - if (typeof base === "string") { - // we need to ensure the parent has observers, even if we are rebasing the url (#2435) - base = [this, base]; - } - - return new factory(base, path); + return factory([this, base], path); } } diff --git a/packages/graph/open-extensions/index.ts b/packages/graph/open-extensions/index.ts new file mode 100644 index 000000000..cfb507ac3 --- /dev/null +++ b/packages/graph/open-extensions/index.ts @@ -0,0 +1,9 @@ +import "./site.js"; + +export { + IBaseExtensionData as IBaseOpenExtension, + IOpenExtension, + IOpenExtensions, + OpenExtension, + OpenExtensions, +} from "./types.js"; diff --git a/packages/graph/open-extensions/site.ts b/packages/graph/open-extensions/site.ts new file mode 100644 index 000000000..547143e79 --- /dev/null +++ b/packages/graph/open-extensions/site.ts @@ -0,0 +1,13 @@ +import { addProp } from "@pnp/queryable"; +import { _Site } from "../sites/types.js"; +import { IOpenExtensions, OpenExtensions } from "./types.js"; + +declare module "../sites/types" { + interface _Site { + readonly extensions: IOpenExtensions; + } + interface ISite { + readonly extensions: IOpenExtensions; + } +} +addProp(_Site, "extensions", OpenExtensions); diff --git a/packages/graph/open-extensions/types.ts b/packages/graph/open-extensions/types.ts new file mode 100644 index 000000000..2d7dde5da --- /dev/null +++ b/packages/graph/open-extensions/types.ts @@ -0,0 +1,40 @@ +import { body } from "@pnp/queryable"; +import { Extension as ExtensionType } from "@microsoft/microsoft-graph-types"; +import { _GraphCollection, graphInvokableFactory, graphPatch, graphPost } from "../graphqueryable.js"; +import { getById, IGetById, deleteable, IDeleteable, defaultPath } from "../decorators.js"; + +export interface IBaseExtensionData { + extensionName: string; +} + +/** + * Open Extension + */ +@deleteable() +export class _OpenExtension extends _GraphCollection { + + public update(extension: T): Promise { + return graphPatch(this, body(extension)); + } +} +export interface IOpenExtension extends _OpenExtension, IDeleteable { } +export const OpenExtension = graphInvokableFactory(_OpenExtension); + +/** + * Open Extensions + */ +@defaultPath("extensions") +@getById(OpenExtension) +export class _OpenExtensions extends _GraphCollection { + + public create(extension: T): Promise { + + if (extension.extensionName.length > 30) { + throw Error("Extension id length should be less than or equal to 30 characters."); + } + + return graphPost(this, body(extension)); + } +} +export interface IOpenExtensions extends _OpenExtensions, IGetById { } +export const OpenExtensions = graphInvokableFactory(_OpenExtensions); diff --git a/packages/graph/pages/index.ts b/packages/graph/pages/index.ts new file mode 100644 index 000000000..a240f98e3 --- /dev/null +++ b/packages/graph/pages/index.ts @@ -0,0 +1,8 @@ +import "./site.js"; + +export { + IPage, + IPages, + Page, + Pages, +} from "./types.js"; diff --git a/packages/graph/pages/site.ts b/packages/graph/pages/site.ts new file mode 100644 index 000000000..25c52ade8 --- /dev/null +++ b/packages/graph/pages/site.ts @@ -0,0 +1,14 @@ +import { addProp } from "@pnp/queryable"; +import { _Site } from "../sites/types.js"; +import { IPages, Pages } from "./types.js"; + +declare module "../sites/types" { + interface _Site { + readonly pages: IPages; + } + interface ISite { + readonly pages: IPages; + } +} + +addProp(_Site, "pages", Pages); diff --git a/packages/graph/pages/types.ts b/packages/graph/pages/types.ts new file mode 100644 index 000000000..5bfeb0fb2 --- /dev/null +++ b/packages/graph/pages/types.ts @@ -0,0 +1,154 @@ + +import { combine } from "@pnp/core"; +import { IDeleteable, IGetById, IUpdateable, defaultPath, deleteable, getById, updateable } from "../decorators.js"; +import { graphInvokableFactory, _GraphCollection, _GraphInstance, GraphInit, graphPost } from "../graphqueryable.js"; +import { body } from "@pnp/queryable"; + +/** + * Page + */ +@deleteable() +@updateable() +export class _Page extends _GraphInstance { } +export interface IPage extends _Page, IUpdateable>, IDeleteable { } +export const Page = graphInvokableFactory(_Page); + +/** + * Pages + */ +@defaultPath("pages") +@getById(Page) +export class _Pages extends _GraphCollection { + public get sitePages(): ISitePages { + return SitePages(this); + } +} +export interface IPages extends _Pages, IGetById { } +export const Pages = graphInvokableFactory(_Pages); + +/** + * Site Page + */ +@deleteable() +@updateable() +export class _SitePage extends _GraphInstance { + + /** + * Publishes the page + * @returns void + */ + public async publish(): Promise { + return graphPost(SitePage(this, "publish")); + } + + /** + * Gets the webparts in the page + * + * @returns array fo webpart information + */ + public async getWebPartsByPosition(): Promise { + return SitePage(this, "getWebPartsByPosition")(); + } + + /** + * Gets the set of horizontal sections + */ + public get horizontalSections(): IHorizontalSections { + return HorizontalSections(this); + } +} +export interface ISitePage extends _SitePage, IUpdateable>, IDeleteable { } +export const SitePage = graphInvokableFactory(_SitePage); + +const SitePageTypeString = "microsoft.graph.sitePage"; + +/** + * Site Pages + */ +@defaultPath(SitePageTypeString) +export class _SitePages extends _GraphCollection { + + private _pages: IPages; + + constructor(base: GraphInit, path?: string) { + super(base, path); + this._pages = this.getParent(Pages, ""); + } + + public getById(this: ISitePages, id: string): ISitePage { + return SitePage(this._pages, combine(id, SitePageTypeString)); + } + + public async add(pageInfo: Partial>): Promise { + return graphPost(this._pages, body({ "@odata.type": SitePageTypeString, ...pageInfo })); + } +} +export interface ISitePages extends _SitePages { } +export const SitePages = graphInvokableFactory(_SitePages); + +export class _HorizontalSection extends _GraphInstance {} +export interface IHorizontalSection extends _HorizontalSection { } +export const HorizontalSection = graphInvokableFactory(_HorizontalSection); + +@getById(HorizontalSection) +@defaultPath("canvasLayout/horizontalSections") +export class _HorizontalSections extends _GraphCollection { + + public async add(props: Partial): Promise { + return graphPost(this, body(props)); + } +} +export interface IHorizontalSections extends _HorizontalSections, IGetById { } +export const HorizontalSections = graphInvokableFactory(_HorizontalSections); + + + + + + + +export interface IHorizontalSectionInfo { + emphasis: "none" | "netural" | "soft" | "strong" | "unknownFutureValue"; + id: string; + layout: "none" | "oneColumn" | "twoColumns" | "threeColumns" | "oneThirdLeftColumn" | "oneThirdRightColumn" | "fullWidth" | "unknownFutureValue"; + columns: IHorizontalSectionColumnInfo[]; +} + +export interface IHorizontalSectionColumnInfo { + id: string; + width: string; + webparts: any[]; +} + + +export interface IPageUserInfo { + displayName: string; + email?: string; +} + +export interface ISitePageInfo extends IPageInfo { } + +export interface IPageInfo { + "@odata.type"?: string; + "@odata.etag"?: string; + contentType: { + id: string; + name: string; + }; + createdDateTime: string; + eTag: string; + id: string; + createdBy: { user: IPageUserInfo }; + lastModifiedBy: { user: IPageUserInfo }; + lastModifiedDateTime: string; + name: string; + pageLayout: string; + parentReference: { siteId: string }; + promotionKind: string; + publishingState: { level: string; versionId: string }; + reactions: any; + showComments: boolean; + showRecommendedPages: boolean; + title: string; + webUrl: string; +} diff --git a/packages/queryable/behaviors/debug-headers.ts b/packages/queryable/behaviors/debug-headers.ts new file mode 100644 index 000000000..77478a1ce --- /dev/null +++ b/packages/queryable/behaviors/debug-headers.ts @@ -0,0 +1,27 @@ +import { TimelinePipe } from "@pnp/core"; +import { Queryable } from "../queryable.js"; + +/** + * + * @param otherHeaders Optional list of additional headers to log from the response + * @returns A timeline pipe + */ +export function DebugHeaders(otherHeaders: string[] = []): TimelinePipe { + + return (instance: Queryable) => { + + instance.on.parse.prepend(async function (this: Queryable, url, response, result) { + + // here we add logging for the request id and timestamp to assist in reporting issues to Microsoft + const searchHeaders = ["request-id", "sprequestguid", "date", ...otherHeaders]; + + for (let i = 0; i < searchHeaders.length; i++) { + this.log(`${searchHeaders[i]}: ${response.headers.get(searchHeaders[i]) ?? ""}`); + } + + return [url, response, result]; + }); + + return instance; + }; +} diff --git a/packages/queryable/behaviors/parsers.ts b/packages/queryable/behaviors/parsers.ts index 68b171310..a2873c221 100644 --- a/packages/queryable/behaviors/parsers.ts +++ b/packages/queryable/behaviors/parsers.ts @@ -127,7 +127,7 @@ export class HttpRequestError extends Error { } public static async init(r: Response): Promise { - const t = await r.clone().text(); + const t = await r.text(); return new HttpRequestError(`Error making HttpClient request in queryable [${r.status}] ${r.statusText} ::> ${t}`, r); } } diff --git a/packages/queryable/index.ts b/packages/queryable/index.ts index c0b73bae0..1fa5af152 100644 --- a/packages/queryable/index.ts +++ b/packages/queryable/index.ts @@ -11,6 +11,7 @@ export * from "./behaviors/caching.js"; export * from "./behaviors/caching-pessimistic.js"; export * from "./behaviors/cancelable.js"; export * from "./behaviors/inject-headers.js"; +export * from "./behaviors/debug-headers.js"; export * from "./behaviors/parsers.js"; export * from "./behaviors/timeout.js"; export * from "./behaviors/resolvers.js"; From 9019579f87d6d39b691ec81b67c840ffe2d77616 Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Fri, 18 Oct 2024 21:32:55 +0200 Subject: [PATCH 16/31] =?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 17/31] =?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 18/31] =?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 19/31] =?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 20/31] =?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 21/31] =?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 22/31] =?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 23/31] =?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 5b2565df2cb2cf8cde4a89f88e0fc6d2ea440924 Mon Sep 17 00:00:00 2001 From: Tomi Tavela Date: Sat, 26 Oct 2024 22:38:51 +0300 Subject: [PATCH 24/31] added missing exports to graph/presets/all --- packages/graph/presets/all.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/graph/presets/all.ts b/packages/graph/presets/all.ts index 6c08ef8c8..cd5690c71 100644 --- a/packages/graph/presets/all.ts +++ b/packages/graph/presets/all.ts @@ -1,7 +1,9 @@ +import "../index.js"; import "../admin/index.js"; import "../analytics/index.js"; import "../appCatalog/index.js"; import "../attachments/index.js"; +import "../bookings/index.js"; import "../calendars/index.js"; import "../cloud-communications/index.js"; import "../columns/index.js"; @@ -29,13 +31,16 @@ import "../shares/index.js"; import "../sites/index.js"; import "../subscriptions/index.js"; import "../taxonomy/index.js"; +import "../teams/index.js"; import "../to-do/index.js"; import "../users/index.js"; +export * from "../index.js"; export * from "../admin/index.js"; export * from "../analytics/index.js"; export * from "../appCatalog/index.js"; export * from "../attachments/index.js"; +export * from "../bookings/index.js"; export * from "../calendars/index.js"; export * from "../cloud-communications/index.js"; export * from "../columns/index.js"; @@ -63,5 +68,6 @@ export * from "../shares/index.js"; export * from "../sites/index.js"; export * from "../subscriptions/index.js"; export * from "../taxonomy/index.js"; +export * from "../teams/index.js"; export * from "../to-do/index.js"; export * from "../users/index.js"; From 759bbcf5fddee15191d2b9b592445b8f5e7253fa Mon Sep 17 00:00:00 2001 From: Dan Toft Date: Mon, 28 Oct 2024 23:06:09 +0100 Subject: [PATCH 25/31] =?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)]); } } From 32ba4c724a4c0417c0fde21a5cbc769d47935314 Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 11 Nov 2024 09:46:37 -0500 Subject: [PATCH 26/31] wip --- packages/graph/onenote/index.ts | 12 ++- packages/graph/onenote/types.ts | 24 +++--- packages/graph/pages/index.ts | 26 ++++++ packages/graph/pages/types.ts | 109 +++++++++++++++++++++++++- packages/graph/pages/webpart-types.ts | 20 +++++ packages/graph/presets/all.ts | 2 + 6 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 packages/graph/pages/webpart-types.ts diff --git a/packages/graph/onenote/index.ts b/packages/graph/onenote/index.ts index dec8f5774..0b6394cfe 100644 --- a/packages/graph/onenote/index.ts +++ b/packages/graph/onenote/index.ts @@ -6,15 +6,21 @@ export { INotebook, INotebooks, IOneNote, - IPages, IResources, ISection, ISections, Notebook, Notebooks, OneNote, - Page, - Pages, + ICopyProps, + IOnenotePage, + IOnenotePages, + ISectionGroup, + ISectionGroups, + OnenotePage, + OnenotePages, + SectionGroup, + SectionGroups, Resources, Section, Sections, diff --git a/packages/graph/onenote/types.ts b/packages/graph/onenote/types.ts index 94f4e12dc..946816c38 100644 --- a/packages/graph/onenote/types.ts +++ b/packages/graph/onenote/types.ts @@ -32,8 +32,8 @@ export class _OneNote extends _GraphInstance { return Notebooks(this); } - public get pages(): IPages { - return Pages(this); + public get pages(): IOnenotePages { + return OnenotePages(this); } public get resources(): IResources { @@ -107,8 +107,8 @@ export const Notebooks = graphInvokableFactory(_Notebooks); */ export class _Section extends _GraphInstance { - public get pages(): IPages { - return Pages(this); + public get pages(): IOnenotePages { + return OnenotePages(this); } /** @@ -187,7 +187,7 @@ export const SectionGroups = graphInvokableFactory(_SectionGroup * */ @deleteable() -export class _Page extends _GraphInstance { +export class _OnenotePage extends _GraphInstance { /** * Copy page to section * @param props of type ICopyPageProps. groupId (id of group to copy to. Use only when copying to M365 group), id of destination notebook @@ -202,7 +202,7 @@ export class _Page extends _GraphInstance { * @param includeIDs page html body */ public async content(includeIDs = false): Promise { - return Page(this, `content?includeIDs=${includeIDs}`).using(TextParse())(); + return OnenotePage(this, `content?includeIDs=${includeIDs}`).using(TextParse())(); } /** @@ -213,16 +213,16 @@ export class _Page extends _GraphInstance { return graphPatch(GraphQueryable(this, "content"), body(props)); } } -export interface IPage extends _Page, IDeleteable { } -export const Page = graphInvokableFactory(_Page); +export interface IOnenotePage extends _OnenotePage, IDeleteable { } +export const OnenotePage = graphInvokableFactory(_OnenotePage); /** * Describes a collection of page objects * */ @defaultPath("pages") -@getById(Page) -export class _Pages extends _GraphCollection { +@getById(OnenotePage) +export class _OnenotePages extends _GraphCollection { /** * Create a new page as specified in the request body. * @@ -237,8 +237,8 @@ export class _Pages extends _GraphCollection { return graphPost(q, { body: html }); } } -export interface IPages extends _Pages, IGetById { } -export const Pages = graphInvokableFactory(_Pages); +export interface IOnenotePages extends _OnenotePages, IGetById { } +export const OnenotePages = graphInvokableFactory(_OnenotePages); /** * Describes a resources diff --git a/packages/graph/pages/index.ts b/packages/graph/pages/index.ts index a240f98e3..f19bdcd32 100644 --- a/packages/graph/pages/index.ts +++ b/packages/graph/pages/index.ts @@ -1,8 +1,34 @@ import "./site.js"; +export * from "./webpart-types.js"; + export { IPage, IPages, Page, Pages, + HorizontalSection, + HorizontalSectionColumn, + HorizontalSectionColumns, + HorizontalSections, + IHorizontalSection, + IHorizontalSectionColumn, + IHorizontalSectionColumnInfo, + IHorizontalSectionColumns, + IHorizontalSectionInfo, + IHorizontalSections, + IPageInfo, + IPageUserInfo, + ISitePage, + ISitePageInfo, + ISitePages, + IVerticalSection, + IVerticalSectionInfo, + IWebpart, + IWebparts, + SitePage, + SitePages, + VerticalSection, + Webpart, + Webparts, } from "./types.js"; diff --git a/packages/graph/pages/types.ts b/packages/graph/pages/types.ts index 5bfeb0fb2..02cbc1628 100644 --- a/packages/graph/pages/types.ts +++ b/packages/graph/pages/types.ts @@ -3,6 +3,7 @@ import { combine } from "@pnp/core"; import { IDeleteable, IGetById, IUpdateable, defaultPath, deleteable, getById, updateable } from "../decorators.js"; import { graphInvokableFactory, _GraphCollection, _GraphInstance, GraphInit, graphPost } from "../graphqueryable.js"; import { body } from "@pnp/queryable"; +import { ValidWebpart } from "./webpart-types.js"; /** * Page @@ -50,12 +51,40 @@ export class _SitePage extends _GraphInstance { return SitePage(this, "getWebPartsByPosition")(); } + /** + * Get a listing of all the webparts in this page + */ + public get webparts(): IWebparts { + return Webparts(this); + } + /** * Gets the set of horizontal sections */ public get horizontalSections(): IHorizontalSections { return HorizontalSections(this); } + + /** + * Gets the set of vertical section + */ + public get verticalSection(): IVerticalSection { + return VerticalSection(this); + } + + /** + * Creates a vertical section if none exists, returns the vertical section + */ + public ensureVerticalSection(): IVerticalSection { + + const y = this.select("verticalSection")(); + + console.log(y); + + + return null; + + } } export interface ISitePage extends _SitePage, IUpdateable>, IDeleteable { } export const SitePage = graphInvokableFactory(_SitePage); @@ -86,25 +115,99 @@ export class _SitePages extends _GraphCollection { export interface ISitePages extends _SitePages { } export const SitePages = graphInvokableFactory(_SitePages); -export class _HorizontalSection extends _GraphInstance {} -export interface IHorizontalSection extends _HorizontalSection { } +@updateable() +@deleteable() +export class _HorizontalSection extends _GraphInstance { + + public get columns(): IHorizontalSectionColumns { + return HorizontalSectionColumns(this); + } +} +export interface IHorizontalSection extends _HorizontalSection, IUpdateable, IDeleteable { } export const HorizontalSection = graphInvokableFactory(_HorizontalSection); -@getById(HorizontalSection) +// @getById(HorizontalSection) @defaultPath("canvasLayout/horizontalSections") export class _HorizontalSections extends _GraphCollection { public async add(props: Partial): Promise { return graphPost(this, body(props)); } + + public getById(id: string | number): IHorizontalSection { + const section = HorizontalSection(this); + return section.concat(`('${id}')`); + } } export interface IHorizontalSections extends _HorizontalSections, IGetById { } export const HorizontalSections = graphInvokableFactory(_HorizontalSections); +export class _HorizontalSectionColumn extends _GraphInstance { + public get webparts(): IWebparts { + return Webparts(this); + } +} +export interface IHorizontalSectionColumn extends _HorizontalSectionColumn { } +export const HorizontalSectionColumn = graphInvokableFactory(_HorizontalSectionColumn); + +@defaultPath("columns") +@getById(HorizontalSectionColumn) +export class _HorizontalSectionColumns extends _GraphCollection { } +export interface IHorizontalSectionColumns extends _HorizontalSectionColumns, IGetById { } +export const HorizontalSectionColumns = graphInvokableFactory(_HorizontalSectionColumns); + +@updateable() +@deleteable() +@defaultPath("canvasLayout/verticalSection") +export class _VerticalSection extends _GraphInstance { + /** + * Get a listing of all the webparts in this vertical section + */ + public get webparts(): IWebparts { + return Webparts(this); + } +} +export interface IVerticalSection extends _VerticalSection, IUpdateable, IDeleteable { } +export const VerticalSection = graphInvokableFactory(_VerticalSection); + +export class _Webpart extends _GraphInstance { } +export interface IWebpart extends _Webpart { } +export const Webpart = graphInvokableFactory(_Webpart); + +@defaultPath("webparts") +export class _Webparts extends _GraphCollection { + + /** + * Gets the webpart information by id from the page's collection + * @param id string id of the webpart + * @returns The IWebpart instance + */ + public getById(id: string): IWebpart { + + const url = this.toUrl(); + const base = url.slice(0, url.indexOf(SitePageTypeString) + SitePageTypeString.length); + return Webpart([this, base], `webparts/${id}`); + } + + /** + * Gets the webpart information by id from the page's collection + * @param id string id of the webpart + * @returns The IWebpart instance + */ + public getByIndex(index: number): IWebpart { + return Webpart(this, `${index}`); + } +} +export interface IWebparts extends _Webparts, IGetById { } +export const Webparts = graphInvokableFactory(_Webparts); +export interface IVerticalSectionInfo { + emphasis: "none" | "netural" | "soft" | "strong" | "unknownFutureValue"; + id: string; +} export interface IHorizontalSectionInfo { diff --git a/packages/graph/pages/webpart-types.ts b/packages/graph/pages/webpart-types.ts new file mode 100644 index 000000000..0fe9c334a --- /dev/null +++ b/packages/graph/pages/webpart-types.ts @@ -0,0 +1,20 @@ +// We welcome contributions to filling out the available webpart types in the service. Do not add non-Microsoft webparts +// 1. Add an interface for the webpart definition +// 2. Add the interface to the ValidWebpart type at the top of the file +// 2. Add the interface to the ValidWebpartNoAny type at the top of the file + +/** + * Defines the schemas for valid webparts provided by Microsoft. Includes 'any' to avoid typing errors for undefined webparts + */ +export type ValidWebpart = MSTextWebPart | any; + +/** + * Defines the schemas for valid webparts provided by Microsoft. Does not allow 'any' + */ +export type ValidWebpartNoAny = MSTextWebPart; + +export interface MSTextWebPart { + "@odata.type": "#microsoft.graph.textWebPart"; + id: string; + innerHtml: string; +} diff --git a/packages/graph/presets/all.ts b/packages/graph/presets/all.ts index 6c08ef8c8..5c5217b40 100644 --- a/packages/graph/presets/all.ts +++ b/packages/graph/presets/all.ts @@ -20,6 +20,7 @@ import "../mail/index.js"; import "../members/index.js"; import "../onenote/index.js"; import "../operations/index.js"; +import "../pages/index.js"; import "../permissions/index.js"; import "../photos/index.js"; import "../places/index.js"; @@ -54,6 +55,7 @@ export * from "../mail/index.js"; export * from "../members/index.js"; export * from "../onenote/index.js"; export * from "../operations/index.js"; +export * from "../pages/index.js"; export * from "../permissions/index.js"; export * from "../photos/index.js"; export * from "../places/index.js"; From 90ba34b12584481db88a0a3ee9c931eadf02879a Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 11 Nov 2024 15:36:22 -0500 Subject: [PATCH 27/31] initial pages api support, fix for #3136 --- debug/launch/main.ts | 4 ++-- debug/launch/sp.ts | 5 ++-- packages/core/util.ts | 3 +-- packages/graph/graphqueryable.ts | 14 ++++------- packages/graph/pages/types.ts | 40 ++++++++++++++++++-------------- packages/queryable/queryable.ts | 5 ++++ tsconfig.json | 1 + 7 files changed, 37 insertions(+), 35 deletions(-) diff --git a/debug/launch/main.ts b/debug/launch/main.ts index 230ee47d3..2a4fdec1e 100644 --- a/debug/launch/main.ts +++ b/debug/launch/main.ts @@ -19,7 +19,7 @@ import { Example } from "./sp.js"; // create a settings file using settings.example.js as a template import(findup("settings.js")).then((settings: { settings: ITestingSettings }) => { - Logger.activeLogLevel = LogLevel.Info; + Logger.activeLogLevel = LogLevel.Info; // // setup console logger Logger.subscribe(ConsoleListener("Debug", { @@ -27,7 +27,7 @@ import(findup("settings.js")).then((settings: { settings: ITestingSettings }) => error: "red", verbose: "lightslategray", warning: "yellow", - })); + })); Example(settings.settings); diff --git a/debug/launch/sp.ts b/debug/launch/sp.ts index dab257330..352e7b5cd 100644 --- a/debug/launch/sp.ts +++ b/debug/launch/sp.ts @@ -3,7 +3,6 @@ import { Logger, LogLevel } from "@pnp/logging"; import { spSetup } from "./setup.js"; import "@pnp/sp/webs"; import "@pnp/sp/lists"; -import "@pnp/sp/items"; declare var process: { exit(code?: number): void }; @@ -11,13 +10,13 @@ export async function Example(settings: ITestingSettings) { const sp = spSetup(settings); - const w = await sp.web(); + const w = await sp.web.lists(); Logger.log({ data: w, level: LogLevel.Info, message: "Web Data", }); - + process.exit(0); } diff --git a/packages/core/util.ts b/packages/core/util.ts index 9bac79d7d..1a0bc2bb2 100644 --- a/packages/core/util.ts +++ b/packages/core/util.ts @@ -34,8 +34,7 @@ export function combine(...paths: (string | null | undefined)[]): string { return paths .filter(path => !stringIsNullOrEmpty(path)) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .map(path => path!.replace(/^[\\|/]/, "").replace(/[\\|/]$/, "")) + .map(path => path.replace(/^[\\|/]/, "").replace(/[\\|/]$/, "")) .join("/") .replace(/\\/g, "/"); } diff --git a/packages/graph/graphqueryable.ts b/packages/graph/graphqueryable.ts index a05de634a..c53bf3695 100644 --- a/packages/graph/graphqueryable.ts +++ b/packages/graph/graphqueryable.ts @@ -1,4 +1,4 @@ -import { isArray, objectDefinedNotNull } from "@pnp/core"; +import { isArray } from "@pnp/core"; import { IInvokable, Queryable, queryableFactory, op, get, post, patch, del, put } from "@pnp/queryable"; import { ConsistencyLevel } from "./behaviors/consistency-level.js"; import { IPagedResult, Paged } from "./behaviors/paged.js"; @@ -164,13 +164,9 @@ export class _GraphCollection extends _GraphQueryable const q = GraphCollection(this).using(Paged(), ConsistencyLevel()); - const queryParams = ["$search", "$top", "$select", "$expand", "$filter", "$orderby"]; - - for (let i = 0; i < queryParams.length; i++) { - const param = this.query.get(queryParams[i]); - if (objectDefinedNotNull(param)) { - q.query.set(queryParams[i], param); - } + // Issue #3136, some APIs take other query params that need to persist through the paging, so we just include everything + for (const [key, value] of this.query) { + q.query.set(key, value); } return >{ @@ -196,7 +192,6 @@ export class _GraphCollection extends _GraphQueryable }; } } - export interface IGraphCollection extends _GraphCollection { } export const GraphCollection = graphInvokableFactory(_GraphCollection); @@ -205,7 +200,6 @@ export const GraphCollection = graphInvokableFactory(_GraphCol * */ export class _GraphInstance extends _GraphQueryable { } - export interface IGraphInstance extends IInvokable, IGraphQueryable { } export const GraphInstance = graphInvokableFactory(_GraphInstance); diff --git a/packages/graph/pages/types.ts b/packages/graph/pages/types.ts index 02cbc1628..cdbb61fa1 100644 --- a/packages/graph/pages/types.ts +++ b/packages/graph/pages/types.ts @@ -1,8 +1,8 @@ import { combine } from "@pnp/core"; +import { body } from "@pnp/queryable"; import { IDeleteable, IGetById, IUpdateable, defaultPath, deleteable, getById, updateable } from "../decorators.js"; import { graphInvokableFactory, _GraphCollection, _GraphInstance, GraphInit, graphPost } from "../graphqueryable.js"; -import { body } from "@pnp/queryable"; import { ValidWebpart } from "./webpart-types.js"; /** @@ -126,7 +126,6 @@ export class _HorizontalSection extends _GraphInstance { export interface IHorizontalSection extends _HorizontalSection, IUpdateable, IDeleteable { } export const HorizontalSection = graphInvokableFactory(_HorizontalSection); -// @getById(HorizontalSection) @defaultPath("canvasLayout/horizontalSections") export class _HorizontalSections extends _GraphCollection { @@ -139,7 +138,7 @@ export class _HorizontalSections extends _GraphCollection { } +export interface IHorizontalSections extends _HorizontalSections, IGetById { } export const HorizontalSections = graphInvokableFactory(_HorizontalSections); export class _HorizontalSectionColumn extends _GraphInstance { @@ -152,9 +151,14 @@ export interface IHorizontalSectionColumn extends _HorizontalSectionColumn { } export const HorizontalSectionColumn = graphInvokableFactory(_HorizontalSectionColumn); @defaultPath("columns") -@getById(HorizontalSectionColumn) -export class _HorizontalSectionColumns extends _GraphCollection { } -export interface IHorizontalSectionColumns extends _HorizontalSectionColumns, IGetById { } +export class _HorizontalSectionColumns extends _GraphCollection { + + public getById(id: string | number): IHorizontalSectionColumn { + const column = HorizontalSectionColumn(this); + return column.concat(`('${id}')`); + } +} +export interface IHorizontalSectionColumns extends _HorizontalSectionColumns, IGetById { } export const HorizontalSectionColumns = graphInvokableFactory(_HorizontalSectionColumns); @updateable() @@ -189,27 +193,22 @@ export class _Webparts extends _GraphCollection { const base = url.slice(0, url.indexOf(SitePageTypeString) + SitePageTypeString.length); return Webpart([this, base], `webparts/${id}`); } - - /** - * Gets the webpart information by id from the page's collection - * @param id string id of the webpart - * @returns The IWebpart instance - */ - public getByIndex(index: number): IWebpart { - return Webpart(this, `${index}`); - } } export interface IWebparts extends _Webparts, IGetById { } export const Webparts = graphInvokableFactory(_Webparts); - +/** + * Contains info representing a vertical section + */ export interface IVerticalSectionInfo { emphasis: "none" | "netural" | "soft" | "strong" | "unknownFutureValue"; id: string; } - +/** + * Contains info representing a horizontal section + */ export interface IHorizontalSectionInfo { emphasis: "none" | "netural" | "soft" | "strong" | "unknownFutureValue"; id: string; @@ -217,13 +216,18 @@ export interface IHorizontalSectionInfo { columns: IHorizontalSectionColumnInfo[]; } +/** + * Contains info representing a horizontal section column + */ export interface IHorizontalSectionColumnInfo { id: string; width: string; webparts: any[]; } - +/** + * Contains info representing a path user + */ export interface IPageUserInfo { displayName: string; email?: string; diff --git a/packages/queryable/queryable.ts b/packages/queryable/queryable.ts index afe87e4e0..722e652b4 100644 --- a/packages/queryable/queryable.ts +++ b/packages/queryable/queryable.ts @@ -52,6 +52,11 @@ export type QueryParams = { /** Returns a string containing a query string suitable for use in a URL. Does not include the question mark. */ toString(): string; + + /** + * Iterator accessor + */ + [Symbol.iterator](): Iterator<[string, string]>; }; @invokable() diff --git a/tsconfig.json b/tsconfig.json index 74384c7bf..cca0157fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "ES2015", "dom", "ES2017.Object", + "ES2015.Iterable" ], "baseUrl": ".", "rootDir": ".", From 2066a0ed8a0fd9389cb609699ddc8a80fe59bd43 Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 11 Nov 2024 15:52:12 -0500 Subject: [PATCH 28/31] fixing linting errors --- packages/sp/spqueryable.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index f6733b92a..14d8421c7 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -358,7 +358,7 @@ 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) + 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)})`]); } @@ -536,22 +536,27 @@ class DateField extends NumericField { class ComparisonResult extends BaseQuery { public and(): QueryableAndResult; + // eslint-disable-next-line @typescript-eslint/semi public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult + // eslint-disable-next-line max-len public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableAndResult) { - if (queries == null || queries.length == 0) + 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; + // eslint-disable-next-line max-len public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableOrResult) { - if (queries == null || queries.length == 0) + 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 bda482e7a0597d780ce10230155a3bad7d9d03a7 Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 18 Nov 2024 09:52:26 -0500 Subject: [PATCH 29/31] fixing linting/build errors --- packages/sp/spqueryable.ts | 141 +++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index 14d8421c7..57096fc3c 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -294,7 +294,8 @@ class SPOData { } // Linting complains that TBaseInterface is unused, but without it all the intellisense is lost since it's carrying it through the chain -class BaseQuery { +class BaseQuery { + protected query: string[] = []; constructor(query: string[]) { @@ -303,78 +304,80 @@ class BaseQuery { } -class QueryableFields extends BaseQuery { +class QueryableFields extends BaseQuery { constructor(q: string[]) { super(q); } - public text(internalName: KeysMatching): TextField { - return new TextField([...this.query, (internalName as string)]); + 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 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 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 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 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 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 lookup>(internalName: TKey): LookupQueryableFields { + return new LookupQueryableFields([...this.query], internalName as string); } - public lookupId>(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]); + 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 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 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)})`]); +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)})`]); + 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 { +class LookupQueryableFields extends BaseQuery { private LookupField: string; constructor(q: string[], LookupField: string) { super(q); @@ -400,7 +403,7 @@ class LookupQueryableFields extends BaseQuery extends BaseQuery { +class NullableField extends BaseQuery { protected LastIndex: number; protected InternalName: string; @@ -423,21 +426,21 @@ class NullableField extends BaseQuery extends NullableField { - public equals(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.Equals, this.toODataValue(value)]); +class ComparableField extends NullableField { + 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 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))); + public in(...values: TInputValueType[]): ComparisonResult { + return SPOData.Where().or(...values.map(x => this.equals(x))); } - public notIn(...values: TInputValueType[]): ComparisonResult { - return SPOData.Where().and(...values.map(x => this.notEquals(x))); + public notIn(...values: TInputValueType[]): ComparisonResult { + return SPOData.Where().and(...values.map(x => this.notEquals(x))); } } @@ -483,26 +486,26 @@ class BooleanField extends NullableField extends ComparableField { - public greaterThan(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.GreaterThan, this.toODataValue(value)]); +class NumericField extends ComparableField { + public greaterThan(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.GreaterThan, this.toODataValue(value)]); } - public greaterThanOrEquals(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.GreaterThanOrEqualTo, this.toODataValue(value)]); + public greaterThanOrEquals(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 lessThan(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.LessThan, this.toODataValue(value)]); } - public lessThanOrEquals(value: TInputValueType): ComparisonResult { - return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.toODataValue(value)]); + public lessThanOrEquals(value: TInputValueType): ComparisonResult { + return new ComparisonResult([...this.query, FilterOperation.LessThanOrEqualTo, this.toODataValue(value)]); } } -class NumberField extends NumericField { +class NumberField extends NumericField { protected override toODataValue(value: number): string { return `${value}`; } @@ -534,26 +537,26 @@ class DateField extends NumericField { } } -class ComparisonResult extends BaseQuery { - public and(): QueryableAndResult; +class ComparisonResult extends BaseQuery { + public and(): QueryableAndResult; // eslint-disable-next-line @typescript-eslint/semi - public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult // eslint-disable-next-line max-len - public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableAndResult) { + public and(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableAndResult) { if (queries == null || queries.length === 0) { - return new QueryableAndResult([...this.query, FilterJoinOperator.And]); + return new QueryableAndResult([...this.query, FilterJoinOperator.And]); } - return new ComparisonResult([...this.query, FilterJoinOperator.And, `(${queries.map(x => x.toString()).join(FilterJoinOperator.AndWithSpace)})`]); + 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(): QueryableOrResult; + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): ComparisonResult; // eslint-disable-next-line max-len - public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableOrResult) { + public or(...queries: (ComparisonResult | ((f: QueryableFields) => ComparisonResult))[]): (ComparisonResult | QueryableOrResult) { if (queries == null || queries.length === 0) { - return new QueryableOrResult([...this.query, FilterJoinOperator.Or]); + return new QueryableOrResult([...this.query, FilterJoinOperator.Or]); } - return new ComparisonResult([...this.query, FilterJoinOperator.Or, `(${queries.map(x => x.toString()).join(FilterJoinOperator.OrWithSpace)})`]); + return new ComparisonResult([...this.query, FilterJoinOperator.Or, `(${queries.map(x => x.toString()).join(FilterJoinOperator.OrWithSpace)})`]); } public toString(): string { From 5e128436c911fadeb4397b1332d73ae830b5839c Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 18 Nov 2024 10:12:47 -0500 Subject: [PATCH 30/31] update tsconfig.json --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index cca0157fd..5c26c456a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,7 @@ "lib": [ "ES2015", "dom", - "ES2017.Object", - "ES2015.Iterable" + "ES2017.Object" ], "baseUrl": ".", "rootDir": ".", From 636e5a8c03db2f125ab10037d10e64c337dc10fc Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 18 Nov 2024 10:52:02 -0500 Subject: [PATCH 31/31] 4.7.0 updates for release --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e73812c..cd8c0aabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 4.7.0 - 2024-Nov-18 + +- sp + - Introduces new filter lamda patterns as beta + +- graph + - Renamed OneNote Pages to OneNotePages + - Basic Pages API support as beta + - Site Open Extensions as beta + - Fixed #3136 for improving paging support for query params + +- queryable + - Introduced DebugHeaders behavior + ## 4.6.0 - 2024-Oct-14 - Only documentation and package updates diff --git a/package.json b/package.json index cdad6fd5b..2806c7e1d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@pnp/monorepo", "private": true, "type": "module", - "version": "4.6.0", + "version": "4.7.0", "description": "A JavaScript library for SharePoint & Graph development.", "devDependencies": { "@azure/identity": "4.4.1",