diff --git a/.gitignore b/.gitignore index 5fdd75b61..e8cbbf3f3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ coverage # node-waf configuration .lock-wscript +.test-recording + # Dependency directory node_modules diff --git a/debug/launch/v3-patrick.ts b/debug/launch/v3-patrick.ts index 6395ae375..9cbe9b773 100644 --- a/debug/launch/v3-patrick.ts +++ b/debug/launch/v3-patrick.ts @@ -7,6 +7,10 @@ import "@pnp/sp/lists"; import "@pnp/sp/files"; import "@pnp/sp/folders"; import "@pnp/sp/appcatalog"; +import { Web } from "@pnp/sp/webs"; +import { AssignFrom } from "@pnp/core"; +import { RequestRecorderCache } from "../../test/test-recorder.js"; +import { join } from "path"; declare var process: { exit(code?: number): void }; @@ -43,16 +47,20 @@ export async function Example(settings: ITestingSettings) { try { + const recordingPath = join("C:/github/@pnp-fork", ".test-recording"); + const sp2 = spfi("https://318studios.sharepoint.com/sites/dev").using(SPDefault({ msal: { config: settings.testing.sp.msal.init, scopes: settings.testing.sp.msal.scopes, }, - })).using(PnPLogging(LogLevel.Verbose)); + })).using(PnPLogging(LogLevel.Verbose)).using(RequestRecorderCache(recordingPath, "record", () => false)); const web = await sp2.getTenantAppCatalogWeb(); - const y = await web(); + const web2 = Web("https://318studios.sharepoint.com/sites/dev").using(AssignFrom(web)); + + const y = await web2(); console.log(JSON.stringify(y)); @@ -61,8 +69,6 @@ export async function Example(settings: ITestingSettings) { console.error(e); } - console.log("here"); - // const [batchedSP, execute] = sp2.batched(); // let res = []; diff --git a/package-lock.json b/package-lock.json index 67c2db293..aaa2b9d29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2591,6 +2591,15 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "dev": true, + "requires": { + "stackframe": "^1.1.1" + } + }, "es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -7642,12 +7651,56 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "stack-generator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", + "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", + "dev": true, + "requires": { + "stackframe": "^1.1.1" + } + }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, + "stackframe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", + "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==", + "dev": true + }, + "stacktrace-gps": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz", + "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", + "dev": true, + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.1.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + } + } + }, + "stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dev": true, + "requires": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", diff --git a/package.json b/package.json index 499277912..2c9266950 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node-abort-controller": "^3.0.1", "node-fetch": "^2.6.1", "prettyjson": "^1.2.1", + "stacktrace-js": "^2.0.2", "string-replace-loader": "^3.0.1", "tslib": "^2.1.0", "typescript": "^4.2.3", diff --git a/packages/core/timeline.ts b/packages/core/timeline.ts index b107b2a26..ea6b2124d 100644 --- a/packages/core/timeline.ts +++ b/packages/core/timeline.ts @@ -80,7 +80,7 @@ export abstract class Timeline { private _onProxy: typeof Proxy | null = null; private _emitProxy: typeof Proxy | null = null; - private _inheritingObservers: boolean; + protected _inheritingObservers: boolean; constructor(protected readonly moments: T, protected observers: ObserverCollection = {}) { this._inheritingObservers = true; diff --git a/packages/sp/appcatalog/index.ts b/packages/sp/appcatalog/index.ts index 40d01f367..d18ebbba4 100644 --- a/packages/sp/appcatalog/index.ts +++ b/packages/sp/appcatalog/index.ts @@ -18,16 +18,10 @@ declare module "../fi" { SPFI.prototype.getTenantAppCatalogWeb = async function (this: SPFI): Promise { - return this.create(async (q) => { - const data: { CorporateCatalogUrl: string } = await Web(q.toUrl().replace(/\/_api\/.*$/i, ""), "/_api/SP_TenantSettings_Current").using(AssignFrom(q))(); - - console.log(data); + const data = await Web(q.toUrl().replace(/\/_api\/.*$/i, ""), "/_api/SP_TenantSettings_Current").using(AssignFrom(q))<{ CorporateCatalogUrl: string }>(); return Web(data.CorporateCatalogUrl).using(AssignFrom(q)); - }); - - return null; }; diff --git a/packages/sp/appcatalog/web.ts b/packages/sp/appcatalog/web.ts index cb4ec5f62..ebf5bcbfb 100644 --- a/packages/sp/appcatalog/web.ts +++ b/packages/sp/appcatalog/web.ts @@ -1,21 +1,17 @@ +import { addProp } from "@pnp/queryable/add-prop.js"; import { _Web } from "../webs/types.js"; import { AppCatalog, IAppCatalog } from "./types.js"; declare module "../webs/types" { interface _Web { - getAppCatalog(url?: string | _Web): IAppCatalog; + appcatalog: IAppCatalog; } interface IWeb { /** - * Gets this web (default) or the web specifed by the optional string case - * as an IAppCatalog instance - * - * @param url [Optional] Url of the web to get (default: current web) + * Gets the appcatalog (if it exists associated with this web) */ - getAppCatalog(url?: string | _Web): IAppCatalog; + appcatalog: IAppCatalog; } } -_Web.prototype.getAppCatalog = function (this: _Web, url?: string | _Web): IAppCatalog { - return AppCatalog(url || this); -}; +addProp(_Web, "appcatalog", AppCatalog); diff --git a/packages/sp/batching.ts b/packages/sp/batching.ts index 721046101..b45f9e512 100644 --- a/packages/sp/batching.ts +++ b/packages/sp/batching.ts @@ -1,4 +1,4 @@ -import { getGUID, isUrlAbsolute, combine, CopyFrom, TimelinePipe } from "@pnp/core"; +import { getGUID, isUrlAbsolute, combine, CopyFrom, TimelinePipe, isFunc } from "@pnp/core"; import { InjectHeaders, IQueryableInternal, parseBinderWithErrorCheck, Queryable } from "@pnp/queryable"; import { spPost } from "./operations"; import { _SPQueryable } from "./spqueryable"; @@ -232,12 +232,17 @@ export function createBatch(base: IQueryableInternal): [TimelinePipe, () => Prom // we need to know when each request in the batch's timeline has completed instance.on.dispose(function () { - // let things know we are done with this request - (this)[RequestCompleteSym](); + if (isFunc((this)[RequestCompleteSym])) { - // remove the symbol props we added for good hygene - delete this[RegistrationCompleteSym]; - delete this[RequestCompleteSym]; + // let things know we are done with this request + (this)[RequestCompleteSym](); + delete this[RequestCompleteSym]; + } + + if (isFunc((this)[RegistrationCompleteSym])) { + // remove the symbol props we added for good hygene + delete this[RegistrationCompleteSym]; + } }); return instance; diff --git a/test/main.ts b/test/main.ts index 6180951f5..f7bf60e43 100644 --- a/test/main.ts +++ b/test/main.ts @@ -12,6 +12,8 @@ import "@pnp/sp/webs"; import { IWeb, IWebInfo } from "@pnp/sp/webs"; import { graphfi, GraphFI } from "@pnp/graph"; import { LogLevel } from "@pnp/logging"; +import { RequestRecorderCache } from "./test-recorder.js"; +import { join } from "path"; chai.use(chaiAsPromised); @@ -198,7 +200,7 @@ async function spTestSetup(ts: ISettings): Promise { config: settings.testing.sp.msal.init, scopes: settings.testing.sp.msal.scopes, }, - })).using(TestLogging()); + })).using(TestLogging()); // .using(RequestRecorderCache(join("C:/github/@pnp-fork", ".test-recording"), "record", () => false)); } async function graphTestSetup(): Promise { @@ -207,7 +209,7 @@ async function graphTestSetup(): Promise { config: settings.testing.graph.msal.init, scopes: settings.testing.graph.msal.scopes, }, - })).using(TestLogging()); + })).using(TestLogging()); // .using(RequestRecorderCache(join("C:/github/@pnp-fork", ".test-recording"), "record", () => false)); } export const testSettings: ISettings = settings.testing; diff --git a/test/sp/appcatalog.ts b/test/sp/appcatalog.ts index 02f867c41..4a78c163f 100644 --- a/test/sp/appcatalog.ts +++ b/test/sp/appcatalog.ts @@ -1,5 +1,5 @@ -import { getRandomString } from "@pnp/core"; +import { getRandomString, delay } from "@pnp/core"; import { expect } from "chai"; import { getSP, testSettings } from "../main.js"; import { IAppCatalog } from "@pnp/sp/appcatalog"; @@ -11,10 +11,6 @@ import * as path from "path"; import findupSync = require("findup-sync"); import { SPFI } from "@pnp/sp"; -const sleep = (ms: number) => new Promise(r => setTimeout(() => { - r(); -}, ms)); - // give ourselves a single reference to the projectRoot const projectRoot = path.resolve(path.dirname(findupSync("package.json"))); @@ -32,7 +28,7 @@ describe.skip("AppCatalog", function () { before(function () { _spfi = getSP(); // appCatWeb = await sp.getTenantAppCatalogWeb(); - appCatalog = _spfi.web.getAppCatalog(); + appCatalog = _spfi.web.appcatalog; // return Promise.resolve(); }); @@ -64,14 +60,14 @@ describe.skip("AppCatalog", function () { }); it("it installs an app on a web", async function () { - const myApp = _spfi.web.getAppCatalog().getAppById(appId); + const myApp = _spfi.web.appcatalog.getAppById(appId); return expect(myApp.install(), `app '${appId}' should've been installed on web ${testSettings.sp.webUrl}`).to.eventually.be.fulfilled; }); it("it uninstalls an app", async function () { // We have to make sure the app is installed before we can uninstall it otherwise we get the following error message: // Another job exists for this app instance. Please retry after that job is done. - const myApp = _spfi.web.getAppCatalog().getAppById(appId); + const myApp = _spfi.web.appcatalog.getAppById(appId); let app = { InstalledVersion: "" }; let retryCount = 0; @@ -79,7 +75,7 @@ describe.skip("AppCatalog", function () { if (retryCount === 5) { break; } - await sleep(10000); // Sleep for 10 seconds + await delay(10000); // Sleep for 10 seconds app = await myApp(); retryCount++; } while (app.InstalledVersion === ""); @@ -88,7 +84,7 @@ describe.skip("AppCatalog", function () { }); it("it upgrades an app", async function () { - const myApp = _spfi.web.getAppCatalog().getAppById(appId); + const myApp = _spfi.web.appcatalog.getAppById(appId); return expect(myApp.upgrade(), `app '${appId}' should've been upgraded on web ${testSettings.sp.webUrl}`).to.eventually.be.fulfilled; }); diff --git a/test/test-recorder.ts b/test/test-recorder.ts new file mode 100644 index 000000000..4a3ecc5ee --- /dev/null +++ b/test/test-recorder.ts @@ -0,0 +1,58 @@ +import { isFunc, TimelinePipe, dateAdd, getHashCode } from "@pnp/core"; +import { Queryable } from "@pnp/queryable"; +import { statSync, readFileSync, existsSync, writeFileSync, mkdirSync } from "fs"; +import { join, basename } from "path"; +import { getSync } from "stacktrace-js"; + +export function RequestRecorderCache(resolvedRecordingPath: string, mode: "readonly" | "record", isExpired?: (Date) => boolean): TimelinePipe { + + const today = new Date(); + const _isExpired = isFunc(isExpired) ? isExpired : (d: Date) => dateAdd(d, "week", 2) < today; + const recorderFileKey = Symbol.for("recorder_file_key"); + const recorderFilePath = Symbol.for("recorder_file_path"); + + if (!existsSync(resolvedRecordingPath)) { + mkdirSync(resolvedRecordingPath); + } + + return (instance: Queryable) => { + + instance.on.pre(async function (this: Queryable, url: string, init: RequestInit, result: any): Promise<[string, RequestInit, any]> { + + const stack = getSync(); + + this[recorderFileKey] = getHashCode(`${init.method}:${url}:${basename(stack[0].fileName)}:${stack[0].lineNumber}:${stack[0].columnNumber}`).toString(); + this[recorderFilePath] = join(resolvedRecordingPath, `result.${this[recorderFileKey]}.json`); + + if (existsSync(this[recorderFilePath])) { + + const stats = statSync(this[recorderFilePath]); + if (!_isExpired(stats.mtime)) { + result = JSON.parse(readFileSync(this[recorderFilePath]).toString()); + return [url, init, result]; + } + } + + if (mode === "record") { + + this.on.post(async function (url: URL, result: any) { + + if (Reflect.has(this, recorderFilePath)) { + writeFileSync(this[recorderFilePath], JSON.stringify(result)); + } + + return [url, result]; + }); + } + + return [url, init, result]; + }); + + instance.on.dispose(function () { + delete this[recorderFileKey]; + delete this[recorderFilePath]; + }); + + return instance; + }; +} diff --git a/~status.md b/~status.md index 517bbc869..4e649b09f 100644 --- a/~status.md +++ b/~status.md @@ -3,6 +3,7 @@ // TODO:: do we want to move to .env files, seems to be a sorta "norm" folks are using? // TODO:: maintain an experimental release // TODO:: need to update our /samples and maybe more? remove rollup sample +// TODO:: kebab-case all file names ## experiments: