diff --git a/package-lock.json b/package-lock.json index f6e693a56..3b1bdd921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pnp/monorepo", - "version": "4.0.0", + "version": "4.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pnp/monorepo", - "version": "4.0.0", + "version": "4.0.1", "license": "MIT", "devDependencies": { "@azure/identity": "4.1.0", diff --git a/packages/graph/behaviors/prefer-async.ts b/packages/graph/behaviors/prefer-async.ts new file mode 100644 index 000000000..bf0e0af67 --- /dev/null +++ b/packages/graph/behaviors/prefer-async.ts @@ -0,0 +1,35 @@ +import { TimelinePipe, delay } from "@pnp/core"; +import { errorCheck, InjectHeaders } from "@pnp/queryable"; +import { GraphQueryable, _GraphQueryable, graphGet } from "../graphqueryable.js"; +import { RichLongRunningOperation } from "@microsoft/microsoft-graph-types"; + +export function PreferAsync(pollIntervalMs: number = 25000, maxPolls: number = 4): TimelinePipe { + return (instance: _GraphQueryable) => { + instance.using(InjectHeaders({ "Prefer": "respond-async" })); + instance.on.parse(errorCheck); + instance.on.parse(async function (url: URL, response: Response, result: any) { + if (response.status === 202) { + const opLocation = response.headers.get("Location"); + const opId = opLocation.split("/").at(-1); + + const statusQuery = GraphQueryable(instance, `operations/${opId}`); + for (let i = 0; i < maxPolls; i++) { + await delay(pollIntervalMs); + + const status = await statusQuery(); + if (status.status === 'succeeded') { + let resultEndpoint = status.resourceLocation.split("/").at(-1); + result = await graphGet(GraphQueryable(instance, resultEndpoint)); + } else if (status.status === 'failed') { + throw status.error; + } + } + throw new Error(`Timed out waiting for async operation after ${pollIntervalMs * maxPolls}ms`); + } + + return [url, response, result]; + }); + + return instance; + } +} \ No newline at end of file diff --git a/packages/graph/decorators.ts b/packages/graph/decorators.ts index 53ec5b0be..ce0287d60 100644 --- a/packages/graph/decorators.ts +++ b/packages/graph/decorators.ts @@ -162,6 +162,24 @@ export interface IGetById { getById(id: T): R; } +export function getItemAt(factory: (...args: any[]) => R) { + return function (target: T) { + // eslint-disable-next-line @typescript-eslint/ban-types + return class extends target { + public getItemAt(this: IGraphQueryable, index: number): R { + return factory(this, `itemAt(index=${index})`); + } + }; + }; +} +export interface IGetItemAt { + /** + * Get an item based on its position in the collection. + * @param index Index of the item to be retrieved. Zero-indexed. + */ + getItemAt(index: T): R; +} + /** * Adds the getByName method to a collection */ diff --git a/packages/graph/workbooks/decorators.ts b/packages/graph/workbooks/decorators.ts new file mode 100644 index 000000000..b5647a38c --- /dev/null +++ b/packages/graph/workbooks/decorators.ts @@ -0,0 +1,24 @@ +import { IGraphQueryable } from "../graphqueryable"; +import { IRange, Range } from "./types"; + +/** + * Adds the getRange method to the tagged class + */ +export function getRange() { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (target: T) { + + return class extends target { + public getRange(this: IGraphQueryable): IRange { + return Range(this, "range"); + } + }; + }; +} + +export interface IGetRange { + /** + * Get the range of cells contained by this element. + */ + getRange(): IRange; +} \ No newline at end of file diff --git a/packages/graph/workbooks/driveitem.ts b/packages/graph/workbooks/driveitem.ts new file mode 100644 index 000000000..c0d3dc39a --- /dev/null +++ b/packages/graph/workbooks/driveitem.ts @@ -0,0 +1,32 @@ +import { addProp, body, InjectHeaders } from "@pnp/queryable"; +import { _DriveItem } from "../files/types.js"; +import { IWorkbook, IWorkbookWithSession, Workbook, WorkbookWithSession } from "./types.js"; +import { graphPost, GraphQueryable } from "../graphqueryable.js"; +import { WorkbookSessionInfo } from "@microsoft/microsoft-graph-types"; + +declare module "../files/types.js" { + interface _DriveItem { + readonly workbook: IWorkbook; + getWorkbookSession(persistChanges: boolean): Promise; + } + interface DriveItem { + readonly workbook: IWorkbook; + getWorkbookSession(persistChanges: boolean): Promise; + } +} + +addProp(_DriveItem, "workbook", Workbook); +_DriveItem.prototype.getWorkbookSession = getWorkbookSession; + +export async function getWorkbookSession(this: _DriveItem, persistChanges: boolean): Promise { + const workbook = WorkbookWithSession(this); + const sessionResult = await graphPost( + GraphQueryable(workbook, 'createSession'), body({ + persistChanges + })); + + if (!sessionResult.id) throw new Error("createSession did not respond with a session ID"); + + workbook.using(InjectHeaders({ "workbook-session-id": sessionResult.id })); + return workbook; +} \ No newline at end of file diff --git a/packages/graph/workbooks/index.ts b/packages/graph/workbooks/index.ts new file mode 100644 index 000000000..1e4c45a51 --- /dev/null +++ b/packages/graph/workbooks/index.ts @@ -0,0 +1,20 @@ +import "./driveitem.js"; + +export { + IWorkbook, + IWorksheet, + IWorksheets, + IRange, + ITable, + ITables, + ITableRow, + ITableRows, + ITableColumn, + ITableColumns, + IRangeFormat, + IRangeBorder, + IRangeBorders, + IRangeFill, + IRangeFont, + IRangeFormatProtection +} from "./types.js"; \ No newline at end of file diff --git a/packages/graph/workbooks/types.ts b/packages/graph/workbooks/types.ts new file mode 100644 index 000000000..a69d39b84 --- /dev/null +++ b/packages/graph/workbooks/types.ts @@ -0,0 +1,302 @@ +import { updateable, IUpdateable, addable, getById, IAddable, IGetById, deleteable, IDeleteable, defaultPath, getItemAt, IGetItemAt } from "../decorators.js"; +import { _GraphCollection, graphInvokableFactory, _GraphInstance, GraphQueryable } from "../graphqueryable.js"; +import { + Workbook as WorkbookType, + WorkbookWorksheet as WorksheetType, + WorkbookTable as WorkbookTableType, + WorkbookTableRow as WorkbookTableRowType, + WorkbookTableColumn as WorkbookTableColumnType, + WorkbookRange as WorkbookRangeType, + WorkbookRangeFormat as WorkbookRangeFormatType, + WorkbookRangeBorder as WorkbookRangeBorderType, + WorkbookRangeFont as WorkbookRangeFontType, + WorkbookRangeFill as WorkbookRangeFillType, + WorkbookFormatProtection as WorkbookFormatProtectionType, + WorkbookTableSort as WorkbookTableSortType, + WorkbookSortField +} from "@microsoft/microsoft-graph-types"; +import { graphPost } from "@pnp/graph"; +import { getRange, IGetRange } from "./decorators.js"; +import { body } from "@pnp/queryable/index.js"; + +@defaultPath("workbook") +export class _Workbook extends _GraphInstance { + public get worksheets(): IWorksheets { + return Worksheets(this); + } + + public get tables(): ITables { + return Tables(this); + } +} +export interface IWorkbook extends _Workbook { } +export const Workbook = graphInvokableFactory(_Workbook); + +export class _WorkbookWithSession extends _Workbook { + public closeSession(): Promise { + return graphPost(GraphQueryable(this, "closeSession")); + } + + public refreshSession(): Promise { + return graphPost(GraphQueryable(this, "refreshSession")); + } +} +export interface IWorkbookWithSession extends _WorkbookWithSession { } +export const WorkbookWithSession = graphInvokableFactory(_WorkbookWithSession); + +@updateable() +@deleteable() +export class _Range extends _GraphInstance { + public get format(): IRangeFormat { + return RangeFormat(this); + } +} +export interface IRange extends _Range, IUpdateable, IDeleteable { } +export const Range = graphInvokableFactory(_Range); + +@updateable() +@defaultPath("format") +export class _RangeFormat extends _GraphInstance { + public get borders(): IRangeBorders { + return RangeBorders(this); + } + + public get font(): IRangeFont { + return RangeFont(this); + } + + public get fill(): IRangeFill { + return RangeFill(this); + } + + public get protection(): IRangeFormatProtection { + return RangeFormatProtection(this); + } + + public autofitColumns(): Promise { + return graphPost(GraphQueryable(this, "autofitColumns")); + } + + public autofitRows(): Promise { + return graphPost(GraphQueryable(this, "autofitRows")); + } +} + +export interface IRangeFormat extends _RangeFormat, IUpdateable { } +export const RangeFormat = graphInvokableFactory(_RangeFormat); + +@defaultPath("font") +@updateable() +export class _RangeFont extends _GraphInstance { } +export interface IRangeFont extends _RangeFont, IUpdateable { } +export const RangeFont = graphInvokableFactory(_RangeFont); + +@defaultPath("fill") +@updateable() +export class _RangeFill extends _GraphInstance { + public clear(): Promise { + return graphPost(GraphQueryable(this, "clear")); + } +} +export interface IRangeFill extends _RangeFill, IUpdateable { } +export const RangeFill = graphInvokableFactory(_RangeFill); + +@defaultPath("protection") +@updateable() +export class _RangeFormatProtection extends _GraphInstance { } + +export interface IRangeFormatProtection extends _RangeFormatProtection, IUpdateable { } +export const RangeFormatProtection = graphInvokableFactory(_RangeFormatProtection); + +@updateable() +export class _RangeBorder extends _GraphInstance { } +/** + * NOTE: When updating RangeBorder, there are some combinations of style + * and weight that silently fail. + * For example, setting "Dash - Thick" always sets "Continuous - Thick". + * This isn't documented, but it's also not really a bug. When you + * try to manually set border styles in Excel, it's not possible to select + * a thick dashed line. + */ +export interface IRangeBorder extends _RangeBorder, IUpdateable { } +export const RangeBorder = graphInvokableFactory(_RangeBorder); + +@defaultPath("borders") +// @addable() +/** + * NOTE: According the docs at https://learn.microsoft.com/en-us/graph/api/rangeformat-post-borders, + * you should be able to POST new border styles. In my testing, this fails with MethodNotAllowed + * Using `RangeBorder.update()` works instead, even for borders that haven't been "created" yet. + */ +@getItemAt(RangeBorder) +export class _RangeBorders extends _GraphCollection { + public getBySideIndex(sideIndex: RangeBorderSideIndex) { + return RangeBorder(this, sideIndex); + } +} +export interface IRangeBorders extends _RangeBorders, IGetItemAt { } +export const RangeBorders = graphInvokableFactory(_RangeBorders); +export type RangeBorderSideIndex = 'EdgeTop' | 'EdgeBottom' | 'EdgeLeft' | 'EdgeRight' | + 'InsideVertical' | 'InsideHorizontal' | 'DiagonalDown' | + 'DiagonalUp'; + + + +@updateable() +@deleteable() +export class _Worksheet extends _GraphInstance { + /** + * Get a range of cells within the worksheet. + * + * @param address (Optional) An A1-notation address of a range within this worksheet. + * If omitted, a range containing the entire worksheet is returned. + */ + public getRange(address?: string): IRange { + if (address) { + return Range(this, `range(address='${address}')`); + } else { + return Range(this, "range"); + } + } + + public get tables(): ITables { + return Tables(this); + } +} +export interface IWorksheet extends _Worksheet, IUpdateable, IDeleteable { } +export const Worksheet = graphInvokableFactory(_Worksheet); + +@defaultPath("worksheets") +@addable() +@getById(Worksheet) +export class _Worksheets extends _GraphCollection { +} +export interface IWorksheets extends _Worksheets, IAddable, IGetById { } +export const Worksheets = graphInvokableFactory(_Worksheets); + +@getRange() +@updateable() +@deleteable() +export class _Table extends _GraphInstance { + public get rows(): ITableRows { + return TableRows(this); + } + + public get columns(): ITableColumns { + return TableColumns(this); + } + + public get worksheet(): IWorksheet { + return Worksheet(this, "worksheet"); + } + + public get range(): IRange { + return Range(this, "range"); + } + + public get headerRowRange(): IRange { + return Range(this, "headerRowRange"); + } + + public get dataBodyRange(): IRange { + return Range(this, "dataBodyRange"); + } + + public get totalRowRange(): IRange { + return Range(this, "totalRowRange"); + } + + public get sort(): ITableSort { + return TableSort(this); + } + + public clearFilters() { + return graphPost(GraphQueryable(this, "clearFilters")); + } + + public reapplyFilters() { + return graphPost(GraphQueryable(this, "reapplyFilters")); + } + + public convertToRange(): Promise { + return graphPost(GraphQueryable(this, "convertToRange")); + } +} +export interface ITable extends _Table, IUpdateable, IDeleteable, IGetRange { } +export const Table = graphInvokableFactory(_Table); + +@defaultPath("tables") +@getById(Table) +export class _Tables extends _GraphCollection { + public getByName(name: string): ITable { + return Table(this, name); + } + + public async add(address: string, hasHeaders: boolean): Promise { + return graphPost(GraphQueryable(this, "add"), body({ address, hasHeaders })); + } +} +export interface ITables extends _Tables, IGetById { } +export const Tables = graphInvokableFactory(_Tables); + +@getRange() +@deleteable() +@updateable() +export class _TableRow extends _GraphInstance { + +} +export interface ITableRow extends _TableRow, IUpdateable, IDeleteable, IGetRange { } +export const TableRow = graphInvokableFactory(_TableRow); + +@defaultPath("rows") +@addable() +@getItemAt(TableRow) +export class _TableRows extends _GraphCollection { + public getByIndex(index: number): ITableRow { + /** + * NOTE: Although documented, this doesn't work for me. + * Returns 400 with code ApiNotFound. + */ + return TableRow(this, `${index}`); + } +} +export interface ITableRows extends _TableRows, IAddable, IGetItemAt { } +export const TableRows = graphInvokableFactory(_TableRows); + +@getRange() +@deleteable() +@updateable() +export class _TableColumn extends _GraphInstance { + +} +export interface ITableColumn extends _TableColumn, IUpdateable, IDeleteable, IGetRange { } +export const TableColumn = graphInvokableFactory(_TableColumn); + +@defaultPath("columns") +@addable() +@getById(TableColumn) +export class _TableColumns extends _GraphCollection { + public getByName(name: string): ITableColumn { + return TableColumn(this, name); + } +} +export interface ITableColumns extends _TableColumns, IAddable { } +export const TableColumns = graphInvokableFactory(_TableColumns); + +@defaultPath("sort") +export class _TableSort extends _GraphInstance { + public apply(fields: WorkbookSortField[], matchCase?: boolean, method?: string): Promise { + return graphPost(GraphQueryable(this, "apply"), body({ fields, matchCase, method })); + } + + public clear(): Promise { + return graphPost(GraphQueryable(this, "clear")); + } + + public reapply(): Promise { + return graphPost(GraphQueryable(this, "reapply")); + } +} + +export interface ITableSort extends _TableSort {} +export const TableSort = graphInvokableFactory(_TableSort);