From c7a6deca5a3b5f3884d1d094ff16a3178e29655d Mon Sep 17 00:00:00 2001 From: Patrick Rodgers Date: Mon, 6 May 2019 11:35:03 -0400 Subject: [PATCH] v2 initial commit --- .github/ISSUE_TEMPLATE.md | 39 + .github/PULL_REQUEST_TEMPLATE.md | 20 + .gitignore | 56 + .travis.yml | 12 + .vscode/launch.json | 40 + .vscode/settings.json | 15 + .vscode/tasks.json | 40 + AUTHORS | 22 + CHANGELOG.md | 41 + LICENSE | 25 + README.md | 79 + banner.js | 13 + debug/launch/graph.ts | 33 + debug/launch/main.ts | 29 + debug/launch/sp.ts | 41 + debug/launch/tsconfig.json | 34 + debug/serve/main.ts | 37 + debug/serve/tsconfig.json | 33 + debug/serve/webpack.config.js | 46 + docs/custom-package.md | 1 + docs/ie11-mode.md | 1 + docs/index.md | 56 + docs/invokable.md | 1 + docs/package-structure.md | 1 + docs/selective-imports.md | 1 + docs/sp/batching.md | 1 + docs/sp/webs.md | 790 +++++ gulpfile.js | 1 + mkdocs.yml | 131 + package.json | 83 + packages/common/__tests/collections.test.ts | 61 + packages/common/__tests/mock-storage.ts | 34 + packages/common/__tests/storage.test.ts | 37 + packages/common/__tests/util.test.ts | 202 ++ packages/common/docs/adalclient.md | 169 + packages/common/docs/collections.md | 34 + packages/common/docs/custom-httpclientimpl.md | 65 + packages/common/docs/index.md | 34 + packages/common/docs/libconfig.md | 156 + packages/common/docs/netutil.md | 48 + packages/common/docs/storage.md | 88 + packages/common/docs/util.md | 198 ++ packages/common/index.ts | 1 + packages/common/package.json | 27 + packages/common/src/adalclient.ts | 226 ++ packages/common/src/collections.ts | 46 + packages/common/src/common.ts | 7 + packages/common/src/libconfig.ts | 118 + packages/common/src/netutil.ts | 86 + packages/common/src/spfxcontextinterface.ts | 29 + packages/common/src/storage.ts | 310 ++ packages/common/src/util.ts | 242 ++ packages/common/tsconfig.es5.json | 8 + packages/common/tsconfig.json | 8 + .../__tests/configuration.test.ts | 112 + .../__tests/mock-configurationprovider.ts | 23 + packages/config-store/__tests/mock-storage.ts | 34 + .../cachingConfigurationProvider.test.ts | 71 + .../spListConfigurationProvider.test.ts | 93 + packages/config-store/docs/configuration.md | 45 + packages/config-store/docs/index.md | 21 + packages/config-store/docs/providers.md | 43 + packages/config-store/index.ts | 1 + packages/config-store/package.json | 27 + packages/config-store/src/configstore.ts | 2 + packages/config-store/src/configuration.ts | 103 + .../providers/cachingConfigurationProvider.ts | 62 + packages/config-store/src/providers/index.ts | 7 + .../providers/spListConfigurationProvider.ts | 43 + packages/config-store/tsconfig.es5.json | 29 + packages/config-store/tsconfig.json | 26 + .../documentation/SPFx-On-Premesis-2016.md | 38 + packages/documentation/beta-versions.md | 15 + packages/documentation/css/extra.css | 33 + packages/documentation/debugging.md | 164 + packages/documentation/deployment.md | 126 + packages/documentation/documentation.md | 22 + packages/documentation/getting-started-dev.md | 42 + packages/documentation/getting-started.md | 216 ++ packages/documentation/gulp-commands.md | 162 + packages/documentation/img/Logo.png | Bin 0 -> 3151 bytes .../documentation/img/PnPJS_FluentAPI.gif | Bin 0 -> 707717 bytes .../img/SPFx-On-Premesis-2016-1.png | Bin 0 -> 46965 bytes .../img/office365-header-icon.png | Bin 0 -> 312 bytes .../documentation/img/pnpjs-common-uml.svg | 220 ++ .../img/pnpjs-config-store-uml.svg | 56 + .../documentation/img/pnpjs-graph-uml.svg | 602 ++++ .../documentation/img/pnpjs-logging-uml.svg | 59 + .../documentation/img/pnpjs-nodejs-uml.svg | 85 + .../documentation/img/pnpjs-odata-uml.svg | 253 ++ .../img/pnpjs-sp-addinhelpers-uml.svg | 48 + .../img/pnpjs-sp-clientsvc-uml.svg | 167 + .../img/pnpjs-sp-taxonomy-uml.svg | 458 +++ packages/documentation/img/pnpjs-sp-uml.svg | 2833 +++++++++++++++++ packages/documentation/package-structure.md | 43 + packages/documentation/packages.md | 19 + packages/documentation/polyfill.md | 77 + packages/documentation/theme/main.html | 5 + packages/documentation/transition-guide.md | 102 + packages/graph/docs/contacts.md | 197 ++ packages/graph/docs/directoryobjects.md | 71 + packages/graph/docs/index.md | 99 + packages/graph/docs/invitations.md | 15 + packages/graph/docs/onedrive.md | 204 ++ packages/graph/docs/planner.md | 194 ++ packages/graph/docs/subscriptions.md | 63 + packages/graph/docs/teams.md | 166 + packages/graph/index.ts | 1 + packages/graph/package.json | 28 + packages/graph/presets/all.ts | 33 + .../graph/src/attachments/conversations.ts | 14 + packages/graph/src/attachments/index.ts | 8 + packages/graph/src/attachments/types.ts | 41 + packages/graph/src/batch.ts | 204 ++ packages/graph/src/calendars/groups.ts | 17 + packages/graph/src/calendars/index.ts | 13 + packages/graph/src/calendars/types.ts | 76 + packages/graph/src/config/graphlibconfig.ts | 52 + packages/graph/src/contacts/index.ts | 14 + packages/graph/src/contacts/types.ts | 137 + packages/graph/src/contacts/users.ts | 17 + packages/graph/src/conversations/groups.ts | 20 + packages/graph/src/conversations/index.ts | 19 + packages/graph/src/conversations/types.ts | 185 ++ packages/graph/src/decorators.ts | 110 + packages/graph/src/directory-objects/index.ts | 24 + packages/graph/src/directory-objects/types.ts | 92 + packages/graph/src/graph.ts | 24 + packages/graph/src/graphqueryable.ts | 294 ++ packages/graph/src/groups/index.ts | 25 + packages/graph/src/groups/types.ts | 149 + packages/graph/src/invitations/index.ts | 22 + packages/graph/src/invitations/types.ts | 41 + packages/graph/src/members/groups.ts | 17 + packages/graph/src/members/index.ts | 8 + packages/graph/src/members/types.ts | 42 + packages/graph/src/messages/index.ts | 14 + packages/graph/src/messages/types.ts | 52 + packages/graph/src/messages/users.ts | 27 + packages/graph/src/net/graphhttpclient.ts | 114 + packages/graph/src/onedrive/index.ts | 16 + packages/graph/src/onedrive/types.ts | 164 + packages/graph/src/onedrive/users.ts | 17 + packages/graph/src/onenote/index.ts | 16 + packages/graph/src/onenote/types.ts | 135 + packages/graph/src/onenote/users.ts | 14 + packages/graph/src/operations.ts | 33 + packages/graph/src/photos/groups.ts | 14 + packages/graph/src/photos/index.ts | 6 + packages/graph/src/photos/types.ts | 38 + packages/graph/src/planner/groups.ts | 14 + packages/graph/src/planner/index.ts | 39 + packages/graph/src/planner/types.ts | 207 ++ packages/graph/src/planner/users.ts | 14 + packages/graph/src/rest.ts | 19 + packages/graph/src/subscriptions/index.ts | 24 + packages/graph/src/subscriptions/types.ts | 63 + packages/graph/src/teams/index.ts | 66 + packages/graph/src/teams/types.ts | 238 ++ packages/graph/src/teams/users.ts | 14 + packages/graph/src/types.ts | 17 + packages/graph/src/users/index.ts | 32 + packages/graph/src/users/types.ts | 37 + packages/graph/src/utils/type.ts | 3 + packages/graph/tsconfig.es5.json | 30 + packages/graph/tsconfig.json | 30 + packages/index.md | 56 + packages/logging/__tests/logging.test.ts | 68 + packages/logging/docs/index.md | 174 + packages/logging/index.ts | 1 + packages/logging/package.json | 22 + packages/logging/src/listeners.ts | 74 + packages/logging/src/logger.ts | 118 + packages/logging/src/logging.ts | 3 + packages/logging/src/types.ts | 42 + packages/logging/tsconfig.es5.json | 8 + packages/logging/tsconfig.json | 8 + packages/nodejs/docs/adal-fetch-client.md | 28 + .../nodejs/docs/bearer-token-fetch-client.md | 27 + packages/nodejs/docs/index.md | 22 + packages/nodejs/docs/provider-hosted-app.md | 43 + packages/nodejs/docs/sp-fetch-client.md | 106 + packages/nodejs/index.ts | 1 + packages/nodejs/package.json | 29 + packages/nodejs/src/net/adalfetchclient.ts | 63 + .../nodejs/src/net/bearertokenfetchclient.ts | 33 + packages/nodejs/src/net/index.ts | 4 + packages/nodejs/src/net/nodefetchclient.ts | 122 + packages/nodejs/src/net/spfetchclient.ts | 86 + packages/nodejs/src/nodejs.ts | 22 + packages/nodejs/src/providerhosted.ts | 32 + packages/nodejs/src/sptokenutils.ts | 115 + packages/nodejs/src/types.ts | 38 + packages/nodejs/tsconfig.es5.json | 19 + packages/nodejs/tsconfig.json | 19 + packages/odata/docs/caching.md | 163 + packages/odata/docs/core.md | 52 + packages/odata/docs/index.md | 27 + packages/odata/docs/odata-batch.md | 13 + packages/odata/docs/parsers.md | 74 + packages/odata/docs/pipeline.md | 57 + packages/odata/docs/queryable.md | 79 + packages/odata/index.ts | 1 + packages/odata/package.json | 26 + packages/odata/src/batch.ts | 108 + packages/odata/src/caching.ts | 42 + packages/odata/src/errors.ts | 13 + packages/odata/src/extenders.ts | 10 + packages/odata/src/invokable.ts | 57 + packages/odata/src/odata.ts | 10 + packages/odata/src/operation-binder.ts | 63 + packages/odata/src/parsers.ts | 110 + packages/odata/src/pipeline.ts | 263 ++ packages/odata/src/queryable.ts | 309 ++ packages/odata/src/request-builders.ts | 9 + packages/odata/tsconfig.es5.json | 15 + packages/odata/tsconfig.json | 15 + packages/pnpjs/docs/index.md | 59 + packages/pnpjs/index.ts | 5 + packages/pnpjs/package.json | 29 + packages/pnpjs/src/config/pnplibconfig.ts | 9 + packages/pnpjs/src/pnpjs.ts | 125 + packages/pnpjs/tsconfig.es5.json | 49 + packages/pnpjs/tsconfig.json | 49 + packages/readme.md | 22 + packages/sp-addinhelpers/docs/index.md | 46 + .../docs/sp-request-executor-client.md | 32 + .../sp-addinhelpers/docs/sp-rest-addin.md | 26 + packages/sp-addinhelpers/index.ts | 1 + packages/sp-addinhelpers/package.json | 27 + packages/sp-addinhelpers/src/addinhelpers.ts | 2 + .../src/sprequestexecutorclient.ts | 79 + packages/sp-addinhelpers/src/sprestaddin.ts | 67 + packages/sp-addinhelpers/tsconfig.es5.json | 28 + packages/sp-addinhelpers/tsconfig.json | 28 + packages/sp-clientsvc/docs/index.md | 8 + packages/sp-clientsvc/index.ts | 1 + packages/sp-clientsvc/package.json | 28 + packages/sp-clientsvc/src/batch.ts | 196 ++ packages/sp-clientsvc/src/clientsvc.ts | 8 + .../sp-clientsvc/src/clintsvcqueryable.ts | 398 +++ packages/sp-clientsvc/src/objectpath.ts | 316 ++ packages/sp-clientsvc/src/opactionbuilders.ts | 99 + packages/sp-clientsvc/src/opbuilders.ts | 97 + packages/sp-clientsvc/src/parsers.ts | 101 + packages/sp-clientsvc/src/types.ts | 1 + packages/sp-clientsvc/src/utils.ts | 29 + packages/sp-clientsvc/tsconfig.es5.json | 32 + packages/sp-clientsvc/tsconfig.json | 32 + packages/sp-taxonomy/__tests/batch.test.ts | 78 + packages/sp-taxonomy/__tests/session.test.ts | 60 + .../sp-taxonomy/__tests/termstore.test.ts | 111 + packages/sp-taxonomy/docs/index.md | 74 + packages/sp-taxonomy/docs/labels.md | 49 + packages/sp-taxonomy/docs/term-groups.md | 74 + packages/sp-taxonomy/docs/term-sets.md | 123 + packages/sp-taxonomy/docs/term-stores.md | 204 ++ packages/sp-taxonomy/docs/terms.md | 176 + packages/sp-taxonomy/docs/utilities.md | 47 + packages/sp-taxonomy/index.ts | 1 + packages/sp-taxonomy/package.json | 29 + packages/sp-taxonomy/src/labels.ts | 122 + packages/sp-taxonomy/src/session.ts | 88 + packages/sp-taxonomy/src/taxonomy.ts | 13 + packages/sp-taxonomy/src/termgroup.ts | 195 ++ packages/sp-taxonomy/src/terms.ts | 231 ++ packages/sp-taxonomy/src/termsets.ts | 183 ++ packages/sp-taxonomy/src/termstores.ts | 392 +++ packages/sp-taxonomy/src/types.ts | 90 + packages/sp-taxonomy/src/utilities.ts | 35 + packages/sp-taxonomy/tsconfig.es5.json | 37 + packages/sp-taxonomy/tsconfig.json | 37 + packages/sp/__tests/alias.test.ts | 58 + packages/sp/__tests/batch.test.ts | 109 + packages/sp/__tests/clientsidepages.test.ts | 92 + packages/sp/__tests/configure.test.ts | 185 ++ packages/sp/__tests/contenttypes.test.ts | 70 + packages/sp/__tests/errors.test.ts | 51 + packages/sp/__tests/fields.test.ts | 53 + packages/sp/__tests/files.test.ts | 93 + packages/sp/__tests/folders.test.ts | 94 + packages/sp/__tests/items.test.ts | 101 + packages/sp/__tests/lists.test.ts | 352 ++ packages/sp/__tests/mock-fetchclient.ts | 24 + packages/sp/__tests/navigation.test.ts | 16 + packages/sp/__tests/roles.test.ts | 85 + packages/sp/__tests/search.test.ts | 9 + packages/sp/__tests/sharing.inactive.ts | 348 ++ packages/sp/__tests/site.test.ts | 57 + packages/sp/__tests/sitegroups.test.ts | 59 + packages/sp/__tests/siteusers.test.ts | 68 + packages/sp/__tests/subscriptions.test.ts | 102 + packages/sp/__tests/utilities.test.ts | 33 + packages/sp/__tests/utils.ts | 11 + packages/sp/__tests/views.test.ts | 24 + packages/sp/docs/alias-parameters.md | 66 + packages/sp/docs/alm.md | 120 + packages/sp/docs/attachments.md | 173 + packages/sp/docs/client-side-pages.md | 220 ++ packages/sp/docs/comments-likes.md | 117 + packages/sp/docs/content-types.md | 28 + packages/sp/docs/entity-merging.md | 66 + packages/sp/docs/features.md | 70 + packages/sp/docs/fields.md | 205 ++ packages/sp/docs/files.md | 269 ++ packages/sp/docs/index.md | 121 + packages/sp/docs/items.md | 382 +++ packages/sp/docs/navigation-service.md | 53 + packages/sp/docs/permissions.md | 85 + packages/sp/docs/profiles.md | 125 + packages/sp/docs/related-items.md | 96 + packages/sp/docs/search.md | 157 + packages/sp/docs/sharing.md | 230 ++ packages/sp/docs/sitedesigns.md | 118 + packages/sp/docs/sites.md | 222 ++ packages/sp/docs/social.md | 150 + packages/sp/docs/sp-utilities-utility.md | 187 ++ packages/sp/docs/tenant-properties.md | 43 + packages/sp/docs/views.md | 91 + packages/sp/index.ts | 1 + packages/sp/package.json | 28 + packages/sp/presets/all.ts | 70 + packages/sp/presets/core.ts | 14 + packages/sp/src/appcatalog/index.ts | 27 + packages/sp/src/appcatalog/types.ts | 128 + packages/sp/src/appcatalog/web.ts | 21 + packages/sp/src/attachments/index.ts | 11 + packages/sp/src/attachments/item.ts | 17 + packages/sp/src/attachments/types.ts | 172 + packages/sp/src/batch.ts | 231 ++ packages/sp/src/clientsidepages/funcs.ts | 178 ++ packages/sp/src/clientsidepages/index.ts | 23 + packages/sp/src/clientsidepages/types.ts | 985 ++++++ packages/sp/src/clientsidepages/web.ts | 35 + packages/sp/src/comments/index.ts | 13 + packages/sp/src/comments/item.ts | 17 + packages/sp/src/comments/types.ts | 171 + packages/sp/src/config/splibconfig.ts | 72 + packages/sp/src/content-types/index.ts | 15 + packages/sp/src/content-types/item.ts | 17 + packages/sp/src/content-types/list.ts | 17 + packages/sp/src/content-types/types.ts | 160 + packages/sp/src/content-types/web.ts | 18 + packages/sp/src/decorators.ts | 119 + packages/sp/src/features/index.ts | 10 + packages/sp/src/features/site.ts | 17 + packages/sp/src/features/types.ts | 104 + packages/sp/src/features/web.ts | 18 + packages/sp/src/fields/index.ts | 23 + packages/sp/src/fields/list.ts | 17 + packages/sp/src/fields/types.ts | 674 ++++ packages/sp/src/fields/web.ts | 25 + packages/sp/src/files/folder.ts | 17 + packages/sp/src/files/index.ts | 19 + packages/sp/src/files/item.ts | 17 + packages/sp/src/files/types.ts | 546 ++++ packages/sp/src/files/web.ts | 34 + packages/sp/src/folders/index.ts | 12 + packages/sp/src/folders/item.ts | 19 + packages/sp/src/folders/list.ts | 17 + packages/sp/src/folders/types.ts | 236 ++ packages/sp/src/folders/web.ts | 51 + packages/sp/src/forms/index.ts | 8 + packages/sp/src/forms/list.ts | 17 + packages/sp/src/forms/types.ts | 41 + packages/sp/src/hubsites/index.ts | 32 + packages/sp/src/hubsites/site.ts | 43 + packages/sp/src/hubsites/types.ts | 64 + packages/sp/src/hubsites/web.ts | 34 + packages/sp/src/items/index.ts | 17 + packages/sp/src/items/list.ts | 17 + packages/sp/src/items/types.ts | 581 ++++ packages/sp/src/lists/index.ts | 20 + packages/sp/src/lists/types.ts | 694 ++++ packages/sp/src/lists/web.ts | 68 + packages/sp/src/navigation/index.ts | 31 + packages/sp/src/navigation/types.ts | 234 ++ packages/sp/src/navigation/web.ts | 19 + packages/sp/src/net/digestcache.ts | 58 + packages/sp/src/net/sphttpclient.ts | 175 + packages/sp/src/odata.ts | 90 + packages/sp/src/operations.ts | 58 + packages/sp/src/profiles/index.ts | 33 + packages/sp/src/profiles/types.ts | 758 +++++ packages/sp/src/regional-settings/index.ts | 10 + packages/sp/src/regional-settings/types.ts | 131 + packages/sp/src/regional-settings/web.ts | 18 + packages/sp/src/related-items/index.ts | 5 + packages/sp/src/related-items/types.ts | 163 + packages/sp/src/related-items/web.ts | 23 + packages/sp/src/rest.ts | 41 + packages/sp/src/search/index.ts | 57 + packages/sp/src/search/query.ts | 289 ++ packages/sp/src/search/suggest.ts | 139 + packages/sp/src/search/types.ts | 471 +++ packages/sp/src/security/funcs.ts | 101 + packages/sp/src/security/index.ts | 26 + packages/sp/src/security/item.ts | 35 + packages/sp/src/security/list.ts | 36 + packages/sp/src/security/types.ts | 452 +++ packages/sp/src/security/web.ts | 46 + packages/sp/src/sharepointqueryable.ts | 274 ++ packages/sp/src/sharing/file.ts | 99 + packages/sp/src/sharing/folder.ts | 101 + packages/sp/src/sharing/funcs.ts | 241 ++ packages/sp/src/sharing/index.ts | 27 + packages/sp/src/sharing/item.ts | 98 + packages/sp/src/sharing/types.ts | 584 ++++ packages/sp/src/sharing/web.ts | 116 + packages/sp/src/site-designs/index.ts | 28 + packages/sp/src/site-designs/types.ts | 243 ++ packages/sp/src/site-groups/index.ts | 11 + packages/sp/src/site-groups/types.ts | 143 + packages/sp/src/site-groups/web.ts | 72 + packages/sp/src/site-scripts/index.ts | 26 + packages/sp/src/site-scripts/types.ts | 106 + packages/sp/src/site-users/index.ts | 11 + packages/sp/src/site-users/types.ts | 152 + packages/sp/src/site-users/web.ts | 56 + packages/sp/src/sites/index.ts | 28 + packages/sp/src/sites/types.ts | 262 ++ packages/sp/src/social/index.ts | 33 + packages/sp/src/social/types.ts | 393 +++ packages/sp/src/sp.ts | 48 + packages/sp/src/sputilities/index.ts | 27 + packages/sp/src/sputilities/types.ts | 202 ++ packages/sp/src/subscriptions/index.ts | 10 + packages/sp/src/subscriptions/list.ts | 17 + packages/sp/src/subscriptions/types.ts | 116 + packages/sp/src/types.ts | 258 ++ packages/sp/src/user-custom-actions/index.ts | 12 + packages/sp/src/user-custom-actions/list.ts | 25 + packages/sp/src/user-custom-actions/site.ts | 17 + packages/sp/src/user-custom-actions/types.ts | 98 + packages/sp/src/user-custom-actions/web.ts | 20 + packages/sp/src/utils/escapeSingleQuote.ts | 11 + packages/sp/src/utils/extractweburl.ts | 21 + packages/sp/src/utils/file-names.ts | 35 + packages/sp/src/utils/metadata.ts | 5 + packages/sp/src/utils/toabsoluteurl.ts | 48 + packages/sp/src/views/index.ts | 12 + packages/sp/src/views/list.ts | 30 + packages/sp/src/views/types.ts | 178 ++ packages/sp/src/webparts/file.ts | 30 + packages/sp/src/webparts/index.ts | 11 + packages/sp/src/webparts/types.ts | 158 + packages/sp/src/webs/index.ts | 42 + packages/sp/src/webs/types.ts | 326 ++ packages/sp/tsconfig.es5.json | 28 + packages/sp/tsconfig.json | 28 + packages/tsconfig.es5.json | 37 + packages/tsconfig.json | 70 + pnp-build.js | 24 + pnp-debug.js | 22 + pnp-package.js | 27 + pnp-publish-beta.js | 13 + pnp-publish.js | 13 + rollup.config.js | 86 + settings.example.js | 24 + test/main.ts | 213 ++ test/sp/web.ts | 314 ++ test/tsconfig.json | 38 + test/types.ts | 4 + tools/buildsystem/index.ts | 1 + tools/buildsystem/package.json | 25 + tools/buildsystem/readme.md | 23 + tools/buildsystem/src/builder.ts | 57 + tools/buildsystem/src/buildsystem.ts | 27 + .../src/lib/getSubDirectoryNames.ts | 7 + tools/buildsystem/src/packager.ts | 52 + tools/buildsystem/src/publisher.ts | 53 + tools/buildsystem/src/tasks/build/build.ts | 32 + tools/buildsystem/src/tasks/build/index.ts | 4 + .../src/tasks/build/replace-debug.ts | 57 + .../tasks/build/replace-sp-http-version.ts | 37 + tools/buildsystem/src/tasks/build/schema.ts | 19 + tools/buildsystem/src/tasks/index.ts | 7 + .../src/tasks/package/copy-defs.ts | 48 + .../src/tasks/package/copy-docs.ts | 39 + .../src/tasks/package/copy-static-assets.ts | 40 + tools/buildsystem/src/tasks/package/index.ts | 7 + tools/buildsystem/src/tasks/package/rollup.ts | 23 + tools/buildsystem/src/tasks/package/schema.ts | 24 + .../buildsystem/src/tasks/package/webpack.ts | 23 + .../src/tasks/package/write-package-files.ts | 78 + tools/buildsystem/src/tasks/publish/index.ts | 3 + .../src/tasks/publish/publish-beta-package.ts | 45 + .../src/tasks/publish/publish-package.ts | 45 + tools/buildsystem/src/tasks/publish/schema.ts | 19 + tools/buildsystem/tsconfig.json | 25 + tools/dev-server/CHANGELOG.md | 9 + tools/dev-server/index.js | 72 + tools/dev-server/package.json | 22 + tools/dev-server/readme.md | 35 + .../generator-package/generators/app/index.js | 48 + .../generators/app/templates/index.ts | 1 + .../generators/app/templates/package.json | 22 + .../app/templates/tsconfig.es2015.json | 8 + .../app/templates/tsconfig.es5.json | 12 + .../generators/app/templates/tsconfig.json | 3 + tools/generator-package/package.json | 28 + tools/generator-package/readme.md | 25 + tools/gulptasks/args.js | 42 + tools/gulptasks/build.js | 96 + tools/gulptasks/clean.js | 47 + tools/gulptasks/index.js | 8 + tools/gulptasks/lint.js | 68 + tools/gulptasks/package.js | 33 + tools/gulptasks/publish.js | 138 + tools/gulptasks/serve.js | 69 + tools/gulptasks/test.js | 75 + tools/gulptasks/travisci.js | 79 + tools/node-utils/getSubDirectoryNames.js | 7 + tools/polyfill-ie11/.gitignore | 1 + tools/polyfill-ie11/.npmignore | 6 + tools/polyfill-ie11/CHANGELOG.md | 16 + tools/polyfill-ie11/LICENSE | 25 + tools/polyfill-ie11/index.ts | 6 + tools/polyfill-ie11/package.json | 39 + tools/polyfill-ie11/readme.md | 22 + tools/polyfill-ie11/searchquerybuilder.ts | 204 ++ tools/polyfill-ie11/tsconfig.json | 23 + tools/polyfill-ie11/webpack.config.js | 30 + tools/tests/.gitignore | 1 + tools/tests/.npmignore | 6 + tools/tests/CHANGELOG.md | 12 + tools/tests/LICENSE | 25 + tools/tests/package.json | 34 + tools/tests/readme.md | 22 + tools/tests/src/index.ts | 4 + tools/tests/tsconfig.json | 23 + tools/tests/webpack.config.js | 30 + tslint.json | 112 + webpack.config.js | 65 + 534 files changed, 45477 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 banner.js create mode 100644 debug/launch/graph.ts create mode 100644 debug/launch/main.ts create mode 100644 debug/launch/sp.ts create mode 100644 debug/launch/tsconfig.json create mode 100644 debug/serve/main.ts create mode 100644 debug/serve/tsconfig.json create mode 100644 debug/serve/webpack.config.js create mode 100644 docs/custom-package.md create mode 100644 docs/ie11-mode.md create mode 100644 docs/index.md create mode 100644 docs/invokable.md create mode 100644 docs/package-structure.md create mode 100644 docs/selective-imports.md create mode 100644 docs/sp/batching.md create mode 100644 docs/sp/webs.md create mode 100644 gulpfile.js create mode 100644 mkdocs.yml create mode 100644 package.json create mode 100644 packages/common/__tests/collections.test.ts create mode 100644 packages/common/__tests/mock-storage.ts create mode 100644 packages/common/__tests/storage.test.ts create mode 100644 packages/common/__tests/util.test.ts create mode 100644 packages/common/docs/adalclient.md create mode 100644 packages/common/docs/collections.md create mode 100644 packages/common/docs/custom-httpclientimpl.md create mode 100644 packages/common/docs/index.md create mode 100644 packages/common/docs/libconfig.md create mode 100644 packages/common/docs/netutil.md create mode 100644 packages/common/docs/storage.md create mode 100644 packages/common/docs/util.md create mode 100644 packages/common/index.ts create mode 100644 packages/common/package.json create mode 100644 packages/common/src/adalclient.ts create mode 100644 packages/common/src/collections.ts create mode 100644 packages/common/src/common.ts create mode 100644 packages/common/src/libconfig.ts create mode 100644 packages/common/src/netutil.ts create mode 100644 packages/common/src/spfxcontextinterface.ts create mode 100644 packages/common/src/storage.ts create mode 100644 packages/common/src/util.ts create mode 100644 packages/common/tsconfig.es5.json create mode 100644 packages/common/tsconfig.json create mode 100644 packages/config-store/__tests/configuration.test.ts create mode 100644 packages/config-store/__tests/mock-configurationprovider.ts create mode 100644 packages/config-store/__tests/mock-storage.ts create mode 100644 packages/config-store/__tests/providers/cachingConfigurationProvider.test.ts create mode 100644 packages/config-store/__tests/providers/spListConfigurationProvider.test.ts create mode 100644 packages/config-store/docs/configuration.md create mode 100644 packages/config-store/docs/index.md create mode 100644 packages/config-store/docs/providers.md create mode 100644 packages/config-store/index.ts create mode 100644 packages/config-store/package.json create mode 100644 packages/config-store/src/configstore.ts create mode 100644 packages/config-store/src/configuration.ts create mode 100644 packages/config-store/src/providers/cachingConfigurationProvider.ts create mode 100644 packages/config-store/src/providers/index.ts create mode 100644 packages/config-store/src/providers/spListConfigurationProvider.ts create mode 100644 packages/config-store/tsconfig.es5.json create mode 100644 packages/config-store/tsconfig.json create mode 100644 packages/documentation/SPFx-On-Premesis-2016.md create mode 100644 packages/documentation/beta-versions.md create mode 100644 packages/documentation/css/extra.css create mode 100644 packages/documentation/debugging.md create mode 100644 packages/documentation/deployment.md create mode 100644 packages/documentation/documentation.md create mode 100644 packages/documentation/getting-started-dev.md create mode 100644 packages/documentation/getting-started.md create mode 100644 packages/documentation/gulp-commands.md create mode 100644 packages/documentation/img/Logo.png create mode 100644 packages/documentation/img/PnPJS_FluentAPI.gif create mode 100644 packages/documentation/img/SPFx-On-Premesis-2016-1.png create mode 100644 packages/documentation/img/office365-header-icon.png create mode 100644 packages/documentation/img/pnpjs-common-uml.svg create mode 100644 packages/documentation/img/pnpjs-config-store-uml.svg create mode 100644 packages/documentation/img/pnpjs-graph-uml.svg create mode 100644 packages/documentation/img/pnpjs-logging-uml.svg create mode 100644 packages/documentation/img/pnpjs-nodejs-uml.svg create mode 100644 packages/documentation/img/pnpjs-odata-uml.svg create mode 100644 packages/documentation/img/pnpjs-sp-addinhelpers-uml.svg create mode 100644 packages/documentation/img/pnpjs-sp-clientsvc-uml.svg create mode 100644 packages/documentation/img/pnpjs-sp-taxonomy-uml.svg create mode 100644 packages/documentation/img/pnpjs-sp-uml.svg create mode 100644 packages/documentation/package-structure.md create mode 100644 packages/documentation/packages.md create mode 100644 packages/documentation/polyfill.md create mode 100644 packages/documentation/theme/main.html create mode 100644 packages/documentation/transition-guide.md create mode 100644 packages/graph/docs/contacts.md create mode 100644 packages/graph/docs/directoryobjects.md create mode 100644 packages/graph/docs/index.md create mode 100644 packages/graph/docs/invitations.md create mode 100644 packages/graph/docs/onedrive.md create mode 100644 packages/graph/docs/planner.md create mode 100644 packages/graph/docs/subscriptions.md create mode 100644 packages/graph/docs/teams.md create mode 100644 packages/graph/index.ts create mode 100644 packages/graph/package.json create mode 100644 packages/graph/presets/all.ts create mode 100644 packages/graph/src/attachments/conversations.ts create mode 100644 packages/graph/src/attachments/index.ts create mode 100644 packages/graph/src/attachments/types.ts create mode 100644 packages/graph/src/batch.ts create mode 100644 packages/graph/src/calendars/groups.ts create mode 100644 packages/graph/src/calendars/index.ts create mode 100644 packages/graph/src/calendars/types.ts create mode 100644 packages/graph/src/config/graphlibconfig.ts create mode 100644 packages/graph/src/contacts/index.ts create mode 100644 packages/graph/src/contacts/types.ts create mode 100644 packages/graph/src/contacts/users.ts create mode 100644 packages/graph/src/conversations/groups.ts create mode 100644 packages/graph/src/conversations/index.ts create mode 100644 packages/graph/src/conversations/types.ts create mode 100644 packages/graph/src/decorators.ts create mode 100644 packages/graph/src/directory-objects/index.ts create mode 100644 packages/graph/src/directory-objects/types.ts create mode 100644 packages/graph/src/graph.ts create mode 100644 packages/graph/src/graphqueryable.ts create mode 100644 packages/graph/src/groups/index.ts create mode 100644 packages/graph/src/groups/types.ts create mode 100644 packages/graph/src/invitations/index.ts create mode 100644 packages/graph/src/invitations/types.ts create mode 100644 packages/graph/src/members/groups.ts create mode 100644 packages/graph/src/members/index.ts create mode 100644 packages/graph/src/members/types.ts create mode 100644 packages/graph/src/messages/index.ts create mode 100644 packages/graph/src/messages/types.ts create mode 100644 packages/graph/src/messages/users.ts create mode 100644 packages/graph/src/net/graphhttpclient.ts create mode 100644 packages/graph/src/onedrive/index.ts create mode 100644 packages/graph/src/onedrive/types.ts create mode 100644 packages/graph/src/onedrive/users.ts create mode 100644 packages/graph/src/onenote/index.ts create mode 100644 packages/graph/src/onenote/types.ts create mode 100644 packages/graph/src/onenote/users.ts create mode 100644 packages/graph/src/operations.ts create mode 100644 packages/graph/src/photos/groups.ts create mode 100644 packages/graph/src/photos/index.ts create mode 100644 packages/graph/src/photos/types.ts create mode 100644 packages/graph/src/planner/groups.ts create mode 100644 packages/graph/src/planner/index.ts create mode 100644 packages/graph/src/planner/types.ts create mode 100644 packages/graph/src/planner/users.ts create mode 100644 packages/graph/src/rest.ts create mode 100644 packages/graph/src/subscriptions/index.ts create mode 100644 packages/graph/src/subscriptions/types.ts create mode 100644 packages/graph/src/teams/index.ts create mode 100644 packages/graph/src/teams/types.ts create mode 100644 packages/graph/src/teams/users.ts create mode 100644 packages/graph/src/types.ts create mode 100644 packages/graph/src/users/index.ts create mode 100644 packages/graph/src/users/types.ts create mode 100644 packages/graph/src/utils/type.ts create mode 100644 packages/graph/tsconfig.es5.json create mode 100644 packages/graph/tsconfig.json create mode 100644 packages/index.md create mode 100644 packages/logging/__tests/logging.test.ts create mode 100644 packages/logging/docs/index.md create mode 100644 packages/logging/index.ts create mode 100644 packages/logging/package.json create mode 100644 packages/logging/src/listeners.ts create mode 100644 packages/logging/src/logger.ts create mode 100644 packages/logging/src/logging.ts create mode 100644 packages/logging/src/types.ts create mode 100644 packages/logging/tsconfig.es5.json create mode 100644 packages/logging/tsconfig.json create mode 100644 packages/nodejs/docs/adal-fetch-client.md create mode 100644 packages/nodejs/docs/bearer-token-fetch-client.md create mode 100644 packages/nodejs/docs/index.md create mode 100644 packages/nodejs/docs/provider-hosted-app.md create mode 100644 packages/nodejs/docs/sp-fetch-client.md create mode 100644 packages/nodejs/index.ts create mode 100644 packages/nodejs/package.json create mode 100644 packages/nodejs/src/net/adalfetchclient.ts create mode 100644 packages/nodejs/src/net/bearertokenfetchclient.ts create mode 100644 packages/nodejs/src/net/index.ts create mode 100644 packages/nodejs/src/net/nodefetchclient.ts create mode 100644 packages/nodejs/src/net/spfetchclient.ts create mode 100644 packages/nodejs/src/nodejs.ts create mode 100644 packages/nodejs/src/providerhosted.ts create mode 100644 packages/nodejs/src/sptokenutils.ts create mode 100644 packages/nodejs/src/types.ts create mode 100644 packages/nodejs/tsconfig.es5.json create mode 100644 packages/nodejs/tsconfig.json create mode 100644 packages/odata/docs/caching.md create mode 100644 packages/odata/docs/core.md create mode 100644 packages/odata/docs/index.md create mode 100644 packages/odata/docs/odata-batch.md create mode 100644 packages/odata/docs/parsers.md create mode 100644 packages/odata/docs/pipeline.md create mode 100644 packages/odata/docs/queryable.md create mode 100644 packages/odata/index.ts create mode 100644 packages/odata/package.json create mode 100644 packages/odata/src/batch.ts create mode 100644 packages/odata/src/caching.ts create mode 100644 packages/odata/src/errors.ts create mode 100644 packages/odata/src/extenders.ts create mode 100644 packages/odata/src/invokable.ts create mode 100644 packages/odata/src/odata.ts create mode 100644 packages/odata/src/operation-binder.ts create mode 100644 packages/odata/src/parsers.ts create mode 100644 packages/odata/src/pipeline.ts create mode 100644 packages/odata/src/queryable.ts create mode 100644 packages/odata/src/request-builders.ts create mode 100644 packages/odata/tsconfig.es5.json create mode 100644 packages/odata/tsconfig.json create mode 100644 packages/pnpjs/docs/index.md create mode 100644 packages/pnpjs/index.ts create mode 100644 packages/pnpjs/package.json create mode 100644 packages/pnpjs/src/config/pnplibconfig.ts create mode 100644 packages/pnpjs/src/pnpjs.ts create mode 100644 packages/pnpjs/tsconfig.es5.json create mode 100644 packages/pnpjs/tsconfig.json create mode 100644 packages/readme.md create mode 100644 packages/sp-addinhelpers/docs/index.md create mode 100644 packages/sp-addinhelpers/docs/sp-request-executor-client.md create mode 100644 packages/sp-addinhelpers/docs/sp-rest-addin.md create mode 100644 packages/sp-addinhelpers/index.ts create mode 100644 packages/sp-addinhelpers/package.json create mode 100644 packages/sp-addinhelpers/src/addinhelpers.ts create mode 100644 packages/sp-addinhelpers/src/sprequestexecutorclient.ts create mode 100644 packages/sp-addinhelpers/src/sprestaddin.ts create mode 100644 packages/sp-addinhelpers/tsconfig.es5.json create mode 100644 packages/sp-addinhelpers/tsconfig.json create mode 100644 packages/sp-clientsvc/docs/index.md create mode 100644 packages/sp-clientsvc/index.ts create mode 100644 packages/sp-clientsvc/package.json create mode 100644 packages/sp-clientsvc/src/batch.ts create mode 100644 packages/sp-clientsvc/src/clientsvc.ts create mode 100644 packages/sp-clientsvc/src/clintsvcqueryable.ts create mode 100644 packages/sp-clientsvc/src/objectpath.ts create mode 100644 packages/sp-clientsvc/src/opactionbuilders.ts create mode 100644 packages/sp-clientsvc/src/opbuilders.ts create mode 100644 packages/sp-clientsvc/src/parsers.ts create mode 100644 packages/sp-clientsvc/src/types.ts create mode 100644 packages/sp-clientsvc/src/utils.ts create mode 100644 packages/sp-clientsvc/tsconfig.es5.json create mode 100644 packages/sp-clientsvc/tsconfig.json create mode 100644 packages/sp-taxonomy/__tests/batch.test.ts create mode 100644 packages/sp-taxonomy/__tests/session.test.ts create mode 100644 packages/sp-taxonomy/__tests/termstore.test.ts create mode 100644 packages/sp-taxonomy/docs/index.md create mode 100644 packages/sp-taxonomy/docs/labels.md create mode 100644 packages/sp-taxonomy/docs/term-groups.md create mode 100644 packages/sp-taxonomy/docs/term-sets.md create mode 100644 packages/sp-taxonomy/docs/term-stores.md create mode 100644 packages/sp-taxonomy/docs/terms.md create mode 100644 packages/sp-taxonomy/docs/utilities.md create mode 100644 packages/sp-taxonomy/index.ts create mode 100644 packages/sp-taxonomy/package.json create mode 100644 packages/sp-taxonomy/src/labels.ts create mode 100644 packages/sp-taxonomy/src/session.ts create mode 100644 packages/sp-taxonomy/src/taxonomy.ts create mode 100644 packages/sp-taxonomy/src/termgroup.ts create mode 100644 packages/sp-taxonomy/src/terms.ts create mode 100644 packages/sp-taxonomy/src/termsets.ts create mode 100644 packages/sp-taxonomy/src/termstores.ts create mode 100644 packages/sp-taxonomy/src/types.ts create mode 100644 packages/sp-taxonomy/src/utilities.ts create mode 100644 packages/sp-taxonomy/tsconfig.es5.json create mode 100644 packages/sp-taxonomy/tsconfig.json create mode 100644 packages/sp/__tests/alias.test.ts create mode 100644 packages/sp/__tests/batch.test.ts create mode 100644 packages/sp/__tests/clientsidepages.test.ts create mode 100644 packages/sp/__tests/configure.test.ts create mode 100644 packages/sp/__tests/contenttypes.test.ts create mode 100644 packages/sp/__tests/errors.test.ts create mode 100644 packages/sp/__tests/fields.test.ts create mode 100644 packages/sp/__tests/files.test.ts create mode 100644 packages/sp/__tests/folders.test.ts create mode 100644 packages/sp/__tests/items.test.ts create mode 100644 packages/sp/__tests/lists.test.ts create mode 100644 packages/sp/__tests/mock-fetchclient.ts create mode 100644 packages/sp/__tests/navigation.test.ts create mode 100644 packages/sp/__tests/roles.test.ts create mode 100644 packages/sp/__tests/search.test.ts create mode 100644 packages/sp/__tests/sharing.inactive.ts create mode 100644 packages/sp/__tests/site.test.ts create mode 100644 packages/sp/__tests/sitegroups.test.ts create mode 100644 packages/sp/__tests/siteusers.test.ts create mode 100644 packages/sp/__tests/subscriptions.test.ts create mode 100644 packages/sp/__tests/utilities.test.ts create mode 100644 packages/sp/__tests/utils.ts create mode 100644 packages/sp/__tests/views.test.ts create mode 100644 packages/sp/docs/alias-parameters.md create mode 100644 packages/sp/docs/alm.md create mode 100644 packages/sp/docs/attachments.md create mode 100644 packages/sp/docs/client-side-pages.md create mode 100644 packages/sp/docs/comments-likes.md create mode 100644 packages/sp/docs/content-types.md create mode 100644 packages/sp/docs/entity-merging.md create mode 100644 packages/sp/docs/features.md create mode 100644 packages/sp/docs/fields.md create mode 100644 packages/sp/docs/files.md create mode 100644 packages/sp/docs/index.md create mode 100644 packages/sp/docs/items.md create mode 100644 packages/sp/docs/navigation-service.md create mode 100644 packages/sp/docs/permissions.md create mode 100644 packages/sp/docs/profiles.md create mode 100644 packages/sp/docs/related-items.md create mode 100644 packages/sp/docs/search.md create mode 100644 packages/sp/docs/sharing.md create mode 100644 packages/sp/docs/sitedesigns.md create mode 100644 packages/sp/docs/sites.md create mode 100644 packages/sp/docs/social.md create mode 100644 packages/sp/docs/sp-utilities-utility.md create mode 100644 packages/sp/docs/tenant-properties.md create mode 100644 packages/sp/docs/views.md create mode 100644 packages/sp/index.ts create mode 100644 packages/sp/package.json create mode 100644 packages/sp/presets/all.ts create mode 100644 packages/sp/presets/core.ts create mode 100644 packages/sp/src/appcatalog/index.ts create mode 100644 packages/sp/src/appcatalog/types.ts create mode 100644 packages/sp/src/appcatalog/web.ts create mode 100644 packages/sp/src/attachments/index.ts create mode 100644 packages/sp/src/attachments/item.ts create mode 100644 packages/sp/src/attachments/types.ts create mode 100644 packages/sp/src/batch.ts create mode 100644 packages/sp/src/clientsidepages/funcs.ts create mode 100644 packages/sp/src/clientsidepages/index.ts create mode 100644 packages/sp/src/clientsidepages/types.ts create mode 100644 packages/sp/src/clientsidepages/web.ts create mode 100644 packages/sp/src/comments/index.ts create mode 100644 packages/sp/src/comments/item.ts create mode 100644 packages/sp/src/comments/types.ts create mode 100644 packages/sp/src/config/splibconfig.ts create mode 100644 packages/sp/src/content-types/index.ts create mode 100644 packages/sp/src/content-types/item.ts create mode 100644 packages/sp/src/content-types/list.ts create mode 100644 packages/sp/src/content-types/types.ts create mode 100644 packages/sp/src/content-types/web.ts create mode 100644 packages/sp/src/decorators.ts create mode 100644 packages/sp/src/features/index.ts create mode 100644 packages/sp/src/features/site.ts create mode 100644 packages/sp/src/features/types.ts create mode 100644 packages/sp/src/features/web.ts create mode 100644 packages/sp/src/fields/index.ts create mode 100644 packages/sp/src/fields/list.ts create mode 100644 packages/sp/src/fields/types.ts create mode 100644 packages/sp/src/fields/web.ts create mode 100644 packages/sp/src/files/folder.ts create mode 100644 packages/sp/src/files/index.ts create mode 100644 packages/sp/src/files/item.ts create mode 100644 packages/sp/src/files/types.ts create mode 100644 packages/sp/src/files/web.ts create mode 100644 packages/sp/src/folders/index.ts create mode 100644 packages/sp/src/folders/item.ts create mode 100644 packages/sp/src/folders/list.ts create mode 100644 packages/sp/src/folders/types.ts create mode 100644 packages/sp/src/folders/web.ts create mode 100644 packages/sp/src/forms/index.ts create mode 100644 packages/sp/src/forms/list.ts create mode 100644 packages/sp/src/forms/types.ts create mode 100644 packages/sp/src/hubsites/index.ts create mode 100644 packages/sp/src/hubsites/site.ts create mode 100644 packages/sp/src/hubsites/types.ts create mode 100644 packages/sp/src/hubsites/web.ts create mode 100644 packages/sp/src/items/index.ts create mode 100644 packages/sp/src/items/list.ts create mode 100644 packages/sp/src/items/types.ts create mode 100644 packages/sp/src/lists/index.ts create mode 100644 packages/sp/src/lists/types.ts create mode 100644 packages/sp/src/lists/web.ts create mode 100644 packages/sp/src/navigation/index.ts create mode 100644 packages/sp/src/navigation/types.ts create mode 100644 packages/sp/src/navigation/web.ts create mode 100644 packages/sp/src/net/digestcache.ts create mode 100644 packages/sp/src/net/sphttpclient.ts create mode 100644 packages/sp/src/odata.ts create mode 100644 packages/sp/src/operations.ts create mode 100644 packages/sp/src/profiles/index.ts create mode 100644 packages/sp/src/profiles/types.ts create mode 100644 packages/sp/src/regional-settings/index.ts create mode 100644 packages/sp/src/regional-settings/types.ts create mode 100644 packages/sp/src/regional-settings/web.ts create mode 100644 packages/sp/src/related-items/index.ts create mode 100644 packages/sp/src/related-items/types.ts create mode 100644 packages/sp/src/related-items/web.ts create mode 100644 packages/sp/src/rest.ts create mode 100644 packages/sp/src/search/index.ts create mode 100644 packages/sp/src/search/query.ts create mode 100644 packages/sp/src/search/suggest.ts create mode 100644 packages/sp/src/search/types.ts create mode 100644 packages/sp/src/security/funcs.ts create mode 100644 packages/sp/src/security/index.ts create mode 100644 packages/sp/src/security/item.ts create mode 100644 packages/sp/src/security/list.ts create mode 100644 packages/sp/src/security/types.ts create mode 100644 packages/sp/src/security/web.ts create mode 100644 packages/sp/src/sharepointqueryable.ts create mode 100644 packages/sp/src/sharing/file.ts create mode 100644 packages/sp/src/sharing/folder.ts create mode 100644 packages/sp/src/sharing/funcs.ts create mode 100644 packages/sp/src/sharing/index.ts create mode 100644 packages/sp/src/sharing/item.ts create mode 100644 packages/sp/src/sharing/types.ts create mode 100644 packages/sp/src/sharing/web.ts create mode 100644 packages/sp/src/site-designs/index.ts create mode 100644 packages/sp/src/site-designs/types.ts create mode 100644 packages/sp/src/site-groups/index.ts create mode 100644 packages/sp/src/site-groups/types.ts create mode 100644 packages/sp/src/site-groups/web.ts create mode 100644 packages/sp/src/site-scripts/index.ts create mode 100644 packages/sp/src/site-scripts/types.ts create mode 100644 packages/sp/src/site-users/index.ts create mode 100644 packages/sp/src/site-users/types.ts create mode 100644 packages/sp/src/site-users/web.ts create mode 100644 packages/sp/src/sites/index.ts create mode 100644 packages/sp/src/sites/types.ts create mode 100644 packages/sp/src/social/index.ts create mode 100644 packages/sp/src/social/types.ts create mode 100644 packages/sp/src/sp.ts create mode 100644 packages/sp/src/sputilities/index.ts create mode 100644 packages/sp/src/sputilities/types.ts create mode 100644 packages/sp/src/subscriptions/index.ts create mode 100644 packages/sp/src/subscriptions/list.ts create mode 100644 packages/sp/src/subscriptions/types.ts create mode 100644 packages/sp/src/types.ts create mode 100644 packages/sp/src/user-custom-actions/index.ts create mode 100644 packages/sp/src/user-custom-actions/list.ts create mode 100644 packages/sp/src/user-custom-actions/site.ts create mode 100644 packages/sp/src/user-custom-actions/types.ts create mode 100644 packages/sp/src/user-custom-actions/web.ts create mode 100644 packages/sp/src/utils/escapeSingleQuote.ts create mode 100644 packages/sp/src/utils/extractweburl.ts create mode 100644 packages/sp/src/utils/file-names.ts create mode 100644 packages/sp/src/utils/metadata.ts create mode 100644 packages/sp/src/utils/toabsoluteurl.ts create mode 100644 packages/sp/src/views/index.ts create mode 100644 packages/sp/src/views/list.ts create mode 100644 packages/sp/src/views/types.ts create mode 100644 packages/sp/src/webparts/file.ts create mode 100644 packages/sp/src/webparts/index.ts create mode 100644 packages/sp/src/webparts/types.ts create mode 100644 packages/sp/src/webs/index.ts create mode 100644 packages/sp/src/webs/types.ts create mode 100644 packages/sp/tsconfig.es5.json create mode 100644 packages/sp/tsconfig.json create mode 100644 packages/tsconfig.es5.json create mode 100644 packages/tsconfig.json create mode 100644 pnp-build.js create mode 100644 pnp-debug.js create mode 100644 pnp-package.js create mode 100644 pnp-publish-beta.js create mode 100644 pnp-publish.js create mode 100644 rollup.config.js create mode 100644 settings.example.js create mode 100644 test/main.ts create mode 100644 test/sp/web.ts create mode 100644 test/tsconfig.json create mode 100644 test/types.ts create mode 100644 tools/buildsystem/index.ts create mode 100644 tools/buildsystem/package.json create mode 100644 tools/buildsystem/readme.md create mode 100644 tools/buildsystem/src/builder.ts create mode 100644 tools/buildsystem/src/buildsystem.ts create mode 100644 tools/buildsystem/src/lib/getSubDirectoryNames.ts create mode 100644 tools/buildsystem/src/packager.ts create mode 100644 tools/buildsystem/src/publisher.ts create mode 100644 tools/buildsystem/src/tasks/build/build.ts create mode 100644 tools/buildsystem/src/tasks/build/index.ts create mode 100644 tools/buildsystem/src/tasks/build/replace-debug.ts create mode 100644 tools/buildsystem/src/tasks/build/replace-sp-http-version.ts create mode 100644 tools/buildsystem/src/tasks/build/schema.ts create mode 100644 tools/buildsystem/src/tasks/index.ts create mode 100644 tools/buildsystem/src/tasks/package/copy-defs.ts create mode 100644 tools/buildsystem/src/tasks/package/copy-docs.ts create mode 100644 tools/buildsystem/src/tasks/package/copy-static-assets.ts create mode 100644 tools/buildsystem/src/tasks/package/index.ts create mode 100644 tools/buildsystem/src/tasks/package/rollup.ts create mode 100644 tools/buildsystem/src/tasks/package/schema.ts create mode 100644 tools/buildsystem/src/tasks/package/webpack.ts create mode 100644 tools/buildsystem/src/tasks/package/write-package-files.ts create mode 100644 tools/buildsystem/src/tasks/publish/index.ts create mode 100644 tools/buildsystem/src/tasks/publish/publish-beta-package.ts create mode 100644 tools/buildsystem/src/tasks/publish/publish-package.ts create mode 100644 tools/buildsystem/src/tasks/publish/schema.ts create mode 100644 tools/buildsystem/tsconfig.json create mode 100644 tools/dev-server/CHANGELOG.md create mode 100644 tools/dev-server/index.js create mode 100644 tools/dev-server/package.json create mode 100644 tools/dev-server/readme.md create mode 100644 tools/generator-package/generators/app/index.js create mode 100644 tools/generator-package/generators/app/templates/index.ts create mode 100644 tools/generator-package/generators/app/templates/package.json create mode 100644 tools/generator-package/generators/app/templates/tsconfig.es2015.json create mode 100644 tools/generator-package/generators/app/templates/tsconfig.es5.json create mode 100644 tools/generator-package/generators/app/templates/tsconfig.json create mode 100644 tools/generator-package/package.json create mode 100644 tools/generator-package/readme.md create mode 100644 tools/gulptasks/args.js create mode 100644 tools/gulptasks/build.js create mode 100644 tools/gulptasks/clean.js create mode 100644 tools/gulptasks/index.js create mode 100644 tools/gulptasks/lint.js create mode 100644 tools/gulptasks/package.js create mode 100644 tools/gulptasks/publish.js create mode 100644 tools/gulptasks/serve.js create mode 100644 tools/gulptasks/test.js create mode 100644 tools/gulptasks/travisci.js create mode 100644 tools/node-utils/getSubDirectoryNames.js create mode 100644 tools/polyfill-ie11/.gitignore create mode 100644 tools/polyfill-ie11/.npmignore create mode 100644 tools/polyfill-ie11/CHANGELOG.md create mode 100644 tools/polyfill-ie11/LICENSE create mode 100644 tools/polyfill-ie11/index.ts create mode 100644 tools/polyfill-ie11/package.json create mode 100644 tools/polyfill-ie11/readme.md create mode 100644 tools/polyfill-ie11/searchquerybuilder.ts create mode 100644 tools/polyfill-ie11/tsconfig.json create mode 100644 tools/polyfill-ie11/webpack.config.js create mode 100644 tools/tests/.gitignore create mode 100644 tools/tests/.npmignore create mode 100644 tools/tests/CHANGELOG.md create mode 100644 tools/tests/LICENSE create mode 100644 tools/tests/package.json create mode 100644 tools/tests/readme.md create mode 100644 tools/tests/src/index.ts create mode 100644 tools/tests/tsconfig.json create mode 100644 tools/tests/webpack.config.js create mode 100644 tslint.json create mode 100644 webpack.config.js diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..5ec978e4d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,39 @@ +Thank you for reporting an issue, suggesting an enhancement, or asking a question. We appreciate your feedback - to help the team understand your +needs please complete the below template to ensure we have the details to help. Thanks! + +**Please check out the [Docs](https://pnp.github.io/pnpjs/) to see if your question is already addressed there. This will help us ensure our documentation covers the most frequent questions.** + +### Category +- [ ] Enhancement +- [ ] Bug +- [ ] Question +- [ ] Documentation gap/issue + +### Version + +Please specify what version of the library you are using: [ ] + +Please specify what version(s) of SharePoint you are targeting: [ ] + +*If you are not using the latest release, please update and see if the issue is resolved before submitting an issue.* + +### Expected / Desired Behavior / Question +*If you are reporting an issue please describe the expected behavior. If you are suggesting an enhancement please +describe thoroughly the enhancement, how it can be achieved, and expected benefit. If you are asking a question, ask away!* + +### Observed Behavior +*If you are reporting an issue please describe the behavior you expected to occur when performing the action. If you are making a +suggestion or asking a question delete this section.* + +### Steps to Reproduce +*If you are reporting an issue please describe the steps to reproduce the bug in sufficient detail to allow testing. If you are making +a suggestion or asking a question delete this section.* + +### Submission Guidelines +*Delete this section after reading* +* All suggestions, questions and issues are welcome, please let us know what's on your mind. +* Remember to include sufficient details and context. +* Please check back occasionally on your issue as we may have follow up questions. +* If you have multiple suggestions, questions, or bugs please submit them in seperate issues so we can track resolution. + +Thank you for your feedback! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..49650fbf8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +#### Category +- [ ] Bug fix? +- [ ] New feature? +- [ ] New sample? +- [ ] Documentation update? + +#### Related Issues + +fixes #X, mentioned in #Y + +#### What's in this Pull Request? + +*Please describe the changes in this PR. Simple description or details around bugs which are being fixed.* + +#### Guidance +*You can delete this section when you are submitting the pull request.* +* Please update this PR information accordingly. We'll use this as part of our release notes in monthly communications. +* Please target your PR to Dev branch. +* Please ensure you have updated any associated docs files based on your code changes + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..637ddca77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Logs +logs +*.log +npm-debug.log* +yarn.lock + +# Runtime data +pids +*.pid +*.seed + +.idea/ + +# Coverage directory used by tools like istanbul +coverage + +# node-waf configuration +.lock-wscript + +# Dependency directory +node_modules + +# generated folders/files +/build +/dist +/site +.awcache + +# Optional npm cache directory +.npm + +.idea + +# allow folks to add things to the debug /launch and /serve folder, but don't include them in git +debug/launch/* +!debug/launch/main.ts +!debug/launch/sp.ts +!debug/launch/graph.ts +!debug/launch/tsconfig.json + +debug/serve/* +!debug/serve/main.ts +!debug/serve/tsconfig.json +!debug/serve/webpack.config.js + +# project settings +settings.js + +bower_components + +# we don't use this in the repo +package-lock.json + +# need to include this for bower to work, but leave this ignored as PRs +# with updates to dist will be disallowed +dist diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..185caaac9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js + +node_js: + - "8" + +before_script: + - npm install -g gulp + +script: + - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then gulp travis:pull-request; fi' + - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then gulp travis:push; fi' + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..2badbfa61 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,40 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/debug/launch/main.ts", + "stopOnEntry": false, + "args": [], + "cwd": "${workspaceRoot}", + "preLaunchTask": "build", + "runtimeExecutable": null, + "runtimeArgs": [ + "--nolazy" + ], + "env": { + "NODE_ENV": "development" + }, + "console": "internalConsole", + "internalConsoleOptions": "openOnSessionStart", + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/build/debugging/**/*.js" + ] + }, + { + "name": "Debug Tests", + "type": "node", + "request": "attach", + "stopOnEntry": false, + "cwd": "${workspaceRoot}", + "preLaunchTask": "gulp-test", + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/build/testing/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..b940666e5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "lib": true, + "**/node_modules": true, + "coverage": true, + "build": true, + "dist": true, + "site": true + }, + "typescript.validate.enable": true, + "typescript.tsdk": "./node_modules/typescript/lib" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..f403cc2cf --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,40 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "gulp build:debug", + "args": [], + "problemMatcher": [ + "$tsc", + "$jshint" + ], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false + } + }, + { + "label": "gulp-test", + "type": "shell", + "command": "gulp test", + "args": [], + "isBackground": true, + "problemMatcher": [ + "$tsc", + "$jshint" + ], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false + } + }, + ] +} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..a3ad689a7 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,22 @@ +List of Patterns and Practices PnPjs contributors. Updated before every release. + +Patrick Rodgers, Microsoft (@patrick-rodgers) +Andrew Koltyakov (@koltyakov) +Allan Hvam (@allanhvam) +Eirik Brandtzæg (@eirikb) +Ole Martin Pettersen (@olemp) +Paweł Hawrylak (@phawrylak) +Elio Struyf (@estruyf) +Fredrik Thorild (@fthorild) +Tomi Tavela (@tavikukko) +Chris Kent (@thechriskent) +James Brennan (@relugas) +Stefan Bauer (@stfbauer) +Gautam Sheth (@gautamdsheth) +Sergei Sergeev (@s-kainet) +Simon Ågren (@simonagren) +Pedro Pedrosa (@pedro-pedrosa) +Sean Marthur (@seanmarthur) +Charles Simard-Lecours (@cslecours) +Piotr Siatka (@siata13) +Alex Terentiev (@ajixumuk) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..b0e69cdc1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog +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). + +## 2.0.0 - 2019-XX-XX + +_These changes are from the move from 1.X.X libraries to 2.0.0 and represent the beginning of a new changelog for the 2.X.X family_ + +### Added + +- odata: added IQueryableData + +### Changed + +- odata: refactor Queryable + - remove withPipeline (becomes a function argument bind and ) + - removed the action methods (get, post, put, delete) + - introduced "invokables" + - added methods to operate on Queryables + - all inheriting methods updated with interfaces and factory functions + - remove ODataQueryable and merged into Queryable +- sp & graph: libraries can be selectively imported +- all: updated internals to use await +- all: interfaces prefixed with "I" +- odata: empty request pipeline will throw an error +- sp & graph: updated clone to use factory +- sp: changed signature of createDefaultAssociatedGroups +- sp: all query string params are escaped now within the library + +### Fixed + + +### Removed + +- removed "as" method from SharePoint & Graph Queryable +- removed WebInfos class +- removed InstalledLanguages class +- removed Web.addClientSidePageByPath + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..af77dab54 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +SharePoint Patterns and Practices (PnP) + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..0cff503b2 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +![SharePoint Patterns and Practices](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) + +PnPJS is a fluent JavaScript API for consuming SharePoint and Office 365 REST APIs in a type-safe way. You can use it with SharePoint Framework, Nodejs, or JavaScript projects. This an open source initiative complements existing SDKs provided by Microsoft offering developers another way to consume information from SharePoint and Office 365. + +Please use [http://aka.ms/sppnp](http://aka.ms/sppnp) for the latest updates around the whole *SharePoint Patterns and Practices (PnP) initiative*. + +**If you are moving from sp-pnp-js please review the [transition guide](https://pnp.github.io/pnpjs/documentation/transition-guide/)** + +## Getting Started + +Please see the [Getting Started guide](https://pnp.github.io/pnpjs/documentation/getting-started/) in the main documentation. + +## Documentation + +Please review the [documentation](https://pnp.github.io/pnpjs/) for the PnPJS libraries. This +site is updated with each release. If cannot find what you need, please let us know by logging an [documentation request](https://github.com/pnp/pnpjs/issues). + +## Packages + +[![npm version](https://badge.fury.io/js/%40pnp%2Fcommon.svg)](https://badge.fury.io/js/%40pnp%2Fcommon) + +The following packages comprise the Patterns and Practices client side libraries. + +## [@pnp/common](packages/common/docs/index.md) + +**Provides shared functionality across all pnp libraries** + +## [@pnp/config-store](packages/config-store/docs/index.md) +**Provides a way to manage configuration within your application** + +## [@pnp/graph](packages/graph/docs/index.md) + +**Provides functionality to query the Microsoft Graph** + +## [@pnp/logging](packages/logging/docs/index.md) + +**Light-weight, subscribable logging framework** + +## [@pnp/nodejs](packages/nodejs/docs/index.md) + +**Provides functionality enabling the @pnp libraries within nodejs** + +## [@pnp/odata](packages/odata/docs/index.md) + +**Provides shared odata functionality and base classes** + +## [@pnp/pnpjs](packages/pnpjs/docs/index.md) + +**Rollup library of core functionality, mimics sp-pnp-js** + +## [@pnp/sp](packages/sp/docs/index.md) + +**Provides a fluent api for working with SharePoint REST** + +## [@pnp/sp-addinhelpers](packages/sp-addinhelpers/docs/index.md) + +**Provides functionality for working within SharePoint add-ins** + +## [@pnp/sp-taxonomy](packages/sp-taxonomy/docs/index.md) + +**Provides a fluent API for querying taxonomy information** + +## [@pnp/sp-clientsvc](packages/sp-clientsvc/docs/index.md) + +**Handles generic communication with client.svc endpoint, removing SP.\*.js dependencies** + + +## Authors +This project's contributors include Microsoft and [community contributors](AUTHORS). Work is done as as open source community project. + +## Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### "Sharing is Caring" + +### Disclaimer +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +![](https://telemetry.sharepointpnp.com/@pnp/pnpjs/readme) diff --git a/banner.js b/banner.js new file mode 100644 index 000000000..8ca0461ba --- /dev/null +++ b/banner.js @@ -0,0 +1,13 @@ +const pkg = require("./package.json"); + +module.exports = [ + "/**", + ` * @license`, + ` * v${pkg.version}`, + ` * ${pkg.license} (https://github.com/pnp/pnpjs/blob/master/LICENSE)`, + ` * Copyright (c) ${new Date().getFullYear()} Microsoft`, + " * docs: https://pnp.github.io/pnpjs/", + ` * source: ${pkg.homepage}`, + ` * bugs: ${pkg.bugs.url}`, + " */", +].join("\n"); diff --git a/debug/launch/graph.ts b/debug/launch/graph.ts new file mode 100644 index 000000000..aa9d371d5 --- /dev/null +++ b/debug/launch/graph.ts @@ -0,0 +1,33 @@ +import { Logger, LogLevel } from "../../packages/logging"; +import { graph } from "../../packages/graph"; +import { AdalFetchClient } from "../../packages/nodejs"; + +declare var process: { exit(code?: number): void }; + +export function Example(settings: any) { + + graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient(settings.testing.graph.tenant, settings.testing.graph.id, settings.testing.graph.secret); + }, + }, + }); + + graph.groups.get().then(g => { + + Logger.log({ + data: g, + level: LogLevel.Info, + message: "List of Groups", + }); + + process.exit(0); + + }).catch(e => { + + // logging results to the Logger + Logger.error(e); + process.exit(1); + }); +} \ No newline at end of file diff --git a/debug/launch/main.ts b/debug/launch/main.ts new file mode 100644 index 000000000..a3f3cc545 --- /dev/null +++ b/debug/launch/main.ts @@ -0,0 +1,29 @@ +declare var require: (s: string) => any; + +import { ConsoleListener, LogLevel, Logger } from "@pnp/logging"; +// importing the example debug scenario and running it +// adding your debugging to other files and importing them will keep them out of git +// PRs updating the debug.ts or example.ts will not be accepted unless they are fixing bugs +// add your debugging imports here and prior to submitting a PR git checkout debug/debug.ts +// will allow you to keep all your debugging files locally +// comment out the example +import { Example } from "./2.0/sp"; + +// setup the connection to SharePoint using the settings file, you can +// override any of the values as you want here, just be sure not to commit +// your account details :) +// if you don't have a settings file defined this will error +// you can comment it out and put the values here directly, or better yet +// create a settings file using settings.example.js as a template +const settings = require("../../../../settings.js"); + +// // setup console logger +Logger.subscribe(new ConsoleListener()); + +// change this to LogLevel.Verbose for more details about the request +Logger.activeLogLevel = LogLevel.Info; + +Example(settings); + +// you can also set break points inside the src folder to examine how things are working +// within the library while debugging diff --git a/debug/launch/sp.ts b/debug/launch/sp.ts new file mode 100644 index 000000000..ba63cbb90 --- /dev/null +++ b/debug/launch/sp.ts @@ -0,0 +1,41 @@ +// import { Logger, LogLevel } from "../../packages/logging"; +import { sp } from "@pnp/sp/presets/legacy"; +// import "@pnp/sp/src/webs"; +// import "@pnp/sp/src/features/web"; +// import { sp } from "@pnp/sp"; +import { SPFetchClient } from "@pnp/nodejs"; + +declare var process: { exit(code?: number): void }; + +export async function Example(settings: any) { + + // configure your node options + sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret); + }, + }, + }); + + const y = await sp.web.features(); + + // const y = await sp.site.features(); + + console.log(JSON.stringify(y, null, 2)); + + // const u = new getable(Web)(settings.testing.sp.url); + + // // const y = new Web(settings.testing.sp.url); + + // const d = await u(); + + // console.log(JSON.stringify(d, null, 2)); + + // // @ ts-ignore + // const uu = u.features; + + // uu.get().then(r => { + // console.log(JSON.stringify(r, null, 2)); + // }); +} diff --git a/debug/launch/tsconfig.json b/debug/launch/tsconfig.json new file mode 100644 index 000000000..8bfaaf826 --- /dev/null +++ b/debug/launch/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "baseUrl": ".", + "rootDir": "../../", + "outDir": "../../build/debugging", + "declaration": true, + "declarationMap": true, + "types": [ + "sharepoint" + ], + "lib": [ + "es6", + "dom" + ], + "paths": { + "@pnp/*": [ + "../../packages/*" + ] + }, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "inlineSources": true, + "sourceMap": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "strictNullChecks": false + }, + "files": [ + "./main.ts" + ] +} \ No newline at end of file diff --git a/debug/serve/main.ts b/debug/serve/main.ts new file mode 100644 index 000000000..2dec1f288 --- /dev/null +++ b/debug/serve/main.ts @@ -0,0 +1,37 @@ +import { sp } from "@pnp/sp"; + +// ****** +// Please edit this file and do any testing required. Please do not submit changes as part of a PR. +// ****** + +// ensure our DOM is ready for us to do stuff +document.onreadystatechange = async () => { + + if (document.readyState === "interactive") { + + // uncomment this to test with verbose mode + // sp.setup({ + // sp: { + // headers: { + // "Accept": "application/json;odata=verbose", + // }, + // }, + // }); + + const e = document.getElementById("pnp-test"); + + const html = []; + + try { + + const r = await sp.web.get(); + + html.push(``); + + } catch (e) { + html.push(`Error:
${JSON.stringify(e, null, 4)}
`); + } + + e.innerHTML = html.join("
"); + } +}; diff --git a/debug/serve/tsconfig.json b/debug/serve/tsconfig.json new file mode 100644 index 000000000..9ad5b81b0 --- /dev/null +++ b/debug/serve/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "baseUrl": ".", + "rootDir": "../../", + "outDir": "../../serve/", + "declaration": true, + "declarationMap": true, + "types": [ + "sharepoint" + ], + "lib": [ + "es6", + "dom" + ], + "paths": { + "@pnp/*": [ + "../../packages/*" + ] + }, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "inlineSources": true, + "sourceMap": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "experimentalDecorators": true + }, + "files": [ + "./main.ts" + ] +} \ No newline at end of file diff --git a/debug/serve/webpack.config.js b/debug/serve/webpack.config.js new file mode 100644 index 000000000..296f077f5 --- /dev/null +++ b/debug/serve/webpack.config.js @@ -0,0 +1,46 @@ +const path = require("path"), + TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); + +// give outselves a single reference to the projectRoot +const projectRoot = path.resolve(__dirname, "../.."); + +const version = require(path.join(projectRoot, "package.json")).version; + +module.exports = { + mode: "development", + entry: path.join(projectRoot, "debug", "serve", "main.ts"), + output: { + path: path.join(projectRoot, "serve"), + publicPath: "/assets/", + filename: "pnp.js", + libraryTarget: "umd", + library: "$pnp", + }, + devtool: "source-map", + resolve: { + extensions: [ '.ts', '.tsx', ".js", ".json"], + plugins: [new TsconfigPathsPlugin({ configFile: path.join(projectRoot, "debug", "serve", "tsconfig.json") })], + }, + module: { + rules: [ + { + test: /\.ts$/, + use: [{ + loader: "ts-loader", + }, + { + loader: "string-replace-loader", + options: { + search: "$$Version$$", + replace: version + } + }, + ] + }, + ] + }, + stats: { + assets: false, + colors: true, + } +}; diff --git a/docs/custom-package.md b/docs/custom-package.md new file mode 100644 index 000000000..093e44593 --- /dev/null +++ b/docs/custom-package.md @@ -0,0 +1 @@ +described how to create your own webpacked pnpjs package that includes just the pieces you need. \ No newline at end of file diff --git a/docs/ie11-mode.md b/docs/ie11-mode.md new file mode 100644 index 000000000..76468adc2 --- /dev/null +++ b/docs/ie11-mode.md @@ -0,0 +1 @@ +explain ie 11 mode \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..e906153c4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,56 @@ +![SharePoint Patterns and Practices Logo](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) + +PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community. + +![Fluent API in action](documentation/img/PnPJS_FluentAPI.gif) +_Animation of the library in use, note intellisense help in building your queries_ + +## General Guidance + +These articles provide general guidance for working with the libraries. If you are migrating from _sp-pnp-js_ please review the [transition guide](documentation/transition-guide.md). + +* **[Getting Started](documentation/getting-started.md)** +* [Getting Started Contributing](documentation/getting-started-dev.md) +* [Documentation](documentation/documentation.md) +* [Gulp Commands](documentation/gulp-commands.md) +* [Debugging](documentation/debugging.md) +* [Deployment](documentation/deployment.md) +* [Install Beta Versions](documentation/beta-versions.md) +* [Polyfills](documentation/polyfill.md) +* [Package Structure](documentation/package-structure.md) + +## Packages + +Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. + +The latest published version is **{{version}}**. + +| || | +| ---| -------------|-------------| +| @pnp/| | | +|| [common](common/docs/index.md) | Provides shared functionality across all pnp libraries | +|| [config-store](config-store/docs/index.md) | Provides a way to manage configuration within your application | +|| [graph](graph/docs/index.md) | Provides a fluent api for working with Microsoft Graph | +|| [logging](logging/docs/index.md) | Light-weight, subscribable logging framework | +|| [nodejs](nodejs/docs/index.md) | Provides functionality enabling the @pnp libraries within nodejs | +|| [odata](odata/docs/index.md) | Provides shared odata functionality and base classes | +|| [pnpjs](pnpjs/docs/index.md) | Rollup library of core functionality (mimics sp-pnp-js) | +|| [sp](sp/docs/index.md) | Provides a fluent api for working with SharePoint REST | +|| [sp-addinhelpers](sp-addinhelpers/docs/index.md) | Provides functionality for working within SharePoint add-ins | +|| [sp-clientsvc](sp-clientsvc/docs/index.md) | Provides based classes used to create a fluent api for working with SharePoint Managed Metadata | +|| [sp-taxonomy](sp-taxonomy/docs/index.md) | Provides a fluent api for working with SharePoint Managed Metadata | + +## Issues, Questions, Ideas + +Please [log an issue](https://github.com/pnp/pnpjs/issues) using our template as a guide. This will let us track your request and ensure we respond. We appreciate any contructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project. + + +## Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### "Sharing is Caring" + +Please use [http://aka.ms/sppnp](http://aka.ms/sppnp) for the latest updates around the whole *SharePoint Patterns and Practices (PnP) program*. + +### Disclaimer +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** diff --git a/docs/invokable.md b/docs/invokable.md new file mode 100644 index 000000000..dbee956a9 --- /dev/null +++ b/docs/invokable.md @@ -0,0 +1 @@ +# Explain Invokable Concept \ No newline at end of file diff --git a/docs/package-structure.md b/docs/package-structure.md new file mode 100644 index 000000000..171fc15c3 --- /dev/null +++ b/docs/package-structure.md @@ -0,0 +1 @@ +updated package structure \ No newline at end of file diff --git a/docs/selective-imports.md b/docs/selective-imports.md new file mode 100644 index 000000000..5fb66554c --- /dev/null +++ b/docs/selective-imports.md @@ -0,0 +1 @@ +# Explain selective Imports concept \ No newline at end of file diff --git a/docs/sp/batching.md b/docs/sp/batching.md new file mode 100644 index 000000000..95a7b5949 --- /dev/null +++ b/docs/sp/batching.md @@ -0,0 +1 @@ +// explain batching in SP \ No newline at end of file diff --git a/docs/sp/webs.md b/docs/sp/webs.md new file mode 100644 index 000000000..447a05b53 --- /dev/null +++ b/docs/sp/webs.md @@ -0,0 +1,790 @@ +# @pnp/sp/webs + +Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types. + +## IWebs + +[![](https://img.shields.io/badge/Invokable-informational.svg)](../invokable.md) [![](https://img.shields.io/badge/Selective%20Imports-informational.svg)](../selective-imports.md) +|Scenario|Import Statement| +|--|--| +|Selective 1|import { sp } from "@pnp/sp";
import { Webs, IWebs } from "@pnp/sp/lib/webs";| +|Selective 2|import { sp } from "@pnp/sp";
import "@pnp/sp/lib/webs";| +|Preset: All|import { sp, Webs, IWebs } from "@pnp/sp/presents/all";| +|Preset: Core|import { sp, Webs, IWebs } from "@pnp/sp/presents/core";| + + +### Add Web + +Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions. + +```TypeScript +import { sp, IWebAddResult } from "@pnp/sp"; + +const result = await sp.web.webs.add("title", "subweb1"); + +// show the response from the server when adding the web +console.log(result.data); + +// we can immediately operate on the new web +result.web.select("Title").get().then((w: IWebAddResult) => { + + // show our title + console.log(w.Title); +}); +``` + +```TypeScript +import { sp, IWebAddResult } from "@pnp/sp"; + +// create a German language wiki site with title, url, description, which does not inherit permissions +sp.web.webs.add("wiki", "subweb2", "a wiki web", "WIKI#0", 1031, false).then((w: IWebAddResult) => { + + // ... +}); +``` + +## IWeb + +[![](https://img.shields.io/badge/Invokable-informational.svg)](../invokable.md) [![](https://img.shields.io/badge/Selective%20Imports-informational.svg)](../selective-imports.md) + +|Scenario|Import Statement| +|--|--| +|Selective 1|import { sp } from "@pnp/sp";
import { Web, IWeb } from "@pnp/sp/lib/webs";| +|Selective 2|import { sp } from "@pnp/sp";
import "@pnp/sp/lib/webs";| +|Preset: All|import { sp, Web, IWeb } from "@pnp/sp/presents/all";| +|Preset: Core|import { sp, Web, IWeb } from "@pnp/sp/presents/core";| + +### Access a Web + +There are several ways to access a web instance, each of these methods is equivelent in that you will have an IWeb instance to work with. All of the examples below use a variable named "web" which represents an IWeb instance - regardless of how it was initially accessed. + +**Access the web from the imported "sp" object using selective import:** + +```TypeScript +import { sp } from "@pnp/sp"; +import "@pnp/sp/lib/webs"; + +const r = await sp.web(); +``` + +**Access the web from the imported "sp" using the 'all' preset** + +```TypeScript +import { sp } from "@pnp/sp/presets/all"; + +const r = await sp.web(); +``` + +**Access the web from the imported "sp" using the 'core' preset** + +```TypeScript +import { sp } from "@pnp/sp/presets/core"; + +const r = await sp.web(); +``` + +**Create a web instance using the factory function** + +```TypeScript +import { Web } from "@pnp/sp/lib/web"; + +const web = Web("https://something.sharepoint.com/sites/dev"); +const r = await web(); +``` + +### webs + +Access the child [webs collection](#Webs%20Collection) of this web + +```TypeScript +const webs = web.webs(); +``` + +### Get A Web's properties + +```TypeScript +// basic get of the webs properties +const props = await web(); + +// use odata operators to get specific fields +const props2 = await web.select("Title")(); + +// type the result to match what you are requesting +const props3 = await web.select("Title")<{ Title: string }>(); +``` + +### getParentWeb + +Get the data and IWeb instance for the parent web for the given web instance + +```TypeScript +import { IOpenWebByIdResult } from "@pnp/sp/lib/sites"; +const web: IOpenWebByIdResult = web.getParentWeb(); +``` + +### getSubwebsFilteredForCurrentUser + +Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member. + +```TypeScript +const subWebs = await web.getSubwebsFilteredForCurrentUser().get(); +``` + +### allProperties + +Allows access to the web's all properties collection. This is readonly in REST. + +```TypeScript +const props = await web.allProperties(); + +// select certain props +const props2 = await web.allProperties.select("prop1", "prop2")(); +``` + +### webinfos + +Gets a collection of WebInfos for this web's subwebs + +```TypeScript +const infos = await web.webinfos(); + +// or select certain fields +const infos2 = await web.webinfos.select("Title", "Description")(); + +// or filter +const infos3 = await web.webinfos.filter("Title eq 'MyWebTitle'")(); + +// or both +const infos4 = await web.webinfos.select("Title", "Description").filter("Title eq 'MyWebTitle'")(); + +// get the top 4 ordered by Title +const infos5 = await web.webinfos.top(4).orderBy("Title")(); +``` + +### update + +Updates this web instance with the supplied properties + +```TypeScript + +// update the web's title and description +const result = await web.update({ + Title: "New Title", + Description: "My new description", +}); + +// a project implementation could wrap the update to provide type information for your expected fields: +import { IWebUpdateResult } from "@pnp/sp/lib/webs"; + +interface IWebUpdateProps { + Title: string; + Description: string; +} + +function updateWeb(props: IWebUpdateProps): Promise { + web.update(props); +} +``` + +### Delete a Web + +```TypeScript +await web.delete(); +``` + +### applyTheme + +Applies the theme specified by the contents of each of the files specified in the arguments to the site + +```TypeScript +import { combine } from "@pnp/common"; + +// we are going to apply the theme to this sub web as an example +const web = Web("https://318studios.sharepoint.com/sites/dev/subweb"); + +// the urls to the color and font need to both be from the catalog at the root +// these urls can be constants or calculated from existing urls +const colorUrl = combine("/", "sites/dev", "_catalogs/theme/15/palette011.spcolor"); +// this gives us the same result +const fontUrl = "/sites/dev/_catalogs/theme/15/fontscheme007.spfont"; + +// apply the font and color, no background image, and don't share this theme +await web.applyTheme(colorUrl, fontUrl, "", false); +``` + +### applyWebTemplate & availableWebTemplates + +Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios. + +```TypeScript +const templates = (await web.availableWebTemplates().select("Name")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name)); + +// apply the wiki template +const template = templates.length > 0 ? templates[0].Name : "STS#0"; + +await web.applyWebTemplate(template); +``` + +### getChanges + +Returns the collection of changes from the change log that have occurred within the web, based on the specified query. + +```TypeScript +// get the web changes including add, update, and delete +const changes = await web.getChanges({ + Add: true, + ChangeTokenEnd: null, + ChangeTokenStart: null, + DeleteObject: true, + Update: true, + Web: true, + }); +``` + +### mapToIcon + +Returns the name of the image file for the icon that is used to represent the specified file + +```TypeScript +import { combine } from "@pnp/common"; + +const iconFileName = await web.mapToIcon("test.docx"); +// iconPath === "icdocx.png" +// which you can need to map to a real url +const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`; + +// OR dynamically +const webData = await sp.web.select("Url")(); +const iconFullPath2 = combine(webData.Url, "_layouts", "images", iconFileName); + +// OR within SPFx using the context +const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, "_layouts", "images", iconFileName); + +// You can also set size +// 16x16 pixels = 0, 32x32 pixels = 1 +const icon32FileName = await web.mapToIcon("test.docx", 1); +``` + +### storage entities + +```TypeScript +import { sp } from "@pnp/sp"; +import "@pnp/sp/lib/appcatalog"; +import { IStorageEntity } from "@pnp/sp/lib/webs"; + +// needs to be unique, GUIDs are great +const key = "my-storage-key"; + +// read an existing entity +const entity: IStorageEntity = await web.getStorageEntity(key); + +// setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site +// you can get the tenant app catalog using the getTenantAppCatalogWeb +const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb(); + +tenantAppCatalogWeb.setStorageEntity(key, "new value"); + +// set other properties +tenantAppCatalogWeb.setStorageEntity(key, "another value", "description", "comments"); + +const entity2: IStorageEntity = await web.getStorageEntity(key); +/* +entity2 === { + Value: "another value", + Comment: "comments"; + Description: "description", +}; +*/ + +// you can also remove a storage entity +await tenantAppCatalogWeb.removeStorageEntity(key); +``` + +## appcatalog imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/appcatalog";| +|Selective 2|import "@pnp/sp/lib/appcatalog/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### getAppCatalog + +Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url. + +```TypeScript +import { IApp } from "@pnp/sp/lib/appcatalog"; + +const appWeb = web.getAppCatalog(); +// appWeb url === web url + +const app: IApp = appWeb.getAppById("{your app id}"); + +const appWeb2 = web.getAppCatalog("https://tenant.sharepoing.com/sites/someappcatalog"); +// appWeb2 url === "https://tenant.sharepoing.com/sites/someappcatalog" +``` + +## client-side-pages imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/client-side-pages";| +|Selective 2|import "@pnp/sp/lib/client-side-pages/web";| +|Preset: All|import { sp, Web, IWeb } from "@pnp/sp/presents/all";| + +# TODO (need to update code to latest from 1.x branch) + + +## content-type imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/content-types";| +|Selective 2|import "@pnp/sp/lib/content-types/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### contentTypes + +Allows access to the collection of content types in this web. + +```TypeScript +const cts = await web.contentTypes(); + +// you can also select fields and use other odata operators +const cts2 = await web.contentTypes.select("Name")(); +``` + +## features imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/features";| +|Selective 2|import "@pnp/sp/lib/features/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### features + +Allows access to the collection of content types in this web. + +```TypeScript +const features = await web.features(); +``` + +## fields imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/fields";| +|Selective 2|import "@pnp/sp/lib/fields/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### fields + +Allows access to the collection of fields in this web. + +```TypeScript +const fields = await web.fields(); +``` + +## files imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/files";| +|Selective 2|import "@pnp/sp/lib/files/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### getFileByServerRelativeUrl + +Gets a file by server relative url + +```TypeScript +import { IFile } from "@pnp/sp/lib/files"; + +const file: IFile = web.getFileByServerRelativeUrl("/sites/dev/library/myfile.docx"); +``` + +### getFileByServerRelativePath + +Gets a file by server relative url if your file name contains # and % characters + +```TypeScript +import { IFile } from "@pnp/sp/lib/files"; + +const file: IFile = web.getFileByServerRelativePath("/sites/dev/library/my # file%.docx"); +``` + +## folders imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/folders";| +|Selective 2|import "@pnp/sp/lib/folders/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### folders + +Gets the collection of folders in this web + +```TypeScript +const folders = await web.folders(); + +// you can also filter and select as with any collection +const folders2 = await web.folders.select("ServerRelativeUrl", "TimeLastModified").filter("ItemCount gt 0")(); + +// or get the most recently modified folder +const folders2 = await web.folders.orderBy("TimeLastModified").top(1)(); +``` + +### rootFolder + +Gets the root folder of the web + +```TypeScript +const folder = await web.rootFolder(); +``` + +### getFolderByServerRelativeUrl + +Gets a folder by server relative url + +```TypeScript +import { IFolder } from "@pnp/sp/lib/folders"; + +const folder: IFolder = web.getFolderByServerRelativeUrl("/sites/dev/library"); +``` + +### getFolderByServerRelativePath + +Gets a folder by server relative url if your folder name contains # and % characters + +```TypeScript +import { IFolder } from "@pnp/sp/lib/folders"; + +const folder: IFolder = web.getFolderByServerRelativePath("/sites/dev/library/my # folder%/"); +``` + +## hubsites imports + +|Scenario|Import Statement| +|--|--| +|Selective 1|import "@pnp/sp/lib/hubsites";| +|Selective 2|import "@pnp/sp/lib/hubsites/web";| +|Preset: All|import { sp } from "@pnp/sp/presents/all";| + +### hubSiteData + +Gets hub site data for the current web + +```TypeScript +import { IHubSiteWebData } from "@pnp/sp/lib/hubsites"; + +// get the data and force a refresh +const data: IHubSiteWebData = await web.hubSiteData(true); +``` + +### syncHubSiteTheme + +Applies theme updates from the parent hub site collection + +```TypeScript +await web.syncHubSiteTheme(); +``` + +## lists imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/lists"; +Selective 2|import "@pnp/sp/lib/lists/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; +Preset: Core|import { sp } from "@pnp/sp/presents/core"; + +### lists + +Gets the collection of all lists that are contained in the Web site + +```TypeScript +import { ILists } from "@pnp/sp/lib/lists"; + +const lists: ILists = web.lists; + +// you can always order the lists and select properties +const data = await lists.select("Title").orderBy("Title")(); + +// and use other odata operators as well +const data2 = await web.lists.top(3).orderBy("LastItemModifiedDate")(); +``` + +### siteUserInfoList + +Gets the UserInfo list of the site collection that contains the Web site + +```TypeScript +import { IList } from "@pnp/sp/lib/lists"; + +const list: IList = web.siteUserInfoList; + +const data = await list(); + +// or chain off that list to get additional details +const items = await list.items.top(2)(); +``` + +### defaultDocumentLibrary + +Get a reference the default documents library of a web + +```TypeScript +import { IList } from "@pnp/sp/lib/lists"; + +const list: IList = web.defaultDocumentLibrary; +``` + +### customListTemplates + +Gets the collection of all list definitions and list templates that are available + +```TypeScript +import { IList } from "@pnp/sp/lib/lists"; + +const templates = await web.customListTemplates(); + +// odata operators chain off the collection as expected +const templates2 = await web.customListTemplates.select("Title")(); +``` + +### getList + +Gets a list by server relative url (list's root folder) + +```TypeScript +import { IList } from "@pnp/sp/lib/lists"; + +const list: IList = web.getList("/sites/dev/lists/test"); + +const listData = list(); +``` + +### getCatalog + +Returns the list gallery on the site + +Name | Value +--- | --- +WebTemplateCatalog | 111 +WebPartCatalog | 113 +ListTemplateCatalog | 114 +MasterPageCatalog | 116 +SolutionCatalog | 121 +ThemeCatalog | 123 +DesignCatalog | 124 +AppDataCatalog | 125 + +```TypeScript +import { IList } from "@pnp/sp/lib/lists"; + +const templateCatalog: IList = await web.getCatalog(111); + +const themeCatalog: IList = await web.getCatalog(123); +``` + +## navigation imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/navigation"; +Selective 2|import "@pnp/sp/lib/navigation/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; + +### navigation + +Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar + +```TypeScript +import { INavigation } from "@pnp/sp/lib/navigation"; + +const nav: INavigation = web.navigation; + +const navData = await nav(); +``` + +## regional-settings imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/regional-settings"; +Selective 2|import "@pnp/sp/lib/regional-settings/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; + +```TypeScript +import { IRegionalSettings } from "@pnp/sp/lib/navigation"; + +const settings: IRegionalSettings = web.regionalSettings; + +const settingsData = await settings(); +``` + +## related-items imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/related-items"; +Selective 2|import "@pnp/sp/lib/related-items/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; + +```TypeScript +import { IRelatedItemManager, IRelatedItem } from "@pnp/sp/lib/related-items"; + +const manager: IRelatedItemManager = web.relatedItems; + +const data: IRelatedItem[] = await manager.getRelatedItems("{list name}", 4); +``` + +## security imports + +# TODO:: link to the security page and document all there, no need to duplicate + +## sharing imports + +# TODO:: link to the sharing page and document all there, no need to duplicate + +## site-groups imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/site-groups"; +Selective 2|import "@pnp/sp/lib/site-groups/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; + +### siteGroups + +The site groups + +```TypeScript +const groups = await web.siteGroups(); + +const groups2 = await web.siteGroups.top(2)(); +``` + +### associatedOwnerGroup + +The web's owner group + +```TypeScript +const group = await web.associatedOwnerGroup(); + +const users = await web.associatedOwnerGroup.users(); +``` + +### associatedMemberGroup + +The web's member group + +```TypeScript +const group = await web.associatedMemberGroup(); + +const users = await web.associatedMemberGroup.users(); +``` + +### associatedVisitorGroup + +The web's visitor group + +```TypeScript +const group = await web.associatedVisitorGroup(); + +const users = await web.associatedVisitorGroup.users(); +``` + +### createDefaultAssociatedGroups + +Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups + +```TypeScript +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}"); + +// copy the role assignments +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}", true); + +// don't clear sub assignments +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, false); + +// specify secondary owner, don't copy permissions, clear sub scopes +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, true, "{second owner login}"); +``` + +## site-users imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/site-users"; +Selective 2|import "@pnp/sp/lib/site-users/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; + +### siteUsers + +The site users + +```TypeScript +const users = await web.siteUsers(); + +const users2 = await web.siteUsers.top(5)(); + +const users3 = await web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent("i:0#.f|m")}')`)(); +``` + +### currentUser + +Information on the current user + +```TypeScript +const user = await web.currentUser(); + +// check the login name of the current user +const user2 = await web.currentUser.select("LoginName")(); +``` + +### ensureUser + +Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web + +```TypeScript +import { IWebEnsureUserResult } from "@pnp/sp/lib/site-users/"; + +const result: IWebEnsureUserResult = await web.ensureUser("i:0#.f|membership|user@domain.onmicrosoft.com"); +``` + +### getUserById + +Returns the user corresponding to the specified member identifier for the current web + +```TypeScript +import { ISiteUser } from "@pnp/sp/lib/site-users/"; + +const user: ISiteUser = web.getUserById(23); + +const userData = await user(); + +const userData2 = await user.select("LoginName")(); +``` + +## user-custom-actions imports + +Scenario|Import Statement +--|-- +Selective 1|import "@pnp/sp/lib/user-custom-actions"; +Selective 2|import "@pnp/sp/lib/user-custom-actions/web"; +Preset: All|import { sp } from "@pnp/sp/presents/all"; + +## userCustomActions + +Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection + +```TypeScript +import { IUserCustomActions } from "@pnp/sp/lib/user-custom-actions"; + +const actions: IUserCustomActions = web.userCustomActions; + +const actionsData = await actions(); +``` diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..bc6d2c864 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1 @@ +require("./tools/gulptasks"); diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..156d0b0b4 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,131 @@ +site_name: PnP/PnPjs +docs_dir: 'packages' +nav: + - Home: 'index.md' + - 'Getting Started': 'documentation/getting-started.md' + - 'Transition Guide': 'documentation/transition-guide.md' + - General: + - 'Getting Started': 'documentation/getting-started.md' + - 'Getting Started Contributing': 'documentation/getting-started-dev.md' + - 'Gulp Commands': 'documentation/gulp-commands.md' + - Debugging: 'documentation/debugging.md' + - 'Building Docs': 'documentation/documentation.md' + - Deployment: 'documentation/deployment.md' + - 'Install Beta Versions': 'documentation/beta-versions.md' + - Polyfills: 'documentation/polyfill.md' + - 'Package Structure': 'documentation/package-structure.md' + - 'SPFx On-Premises 2016': 'documentation/SPFx-On-Premesis-2016.md' + - 'Transition Guide': 'documentation/transition-guide.md' + - Packages: + - Packages: 'documentation/packages.md' + - common: + - common: 'common/docs/index.md' + - adalclient: 'common/docs/adalclient.md' + - blobutil: 'common/docs/blobutil.md' + - collections: 'common/docs/collections.md' + - 'Custom HttpClientImpl': 'common/docs/custom-httpclientimpl.md' + - decorators: 'common/docs/decorators.md' + - exceptions: 'common/docs/exceptions.md' + - libconfig: 'common/docs/libconfig.md' + - netutil: 'common/docs/netutil.md' + - storage: 'common/docs/storage.md' + - util: 'common/docs/util.md' + - config-store: + - config-store: 'config-store/docs/index.md' + - configuration: 'config-store/docs/configuration.md' + - providers: 'config-store/docs/providers.md' + - graph: + - graph: 'graph/docs/index.md' + - contacts: 'graph/docs/contacts.md' + - 'directory objects': 'graph/docs/directoryobjects.md' + - invitations: 'graph/docs/invitations.md' + - onedrive: 'graph/docs/onedrive.md' + - planner: 'graph/docs/planner.md' + - subscriptions: 'graph/docs/subscriptions.md' + - teams: 'graph/docs/teams.md' + - logging: 'logging/docs/index.md' + - nodejs: + - nodejs: 'nodejs/docs/index.md' + - AdalFetchClient: 'nodejs/docs/adal-fetch-client.md' + - SPFetchClient: 'nodejs/docs/sp-fetch-client.md' + - BearerTokenFetchClient: 'nodejs/docs/bearer-token-fetch-client.md' + - ProviderHostedRequestContext: 'nodejs/docs/provider-hosted-app.md' + - odata: + - odata: 'odata/docs/index.md' + - caching: 'odata/docs/caching.md' + - core: 'odata/docs/core.md' + - 'OData Batching': 'odata/docs/odata-batch.md' + - Parsers: 'odata/docs/parsers.md' + - Pipeline: 'odata/docs/pipeline.md' + - Queryable: 'odata/docs/queryable.md' + - pnpjs: 'pnpjs/docs/index.md' + - sp: + - sp: 'sp/docs/index.md' + - 'Alias Parameters': 'sp/docs/alias-parameters.md' + - 'ALM api': 'sp/docs/alm.md' + - Attachments: 'sp/docs/attachments.md' + - 'Client-side Pages': 'sp/docs/client-side-pages.md' + - 'Content Types': 'sp/docs/content-types.md' + - 'Entity Merging': 'sp/docs/entity-merging.md' + - Features: 'sp/docs/features.md' + - Fields: 'sp/docs/fields.md' + - Files: 'sp/docs/files.md' + - 'List Items': 'sp/docs/items.md' + - 'Navigation Service': 'sp/docs/navigation-service.md' + - Permissions: 'sp/docs/permissions.md' + - Profiles: 'sp/docs/profiles.md' + - 'Related Items': 'sp/docs/related-items.md' + - Search: 'sp/docs/search.md' + - Sharing: 'sp/docs/sharing.md' + - Sites: 'sp/docs/sites.md' + - Site Designs: 'sp/docs/sitedesigns.md' + - Social: 'sp/docs/social.md' + - 'SP.Utilities.Utility': 'sp/docs/sp-utilities-utility.md' + - 'Tenant Properties': 'sp/docs/tenant-properties.md' + - Views: 'sp/docs/views.md' + - Webs: 'sp/docs/webs.md' + - 'Comments and Likes': 'sp/docs/comments-likes.md' + - sp-addinhelpers: + - sp-addinhelpers: 'sp-addinhelpers/docs/index.md' + - SPRequestExecutorClient: 'sp-addinhelpers/docs/sp-request-executor-client.md' + - SPRestAddIn: 'sp-addinhelpers/docs/sp-rest-addin.md' + - sp-clientsvc: 'sp-clientsvc/docs/index.md' + - sp-taxonomy: + - sp-taxonomy: 'sp-taxonomy/docs/index.md' + - 'Term Stores': 'sp-taxonomy/docs/term-stores.md' + - 'Term Groups': 'sp-taxonomy/docs/term-groups.md' + - 'Term Sets': 'sp-taxonomy/docs/term-sets.md' + - Terms: 'sp-taxonomy/docs/terms.md' + - Labels: 'sp-taxonomy/docs/labels.md' + - Utilities: 'sp-taxonomy/docs/utilities.md' +theme: + name: 'material' + custom_dir: 'packages/documentation/theme' + palette: + primary: 'blue' + logo: 'documentation/img/Logo.png' + feature: + tabs: true +extra_css: + - 'documentation/css/extra.css' +markdown_extensions: + - admonition + - codehilite: + guess_lang: false + - toc: + permalink: true +plugins: + - search + - markdownextradata +extra: + version: '1.2.8' + social: + - type: 'twitter' + link: 'https://twitter.com/officedevpnp' + - type: 'youtube' + link: 'http://aka.ms/sppnp-videos' + - type: 'link' + link: 'https://aka.ms/sppnp' +repo_url: http://github.com/pnp/pnpjs +edit_uri: tree/dev/packages +site_url: https://pnp.github.io/pnpjs/ diff --git a/package.json b/package.json new file mode 100644 index 000000000..75fbf8f23 --- /dev/null +++ b/package.json @@ -0,0 +1,83 @@ +{ + "name": "@pnp/monorepo", + "private": true, + "version": "2.0.0-0", + "description": "A JavaScript library for SharePoint development.", + "dependencies": {}, + "devDependencies": { + "@microsoft/microsoft-graph-types": "1.5.0", + "@types/adal-angular": "1.0.1", + "@types/chai": "4.1.6", + "@types/chai-as-promised": "7.1.0", + "@types/core-js": "2.5.0", + "@types/jsonwebtoken": "7.2.8", + "@types/mocha": "^5.2.6", + "@types/node": "10.11.3", + "@types/sharepoint": "2016.1.1", + "@types/webpack": "4.4.17", + "@types/whatwg-fetch": "0.0.33", + "adal-angular": "1.0.17", + "adal-node": "0.1.28", + "ansi-colors": "3.2.1", + "chai": "4.2.0", + "chai-as-promised": "7.1.1", + "del": "3.0.0", + "fancy-log": "1.3.2", + "gulp": "3.9.1", + "gulp-istanbul": "1.1.3", + "gulp-mocha": "6.0.0", + "gulp-replace": "1.0.0", + "gulp-tslint": "8.1.3", + "jsonwebtoken": "8.3.0", + "mocha": "6.1.3", + "node-fetch": "2.2.0", + "pump": "3.0.0", + "replace-in-file": "3.4.2", + "rollup": "0.66.6", + "rollup-plugin-node-globals": "1.4.0", + "rollup-plugin-node-resolve": "3.4.0", + "rollup-plugin-sourcemaps": "0.4.2", + "rollup-plugin-uglify": "6.0.0", + "string-replace-loader": "2.1.1", + "ts-loader": "5.2.1", + "tsconfig-paths-webpack-plugin": "3.2.0", + "tslib": "1.9.3", + "tslint": "5.11.0", + "typescript": "3.1.3", + "webpack": "4.22.0", + "webpack-cli": "3.1.2", + "webpack-dev-server": "3.1.11", + "yargs": "12.0.2" + }, + "scripts": { + "start": "gulp serve", + "test": "gulp test", + "package": "gulp package", + "lint": "tslint ./packages/**/*.ts" + }, + "repository": { + "type": "git", + "url": "git://github.com/pnp/pnpjs" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "keywords": [ + "sharepoint", + "office365", + "tools", + "spfx", + "sharepoint framework" + ], + "maintainers": [ + { + "name": "patrick-rodgers", + "email": "patrick.rodgers@microsoft.com" + } + ], + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs" +} diff --git a/packages/common/__tests/collections.test.ts b/packages/common/__tests/collections.test.ts new file mode 100644 index 000000000..99ac65e1a --- /dev/null +++ b/packages/common/__tests/collections.test.ts @@ -0,0 +1,61 @@ +import { expect } from "chai"; +import { mergeMaps } from ".."; + +describe("Collections", () => { + + describe("mergeMaps", () => { + + it("should merge to maps with unique keys", () => { + + const map1 = new Map([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]]); + const map2 = new Map([["2_key1", "2_value1"], ["2_key2", "2_value2"], ["2_key3", "2_value3"]]); + const map = mergeMaps(map1, map2); + + expect(map.size).to.eq(6, "Size should be 6"); + expect(Array.from(map)[1][0]).to.eq("key2", "Should be able to spread map"); + }); + + it("should merge to maps with common keys", () => { + + const map1 = new Map([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]]); + const map2 = new Map([["2_key1", "2_value1"], ["2_key2", "2_value2"], ["key3", "2_value3"]]); + const map = mergeMaps(map1, map2); + + expect(map.size).to.eq(5, "Size should be 5"); + expect(Array.from(map)[2][1]).to.eq("2_value3", "Should overwrite the value"); + expect(Array.from(map)[4][1]).to.eq("2_value2", "Should overwrite the value"); + }); + + it("should merge many maps - even", () => { + + const target = new Map([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]]); + const maps = []; + const sub: [string, string][] = []; + for (let i = 0; i < 10; i++) { + for (let j = 0; j < i + 1; j++) { + sub.push([`${i}_key_${j}`, `${i}_value1_${j}`]); + } + maps.push(new Map(sub)); + } + + const map = mergeMaps(target, ...maps); + expect(map.size).to.eq(58, "Size should be 58"); + }); + + it("should merge many maps - odd", () => { + + const target = new Map([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]]); + const maps = []; + const sub: [string, string][] = []; + for (let i = 0; i < 3; i++) { + for (let j = 0; j < i + 1; j++) { + sub.push([`${i}_key_${j}`, `${i}_value1_${j}`]); + } + maps.push(new Map(sub)); + } + + const map = mergeMaps(target, ...maps); + expect(map.size).to.eq(9, "Size should be 9"); + }); + }); +}); diff --git a/packages/common/__tests/mock-storage.ts b/packages/common/__tests/mock-storage.ts new file mode 100644 index 000000000..fa2b84307 --- /dev/null +++ b/packages/common/__tests/mock-storage.ts @@ -0,0 +1,34 @@ +export default class MockStorage implements Storage { + constructor(private _store = new Map()) { } + + public get length(): number { + return this._store.size; + } + + public set length(i: number) { + this._length = i; + } + + public clear(): void { + this._store.clear(); + } + + public getItem(key: string): any { + return this._store.get(key); + } + + public key(index: number): string { + return Array.from(this._store)[index][0]; + } + + public removeItem(key: string): void { + this._store.delete(key); + } + + public setItem(key: string, data: string): void { + this._store.set(key, data); + } + + [key: string]: any; + [index: number]: string; +} diff --git a/packages/common/__tests/storage.test.ts b/packages/common/__tests/storage.test.ts new file mode 100644 index 000000000..4fe1612ba --- /dev/null +++ b/packages/common/__tests/storage.test.ts @@ -0,0 +1,37 @@ +import { expect } from "chai"; +import { PnPClientStorageWrapper} from ".."; +import MockStorage from "./mock-storage"; + +describe("Storage", () => { + + describe("PnPClientStorageWrapper", () => { + + let wrapper: PnPClientStorageWrapper; + + beforeEach(() => { + const store: Storage = (typeof localStorage === "undefined") ? new MockStorage() : localStorage; + wrapper = new PnPClientStorageWrapper(store); + }); + + it("Add and Get a value", () => { + wrapper.put("test", "value"); + const ret = wrapper.get("test"); + expect(ret).to.eq("value"); + }); + + it("Add two values, remove one and still return the other", () => { + wrapper.put("test1", "value1"); + wrapper.put("test2", "value2"); + wrapper.delete("test1"); + const ret = wrapper.get("test2"); + expect(ret).to.eq("value2"); + }); + + it("Use getOrPut to add a value using a getter function and return it", () => { + wrapper.getOrPut("test", () => { return new Promise(() => "value"); }).then(() => { + const ret = wrapper.get("test"); + expect(ret).to.eq("value"); + }); + }); + }); +}); diff --git a/packages/common/__tests/util.test.ts b/packages/common/__tests/util.test.ts new file mode 100644 index 000000000..f38f50ac4 --- /dev/null +++ b/packages/common/__tests/util.test.ts @@ -0,0 +1,202 @@ +import { expect } from "chai"; +import { getCtxCallback } from ".."; +import { dateAdd, combine, getRandomString, getGUID, isFunc, isArray, getAttrValueFromString, extend } from ".."; + +describe("extend", () => { + + it("Should extend an object with odd fields", () => { + + const o1 = { + title: "thing", + }; + + const o2 = { + desc: "another", + }; + + const o = extend(o1, o2); + + expect(o).to.deep.eq({ title: "thing", desc: "another" }); + }); + + it("Should extend an object with even fields", () => { + + const o1 = { + desc: "another", + title: "thing", + }; + + const o2 = { + bob: "sam", + sara: "wendy", + }; + + const o = extend(o1, o2); + + expect(o).to.deep.eq({ desc: "another", title: "thing", bob: "sam", sara: "wendy" }); + }); + + it("Should overwrite fields", () => { + + const o1 = { + title: "thing", + }; + + const o2 = { + title: "new", + }; + + const o = extend(o1, o2); + + expect(o).to.deep.eq({ title: "new" }); + }); + + it("Should not overwrite fields", () => { + + const o1 = { + title: "thing", + }; + + const o2 = { + title: "new", + }; + + const o = extend(o1, o2, true); + + expect(o).to.deep.eq({ title: "thing" }); + }); + + it("Should field fields", () => { + + const o1 = { + title: "thing", + }; + + const o2 = { + bob: "new", + sara: "wendy", + }; + + const o = extend(o1, o2, false, (name) => name !== "bob"); + + expect(o).to.deep.eq({ title: "thing", sara: "wendy" }); + }); +}); + +describe("getCtxCallback", () => { + it("Should create contextual callback", () => { + + class Test { + constructor(public num = 1) { } + public func(a: number) { + this.num += a; + } + } + + const t = new Test(); + + const callback = getCtxCallback(t, t.func, 7); + expect(callback).to.be.a("function"); + // this call will update ctx var inside the callback + expect(t.num).to.eq(1); + callback(); + expect(t.num).to.eq(8); + }); +}); + +describe("dateAdd", () => { + it("Should add 5 minutes to a date", () => { + const testDate = new Date(); + const checkDate = new Date(testDate.toLocaleString()); + checkDate.setMinutes(testDate.getMinutes() + 5); + expect(dateAdd(testDate, "minute", 5).getMinutes()).to.eq(checkDate.getMinutes()); + }); + + it("Should add 2 years to a date", () => { + const testDate = new Date(); + const checkDate = new Date(testDate.toLocaleString()); + checkDate.setFullYear(testDate.getFullYear() + 2); + expect(dateAdd(testDate, "year", 2).getFullYear()).to.eq(checkDate.getFullYear()); + }); +}); + +describe("combinePaths", () => { + it("Should combine the paths '/path/', 'path2', 'path3' and '/path4' to be path/path2/path3/path4", () => { + expect(combine("/path/", "path2", "path3", "/path4")).to.eq("path/path2/path3/path4"); + }); + + it("Should combine the paths 'http://site/path/' and '/path4/page.aspx' to be http://site/path/path4/page.aspx", () => { + expect(combine("http://site/path/", "/path4/page.aspx")).to.eq("http://site/path/path4/page.aspx"); + }); + + it("Should combine the paths null, 'path2', undefined, null and '/path4' to be path2/path4", () => { + expect(combine(null, "path2", undefined, null, "/path4")).to.eq("path2/path4"); + }); + + it("Should combine the paths null, 'path2', undefined, \"\", null and '/path4' to be path2/path4", () => { + expect(combine(null, "path2", undefined, "", null, "/path4")).to.eq("path2/path4"); + }); + + it("Should not error with no arguments specified", () => { + expect(combine()).to.eq(""); + }); +}); + +describe("getRandomString", () => { + it("Should produce a random string of length 5", () => { + const j = getRandomString(5); + expect(j).to.be.a("string"); + expect(j).to.have.length(5); + }); + + it("Should produce a random string of length 28", () => { + const j = getRandomString(28); + expect(j).to.be.a("string"); + expect(j).to.have.length(28); + }); +}); + +describe("getGUID", () => { + it("Should produce a GUID matching the expected pattern", () => { + expect(getGUID()).to.match(/[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}/i); + }); +}); + +describe("isFunction", () => { + it("Should find that a function is a function", () => { + expect(isFunc(() => { return; })).to.be.true; + }); + + it("Should find that a non-function is not a function", () => { + expect(isFunc({ val: 0 })).to.be.false; + expect(isFunc(null)).to.be.false; + expect(isFunc(undefined)).to.be.false; + }); +}); + +describe("isArray", () => { + it("Should find that an Array is an Array", () => { + expect(isArray([1, 2, 3, 4])).to.be.true; + }); + + it("Should find that a non-Array is not an Array", () => { + expect(isArray(null)).to.be.false; + expect(isArray("")).to.be.false; + expect(isArray(3)).to.be.false; + expect(isArray({})).to.be.false; + expect(isArray(undefined)).to.be.false; + }); +}); + +describe("getAttrValueFromString", () => { + + it("Should correctly parse attribute values", () => { + expect(getAttrValueFromString(``, "att")).to.eq("value"); + expect(getAttrValueFromString(``, "att")).to.eq("value1293\\.\\?,/\\|!@#\\$%\\^&\\*\\(\\)\\[\\]\\{\\}"); + expect(getAttrValueFromString(``, "att")).to.eq("value"); + expect(getAttrValueFromString(``, "att")).to.eq("value\""); + expect(getAttrValueFromString(``, "att")).to.eq("value'"); + expect(getAttrValueFromString(``, "att")).to.eq("value value"); + expect(getAttrValueFromString(``, "att")).to.eq("value"); + }); +}); diff --git a/packages/common/docs/adalclient.md b/packages/common/docs/adalclient.md new file mode 100644 index 000000000..67cee4443 --- /dev/null +++ b/packages/common/docs/adalclient.md @@ -0,0 +1,169 @@ +# @pnp/common/adalclient + +_Added in 1.0.4_ + +This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with +SharePoint Framework's permissions. + +## Setup and Use inside SharePoint Framework + +Using the SharePoint Framework is the prefered way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on yoru behalf. You can also read more about how this process works and the necessary SPFx configurations in the [SharePoint Framework 1.6 release notes](https://github.com/SharePoint/sp-dev-docs/wiki/SharePoint-Framework-v1.6-release-notes#moving-from-beta-to-public---webapi). This method only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined above using the constructor to specify the values for an AAD Application you have setup. + +#### Calling the graph api + +By providing the context in the onInit we can create the adal client from known information. + +```TypeScript +import { graph } from "@pnp/graph"; +import { getRandomString } from "@pnp/common"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + graph.setup({ + spfxContext: this.context + }); + }); +} + +public render(): void { + + // here we are creating a team with a random name, required Group ReadWrite All permissions + const teamName = `ATeam.${getRandomString(4)}`; + + this.domElement.innerHTML = `Hello, I am creating a team named "${teamName}" for you...`; + + graph.teams.create(teamName, "This is a description").then(t => { + + this.domElement.innerHTML += "done!"; + + }).catch(e => { + + this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`; + }); +} +``` + +#### Calling the SharePoint API + +This example shows how to use the ADALClient with the @pnp/sp library to call + +```TypeScript +import { sp } from "@pnp/sp"; +import { AdalClient } from "@pnp/common"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + sp.setup({ + spfxContext: this.context, + sp: { + fetchClientFactory: () => , + }, + }); + + }); +} + +public render(): void { + + sp.web.get().then(t => { + this.domElement.innerHTML = JSON.stringify(t); + }).catch(e => { + this.domElement.innerHTML = JSON.stringify(e); + }); +} +``` + +#### Calling the any API + +You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. + +```TypeScript +import { AdalClient, FetchOptions } from "@pnp/common"; +import { ODataDefaultParser } from "@pnp/odata"; + +// ... + +public render(): void { + + // create an ADAL Client + const client = AdalClient.fromSPFxContext(this.context); + + // setup the request options + const opts: FetchOptions = { + method: "GET", + headers: { + "Accept": "application/json", + }, + }; + + // execute the request + client.fetch("https://318studios.sharepoint.com/_api/web", opts).then(response => { + + // create a parser to convert the response into JSON. + // You can create your own, at this point you have a fetch Response to work with + const parser = new ODataDefaultParser(); + + parser.parse(response).then(json => { + this.domElement.innerHTML = JSON.stringify(json); + }); + + }).catch(e => { + this.domElement.innerHTML = JSON.stringify(e); + }); + +} +``` + +## Manually Configure + +This example shows setting up and using the AdalClient to make queries using information you have setup. You can [review this article](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/connect-to-api-secured-with-aad) for more information on setting up and securing any application using AzureAD. + +### Setup and Use with Microsoft Graph + +This sample uses a custom AzureAd app you have created and granted the appropriate permissions. + +```TypeScript +import { AdalClient } from "@pnp/common"; +import { graph } from "@pnp/graph"; + +// configure the graph client +// parameters are: +// client id - the id of the application you created in azure ad +// tenant - can be id or URL (shown) +// redirect url - absolute url of a page to which your application and Azure AD app allows replies +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalClient( + "e3e9048e-ea28-423b-aca9-3ea931cc7972", + "{tenant}.onmicrosoft.com", + "https://myapp/singlesignon.aspx"); + }, + }, +}); + +try { + + // call the graph API + const groups = await graph.groups.get(); + + console.log(JSON.stringify(groups, null, 4)); + +} catch (e) { + console.error(e); +} +``` + +## Nodejs Applications + +[We have a dedicated node client in @pnp/nodejs.](https://pnp.github.io/pnpjs/nodejs/docs/adal-fetch-client/) diff --git a/packages/common/docs/collections.md b/packages/common/docs/collections.md new file mode 100644 index 000000000..d91681abf --- /dev/null +++ b/packages/common/docs/collections.md @@ -0,0 +1,34 @@ +# @pnp/common/collections + +The collections module provides typings and classes related to working with dictionaries. + +## TypedHash + +Interface used to described an object with string keys corresponding to values of type T + +```TypeScript +export interface TypedHash { + [key: string]: T; +} +``` + +## objectToMap + +Converts a plain object to a Map instance + +```TypeScript +const map = objectToMap({ a: "b", c: "d"}); +``` + +## mergeMaps + +Merges two or more maps, overwiting values with the same key. Last value in wins. + +```TypeScript +const m1 = new Map(); +const m2 = new Map(); +const m3 = new Map(); +const m4 = new Map(); + +const m = mergeMaps(m1, m2, m3, m4); +``` diff --git a/packages/common/docs/custom-httpclientimpl.md b/packages/common/docs/custom-httpclientimpl.md new file mode 100644 index 000000000..5d214e347 --- /dev/null +++ b/packages/common/docs/custom-httpclientimpl.md @@ -0,0 +1,65 @@ +# Custom HttpClientImpl + +**This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation.** + +It is possible you may need complete control over the sending and receiving of requests. + +Before you get started read and understand the [fetch specification](https://fetch.spec.whatwg.org/) as you are essentially writing a custom fetch implementation. + +The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. + +```TypeScript +export interface HttpClientImpl { + fetch(url: string, options: FetchOptions): Promise; +} +``` + +There is a single method "fetch" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. + +```TypeScript +export interface FetchOptions { + method?: string; + headers?: HeadersInit | { [index: string]: string }; + body?: BodyInit; + mode?: string | RequestMode; + credentials?: string | RequestCredentials; + cache?: string | RequestCache; +} +``` + +So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the [fetch specification](https://fetch.spec.whatwg.org/) - which you've already read 👍. + +## Using Your Custom HttpClientImpl + +Once you have written your implementation using it on your requests is done by setting it in the global library configuration: + +```TypeScript +import { setup } from "@pnp/common"; +import { sp, Web } from "@pnp/sp"; +import { MyAwesomeClient } from "./awesomeclient"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new MyAwesomeClient(); + } + } +}); + +let w = new Web("{site url}"); + +// this request will use your client. +w.select("Title").get().then(w => { + console.log(w); +}); +``` + +## Subclassing is Better + +You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do _something_ to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation. + +# A FINAL NOTE + +Whatever you do, **do not** write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret. + + diff --git a/packages/common/docs/index.md b/packages/common/docs/index.md new file mode 100644 index 000000000..d271bc72b --- /dev/null +++ b/packages/common/docs/index.md @@ -0,0 +1,34 @@ +# @pnp/common + +[![npm version](https://badge.fury.io/js/%40pnp%2Fcommon.svg)](https://badge.fury.io/js/%40pnp%2Fcommon) + +The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well. + +## Getting Started + +Install the library and required dependencies + +`npm install @pnp/common --save` + +Import and use functionality, see details on modules below. + +```TypeScript +import { getGUID } from "@pnp/common"; + +console.log(getGUID()); +``` + +## Exports + +* [adalclient](adalclient.md) +* [collections](collections.md) +* [libconfig](libconfig.md) +* [netutil](netutil.md) +* [storage](storage.md) +* [util](util.md) +* [Custom HttpClient](custom-httpclientimpl.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-common-uml.svg) + +Graphical UML diagram of @pnp/common. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/common/docs/libconfig.md b/packages/common/docs/libconfig.md new file mode 100644 index 000000000..a7dbbf310 --- /dev/null +++ b/packages/common/docs/libconfig.md @@ -0,0 +1,156 @@ +# @pnp/common/libconfig + +Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core +configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and +contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications. + +## LibraryConfiguration Interface + +Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below + +```TypeScript +export interface LibraryConfiguration { + + /** + * Allows caching to be global disabled, default: false + */ + globalCacheDisable?: boolean; + + /** + * Defines the default store used by the usingCaching method, default: session + */ + defaultCachingStore?: "session" | "local"; + + /** + * Defines the default timeout in seconds used by the usingCaching method, default 30 + */ + defaultCachingTimeoutSeconds?: number; + + /** + * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval + */ + enableCacheExpiration?: boolean; + + /** + * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) + */ + cacheExpirationIntervalMilliseconds?: number; + + /** + * Used to supply the current context from an SPFx webpart to the library + */ + spfxContext?: any; +} +``` + +## RuntimeConfigImpl + +The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a [Dictionary](collections.md) +used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method. + +### extend + +The extend method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any +existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please +see the section below "Using RuntimeConfig within your application". Note there are no methods to remove/clear the global config as it should be considered fairly static +as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. + +```TypeScript +import { RuntimeConfig } from "@pnp/common"; + +// add your custom keys to the global configuration +// note you can use object hashes as values +RuntimeConfig.extend({ + "myKey1": "value 1", + "myKey2": { + "subKey": "sub value 1", + "subKey2": "sub value 2", + }, +}); + +// read your custom values +const v = RuntimeConfig.get("myKey1"); // "value 1" +``` + +## Using RuntimeConfig within your Application + +If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To +do so you will need to create an interface for your configration (optional) and a wrapper class for RuntimeConfig to expose your properties + +```TypeScript +import { LibraryConfiguration, RuntimeConfig } from "@pnp/common"; + +// first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because +// TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. + +// create the interface of your properties +// by creating this separately you allows others to compose your parts into their own config +interface MyConfigurationPart { + + // you can create a grouped definition and access your settings as an object + // keys can be optional or required as defined by your interface + my?: { + prop1?: string; + prop2?: string; + } + + // and/or define multiple top level properties (beware key collision) + // it is good practice to use a unique prefix + myProp1: string; + myProp2: number; +} + +// now create a combined interface +interface MyConfiguration extends LibraryConfiguration, MyConfigurationPart { } + + +// now create a wrapper object and expose your properties +class MyRuntimeConfigImpl { + + // exposing a nested property + public get prop1(): TypedHash { + + const myPart = RuntimeConfig.get("my"); + if (myPart !== null && typeof myPart !== "undefined" && typeof myPart.prop1 !== "undefined") { + return myPart.prop1; + } + + return {}; + } + + // exposing a root level proeprty + public get myProp1(): string | null { + + let myProp1 = RuntimeConfig.get("myProp1"); + + if (myProp1 === null) { + myProp1 = "some default value"; + } + + return myProp1; + } + + setup(config: MyConfiguration): void { + RuntimeConfig.extend(config); + } +} + +// create a single static instance of your impl class +export let MyRuntimeConfig = new MyRuntimeConfigImpl(); +``` + +Now in other files you can use and set your configuration with a typed interface and properties + +```TypeScript +import { MyRuntimeConfig } from "{location of module}"; + + +MyRuntimeConfig.setup({ + my: { + prop1: "hello", + }, +}); + +const value = MyRuntimeConfig.prop1; // "hello" +``` + diff --git a/packages/common/docs/netutil.md b/packages/common/docs/netutil.md new file mode 100644 index 000000000..c4b8d71e3 --- /dev/null +++ b/packages/common/docs/netutil.md @@ -0,0 +1,48 @@ +# @pnp/common/netutil + +This module contains a set of classes and interfaces used to caracterize shared http interactions and configuration of the libraries. Some of the interfaces +are described below (many have no use outside the library) as well as several classes. + +## Interfaces + +### HttpClientImpl + +Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method "fetch" take a URL and +options and returning a Promise. Used primarily with the shared request pipeline to define the client used to make the actual request. You can +write your own [custom implementation](custom-httpclientimpl.md) if needed. + +### RequestClient + +An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The +difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the +underlying HttpClientImpl fetch method. + +## Classes + +This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl. + +### FetchClient + +Basic implementation that calls the global (window) fetch method with no additional processing. + +```TypeScript +import { FetchClient } from "@pnp/common"; + +const client = new FetchClient(); + +client.fetch("{url}", {}); +``` + +### BearerTokenFetchClient + +A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and +the token is treated as a static string. + +```TypeScript +import { BearerTokenFetchClient } from "@pnp/common"; + +const client = new BearerTokenFetchClient("{authentication token}"); + +client.fetch("{url}", {}); +``` + diff --git a/packages/common/docs/storage.md b/packages/common/docs/storage.md new file mode 100644 index 000000000..46753447a --- /dev/null +++ b/packages/common/docs/storage.md @@ -0,0 +1,88 @@ +# @pnp/common/storage + +This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with +a non-persistent in memory polyfill. Optionally through configuratrion you can activate expiration. Sample usage is shown below. + +## PnPClientStorage + +The main export of this module, contains properties representing local and session storage. + +```TypeScript +import { PnPClientStorage } from "@pnp/common"; + +const storage = new PnPClientStorage(); +const myvalue = storage.local.get("mykey"); +``` + +## PnPClientStorageWrapper + +Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used +from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. + +```TypeScript +import { PnPClientStorage } from "@pnp/common"; + +const storage = new PnPClientStorage(); + +// get a value from storage +const value = storage.local.get("mykey"); + +// put a value into storage +storage.local.put("mykey2", "my value"); + +// put a value into storage with an expiration +storage.local.put("mykey2", "my value", new Date()); + +// put a simple object into storage +// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects +storage.local.put("mykey3", { + key: "value", + key2: "value2", +}); + +// remove a value from storage +storage.local.delete("mykey3"); + +// get an item or add it if it does not exist +// returns a promise in case you need time to get the value for storage +// optionally takes a third parameter specifying the expiration +storage.local.getOrPut("mykey4", () => { + return Promise.resolve("value"); +}); + +// delete expired items +storage.local.deleteExpired(); +``` + +### Cache Expiration + +The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. + +```TypeScript +import { PnPClientStorage } from "@pnp/common"; + +const storage = new PnPClientStorage(); + +// session storage +storage.session.deleteExpired(); + +// local storage +storage.local.deleteExpired(); + +// this returns a promise, so you can perform some activity after the expired items are removed: +storage.local.deleteExpired().then(_ => { + // init my application +}); +``` + +The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. + +```TypeScript + +import { setup } from "@pnp/common"; + +setup({ + enableCacheExpiration: true, + cacheExpirationIntervalMilliseconds: 1000, // optional +}); +``` diff --git a/packages/common/docs/util.md b/packages/common/docs/util.md new file mode 100644 index 000000000..d6acac91b --- /dev/null +++ b/packages/common/docs/util.md @@ -0,0 +1,198 @@ +# @pnp/common/util + +This module contains utility methods that you can import individually from the common library. + +```TypeScript +import { + getRandomString, +} from "@pnp/common"; + +// use from individual;y imported method +console.log(getRandomString(10)); +``` + +## getCtxCallback + +Gets a callback function which will maintain context across async calls. + +```TypeScript +import { getCtxCallback } from "@pnp/common"; + +const contextThis = { + myProp: 6, +}; + +function theFunction() { + // "this" within this function will be the context object supplied + // in this case the variable contextThis, so myProp will exist + return this.myProp; +} + +const callback = getCtxCallback(contextThis, theFunction); + +callback(); // returns 6 + +// You can also supply additional parameters if needed + +function theFunction2(g: number) { + // "this" within this function will be the context object supplied + // in this case the variable contextThis, so myProp will exist + return this.myProp + g; +} + +const callback2 = getCtxCallback(contextThis, theFunction, 4); + +callback2(); // returns 10 (6 + 4) +``` + +## dateAdd + +Manipulates a date, please see the [Stackoverflow discussion](https://stackoverflow.com/questions/1197928/how-to-add-30-minutes-to-a-javascript-date-object) from where this method was taken. + +## combine + +Combines any number of paths, normalizing the slashes as required + +```TypeScript +import { combine } from "@pnp/common"; + +// "https://microsoft.com/something/more" +const paths = combine("https://microsoft.com", "something", "more"); + +// "also/works/with/relative" +const paths2 = combine("/also/", "/works", "with/", "/relative\\"); +``` + +## getRandomString + +Gets a random string consiting of the number of characters requested. + +```TypeScript +import { getRandomString } from "@pnp/common"; + +const randomString = getRandomString(10); +``` + +## getGUID + +Creates a random guid, please see the [Stackoverflow discussion](https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript) from where this method was taken. + +## isFunc + +Determines if a supplied variable represents a function. + +## objectDefinedNotNull + +Determines if an object is defined and not null. + +## isArray + +Determines if a supplied variable represents an array. + +## extend + +Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwritting of existing +properties. + +```TypeScript +import { extend } from "@pnp/common"; + +let obj1 = { + prop: 1, + prop2: 2, +}; + +const obj2 = { + prop: 4, + prop3: 9, +}; + +const example1 = extend(obj1, obj2); +// example1 = { prop: 4, prop2: 2, prop3: 9 } + +const example2 = extend(obj1, obj2, true); +// example2 = { prop: 1, prop2: 2, prop3: 9 } +``` + +## isUrlAbsolute + +Determines if a supplied url is absolute and returns true; otherwise returns false. + +## stringIsNullOrEmpty + +Determines if a supplied string is null or empty + +## Removed + +Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods +below for use in your projects should you require. + +```TypeScript +/** + * Loads a stylesheet into the current page + * + * @param path The url to the stylesheet + * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues + */ +public static loadStylesheet(path: string, avoidCache: boolean): void { + if (avoidCache) { + path += "?" + encodeURIComponent((new Date()).getTime().toString()); + } + const head = document.getElementsByTagName("head"); + if (head.length > 0) { + const e = document.createElement("link"); + head[0].appendChild(e); + e.setAttribute("type", "text/css"); + e.setAttribute("rel", "stylesheet"); + e.setAttribute("href", path); + } +} + +/** + * Tests if a url param exists + * + * @param name The name of the url paramter to check + */ +public static urlParamExists(name: string): boolean { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); + return regex.test(location.search); +} + +/** + * Gets a url param value by name + * + * @param name The name of the paramter for which we want the value + */ +public static getUrlParamByName(name: string): string { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); + const results = regex.exec(location.search); + return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +} + +/** + * Gets a url param by name and attempts to parse a bool value + * + * @param name The name of the paramter for which we want the boolean value + */ +public static getUrlParamBoolByName(name: string): boolean { + const p = this.getUrlParamByName(name); + const isFalse = (p === "" || /false|0/i.test(p)); + return !isFalse; +} + +/** + * Inserts the string s into the string target as the index specified by index + * + * @param target The string into which we will insert s + * @param index The location in target to insert s (zero based) + * @param s The string to insert into target at position index + */ +public static stringInsert(target: string, index: number, s: string): string { + if (index > 0) { + return target.substring(0, index) + s + target.substring(index, target.length); + } + return s + target; +} +``` diff --git a/packages/common/index.ts b/packages/common/index.ts new file mode 100644 index 000000000..760d6e836 --- /dev/null +++ b/packages/common/index.ts @@ -0,0 +1 @@ +export * from "./src/common"; diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 000000000..4327ef28e --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,27 @@ +{ + "name": "@pnp/common", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides shared functionality across all pnp libraries", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "adal-angular": "1.0.17", + "tslib": "1.9.3" + }, + "peerDependencies": {}, + "devDependencies": { + "@types/adal-angular": "1.0.1" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/packages/common/src/adalclient.ts b/packages/common/src/adalclient.ts new file mode 100644 index 000000000..32cde2e29 --- /dev/null +++ b/packages/common/src/adalclient.ts @@ -0,0 +1,226 @@ +import { BearerTokenFetchClient, IFetchOptions } from "./netutil"; +import { ISPFXContext } from "./spfxcontextinterface"; +import { isUrlAbsolute } from "./util"; +// @ts-ignore +import * as adal from "adal-angular/dist/adal.min.js"; + +/** + * Parses out the root of the request url to use as the resource when getting the token + * + * After: https://gist.github.com/jlong/2428561 + * @param url The url to parse + */ +function getResource(url: string): string { + const parser = document.createElement("a"); + parser.href = url; + return `${parser.protocol}//${parser.hostname}`; +} + +/** + * Azure AD Client for use in the browser + */ +export class AdalClient extends BearerTokenFetchClient { + + /** + * Our auth context + */ + private static _authContext: adal.AuthenticationContext | null = null; + + /** + * Callback used by the adal auth system + */ + private _displayCallback: ((url: string) => void) | null; + + /** + * Promise used to ensure the user is logged in + */ + private _loginPromise: Promise | null; + + /** + * Creates a new instance of AdalClient + * @param clientId Azure App Id + * @param tenant Office 365 tenant (Ex: {tenant}.onmicrosoft.com) + * @param redirectUri The redirect url used to authenticate the + */ + constructor(public clientId: string, public tenant: string, public redirectUri: string) { + super(null); + this._displayCallback = null; + this._loginPromise = null; + } + + /** + * Creates a new AdalClient using the values of the supplied SPFx context (requires SPFx >= 1.6) + * + * @param spfxContext Current SPFx context + * @description Using this method requires that the features described in this article + * https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient are activated in the tenant. + */ + public static fromSPFxContext(spfxContext: ISPFXContext | any): SPFxAdalClient { + + return new SPFxAdalClient(spfxContext); + } + + /** + * Conducts the fetch opertation against the AAD secured resource + * + * @param url Absolute URL for the request + * @param options Any fetch options passed to the underlying fetch implementation + */ + public async fetch(url: string, options: IFetchOptions): Promise { + + if (!isUrlAbsolute(url)) { + throw Error("You must supply absolute urls to AdalClient.fetch."); + } + + // the url we are calling is the resource + const token = await this.getToken(getResource(url)); + this.token = token; + return super.fetch(url, options); + } + + /** + * Gets a token based on the current user + * + * @param resource The resource for which we are requesting a token + */ + public async getToken(resource: string): Promise { + + await this.ensureAuthContext(); + await this.login(); + + let token = null; + AdalClient._authContext.acquireToken(resource, (message: string, tok: string) => { + + if (message) { + throw Error(message); + } + + token = tok; + }); + + return token; + } + + /** + * Ensures we have created and setup the adal AuthenticationContext instance + */ + private ensureAuthContext(): Promise { + + return new Promise(resolve => { + + if (AdalClient._authContext === null) { + AdalClient._authContext = adal.inject({ + clientId: this.clientId, + displayCall: (url: string) => { + if (this._displayCallback) { + this._displayCallback(url); + } + }, + navigateToLoginRequestUrl: false, + redirectUri: this.redirectUri, + tenant: this.tenant, + }); + } + + resolve(); + }); + } + + /** + * Ensures the current user is logged in + */ + private login(): Promise { + + if (this._loginPromise) { + return this._loginPromise; + } + + this._loginPromise = new Promise((resolve, reject) => { + + if (AdalClient._authContext.getCachedUser()) { + return resolve(); + } + + this._displayCallback = (url: string) => { + + const popupWindow = window.open(url, "login", "width=483, height=600"); + + if (!popupWindow) { + return reject(Error("Could not open pop-up window for auth. Likely pop-ups are blocked by the browser.")); + } + + if (popupWindow && popupWindow.focus) { + popupWindow.focus(); + } + + const pollTimer = window.setInterval(() => { + + if (!popupWindow || popupWindow.closed || popupWindow.closed === undefined) { + window.clearInterval(pollTimer); + } + + try { + if (popupWindow.document.URL.indexOf(this.redirectUri) !== -1) { + window.clearInterval(pollTimer); + AdalClient._authContext.handleWindowCallback(popupWindow.location.hash); + popupWindow.close(); + resolve(); + } + } catch (e) { + reject(e); + } + }, 30); + }; + + // this triggers the login process + this.ensureAuthContext().then(_ => { + (AdalClient._authContext)._loginInProgress = false; + AdalClient._authContext.login(); + this._displayCallback = null; + }); + }); + + return this._loginPromise; + } +} + +/** + * Client wrapping the aadTokenProvider available from SPFx >= 1.6 + */ +export class SPFxAdalClient extends BearerTokenFetchClient { + + /** + * + * @param context provide the appropriate SPFx Context object + */ + constructor(private context: ISPFXContext) { + super(null); + } + + /** + * Executes a fetch request using the supplied url and options + * + * @param url Absolute url of the request + * @param options Any options + */ + public fetch(url: string, options: IFetchOptions): Promise { + + return this.getToken(getResource(url)).then(token => { + this.token = token; + return super.fetch(url, options); + }); + } + + /** + * Gets an AAD token for the provided resource using the SPFx AADTokenProvider + * + * @param resource Resource for which a token is to be requested (ex: https://graph.microsoft.com) + */ + public getToken(resource: string): Promise { + + return this.context.aadTokenProviderFactory.getTokenProvider().then(provider => { + + return provider.getToken(resource); + }); + } +} diff --git a/packages/common/src/collections.ts b/packages/common/src/collections.ts new file mode 100644 index 000000000..4df5f0516 --- /dev/null +++ b/packages/common/src/collections.ts @@ -0,0 +1,46 @@ +import { isFunc } from "./util"; + +declare var Object: { + entries?: any; + keys(o: any): any; +}; + +/** + * Interface defining an object with a known property type + */ +export interface TypedHash { + [key: string]: T; +} + +/** + * Used to calculate the object properties, with polyfill if needed + */ +const objectEntries: any = isFunc(Object.entries) ? Object.entries : (o: any): [any, any][] => Object.keys(o).map((k: any) => [k, o[k]]); + +/** + * Converts the supplied object to a map + * + * @param o The object to map + */ +export function objectToMap(o: any): Map { + if (o !== undefined && o !== null) { + return new Map(objectEntries(o)); + } + return new Map(); +} + +/** + * Merges to Map instances together, overwriting values in target with matching keys, last in wins + * + * @param target map into which the other maps are merged + * @param maps One or more maps to merge into the target + */ +export function mergeMaps(target: Map, ...maps: Map[]): Map { + for (let i = 0; i < maps.length; i++) { + maps[i].forEach((v: V, k: K) => { + target.set(k, v); + }); + } + + return target; +} diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts new file mode 100644 index 000000000..dbb9fdaa1 --- /dev/null +++ b/packages/common/src/common.ts @@ -0,0 +1,7 @@ +export * from "./adalclient"; +export * from "./collections"; +export * from "./libconfig"; +export * from "./netutil"; +export * from "./spfxcontextinterface"; +export * from "./storage"; +export * from "./util"; diff --git a/packages/common/src/libconfig.ts b/packages/common/src/libconfig.ts new file mode 100644 index 000000000..9530287c4 --- /dev/null +++ b/packages/common/src/libconfig.ts @@ -0,0 +1,118 @@ +import { TypedHash, mergeMaps, objectToMap } from "./collections"; +import { ISPFXContext } from "./spfxcontextinterface"; + +export interface LibraryConfiguration { + + /** + * Allows caching to be global disabled, default: false + */ + globalCacheDisable?: boolean; + + /** + * Defines the default store used by the usingCaching method, default: session + */ + defaultCachingStore?: "session" | "local"; + + /** + * Defines the default timeout in seconds used by the usingCaching method, default 30 + */ + defaultCachingTimeoutSeconds?: number; + + /** + * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval + */ + enableCacheExpiration?: boolean; + + /** + * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) + */ + cacheExpirationIntervalMilliseconds?: number; + + /** + * Used to supply the current context from an SPFx webpart to the library + */ + spfxContext?: ISPFXContext; + + /** + * Used to place the library in ie11 compat mode. Some features may not work as expected + */ + ie11?: boolean; +} + +export function setup(config: LibraryConfiguration): void { + RuntimeConfig.extend(config); +} + +// lable mapping for known config values +const s = [ + "defaultCachingStore", + "defaultCachingTimeoutSeconds", + "globalCacheDisable", + "enableCacheExpiration", + "cacheExpirationIntervalMilliseconds", + "spfxContext", + "ie11", +]; + +export class RuntimeConfigImpl { + + constructor(private _v = new Map()) { + + // setup defaults + this._v.set(s[0], "session"); + this._v.set(s[1], 60); + this._v.set(s[2], false); + this._v.set(s[3], false); + this._v.set(s[4], 750); + this._v.set(s[5], null); + this._v.set(s[6], false); + } + + /** + * + * @param config The set of properties to add to the globa configuration instance + */ + public extend(config: TypedHash): void { + this._v = mergeMaps(this._v, objectToMap(config)); + } + + public get(key: string): any { + return this._v.get(key); + } + + public get defaultCachingStore(): "session" | "local" { + return this.get(s[0]); + } + + public get defaultCachingTimeoutSeconds(): number { + return this.get(s[1]); + } + + public get globalCacheDisable(): boolean { + return this.get(s[2]); + } + + public get enableCacheExpiration(): boolean { + return this.get(s[3]); + } + + public get cacheExpirationIntervalMilliseconds(): number { + return this.get(s[4]); + } + + public get spfxContext(): ISPFXContext { + return this.get(s[5]); + } + + public get ie11(): boolean { + const v = this.get(s[6]); + if (v) { + console.warn("PnPjs is running in ie11 compat mode. Not all features may work as expected."); + } + return v; + } +} + +const _runtimeConfig = new RuntimeConfigImpl(); + +export let RuntimeConfig = _runtimeConfig; diff --git a/packages/common/src/netutil.ts b/packages/common/src/netutil.ts new file mode 100644 index 000000000..f768d92d7 --- /dev/null +++ b/packages/common/src/netutil.ts @@ -0,0 +1,86 @@ +import { extend, objectDefinedNotNull } from "./util"; + +declare var global: { fetch(url: string, options: any): Promise }; + +export interface IConfigOptions { + headers?: string[][] | { [key: string]: string } | Headers; + mode?: "navigate" | "same-origin" | "no-cors" | "cors"; + credentials?: "omit" | "same-origin" | "include"; + cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached"; +} + +export interface IFetchOptions extends IConfigOptions { + method?: string; + body?: any; +} + +export interface IHttpClientImpl { + fetch(url: string, options: IFetchOptions): Promise; +} + +export interface IRequestClient { + fetch(url: string, options?: IFetchOptions): Promise; + fetchRaw(url: string, options?: IFetchOptions): Promise; + get(url: string, options?: IFetchOptions): Promise; + post(url: string, options?: IFetchOptions): Promise; + patch(url: string, options?: IFetchOptions): Promise; + delete(url: string, options?: IFetchOptions): Promise; +} + +export function mergeHeaders(target: Headers, source: any): void { + if (source !== undefined && source !== null) { + const temp = new Request("", { headers: source }); + temp.headers.forEach((value: string, name: string) => { + target.append(name, value); + }); + } +} + +export function mergeOptions(target: IConfigOptions, source: IConfigOptions): void { + + if (objectDefinedNotNull(source)) { + const headers = extend(target.headers || {}, source.headers!); + target = extend(target, source); + target.headers = headers; + } +} + +/** + * Makes requests using the global/window fetch API + */ +export class FetchClient implements IHttpClientImpl { + public fetch(url: string, options: IFetchOptions): Promise { + return global.fetch(url, options); + } +} + +/** + * Makes requests using the fetch API adding the supplied token to the Authorization header + */ +export class BearerTokenFetchClient extends FetchClient { + + constructor(private _token: string | null) { + super(); + } + + public get token() { + return this._token || ""; + } + + public set token(token: string) { + this._token = token; + } + + public fetch(url: string, options: IFetchOptions = {}): Promise { + + const headers = new Headers(); + + mergeHeaders(headers, options.headers); + + headers.set("Authorization", `Bearer ${this._token}`); + + options.headers = headers; + + return super.fetch(url, options); + } +} diff --git a/packages/common/src/spfxcontextinterface.ts b/packages/common/src/spfxcontextinterface.ts new file mode 100644 index 000000000..f080408cd --- /dev/null +++ b/packages/common/src/spfxcontextinterface.ts @@ -0,0 +1,29 @@ +export interface ISPFXGraphHttpClient { + fetch(url: string, configuration: any, options: any): Promise; +} + +export interface ISPFXContext { + + aadTokenProviderFactory: { + getTokenProvider(): Promise<{ + getToken(resource: string): Promise; + }>; + }; + + graphHttpClient: ISPFXGraphHttpClient; + + pageContext: { + aadInfo: { + tenantId: { + toString(): string, + }, + } + legacyPageContext: { + aadTenantId: string, + msGraphEndpointUrl: string, + }, + web: { + absoluteUrl: string, + }, + }; +} diff --git a/packages/common/src/storage.ts b/packages/common/src/storage.ts new file mode 100644 index 000000000..3dd38146b --- /dev/null +++ b/packages/common/src/storage.ts @@ -0,0 +1,310 @@ +import { dateAdd, getCtxCallback, jsS, objectDefinedNotNull } from "./util"; +import { RuntimeConfig } from "./libconfig"; + +/** + * A wrapper class to provide a consistent interface to browser based storage + * + */ +export class PnPClientStorageWrapper implements IPnPClientStore { + + /** + * True if the wrapped storage is available; otherwise, false + */ + public enabled: boolean; + + /** + * Creates a new instance of the PnPClientStorageWrapper class + * + * @constructor + */ + constructor(private store: Storage, public defaultTimeoutMinutes = -1) { + this.enabled = this.test(); + // if the cache timeout is enabled call the handler + // this will clear any expired items and set the timeout function + if (RuntimeConfig.enableCacheExpiration) { + this.cacheExpirationHandler(); + } + } + + /** + * Get a value from storage, or null if that value does not exist + * + * @param key The key whose value we want to retrieve + */ + public get(key: string): T | null { + + if (!this.enabled) { + return null; + } + + const o = this.store.getItem(key); + + if (!objectDefinedNotNull(o)) { + return null; + } + + const persistable = JSON.parse(o!); + + if (new Date(persistable.expiration) <= new Date()) { + this.delete(key); + return null; + + } else { + + return persistable.value as T; + } + } + + /** + * Adds a value to the underlying storage + * + * @param key The key to use when storing the provided value + * @param o The value to store + * @param expire Optional, if provided the expiration of the item, otherwise the default is used + */ + public put(key: string, o: any, expire?: Date): void { + if (this.enabled) { + this.store.setItem(key, this.createPersistable(o, expire)); + } + } + + /** + * Deletes a value from the underlying storage + * + * @param key The key of the pair we want to remove from storage + */ + public delete(key: string): void { + if (this.enabled) { + this.store.removeItem(key); + } + } + + /** + * Gets an item from the underlying storage, or adds it if it does not exist using the supplied getter function + * + * @param key The key to use when storing the provided value + * @param getter A function which will upon execution provide the desired value + * @param expire Optional, if provided the expiration of the item, otherwise the default is used + */ + public getOrPut(key: string, getter: () => Promise, expire?: Date): Promise { + if (!this.enabled) { + return getter(); + } + + return new Promise((resolve) => { + + const o = this.get(key); + + if (o == null) { + getter().then((d) => { + this.put(key, d, expire); + resolve(d); + }); + } else { + resolve(o); + } + }); + } + + /** + * Deletes any expired items placed in the store by the pnp library, leaves other items untouched + */ + public deleteExpired(): Promise { + + return new Promise((resolve, reject) => { + + if (!this.enabled) { + resolve(); + } + + try { + + for (let i = 0; i < this.store.length; i++) { + const key = this.store.key(i); + if (key !== null) { + // test the stored item to see if we stored it + if (/["|']?pnp["|']? ?: ?1/i.test(this.store.getItem(key))) { + // get those items as get will delete from cache if they are expired + this.get(key); + } + } + } + + resolve(); + + } catch (e) { reject(e); } + }); + } + + /** + * Used to determine if the wrapped storage is available currently + */ + private test(): boolean { + const str = "t"; + try { + this.store.setItem(str, str); + this.store.removeItem(str); + return true; + } catch (e) { + return false; + } + } + + /** + * Creates the persistable to store + */ + private createPersistable(o: any, expire?: Date): string { + if (expire === undefined) { + + // ensure we are by default inline with the global library setting + let defaultTimeout = RuntimeConfig.defaultCachingTimeoutSeconds; + if (this.defaultTimeoutMinutes > 0) { + defaultTimeout = this.defaultTimeoutMinutes * 60; + } + expire = dateAdd(new Date(), "second", defaultTimeout); + } + + return jsS({ pnp: 1, expiration: expire, value: o }); + } + + /** + * Deletes expired items added by this library in this.store and sets a timeout to call itself + */ + private cacheExpirationHandler(): void { + this.deleteExpired().then(_ => { + + // call ourself in the future + setTimeout(getCtxCallback(this, this.cacheExpirationHandler), RuntimeConfig.cacheExpirationIntervalMilliseconds); + }).catch(e => { + console.error(e); + }); + } +} + +/** + * Interface which defines the operations provided by a client storage object + */ +export interface IPnPClientStore { + /** + * True if the wrapped storage is available; otherwise, false + */ + enabled: boolean; + + /** + * Get a value from storage, or null if that value does not exist + * + * @param key The key whose value we want to retrieve + */ + get(key: string): any; + + /** + * Adds a value to the underlying storage + * + * @param key The key to use when storing the provided value + * @param o The value to store + * @param expire Optional, if provided the expiration of the item, otherwise the default is used + */ + put(key: string, o: any, expire?: Date): void; + + /** + * Deletes a value from the underlying storage + * + * @param key The key of the pair we want to remove from storage + */ + delete(key: string): void; + + /** + * Gets an item from the underlying storage, or adds it if it does not exist using the supplied getter function + * + * @param key The key to use when storing the provided value + * @param getter A function which will upon execution provide the desired value + * @param expire Optional, if provided the expiration of the item, otherwise the default is used + */ + getOrPut(key: string, getter: () => Promise, expire?: Date): Promise; + + /** + * Removes any expired items placed in the store by the pnp library, leaves other items untouched + */ + deleteExpired(): Promise; +} + +/** + * A thin implementation of in-memory storage for use in nodejs + */ +class MemoryStorage { + + constructor(private _store = new Map()) { } + + public get length(): number { + return this._store.size; + } + + public clear(): void { + this._store.clear(); + } + + public getItem(key: string): any { + return this._store.get(key); + } + + public key(index: number): string { + return Array.from(this._store)[index][0]; + } + + public removeItem(key: string): void { + this._store.delete(key); + } + + public setItem(key: string, data: string): void { + this._store.set(key, data); + } + + [key: string]: any; + [index: number]: string; +} + +/** + * A class that will establish wrappers for both local and session storage + */ +export class PnPClientStorage { + + /** + * Creates a new instance of the PnPClientStorage class + * + * @constructor + */ + constructor(private _local: IPnPClientStore | null = null, private _session: IPnPClientStore | null = null) { } + + /** + * Provides access to the local storage of the browser + */ + public get local(): IPnPClientStore { + + if (this._local === null) { + this._local = this.getStore("local"); + } + + return this._local; + } + + /** + * Provides access to the session storage of the browser + */ + public get session(): IPnPClientStore { + + if (this._session === null) { + this._session = this.getStore("session"); + } + + return this._session; + } + + private getStore(name: string): PnPClientStorageWrapper { + + if (name === "local") { + return new PnPClientStorageWrapper(typeof(localStorage) === "undefined" ? new MemoryStorage() : localStorage); + } + + return new PnPClientStorageWrapper(typeof(sessionStorage) === "undefined" ? new MemoryStorage() : sessionStorage); + } +} diff --git a/packages/common/src/util.ts b/packages/common/src/util.ts new file mode 100644 index 000000000..6469f50f5 --- /dev/null +++ b/packages/common/src/util.ts @@ -0,0 +1,242 @@ +import { TypedHash } from "./collections"; + +/** + * Gets a callback function which will maintain context across async calls. + * Allows for the calling pattern getCtxCallback(thisobj, method, methodarg1, methodarg2, ...) + * + * @param context The object that will be the 'this' value in the callback + * @param method The method to which we will apply the context and parameters + * @param params Optional, additional arguments to supply to the wrapped method when it is invoked + */ +export function getCtxCallback(context: any, method: Function, ...params: any[]): Function { + return function () { + method.apply(context, params); + }; +} + +export type DateAddInterval = "year" | "quarter" | "month" | "week" | "day" | "hour" | "minute" | "second"; + +/** + * Adds a value to a date + * + * @param date The date to which we will add units, done in local time + * @param interval The name of the interval to add, one of: ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'] + * @param units The amount to add to date of the given interval + * + * http://stackoverflow.com/questions/1197928/how-to-add-30-minutes-to-a-javascript-date-object + */ +export function dateAdd(date: Date, interval: DateAddInterval, units: number): Date | undefined { + let ret: Date | undefined = new Date(date); // don't change original date + switch (interval.toLowerCase()) { + case "year": ret.setFullYear(ret.getFullYear() + units); break; + case "quarter": ret.setMonth(ret.getMonth() + 3 * units); break; + case "month": ret.setMonth(ret.getMonth() + units); break; + case "week": ret.setDate(ret.getDate() + 7 * units); break; + case "day": ret.setDate(ret.getDate() + units); break; + case "hour": ret.setTime(ret.getTime() + units * 3600000); break; + case "minute": ret.setTime(ret.getTime() + units * 60000); break; + case "second": ret.setTime(ret.getTime() + units * 1000); break; + default: ret = undefined; break; + } + return ret; +} + +/** + * Combines an arbitrary set of paths ensuring and normalizes the slashes + * + * @param paths 0 to n path parts to combine + */ +export function combine(...paths: string[]): string { + + return paths + .filter(path => !stringIsNullOrEmpty(path)) + .map(path => path.replace(/^[\\|\/]/, "").replace(/[\\|\/]$/, "")) + .join("/") + .replace(/\\/g, "/"); +} + +/** + * Gets a random string of chars length + * + * https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript + * + * @param chars The length of the random string to generate + */ +export function getRandomString(chars: number): string { + const text = new Array(chars); + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < chars; i++) { + text[i] = possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text.join(""); +} + +/** + * Gets a random GUID value + * + * http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript + * https://stackoverflow.com/a/8809472 updated to prevent collisions. + */ +/* tslint:disable no-bitwise */ +export function getGUID(): string { + let d = Date.now(); + if (typeof performance !== "undefined" && typeof performance.now === "function") { + d += performance.now(); // use high-precision timer if available + } + const guid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); + }); + return guid; +} +/* tslint:enable */ + +/** + * Determines if a given value is a function + * + * @param cf The thing to test for functionness + */ +export function isFunc(cf: any): boolean { + return typeof cf === "function"; +} + +/** + * Determines if an object is both defined and not null + * @param obj Object to test + */ +export function objectDefinedNotNull(obj: any): boolean { + return typeof obj !== "undefined" && obj !== null; +} + +/** + * @returns whether the provided parameter is a JavaScript Array or not. +*/ +export function isArray(array: any): boolean { + + if (Array.isArray) { + return Array.isArray(array); + } + + return array && typeof array.length === "number" && array.constructor === Array; +} + +/** + * Provides functionality to extend the given object by doing a shallow copy + * + * @param target The object to which properties will be copied + * @param source The source object from which properties will be copied + * @param noOverwrite If true existing properties on the target are not overwritten from the source + * @param filter If provided allows additional filtering on what properties are copied (propName: string) => boolean + * + */ +export function extend = any, S extends TypedHash = any>(target: T, source: S, noOverwrite = false, + filter: (propName: string) => boolean = () => true): T & S { + + if (!objectDefinedNotNull(source)) { + return target; + } + + // ensure we don't overwrite things we don't want overwritten + const check: (o: any, i: string) => Boolean = noOverwrite ? (o, i) => !(i in o) : () => true; + + // final filter we will use + const f = (v: string) => check(target, v) && filter(v); + + return Object.getOwnPropertyNames(source) + .filter(f) + .reduce((t: any, v: string) => { + t[v] = source[v]; + return t; + }, target); +} + +/** + * Determines if a given url is absolute + * + * @param url The url to check to see if it is absolute + */ +export function isUrlAbsolute(url: string): boolean { + return /^https?:\/\/|^\/\//i.test(url); +} + +/** + * Determines if a string is null or empty or undefined + * + * @param s The string to test + */ +export function stringIsNullOrEmpty(s: string): boolean { + return s === undefined || s === null || s.length < 1; +} + +/** + * Gets an attribute value from an html/xml string block. NOTE: if the input attribute value has + * RegEx special characters they will be escaped in the returned string + * + * @param html HTML to search + * @param attrName The name of the attribute to find + */ +export function getAttrValueFromString(html: string, attrName: string): string | null { + + // make the input safe for regex + html = html.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const reg = new RegExp(`${attrName}\\s*?=\\s*?("|')([^\\1]*?)\\1`, "i"); + const match = reg.exec(html); + return match !== null && match.length > 0 ? match[2] : null; +} + +/** + * Ensures guid values are represented consistently as "ea123463-137d-4ae3-89b8-cf3fc578ca05" + * + * @param guid The candidate guid + */ +export function sanitizeGuid(guid: string): string { + + if (stringIsNullOrEmpty(guid)) { + return guid; + } + + const matches = /([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})/i.exec(guid); + + return matches === null ? guid : matches[1]; +} + +/** + * Shorthand for JSON.stringify + * + * @param o Any type of object + */ +export function jsS(o: any): string { + return JSON.stringify(o); +} + +/** + * Shorthand for Object.hasOwnProperty + * + * @param o Object to check for + * @param p Name of the property + */ +export function hOP(o: any, p: string): boolean { + return Object.hasOwnProperty.call(o, p); +} + +/** + * Generates a ~unique hash code + * + * From: https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript + */ +// tslint:disable:no-bitwise +export function getHashCode(s: string): number { + let hash = 0; + if (s.length === 0) { + return hash; + } + + for (let i = 0; i < s.length; i++) { + const chr = s.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +} +// tslint:enable:no-bitwise diff --git a/packages/common/tsconfig.es5.json b/packages/common/tsconfig.es5.json new file mode 100644 index 000000000..708d73148 --- /dev/null +++ b/packages/common/tsconfig.es5.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.es5.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [] +} diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json new file mode 100644 index 000000000..97a3c5c30 --- /dev/null +++ b/packages/common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [] +} \ No newline at end of file diff --git a/packages/config-store/__tests/configuration.test.ts b/packages/config-store/__tests/configuration.test.ts new file mode 100644 index 000000000..e1aede9bb --- /dev/null +++ b/packages/config-store/__tests/configuration.test.ts @@ -0,0 +1,112 @@ +import { expect } from "chai"; +import * as Collections from "@pnp/common"; +import { Settings } from ".."; +import { default as MockConfigurationProvider } from "./mock-configurationprovider"; + +describe("Configuration", () => { + + describe("Settings", () => { + + let settings: Settings; + + beforeEach(() => { + settings = new Settings(); + }); + + it("Add and get a setting", () => { + settings.add("key1", "value1"); + const setting = settings.get("key1"); + expect(setting).to.eq("value1"); + }); + + it("Add and get a JSON value", () => { + const obj = { "prop1": "prop1value", "prop2": "prop2value" }; + settings.addJSON("obj1", obj); + const setting = settings.getJSON("obj1"); + expect(setting).to.deep.equal(obj); + }); + + it("Apply a hash and retrieve one of the values", () => { + + const hash: Collections.TypedHash = { + "key1": "value1", + "key2": "value2", + }; + + settings.apply(hash); + const setting = settings.get("key1"); + expect(setting).to.eq("value1"); + }); + + it("Apply a hash, apply a second hash overwritting a value and get back the new value", () => { + + const hash1: Collections.TypedHash = { + "key1": "value1", + "key2": "value2", + }; + + const hash2: Collections.TypedHash = { + "key1": "value3", + "key2": "value4", + }; + + settings.apply(hash1); + settings.apply(hash2); + const setting = settings.get("key1"); + expect(setting).to.eq("value3"); + }); + + it("Apply a hash containing a serialized JSON object and then retrieve that object using getJSON", () => { + + const obj = { "prop1": "prop1value", "prop2": "prop2value" }; + + const hash: Collections.TypedHash = { + "key1": "value1", + "key2": "value2", + "key3": JSON.stringify(obj), + }; + + settings.apply(hash); + const setting = settings.getJSON("key3"); + expect(setting).to.deep.equal(obj); + }); + + it("loads settings from a configuration provider", () => { + const mockValues: Collections.TypedHash = { + "key2": "value_from_provider_2", + "key3": "value_from_provider_3", + }; + const mockProvider = new MockConfigurationProvider(); + mockProvider.mockValues = mockValues; + + settings.add("key1", "value1"); + const p = settings.load(mockProvider); + + return p.then(() => { + expect(settings.get("key1")).to.eq("value1"); + expect(settings.get("key2")).to.eq("value_from_provider_2"); + expect(settings.get("key3")).to.eq("value_from_provider_3"); + }); + }); + + it("rejects a promise if configuration provider throws", () => { + const mockProvider = new MockConfigurationProvider(); + mockProvider.shouldThrow = true; + const p = settings.load(mockProvider); + return p.then( + () => { expect.fail(null, null, "Should not resolve when provider throws!"); }, + (reason) => { expect(reason).not.to.be.null; }, + ); + }); + + it("rejects a promise if configuration provider rejects the promise", () => { + const mockProvider = new MockConfigurationProvider(); + mockProvider.shouldReject = true; + const p = settings.load(mockProvider); + return p.then( + () => { expect.fail(null, null, "Should not resolve when provider rejects!"); }, + (reason) => { expect(reason).not.to.be.null; }, + ); + }); + }); +}); diff --git a/packages/config-store/__tests/mock-configurationprovider.ts b/packages/config-store/__tests/mock-configurationprovider.ts new file mode 100644 index 000000000..12196a39a --- /dev/null +++ b/packages/config-store/__tests/mock-configurationprovider.ts @@ -0,0 +1,23 @@ +import { IConfigurationProvider } from ".."; +import { TypedHash } from "@pnp/common"; + +export default class MockConfigurationProvider implements IConfigurationProvider { + public shouldThrow = false; + public shouldReject = false; + + constructor(public mockValues?: TypedHash) { } + + public getConfiguration(): Promise> { + if (this.shouldThrow) { + throw Error("Mocked error"); + } + + return new Promise>((resolve, reject) => { + if (this.shouldReject) { + reject("Mocked rejection"); + } else { + resolve(this.mockValues); + } + }); + } +} diff --git a/packages/config-store/__tests/mock-storage.ts b/packages/config-store/__tests/mock-storage.ts new file mode 100644 index 000000000..6d1ca5660 --- /dev/null +++ b/packages/config-store/__tests/mock-storage.ts @@ -0,0 +1,34 @@ +export default class MockStorage implements Storage { + constructor(private _store = new Map()) { } + + public get length(): number { + return this._store.size; + } + + public set length(i: number) { + this._length = i; + } + + public clear(): void { + this._store.clear(); + } + + public getItem(key: string): any { + return this._store.get(key); + } + + public key(index: number): string { + return Array.from(this._store)[index][0]; + } + + public removeItem(key: string): void { + this._store.delete(key); + } + + public setItem(key: string, data: string): void { + this._store.set(key, data); + } + + [key: string]: any; + [index: number]: string; +} diff --git a/packages/config-store/__tests/providers/cachingConfigurationProvider.test.ts b/packages/config-store/__tests/providers/cachingConfigurationProvider.test.ts new file mode 100644 index 000000000..b5193caa9 --- /dev/null +++ b/packages/config-store/__tests/providers/cachingConfigurationProvider.test.ts @@ -0,0 +1,71 @@ +import { PnPClientStorageWrapper, PnPClientStore, TypedHash } from "@pnp/common"; +import { expect } from "chai"; +import { CachingConfigurationProvider, Settings } from "../.."; +import { default as MockConfigurationProvider } from "../mock-configurationprovider"; +import MockStorage from "../mock-storage"; + +describe("Configuration", () => { + + describe("CachingConfigurationProvider", () => { + let wrapped: MockConfigurationProvider; + let store: PnPClientStore; + let settings: Settings; + + beforeEach(() => { + const mockValues: TypedHash = { + "key1": "value1", + "key2": "value2", + }; + wrapped = new MockConfigurationProvider(); + wrapped.mockValues = mockValues; + store = new PnPClientStorageWrapper(new MockStorage()); + settings = new Settings(); + }); + + it("Loads the config from the wrapped provider", () => { + const provider = new CachingConfigurationProvider(wrapped, "cacheKey", store); + return settings.load(provider).then(() => { + expect(settings.get("key1")).to.eq("value1"); + expect(settings.get("key2")).to.eq("value2"); + }); + }); + + it("Returns cached values", () => { + const provider = new CachingConfigurationProvider(wrapped, "cacheKey", store); + return settings.load(provider).then(() => { + const updatedValues: TypedHash = { + "key1": "update1", + "key2": "update2", + }; + wrapped.mockValues = updatedValues; + return settings.load(provider); + }).then(() => { + expect(settings.get("key1")).to.eq("value1"); + expect(settings.get("key2")).to.eq("value2"); + }); + }); + + it("Bypasses a disabled cache", () => { + store.enabled = false; + const provider = new CachingConfigurationProvider(wrapped, "cacheKey", store); + return settings.load(provider).then(() => { + const updatedValues: TypedHash = { + "key1": "update1", + "key2": "update2", + }; + wrapped.mockValues = updatedValues; + return settings.load(provider); + }).then(() => { + expect(settings.get("key1")).to.eq("update1"); + expect(settings.get("key2")).to.eq("update2"); + }); + }); + + it("Uses provided cachekey with a '_configcache_' prefix", () => { + const provider = new CachingConfigurationProvider(wrapped, "_configcache_cacheKey", store); + return settings.load(provider).then(() => { + return expect(store.get("_configcache_cacheKey")).not.to.be.null; + }); + }); + }); +}); diff --git a/packages/config-store/__tests/providers/spListConfigurationProvider.test.ts b/packages/config-store/__tests/providers/spListConfigurationProvider.test.ts new file mode 100644 index 000000000..f7edc17e7 --- /dev/null +++ b/packages/config-store/__tests/providers/spListConfigurationProvider.test.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import { SPListConfigurationProvider } from "../.."; +import MockStorage from "../mock-storage"; +import { TypedHash } from "@pnp/common"; +import { Web } from "@pnp/sp"; + +declare var global: any; + +describe("Configuration", () => { + + describe("SPListConfigurationProvider", () => { + let web: Web; + let mockData: TypedHash; + let calledUrl: string; + + beforeEach(() => { + web = new Web("https://fake.sharepoint.com/sites/test/subsite"); + mockData = { "key1": "value1", "key2": "value2" }; + calledUrl = ""; + }); + + // function mockJQuery(): any { + // // Create a mock JQuery.ajax method, which will always return our testdata. + // let mock: any = {}; + // mock.ajax = function(options: any) { + // calledUrl = options.url; + // let wrappedMockData: any[] = new Array(); + // for (let key in mockData) { + // if (typeof key === "string") { + // wrappedMockData.push({"Title": key, "Value": mockData[key]}); + // } + // } + // return { + // "success": function(callback: (data: any) => void) { + // callback({ d: { results: wrappedMockData } }); + // }, + // }; + // }; + // return mock; + // } + + // it("Returns the webUrl passed in to the constructor", () => { + // let provider = new SPListConfigurationProvider(webUrl); + // expect(provider.getWebUrl()).to.equal(webUrl); + // }); + + // it("Uses 'config' as the default title for the list", () => { + // let provider = new SPListConfigurationProvider(webUrl); + // expect(provider.getListTitle()).to.equal("config"); + // }); + + // it("Allows user to overwrite the default list title", () => { + // let listTitle = "testTitle"; + // let provider = new SPListConfigurationProvider(webUrl, listTitle); + // expect(provider.getListTitle()).to.equal(listTitle); + // }); + + // it("Fetches configuration data from SharePoint using ajax", () => { + // // Mock JQuery + // ( global).jQuery = mockJQuery(); + + // let listTitle = "testTitle"; + // let provider = new SPListConfigurationProvider(webUrl, listTitle); + // return provider.getConfiguration().then((values) => { + // // Verify url + // expect(calledUrl).to.equal(webUrl + "/_api/web/lists/getByTitle('" + listTitle + "')/items?$select=Title,Value"); + + // // Verify returned values + // for (let key in mockData) { + // if (typeof key === "string") { + // expect(values[key]).to.equal(mockData[key]); + // } + // } + + // // Remove JQuery mock + // delete ( global).jQuery; + // }); + // }); + + it("Can wrap itself inside a caching configuration provider", () => { + // Mock localStorage + (global).localStorage = new MockStorage(); + + const provider = new SPListConfigurationProvider(web); + const cached = provider.asCaching(); + const wrappedProvider = cached.getWrappedProvider(); + expect(wrappedProvider).to.equal(provider); + + // Remove localStorage mock + delete (global).localStorage; + }); + }); +}); diff --git a/packages/config-store/docs/configuration.md b/packages/config-store/docs/configuration.md new file mode 100644 index 000000000..01174920e --- /dev/null +++ b/packages/config-store/docs/configuration.md @@ -0,0 +1,45 @@ +# @pnp/config-store/configuration + +The main class exported from the config-store package is Settings. This is the class through which you will load and access your +settings via [providers](providers.md). + +```TypeScript +import { Web } from "@pnp/sp"; +import { Settings, SPListConfigurationProvider } from "@pnp/config-store"; + +// create an instance of the settings class, could be static and shared across your application +// or built as needed. +const settings = new Settings(); + +// you can add/update a single value using add +settings.add("mykey", "myvalue"); + +// you can also add/update a JSON value which will be stringified for you as a shorthand +settings.addJSON("mykey2", { + field: 1, + field2: 2, + field3: 3, +}); + +// and you can apply a plain object of keys/values that will be written as single values +// this results in each enumerable property of the supplied object being added to the settings collection +settings.apply({ + field: 1, + field2: 2, + field3: 3, +}); + +// and finally you can load values from a configuration provider +const w = new Web("https://mytenant.sharepoint.com/sites/dev"); +const provider = new SPListConfigurationProvider(w, "myconfiglistname"); + +// this will load values from the supplied list +// by default the key will be from the Title field and the value from a column named Value +await settings.load(provider); + +// once we have loaded values we can then read them +const value = settings.get("mykey"); + +// or read JSON that will be parsed for you from the store +const value2 = settings.getJSON("mykey2"); +``` diff --git a/packages/config-store/docs/index.md b/packages/config-store/docs/index.md new file mode 100644 index 000000000..4f3e1003e --- /dev/null +++ b/packages/config-store/docs/index.md @@ -0,0 +1,21 @@ +# @pnp/config-store + +[![npm version](https://badge.fury.io/js/%40pnp%2Fconfig-store.svg)](https://badge.fury.io/js/%40pnp%2Fconfig-store) + +This module providers a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed. + +## Getting Started + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/config-store --save` + +See the topics below for usage: + +* [configuration](configuration.md) +* [providers](providers.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-config-store-uml.svg) + +Graphical UML diagram of @pnp/config-store. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/config-store/docs/providers.md b/packages/config-store/docs/providers.md new file mode 100644 index 000000000..6e20fbda3 --- /dev/null +++ b/packages/config-store/docs/providers.md @@ -0,0 +1,43 @@ +# @pnp/config-store/providers + +Currently there is a single provider included in the library, but contributions of additional providers are welcome. + +## SPListConfigurationProvider + +This provider is based on a SharePoint list and read all of the rows and makes them available as a TypedHash. By default the column names used are Title for key and "Value" for value, but you can update these as needed. Additionally the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. + +```TypeScript +import { Web } from "@pnp/sp"; +import { Settings, SPListConfigurationProvider } from "@pnp/config-store"; + +// create a new provider instance +const w = new Web("https://mytenant.sharepoint.com/sites/dev"); +const provider = new SPListConfigurationProvider(w, "myconfiglistname"); + +const settings = new Settings(); + +// load our values from the list +await settings.load(provider); +``` + +## CachingConfigurationProvider + +Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a +provider and caches the configuration in local or session storage. + +```TypeScript +import { Web } from "@pnp/sp"; +import { Settings, SPListConfigurationProvider } from "@pnp/config-store"; + +// create a new provider instance +const w = new Web("https://mytenant.sharepoint.com/sites/dev"); +const provider = new SPListConfigurationProvider(w, "myconfiglistname"); + +// get an instance of the provider wrapped +// you can optionally provide a key that will be used in the cache to the asCaching method +const wrappedProvider = provider.asCaching(); + +// use that wrapped provider to populate the settings +await settings.load(wrappedProvider); +``` + diff --git a/packages/config-store/index.ts b/packages/config-store/index.ts new file mode 100644 index 000000000..f00a597e0 --- /dev/null +++ b/packages/config-store/index.ts @@ -0,0 +1 @@ +export * from "./src/configstore"; diff --git a/packages/config-store/package.json b/packages/config-store/package.json new file mode 100644 index 000000000..97f46d941 --- /dev/null +++ b/packages/config-store/package.json @@ -0,0 +1,27 @@ +{ + "name": "@pnp/config-store", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides a way to manage configuration within your application", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/sp": "0.0.0-PLACEHOLDER", + "@pnp/logging": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/packages/config-store/src/configstore.ts b/packages/config-store/src/configstore.ts new file mode 100644 index 000000000..9d98806b5 --- /dev/null +++ b/packages/config-store/src/configstore.ts @@ -0,0 +1,2 @@ +export * from "./configuration"; +export * from "./providers/index"; diff --git a/packages/config-store/src/configuration.ts b/packages/config-store/src/configuration.ts new file mode 100644 index 000000000..33fb6225f --- /dev/null +++ b/packages/config-store/src/configuration.ts @@ -0,0 +1,103 @@ +import { TypedHash, mergeMaps, objectToMap, jsS } from "@pnp/common"; + +/** + * Interface for configuration providers + * + */ +export interface IConfigurationProvider { + + /** + * Gets the configuration from the provider + */ + getConfiguration(): Promise>; +} + +/** + * Class used to manage the current application settings + * + */ +export class Settings { + + /** + * Creates a new instance of the settings class + * + * @constructor + */ + constructor(private _settings = new Map()) { + } + + /** + * Adds a new single setting, or overwrites a previous setting with the same key + * + * @param {string} key The key used to store this setting + * @param {string} value The setting value to store + */ + public add(key: string, value: string) { + this._settings.set(key, value); + } + + /** + * Adds a JSON value to the collection as a string, you must use getJSON to rehydrate the object when read + * + * @param {string} key The key used to store this setting + * @param {any} value The setting value to store + */ + public addJSON(key: string, value: any) { + this._settings.set(key, jsS(value)); + } + + /** + * Applies the supplied hash to the setting collection overwriting any existing value, or created new values + * + * @param {TypedHash} hash The set of values to add + */ + public apply(hash: TypedHash): Promise { + return new Promise((resolve, reject) => { + try { + this._settings = mergeMaps(this._settings, objectToMap(hash)); + resolve(); + } catch (e) { + reject(e); + } + }); + } + + /** + * Loads configuration settings into the collection from the supplied provider and returns a Promise + * + * @param {IConfigurationProvider} provider The provider from which we will load the settings + */ + public load(provider: IConfigurationProvider): Promise { + return new Promise((resolve, reject) => { + provider.getConfiguration().then((value) => { + this._settings = mergeMaps(this._settings, objectToMap(value)); + resolve(); + }).catch(reject); + }); + } + + /** + * Gets a value from the configuration + * + * @param {string} key The key whose value we want to return. Returns null if the key does not exist + * @return {string} string value from the configuration + */ + public get(key: string): string | null { + return this._settings.get(key) || null; + } + + /** + * Gets a JSON value, rehydrating the stored string to the original object + * + * @param {string} key The key whose value we want to return. Returns null if the key does not exist + * @return {any} object from the configuration + */ + public getJSON(key: string): any { + const o = this.get(key); + if (o === undefined || o === null) { + return o; + } + + return JSON.parse(o); + } +} diff --git a/packages/config-store/src/providers/cachingConfigurationProvider.ts b/packages/config-store/src/providers/cachingConfigurationProvider.ts new file mode 100644 index 000000000..ff5e6b6e3 --- /dev/null +++ b/packages/config-store/src/providers/cachingConfigurationProvider.ts @@ -0,0 +1,62 @@ +import { IConfigurationProvider } from "../configuration"; +import { TypedHash, IPnPClientStore, PnPClientStorage } from "@pnp/common"; + +/** + * A caching provider which can wrap other non-caching providers + * + */ +export default class CachingConfigurationProvider implements IConfigurationProvider { + + private store: IPnPClientStore; + + /** + * Creates a new caching configuration provider + * @constructor + * @param {IConfigurationProvider} wrappedProvider Provider which will be used to fetch the configuration + * @param {string} cacheKey Key that will be used to store cached items to the cache + * @param {IPnPClientStore} cacheStore OPTIONAL storage, which will be used to store cached settings. + */ + constructor(private wrappedProvider: IConfigurationProvider, private cacheKey: string, cacheStore?: IPnPClientStore) { + this.wrappedProvider = wrappedProvider; + this.store = (cacheStore) ? cacheStore : this.selectPnPCache(); + } + + /** + * Gets the wrapped configuration providers + * + * @return {IConfigurationProvider} Wrapped configuration provider + */ + public getWrappedProvider(): IConfigurationProvider { + return this.wrappedProvider; + } + + /** + * Loads the configuration values either from the cache or from the wrapped provider + * + * @return {Promise>} Promise of loaded configuration values + */ + public getConfiguration(): Promise> { + // Cache not available, pass control to the wrapped provider + if ((!this.store) || (!this.store.enabled)) { + return this.wrappedProvider.getConfiguration(); + } + + return this.store.getOrPut(this.cacheKey, () => { + return this.wrappedProvider.getConfiguration().then((providedConfig) => { + this.store.put(this.cacheKey, providedConfig); + return providedConfig; + }); + }); + } + + private selectPnPCache(): IPnPClientStore { + const pnpCache = new PnPClientStorage(); + if ((pnpCache.local) && (pnpCache.local.enabled)) { + return pnpCache.local; + } + if ((pnpCache.session) && (pnpCache.session.enabled)) { + return pnpCache.session; + } + throw Error("Cannot create a caching configuration provider since cache is not available."); + } +} diff --git a/packages/config-store/src/providers/index.ts b/packages/config-store/src/providers/index.ts new file mode 100644 index 000000000..a8911724e --- /dev/null +++ b/packages/config-store/src/providers/index.ts @@ -0,0 +1,7 @@ +export { + default as CachingConfigurationProvider, +} from "./cachingConfigurationProvider"; + +export { + default as SPListConfigurationProvider, +} from "./spListConfigurationProvider"; diff --git a/packages/config-store/src/providers/spListConfigurationProvider.ts b/packages/config-store/src/providers/spListConfigurationProvider.ts new file mode 100644 index 000000000..5d448c8c3 --- /dev/null +++ b/packages/config-store/src/providers/spListConfigurationProvider.ts @@ -0,0 +1,43 @@ +import { IConfigurationProvider } from "../configuration"; +import { TypedHash } from "@pnp/common"; +import { default as CachingConfigurationProvider } from "./cachingConfigurationProvider"; +import { IWeb } from "@pnp/sp/src/webs"; + +/** + * A configuration provider which loads configuration values from a SharePoint list + * + */ +export default class SPListConfigurationProvider implements IConfigurationProvider { + /** + * Creates a new SharePoint list based configuration provider + * @constructor + * @param {string} webUrl Url of the SharePoint site, where the configuration list is located + * @param {string} listTitle Title of the SharePoint list, which contains the configuration settings (optional, default: "config") + * @param {string} keyFieldName The name of the field in the list to use as the setting key (optional, default: "Title") + * @param {string} valueFieldName The name of the field in the list to use as the setting value (optional, default: "Value") + */ + constructor(public readonly web: IWeb, public readonly listTitle = "config", private keyFieldName = "Title", private valueFieldName = "Value") { } + + /** + * Loads the configuration values from the SharePoint list + * + * @return {Promise>} Promise of loaded configuration values + */ + public getConfiguration(): Promise> { + + return this.web.lists.getByTitle(this.listTitle).items.select(this.keyFieldName, this.valueFieldName)() + .then((data: any[]) => data.reduce((c: any, item: any) => { + c[item[this.keyFieldName]] = item[this.valueFieldName]; + return c; + }, {})); + } + + /** + * Wraps the current provider in a cache enabled provider + * + * @return {CachingConfigurationProvider} Caching providers which wraps the current provider + */ + public asCaching(cacheKey = `pnp_configcache_splist_${this.web.toUrl()}+${this.listTitle}`): CachingConfigurationProvider { + return new CachingConfigurationProvider(this, cacheKey); + } +} diff --git a/packages/config-store/tsconfig.es5.json b/packages/config-store/tsconfig.es5.json new file mode 100644 index 000000000..b67449ab6 --- /dev/null +++ b/packages/config-store/tsconfig.es5.json @@ -0,0 +1,29 @@ +{ + "extends": "../tsconfig.es5.json", + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + }, + { + "path": "../odata/tsconfig.es5.json" + }, + { + "path": "../sp/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/config-store/tsconfig.json b/packages/config-store/tsconfig.json new file mode 100644 index 000000000..372e2bd7c --- /dev/null +++ b/packages/config-store/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + }, + { + "path": "../sp" + } + ] +} \ No newline at end of file diff --git a/packages/documentation/SPFx-On-Premesis-2016.md b/packages/documentation/SPFx-On-Premesis-2016.md new file mode 100644 index 000000000..c20bbb35a --- /dev/null +++ b/packages/documentation/SPFx-On-Premesis-2016.md @@ -0,0 +1,38 @@ +# Workaround for SPFx TypeScript Version + +_Note this article applies to version 1.4.1 SharePoint Framework projects targetting on-premesis only._ + +When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premesis it installs TypeScript version 2.2.2. Unfortunately this library relies on 2.4.2 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. + +1. Open package-lock.json +2. Search for `"typescript": "2.2.2"` +3. Replace "2.2.2" with "2.4.2" +4. Search for the next "typescript" occurance and replace the block with: +```JSON +"typescript": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", + "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", + "dev": true +} +``` + +![Replacement blocks highlighted](img/SPFx-On-Premesis-2016-1.png) + +5. Remove node_modules folder `rm -rf node_modules/` +6. Run `npm install` + + +This can be checked with: + +``` +npm list typescript +``` + +``` ++-- @microsoft/sp-build-web@1.1.0 +| `-- @microsoft/gulp-core-build-typescript@3.1.1 +| +-- @microsoft/api-extractor@2.3.8 +| | `-- typescript@2.4.2 +| `-- typescript@2.4.2 +``` diff --git a/packages/documentation/beta-versions.md b/packages/documentation/beta-versions.md new file mode 100644 index 000000000..d067ecfe9 --- /dev/null +++ b/packages/documentation/beta-versions.md @@ -0,0 +1,15 @@ +# Beta Versions + +To help folks try out new features early and provide feedback prior to releases we publish beta versions of the packages. Released as a set with matching version numbers, just like when we do a normal release. Generally every Friday a new set of beta libraries will be released. While not ready for production use we encourage you to try out these pre-release packages and provide us feedback. + +## Installing + +To install the beta packages in your project you use the @beta version number on the packages. This applies to all packages, not just the ones +shown in the example below. + +``` +npm install @pnp/logging@beta @pnp/common@beta @pnp/odata@beta @pnp/sp@beta --save +``` + +Please remember that it is possible something may not work in a beta version, so be aware and if you find something please [report an +issue](https://github.com/pnp/pnpjs/issues). diff --git a/packages/documentation/css/extra.css b/packages/documentation/css/extra.css new file mode 100644 index 000000000..6391b1ff7 --- /dev/null +++ b/packages/documentation/css/extra.css @@ -0,0 +1,33 @@ +.md-logo { + height: 32px; + width: 150px; + padding: 0 0.25 0.5 !important; +} + +.md-header{ + height: 75px; +} + +.md-container{ + padding-top: 70px; +} + +.md-sidebar[data-md-state="lock"]{ + padding-top: 75px; +} + +.md-logo img { + width: 100% !important; + height: auto !important; + margin-top: -0.25em; +} + +.md-footer { + margin-top: 5em; +} + +@media only screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__title--site .md-nav__button { + width: 150px; + } +} \ No newline at end of file diff --git a/packages/documentation/debugging.md b/packages/documentation/debugging.md new file mode 100644 index 000000000..af7466dd1 --- /dev/null +++ b/packages/documentation/debugging.md @@ -0,0 +1,164 @@ +# Debugging + +## Debugging Library Features in Code using Node + +The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses the [launch.json](https://github.com/pnp/pnpjs/blob/master/.vscode/launch.json) file to build and run the library using [./debug/launch/main.ts](https://github.com/pnp/pnpjs/blob/master/debug/launch/main.ts) as the program entry. You can add any number of files to this directory and they will be ignored by git, however the debug.ts file is not, so please ensure you don't commit any login information. + +### Setup settings.js + +If you have not already you need to create a settings.js files by copying settings.example.js and renaming it to settings.js. Then update the clientId, clientSecret, and siteUrl fields in the testing section. (See below for guidance on registering a client id and secret) + +### Test your setup + +If you hit F5 now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly. + +### Create a debug module + +Using ./debug/launch/example.ts as a reference create a debugging file in the debug folder, let's call it mydebug.ts and add this content: + +```TypeScript +// note we can use the actual package names for our imports +import { sp, ListEnsureResult } from "@pnp/sp"; +import { Logger, LogLevel, ConsoleListener } from "@pnp/logging"; + +declare var process: { exit(code?: number): void }; + +export function MyDebug() { + + // run some debugging + sp.web.lists.ensure("MyFirstList").then((list: ListEnsureResult) => { + + Logger.log({ + data: list.created, + message: "Was list created?", + level: LogLevel.Verbose + }); + + if (list.created) { + + Logger.log({ + data: list.data, + message: "Raw data from list creation.", + level: LogLevel.Verbose + }); + + } else { + + Logger.log({ + data: null, + message: "List already existed!", + level: LogLevel.Verbose + }); + } + + process.exit(0); + }).catch(e => { + + Logger.error(e); + process.exit(1); + }); +} +``` + +### Update main.ts to launch your module + +First comment out the import for the default example and then add the import and function call for yours, the updated main.ts should look like this: + +```TypeScript +// ... + +// comment out the example +// import { Example } from "./example"; +// Example(); + +import { MyDebug } from "./mydebug" +MyDebug(); + +// ... +``` + +### Debug! + +Place a break point within the promise resolution in your debug file and hit F5. Your module should be run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios. + +### Next Steps + +Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally. + +## In Browser Debugging + +You can also serve files locally to debug in a browser through two methods. The first will serve code using ./debug/serve/main.ts as the entry. Meaning you can easily +write code and test it in the browser. The second method allows you to serve a single package (bundled with all dependencies) for in browser testing. Both methods serve +the file from https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing. + +### Start the local serve + +This will serve a package with ./debug/serve/main.ts as the entry. + +`gulp serve` + +### Add reference to library + +Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire. + +```HTML + +
+``` + +You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but note that any changes +included as part of a PR to this file will not be allowed. + +### Serve a specific package + +For example if you wanted to serve the @pnp/sp package for testing you would use: + +`gulp serve --p sp` + +This will serve a bundle of the sp functionality along with all dependencies and place a global variable named "pnp.{packagename}", in this case pnp.sp. This will be +true for each package, if you served just the graph package the global would be pnp.graph. This mirrors how the umd modules are built in the distributed npm packages +to allow testing with matching packages. + +### Next Steps + +You can make changes to the library and immediately see them reflected in the browser. All files are watched regardless of which serve method you choose. + +## Register an Add-in + +Before you can begin debugging you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you [configure your farm accordingly](https://msdn.microsoft.com/en-us/library/office/dn155905.aspx). + +1. Navigation to {site url}/_layouts/appregnew.aspx +2. Click "Generate" for both the Client Id and Secret values +3. Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions +4. Provide a fake value for app domain and redirect uri, you can use the values shown in the examples +5. Click "Create" +6. Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article. + +### Grant Your Add-In Permissions + +Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to [register add-ins with certain permissions in the admin site](https://msdn.microsoft.com/en-us/pnp_articles/how-to-provide-add-in-app-only-tenant-administrative-permissions-in-sharepoint-online). + +1. Navigate to {admin site url}/_layouts/appinv.aspx +2. Paste your client id from the above section into the Add Id box and click "Lookup" +3. You should see the information populated into the form from the last section, if not ensure you have the correct id value +4. Paste the below XML into the permissions request xml box and hit "Create" +5. You should get a confirmation message. + +```XML + + + + + +``` + +**_Note these are very broad permissions to ensure you can test any feature of the library, for production you should tailor the permissions to only those required_** + +### Configure the project settings file + +1. If you have not already, make a copy of settings.example.js and name it settings.js +2. Edit this file to set the values on the testing.sp object to + - id: "The client id you created" + - secret: "The client secret you created" + - url: "{site url}" +3. You can disable web tests at any time by setting enableWebTests to false in settings.js, this can be helpful as they take a few minutes to run diff --git a/packages/documentation/deployment.md b/packages/documentation/deployment.md new file mode 100644 index 000000000..29b4d8791 --- /dev/null +++ b/packages/documentation/deployment.md @@ -0,0 +1,126 @@ +# Deployment + +There are two recommended ways to consume the library in a production deployment: bundle the code into your solution (such as with webpack), or reference the code from a CDN. These methods are outlined here but this is not meant to be an exhaustive guide on all the ways to package and deploy solutions. + +## Bundle + +If you have installed the library via NPM into your application solution bundlers such as webpack can bundle the PnPjs libraries along with your solution. This can make deployment easier, but will increase the size of your application by the size of the included libraries. The PnPjs libraries are setup to support tree shaking which can help with the bundle size. + +## CDN + +If you have public internet access you can reference the library from [cdnjs](https://cdnjs.com) or [unpkg](https://unpkg.com) which maintains copies of all versions. This is ideal as you do not need to host the file yourself, and it is easy to update to a newer release by updating the URL in your solution. Below lists all of the library locations within cdnjs, you will need to ensure you have the full url to the file you need, such as: "https://cdnjs.cloudflare.com/ajax/libs/pnp-common/1.1.1/common.es5.umd.min.js". To use the libraries with a script tag in a page it is recommended to use the *.es5.umd.min.js versions. This will add a global pnp value with each library added as pnp.{lib name} such as pnp.sp, pnp.common, etc. + +- [https://cdnjs.com/libraries/pnp-common](https://cdnjs.com/libraries/pnp-common) +- [https://cdnjs.com/libraries/pnp-config-store](https://cdnjs.com/libraries/pnp-config-store) +- [https://cdnjs.com/libraries/pnp-graph](https://cdnjs.com/libraries/pnp-graph) +- [https://cdnjs.com/libraries/pnp-logging](https://cdnjs.com/libraries/pnp-logging) +- [https://cdnjs.com/libraries/pnp-odata](https://cdnjs.com/libraries/pnp-odata) +- [https://cdnjs.com/libraries/pnp-pnpjs](https://cdnjs.com/libraries/pnp-pnpjs) +- [https://cdnjs.com/libraries/pnp-sp](https://cdnjs.com/libraries/pnp-sp) +- [https://cdnjs.com/libraries/pnp-sp-addinhelpers](https://cdnjs.com/libraries/pnp-sp-addinhelpers) +- [https://cdnjs.com/libraries/pnp-sp-clientsvc](https://cdnjs.com/libraries/pnp-sp-clientsvc) +- [https://cdnjs.com/libraries/pnp-sp-taxonomy](https://cdnjs.com/libraries/pnp-sp-taxonomy) + +### CDN and SPFx + +If you are developing in SPFx and install and import the PnPjs libraries the default behavior will be to bundle the library into your solution. You have a couple of choices on how best to work with CDNs and SPFx. Because SPFx doesn't currently respect peer dependencies it is easier to reference the pnpjs rollup package for your project. In this case you would install the package, reference it in your code, and update your config.js file externals as follows: + +#### Install + +`npm install @pnp/pnpjs --save` + +#### In Code + +```TypeScript +import { sp } from "@pnp/pnpjs"; + +sp.web.lists.getByTitle("BigList").get().then(r => { + + this.domElement.innerHTML += r.Title; +}); +``` + +#### config.json + +```JSON + "externals": { + "@pnp/pnpjs": { + "path": "https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.1.4/pnpjs.es5.umd.bundle.min.js", + "globalName": "pnp" + } + }, +``` + +----- + +You _can_ still work with the individual packages from a cdn, but you have a bit more work to do. First install the modules you need, update the config with the JSON externals below, and add some blind require statements into your code. These are needed because peer dependencies are not processed by SPFx so you have to "trigger" the SPFx manifest creator to include those packages. + +> Note this approach requires using version 1.1.5 (specifically beta 1.1.5-2) or later of the libraries as we had make a few updates to how things are packaged to make this a little easier. + +#### Install + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp --save` + +#### In Code + +```TypeScript +// blind require statements +require("tslib"); +require("@pnp/logging"); +require("@pnp/common"); +require("@pnp/odata"); +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("BigList").get().then(r => { + + this.domElement.innerHTML += r.Title; +}); +``` + +#### config.json +```JSON +"externals": { + "@pnp/sp": { + "path": "https://unpkg.com/@pnp/sp@1.1.5-2/dist/sp.es5.umd.min.js", + "globalName": "pnp.sp", + "globalDependencies": [ + "@pnp/logging", + "@pnp/common", + "@pnp/odata", + "tslib" + ] + }, + "@pnp/odata": { + "path": "https://unpkg.com/@pnp/odata@1.1.5-2/dist/odata.es5.umd.min.js", + "globalName": "pnp.odata", + "globalDependencies": [ + "@pnp/common", + "@pnp/logging", + "tslib" + ] + }, + "@pnp/common": { + "path": "https://unpkg.com/@pnp/common@1.1.5-2/dist/common.es5.umd.bundle.min.js", + "globalName": "pnp.common" + }, + "@pnp/logging": { + "path": "https://unpkg.com/@pnp/logging@1.1.5-2/dist/logging.es5.umd.min.js", + "globalName": "pnp.logging", + "globalDependencies": [ + "tslib" + ] + }, + "tslib": { + "path": "https://cdnjs.cloudflare.com/ajax/libs/tslib/1.9.3/tslib.min.js", + "globalName": "tslib" + } +} +``` + +Don't forget to update the version number in the url to match the version you want to use. This will stop the library from being bundled directly into the solution and instead use the copy from the CDN. When a new version of the PnPjs libraries are released and you are ready to update just update this url in your SPFX project's config.js file. + + + + + + diff --git a/packages/documentation/documentation.md b/packages/documentation/documentation.md new file mode 100644 index 000000000..81323a6a9 --- /dev/null +++ b/packages/documentation/documentation.md @@ -0,0 +1,22 @@ +# Building the Documentation + +Building the documentation locally can help you visualize change you are making to the docs. What you see locally should be what you see online. + +## Building +Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your [Path environment variable](https://docs.python.org/3/using/windows.html). + +When executing the pip module on Windows you can prefix it with **python -m**. +For example: +``` +python -m pip install mkdocs-material +``` + +- [Install MkDocs](https://www.mkdocs.org/#installation) + - pip install mkdocs +- Install the Material theme + - pip install mkdocs-material +- install the mkdocs-markdownextradata-plugin - this is used for the version variable + - pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) +- Serve it up + - mkdocs serve + - Open a browser to http://127.0.0.1:8000/ diff --git a/packages/documentation/getting-started-dev.md b/packages/documentation/getting-started-dev.md new file mode 100644 index 000000000..14a281f69 --- /dev/null +++ b/packages/documentation/getting-started-dev.md @@ -0,0 +1,42 @@ +# Contribution Guide + +Thank you for your interest in contributing to our work. This guide should help you get started, please let us know if you have any questions. + +## Contributor Guidance + +* Target your pull requests to the **dev** branch +* Add/Update any relevant docs articles in the relevant package's docs folder related to your changes +* Include a test for any new functionality and ensure all existing tests are passing by running `gulp test` +* Ensure tslint checks pass by typing `gulp lint` +* Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work +* If you have an idea for a larger change to the library please [open an issue](https://github.com/pnp/pnpjs/issues) and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :) + +## Setup your development environment + +These steps will help you get your environment setup for contributing to the core library. + +1. Install [Visual Studio Code](https://code.visualstudio.com/) - this is the development environment we will use. It is similar to a light-weight Visual Studio designed for each editing of client file types such as .ts and .js. (Note that if you prefer you can use Visual Studio). + +2. Install [Node JS](https://nodejs.org/en/download/) - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). + +3. On Windows: Install [Python v2.7.10](https://www.python.org/downloads/release/python-2710/) - this is used by some of the plug-ins and build tools inside Node JS - (Python v3.x.x is not supported by those modules). If Visual Studio is not installed on the client in addition to this C++ runtime is required. Please see [node-gyp Readme](https://github.com/nodejs/node-gyp/blob/master/README.md) + +4. Install a console emulator of your choice, for Windows [Cmder](http://cmder.net/) is popular. If installing Cmder choosing the full option will allow you to use git for windows. Whatever option you choose we will refer in the rest of the guide to "console" as the thing you installed in this step. + +5. Install the tslint extension in VS Code: + 1. Press Shift + Ctrl + "p" to open the command panel + 2. Begin typing "install extension" and select the command when it appears in view + 3. Begin typing "tslint" and select the package when it appears in view + 4. Restart Code after installation + +6. Install the gulp command line globally by typing the following code in your console `npm install -g gulp-cli` + +7. Now we need to fork and clone the git repository. This can be done using your [console](https://help.github.com/articles/fork-a-repo/) or using your preferred Git GUI tool. + +8. Once you have the code locally, navigate to the root of the project in your console. Type the following command: + - `npm install` - installs all of the npm package dependencies (may take awhile the first time) + +9. Copy settings.example.js in the root of your project to settings.js. Edit settings.js to reflect your personal environment (usename, password, siteUrl, etc.). + +10. Then you can follow the guidance in the [debugging](debugging.md) article to get started testing right away! + diff --git a/packages/documentation/getting-started.md b/packages/documentation/getting-started.md new file mode 100644 index 000000000..dc615ed94 --- /dev/null +++ b/packages/documentation/getting-started.md @@ -0,0 +1,216 @@ +# Getting Started + +These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install +the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install +more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. + +If you need to support older browsers please review the article on [polyfills](polyfill.md) for required functionality. + +## Install + +First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any +environment or project. + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/graph --save` + +Next we can import and use the functionality within our application. The below is a very simple example, please see the individual package documentation +for more details. + +```TypeScript +import { getRandomString } from "@pnp/common"; + +(function() { + + // get and log a random string + console.log(getRandomString(20)); + +})() +``` + +## Getting Started with SharePoint Framework + +The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 on-premesis please [read this note](SPFx-On-Premesis-2016.md) on a workaround for the included TypeScript version. If you are targetting SharePoint online you do not need to take any additional steps. + +### Establish Context + +Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request +urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the spfx context to the library. Either through the setup method +imported from @pnp/common or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if +you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. + +The setup is always done in the onInit method to ensure it runs before your other lifecycle code. You can also set any other settings at this time. + +#### Using @pnp/common setup + +```TypeScript +import { setup as pnpSetup } from "@pnp/common"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + + pnpSetup({ + spfxContext: this.context + }); + }); +} + +// ... + +``` + +#### Using @pnp/sp setup + +```TypeScript +import { sp } from "@pnp/sp"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + + sp.setup({ + spfxContext: this.context + }); + }); +} + +// ... + +``` + +#### Using @pnp/graph setup + +```TypeScript +import { graph } from "@pnp/graph"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + + graph.setup({ + spfxContext: this.context + }); + }); +} + +// ... + +``` + + +## Connect to SharePoint from Node + +Because peer dependencies are not installed automatically you will need to list out each package to install. Don't worry if you forget one you will get a message +on the command line that a peer dependency is missing. Let's for example look at installing the required libraries to connect to SharePoint from nodejs. You can see +[./debug/launch/sp.ts](https://github.com/pnp/pnpjs/blob/master/debug/launch/sp.ts) for a live example. + +``` +npm i @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/nodejs +``` + +This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the [packages](packages.md) page. +Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: + +```TypeScript +import { sp } from "@pnp/sp"; +import { SPFetchClient } from "@pnp/nodejs"; +``` + +Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. + +```TypeScript +// configure your node options (only once in your application) +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}"); + }, + }, +}); + +// make a call to SharePoint and log it in the console +sp.web.select("Title", "Description").get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +``` + +## Connect to Microsoft Graph From Node + +Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see +[./debug/launch/graph.ts](https://github.com/pnp/pnpjs/blob/master/debug/launch/graph.ts) for a live example. + +``` +npm i @pnp/logging @pnp/common @pnp/odata @pnp/graph @pnp/nodejs +``` + +Now we need to import what we'll need to call graph + +```TypeScript +import { graph } from "@pnp/graph"; +import { AdalFetchClient } from "@pnp/nodejs"; +``` + +Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. + +```TypeScript +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient("{mytenant}.onmicrosoft.com", "{application id}", "{application secret}"); + }, + }, +}); + +// make a call to Graph and get all the groups +graph.v1.groups.get().then(g => { + console.log(JSON.stringify(g, null, 4)); +}); +``` + +## Getting Started outside SharePoint Framework + +In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options. + +### Set baseUrl through setup: + +Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when +working against unpatched versions of SharePoint 2013 as [discussed here](https://blogs.msdn.microsoft.com/patrickrodgers/2016/06/13/pnp-jscore-1-0-1/). +This is optional for 2016 or SharePoint Online. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.setup({ + sp: { + headers: { + Accept: "application/json;odata=verbose", + }, + baseUrl: "{Absolute SharePoint Web URL}" + }, +}); + +const w = await sp.web.get(); +``` + +### Create Web instances directly + +Using this method you create the web directly with the url you want to use as the base. + +```TypeScript +import { Web } from "@pnp/sp"; + +const web = new Web("{Absolute SharePoint Web URL}"); +const w = await web.get(); +``` diff --git a/packages/documentation/gulp-commands.md b/packages/documentation/gulp-commands.md new file mode 100644 index 000000000..1348e5592 --- /dev/null +++ b/packages/documentation/gulp-commands.md @@ -0,0 +1,162 @@ +# Gulp Commands + +This library uses [Gulp](https://gulpjs.com/) to orchestrate various tasks. The tasks described below are available for your use. Please review the +[getting started for development](getting-started-dev.md) to ensure you've setup your environment correctly. The source for the gulp commands can be found in +the tools\gulptasks folder at the root of the project. + + +## Basics + +All gulp commands are run on the command line in the fashion shown below. + +``` +gulp [optional pararms] +``` + +## build + +The build command transpiles the solution from TypeScript into JavaScript using our custom [build system](https://github.com/pnp/pnpjs/tree/master/tools/buildsystem). It is controlled by the pnp-build.js file at +the project root. + +### Build all of the packages + +``` +gulp build +``` + +### Building individual packages + +Note when building a single package none of the dependencies are currently built, so you need to specify in order those packages to build which are dependencies. + +``` +# fails +gulp build --p sp + +# works as all the dependencies are built in order +gulp build --p logging,common,odata,sp +``` + +You can also build the packages and then not clean using the nc flag. So for example if you are working on the sp package you can build all the packages once, then +use the "nc" flag to leave those that aren't changing. + +``` +# run once +gulp build --p logging,common,odata,sp + +# run on subsequent builds +gulp build --p sp --nc +``` + +## clean + +The clean command removes all of the generated folders from the project and is generally used automatically before other commands to ensure there is a clean workspace. + +``` +gulp clean +``` + +To clean the build folder. This build folder is no longer included in automatic cleaning after the move to use the TypeScript project references feature that compares previous output and doesn't rebuild unchanged files. This command will erase the entire build folder ensuring you can conduct a clean build/test/etc. + +``` +gulp clean-build +``` + +## lint + +Runs the project linting based on the tslint.json rules defined at the project root. This should be done before any PR submissions as linting failures will block merging. + +``` +gulp lint +``` + +## package + +Used to create the packages in the ./dist folder as they would exist for a release. + +``` +gulp package +``` + +### Packaging individual packages + +You can also package individual packages, but as with build you must also package any dependencies at the same time. + +``` +gulp package --p logging,common,odata,sp +``` + +## publish + +This command is only for use by package authors to publish a version to npm and is not for developer use. + +## serve + +The serve command allows you to serve either code from the ./debug/serve folder OR an individual package for testing in the browser. The file will always be served as +https://localhost:8080/assets/pnp.js so can create a static page in your tenant for easy testing of a variety of scenarios. NOTE that in most browsers this file will +be flagged as unsafe so you will need to trust it for it to execute on the page. + +### debug serve + +When running the command with no parameters you will generate a package with the entry being based on the tsconfig.json file in ./debug/serve. By default this will +use serve.ts. This allows you to write any code you want to test to easily run it in the browser with all changes being watched and triggering a rebuild. + +``` +gulp serve +``` + +### package serve + +If instead you want to test how a particular package will work in the browser you can serve just that package. In this case you do not need to specify the dependencies +and specifying multiple packages will throw an error. Packages will be injected into the global namespace on a variable named pnp. + +``` +gulp serve --p sp +``` + +## test + +Runs the tests specified in each package's tests folder + +``` +gulp test +``` + +### Verbose + +The test command will switch to the "spec" mocha reporter if you supply the verbose flag. Doing so will list out each test's description and sucess instead of the "dot" used by default. This flag works with all other test options. + +``` +gulp test --verbose +``` + +### Test individual packages + +You can test individual packages as needed, and there is no need to include dependencies in this case + +``` +# test the logging and sp packages +gulp test --p logging,sp +``` + +If you are working on a specific set of tests for a single module you can also use the single or s parameter to select just +a single module of tests. You specify the filename without the ".test.ts" suffix. It must be within the specified package and +this option can only be used with a single package for --p + +``` +# will test only the client-side pages module within the sp package +gulp test --p sp --s clientsidepages +``` + +If you want you can test within the same site and avoid creating a new one, though for some tests this might cause conflicts. +This flag can be helpful if you are rapidly testing things with no conflict as you can avoid creating a site each time. Works +with both of the above options --p and --s as well as individually. The url must be absolute. + +``` +#testing using the specified site. +gulp test --site https://{tenant}.sharepoint.com/sites/testing + +# with other options +gulp test --p logging,sp --site https://{tenant}.sharepoint.com/sites/testing + +gulp test --p sp --s clientsidepages --site https://{tenant}.sharepoint.com/sites/testing +``` diff --git a/packages/documentation/img/Logo.png b/packages/documentation/img/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..73dc570b38549a22788bd8911d7da860bb191ae4 GIT binary patch literal 3151 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 z zp-lji1=OOb3^ed6T;AGqGSbHIH;dY(Z)$W&HIM6?#Lz2w^ zj$>9Ye1vrnfMm@k#FJ!9sUWXE<(JkDfMj?_coO8&yii+%G?SWX!*BvPS*YZ$b4*g! zigZO~Rw_sJF+@628kxsY*1@AAO<*OfNW(UNJLzzTU3$47;wh?=4C#zKFT6iAsKYPQ zvXh8Liq&_eyhR|6t|Pa9q*oZizyE`l6|A`UPC5R9Wq(`KVDC&~sQEJf4VLkI5q>v< zl&?a?tbia>V9b`%ZWCnbgqU8=ksr$$bVSsU%>h&hH|jKWW*6RT+Sm?fFH^o<0`>XxW$WeCMA0Tqx!UkXZ|X@DPykgk!Jy zB;H6DhNbVYR!fxugmx_?KxlWQ2dYca{ABNXIp6h)$xdjOe3Y$1EXmGlPI2vJ0qv_y zu$rvdiujx1W50nJF>I^!iCTA|(E<60KT!Hi4g;C~Dt&5!wuE+n{o-Z9>Zqr)=sgTb zXG9=zF=_B}l8$gLz{^bh@G<0Egkj9B;<6ZK?zB8eYm-)r04Em~m039Z7oD#ku|pbvT*!5gLH1?<%- z(d3TKs1e*D4*``OT~V`kcb>cl!Z3qrM+U8V;;T^-dy7}+NuSV4OSbwimWl(+`S$UY z0P7mYzt=Swz`BM*4A~V)181^ez`%yA7V10SvA`FwvTDz_XkLJvz1j=S!5fYA!~*0@ zFC8sDm7`Kja?mXww}{acmT~}Wc7$B(`Eo~*Si7cLUy;~b4ft|XjK;Ho>iN<`+YMrJ zNrw<7#Az9jUu9wnz?2-Af+m|Wy)wY?M%XYK5mI=2Kr>B5D#N9LTW0T8*K^7+&DkcQ z>p`cG?!gSv7n)q?iv?;?LSK3j5TPo&EcK*fIe+2{*%3Uyuq7v$LKcLOd= zW#kTx#7_sqnNtT}S10G{_(o# zR&4>^{454BBKe_1`QKb`FWS&Nm?0>;MHypN!sC zaNGIJv*&vSF_9pEx^4e$wxi(#naGLAqz7gdkL!6S!^Fm2VpXSO4^GEX%Le6I2dDc> zT?ONBMGnrG4Os@o+N_@J*NS-<88fy#bPzY}mvv(D^6>PX;bRL{bg_{!jgc@ntB9Bp zcHT(z;wj|-2Lf#!a&*}#iIZsI}1Jz zst0hv|HX2vwxN>FUb3EcOmm7!aEVL#o!>f~#B|Rn@H|`Lon7Ud-w;^d9#q~DbguI#tLrGM z`)Fkky^bB#emAOfF1q_cY~PdEtDLy&&rY#loa$fxAJ#Y1($dn?f$Z1+Uh%KgCibre z=PSmI*9k-K{(Cg`??mhW^}hi`7W{`H1Iw+7C=fd_882yBX5LyglqsYhurS(MJ#rSa zO)Lu4R>Mw_+7B$Zw$)A)l075%i*4&n@-+VpLF8+kG98rfXqc`x&n>_;gLJ9Sa&FS826xnCfH`EvD{qT18Rkf*u*~gqP??$m8-7mNUTOPz<7pW?oh0Wo z@{%xWMmNh0c7V#s!OcQqvCtj+-Dv*RP9|p>!bdm2e4QJkkHvex#NRAO#$Ak~)(9QG zv1>CLd8BV+wQ6iz*4QVb@9{>>QuD3S=wdZ%K5oLiU6$}A#rH(A8TqEv4GYA*)r54_ z25nz6bW?mU=%F@Th(^t)L%N6-PCUqBoRlytu7xCL}vuEVNAjeD}_tu zAt0_xts(X*You73hFC=;nGB_2iaK!e3>Cm6xp|$xgF2%k%_)_m1j~IMRCW<+=GW^px)oF zoRr|P3YnA>E+b|Mhi?kMWF}u_z5h@9hA9){bKRa2qUsn zk;voYEi~3UhlOu>5+YYRmfeVr1lEg)3AFK&+VRovm-;$A>K{&N-|={JeP3vn_w+&1 zReFQdj-QAK>*J9Wbd}T%46Y+#dF}E;aG+TMG}}pi!s(*H!1eAPzb~6NU7j!4Mayv3 zQbVWY^)$kt=k%%eG!O3N73sQDD%ve6hW~V@bKd6SQzS*XCo}zt>7j;7y;N~bcOQ?M zVr9A%ME1h`WXA*j#L4cZaqrEjXZ~8Q^zo@{kJ?INe4lF_s1HAklWguWwTw$m95*A= z2rV1p?7U!kDW%;vYo6QgU}$}QfE|y0H0yZo{!5^%dF|5)NqmNq76p{1s+~nZ^P_M^ z6Np=12UMIiAkIl1j!7JNabDe(WsVVFT23LzS*nfd#XUk+P{2xQO+ZRiSo%Fk#Z& z)o5wbd@Lj3QAU!26cM~1KPKo%zYR0(&$1N*A3is(lSFjUH%D-)&;@eKvVbk^m7lPd+fp zqaLsd`-!R&y2(z)aMR&#QP2g%Y-|%4N6y3RdDJ8$vp{cY)bGGs( z!@>&7|61zfyao4eCE>GacB&=MWhv+ks`C7%iIfZhvY#^mJ2GV=AVWb4bW^q`GT7T= z+!4zw;0r>2CS<3Mbf3G+-1s7Q!IhL0+9Viqm~$f}Y&8j&SLNe+<6(oYx56zk(7`5; z{PQ&F^Bwmwj=f9yml!6QK0h_PZ`ATXnhd<`?;qfDx7VS|Snt8!f{5c^MI3twJmhQ~ z4j#!!#<;Su8ZktnU<$*W)I6}?-b_k+%7nj)o?*M;4!cOtmP&{K@xS!o{>$iI>X2h* zQDx=!-F)(=!6_|*m-l(_w+jtx)Rnp@VZD)f;rD)G>fD)wHzt;nKWtpNUpzKRL#{xh z&~L?3Bwn!nrcAJXjykXHmBv+BT$@nvipL!c}Q&d7rwRHvWfn>GS zj{AnLt(EQYs5IiAhj4wsK*{r*)D2h%zD6!tn1#ohdx=aX%(*>9o%Y#_bq!wAJ@ne) zUN5vyIU>96&~0PeUQ6HmbJL&8^MA5ydSgrKssuXCa<->>SPj6CehJcw->yz)A3v$J zG$iSF9_^gP;5t=A30ejv)9@aUH+*|leBvQ!ynQ%(cgOqpl&m*C+b2z2>LEn9)LjoglDsk52j6sh>B4e99XdFcNEKb&pSEw1 z!go$+NZ!FF%O&XP_8W3_su!IPhnPst-k%?W_Ssl@hwIe4E87Q0dX>9z>zn?##lyyK zuJ93mnBvZE45u2ui27RhRCW*FsMei{yxTD*{Fiu!11r|bZl^8p`LHqN>9t;C*7r&! z;oBW-+$XKS8=G<@RSFofNe}>|Xj{F{^+*0+RX$!*KoS@ZeU~;@uhe|QXKzxs;TfN> ztj+gdJiqRj{2H0q=gOFtxVXy7)u8w?vw-N zw?O!aiGoDl6D?R}j@U_#{O);!Im0CSybYJFc|aZS%4Pf*6yCyt_)lREGrP8n5e<|BvIXLDKZtaI|o-l2ZfTc;dtB_6_wkHyTq1VtKvC~N)cF$ z^>j*skQ2XUV^){M_z{5tLk^=Hha>A!HW2zPFgS^EaF~t4bJTh$m`S!x`!uGOjl0T* z=TA9^yvStBWexgeMNph3L0IMs@lgt?lOwI1aQZ>Fk+qRy=@eFZ3EoR_^k+~Hu4&pN zo7z8@C>tWgf7)?n^^9qR@ivdsA8mGg>Cfz3j_!yD?Wk9r}0~CB=(tX2X^IG*rw1&wZ|*e=gKLg+JadNd`T%f|wS- zIT6x^6BK6_#ZuYWgA5Q2k4wqL#e%@^iRfWE;_-7Ko)+Y_ki%a71%9v80(7z(LZHGe z=q3WV&hv1+(VkPnMFFHVycDF0MiRwi1JQ^hQw5X|Q0p$&&T!W{PdQSHd`+7yqwB2P zl1yM@AyD)TT8Ub|r^Fo&Dy1P+C|C_T$PJH`BkXCiL$Rk|dIsivblnG<0Kcn%sYb0TX{ufz_qKY7d`7S-;glB#Y_{)+V5#ug zsUq?4ZnBtY7>fk1RAtF*$n$D2&WMt+!)@AfAt37rP`jV5`bYBaI*)w$5=tBE>5|$v zTfkOY0(&KK={}@~iBO)xG_%05E0}ycOi~C)ih?~(N4B?svu+r4%78UAk!KS+g@GC78x0kWo$yhUsJe{$;eoD%oSF!Fc(w-U6!l~T3o(#(UTC>KAm9f*9#nav? z0d3u7XkVm(oR+3t60k_N##^=N!NKIW`@Q$j+RFzWVCM~i2%__(tk(_BESc0qX)QKp zj84gGJmAn{n3keZ*{Jzr{@Ap-?VI)z-Pw|4PcL%a4ARjCp6Q+I|=!4n>uau1?$GTAl-Zqx%#;=_|1$9iIJcf zCRTx+!V<3fYf0x#qAn`>=o&cS;AO>>2c*{uD*JUcX3@4uW5Qo_?cC=eq<)q3CP*`x zmgrJ^elFmWW)bTKX4IkO@ieymRf$qS)4Mu5aBb76*%B%PYlo*gv#|~!Ff2eVl9i&x z!3^NxQZy{pa?>*r57tA4pIF%=)l`4+zT3D$#UpvpLUe8|2#HwDwf@>Z_X>4}UE2;q zKGv$xY3z79*8T`{?qU(RVTGZCLItc}{EJr`-?5v?a`pL&jbzYn88)nDNFQ-6IIhu^ znuu=npniQ#I`(~M=8%-~6n5AaQbWagtU&Tu5c~@IiM$GvgFzWV*G!Lpu4^F~1}6n} zOTEUP27$dON;|`P{txorGpeaYUDTfGJ%%2tp?3(q2pT#FY7kTq)KH`-Y5+w*#Dv}n z9TYWG0YO7kP|(mjgrZ`@AQn{AU~h|c<8t4%*WUY_v(LTbj&Z+l+&TQ^5C4#niO>7K z&m(!J!Q`bv3>llqI9%KZ?yE~5VPPE}!)1cN4P0yo17R)Z?@rQ>q+v%m$U3g|8Q>U# z7--AGjiS8$0#WYh=Z)k(>l6XcuPPIj$Z4`C0ADKSrZE}k@f&>Pj0)P12rsiHj3A^!F8J}|l8 zvP;}5OYP3k7U5p6`{GHydc0FtA2HL))oS2!Nq?jFfR}IF?yd&k2Lq#@2c%^O{eQ#- zh7AP84bGJeDjppOYuv71PIxCZ#4#R1x(`J)ZjO#4#C8p>Rojbsl82M5hMk4`X`DgE zid#Y>kn>~MlRk{%3}=4^gfaA7;n#oONS@nBW%5W_BXG7+ynJQk{K_zf520{@s&a8# z4v_O;w03+edmQ2>099fo+_;ctt1+$Q5qS>Ok~g*`50Xv*cdm@RP8!)ngW@UUo2P$X zbB;8QKevZTu%OoLku4R#2DWfLe(ZMF7Edn7jRtw(HY&{9kVunDzFRzLkPOBouUs5W zn>6K5zWw3m$pe{Frf!8znQ)=UtK)D7xH0e#F8pK+TY z$WM1CjVLf68|c$cc@S4V2sAN0>1i}}HEzbKc}yvLMl^fW27u}u61M}!OmMUE@}m~I z;x&!4lYvNE>p5GTg#95T<+U5=R|sRfFy^as4TJ&oKu$odt}fZk%+lW8(amk^wr$(J zynK9o_Uzl|_Y3p|2Zx4+9t;aReE2Z^$dT~K$YaNj9Y20NCOSGcCgwzJY}_w@+n;Bd ziHXTcNhv8QsVOOGsj2CIK)I|xgla!+YJaNKgdVlh($hkf+L<$dQ>mR7n$-THQWJXA zgc7x1`m}!%r?s}WwYUF+IPI_2G$C;7?;jW#7#tiN8X6iN9v&GP866!R8y}yTn3$ZL zoSK^A&CSi-x^?^4lH33F^)?~4GWa{T@*Sr8B^a2>{3Et<%3Y@%FXtjO{TFC>G;|Ro z{=inFPztSbB&Y{TRwpqWktw2KURa5;HetV1 zAM`-rPvJw1^{5%DnINE0#ivJztxA*`Dm&pJjJ0<6c;oqQc35Bv`tRg$_-P75+%y%kPh;5Rn4FR0am|8pjTu0=qg^BA| z-#+3dV%Bf%>R4SGF3{dL+12@8m^2mU0J^Vzd^O*C@X=)V_0MnleQTNPx83Oa`hI!# z;=ZXH-QPb8md4I+d-N4*q7H|!ZTAO=u6?vVzAqjm88XL!s}W~U+P768q$TMpOiAK{ zW@xDN!=e&Lb-u}+B#FKAJiD!327_p{jB$^wk1>xyWP}C+Qd^01rnFT^^`hg31e1Vl zvuDFRJAVr6v5i<`o~_TuvDXFbo(LM~k;9(-5$yN67; zDoYARd-2p^S8RiYEZ)nzVRdxg_4d2pq9Snc9U%ed*Ql~$gk_UpE}odz;Y9y*A}|1(hU4y} z6!pn%B9IuUP&Y;I&R6@ngIH)NJ7wp{Rt5+k_+&cSljy*3#Q8j7CPUo#A_(*hh-K#w z1(9IlC=q&+d~CGP7D{^XRbtylkgB-dXA_&DGjd&d129~njlH947jpoyz9Y|p0Hum3 zA5FZZMF1RF;Y<*fwt9H~StW;}9bSvqc^NyXyLs5iCkTcFy{9ZuPJ#RlE(+>!2tR6l zL5m9ZV`YL6LCXidXB?Y$6#IB@YIcm2F$K?f$IPUm8#^EO7-o zx%PjhtSeWv160T+Ee>T(dd*AOAWM0G(qEByZ6zU_0Bm82?0&6qLgHSC_-j2XDsq1M z^;K#4j2@8R98XeY7k2#~fLt2xA-jQbh5&Yd(Cj3?xjg8*TT4auA!oHxk-4g9K|8Cb5z9jxZuz9uhT4(z&&E{X<7Jr@42t!T6 z)ch~a=6`QgGcx}3`TKi?$Ky@^d}d~5W@l%GZ<(3DzG;4LQ~&R*Q~zA4{x3JGgeY%*`quul$1R;A zfnkLG>~U-D(Z2mhkNaBnnOm0@ZZaUdp1hj7%MZ@&dDk%t9~J49*<`orS2TfOu2VK1bIpD&Y}0>a2mquZe^n&vRBySPSKHfAG<8w zanjB>NA8&aEqj}FiPeIBvh9_1&Q_GTiU@h`iM2>GT=RI#vvo(DjB^({Y0KLZ&)G=Y z4Y@g8QxPaWbCJF3DJI9@l5Rz}Vx?ZbAUo(UL>rBZBgQ#yn)CDi`<&xQt!T4ZZbjND z0A1>2{3xsdyD$QWq1+!D?-gGAaM@+huqi}W>=Gnr63)Q1W)!GOd%p^_}T8f2tHaXW-q@5!usPt1oAkzKl2(C^g zg_0~c+I$>nbQuWsl7P1qe*US?rx}`~lw&eX9bE*2oqz|@+hsL>XMW5dJ^n!e?0@Ub` zfCxqTp=wfik)_T90jRDt>SA+eRO7>*{eY7#IeI>ot2;Ouk%@{?dlDu+)K8d!-Izb_ z7s5z_w($_e7XMBWy?|$+FG|_GVWjlqG1$A^4sX3aU8%s%Dp^Eez+Yruqq4U|*`?ZA zq==uJvw3*2CaQ0o)wCAEP48dmw2Yp4VrIz|^$b;fF&LG77_#eGa@rYS%=FqYZzaa_ zk+s}dL}tn&|4|t3V`o_bVeiH5`su-UJWuNKY_6WAyD2sA3qF2m2^8l15GDO?Y($eV zn}R>OEABHk=IsQ6tDrmwH&B{wN>u!+x-9*;^W}}YcbanVKY{EcH`;1l_vO^J`WrP} z4<1eZ5QnzO-Wcwj!q&WZtNzhs>?@n*kFishA$BHYknG)IfABZiDG*X5+hBNkgQ?e` z8tE9CHdh-R)4MAv|IXEoR?4_~CAc~@SWTX)D3gt(CWx0_gqaGMqjp^Fv^)q>11wv+ z3Y8#mk$xdG_cedIV=iMgBhJqdE%?%8B+EiNv3-=%{}xL7r>*pcFW==Y_=KhmVK%f^E&Bgy@Z zV@4NTCNgz|Et_#+!uPJ!q1f3ocZ8eD_0p5!r;W;5N-{PA`pp_x^nNsyYP`E6Ei7z>XYM4GcKQWRSsu9&N_bYc-eSZt&yTYi z`cd;8NY`+NqqWxd0#Q4|7Ba}X5rF)xbO+g{jqRmLO;f0&5mnh6DKx??g%^CEkZnaF zVUv(s3~L!o@31X-pGKxH;fv~qK(1c>quwpW$1J*STzAj4NNS}vHyu8y?YktiC*t+h zrep1|_N?}5?6j`3sm*cQkU~g7A=N}#HQc~YFhwL@mz>$yPEH z6`e=kR{k2$baqtOy%Y29s9q={1Y-2EwWa@V?|w^$cUjO*BhccC*9HiC4IeMrRv~ey z&kG(gd+rK0GJF1LAJBhGt+>6Z{!4}F>Xzby)r8lf>Zgv+^oougQz(oG!%x-?l6dfI z5alP}?&Uv4}_F$2Y0Ivk5JI6>&-OGwy29i|{iW7{a6T&i8&>6PwRX ziyre)p1x&qWm&6Ta#mwn;=WTq1@8ypqn058$FG`0;pJx{x1)PTc zB@eKB_02(;r2a#5S7-{~w@cr7)$C?=Pi$-1wa?vx^uV~Mt?dglQu!atWbO&hr2o*^ zxydS6Wcs4EDSN5!HRM5s%z3J(xV^~Sh7*LOhR6LNCG#FLH(xthy!1wxF>y#o+TxA%Ng*PN}2`O&=5O0_~BGx*2cE7R98tx zB>mN1|B>L{51KnyGvGrtFAqQ-Cf})F^pmT?*De~&ERN+adZ`hYo$vC?;>6S?z^?B3Gll2# zI-Q^G{X9CBUtDPYkRztxo;dpPLtlTWf&0|AbEP*va-2q91#J6%G4`QK`;@iUkxh~h zzwa&U`u)+{)OGoMpTaN0skJX_%6>dMGW2EqX#Bfi{gaP9ZhpIYbK|FD!j{eJJ2$>B zeGFzZ#`1jve@0|L4JpSu( z@$W<8ApR);$v~MiH2uQRKXWt0M_k;X0KTXF1R})RxtRet@9gR$gwe2p#<*kvR3wtH zV1Srq<1+c!{h#FfYmaGm#GUlB7UQC)0Hkju=rCVH`U^vLn28iQC9(>;E5Mx=h$wtH zbw4Q{3lfUtA_!#oC;=nNqWZyPdZ9EHUTZ?5C8O-z$A*q4->|aB8gsUDPOYOc5FXS6lDlD#6$ob z2AWyrU9PI!`V@DAdT=usl+Ey(XP^ialVK8Ofh?o6h`LV3EO4>=Xj%7o-Uvp{%i)~Q z?{ZenbDnC@?@^We*dZ4jS;YmW*)_O2E*2#QA7Ml8@!^^N!Z*o*gr_8ep_t}&MN6KN zzNoT{x3ch{{k%l}70&wKR@ZCIt%=_d_lP;}a)oN78?44)hwl)|I@u5{8n$j2RhJCV zX;`KA*e(`Ch76maqGww$X9fd0STJt^wv&StBVkgMMc<_oTB*uCWQ-&mqDxV-Bw!^; z0BSLH^>{I|qByJ`a++d3&Bp9wBqdg%1X9J1$i)X^(zKJ)?w2YjcPgEu;*QeyO>RtTzJ?nnRa*)z#BpV}(}7_&;yO?pBA2|g-fw%}k>kf3`hL6c0-Zr!eZ@LmCCJuuw!RxdcpQzb#?E9bzLZP`C!jKHz+qbhagM z>aZqonpBdYDKuKkYSgG$JPtIpJrXD>x`U2gp0123Sug2fre&!yMc)bHDd zt5`rkL(azROq^J?U537EzEJ2ULa6d6ZQc?DxL1-{)S@S}<)F ze+lCyDf)XbsE4XNO-T!OF-NY}DSS%}a#7myu1=DImXA%7L!ZjBtk)HsijQQmBO8tv znA{W-=K$zV|MfY_xI_<}FN;texuGMd(sn1ji=}^gr&UdrjS=bM7Tb$0od%T#jjf}w z?+NPMV#qZP&cV3c_F2feNJY^I#RMbsl-Zj4EBVOO4fDzc^QhA~jN>^R+$aU!$H!f# zSwv+J_VTd%2xfc9II;VeEfw4$+>We8+Q23mdFNNt^1gUKP_|N>w~iL9NuNA@@%(g6w~UX@ z{SC&kW^=_^R}NIiv08hX`iCeXpYjYmA^~kSris$@p|ydkX0=k#^ufbAW~OFba8^U? z?Bap-OS|386h%Gh*dJx_2ec4K0-Q3B7zH2}T+9p=rb@vwWMr}kpbRQ*foKD}-vF42 z;WG)Sdlj-5A+0pTNuMNw7LYiWD!71(tA}K5?m%Xw2D@~KW^mBoixkep8|+`7LB=u3 zpdHAhiWI_04|ESWrJTI?^b4Vc)$)!;>y6E3^%qY!p4f2D@0ur&@_hTX9z(G&V92;Y zSB->uE)8LGgpL+;D_gFdi^27UiwD|l^RZGdw2fJt>B7ASPq0t~27JZV*5^Wdd0#v2 zIKUv|ve+8GErVZ21O8MTA~fN3G`NG08xkM_JYXYK*!eufJO@`o!zfoD+J>$Rg4iJh zr~EV<-VW8}$Fj4v>T_Rqh?EygUL>$GD_lx(=!}js%ZAd-wBqJY@H@EPW85SO`P%#% zf6+o&fM(_4&huQdjd2CfuCYYROEI$ z@D++v)SSz9HgDQI-#<`!5x zE9nvSr-~Q}LdL`6?96w(G0R??TV}|)Z}sUQ)bX=h?hGM#40+@*m^+;CKxENA3A=jP zc76yvKg=i}$+H@UmC9sv#lPMqo}&YN2tIXIXXJeO=#Aj`#s}0(KSq%fV=Zn}Va^6P zHr6q|?ONB^=ee;PR@=C8<8S-LQS|ZQ^6^o*ll|k=!Nzf3*7$_niFMS8TXGY4Kiq9D z!HW$ntW0>3Cq$Dca1|2|eoXLnCPk7b+-N9Q9<+!BwPa7Mu0W{-uq6Ywi2%j0CSQ(I zS5_tkI#VCYp`Ws*y!aqzK4k636e4-Tm4e#Lg%*&&@568s523=7yRVJF1`>+I1{PO% z>u?iyyCyKxQxjMU)Q&WxvS(&AZsxggMooiCkZ1I9)FcgN|s zJIFj9<{>Zn>}|E*Za=_6ZFy5k)^`i#XN#?GKk~g(uDjKWdaw56y}G7}x(P@w@7|@~ zkX65J{TtHs0ti1h%Y6Y~Kn$o6Y8^IFOl<5o*g4w>qv=jmH(@kgSP0s_Khz`SkZ0(T z9S0+J9zM3~$njqtA7MJ(FX3l8Jut28SI0+KPd|L-%CDNw;d57yobUK+(LLO#N?hpP%XUmkIrEfd3N@^8Qb|ga#quugCKeqAJ8cHiKoi%9S=(IpxaQN&awd zskX}lLufn_z!)hZJD1q$UOZEZosPN%t*o5-5ApS{$>qPTLH#eW>wf~*|2o(F2W0(66Y8(9`hNzYUITTwfm?|FCkT2d zR}`0_lScS8QvHdbT{D#fisM{bYbVYK5wtkb1LsvGT50!nSfC1E=?lU3u-)=J)XDNL9(V{n-1L`%m2QI}@_P zuQ^lH^OD(@wqAi@^IBdvU{F+HuleKMRVkw(uOGU#>i1bcJCIJ{z)!b{>=1LZqPTi5 zCKpKTywND22c;jX9bna2`oDQt{gWt)z@j&m_*&}KT1FD!i6DE9i{^@G83ghaDz)m7No zRFf7Q`E{WYQcW=8^hjxm+n1xCjPqg}BHAh@HoTxE0RBZikSWXPHplZPzq~Lz3vFq9 zKWR7};%Im3HLcgzHoFwLE>)8_JGp_sY_I?-dTlsqqRbU)uUF=kY;x7Kj7SP7!?Gv& zanXG)=5J|skZ6Z=v%Si0N~KSc*_?uONPMY~`lcq?r9#SS0lhgE9lQx0(KK$RsaijI ztDzD()l2!c8`wtorFa2z{gV$i&&}gW47lmFCp3-9lMz+vkYkL17tvRn2mQ_-e-_$% zbnP51F$Kcn*o}dP_>TdZWc%k2LDKeiSxC_^L%d9eQcSjq9)Ow9ljgH=st8)@LeasU zgN+xKG(2q4w~=;g?m=5qo*^5~-gh=tc|u?sm6KRm+Nl*(oC3x|X$w5Hor@zG#05Nw zwrF(i1szFCd>Z!dS^{739-F|)vsfN`3+fI@dD+xpI$qu#J#K1ik|7k&8(vjtbweG{ z`k<|FuZvEE&#+Tz`&n}E)^UVaZ$&#eHt{Lw`Qvh4KgwGP2VX+((2Hao?KAQ(*Gc4d zn$B*o%zY!*!pcb9qW&QdW;F7^#6aWza9CPQ>LGmF7vwg4zg*c}zotWn{MYWZ?n&oA zR3-G~7?Ho6mY)u~KNvn=tg2yVSv?ThR@7=l`pD(Ef zY6p%OOI0y&B#W#qf=+N{)2HirbtuwQH1}+x>Z|b7g@+C2CNJF62wD0FGS=5i(4V4l zpI>%=A9pSA!1^m2&`z861b)RKp?9ZC3vO6E?`toIke+uK?hYMvro|saPz`hBl^shU zxP^XE4gEPv_R5_7b>e)^p&SBYy>T+m+ZXR@LFqF$H8{V(+qUE5^?hq|DGWav1cB$; zTN>M8j7b!++w;Krmjt|&djPTjUXt11(bhqEeq@)kxW8B3`qMaN$XYNzET=AI|XV=N@%qXGJyINshA}pwR{@VQtK97A%UvLU@)Omo1%a71%N# zMb!HiEO)}3p(Gsx2qk^8mxxMV1^fGzANL#8y47g;B#iF3_O3`FR8=R~#r1mi<>HR< z8oR*yvAr|-B{#6ow3wRX{^xQ_g|dw3pR$aXKV=!UzCUFdr+>;af_D9^KJ83Cvw#iu z;A&1r6u&5b=BB=5xPCI~oaos%SK+2OCrv>&=)a=9L-q}pKq zU^&H1Y5(vMtpl#Hj!xi-QW%&Bo+<2~W6OOD#f2`hUK-n<(lL2?tOep>zp;OV@!8?o zhNs!ma+)FgMwz8*o)s90XTXoub&LoG!>t^maF5#Ke@?sxJ)LBbP$%P-jf8eW)Q%bILtx)*j|zgLnm{Z=u=+qK9E6{9gpjyBjquK~;3DkZmcp4bWrbN++SfvdW9F~SPF;d6&+yA4HKvn+}^ zj;LqJ#Iy>o|MIF7=_NVG{>hPGK1M5YhLvbsttdUj=|{4e`n$be0~-86B}@3Ol|{D` zkIbhA1-01Rr$FE?8SjzhL_^;ux04pp-s3?#TYr1jxZCy3-KnndCeW@oGA#4c-25xX zu;-CZ2morfn{M^2aNU+e9L2RA0d#7Lw36KO(*9ct#}|@lB$*tJ{Xo75B9sjrw%PBq zFlgp^R{ro%!;GxewJ6n_nr{H6tnXs&ar3)-S>f6A@&`&z`-g)vc@E{3=2CHXyrQp% zLh_kLa<$eAO>aytCCYohCE;zuwx!daB^l&QBaGebB>SC_^Ux;jgS+!7)F#~O?trd) zzM3&(2NnF!Nm*~c&=i~6xwoQk{Q8??-+HXYo3;Hr`^L?U3b%M` z1H*x}uiK~~m3~zf{sjj<*?qnMt8l{|CvFc&FPcMh4VoG<%KEPJ_xt3pyAYOBKOFzX zZoJ6yOAjH3a9GX00=*{DYp2ppPBF5Is&zfvw_5lhl_LJQC_Or8BKzBe1OEOSBB=P( zEpL27lkifJ0tey~8h+8M>W9u(yqi|DVYfL}eb8?QHfZW{Xwr~5R&jOTA)RiyUgZrf z%VrzDiLeq>0!#6otSzSR^76{DecE>iYJ+E9pZ)Q~S6Ax5nbd-_?l>zaK>thcz9d1) zwm4^g{g=Vx(AAvWA09;gucLRwwLi9XJ(C50oshlp{+yK6vfcVy(*-vwYd3XUdtCk& z-E!mO>p1I`SvvZXN*GYx*Z&9J%=|ap zAS#eLnbCX#2SPpP9Kq8N52;uYiiG$Vg0crnBrUMUbAL5dq?8~{b`UWkiXck~NHS)W z1kR-5-}14;0`Lq8v0}#D+D_aEvyKAnzRKP_wDm1J380pu^z{JcO2p zC)`;+&6B}W6^TEVDAt^^F3%pB0H5K5nF8EVhHp6?Xkn#7-XZ7t_-T$h5>?pmdIa&+@gcdmqNCg$C1kj zMqkr8WV0ncHiE&7fM9wrFj%aX`IXQ2i;d`XXQH+<-pQK0C!2NiBWk`XnMzT>E zq=$sqo`xI}q=N-X7Noc_X`wHJz!8*NtdS5cn#mRBXK%X{f&t8i`a*Pjp>8B~1Gq>Z ze#kb87|O=oTRpe^ssh~3RL@i?m4x3Z5vXEk@@YiR`Q3R0;r#WA@DriZA41vBTa1=% zAWI#fTY#=J+*EQ_|95cNqewvNG-MWLTKZL@w1jx(E>f3+WAedMe5?!=HO-J=^e2vS zQ8`qc)(5>0aH~Du(ngX&7SAfw>Jd8ztY!p(sUjz(s5mu4{0?M6WC6iVD{iKYu=S;~ zTP&y(I9FW?d88TVg09gfoII46o$FB>RAAf~snSow;4YXNQ1B6v_#*^&NVQ&i0g;5p zEzg9s*jd1!7d8rQah>F~;?>&lZ}6zymh%aaNiLq;R%&xAC_9k^Ng#zQn;nBxrVi#l zcQkPyLap#{$LYAY0&H@mVL6Ks4&WmI({L&teH*Wh`LC!)3UX;}wAj zK30kY*7Yu^G_TS3ZZt`+(bLRW@J`-s8P{{V(5Hc#W?#7>0lvt}DFCJTFYg+DZ~3Fd zDuXO-c_6eP@4^W!<0lQ4pF&NWYt!cqWmk90K359TWhkJGjD=#;NfMTY-WZAwxSVKR zgx=UwrAGROW2YOGrEg7R!te3VTt8l!EEMPf%p4w0j{)IP@Szm&Jg=&#LdaWD&-ocY zW-lJRw|3yc(nWN0`K(2gW;$~4K=O=tp{aIqF9Ks<25~viI+~tD4sE@KYV6I(cdb7` zI0lqb@p&YWDw4@ilh3Zgw15*^$iDv4_#BGGmikNQuCzzIXs_;*^U8z^-T1q-wS#+A z^aMEGcJLtMv_1`^#Rq!m*l9%!kE{Rb(DbQJ)wFcSi%q==k}xQ{5rpaRL6G0J*&Y{(uE9Xhy!^;+xo5{LzD=_o^;iC`@0- zr^&>*e5)R*uiiFVO?h?Q@NOeUvF&~t^Ch<&Oe@?iRlE6MQ+<^^l7w9U4!^iYLUnt! zTX0R4$XI3tzMbJ5?TL?oUkTB?GH-YzBI=3-ultegjr2vc2d}yXz>UHJNEE?ok&3c4 z0*~??Xi3gNJltCj^25A?!|{xQNPzJXA9)mj_;L{( zU=#?o2QIqVImO%{;}$wznz>nHrQ%YEsCpdm$V^;IF3x1Um$)e1yF|FE#Z-4YX&TpO z+t`fD-*f0wMtbzx<8#X@NI6l5Y5T`*mk|(Fd z!EE~2dJ238XY&5ay_`@g@icAO|i4^3ctf2SXdc zsyR>(8hRTS8o)vmew#Q;he(`tBLOt@_i}(?W1y9lNfsKx8Ko}+W?3~qVJZ`r_s zoXo=Sv%Vd;=k}hnu76{~4nJ)pBSOY#{_h#15EuWIF$%fypN#PrI{qiR_^%}S-|x8o zmkX}{8$o#?R}@mj3$=fvh(c`mYr*yD)BkkG_5Z_=@TCwE{@P{z@gw>FWf=H>Vu0`; z{2Ra4T<3xRyex09NJ)^l%lMyX8luHMz}g zb?#L*xqmRmk?O5K*IXqO=Ni0P4?K-ICOq4qIYr@IuQc9G4VY@!<7)bXd6nMGi#5$I ztHJDB>`e}Ly;tK|kL|TKx0O0~73s6bf6RP8wfl%r^`Qc07J0WjM$3l@wrh7GEtWLm zzEB*-AoTOwDynhJVoF0L)UWp}&HV*RmPl-m8u<);(D z0Zk`4S(l|)>jp9y!FN=HdA4gpqR7oP4DD&7Y$PQY8;4%2=BJ4?inZ>HUHu{h!Q>vxqyi_Dd?wcB(Y6HoB4 zhC~I)t%>{4^%Ug^@_x$BQuV=QLol}4`MsN(C?~{E`$P7sq(arh(k1C0IomAb>pYVD zE*%q^oPwoo%2Q;tm0cPnROS63D1z`l8Iro^6YsZnc|qB(`w=+!+IX(oFSvk`j&Kv z2bVeT!;`0jbk{DVk(i2uZp{b$Iy?9;b$4yv{^0z(9RxHm={qOsfJm!1fI8wprBUWcKT;-1_@;BFCDkRXmnsTPcFmP|GD+wWW z#fhsBaGXN=vPcfgb9xyP!TnuE+*w!B-eyoVNs61qY2>Buz zN*>Boow(Ldr}U@v!1m0Qw1q5cB4o2ewVi$HjOPY=Reezxti&)_Iiu9RVuY$3J$xwE z8n-s7=Ob^@k}~b9({B0FO>9c-gHl@PkyXjC_MSbxhe&Q0ue9jCP+;`xy{^@5*Yot* zB6IcIjrKvkHGGa8>Hdzrp}ps}9Io=y_|RgESCS25AuERcK}(3Xmqx?p!+OL}_}LuW z2k|$*?<5%_wyqG94hD6&wZCXjG0gLf^440C(0#CeFp0?>jGZQ6Rq62&V@P+%ruNDm z`_3Ly7%2B!)+=&5!V@1PCqcD7S|*w_Ntlz)f7J*iLQDOn%;;QFX?UBf@E$IC*a6N! z!2P3_C2LtliU~G}`LM%g-Cv;ROmEwKOdA)MV)g1*rmFlt(L}$~>Qcl*l<3ZZW!7dX zDJp`9MxAt={WzJR6@NiWXT?^A$ADpPx_Hn7ZOUB9-r;2a<)1@tNmO#%TDHF$TH+WQ?DZgpBdoqL+{{;$cF@ z_}t|uW6bE+Woa@s@|Uq&o!QQo206U~h>&>ZlGWE53-^9`EOlaGmgK%6+afpLdeA|U zc!8UPHe3Zq-?l$@E)*I5?(0-cLT$t~RLDbW;&fvFGn2uu%M|H+>qKWjvbdD$xWh>- zO^Kx_=8*r&)KYFK-pu$o8khhpI7)cO!y03g668n^o-rCN_zEwe7j}s^U7u zLWAtRb=+KG3?6`d(uLSYKoJ~>t`0qYR}wfUy_7ZEtAPvtQbEp(Rx;>iOI7|6%O`6Agtsmk)J!%h@Kojj}5Y7q?=DL2alg_R?%l`m?x zwm36exoQa8*ZS^AK~whm+JKK!*Lq;lPql@|VCw?f89G@N=MqdNq+{re^{fg7 zTvHD*iq4fg!@wEK3~s3E+y9Htu_bvq65y(AQzmnq6PxbHWa(rfcDcmQuPuIGE{#PU~3tseL7=r5JPI9hI{CL^B;)SXe{(I6Q_FAClRuz5U1S+gGj{$6Mu)bI5*4W7<(F zTz6;>+B zonsCQeJQt|rD;)XW+O>ZRp%ZNFJTt`2^(=4F3iG{Z)tc|aQAp&h@;8|loCRXeqra)oPegnkkEp0quWl7-iu~(c$3~bCNzQ9b#B+gY=)Xxp1ph z)Ad-wWREjLd|=*gQfG||H+#LN+I@a_88?$0R`Sqm3?N-G8u=^lC0IsFPk?4K^ zmtuU`_(4!0&qJI)+M~<^ov0nAePld zh|z@89Pqk|cris8DjEACim;u44WXX;ZI{P2cVomN^>?#v^$0nUV+7boNe?PUj|=Y= zz*>(X5yJn;I(x18Ge7%N$caf>3J*1)wB$hY2kO#KWb9dzb_>N2_6fa<4jTY4NhDA`6CmG1V%|pQzMGE~U);W$;8?=OMv#05 zSulh^(~J&c3(z8LsdNH%7X$F4VQ);U4kDz5M-rjHo+uC~p0c)A(IQ{9)aXpE_!Ji+)Y(J!NTrjYYeisn*-=$ zNGbV{7#Sq%iw00lVZtlW-$Dq*S4&gs(ZGvRFb@_HN?2c-lG4d6V3jVJC73#ITdqYY_yjibUjcB4wUS9K;xlvTq6U~U7F4E;=!ua480)_}6+kl-B zAMzw4kueO6Y!alIx2f@cUa}veR@ol`psRPnd&rOu56lU+)XiO>4iamS1O5<_D@*{g zVoHne0u11(QiOA=U^dDuoOup1r_Ak%Nf_bnM~2Eh zjy6?the41jhNc>9607U|uF{}Ep|{MG3aQdnsxm!bFE|Qm7GM?U3Xw-CUyc&zUs&u} zGNCP<^@(9DMbxIta~fjX(v6)D#$4tCU~pzD5*j#(Ef=Rv`8It(<^ ziayv2V^62r`~0J23Td#Kw2G=(qXv5T`J-YDmLDbi(h)^Ly2`XbT^ixz<5OA$Obt*2 zN~~nNU)*8o@)_0Gql{JkxT%{4E3ehN0U(e9*g^?{%Lkj0F+&uX3J3EobdzucK9P+* zR<|+kTIEQhC496j<1Vb&sXopak^HR`QGt%6mV(KsWpw}tOBThTzaYc`H)ezTuI*Z^xD-8F2~^0h)) zkA&^sz6P#m6zj84LiAV7bK-iST3LFxZ*P2Cu&)6^0UvF(I05LAvA2R{PcX2_G%#oh z(BT6|K=4t1`Y2ylvUup<-XEMC@#`z~OS&72afu{lKu`w= zUzRH*Bb>45tH;ii#?2|MWk8;$LMHf^z`QG%dnt$07>5L;8H&)+(~ds@DACYIr?I7g z^noPouHxnym(y}*OiNDefAdi?Z$oF%Sb_a%ld284LfLD6B*%ULIbaBG0wT(ZWOo2N z!#=;&{`zkSsjy^7;8Eb}nd^S4Cs=u~Mq2zV8+(xshBqVZQbZSrVk=N54y=8Su4fVw zQ1PtQShv*^3kSRRq;`J~zVP6pRL$q^rji?vH;B~T@t`Zl6do79SrT*R&W$DQs4w56 z-d?}iK6i6X))kQBHW_oF<(uJg+{^Yi&~jT*54h|PT*8VAirOP-6(#1oRWhtcy6c!k zBVmk#m2d1p0li9Ay^C!ZN}>td#t<~4cfzyBg%2eOdURHL$zcR}D%6VB zH|p858R)a>!e$b{!j(@9onXO6uBY@59))HLkX}EKx^lhd+#bX#U`v2{`noy(=!5qR z?2RLApg_gRgFT*u262N*<%4iPNCqEtc*T_-M-X~RRI>=tGE}H3YpAGjP3 z8PJ&$!Z*Wz58s5R<-hd4|BBsqdsF|Fy!}^;d`E(#k01H3Z^?gc@dPeize`sY3Pz(DWNU|;{w=kJn{(UHMl zo{_P!VWDbdOeh=q`TYGbe401;FSp@;SCIUNhvAEhkNz8}t$)1@|5@DnBe?a~=GMPn zi2wgPyY&;k0bW3rFca3HKnaVcw>GU*O?&iy7S@;1d&uI6NQ~8VfIZoQ1)k0 zYu!-OhrNzHBi-AYG>Ue#5PvPcFU6R&Y%@9szZ`-(mT;Y_zwagjFtXQvGMABEZoTft z=xoHfgb?e-ScpX=e**F8lgXFQ?Y3>ykdwF5d_IRX)U0!67A1TFSyd{O`#Crho1F=G z68-%cu}P^kvd}OnY)1}6J7moOd>i$VX~a#iKw|5^TDuRM6BLs}=ZYN&drF^}y zg^8{t8jULw=+SQKi#E*L(pu+Al_c8u%uIvE4uQ`Oh;-^wfNVqbAo&=XmaM-==u*{l zB{LA(3Z@vP&>3yM}loNfTOJI?xRd5I*zy5%N{|LV3jk_ zr(f<|4QSVl8_b5|I-2`8tRGqKZ&$k8?Aj9}7?pcg4}UdsN%0JQFQPwub32KXe`v<& zVl-uK!t8x=SZB!op3T=>HkCg(bV>TdU^9~FV&IbG7Ny`j|d;^W9Al9MN0n#c? zibd$!#_C_Yn^PL9_XA^Ds;a|E^s2_|UKI0r6}4SuoY*7YZ|A)*t@^nS?W4pwY1B)Bq$J7cxRCzS)x&j#OEah-tI41hx#Av zy=PQYi=wVQ(<_hw(n~@|TIf{_y{Z8$fT*DfiX}9qi3zap%wc$jBI(BboD=@B2PaDe4ZgL+V}o2bXUi zG7_(jG%_alvj8n*zjd^qkH$<)mKm}8W?=HPDR=FmWO&evn&{Jx`B(Qr_72Rel6pzD z@7uOqyfCQvyK9T+b&C-lN5PQP)un{5bB_FLaeYRm4bZezN44Zi6g>QQfMSwCdBT;p zO(IBdqRlX|F`SoM7p>@tw6ve0Jr>Pjp|~iM3f966MV;0M73I>^?!sS%ud`Vn&bE@( zy)PT)*@%Xm*C%UQJ;ZF;`RY!^z)+em90J`-U?H~OJH4Z7QAGJ1PwPkr#|7SM35NXS z6FNAs&_L->I#2u=x5D>14_ZCrD{?VYOD8aUdqGNRlUAs}znY`)2H+q-)zeS7}fGJ}`G*Fd#!2gTH! zdrJ`?gTL&Xs4QjZe>3O8?wvCc57wCv?|m2CVbM=r!NJJS^G@o-`7IHIh=A_{A{sXy z8A?WQy*XsjG%8H9fQ}{9mUf*2hJB z5>b_r81rW;@eYJvS)aV4AL<1hiXpM(pDs5WXg+VirNAI_LN;d8Ry$M+0ls776Vmwi zgF|TjNKL7*Iypj}y^7)X2>N5~IkBesV$|ram{|H24FS;VoN?c3;=L=QKh9H+joas;AbaG+* z8LJ0Hf@_E*&8eRJ^3$x|`}^^gT#+BknHwoPz8>q9B0)rYSm#$(uQxf~fT_;lY?q^M zPkWImzGGEl8H_Y{-Y~Xt-tKKcQDh+%r*vcrDxdMlVaj$y{~F^}@{tD_GI!~S@Oy*G z$jB@wc(LT zZ&7FQi>(Na2_ZQFP#G!&55(l7Pnd(lxIS$CkIli)Up}{?0?qo&b33ojpS}A)@_rOP zs3!>bCJp1?l=9*1p8j<=Ghnc}D5k*&dzn}sLRO&Re01($skjRcu2qCHg||Ze1>TQ# zbsotjZNT(fAV~ptKIL+pM6PF$KbcpnOw0Hq&?D zptCz)oke0;pte?(uTt9lS{ z>rWxtwwsJHO!4;=ElDBsvt)ao@)`?xVMq!STE$TuPp}PY_p0tEY_R6yI6^2{fSuDs zQ__I|0rq7vu1N@$VZ*w4=z1e;Bm?OChyl|Xy&(4iAXP%+p;&@z3uX;gjIE|Zpfu?F zNPHU=g79LN%w!eHWK)O#J!Dj3=AR#(n31Ai3cB8A}0-~_e*GmHftu$X);x`G(TDuGLYIFEUFCN z0zd^4B)SkM36rs+;e2Bh)Kw8z*zoXvbS*(qQ;2=tfV{?s#Sd&o3DHNkWtFWaR?B2V zsTemw;>QXw-IiE}E>>yFuI3-XWHXz6i<$hjqGI3da!r%UjvRjM-skC3)R#JzzJP(Pfo_p`zd;% zN34SO?RdZH9PU-93E=#h&_)tg8bF<5*w}pl7Sd7SY^;pHa!LX<-cP!F(eiO@(M}D- z1==B0aGac5u_PN?TbW&u6?BD~RGJl2(w0_+f#p(43@nojzEsv$GEK7O2lxcofR)%h zb0HFw$qNnRLi?CO*O`j&#nc1Ur1JZ+SX&2+w4mju- zkg$Wajf+bj@3)rOs0Icg_WQ5N9EzjhI|cAFbbKSl{+miNiUIFt!#PLbR{_gvVKp6Q zE8kU_ky@cg!Y#~HW@rI!B&;M2dMb-h@h}VGUR?XKstjF?S&3WwQf%4=>tUwdTukEn znfn3e1rnu^#--a|7+rZ-BT-Vn0Ihr+XCw7$(}Nj{Bf&O)1bR3`-#r>oAz{y)syWje zBE7%%qvZ({=h%5ZENi-aHxuyS#D$Y^T0&?O4`I z$P;QYRMBgDhjV+FNT28u&TbqGa z;Q^(T;sq6S6GJ&q%4RS)U(#X2{a~fm1m&+IC4q@x=@VMc`5 zeFUT7i?9W6B#(kmA0}dRqD)`!nU-cnRAaOn%e~D^0b=WU7HJc znDi5h7p$tkWfQ-4BEARWdw9sJ*!_78kaC_>KOOCs<{%Ca%${iM8*eoeKYW$MJXTqIrjKO{Ex4xn+Og)0LrZkassed2~n^h;YO&to*p^a9Hy@wiZ_k{lq2vIU~OHv zA-t9lKPtr42q6`Vd-Pc6HU+lqktSwcD3=|N^B;}f{h&+I&b4T;(tc;Gr(LY`PNFaB zJc=6Qd{ol4CC2mf1qtFse|gt{#}|)%xJa*Y39`O)^h5OCnyB!aOS;{c4t$F`^zD+7 zbazyZJBAI2@94f6)1C0RJNa8T(|TP3=wYRT!ahPW4UHxAoHFT2Pwb%p@Jl3Ao{qaI z31UcriO>mw9Gokyx7`F%4`4X2-49Z5YP8Fz)4)Y=_~{yVGdj*!c)9rl{E`56VW;~+ zK30`|rC#lF)9WjBlE_O!*uZ1AVvpX@Gj7M3=-exPeEF*uk==K1bl-tTxcnuCFztqWo7#kQoSre7vpyK8*FZB-X9~pMQoye=B+Z=Ts;*=D>eWh5oVLnRgQGc7l*7 zg+&8{Ryo;Q^YgY9vA36%`c_u@g9XmI6T42^5?>Kqw4G%^gn|qkUO>gTCP2F=G?z3dH&DT>90M{t9`wF zeOIquy>{)|b&xu-241C&fB5IB=dYgUmoGo7p5MM@eEi|KE_HF`}wn3ex&-F|5-utC94vvOi;3T?-TCh=adkSTz=V{)}Nw zowR_UH%we;n{JUS(NA~3c=}$8_xYby&#?RL^uZjxFR?$@2Fc}{Zd^L&*M^##I9I>7 z#`gH<@Lgz~qlZMKfBSkbXym5BfY`j0Uf%W_=Yl)!zKsk!+%8AK_g;wo2HY3+`%isR z`Vau?2ujZ*7H^+_Gdz6nobt|Hi9lpan##zgS4s59;Cqqlsy~VJ9UZWG9k5d^KcM$O zYed8oy9f40ni1ksMQ6$m|Hj3b64qQ#KRB#dbInvV>*4+LzgZZX?mqiI*1-Qar=^@X zHPTDopXCH(wyVwBlJQLhYRqMiNz94q+Z!rKB>Dwlc zO*CMM-vaLJ49MNOB$ld9>DsdK%@eY0(DfnXTPS_s=|bCXPkvDP)YuMGuJv4Nu+nSB z+~$qX$Aq<>Q|mIHqjHy0-9!@ks;(mGJrXE*EnC%ok8Z2O4Y(2zU4S+idY^|X*6S=( zBhIAUHj-W@7b?-CvUxJkSBp8dN_TnoR@GFJi!~E@6bOzCm073^&7lNnKFs&OQqz0EPDju+>?LM9b5ScVv+SBgJ;sPSt6yWzs-NtQm-k@&b*AD zQ>i){ZRyT7g3*1%@k1zF`WBc#%#!b+k^3uVQm&dk{FYJwD z@9Xb8GlRku%vTR&%RUIWbFs&_-sr++e~(l6`}B`|*OR(C_LH?53r+R8XI@=s6;-I% zr|bVl?50Mejb2czTs&dzu~Ca-4SLbnXvd)v1)h2E%#r1afU-DMIm~m8&ZRr2On<}# z%{)qED?eXK?7T5kFZbs5byj)Lp5`LPW^&}g?>j}5@Fn?(=(hdQBPg}EE19N@%_)z@ zJCZVQ3@!YKpQ!zqFCI|*#p||CMd%1V&h`QOk=n{la(xKORU22sOL}j2Ct6SO|#K=y(I|sWc1B0573-BMDMBiH6 zMqMk)+i@^R(fi$R`v4T)Y8;WW_|Vr#pOg_JX`r9rQ1`A}T6X*{N95^D=EemG4o)tN zAzgN1mD^dnwXqb|e$2R--f_MrsQewVBwrA>z^d?@G1zB{2K79t1x8t$BLb{>@V)g8 z$ApAKDp@18?+kJK@7hCol|;+C+=d=cy6lhekv*sDolODMp_g9*6X#ecy@3u{ROMjF z*h-GX$n8Vc{3NcfW8J^rGW${Pd!qy9s*H(>Bwh#&2{lgI@II3B6P|&pLcpAQA`;nO~NM;ohz#wd!ya968UjaK!p8NOe=Z7Om!^`_h`6 zYj&^gBcB_f!9qiB!n?yEmSIL)e#CAfH_3`#xg}dYrxn6M^)A{I2jmvP2Uk*)8Uf-) zto?i_MC_cVUH%{~L+!!|HK!~`%>+W0|L?Ik-EKuJJXA-VSYK8K?vJ_<)IJ%!TBkn!ZmhxhM`(p;O>gX9exM<^pR7)czui3FK6$~_HNuzciIm@L`UC*^h^Lv=g7>h84G zJtHeyhr)zlQ;acZ{YmmG7HeWW z(PA-YDpmZw#g>fkghEFuNwkFOX*I-P?d2`!l&Vx7(|6J^e)$)!6ngq>TMo#T%FZ4M z(+PN)v*X%nyQgW}`gRU$``y?trANM^IzJY=M_Ejvmg?Fh6bCGCFwsdeL z*>BGnThxd>D?8abgu1}$+!v>#m`!QjdUP>svIBNB80Xu+lnA8B?h6#bi4E=NB8_r8o@-Edam_JSJ$`W|C(=IA#p8fV+?puIg?w26Fc_I*FQG%p z!pN8`jPqd&qS%Rc`9lYX&uyHu%Zk4tc|82mZK*3`gY@*syil89x%wez|Eq=t(P!rs z8_P$_-%k}r;-8bwSgkm`eRF$Q^ungmYpxw7KbBSeG#))@CtU3M@dk`xJpyA`U0@6= z_`3D86)=Vsad$actWN`C5Z4dxCQjPe%$2mQ4oi2f6o_d&SN-kActl!a$kE!om+H5m3to)6FB2nc^ z?)tp=RCgY|5Wz!5AE*^42y$@|)U5dDFp0O}Gd5q=G6)srC-yEy&Ko|P&8qYhi0xNd zbI&s%@-?4}tO{Lpy;G7P0g~?vT2b9(3My9D0lJH&lnD_n)tAJf~m1rdw6Nl6@G zMS3~mrUZ~|ir5dj$SpqfsSr8bog`V5gbhT$mnM=b3B7EnsMm3cSy>c-d_y%slaa3h ztpGbYaCd=T!PHW6S$NKbXN(b{rbcLv80;+cagQDPSsax`?qTjh9VJSd?P|YL= ziA%*Sl6)r_(qB0^6dy_`7x_*V(O~1g1)`u9MD;9!%1rX77J_DF%2;>G`{(EaRoMvw z{%hm50EBncJ+Y^ZsAveJhP`#|vk*I#seF?!mdlsE$QGI8i!BS0;gw>$w!t#8vU0*L zmiSq@gsjvV=~%YdIwMh+PgxCdQ3s|~QAGSQK|hi$8uSr$P*Oy{LB{cgQV`V;8zmP1 zob)7ZeJ#UziceWQqEs9yx;EL{J0?`DqD@E#j zP1%bKu|(QhKs*srOoMfK7i(4(rw`~;7pjylz0@bs6ZMZOUtZI_VmsV0*^<8Bm{63w~VaZ*(l_KJjp#wDUwkoRejHElvsxPpckDgl3~xIG9oF9gdpn-#iZ%RPv9hi zT$5&_75NB|Fk#j>p?|F2ybZR`(pZnG5h7GEKnFta^L5Y4(F^AFin>l|HMW>?y)cHz zMWC|nXk983h-4?{aYS{guty}Z5CHm|jlRl~sU|=wXktm>2a_Ld>1Hckd*3p?qso-p zrnVP-P^$_vh0kCRGGuWj+6^Ldg=N`=Kitokx>ujdj<2AI6;dGIE0U4h+Lh;UCpwe5 zDE?Y6Me{iJz5A5s5KX%)IxNRK91{pq*2rjL=}p-+Q|&}$>jCI8SBK2QM?8df)1d?g zKI*P?7e@pgbZ|z>wzj?rWw`O9abD{j`aK5|76lMMUlU(GiYC@W!7V|pN z$qnvZWA-kqlzRE}R@C&`8l!xki{2xg*ccHqfaqvIBXW za2%BP-J(EIv*^(>@#U~Y=IKQ7wLmw-I($;A_yrd*bd`Ajj8xjFw<&}3Iy3(K1IdpS z*Jf{s&p(!EYl*K7ll-We(DdM%7&&our~B6{*XzL;)?+s?hE)Z|u#()s7*^efYuMU2 z{1aZ@XC5KI^Jfezpr3EjKN-_6eYSsfYrj#T*e5}MC49h;JfN23W<$a{^ZQNd{gP9F zKLsI59^c6>IUd}UK4@`v(3A`G z5Ja_vgSW(o^gV|#6ykO|0@E=>OCqS$!sG-PGbTZjDKfwX{J$&c*dQ8tSbsjmln2ll z*mE?j8-uXs$wz=y$1@CsD3f3(VfiiyCn({=>u*bB;TIXMKl8N`T*=k z_H8T?-XMVC3@7*AyoI&9QAFh*2;^_0!9=NuHXfEkg)=>GVFN*%XCRQbJhbKem;GvztZ-PAm3LqXLJ0@pp}X4CCzXDg@pI18YwN zgYL(q>`#c(cHKJ=2{Y#s0)E`1E8Y*5P0t10U`k)e}O}s0DVCBAqW|@{@0N4 zSJv$BXXL<&=Rbf&a2oJGUcrwlEpjlD~o_?Uj<)8F(X|2_Mf{}$5y zi~8h$tCxO3#-Dp~sjziDzW-8bGFm7}1R*1hJoZ!3^{3LLKrL9sH}>`POUm6x zt$%h&@tFBy`gC-}HBq8r$@ZZwYsq4JYF|XAdY@=jsb2+ZH;WF`%9Td!Xhfk1wH^1) z+#`%;zX9A0u2yyqLNqQEkLgvZ9ihscw&}#_?cjR-cI?YBa<0K;lOW#|oSBT{`m0X{ zkkIU13%Lfp6vLg#-#&gmc|;6e>|s#NG#Z0w<9l%OqI)j|r7CUh^e{MYF)M&nXshIE z%aron3@%H)qsr)fEp|)s)Rjb*ZnDKV=`eVAZfI+lnoq?e)IX9+J(9ESWG-LmU`k%3 zNAje&qz*u#1q=uWsd81uVA8t96G)A*lyOAl+bCyPn!tV>?tN2yxDKp)0b(s`DV#=j z0XIh;r?xWDEc$L?Aa^b4D`pA#Xgm@thGW;`!papo0Wm)yj(7CZCSXEM+D4e6YXijj zX{#zw`ZaSr`S$B?=PyDlcBkyaUU)oFoQ+f}70<2~@Ax8dAj&(lvNn)Y%)*JrKvB@j z4=i6)6J?eoQ}4;FM4zDrrs%kjeB$)!2m8k ze7!f2nj?LQ%)y+m4re~)3|_B-5Q-daF4gK&N=WtQG7v5h& z;}mn?OZH8N>_rr*2pzSa*L51`3$LMvn?qjW40c*r$jA0Z_CBvUQ$^3g*kl&%44Q8{ z#v=uNGa6qjCWSQwm4~!Eb+5=;Y3^6M9OFGuJk)t&X0LU`XZo`(@*Cp1Wx~7Pyl{1p z{pjF>d%l2dJ3D(Q`gZ(PzpGY*%N)6e1Iq*2NU>-BR}KxH*m+gr3Uydp`f)N-PI&Rt zYHU}>E#E_mO`YinEuu4K5czRX)7bny2fN87n+WuU?>`4$oMg}fM5M+376@^$YZCb8<+_Y(Z7TI3w$Slr()sFsgC;IvR7@Vk=J+vJ+ zpqu1-q^Jmv{!S`p^b9eeN zu(B5m7Q9Fe6J;L8PI#VdQYLDjZgn@)F*95@f3st* z|8#|6S+kt!{+na(i3d%iEAk=`*zya#Mklm4q4|VNn=}~+ag6$9U#ebGQI42nl>4>; zdNO6!(7SyB$6@nGW>aW8{l!Bf)xnCoR?R1G{7ksqn#Q*~S*(ku6B0^V57Yv< z>6m245$!1SZC# z6#_dctibggVTzue!!d^|MhT53k7@nu%+757&2jsDa^!f3){WZ}0rc$htN28t+|AjC z%Z(Mk<_-C&Do^P?I0gIcnLY0_IIUo_bA}eUPKA4JO?XWbev>a^gO9Zvx&YvI*49Uq z!c>FQ0IXqaAR&n7NVK0~H9p#VsH^HRD%QB}2>@-z~WS`(yQ)Z2z580d_f}76NfD=hK60ib&pN zD*ht+_qcUWm$ttwO8pAPp;89Z2nF0UP0v~E+Rjz`SMVY?lM7@~z45`VtAs2gg&?h* zxS^=gn(WK9dOJ?JI{AMHLMXD7<9_gNXAkB^SJ_I~cW&C*rv zuTv3SADX*0UilbI-s{@k@;&K?^RDB3{G+u`9!4OL?nBsPNhc)*s)ebbf1_yqz2bT?2l+Ogz5&q&s<8F@>oKACbO7O<$MnzV%JS z9~!?EZWL}YLTvqDBZ9NQ@3^8m^)j3#)yqe6nMQ;nTlXQ$^D#<6D*&5-3js{UX^4!0 zWVaW|4?h!~nYe30lsNgg(%4p6JLDzaVH^j3mWmN2NqSXm#cbQEQG(nU>+J{MitZuY zO#@Qc7-=Em0v~sRfl(u1o#{lWrDWOJM6rH^f(-Fl7@8ztL4g$MBG`GM4ZZ_*j(}LN zm8v(BZZZ>U@R5KFr(@z2uv|0nE>D$)XlKHwp(uR|Te_ldy^e`_L}L zC5l~5KdhbSrVpS3=HZ=mcvca*lBul1#=6@hFA8Ceb3O_VvAAA>>li|x1n3J<${38Z zATfY@w9+s8I;t9HSbgm?s$7uh%&5GUna%4if(okiz9hCcnqq@#SsmVc9ZXVrruvkF zt{E~L*jUD3u%?6J??mNyoi+XA8+Q_LJ7YsMeH15OLT|5Shg^!R9hX%M%$6OF2cBtb zQcFy2iu2b|u{rcqt3~}NZRlwlLREk{L51uCuxIRC?c0nPBxIA&{$tSLA=T0fR`7OM zh(aTKGiTdYXQZe~5<$C|B!DVbsdv)qYusuid&x3bD3OWo#w2xqNhDISlIxRF2jG*H z<|4CsXIL^_&88(ZbYX^kWgxVRWOhlYEPvGUcc^;Jry5*8{3LB>sfNwnEJZ8uHsWjJ zu3|ImYV{4(ve9#v#rM?p_?XKijMyh{1~rz!!JXy8oB6mpuEkomjFS-S%=LDn;zENl z>AQGQeC^R4NAuZfEJwfK;EPF>g=jB*~L&s!|&^XSN=u6pKgcp z+Vy2KDB9pBM1rbZF=Gd#7EhPoZcXJF+`a>pCWj z?RpacHF>pW0aR76hP~GSDdShD(a_3VfK5NEPeoo3AXB11**df{4sfI6CS7r!y^tC{ zu8o59EeYDdmf6F=UFM;x=!dUdszbY1%4N!cvy1xd`r7yg9cU4ebV-bSN$Fwn`j@zG za$LDAT>Zm#<@NROou|85GH5zN?GBDhM|Li8A4NinNiscDbe<%7mm}_IbBCk9iQ*Zv zq^2IpaQPIEo(x=hg|T_rYh1cVzB|o@%SNWALaM0FwZLY3CPr=vcZq=kiBt$>?^Um~ zNz`%L>alV2Wa)l%9+h~Z=Gg3_c=xws{g30>EiKo+UAejA*noB4^l0D6nJe1$eK%VY z`{ujFZ&@E3$xNCvkL&y#cjw#Hm6&65uRWgcyw)3YZOPi>)s<`Qcdxy@;VzWFzRBeJ zhi{e`_H~djet&#@?acMQnCo{wT!#nnzUlDLwLEMeuQU^8!a?iY0>jVp z2)g|R()}_|2<8-691W(RyO~0Q8BkD12{1f`s1z{p4i3)Lp`y2*3OX z`}aBh8YcUj!vJ(${BH;hvNF933xDR{K!E|6eEXSsYuI!C{9n>;moA0<5*YA!5n$jA z#9Cm&4YU`42{&+z42-wEeEFAz8;FsZ1HXI#zhnTZ{~QkkVbbcV@V^Cc0YQuEpDuJe z763>(g1TkU9gETkn4vTb=VBx}$NHIMqNGtu7Ga4#$jU`J!DZvpoYL|Dvm=i044;po z;YJ}Orco%o0&P4IvYHCdRT4p}pd$kXBkQPnH)ViGH9i^|gtiPyQyr|3z-$L1--QvC z)i(onULO~E(zc#(#_YwwkrYgvD`70iH3yB43fK_7jgum8uzFv_pU*_$a%4HWcwffz z^K_B@;ECM%ctQ_8;9FZoXNgK$?P*eAIp-tRGX<5H?c{#AHL11zvN!z(S~W&yYH9%a z=1S1`r!<)(B!}nj8=H2oaeH}-_oLr7?S6VGTJ657l#PH^{;gkFIl0C6PZ#=Y*?~;u zUq}`MXaTybAdv_;3OtE_A=$qLJ^um5 z{%@1mUjX*M3t|N`lz*%34*fj-qp3=*`wPw*Xq z+@P~rj*WMgAC@o-tQ^4RL|75kb1J2i2Kj>3)LJA3ml96YWh`;{r?N~S9V#;tax z&iicBtLFtkicYQbrfR$R^m?yGnR6DDpgT32kGrlZXF=OGYi)Ar(F%L}9i!pRS#T7* zRkV5N(r6WF$G>E%>fMwykQ#i;PV*^#%_))z3rjM>k}{einV1LZ1L<+`kkwI${4#Mu zt8A6@WEX=z;EGVe8)3B~EZAi2<1wRyzFxJi4cSyrXoh;DNzlL{?8oi%K{9nR2(4YZ zq#+Gg1#X(Ug)ZQ%DwBmuQ46=A+9?#$DNv1M8Yw`sKA@i|SGPoD5vt&`Y8v(=p1tZ} z21)~gViK~YanEUPgn^ZNGZ|Z)E!C0jiUem>U0o_dOZ?r`2vk>%9B@!ofxsfi(*!)} zN-pWyu3RQ(ADQ9E7*O?9pzYkbW^^Z9RrS=PFQW)~-6#|rgNdK3bhZlWhlt>zNN94N zVL38KNUD>}ym^#|Dk2y{)l~|#mm$UhMvW0;1yjF?7lt#=i3Qu~oKxr=HVf~$VfNY| zkz-*f1!X#7#}Mcj<=SF}X%s1VX1L*c)vbIfp|Age)%C}W3(O-1o&??yk_o~D+F?gm z@!B{#kiuhkJl#fMHt3S;f`9aCsD`RF@?4wWZ(cZj7Bz;XE3w=w-z#f0GT({oZ<#xL zMH&oekW>Qp*9_$jAK|9V%fd+`V)Hg34TKx+O(RSZH3ekh0dn2GbH!)(n6jnH=|;7 zDIKX7^knZbBLpUArLJ$#x##}}_Q;H4whLS{SSDjeX|hdEOR92-ryQ!`8I(~Tspj2( zgSVai`6fn*G^|b?dpIV$OPL4N*kd8M#M!9Kyz*9WJ{?fXujK;LCpa`aIbD)qSVbbM zB2^u|osCwiOEw%YeCo65h(ge(&$cP>K?nGDLiTjNdhDL*N?%cGszX|eWHl-E!U0V( zG8Sr^zzsMa#Lx0dvnQx9XP)M@bU)wVA`x!s5WU;1Ty-ndn$57gjl9S)4j@R%dP*YK z2svA7$%M6i;Zz0NyIC^mQAmOeLRpjqwIfVr`8=khTv*x4x?0&DcOivxhDez6Vb?Rk zyJSXPM>1kg<%MtM%-k6pF{}d{z~3k2=UrXS-0UcbOPWx8KjzW|?sPHn6Us0**Aw_o zc2?~;Sz>(b)Y*Kc%`q_d=~ih2=04$*VD59`WFDCN>>JbY8J|Gx zW{OIGPm@=m_SN8XjG|xeNKqMd>={efzkD|IxbEOYYf_Z%`btM7_z251jaq5i`@+F? z)MJ!7!Wbx(Gcb;>$%|>OiCfNx!C4tkKccElikcl-_gMtJGpacqDAH^(zy7JP!~Ei! zHYD_NSEEuDXT$H=g%{4ID!#F3!PSz6RV{dq>pI2~KEF%7eL1{OQw9p%haOT$kU_#j zeh2gwhBez66^1cIvHIj+*XMIsQ#P z@Ml>>%U3OhN9NDKl?W1PGt~BX!e)!fw#_>x?v!6HO?Bn*AT$9LPCK=Jdo~Iuoeq#G zERy8~bxUtw-Z~Q&2}*=WZ1Dv|xr_?evm|H|ZwBABLrdFBdwcHwyNxQQZSIzOJ~glI zw%EsV{qs$?o!P(bJ`!c>UG>VNd*WWZ?eyJa<(qN2J=D&TZThkK>HdymYY>fUf!7Yb z<%6>?#jq+(LB>n$y2##wKu9BK4#yY&pNBgr2S?jZP7EHf9Uh0eswm%YLFEumV}1l2$9Is z-ZBOoU(08FJ2SgcZL^J0Q0=@Zl{=nKm&m*Ev%;+8|-P_L3iZ8OzWc`LJOD zQq9Ad(glEa_@MFA#D*89hd1+IKmGlZnvL3le#nl5fwiwLvPv|2qFgvtS+^q;s$NL- zo^b>f$QR>tP>JwIO2G*#6Of-t_&M-+narg@3+k#f0f{YU;nw z!Baw6B1s&L5vqsn4ndms0a3r7(v4?4@$ubeM^3j|Smq({m0A!HgRA6_V0+AoON%l4 z!tL<(r0v$rvtp?hLrQiX8(p`mNSs(YH2eZ#>~nT*-HUU>N?so(N6I5#=eP;8&UOmw zZ7c??+l1+1owK6n)rM4+#M3(u42Vytv zb7&R==!Md8-`39Pda-`uz^wu3jFFUj_?*NpZkogsq_ghhH|rY}a$(C1%=!xms1wio z{;tm0+~p_8I+BxYz@3k|dFj|%ix1Ek5@v&mK<9{;qtqu```Nojlv{R#%34DBro14+ zF?2uq0Q**7V927UOp?=)TN$FRO~Q4u9@zEuooi?5;>>s0wiCnEmrlNa6C^2$6NRLx z(4eSb#31Ar=lfgFdSU}*=me39P+~11KD-)GO<9{j)w3EzP-wd-3^|9&V2UihWW^b! zB;4~!``BKHUe=CU?ZIe@-M_Wuym4wu&T5IEeqR|UL}2b=g1Nvv1d~X?o}ZFQ}6`=S@jmx-!m;$#p;rdS|6=p;b;iU?IiGilQ_6?If8Mb(0UsYD^|KA=9S>@&7U z@TZ}bB(x?MQou4+2w1(|4o6m9gUz^_zd# zk@oR09;6+WBv_8P?HZAUVhQ7Z5RoMS49$U6vduW8BLhXq6C^mZObX9H)Hi5>h2Rnk zrLrt$ZWJ`TC+pB>J=&3+wr~YaqwU`vAn_!U1QYGYjE^E9$XxUvA+RhC&&H*YtdUcpk4&ZXgy|Zivq3U2i8&*uT=OrH(2R( zk=wn|Be0+&^QLO^!Udbzm#v2t#D{C>xhD;3RCVQksK1nnul(rqRD9n6Gs74 z8pe#i&y<5H7(_)pH2p2r(Amwx*biFFhP`ef95+Xc@gWIW1aIw(uC@ZuBC>z146{=0 zKb|RWD5XosIFXdP7d6Tk5Ik1@c8tUID|9~!1aL6h z1VC=f(F5_oVV>5ZXyop7+2tezZMyL8z7riPjqFw@v(sh<>(}>xI6;$Ya`J|dsF=1W zNFqIt%tb12sarXi-HfpKc?fznt(K5dO*(>NLRb`RAqAn-wNsC;yoHb9kdViL4fWB0 z>dO*xm-R81`fZpp>z2}O`!W&|S`<0;Ioy`ORnFKp+4|#}CUsoS)6Zp6{tak4eAN(J zPDl7v*V3FJaSW+C4$>vWfv}Zso{(*Mxgyo)P&IIRe`;IxMBAB4o73*tnuHW9p?x_U z;p_uBPQiK_N*M`IEEjAoA0^9(dw)+n_NydMHZs*j>RD?nN*J@bBbt9Y^7sb}m+_+x z4`N&%M7nhsnpZ@-bwqFY98EK47-hOYO=zc=w7;6rS$@>+8S5^baQofre6rX1PhTV6 zRi7_SJOBOiI@b!GDBp3eJ?Jqr6@=u=e733MH|Hm^rJ3G6&x-R@rV`P7=h5mgf z^#AdU|GO07|2yyYXT(gh6-fU#i)M}%Y{AH1ct(?3wjxB*<}Zt8ww}G-zanN^U88?4 znx*bv*8aI@W}p3Y(M;Rp&qXtL;?G614*y9|S$gN(?!laUZXcL_U50qjSd7^O?+BaB zaWz>#S+@aoTkb4R3ybYS{m~@r(G&Irw$ZGuo+)sucUU*@%Bg7K_T#jYFJBP{XxxEu z5NwR0{Y_?zOpJ}OgR&slC>Db&eXr_TxZY_!?QBss+B7AT_=l~Yk!hGR+g<_r&tQYK z^ZZY+vG!N8G3U(Mib`)*4QTS|UV}?$)1p<_=7nq2JeS6p0R%K`#R;o@Qb?lyXWSWQjc{Yv$D-q~WA^*ppfCB>VCPR)Z($cZ{a=9FKo z(6>wKzZ+Ij{VUmkMq*?u>7X7C=PWr1YaJ zSM69Oe@`|d)&D{^ILzkizmkpY6TRc*vk+WRn|g(eqs(8(hU_o0QU3sPEuTS!XFRuK z!S>JxQg$clK?hpb(s{&wOd%ceiedS7Mpd=FICETC`T%(O?;3Tt2wGzJvMJZg;y>_4 zARC#pR9PdLx%5}OVH@*#N7wCJjO>DHFY6IM51WSmo)-r>Ge!&= zkAFxK+;%GIi_Z8(JCfELm2`zF3>Wy6-02+iI^)}aQ1v~X@W@~^54r~8jumbMS38kO zpW5FqALdV*JBQ5Adi}A*%|Ik@&(yUDkapaBZHo0bIl6kS^y4>|+01>-{i+^Yld`T! z-n2u*#d2+ohqc&s@=q%x=w|KDt4|HLC*M2PoomoY_YH7l7$JiT-pqmz`x7$|8dfBM z&<{i&Bb6ZXU`?^FxYj)M6%QB0l{MdAK?t?(m@S zxIm&roUf?v3J0qO6>IrApytUjdnnWsjVPXkNUf4`XUR~VEVvLKWU@pwI6Qi}&M%yi zMPu7bg!FiRFfys%{KQ3!o9ls>Gt8l(?cr+!xIHDMBPK1oTqF)y4!&w<#{mGsKD40QOk#_-6d%5BJm3kPeuisH6>CySF%+m{0mc~o?-Fa7T(4nq>bf5UHcke35E9#WYDq^7c z`^s&R>P8iQ3ZX{ts~o^cS4*#}htdN0ylz(WiGV3Smu74~IBEAb>gv@G2;;~qk6mMa zw@AoIgR!{^PBhGW!tLspX82P>kNAwOYFHJH z?dNeZ`ZbBV6%ywc91)DjOtxAf>+jP=fiaB@{RD!m18G4pOd*8ZZ2}j3+Iba*LJ*E* zdE6GizLvn zKHUU|$ANsuED`jX{T@7@ug?grl*rqjmRWGT0)P;8ByI0XaP;f|wxJ1739$+3Mq6?^ zBEU{U%zuqQq^zVN1OP56dkCw-%Pikil>9RCF34BTxNUpoUG->d%Ms1>-c{Ibr-Rm= z-~G|gZ)##YL$#`X!ED#h1lf@Na!0X~P4TC7oflvoy z58U5zl9*Ct?Vp?kdcU;Vu$0IWSE@O=v=%Ek@FI(j?Ako=X^b0XfAgAzkK)Lo7Q=!~ zH;-SIEW|Wv#VKMxSdm{cpqsPtUz_&4%W6-8^CZ zBkyfU5PrEzH*UcH*bMiZ={w}9o65NB>%eWebIYg$h)j0vn^pKwe|&E+hw-`p<<2c+ zLP;1ysIocIZhs+@B9o4vh}*A<#+1Sm4-AGHySR5e{;5-a@8_rAPeLRq5Z+4Cwn)4M zZ~h8gy}!?v0`6GNXee_PivuOps1xt(_dgl45DD|cJyht;E3BM&y^)>0OJjMPhRb?h z`pd$$qIC03HVspmFfQazsMv}?9bH`ZT?0!snkXiFPyua(uD$gt5bS|F%?oe?katAIFZRp(nHPZ{NH}$zT zqJV(ST2Vxk5LSM2J7~~c4&u~`oE#e!4#6I!0AX&d;Jt2wuH?%^L0ny6H z)X6|f{FRoxk(RhXDxs2yHXtUux#j0pEbR zcxWEU+<=A_XYFpi2(M*orj0^Nxv+vP-CZ=ymsoIVin_ZFIYpCmXJItyN}`#Y&`5G- zf=dRJngPB8A5#Hhs3%pxWf~*nCyHhkf6{OorNSt&Q%zP7qe)RpZ0S&ws{Fite2XfX zJgyYDPMByA9z-h*-69Bo-5aS|wvA=`A~A{#Pf?rlo2wcsoH7+|*^(C{d9EDAEt6*} zNcjsZaT))_Rh*zUYjolkR$-9lijUOErzEYzPp(sDWKm@?07zDdf!gjgn+sonTz39m zW13q!1{RV5p7Ky(RZDS!V_X2~l3~A5S{_{8QdL?WE1c4gKPtaVT_96jFEgBMO)`L1 z5%p`i1XQ!8h9@MqOvU{wER$%ZusLqES8mtrt6JU$^B6J2V~pIb#kwMM>1*i{6mlj7 zz!tM$`9y36U{x3@yhT??fgoKWfpmo{GSIzSQ@;A7OO*XJ8!#M z>0}6Az`hJ=Ueo)k2Gx}zgr(u_%lcLMN=BjOWH|3QEhf#Q>9!H>gytzA=dxS*^T0Z3 zgxX@2XR~FajDKU1c?e38x%8=#TjzUjy((ZoNWH8`p8f;b>Q4Bzp z$>=H$RG5Rt8WF(4K^ zGNI*$HAJKWHUOcKg7Ks#mb}&zq@luBF=13u{f`#b_gX!p>P)NEQSBIvx)H>bg>Yb4 zV_8rKEZnrt{)-ieTkIo5LC|vNPtZcayW{<}ib791xm@sWwqMaJA&$nM?u%m@#x&`P zAzvp&q{iP7aX+B#W(zf>sL<65&PeP2&NY>!uBjQ3Da z*!`YE%P|y*UaDblzz`TO>kUn`3%>3UR?~ZQ>&n&Eqz|wM-|TAq0kRe-Xgbw{l8Aq1 zIp`gM%Q-YZ06_~H0VHz>OhC=2l1QS_eysozTZrZl5Ya&YZ&!f-PdWKth-m(!zSjR7 zLh|4KmHa2liSR!vCuc4Hrkq^Q`b#-+%J@?`37-6=oU{r2Qcep0R8Dxel#{!yTNfX{ z4TI77eN{HRl5}4cch$Wia!Hyho>p?Mp`YLGQR#&?s-Pw}_gw5%CwKh|nYowi^uhMG zvBB66(hJwgM{rj1X5LpHeDQal@?DT-v(b4^rjyiVSb6uEkM3Y6-&dCIZ#=S8w=-qe z3d|LKAH~Ln;m^5=<$)FCy*g`*dWdU(cmlLdXcm9jCUW zELOD)+z-S!Wz3;@ zc*uOY=}DKm0I+atxQs?wQS77zjkpLU4SLXhIfA=4T9U7pixx)BHXY8}adisavKg@% zF^bH4&?S!@`nd*f1ih#vSUN40CgV>(mnvsQSDKXNN^#8H-Rh(({v%(%QP9Wg5F9XEp(>Rwkb z7Zzy~Ka+?05h60WJr=Uu>1CzIe{7S3;uVnyODX@k zdQjY~JL5_=S5X)dg?H`hE0Lx}T)DE{&mh2Mr89QE-9Ep!y=un;HAdY}MPp@ue9(gQ z^@4}WplHH46R;(klxk0NZghB#}mF$Y!OY%^WNDv4!=ATdIRX0SYGsbNr1;U z>`aKO?epWHUFo1_;wAbZ$*^w)VJ8r5JRa$;1bQZ8CJZX%=8tHmt{G8~PXY8y2v{=c znQUn$2B2mFT!U;T5dQQ`xSVY0cwV5I9oF&33aC6aY(I8~uD!7%esIbA)UnA)twR~= zhj5Le+o0eyt|%|itWg}%KdC>+S8z0`QF3vi$arZ^J#@HHx~TZt&TrZ($v+widHmPR z#a&fW&CbdlD4w!5nO7(P5sfAP^d4^=`Lf}&N~*=+#YY{r%pYe}^yjBts*Ce81+OF4 zxFmA_C}emj=Zu29ND|_AZL1^)pBH}Dy$rS)H0RS_xf65DKIXx~(WyJab*^VqwpZjw z%sx)~Dt_0e$v|uoXYjE97gV|U;hs+DykoZ6hvUxUiux{%)q6sfJo<~C9t(1VP9M2u zW3;K0ny~Qnbdbw6lLcRA5hIseTPlsNWKs0t+4Jn${d2Kzj{}!fVG=aFefg;_AcaFBoCLpw@%<gq`&v>#x%>#I|s>dt){WInYpzEMQB|sb1?zUQ`BXTTg_(}pp z@?(^ws>TYont-R#X*tRo0LqKypx}vz>>?k@oP(8?FelV3W$j(^BL&YE+o(GD+Pl^A zi*~T>)!gda!jcYnBX$Y3X-?;Q_P)<^)eql$E7X@ygizjB4xEq7dA|Qo<%BWkE`~e$ zcwP7(l@p&*a8$jeoTS~%Difa~fyxPIVaHu-jhJ<&_3ybNQEMlFT?^OCikIZ4-9^+{b7y5%(0{_wZ+ zXHB>GiCeHfZ6L>uF;rBxuSNHAuUVmKw zP59&QpmOqI#N_1nH)m=rQU0Ac77cZ0LXx9AJcC1)t&lBFli`p5RVax+aO%P=SVC*lRyb9N7x3Rfsx)qOWr5am-O{YSs- zR&?IHc=%Zg7@hyr^1f`j>$z4%@VNiwJ(ahU3JgSYyl`M?UgfXSJY~H$+e!mY!*WV? zMKq>cX+X$>K*OVtlNYfC{n zTol&KT;Qyzm5c=yEx~X*CJHeTz#ZmeUaW`lnpN#{!&gdCBV<}JE)u_Y|dn3GyhUfw0|il&t9d5=rknUbDQ|YIz@l0 z+7Q9|ao<$E#vQ%lX%bn1cqgd~5lK+K7n0vR72zBSTE_f}Q zn7D;RtVZXwK_p^85C)ORbr6XVwLm1Y1FyUi4Meg$J-o8oo3ngD972J$-A@ix0owOv zEiV*=O`-g3^SQy|CCC^2P%}xuZkbBga^AB{h%9cdX+CgwYIml#wNO-6_)o}Dct7?4sFP4tf}@ZJCg~vLIIEZw zjHwNqHWVMEyFIJ-kE*YmcbEyS7d0v0A{-<&m=&UW1re)cC{3f;Phn(RFd>HGsiZB7 zL=3b@Ry6mnVCq4Ogl1+H1Ay|7*0lCTSlo~OrBXn&$)c|B!Z~j>X70*4pSUwSOU|V2 zlRZN`^YCTq<|1>}7yik7OCmAzu0dKr;wZvrn8@AFEYMaInJ}FdS@GJA$Ya%n7XQZP zq}R<&Et>Iz&F47H$}KSE2|+(uSPCib92=9&faHGJcQQM{bzijZ7sJ~dCs3pl&XDMi zgox%a2ZPo&$NOziay!dA!s?efVs04S)Yfj;yvNzF!`Hpz>XP+Tbw^)!$84MR*tZTT zk@k6$$UEMhO0k^_uWdnjWNxrC{Z8jfn0*}w^VDR|^Mv!orRQJ2wgZ)u+>htEVRr9L zE~L3%`25;-qxwR^r3*j8Y&XAMh!kW&-`Kzpu)?BQ=x`hC5X3_~SjLbESEs3VK2D_bh|I^II+|AC?)6shWUfY8n|1`O=_dVbg z66hQjvNtSr?~x-e$BzG(M>yUoS%2O7h5)l*>;~vR|AWeR*;W zyyTRYmX?v8o|%!6m6es9l||3a&Y{!k{{!1e*=_8tt9+Bc{V!xK{|n|lAm=##YlQcY zDvB7i(r(UtB2QE|?7>vmzf5b)bA_D#tQ}Q=N%aTx$cD)(qo_ZV>V^7{2S4^~sVH_O zHaHV^a9Trdv{#&tZreJp=hyk`xE{qna+^ahm{jMz)^PB8?{E8hjy;{JOBB?4I289` zup@za;jw0SrMZKRYP#o@6yG}up0Dnxrlz0@**(oWMm@?thCNDRs6M$ITDYG-6>#X- zFW+q=>=jz(o7R5Z@qE%sWY9u6;#j?qy#L#WBgQ;mUJdlJr}Oh46H^@+Vb2YT68Sq# z`$(_)%WA#~{qTGH+r#Y5yleZD2;zr!>{3q<^4)vh{(5zCF%>W;1l&qF@imE-+Yx+g z#bR=s_K)s8I_-pB)Q);Re@Drz3B$w5@p#xa-7}(|D@t=xtVyj~3jSzliW731-u22k z@8mDf`wpMrckG_k?U*VS+xCaPzg<9LE!rtfwRV11ca*ul7*LB(zm6sjzsKoT*m0ib zvCE@0lh1a|MYu4lnvg)EeAwiy^^(T9p}jQnLIL;t@n_$~70wuVCRQG~yhS$HTrlwg+kO zR?hC|gNSd}%TSkEH7;hKceTLw<-5M<7b#OF*BsFr$ktMj#E+`y5|}gc(0a=CGRC^L z12VaXlP94K_y=5A>`o+!jQbeZ44>NYxTd87&%e42wf4PQxlHu_zBa>4SE+|p9#F0x zkzYu&J&a~Ws;euK9k~~R-o8&GiOyQkua|b@bm7+R^g|0Pt=j7}%al1xBbu8RJVkyw^Vy=IJ3QU)NMI2fz+lcd16%X%x zt>$xPO@DLo{x`*gm~+GrSlo`NSj!q%gk!{s#hGn4=FQs=_V$)aCI2?yVA2_HL9Ssr z$o71id~j*aX5aKs2Cc5<)+KHtfE@rH^ey%?J3k-!-QYom#DG=O*KHZB=5MW%eOcd% zO6ytI?;GB!?IOt~L3xn%GTin%x{s-50T(e7rr1meE$ZP6!Gi-mL6p-=FWw5cOZpun zd_adb)ma?AH;=KviJ|-RMYmmHWH3!!^7u+S6M~<*FWtLmunT9ot#W2kfuqb&!^`vW z8wsuJ2={tCAf5c7&KHO$<@zkFK(g$v-cxfyzzI);C0ZVdW~?`OGt#_rHW1sWo&(R{ zHVACyMlRyMxaqnE(6A%K!{sV*{j)v4=Olg27YUlfg^0rVv!?G=zZGr_@R<_4V>v4v zxt<@?4@JVOrJ&)>?B$*tyRM4PD(J+qjo>iSaKFVv*Sn*z==D6&mO_l@Tz;TqgHZFE z2}WdB=G4ZVpdo1zLRrbRHgGl0|9}@ePRiFadoNb30IQ5#M=N@3B|G^%7FZ0f@ECkw zW*-WwD0>`EPk~AG+2@CDp1pqPTeo80k8{NAoFWRSqD(!L+yN%lgVY2Rog$j_YC>m2 zz1=GA4mTMDSKT<8Adq|gN0U+b;*E&vi;mCDnoTlGZk`y_-SJyev)Ob0Tk%U#nx8g@ zn|DnY-%9>=QQgJL5kp#?3=FY=dRFJ0FexXj8C^YD{A#ro-za+3Ot1l}T61*s)%7o# z$FNsatP|X+IU)+~N<2rMEL!AU@^wDyx49MC2*lP@ZigFP?%M8b>|Oo^V+B3Wgy#f& z8>&2eCmrn@TApQl`;}L4)BV<0Rw(G{+OTU>v*$0XIxg0UahpkpvA4JLk?VN^TXt+h z7_`?>=m3(xh+5^@5@xA+?sQ)IImuDTdjb#6y+P=Vih{hIf`RESf3eicS@|gyoDLx? zT-CcMncFDy@vDoCUiaKKs{2)`{XzD-O-Kw-l6j^Wy2mNOwUl(=++tuT{J_|zb;X!k zQ-?SrY`^7fJ*5aro$K6Qp1abtDcE8z6own&q`^Nj({M&S-t!jugm1YJFTDS5QJ!U; z4K9r&>c3k(5~ls03uvSLbVisKNOISuosm?JxOssyS;Fo6@4ogNSn}AQDQZYctMBjk z^1OIjs58|1X1907QQCkt?s%$$Pp=~OSYL~F&fP40pUUX4BvL!?)0@NogsW2Df_GLa zcGf3%S?uVbAizyFUY5?__FQwJ!e_w2--KZF>jm4+d4H|$YA4y+(WF1yc{ zW?zE*mP5!61Yh3eC|gTbCe9Dss*4YCDjijijs+e;E}Rfduah8qX8BI7CTe#ssgk?0 z)Hr_p(QlsCh*g_9@>tDjsXtOHY+epmJr#db*ZE`9zL7UhPK4zHKD<}@4)c7UEp-(9 z)Dru|CHEmjtbDCRP{(&(;}km;KYIrbQDK`4k+)|u*k|6a7!#cD9A&c*Sg@VqduR2S zfB16l1{L?dx-U_#{a%9ycC5bPRECmOJ=T_OAu>kJ((|d=wi(f4>Hi&1Cn!JB$fsLT zvzKz7-rGa^sWYi2{*(`W!cg+Los3iYdOixBPKH#F43jT&>^M&B%5(m z80AM_w8~Q6|NPx=&U6KJ^1!G7c-qdu?1{*k*btq~b)9`?&y=t5Uvb-fkMA;jVR!|9 zBWm*la@}mr_9SesWb;GQ?w>E*E_}Ow@$#pVlRMwa>_|Mty#^peX)bW~_eZ=;09iS* zKkPBzCZ41^^83&A%nKgZWHNU{NGb0T_~aMand(2xN#f8=SV7Plbo>GVYWvi#YOpy zRPg#Cv=kS_BhiXV&0%IvB3_=Ml5@Emr<}$U<|%f519y?6?eN^$DGruP zHEE~nz+)9th~}Oo+<6L2mC562gek}bBA7gUy0|V1OoD_MWnm_uJca^(olJx`5qF-7 z=BHvugT$U^W0n1QTBr~x3n~oV)=P!xiDITvr{1g;4vSzo>^&C%BxX(CkaB9cx$yl$ z;cXP3tu-;(9WzBnJ+#L8P=PK#nAi%Y6AuAfLsysxGzmGeB6)l)<%|nvnvHy8EsDpZ z|ClV2#uT3&-^3~GF9P>BcXjnxeir(R(z0kL?KMmy7uv_Nq>y-2NEWDls43cxN*4rs zWeOnjg1SaS)LP&yPQXqx=qah`!~4>|8=Zm@usc-nE#y>>`iYQzDeGRSnTDA+kFX0NT7HW?S0 zjyp)iqwXl}k5bDJ=1D+=hFLMg=g<5xAaicfFyQH#T~KC6tMhXg{hSu`>!{43otUD#zJGS;Z#HVS!x3Q^}2nwa3BtZe9d zqah>vS?KBHg^H7LXXB?DzbzzUL!~?jxZ~uVR(7g>x)R-=Q)kix^p>O#1TeRqfT44D z9eyn`nPYUR|aI*|~5WWGd*Pr`%6~H+24)net$e!8#Dc>XpD_&y<{>L)Pz|wWPd9juckmp+dWpA z%J1&vv=O4i1UCI8Qz$q(1uFOf(BJ~e+Q=zF-c?eu5(j&W#gk0bR|`U<6rrZsxIz|I zVB)9*g~#Yco_pPfCIE#DTew{Tf9GEN9?!4WcY@puBxR zdvxIR@&NO@b8|b;>M$tpJJ`HD_~z1J_nSe2?;sBVw_pQ3iOzNu$eHk=3(Es$078~M z_&xw`!vR{I9!%Vx{L*&hD?}ZlCvZS zF6c9=iW{2ussBMO=6^C*`dHf)`|&kGx0(OTI>)r13%7(iqu;Lvg%ofug-Ojr`K;hhH4vd+D!>7!RXQ zD0V#?DdoL0GW0n*4AggHaXbeMg3rJX@U-eR=!;oxZ?lVZdo6exg~A`flwofdOlb+9 zvlp74Oy#d!%a;@UMk=#UaNbPzhBj@iKXWr_)ycFS`}B5s($!uci%Sza<dJ*B32BOcL{7Sr^`)i?KOv=o zfD-YJ7@An|FdGnVB8t)_al;&|@E$3`s9cH{mn~X1%sF_FK_|4zov>p<6@4TDi4=`N zw7){#jbJ{3mJHY}8O=t26qfP;Hbwc_5u_Sak!No#J@>|Or zh#__UNf^5!Ri?O<%I(1W%C^6iYB@Ue5|S5adZerUAeS!aD@&#~WCqaiN4u)*r_sLF zY`RE|O&a3p_pImEna!8)%bgOrqt89T5ude#`}_tziz6p&ZDi3uDk~&T2%VKt+Z)}t z(A(Z6T6Im`%5{w9(b0SKXlp|_BPC^RnlPA=F+4W4_iTPnmy8S}Ri=qx0X_S5Gej%w zl*j6m8*PrY)Em38ZLBtWU{52mG->W($hm|h?+d`K6Eu+mmFuYrH@A;O(ovUwybl`* zlMvqIFOD<4G|EdZxCQFO3lV`Ls{;lkw!DdfK$O`_k$ZLhZX<-$SAd1)*%!p^A}cyuz6l z73p7xLj6b<=CM1E$gLm>Kl;8Bt$b!QvPM}Rect<= zXYIHQe?MiHi^us)Ixh++}H6quNLGq0nB0cmG zHhZ2lrg4N+I5XA_cXDjz*XD?w!A@i$tDmH%%>%N|E?5K6v2EK~mRSle>G#Z5csEXEDY&etOj&$}GqM~5?D4Hr zHn|x^h4}%t1l4K#m@G#5>@#`o)@f(w;*!(2=ZeO1Gp>geXkt^AFbC~y)JHAlq+Caz zcL?2UxH(N~5s&8KuX=9PR(Zam5TzgN9lUH`_;AM_4HhY~vmwq(`oljK!u?9b+`4Y(o2K2s$fehbKSU$eHeGlVONHuQ zxhos{qBGT0R#Rn*r>t(|-6zC&T5ogq<>RBK%W_GUx&&IVf}XExFIhy-kt(nYU751+ zROUJHn(+Z$e45|{0p&upShjb`+`8u{_RdmP+$G0Z!R-%y4Su7#qMZ?UL~chEoUXI8 zLis&M>9yuK9m}0BZHqo9?Ujb)Wu~DN05zAQHd7y32%LWo19=G9J<$b)^>Q(guP97i zB@`CN&K&d_L-3;~Cfw;RyS1nHw91v%!;*)S_& z#%+IsM@P`7QYqgq)5kiZ3Oe|XqdV&lJi+o@WN^}*tyUYpb#Fcx9lQv20N<~sqccRb zm~`QRRDaY8O|glsOEEu6;H0)9A@on_L}9FWxkBxM}|$Uw8NG2#G$pU9P*KRfA{6^E5g2r9sQ%& z^eKqE5p8Zo(dgo-zqhIH;X8isv#c<&FXJ;*Btx>Z zj3(nNB9;?;o(&4L&SYB8RJUut?DsiDPvwc;?Xn20p~oa)5!lp=_QlJqD9h4vHh{Cp~1Jpe(TDm(Apej`I&aOC^P z;~{s44t?&uKe82x`KWs)Y`*XEXZ?Z6QijY|_v9dmrpct(t98^W|8GwOUW$RyoEAd&2YSOaX>VX36p22iOqAejP9%! z6!v$@AS3A3K7Fgj8%LB(<{rA#`dRig`F%;7uK@O;6=9F2Qw7YxGIX4xzUxey_Y+}W(Aar&tsu(ml=L5QQlPQ(-Uu{yj)KO{ZP3`M=z9K}-d6lt5Q3%v z2qJsbcUKJzPf|1Z!x&WWi&$`A4Qlj_gAuzR%di$|uFT-StXje%Prk6;&E`{rywxJo zbbTYnlHO1Oq!H?kq>Ar*jegO_lc?$-6s8*6^t}u zqqAzz?xJI`2>f1r$maEUNZjRjQ2fqU2+kK6rT%E=`uHV9`#D#y(oD49#QfkmO&c98 z7($`NszK=PEE{c5nr&rZOc=@UVqHGPp8VQ(^5;H;S{e5JwZL;snUB^4UX_!_Y>-A) zXzr5`%{0Um3|2=)t8pMDti&ULE1Cr_S<$RzqSZ(cEhZ+KqMEaUKykdYNH9G%CL53B z1<=M9HPOCNsZ5zucr=~>$S|ebxM(~XU-u>6;C_akXr@6k^4lDS&Vn^RdRlp9J!(eoM`uD=C=*Wn%|57`O6Il7 z{6`|_{VE0E-Q#-0gqv&elcJe7y;76~=tA!gh3*3;2`pt<_6DzyVgigE?SUk zJoOa8)+S7N_+NjXXQu?Oq#oUzj~QLd$8*sKrt+OaeCvn_H@q_I*qgq2 zRa&u{@pvy=iE|wMw>f!addkC96qOMW;{rWR@vY}dvaac$Da#V_Q_E$VizykB_ZZqw zm)?RnpDfXF_^R86$BG#p4WM8vxUeDyHiv5dzB>`cfK@YL#pSRwOrun4=>fN$0=mUC zSdlUnvy+{S@r|Ve5E)XIwNd`;R7UP%r8=o3FTNzN6_%}<|NINIh5>&#nh-1(+P-dJ z$26c{EDXP0cqHp6iCl(tt6okk8Z|d;5OZqfuclrUzG#cl!8;)q;b=B2A{bFYfT}Uk zrvaEK0VASZAgRgpu;0)M7Fv zYH2a!RcjrTQl%f76&wdpxS0K1Fd83gr>Y-KRtXzN?(4~}p&-t}3NMXTw?df-D%S8Y zgP*OcLm$iF@0s;{Brh(;j~ILc2K`o%UQbLtvvNW+2;EA;?4xP-AYjWnh(-!Fn1r74 z_eYT+r&vXu^g0`ra?Q{vWz}->LZity$jh(jU4t3Wm85U0bunLb&jJN@Z6yl}1<$hq zd!kG$3**hHXYL^gb5Mci*yAgfngOIgJeW$lW$mG5C*_ZwAvXoxK6hBWsZ7+2+J>bJ z3i|qj3VN)+Dq3PC_P4wI(b>^YO}`uu`;g}m3)sIDH0WyqvTgG$i=$pf*(pauY@_c- z13I=*dwipenxhmH>^qmL zZD$kkH@|hR%yd3^-T6$-b~OzDwEFz(u=8(o&%ax;dE<@$&E&#I?1j&27dFCdJ`F;@ zEM3@ay8v9j0F|)0U)_nSvC(BfL1`!O;{^y$*Fkq=AeSY`WAhj4Kmclh1_neO8~+LF z0H;nM?f55G$9q5dpJ>N_8Fl=(bB^mBQiF=gQ7bQNR0FGp?)L!RPPzD*4)A%ty>u6 zMZR2VpZz(r=3T0g@rU0baM$RDT-DfU36d)3`Xkf-CGRef28 z@OHS^t7U5Fh+|*ZL2Z)x+brlc#aJLGTb0hBEgx&g7BJ2+JzN6rx7``m+3eCh z%snt0+)jOdW8SqbZHtb4fLvNMFd91=t^WQ=eEb}JP}1&+)?;U0E&aP!P7EEtS^Pty zU*x_^3hP(}znt;&D-k5jyo|0N7b5(ih<;2;xov1 z7RvrfFFg-+EYCvN%DckEvUrQ_ATTyD*T9FlO$+={A-JRtb14D_v}*Ycpk12+MWBYn zpS&h15BqS;2v5i1-jtjdkas9_yrhSy|F8t{iD26XT-;=Qq+YSfa!OsJNY+ zP_t3S@=|ui7bne>)vpsi_gEt@!Ivo2UV9ZOlulMFK({DmXX%mXZl!+PvgkfihZHd4 zvultPJ8ObFDb12KSJ0z|JCt9E6^gb%OXfb6Ok7deQ#+dZHC^`OpyBwK{>vi9?9_g_ zJ=69k0mbFBXBD?l$F42Z5$H`)opDXeENyaBTuq7;@)(TNE=)S7Au&C(f5}y?W%!)d ziQ?-Azv(EAfYOc&|E#Zgl)|K0la7AzY@o?Sxf@AMdItrrhk6Uh&w3zX5f8@&ln+hQSQP5M2r zoXl7gIB6n+cP)3{yHpRvMTU`Z-vi97J+6vLF9Uhh>0Rz5VP58Fs`cZlCEwFM41S@P1tZ)X; zy^7FP{nY)iG~Eo4+jpdBZE15ZD`4_cXXnx54qXoj6}CfSJTv^vh{v1dQ=&F|Rd?Dv z5J{UgmahW%8s()?8rLj^LXP?ETAAP%bh>=H2!c-~r16M%WnDNS7IrvBszKc>$hT8V zyE9ShT$Y0AQtI27fn_P@@NcFg@Ex+_>W@0c+sy#^_whO!GrN|dndJUeM|Ju*>1*%Q z*SojK;GFdrld zcFj#x4vfY`6X1$=<~)Anbjkd?D9e8GvF{R@`D9t1lT!VZnsYUGx@5QSj>wAC`EKu0 z`{eU>_BDk;CsYlmAG^6xnY&v$-zFx6N6e)D_Ts#9%CV77HIq4b?EO2lN4xN%PTE%G zMRQ>M`l-}o6+4rq^*wR$j-sz?wxltaLN&s(l9riZ${+g|?#?#ft1I~ge&@1_KfdXV zYkl@F9(g-%X1a8;^>fef-M<~XqWIwHuImH0f0o5u8B+UjKGyC1$nOn5OjmZQZj6R^ zbF&@?+|RbSEGEVTU|zX^(%!E#JX^Shrq6ew=<3T%z_n|5&3w1h&ESa-aTwB?_2Kxt z71B?t%xU;14Y_WPau|Y?>TB50G2?->^1^4?)ai728nO~V1ej` z@ptrMOePN*@|Ao6Nl6N?3vD|DCBw!)ZKm;>ix+f3t5@LIIcOzO^>C4-W%u5pRcJl| zYRABsGSvf^&~OrRJQ7h%!{$+y&@_ZIR9+j<_618b7SL3JNE04~B0~Af(Jd6TG=TaF zfv=13;ivE@G@nC19=As073FJN751VU-S<8ItpB2)Qb!08egR~C}=8C&v`QMBGVTs%+w!FQCHKgKY*45$zXT1`Z;iqUe7hqH(<-M;~u)AxiCUh$~} zSrEwZfk1{p0D;VaUA$8ZwB$amcOwtVK=RLH=nUAS`}*h>XwO1ke@MLhRAT#6C+Hny z{R$k%Le!CA=pvE;1(n53iE}|#5@Bc#tParF@0X-Ik8C8vC#*%btrT2JD||gw_$~x{ z^}fdwyTTV<$Ft}nF~4ApOYs(rL9Atr`O+nS;l* zl%V-auGy755J|n)@9TU!aGSxd&R+Zu62@Iul&=JTFI8$uXTJwVsg~(R5Y|4nUbdJ9 zA*YwCG0G0Kib$aok47*+EMxZz%Lsy4hAI~FF-6dF3(E-I(FBv}+ez~L<+@@`#mO|h zISpfi&><@2?KY0**-ZxJ zEW`f&iYigY_vTT@IVL9>p{F_e^%Ut(<@kMadd~8snR>JcVV~3Z&eOWJs;;&1+7%sE zy8<>Dk!m2ES@{KLNKpBvG&LHmngOF^f+yMaQn@vepJbBAy5+{Y_agw1&zLbp?zG^I zX=pw!q@Rv|)>nGgI3KoD-8Nl~D=J+!jQ=Avf3J!t@~)^B3u8x86*W?||6q8k$pl3( zm1~0Lw$_%8)$U%YjZ|zn=F)Jt*w~g>b}Y3{zerGvgwduY#7@J}EZDtYphgu0HC(VH zg(=u0`X{JS4c=u&!(Uz$F1=U0W3j|gtR9L-@8wk6PnYbNK8H1m52Z3|7SE|sN(L7& z2iuyU%yWA&Wussn=@+Vb6P$eJvtblh_0DbKaDw-ne$1vP`;hXtNY!t(8Uiif?OO6O z@X4>i60nHgAph-1;ufx!3t#(nO#Kp$5-FoV=28kC>R9rO4i#1!m z*roupFujB!U$S1dCw;favMxEO!)y**+B4)xZpSweurBJ3ZZ&w1#GWgqJ?O3;pCN}s z?NBfw)2G)P7~Xp^qt~;&w|AoV*!Nxx3m+=c*9Py4`EDOq(-+GFIZ45~b9;_n?`tgu zQh55ogJiL$eeS0Hx$5?b-}~zY2a1>XlpYwUiXNz37XJ&^zya_V^8e+=YBx#W+{*M% zV)Ku8U@66^Lsh{>{cp-UJTN zQvZ2lH7{?Sr})=Jc@Wpcn5ly2i)ywuR$|3UTe;sYT+?`~!LDQr*F2wV0ykE5CZANd zHdbK;pg&ha?p4R->^2!GK zdpSoR9UV5+&Z`n+#?j`c$KHY`j%a_4T;jwp?P6T5$a{hko=js0-#-_0%VN{oh(=;` zhl~z0do9$x^$vP&XQS_h?r(p9+Sm~PgsZBxT*`+p}T~MZwYuhtPcs5I*;LN4@i$$Z3e~r7yFx8`_?}>L?~-Mt2TZf{e+H_siFk z8^$~6-3;JQ&6~fZ^H?l)ebm3u^gQ3J+=SJ|>$o|OP*>{()vx>W%-*l!1s6kL^A|NW3$-HsS zb=IA0?S0SPcZ|F5Js-}05C$+9e0a(8B=4{CJ%WMlw-?f5BT|Vtru*Ux0Pk>X!b|t6 zK_AI-zhdk%D0@5D0CHI)q+0fh3Xwr@NrrJ4bi_LsYzk+ScAMrplqoi2YJK>|1g3tV zG!B}yi1f%e+iX8x(@Y`hDqMGvX|{3*zWp}&TiC=DoDqkf5{0+2Pq;|Yf?@G-kMn6D zxu5$m)X;M2+iu24D^&h)3O}Md{2&z+uZ^}J~P?T>dK#>j$rvi#Q7)~P<% z6EmwIi{I{+_WXGDc~s$#RG0nm@!yL#Rx}jCbx(v7otZo$QqUQ4%I;g6AdO4Q7v;!T zM$_I^)L)dF&XDRm^~(R#MJ?T;`4)5W@}T0$$Q1XJWJOP7@^jQ6p8dU451ATTTwuSm zvJ=PI0jut_)&0peD$TC*rqnNP;NVH$3?sO1J6I2rCYO{1FX;t?+4C})X=$jDbqYMt z73m}+%46GpEiig7UCWgzL2$EFMv0Je0sc`TY;}Im&;i{iKx68POjV(Xq=y-N)E99| zO~A9A3J7vNHXjL-J5N!XNGAm!S@K^?r3{vsW*PZLJWK^Zj@R=ei z*Q}4B#Q7iuJT}p=j|L7GC1vuE zhhASKz9wTcVandpUevn$;<&qs+3^{=$EspRK4fC*_I7*8fXCE{B$MfOR7dIf!K5PH zV|7EWqiilYgHBc=dDW6Q*bc8+D5dZXI|RgD*I6YXLrO3C6D;YgLj!Ta>K(3txWX+^#nbh4Xhf}UF;$b;(+K6NtW>H$q&UrcsB-MUbZ+cnb|o6nXV zV0-cxo`Yj&chDPGaw&acB38+6l&(~%?KWoFjvMT-v}(|k@Usd`ET~x5PrVQz0**1( zfyFY&xvqGlPd^~ZWzCD=&DT%G`H(P_`6|@iL7r%z0UlUY=>-Q7NE785$J?GpmF5kH zD!wDTM$4imyXzn9+RE*k0~y*q<-gbu@^Qb^>QQppo3xyXKHK@u>55;RU2+7&d68fp zaM0e=1xTM+-zyxOv!(A%S1o#ZkZhv1tgBX&Z2H9g_PKK+?2!^|?M)2r8 z_hRpu&NS*l>F_&C^c{m*U9N!MM0wdkd(35Hl~S?bjO=CP!peK&db5|HlcRSay)5C@ zXU~1uar1e(m-8_1$YmgL@##v~v#Et3h3;J{%Dj*U*e;D|P01zhSDvui9KQNx?eSer z9#L@M!tiU^(q3ga5jbCUC!(D%-R}4v+Ee~L)#y!s_oqS9fWkuSNvy43op1c)#qm*?xDzLLlcMuq| zjfLoa9d8Sz_D+dri9n=<_zk}mrz&pFZD)VCk5{Y^f#rlfkPB?fyimx_f7E7Rs9bH8 zEqbDoe-CV=xiG&LQJ8P}@ykZU`?WH)*>7gVmQPo2B)`{Yy<<#LIH;c0e{6W>`%GTT zLEZ8E4a?kUGqS?xCNb8{v$N4>T3bH1gd}gC?>V_Ntnj(5;N^zbZ+#1oTEc5O?taj{ z9AmiSMgpU#(qQxefFuCDR}6M|ZZm<3^S=)MaFHY3A9n%cB0<=rT|v*2Baf-ae6^B} z1v#vn?JN}bGaUedG={r~d0+tl@SS89Ad|6|hHwQ~E-l2GXh%`z&|2nq1^WgACK@J7Q7)W~u>pGYel^Vnm&9G43S34@s8%yDC&BL$ z&o<4eK5RuW(!d82Omn+P87kuU1lVB}+w-wFwv{;Ad-R)QY!Q2mSxJKR6yk4Owli2; zD;3c$wCB4*(`I8q`QseQSV@sz-7qGBwdXOjIy~DDzP-{j0WwOb2pWDOrOOQGgd2MKie) zV@&E|F7uF+G|(b&Hr5n*-eANefw9~Zh&>?a<3 z3@W5h@N@xe6b*}*N8H2;$;~l=J~2*E8M3PJi~zFuNm_PqTz)ATj6=Fo9mi-3XOCaIswj+jK)hL;}OzUB2`c>{7Mha+`dERi39c zidmf?)JS|W$as*L$4JeaoZfXMTrq5Ljwtvj`U{+LjKoyPlb%}YF$%=_ zjYM}JV>(6!S300w9m%&56b)kW9lFZI7u}U!eC-guYlI?_gg1<&N7V#!KEcvMPDYXB zO(z8B>`Et&5=v$CDxcoYFW{*BDPDFM#Bi6&P!&=TN&?)`F>wG&o(Sn6q8zZ`zPZAT zJUA;6K1m@i>A@lrN=nqOK%V7-(XehApoz#hGC|4irN&DG1#=WIr6__D0re#xGo>*4 zU18FUU?jrF0Xn1?+g4w~IESdt*C--5bQ+`L@LJEKO4Bzq<>?>+XH7~?b+4P{TDtIM z5-OeOSJ(komrk3ULmk(TX2gSUp_%W}7&YC`JCNj~TTu@IL^D-8Bpl!;FMkAuv#Lpq>9p zJ7O1Ng>7T`(Jqh&aPC5JxHh#Hpd1QBb*kg^6v&1E(f!f7@x4Q*ufzBX8-f7I9q3dz zXSP)iH6(W0m~{z}AP!qyowY3-H0Wtu_l4tZ+Dx4mH@Y^SgR)3n{(Wp4(NGI|Y1p~$ zjf{?y=Xwsx0bvqEtF@=Cu!S37@V(JZ+Cpz;wDBG9N_){J?BARH0*yEa)gS0>E(BCt zp(m$qm)tIQlmmgd?Hf7jkv=s2A@V}16lgM-7lbLU)KTrQk7^mH)^ za51^)VtVm{`K1dNT>lNa2?z)b3?yE<6ciK`5)u*?78Vf^5fvF39TgQ56B8F38y^>U z`J!E7pj}e9Q+C9;oOnXXWkTs?LKz*vN%gEEd)4Il-OTlCrues%5_?LCz0|NlYS>Ui z^vu6OG4#ZnKXI7MjEt)ym3iSFcj3)T*kg>gwv6>gw8>n(MW-*X!!)>*^XB8X6lLn{M2=dE-X&&6~}C z7wXXKp1QiaySsaOdU|{Rtb4k1r?0QC|DTOdZKTD&msk%E(<`h;M@PrT#>W3+gZ1nz zy}EjCet!P%#np>+Vb1?Y%AEhD@s|HoGxPuA59xRg*MG!wOe>7r+y;Sv%gp&DzoFwf zWmcd+YEYmSkd(&Ei2H@-6xQRj^uwG13N9@ha(a-O|6~&} zD%t>36*TJ(7^_#T1v~V8No3(U=?_*6t0NhgG&+Q!`z7;YG4mtaz z_=z;>>MPLIWT)lvGeY)F1BA!2nYP?xb7R_FZQYw$d}=6Qu758}M&=ge=z!<5R|1=L z2!7a9e8s$xGdg?`mea=OlVxLRl}Bx`hQ1y^7*)uon-kI``8BMNTuil!rmXV@rH?}s z2j{@&mOTq~B*OwY(23Q)@#(jDneXgo>>Yby;ommw2-is71~)dp(uOXjZV$lal1z_mK=^@SB(HR5e(pvDe%A!`tj7CRn z$U`l??O^BAjLjP(`n+#z=ZuZU18i;V!tC5NCFNC|{1ETBonsB;!)D__k~}~%Z*kY% zb0N%S9)*snyjOXm=EjfT;u#9!Yz$>1K?}wAaYufUoeSkIl5*e(vC~(n2>ynVedAmP zl52)sYpI+6Gc_p_uE8bWv2aMbv?vHsww#tGEk36iBqxZI z2KOm>W30AU=iW5qe}$bG0XY8j&WF)3Sf{OXN6=h#(GQ#b9WZ2H2Z~( zKq-+17jf<|I|tTFSxe{o)U1+sXR5e%{Pfu%a6VQIxASgu$sE^s6iEnF2rMa`x;}1{ zopsagSmuP#kEO9nL~wlaTFoy!$5VAb@3f=1xRfKi3BA5a=?8ocJ7JQGZk7(%s+BH* zLAo@uQv_Ts?eDP{XZ@rcmt*&wYf$65ao4kemtMZKvtXJXi55%kO_F<*mUka&h;@w- zHe^>G%l^5aG^|2ByLYoS zot2QxngP#|?Y5syQX7qwF{c>{L`##kLmb?FY%amqt=0u7ap8&Hdf!rJ14r9yT-K8f zREHLd;}4Rr`MpMZn+~}esE9=yyBQh-8CpN3#7=*fxlnz0)5mV!Nk2^DPB^5zaL20% zgw>I-gjI+J3|+R)XtMq6HkQD(P4JIf6P8b%>}6lb55Mbwz9xO~?n2wGczF>5B=g=i zHnqHKD$iZHGz+cOW@N)$wx9ZZ>$H< znMh+>gA?WRDuU`Z6BmU;6xl&02VXdCaM`ArF)uals??&Mj6?VaGaPQZv4l@-SVl$P z+keU}R*7svnsl*AWVs>uU7Kvw^AoLWMYO&cG#J+U*!j8IiJ>D$>{s}Kj%w8l0dvd_ z99hme%X`g2Wr;F5xSKYV(qZjSaEw@=zzJ>|C>A4_VRMt68KtX!3$vQ#+rJ8C=^i`r zLy=-Hxh9e{-D>#I(M_++Lsb1~A;krGu{%t`L9I-puuJ0U+5Be{f*`J|rI#$?qGpfR zjC5RJ`fz0Gz?Y^^5bbC0YAmx;67015R9@wL@uJhIhl|>n?wBv85q)SluF&>jh<3-@ z<+2wur>lGJ&%Dz9F<}K3(%}$WACi3hY`#YFx%h;O!V6>C^0u$&9nP0Bh7(uvG_cix z44R>0EMWFoyp#B|Y?&8#e!Sd`4!e4j1qYQKtWRE&+q(EMX7z?m zj?V{p6XkW5Pj;Ti@lyr76DOk6n%MpTJBWef{S1+@1h#K7fRVI;T%OCjOG&51P<7Qw zn(Qlx7Y~NIqHI7)(?tsUZWiAmofqRTdd9pyB_`aWBXmmi4F~Jq2#fcPjE(PX*j}Dd zrH8$vNB*(yj%&SHqQX&=N&Ug4T({q5MQKidO^antJuqf}f@hY456L&VRLbxc5}(r_3vlREHuO$9eL| zR_f6;DGOpJbIJYBHY0-aR(n0me=rkmo@BFy-#7d~<%kCWkqpC%^dCH$`eKqa1{;Fg z3l<|g8*%v`-E6;idvn5wfr+ksU!ZMW^oANyo;>IcT6B*+|M_b06@cjq9##O5F7&Xm zsRs#I1xcg^$tg0_Qln3PCGxeo34RSyGz4QsAXm^X^6QC`uI?Ij?s8R9EoiuGL6Wyl z@{wqWVK1j~X)?!9@`QDWrG}lLm5MociiM6T{!2)<3cP_7tP&JE!U_0VndwKE8P+@U zR!MCnvhku3t`zpR>!AXxY+4$^ylm#;gDK}7*e%pG3C7A+j7~OEPMMdQN5_d#dB4@>&<0Mjc1N$rT#U1TI4 z(KG<{i#Smu9YELm@F&U`tDcX}hoxapSOCAZ6QudEhyV<-eGZY!4p_|n#v{!iq#i-x zh$oRF7Uz9pzWT%+^Bpws9ef%`$B_;?FwFayo*H-{z!ZShPWFx@+cUQ1Lu+?WM$(X% zNhkA`;+k3+St;;aqkhr(NE;gH%A8?kDG10UbKY2as1s72JG^#<( z1=Q;I^g3I|QXCB~uB$69g+rdj!mjOtzObFE9aF@$ih}TrUhGHO4rpW>nqDAvT%ZvV z{VAHi@$|7L{HCpuNp47kYKzYG=%V#tbfZh3VT>&y2KDtf{<75a-Ru|Q!tOX}hy46U z$AG@Y7={6OGzOH1_2+Sg7GaSpOI8mS*{mKFtFzNn11YaS>Ea9}CKJKD^8xFsdGqgL zA4__@^dV~^a^C8BooOwKMrS@z7rW4ka0U?U<2LmJhAA7x9*`1ScJP$}WIG9I;9IE8 z3(($EG?e3|ZR4rI@w~2qo9YC{xX9$KvHb)K&TOESz><`TeU&y;rtwd&rrM(xSQcVsl%YP}LdEM*1H zd~tAUkTZPZp|{BGHSX!Nbn)EvP(eJ0eYr=122WD2*MgJ;t`%`imc!l8Bh1jEtkk>0 zA^h5*w-2LP)`o-u0nvs|u20~9zW>`%A|F#FDHub4UZI&<0V@a7?2l_clrIrO2F^AU zrGk7w6rOeX+{A^S zZ`vrCQKu_Q;`l|PC!?T^t*~*hSjLcaPH^E{S!C`$kU!T%MB1*7mh-HXuXo2oy$Bx{ zi?lwUV{L^s1AMoYl0jI;Dgtyx5nM%qaRBfp!maW0O1tZqmJzP6Cb$qhSDARUp^IE? zrEIJiI1z%d+c>o?YR{;Pd*^Z28f_mAi4dhGyyeOIS}Q&ddVSLgHuRqjP0oKoDgYf)IS*9P zzo9#q4i9(fU;M+}(MtV$B&*o3+so<){~pQukK4sZwSCSJSsU-p^9?GKDPebZ$Iqu(@%5 z=1$kvVypYqRg1pv4=;K{wqDKj^?ZCiki;Zx+26ajI$m)6!o&XCpWZt&z~Q*NcRp_| zb@;!2c(?ECN7^t~rtq12{og)4zFu=-_TJqeUw2o>SmFQZsM7wWD{=;`K{x+Qy!nq% z6#X*(9gQ+F)cQ{WDY_-h#o6Y60!#UL)1fI3cXv-uPakh@-vEF2z>A)~zW)Ejr-+xl z!$Jf8j#9-$hx`|TDy12v|2wQIGKtPk{A)}z9kI&FO8Glzm77ccH{7bEnDQT?t7})w z{uR9{udc5AuV7eov)RAFSUuhC|AJ)obo?F6x_9^X{{+t(85#U1q%}D)`mdM z^sh5BJ3IY<0dGB_BV5m(J*9(OFJAoJf%g1Q6Wa2tSFc|GIy4&X6^*v?<_&FimHzq7 z>aVl5w)Xbz+jsBYtZ{J6VI z|JdF8b@uo7>5oqb2M3=&fBy38e5DWU;UWF=|37dTeNn{yM;uC5i#SlF>d*c!+0E=C z2AMo&|IBV?*Lt>FJ^iPyXhK=cB2_Jht}E(nu*&)E80_?)i$b$N)*1Qdq7XB+FjoDw zD5``U6f^mItR-S=tGvd>7|%D$hBE(J6jwN{yS`JP4w4Clr4k7qsMOrs5%fi|J#VUX z9Q+LsTd7HuKR3Mq*PrgCFABzapC5+bUtqbwV;*J?2qC6(t?rlK4WWVux$d7-^^ZC1 z+I~H-aqo0jA_ij7ug#OleLZ6L{al+6-a37X@aKKY;cAz2DQh{NwM>7F7T72LO|%k)Yf@v;??h11%B79Z890m>+wc%*g^B zNa68n5COQt*H?7-Q^~Fn;o|iwvS_}~n~a}TJk=Qt2n-IS)b6tyq7VqL!GRAc11!dH zHl1vBZiG_~-_X)(t|5``!83&C0eQ?5J`%aMEc#0%M}~^HCc`eaYm!(p^s(N>h?KT0RKi)m zcT`|oIvK1BZ3C|G^8_VPLce23pg7{U8F6r2P?q}9^8nEV*2s}yg!&7t8!0Q&SWGYb zbiSHNHcT$j!nfrRn|@r0p8Iyag&;bi_GC_EuK2aaqtxqiZsjsMx~+(}tUB-n98h~+ z1}8jEX}ghSrV>Ji*lBdYn_p6cb%bZ66b1;4}Wn;Rq=ABW+-sOTLU^~ztc=r z8wfOGtVrx$7B9=s`Dym$^7ZDr^c|Ak6DJnX*hqC6B=@@9dls!L{{^dH|aQ0xckL%;T%+8wfZutz2*eduC7XCa;Y*yD8;3aM?!fsg&CtF*fIz8LvrK zerEEsbnLSGTkh5mYRkXoI%T4AzJCkCz3@}#mU7};#@@T2*^BjAdSA&I(jCvs#ueoQ*s^a?k8v8Q_-UYH z(mU=~fY{Y&@er^MpaWEfnV~1zZXh40D!;Z(B13{t{1N^Mx0Q#g1!2!NyYkoWrDFXs z0LR)8|3e@B5%^iOZDU5vY7PEdq&`W2C&Afh9jHBN-E@i<3T}g^94e5QtUa*d{3F)T z41BUoUTv}%Cw$5r1Qve0b^hqYh!g5s$#Cw#EwZ+To??IAePi1b`AL#<%HwY zN*O{K34B550fyiNB;v6w`}6T9EpDyIj@~!EO7Kxk-AcBD)(j9z9#y}?#%$`ipPwZ9 ztU%kLCFJ@ILo_Z$R)Gd4w3-5W4yml%u34PpGyE+)gLKg!0|?M%Wha6Z9VC)`q#;O) zNJF96*3@iX5_7@PNlB&IT649sZU2bqA;qhNrqfH)jOktG;BYT zLL;rwei-IPnBqil{z(=W>E=gVg*tQi?_iD@a+03nyCe>QBo=uen4~z~>T38bS2132 zE7_@v`RPu$aAeQbc3HL& z+#ok)59OZUWsq4o|7~GW^~>#BfuR`*ayL6kPg}DT%WL2aix0siH(&i8X#7A!>}^Iw=4}^u3O@S0*=SI9Xx4($l#fI}6vEt% zZye}5c>aaYH;H6!3&2c96W4stgzwwv^vhDLw1)1t4||9)piEl1^sT#RM3VX#UH4tB z3$B8T8lD@Y-`|yUoNOw{I$Er`3_?9ng~M)Ez-`t0k=$pth@pnE3SQ+*jkdvx?X3qk z$J-7^7`Ck7(N>6PRd-hW4MUY_EW~@-W^8UH=*m%)wQ*VLc%rh>d&*BG;RYQZDKz;= z!j{cbI)rCxaK3#f$Mj7?Y}vHyDWwCb6k3Z@!LcYgq&WS#21&H!EDy9}vj2{E;z$*b zucR@RLD1X$YS_a&tCb}j+1`rx&l(mOZ{wUvC=&jmHXCz@v4KMt=f`*ps) zbV!Yl@9tvl7q8)2PfVAtOT-2{b(`F9QoH?99&Ql1?tY)r3&yrESH>9}H!OK>i#pL= z-|*ogW7~^YLOUVDzZ>jxmoCr7w+JlypAUV#e%CzMT996KFXE;&keFOhk^f>l$p^&K z-5e3Ermm%;yA_NMXeMxx*lN@S6lcr4{wPCy{gMf`T-svbVLk zy_o4(s3%4pZ+{u=3c3Tp!6eIuKwCm0jQ_++nLzonE^dJ z{oWMArIb^yXS?0Q{3PN7jjVzevHH3Z;ZkT4K@&`5n4P=eKtWtaB*(-%ohdcllCh{O>KWrtqvyG!Cyk=r z2+zQGz}L)a@9?2gjl1?)9N@rlhtM&y#QW|nrTV~4JMTHN zSRTWLeaQ zKz%eN=!Qcg6=h8T;<5SSWo*j!Aru=i)!vXnGy+WY(v$}LVHzqXd@nIb3^Ao`K-Q6p zvLtx(=%*AY=bAX?V|~KRmc%U96D$~0PI$?h+ejVLqI$FQ#JHv3yf4s>mDN*buca z5XCs3y}5yMcf2Z>M|V$WUXO@wlyaGTLjV1c1IKGE#+MleGhbt48m{L?aaz5gAU1?2#Ko4Xu% zvVR2ogCIqhaY*zU174XA#0l=j!l!nb9}rK1X$%@%xua`&7)|rA=<|}A!7|Shgx4X* zr4mDVZI#>_&x)3YRx1juDw@q%HsRqfv2cN~IvBA)^BLT>h~XjLGOv~S3e7@aNkxm@ z>IojfPD1`hF1p8udW?rjVo*2Tzz^^UR*LM-H%~psLL-x_PIrn`q>|D%3N;yvP8$`S zZcoaSxtU@rHR>l}Ehc8C$EZX@`H>jMh+wFV|G8&+mUGJ5bMUwj*#Ii)6^`jNUW(=? z%l5=nIi-c#c$73<&7kFLHBVF}dzpLrQobWyO^m(y4~xC#LeHp|Fm5>v^Px;&MJ4dl zDM^ff467YG&U`YhB-$6aNrN22F4I6^PqMKGlizecc!mlbb8LWn38;>8Q2 zSMD7lDr)@e^SUBNpGA}uRV4OQ6lw(YgllA^G55h>zsaI2CB)1g=-0XFv_J3cGwW6T z#{Af~vTgzXcoz!pBrQ{!8lL#yud^XA$;EzQzSrK`m;@fh^F3kjY~QSZ&3koK#^?I@ zx#_yArDIojzOgHvL|4qI8;;~ozGsZPZk0BNk{=*`L1e;3=nBk!1*=+^D-gVk(sUdu zwbRd=L)p+Em{w%S+abtgH7tXQbsc7?vaaA~x642)QjzdZ<~B4$xTi`Yn#-yGf!+Lr z^FKKW0;3iyRUS917Ra%WBS+7IE`=0k%Rc9L7e?mxt9jLPX)Y|MWh@+sh^w8?ITFLf zvFMC(+_?^0ydKq`d)is*2;QYlgAU<_m=wo;mQ+QYBr)HvXPvmfhG^hIz`UKxaC!`V zq*|78r-Rkn2Z_wD@F+wExM6_#)f{t?^B7h-So(HC|MUF&^jcKFQ_sx5)4EJf4#+3M zSa9Sh48Yi-j#`C8wW9FEXr2OmEdXZ6Fm*YP|C}r&Kw8y!ifq!oPV$6?eY@`|#xRK~ z;#5^J2vIo!fI5lAs#KIqt1a}im%yEl)X`)!#*#CU8smvYf0*$-nS(of>TI8qi|SC1 z`&5I1!^ryMXuh5Rgr>VgkJoxBrFiUpfEE6Rf<2Bo5u>dqcCyyJ=cG2VBflF}F9zyq zo#Ar1Q{NaKIbN>So;C})vc5bbBLzyNG7l@Ab~X^tq@wJI5!!R$$z4XC0mKu`Bal-? z;al=+rpOhu$W^m|cBV>h-i3&LgqH}@r0yB`0W#|f`qX2+-#C{Ld70*NBW)4Xd*r3OW_@#8 zW|enK81}3RW^Q+ZQ~$6;1_HW(oFteCyZ}o73hTydOUbK9D5yy&sEaCS%E~J#DQPh) zTf)@rVCoJq4M&*fIhfW3CKWulqCUH_30m2VS5X&s+70%bC+s&b4x9@N=MB^Gh3Wdi zbp2s^0Sx+=7z~3MOu`w=A{or087wbLk$fd6>uOkTRcz7q%O&+G6*tnZHfL40=G3%Nu6N|sb>!D| z7B+MhH}za;>8ot*zuI=Us^flj=l!NT4{r94w%(uU7@X=Hn(7*!zB4x0H$H!Na^e2e zqrsUcqw`A-7hgVJUU~Xzb?NomGuqp=jSp`(KfK%8dB6Q}HOGUyzKRqb58N*T9S zxh6u?+epS-

    c$#U(E3SDsg(g?a*m}4p2r$PRm1GONbY{0{Xqi`G-6V`y`Ux&BZrtP z<=t~<6&OnT>CnjDU=BW!(CcuxZ>02g)j+;L^>D=`GeNP?*_gTlXP1(J6+cT78_d@7 zMjZ{c=^f4%L%DaB>y*8z_G`OQvakK-phZ8vUT4+UNg0QJh+3w(2=yd@j72k~baz=RJ|;06;0M5*61Ag7tYZBuW2`#p>QuNJx*d~+Fzy`{((+g_!l=KK zt}_%hzygkpS~L-6>t#kgu$+02E|^y!C>iD{UP>0Sr;eqH#5@nWp5*wdj_wZxbPa`E zb2b{zz)Xk(HV_QOiWj8av|(2&fwRW52P8L=t`s-f))f$O!RR@>U{m@_>7q4V)s{|{YDUN=5;FNbe^ z=%J+^-|1Z|_TRa^UUy^X&UW|KPT%h6@sIrnPyIjMJ$!fLwm#km80B^cpvMAs z2jNmTcZX2w+q=VPBe}g1PWynp2Rz<4_eKRGw)e&`X>$AH;w5ykpmhDs{Ykl=ZMuYR zOzsn1LKpC9M&td>PY<=eZhxA^G0Gpz=^wjzFmEi?e6V1y{^8&e-bnuQV_W-+pPx8* zH-CQW9P#1vA|Xxw%aTXQ#V^l%>YKkj59s;uE8oBi}cFJ@YE|>xZwe zNsJ1IwB%!fhbv^Mmcut$>N|(46eERiYlZfK-`g&GN*Qe>}uIlTN zNWWO9k}E*8q|si} zshYQMSLrm&*4F0s_7lCK&*sWSx776YQzzzmvEPXc40nmf!vn(Z_eX9F^Yw zXG9;Ltz-W6Ke?G?;{tawh8MH6{Hs^9;I#S?>f}KKoy3)pe9~*XKHT8Y;boTxffYz9 zz;W1JT?D5H27_4|6gB1)=EIaa^V*>HqiNQ%Gku|kO8`JacV1gX@cwK&Ur{X=uCs1YDv za4ZMH9}a!bGN_Tvt<4t;=z8>j`{QlnE(UKSG8x7T0g_MYl>%T++#tX}YY@xTw#Gg& zQhDxEYs_Gd9|UA%Cu%b-SthLVjL!0M01jS_Z;PG7H{O;IQf1zidKCMt?5)jDK_Qy=ecI{_;Ei zw+7^C!P#wpO)nNQwjiL02l!|1N!7L102A0`q{CB*>!zlm=(7;isa5e^3xOpUvFqDE2ZCS479Q$hVa3)Fe20F+p3vArXl!+B>2kl-?cUbw-geug^R7qNJ&*2to;?Gey@OtNhP?Vlyzfu= z4nOoAdFcCK)^BXVfBdoE#1sFirvX!o0W;4oK71Z9`{LsK>x&DoFFjqow77O@@$IFh zcf@D!iO<%F&o_uKHiMS8f?jS1z4{PL`xv~k8}eo^WOYAe^;5{&LCD)Lp>IEjtsjQ2 ze+%3E5x)I<_=ls2oj;N@;?i>x>7_myl;rBV%DSei8@KA(yIcD1^^8wFn4BJ)emF7z z=;8dM+2xndU%g&_z4B`1&FeR-wAHt7*50nJ)1QZ1>wEOAez5=F@$~!OfAha`1?VqN zr~fG(Kspb%u9T;+2+PtnMsLdL78c|**~;xfE{5^^8V^CtC|AVBQea}~}+9jTI+ zl9-m~yWb$Qp)>n1M=9yy%_wDn@wX7zZpCUE1Yf1(`@CPeJ{Oh+7T#{GIS@&b@SH^J z&{m+O$AnN?n$|cNhmgnQ8k$x`Z$R|!r0h}#(wXIbnrwG50PL8GSHJo1w83mcR?u_6 zjf21p5;H~_li%oJa^={G4hb}!QCr|L!!9}1&1u{lmdW?7@Im zxa<{7Qk{1BGq72fqHd4M(Qc*1oIda8I31lxCHR_NlxgU@1Y7l$!K*=_oA$hLM-6*4 z1P`rZa5|Ep=|JoU2tr)!S`HkENF{hOP9Z>%OOV6u?JL1h+q$!CD75$h>lq&WFgJJM zSpz7;0m&f46dEe+x=wfN{qpM3&;J9v-rv1?|1|22{5lW*scQOHNz*Se)6C4nhYx4}hn{KS z?{>YXPZ$5UWADGRto~Q{9s0s#ng=ukcl9WFZ%TKCWld%!vuX^>1=8`o- z*ex+5YqUx%iq-bEN0&RG+hLCi}QbD4&Ji9pI3;lKs zVig+fBREUk(1gTDTXC9SiOYgM7;n6v2y)`o9fJ6+wc&L7KNl4-raR{W%-P#mhfHbl zU8tnbypD{MfA6S3^m(x+a>fRUx(#SS?zOGtUNXX!g6-c*X z!gQ-R{Sf<X_mY<3Y z+3#3X49+t4D>90x_1k~IC9y#-@I=N=mS2iVK_l!c@a>{Hsz+0NgB1g8SYJroS&%^K zb`^j271u9<9N)MfCy1!m&g=8Ia*Bw>!Zf-|eW7hXc}El40tTiaCPur$EIb2Xi2zjc zJ}43!1fXM#!RMSEzGXf;&%^m<*!Rlk@DD=D>>~#vk$jXu*S+RJqlfTIY7CfRwTl7X zutAYgA?a=E7{6mbW7{P08+H7)e2mejBV5C?Z1vJD_F6d@OpszIlyQ@AtBZ$lS5Hho z5)1Y<5qrG1P+Vz6rVbchtN}^m5>qZ8=5yW3MA%wQG`muX16q?P*M{6d zy7(1&Z|X=`yE{k={+{V5D^l3{s_sZWby`GEu9{sZcqCOkC>=j-%)z@qBJE@)Vb@>H zZIu5&K6+m40!obgfw5tJXj_&~s2ks6L!NT`UN$sHOyITjm}W~`PPo39&?n>3)BB29 z@jq*hL1c{O+1n{+r_*g4+RO9BQ6d=lh~yB!-A2vE#JG!&s8uUIR8bPikaIN^ibCH+ znTc>#lu2tU8Dxbnh%}BMT+i#8&fZ+WG)}Mi@C=*0#)%i>%OrFfNN{EzD*Wj%>#;!h zHll9=dj$o`wpJ(f#hF#(3U!1R2qAq5dV?O7dugi>1i3E&AWu3&59C5BEX2JvXQFa+ zyeMP~=QoT^M+dyV4`tF5H!!ESd~qnMS5sFwUIFd3Cbt)k#{U7YTcTiLNzxSJ-l>#h!=%I%qgeIt{ zp@X0Xr56p-t)UlDlF&kv(1Rj|B3%uL0*V@{G&Nwu9>6a83)tOm_7nF$=gc$vIrq6| zesj+~_uP4gf0!Y_%=%K+=e^#a*NZp=3%}%qy!jjj7sVuMinDxUIKVGy&iqJ3*v}mZ zw+Lfg4zDbdp{J*#B5-%#A?Kh20gUu`DC_Z-#|t_+Zg+@KH}N2MGSOg-qBAOlLqXx= za$7WwJW$J?<#h_Djo!Cy&&1x@l?ch#-+v)$7v{7BjRV+@gt>ZsQ+j59-&jDM3fd{C z;iJmnuGtI}$8KnUc77#%j){Xq>z2pMI+&0%{?{zs5eRQm{o90|`I*)GSI#PK$o`fi zeYN|5pMj^T)?;31(9Zh-V*()JcbiD2VwA%i1)~J{VsYNY8OXL~d+Z-FR=FDxBUxK> zqr$K!lHOk%b0lw7m=7DMAdDUl1m%{X^0#ll-E*8f!}8(b3mH*<27X6F_V;L$FabCp7pdrm&lSKBR9h&>9J7i0%rKDmzCqJGe8M<8oFN$XwS{MGKTd7 zRjV(Yk#&acf(JWT^Ye~B4a2v^k4UMubll%mO~j37sd@2t>>m2I4}W8FZt|t|H7Nu= zeBM!Kgu@u^H|aMt7|(7cvz$fcm>Quh20L@l{L?&C&JMn+=g9FUI6F==fG)6Az%bC) zo;sQj)7uL^y;6G&(H8t06vF@3n{RNoFP-w1%wPeOMhV{XJeLcr+V&Ls1BKJ zq7Ff3^$qiO`#-uhb2GWG@ad=3h$~mP0$G|J<xi(D_v0ZUl_eozO1j=z3B4OZhZBW&Pu86>E{Oi*vXv-D=N>|lFTyKQTLc7 z)CmV(uJ2!W)98G{#xDa!>;G8i#d=x_11S)w`=#0Ho1dCD{Qibovi>%b!hi_|*l^CJ zd%ZW4&dP}1IwL6euW$R(1&1u5CAHt5)N5V@7%+d@-X${RU+wI7W$}wt>Q&GxSRbbxRwfR{Wb_&NE=GW_P}-q&V#$4P>r{F z`IZC`ylElJV1);@jBu2*Be}ovT+RG($MeYNqI^q)hIdw$3EScEF+xy$84~dn>Wgc2 z3aKNXRiHS^=mnU3n8;)h2{5>}7&t1JITT$i)2_OzgK|JA_E^};S2_JnPuoA+VnOLR>XtDOOf?ua1JpYMMouz zv^3}ddIeF=vkRBohvK33@{V}jM;Q_z+N7*}5Kf3e*aBJVB)}p7rccjGB|t2MIq`Ey zs1UMxn9eHhWxCSXP$5#SFLBd1jj9#zV3EamA~39o|5&X4DqQhyfHuv_hiTP+@hG|!d4P-L@_@Az z;Di8usjC1Eg%^WxE)-tIFid2GXDXW84HtfJv_TPJ1sq?*VB)>;tS!-qT0U@$0HJ^5 zxbmSH+{92(k&wimbuYF|E%sD~9~N=Sx#sm2c9kv1P)t}J&ki%Vs_#UHSkY6r@;2!S z(WM-i0u|`7Q}X9Qd?}a%cI&VhG^DKzH<36#m*~o8CW_dJV15K2DrszS1f?^n>_mPs z2S`&iV(U*7t6HR;lvRCje&gBeWvFFD867EzhF}Cx-zbPR1rwr;@r#t)q@i!WM5L6% z@;R^{ei$wvnL;)*?hoo|&5667`hc4y1u3niA*N&42WTjtd3GpSv5JqboG3~p9%r?( z4vSKy7+DYJ5)IR;PAof~0G-^ppcxHq?2bvIL*dJa*WA=3<*G;5lVZ@a``zXDxTOeG zYIme0JIuQxROAC~Q`%MXCoxI8@7KCSB)U8MpM30*c`$ufta%>zxJ@qB+)VG>bKl)God~j;@`zeSj54N6%2;%)z`PYaS`zzw5 zL1ZLvr`I`=B|dJ4o$fAy+ucHaJ$46pMTT$R8|!`W&<=W>BtZ;e746FAgcVeUm()d9 zwjSX19c;LCsOd^v%TQd~aB`P0rF%BD=T7Q{d+EJ%>3t6~ElXBq0IAgxR$yiA7WVaC{~Wz9x*XEIEc_zEiz5BT*!s0S~$1PiR*V3 zC`$e@(8*bjH8kW=g6Eg4xo#~O?9V6{39>U|CFg+bNBTTq`JMqDTXn)Up(ypl?h5-yFe16NPbX1d|-&hjFlDU$B(R!AG1Dx&ie5q=l9=p z|M-Lb7l-=4PsH+Hqbrjn6Z-)5064<|A_M$g2Bs{dGcx|K-t$(A`SCw#L~VS%KWRk! zU&sHVOFP_~maL0z&)txT)!1uAl6cR(RVChYqp{Q58n+|?N$tKJW->7wc^#;}bE4K! z;$54Cd+XFGG@FRCeWs8A(;nw^Z?!$^B{-FdZlZbGKBLHAj}96XP1!!?S~9UyHw=Y!trcTAEXnndCwmsynRkXCA}!qWa=ku9UgmgSeaamt~80Zu5JPiJDb} z+t8Q^H>$p}JZozHc%%5qv@9+rU>CZAjI+I3^i@6G{;L-|0@BNwR_j!7#2Ou8Ps~H7 zwXdu1yOGm;|L(p;-&cM~$TibQ8&2GfRg;R7#R9WzA|ucIZbWoK&js)P^L}OZj3SMj zilNPsh2Jb+`W2hp&Kn<^eo}98ErkAAXZZEI)N6H6drzaQ85VBl-Ok!>xSb=)r043@^r4)ueWl`-}fGc5tI4m^6O<{nTg9w1?MpQVk z-#WM+O}D6nZ1F(dCIw1G&2!H`IL0KucCo$WT_{U-J9zyBWX;vMLU>bGfxrDOm7Z_K zvEzg3QkH`j1Pg+1$fDYH5(kq!qv>rA&-2fZ@Y+#cTj1kT#>Hdwtzf6U(&h9|^o;9o z_SjNeB%w znQhaNE8R6WNA#^5J*HxJ$XDGPFfiE{uM~=SZ_6`%7_-;8=n!XU&BD7~4UgBBQRAQ6 z_XTcU5U9u=M7f!pul7HrH=oAgi*}xTC40;u*3Y9zaNcyKLd9hXb3LOV;gr;q7RUQW z)WuI*Z8X-sj+Feig`(~x*w98Sedvr$864+RknMM+iA;|WxcXhuFw{7)9^aO?@jf*t z3ZyyFNm)(rew$qTl%t6tE^uyAX&F~PO8vO=X4Ch$*}!{(qC@)^r4L>v!Ha{W>L-}Z zgjr!=5FJO5g`>UGKXic@6C5|TH|Ym{XuxqM!$>gL0h*Jaaw^2dy-^DMmZf2w)aG?#A$9M!Y+wfm?>n+~`{Cn{ zGk+pT4zgJI*|(xjJ>9%mJgb-U^YyY&P!u3^gxL^BM#rM9&n)GJ-3%UivU0l8&R!R_ zpF5@{`mECL>!u8hj~e#G3vBuOf-9KSyI%U9Ta4uqdM~y3ajdFQ8ir9e3@$1poG?Y6 z+okaVd*o|mUdfw-5ry1Bqq;cY469lH@PU|+U2#&D)Tjer99;3wBk=n&oJ^x4JcDPt zcW!cCRAn>nyZy1DnwQonv$gTb?YDY1?SThOti?!MJMP^YxtIIRww+^L+yV2kTRixg6`xuw!O+FDEY@o0kf11qRY>3}Q3*yQD0#LC^ zj$I=SxZ)cRTb&G_;H!@bN`5=(w{~Ooc5=Z0jCX3%a#xYux)VcU@FEZ3OXCfxS`pJtUv&s@8Y@s$gR1h`3zq_oWu*nE`=< zF#}@ymVk*)g~GdpD7cIEKSa|0lW|YQx}X>t{<@*-x2wCquZC}2#k|c_eNO9K-9)M} za}HMk2sNfZYhIm5vWjT}NB@v!up?R-;8d%NZQndL@dl;vHo>J)`z(**v#2&1kR5dg zl%hvCDag}P(g(_===3ZlWm8qd<>`~&kL z;aT|;86OiANdRfkVJ!Tm6Wf__Xa)|7jK`D(A43oQXs*?V2yHigX?J&nMtqxREBTfsG{AikY`84YSdBPZ!@FiU6Ac}u;*~_;+h}Tc z^R^Xd`NO9b0W$Uy5$5_;>M8?gPQ=I3<^Mo4M*ZPR94vAUM5oGDSbO6LyMmXILn2s; z7{3pqS4xL8fLU2#z_>_ikOq^E2bAS>_wd#nDVV! zDLS8NAC{pBuuw5M!0SC5KVpIuqz+JZZOe4MQ#98^%SFp#((7>yjzyngh=7M;r&wx6EVV!c!M3(ajq>$w^I@Z3^F?%kILF-Wjqjy0bo-8W z-?N$)Ay?vUFS1M~Il#NbT*z%~wiq@Baf~J7Nqo!pS@eAYCH@QY77-mJi@7U@zRf{Z z)MNX>q%}OeRXlKyh_X^l6whMMh%+?;;Cv3#1}r!~j*n&J9~I>HicD=J-&1iD3o?pwX}exj8y|u;^Gk z+DweQ%7GSutN0sGm+%$aQYtXY4|{#VqB3h&f-F!GhSm;*pAnF3K=e%_oM(X7N>i@p z=I@p*{Qer>!sb-PZ{pqLG0OzZVTMR1?Vxoi`)EjSj3{|Q<9@jAzY?5HBdwY*c z)H)j?{DuJaIyxs(g|#m*<0wS>CcX4iYC*#rG_0+PC#*y)SBBM~!YA@isT{|>!40(L z=y4edZ7QSk>+tNdlx5_kz<%L2X^L;Sj|wOG16Iy(0Y!>NfsI2dHhu(Tg~0t)A=ZYr z@sf>eX`ubec>c=5alhAS#r)FMzI+%Ri|8+eu9G^u4&=XK_7V#Tfu*0v^J5t&>OJZ% zt3+Kud)-swLz3RNf7Y6(^Yy2qN}o3T-p8_2akdF zN}gemKs689_%^$jgDFS!&=cHh?|Azt4z4{_s*et(LPpaE0F16LBx7GbLzjtnS62|+i-Xhzk^cNgXRmJo2ihSD6vR4mx84akgTo!ZM5GN5 zQAOdKZvZViAe)77i6P9e^nAN2tdhsq;v=_F;SMICQwMxAy~o`|Zp#UXmjH88eBmSw zDMh}3d(^Rs4-YtTf!KAyhl+#`BDgd}gxYxpGQy5?aqm0XEjU;ucmWoMs1YF&9(52+ zy5&yvYIR-cYwsXW@hLp`=Yl@%`o6KrK1a24A65IyPV|kmw-;XSPy-Rq+WSvDlC20n zQ{{PKV#}q|CbDJXOMDZIE%|a!@a2n#;r)9rcd1=IS!5{8Ab$|Tv&U&1hnC+yIv zOZ5875PzZ=y2$9pa(PDr5C zznIkj@t({7o=N?;k_&U_?SHor=D!BT^PiWsO1`or;fa=%_2?)@79g(~Lgm*F=lxGz zazCGOS%_2dpWTygz42q%&BsIj1cJf54SXbm*opN14G`}K}y{{H~AYLGcUbP&SYqM~i3SF#+U55fb4Vc4O)CGc8_s=pc9(^ZSfG!uW z6dLIbly1?!{AmS1;MUq@VX8l^c$$Wqy87Wx{T!Oo25SYA8df3TB=MLBA|&K_sf(^U zemZPQ0sX180IzH}I|0uqjoh6keO6eYiKod7o+vAr0hZYG2kNNl7-F$X~U&;&+VJUFB_01QD0eN8hhDGk&W zC}Q=naL74in`Jx`-yw_>5IQ=^*GWVlWBpVsAPtgsDvl+<>-Wr}fM&jfpL|CjHR?!G zy$#_gy(k_UcuZkdrDs2o4KYlK*MS|0+IIgv64oXxARL|0ehS^ww1Y*c%+MLbwG4=0 zrlCN@bt9EKYaiksw#g0Q%cSFjk35x)7$hZ+To^Sr`s{yxsM)bI(qL?$oRB?3i}uicQAzM4`j>^jZEC2h9vloon{#aTf^M6bDAt( zCRy$Gf0X<%ESW)@)87csjbKVCXIYi(8H7?rFifDgE8_8iapU)X3HnVsxvO#a3l5yl zO3rYM3OrfK-=Yc&oqmYdvWFEQZ5QpcG@1lcolm#AZOhj_MPAM^EEkvR2m_z_o*RTf?^$B% zPHAUOKs5W=!GJvQn#mu#Jt!_9vexdv0yV=(^qLxbD$A&Y-nIY@+KhVNQh)5NO=4V_ z_LRaAqfxymNQXZUFw+fi(cJ`ATQ@Brvcx0!J-L=OtwjgNehXd>8q?VYaZ=9Z-Ktr2 zt+E_hP`*aXksr5WZEcDMPm)I9s`HIL&JJdK3LTBX=lS28N=#SHdC2!9X3h7zhj8c^ zreXRF&XYmgkTJhIkVwtnP1}RovNVd{A7iS~z){)-KFf7w6l@r|VKRa(R1NJ`XVori z?+$EK_m~}H4$NrB)i!F{>~Lc~?$S7L;?waeH%jg8WElZomomWT3kzKk`LIRNP6A>@)**&x_!} zKuxPtja!Q-<%zTSXs33S{(VZ_H7}YLt&w|9vE940vsZUY*?+gYg=yHS^ShqrvpfB= zVuEJ@x({x$jn1$pP+b{}k{M|+W0MF*xJVg(?Ha`Fn#vg%bFr_3%g zv=Gx!szbBM+=ap z(4+Fxp5!QRtW=VLXrjhPK)A#P@eC(f=d6La{b+F z&Ib6z{R=^s1r6hXM%>WJ^Y50KByPaC0)f_5~1baK5Rohb5Tmm4-41WAQ;WvAir*I_viEHp@~2F)Bx*6I!T9FW;2zE3CuD^GJbZKvdrR0r#9 z?Ui?*p+Yr+m^lh`rnRxL#wyT>>sM(CQ@VE?3hN=`tVFI|C%B;-a4CHw#Y0IEv5yxG zk{R+Dj&hcpMtUN;ki(Tz)>=^-E*rEl`r~&UM&&*|6k7LBmg*ncLkVT+yS&KBuC$4W zo7JER3wT%SmG_r97%N$1Ib<)uv;q)oLcmz#J39B880S?!K$x2^IGCkwf@s8)A{A1a z(=GdUeR)2)@gJY7O-=1aFa!|erOuE!7chj>tH(4vl7c$V?EaPorlwUxD@<1wL!0ab32!!2um%B%8 z|8k-49Mjs5gCwuUTd?1RK1h>}-)_bjEZh>n$T542#7DY2$v*+jh;s;;S5~lXj6@k{ z-|yx@7CG+MPfL-JNd1+Nk1CG`Z2Ug&F;5TZG<_H%YE&?|pWHv@yf@S~M5gfmZ~dS7 zg~$_8r}Q7*J~(yZ{Ko2onH$U(%~G7+c7NDAu;J;g)tgs_ul(casSP6dp_EJVSKsAs z9+;Cq_wDR!4TG3s;^ zrl z!uc?s7(<|<8-xeR&IYfB&~zGB1!8eZkj=auax`wg6{Vzo5vZV=hKjM~JX#~ooIYPP z@1GaXRk|Bfd;Lh@ptzsOHZjH7pXPS-^d0xONt%02QHbiP@WA z5zpVvN`RP)u=O@-Mci^N4^SY+z9E5=+%l82;s+!M6H;!8VV6SKYu^;v#b&X-VJhd2 zjT$ms@||xuZ1;#QQMv8hw@zvjnjhga~a87JQ?eE`p>oaMwwj z0;r}HyqIqd=HmdvbluUtlsqdCC`~;EV*n&7_ntfM=(=)dAe75zUrswVnaa(V{P7;= ziX$uPEK~#TZyVJ?UtHy(C4oMk1H)640vNcYFkGDtTvv$EP)4`iMw}5MB4<(k0$e5^ z;}x^PiUVz??|;^88W)znVO^dgx!8Oi`?3+Fln2C?9?Mt<*ZPD(zRly(cwBL@#Et$m zuU4m@T}5ELus!K8=5?>FLdlsBp^ZFFVfS$`dPFduw{}ff2Eqk|d;?P+$o=uE)J@hM z8d)@VKmGhG(B>^@dr5AcYLjcJ+fYQ+X5;j>`=CY;SO(x<#mKGu(d03ix=y>X+&|5$ zw6XS8T90jNv8`K&e{=ZynwU#&(U+QCW||LT73iuh*-g!f$5VpRTTWHCr0Sg@s){ZOG`yRF%W~VG>P~_8 zVB0fQ;YUT#%j-K0N+GrkgaW7Y?2i^XI>JueDcsVz0fJyoc4T4^n*?W`q_e4uPOp?=)`X8+WK)?vjJHY7i0X#Lmx`piUI^c16!_ znXtCWpjL9BtPTCVT`*j?^!&Qu^U8nmqoLBjB9ahE3Fh?weF^4&%}4$>W_5{H{jb(b z=F#o{TsZRYI@KjM^?y^TOY)Kb*`5A3Yr3QX^Kadkj*dN&;)|s3GB$QlQh9myEbZmX zjDI3DsjmJd^7?Bz=sz_y`8SXHpS>35pS_lUDYdMw=KXgIQ~qljE&oKAB8f<#fI2`K zERsYde=EUcBhab?|5AckKTV=H{#k-qa_<&RN)+o?_s8z|Rf5r-m6TwjZ=Kwfg$>0d zw$3z~dfaoK?0Mt?)4|J=v)8><) zQz}-Q%QUFh{UcmvPFYzw%N}2I95`R)7ylp*0&C>41kh5pBbO+!ODM#l`;sZ>decSx z8l5|FQ+ku++Y@l8^lQaHy^5hCt7}E|_n{^|3)7aBl1bR?(pH0q$&+(k6wQ%{IEZ%d zI1OspVaMUnqb^hrnK4X7qvnxp)M*7PQ5Sw!b4WKs;lP~>id9Bpoe8+c{xVM>{%+Y& zaB!r~B$V`e7KfFVJg~(L^*O>Y7k<8Z2ybuQ!9ucHsn;zzrnFp(Lw#+ERZ0S#Eod2p zPLV=Xpt!(5X`EB2xr%P`);r*)NzON3DCSS8YYKI)JM4Tu1Ph5dlH-3kd1MdMuhwfv z;JNsi9ENdBI}8?bC^?zZJ#~{YWEL}?bNyJ%V@sH7V-L#}ccDkXGGDJLa9cy@8wD+Sg37Bo%8)y!Dk$H6qi%a)G~S}vW;K7ZAB-{A*cU@qt5w%~JRtUKxzq$4azUHxG6g}wC zmMVoI1WY$OK^xpsf&0cRI$!S2gb+U&Z0f{ap*X%WIG;-X=$8y66iDti$_mRjoqG{< zu58ENH{nj(VSHtQ-J3zvdXB2QIZ;Q!(d?3kwrFm^7qv*ioR}a3@1? znuap6r`x?sdkbx(=Nd)P9sYQ+&jeX?iaai2sVKZKCV$HJ+eH{wl~`$fyXvJQS;Ix9 zk$I~WAB_Z(S`zzZXPOhx+Ghsg@?(94mvFs*KUdgV!-0 z`>^udnQ4fX)WjB@-Lr1Di=s@G_|LW9SWc9fuGlDh-_*?<20aX#tSg$fMSI=$=sYl6 z+WM&}fuOR*&9S@U{)A`j#aVQyL|}bLP*H>s|woRcdO%$iu=7NVM zfitS9Bu1JGcbe!tbRu8nGwgU!?}9ydPKkz)Zix&;-#Ehi?ntxzUXN`uT;@qf+Lmm+ z8(V|skR;dT3+tB(?aE|tUQV=zZ?Y~94pPZ74-g^4M&=!%)(;%USP&mON0UL^@c9h= zHG5_61{DoL)&yKo9MQO?l+3-kOUBY1--9>fS|FpKYbOsFb(lSKTy+ZeSR8zlIz;e; zJkF99v4W>*(9~8Ji&|MF-bC&mXh*XeBdo}tZMfS*tuTaqugI3Obg00kW^z`ZsM51X zenuo6Ahu^dz&|sJEr#^Qso)X!IVz)aY8rq6s?zF$n?1!^E=%_>wRPQ(M1) zN~%Nmkc)JuScCgt55pI%(I;H1P1C_KiGDQKlJQZQojtzo|T(C)vK)d*g{lKdZ~bStkWO4maCE zMTc@&DpRjVy3^m`!`z1ibCoxsFDdxw=Pa3%^DuH9v&g;aLgemEk|M%#QDjjA?DOT_ zt6%PKmVr_&HHkPYFqHwe4ylaw)rBR$FA9wq9AG`2R4=lC`?GZve|~(Xi2p%`snSu? zO5yvt{%*~!8`du!{ObL5qiX%c;itg^DY9=`54DIdKl^agR*>@TA568=B9tu&q>OEO3$^EYmE|hDfpeo z;%izE&kgH?AyL~Bpi4hrQtQnhiEWEx6DYLRBAMKmjkYu0(}>qdihoIMrRO9 z3quPqNj%sEp<^%+r%PLZas@R8*^;7-yh=eOX#;(hs3ATws1(x*(Is(k>sjbgDpGsT zUgaT7Elr7P3pccdpW-78qcf?|u*>`u2`khv8jG}p8&GftWcYS5Vmm3@ijN$kgLHAG zl@RM1ll)+M^k}0_TFU?mLY$Ut0 zNlesL0eQ8S?;9?}eTyeAb}PP9&fG1Vv!f4pg`cwhb+%h72>qtGEfqW^$UjAd1dGw6 zXqb#0JnEGqaxUReyw;Mk;qKx72ql&{i^azeKu{M&__G|%!))*xABjGV91$8d zQ}-yutlzrGmRhv262b;VJ{&IC3NSC&!s%lC6*1sQg18W}>BJl#AgM&0?@TIa;%1^t ziyg-E&+v*L5t8KRa-}N49UPo5!zN0WbbtW~6(=PNa7IM9CJg65H99HkMy&y9=Nv&raeE97M|vdjWK&c_#X zuzxg^9={uDC19~nAGMB7wh}TFgvFng8Nf1zO9xj{;!DJMD-Qk)ziL8^Zx-e^Qwk>N z;2C zZDngP1t-4CZU353_dWh>Hh7G5e8ZjuP?njUpG;m(>`s%jGn7lTiq|((+}<0%2A$~n zJkG!C#F&cQcunoLk>k_r6sEq{s>`07jg`F;VtW$Vb1HS)W=NMOqA;aJI18xFD0R@3xhHWH5KVjs*$Dc!L!L}~OM+^%F5umgoeq*k@HpdErNNe>b~zg=d}*Q)BimNBx2#h)HZJAY5oXONT`G6k=) zvb-hZ`}p`F$*_L^ezIge<}wlVJs5B9!V?EfvM~0UPN8q^kk2j+R?I5ELf;PhFuRaM;b}EsLmSosZ@rb zvfaN8F26}kr?dkK(l^)YyEEa?HZ89nI<(@AFUzHb<(o=Bh`<!^T>21V0=Kd7i~3)?U@(Z`uj7C~>VAP$B*X6i6uRDgFxyHcuI;^wYgtV!Kmwf)V}% zB^vm4XaAC^=UGa?Drr`~z$)RnwJMTwOQBPVacF^+kpz@@#PMXh&r4>9|Hssamy%}L z7HOJ$M%#7e{hbuw-mzeRrXiiSoz*%a6 z5!*%&om5AdREaSf#jfOR{QVWorQ|i`F^Hzd1;Kh%k34TRhnhH}^lZ+W}9F$1LSfbzDTl45%Ps^KQc7vm!5 zlV+g~;E}^A8yRn_Aa`PKS65=?*3`ww5szLipfI9KAXV}W)Q}jY!5o962XSLC6N2`A-vJE>Om~kRWl%Mr zCic@NigJ8aG0&I(cjlnhtutuuU*(y+C zJXOR{D(fWsAu?MX8iVP@wz--QV)=l)BG{enV5(<16tEf@XM;-nZKJF^!oSPHHKNTd zP7#(K(RLlV*sl^Bg`O+us4YY+`zes?=xmh3^o$#*CjrI)#|Ba zEj^mR$!&Ae(Iy}qh`x6SMgylF5{!@ih5_ya<@6W%`kOTG)RsNREaj`0hH-sEmB;Hn zv_vXuQEBL*q{RR&V#BcN_96?EoXh$QaBt^>j>pmGgS;M3nwC_aGW=c#*0lca+B~T3 zV4CuL@Mj&>May;1=>#43akcSIl(#Tf3+(&3SCM6U`TY6cp6a7jj!1C(T5S_L6 zxj|r2HZMNh!t)5^;d1*Uw<~C*#eo~PA5V0xM#QRj!-D5yo1bG$+#7Q859@+%99&4; z#M$sp21RN9QmCUYjoUv)RD6EqzI@r^83rm_Z=rEG3Rh)ZpQ|&uHk9)el3${Tm&-dH zWW6V>n4s5VXUiPDMkp6aQPa^mH6c!=ha)XCS3|*(1jo_&EWU zAE%r!JDKWp_G@I6aPXav#bm;%Aer$1ytth7`NW*=8AG z=y1|7ewLrBIG`=9+d8bVmY=t}$%_bPxmRk-Sr|JpOa1!d)ynZLwhXmG05y|u(W+m- zE0`(`a4owHjF|^d-@tTskBa)9=hv^33((<=nZjDkw3hemwO&xMD2p?r+lQ}Y?;NxL zFT`a?BRG^_d_|Ar^4wrBCwaAN0WsWRaRmsoMWKWJ~gVOxX_`xzc_&bMe z1E2cK83O5hDkI-t?mYDy>D+E(;$lG6Ys@)2hD?Y6qAm9vv5hkbRTesx8iW1FS_fHz zaHpo3$k2`*E1S0phY|x_R^_R4! zgwr^WdHBw#z87bV5ir=~C9I6P1xwA{5d#ozQEE_12+<(Ue?}0Qdb)i#@?GZe+ZPxE z3Sh%u4)b@AVlBD&WHI)^80E16OJvhFooPQCoAIp=7Eqo8L<^%06pXY5%tWz4?xYx- ztCXcw#FT-$F|zfWI49dy-mQTxBIHzEFXo*j?vd}{hIk3lTOS{6j8&Z8BcGr?=0`JC zwp(@5wNz0O`t4|c0rlMV$Us?{00Z}pAKQLTF54x8hAr%4qLayYZg!8gg6>|k5m^j% z=LX=5<9m!CzU7OGl$k2!_$)UW*5YtxjsHAFYpEvBG9(Ad5jvzE8Lhg9x>t6X;e?&* zKh?Z(%_X4m;lV@ckZpnWh(bcM{Mvavd37C&uy0NX5(ni`L_Dg<9n@1bWxE&g06)5b z%jJZ!awD;20*7R|UORc~4{=Ks>Msl!m0$%Ej!T6@dWC?ZGenw8@FMI`pDmSJxhhX` zRs#7f$LHE(<+$n)V0)8u{jyj-1dM4d?7_%=}H@WVw-s7yuP3KI=af1Rx#Z_9lj z(jZ^RVIGmnRx}u&m5CpBYCv;Civ|Tb9bu1TJ60T?=(*kPFvGU*esVlOZIpd6%H}}4 zlw-)p^`xt-7hu6__G`*rp4)7{wmYYFDN?!X!A;kNvyCU0Vt1Z+aPR7u^XJbThz-|c zk}WAv6oUfg{`MG7VnFTXmZ}A82x$McfBs2n^1C=wD7$w6$@xe62Ba(S8cHna#W>@& z+t=s&A-=9*>jV9HnI_iGS*L$e&09XqBhA?eExw4czV?$b{vQlI z`#4r9j`FGQ8|(FU4q^ihhU1borBt_N&9yvze8F(!wf+n=q4BA__gsJfd%mLM+&nb? zk9AM7%#_#KX(Mv@pcJAWI=T^ysUqN~{8mM5&5%f3(*jd3hF)nc8g$ljA1RcK{>mZl zH@qX^YGZp2W^sR$I6<|d{{d= z-9wB-EhC%1#!dR`8l^yP+8R1SU`K^$ISP^?4!R$%C+J3QrvTQ;upDRw9vZ>iudJjdEVzBk(IzcSkS&b792_|O-^Dspmc&-q06ryRuR<;rY)(i_P3Z`Ta z`h(5dta>{)WVXLW)~X>z!fEk#oTIy?W&{UhcD}+C)0kfD%+J*b4d}zI*iR6 z&&xzPz}vYQXZkWO8~AxsFo6tceiuZO5AlzKSJ9Lz=-y}s^e{1+%2{j52TX;r)jUin z2fYL_qn+DdA<0WQ159(n<5#ta8)nr>%*Qdx8qU*r2u4uGQYy%og zI4qH`yc$=80VUFgCC^a077X;oXiN$RijqaT`O0f3%a+A}O9AqmS{#HI)dB5H^%Ujb zn=5Y5Rgeei{xx#mfu?9^w(mNnPbk@Vw+xMm1hYYU1;2VYz~^ycEh)h|^~ zE-HMfs&-^nXQb`h{k_`0tC}@&kQvC`7{_G?%4hd;RcE-z0%bAenhFnv(h2zrkDBUO z1t|1S)CS_$tQsqsRk{CXR_*QW?fXl1{I@NM|87t9|9`k5@&C^7`)gK(Q0WbSH?srY zM27;xpR?+sW1;3R2&kp*_%8^kLR$PU2&l@%Ul7pAnzadUpR*Grix@J8VG`joPuSp| zqPcalB5DebULkyX9xEA9cj!9aEq#OV{WYR`02f%=hUog2D#M^L%cQ0=J$rpasdN@~ zyK_4eY?tSoqQ7jUDXsD8luHAesTVA zpskj$tIuNoda7=3=#%vkFE5|{!;jh;kVu{F+N$;RcGOOZNZ?;5)2_fmS1Co;hHcen ziKNcjG{cRIu~l^vwDoqg3G(;!&lRs21uG}8E3!$G2Z(l zU!IJ6kc?+u^Ec-OGd>&rkdR@#S7zwtPaRNYb&3X(_9wgE*tldlAVyh$7!51;ZQrHO zoJx?fNA=|9ZFB3bRRzI5RE=JeHq6G|fCe< zLw?$Y<`E_9oh{=#2Wf5wma|fK-L<;0J7g$?jEPcM1L{0VU-LEI;MPf<-rnMji|e!t z2t!jR?&(YT#v!3WhL8klw^V&cJxfNgB#QDnm3EAv2!01`nD8%s9|gv#K<;>sRCqK< zavpid#JoOn^u$a^=RJ@|@x1aBhuHZ-@(I+-o!4W`2gKwWk&L|UzUmt$xxs7{l1g|S z7D~u0xhmB*(1+w6-K7_`;8w;%Kmt_;%N3KdFuP@*~bxfIiDxDr}%uC-jtN{b;jU*&bK$i-dn$Ze6TBL z?#qtht$$2>H2|F=OTp2HxQKWK2)?EgZYwcU&XThX&1AbSF%~?03k&fd-FZvM#x@Xg zgPi>qr;@fAAFuU1zM4bJKeH#d;&~t}+K>LU)HqL%rliP==t(*O%bz=)5Qg1k#<(+! zS*&|COwk*qZfVAcBB}hxhL7`}j=TsCl?cdY39wygArh2l3JwR8Av}SX$pA#VD0XbK3lIIp121|q0g2=1ZqvAGj|KHuEDq{b z)EyDnhsQ3sPJ9&*N1cm)Ir&>E4x!;JfL@90|w-Ci9B* ztCFDNG=!WMUD5}Vh8t-Q=2v@?nL(mcKNrL{>DPnv>YYe#f;w-OIb9SoLdOX zlbyWx?P)9?QH54B1gu>e>8>m}64z7A*NO@P?;BJfnLf8*%M$MTIa}~~tEbTN z6PFa67tPh_v^}+VkzC%~NUVR9_2k1=C_6vr=+~zhwKf=N6w>{E&&v`L>^%ruH`8m1wGr%oYP|6Iq9iwQxf${_N%dG! z2evtKs!sfcoSy@%&UphWXv?@JqH*W7A?8nQD6UxhCadoxPT%YXcd4oIDThxgGx-*$HMp0Ty~%4J)9we&D452wW3}0Qiw$N@rl<*Zk0kNa z#h(~zdwz>uD!;3IZ8`1OW#rl)ijeX{5lR{m=xQb_G7u1i)F{Q$c|i!87lh#TD|;pL zf>1ZMXD|$^1KEsqImerX%%)$~q(OI*o2JS~zMxTWlGMcFsqF{A?o!|BG5p*gX}b@R-`Sn9oCN)^@`kblHk^Y=$!CDtg6Z>rczJfaay2$Oytjn?xY%lbLKN>!vEa_pJ>Q zP<|Y!;CQwkHyITc>mf&%&cN@arriOQA0^#;O@^GD1HtB4#^yNf(47X5*O}!mwtx-9#+GCwUhEUshc@fsf4%j4h~+eLNoHC5xRNz*m0UW;3zv zZCl0%q1yYj?ZbraU()d3#5$ZGkm!$jAp128+bk+YkI1U)a zKyM-6ZQTNf+@;(v=(jkaX|Uen>j?c}ZvbL(*OUA?cLnC7nOep18EsaNWuyPk7J+ znAJN|rZ2R#;dV791V*$U#P0Jo$nuZdD9`nZO_a2o_}QmcZ%URcq;EWVu=``=^rndANNjz0-sy7llR~V~WosK< z>P;;qmo>C#RPNiOe=77L#;%FUy$_CAb>TjGAJynGWuvaZFCtHmNHS3V#=E46G(N1T zq|JT15~k^vIuU@B%oLN9MK+hT@0<|kL35n(jTSo{xfgUt%&Uzk)l5(i`HYAaXl*4O zD>O%T$UP~R46LHTLW0pnH2HDQ6P{}KLj?tG;%JZlFzX>mXXyIX$3P&OT5L{aa)dTP z;>FFIOx8bCNHqOc?vCn~zwy^N!`!RA#;-Qt$lzG)jGEaFEMniD&nFW1SQ?=;?t$ww zZa-N#x4H63f5hhLaK6so-iK~VC!{lc%0_1srnRnaw>C!em8=+r6b27AwtpDy@zc6) zC~P{nj!<7XP5RPf17Fp0Nz;DRVO``@Eq$EVN=uQVTAlG1w>){9PAF`VbwL)!WcBo$ z?6_|8h_^G3!`?i2TXXFh!dz8gBvSX}f^EV&$;E>Wx5llv@q6hJrv$Ylo_g2E4v+Z} z1%{pojAD54evSSw+VQPyRYi?WQeOo_#8|)#Z!_o`y@%wC`EBHygU-cctsc|kNkaXR zLF>HsIP2p`cGxA7M!Aq`zR-z^T4XQapfykgy486bm`h>huDh!Avw*>K$pTly^_*L1 zrFvH)OQwB^uHRzOp%EX6Js*xbUa2}slJ#<5x=NRoCSO-rzG&5=n(6e2;|Um}QoFOU zoqFg4YHJ29cQ9rO3r;1055%0RL>4Ow6%e^LeCMd5?3`zA5=99Ws$@ZW9Q{5|wcfDQ z0*O^QHH|R8aQ%a2@b>Q?jW-T||D>0$GB-;o^q%{St=&HNMRDKo+}9g1ihq1N>$Ccg z?*Lc*$DDol@E@~tUmtvem`PvaB;1=ziI5%5Qn-E#Pgz$1W;9!Y_NAELTIt!{nxIiN zgmI)x4Jt{cY^y6)cf+ov_AF|ATmOt6F`H*s{|1rzgtEe-F5mV#hzQ0p zH?+@;Ea2R9eNUJbzWc{8zuxsmb;ScYxkI}v#prSP$1;ciu@QLmy2^nq~CMYJD{j$4XxN4 zext+^O_7X{O69Wog9~61x~lipJ6a6&eO)y}wQU&~BxeGxz5J3x)WrF`S@Vu#XoP$F zz+?pFL@%k!Q*B62mo=*9t~!bXQS`5<_h&-yriKi=6LTTtw{lDM*fb zx_hQv^UQ5mKzg4~xYePk`kS9bv6PLMjTmM&I~_{xvTH<+^Q~jwpdXtR2O~M>7Y2fa z8$>jCEX&K=Wo8)T+@gFz?w50kg;^raZ(4)HUmNd6mX%F|Za4+={rhak#YBxG@vf$( zt|g0v7!{)rw;tS9=dBU-C`1=444GvbrLAW|j}Ybi#hWz<}EyYUN7mNtq?Trs{c zO}1Xf!Q8!VC`lv;U_qb~XI+hbju3&(keGFdSBeNX+G%mEt84F3BHoN6H4_&I*`YM( zo>&Cm!e-s?e7~cpOa)|O7z(hHH4ls*yzMx0yL$7CYL(~?cuuOQHZ9X^(7 z4q3x=(Gt0g2wUD1FP#2BPZ#gD^17;UbJcLiM^B|5YoW~eQT`&YD}{fY)Q&QLacBs7 z+O}G$?a2bNUI^{5^+R0MXI87r&a2E_)$QGS^AEtR#3`f6xgal{DDvB%^3(r1;1SCDt z+UZDD)HNxNJq>}^aS^s798z=5?OP|K9S()%3dfGoeQ`l>{x?=3G*U*8h-ps8S7E05 zc$%hZG@ED|R5>(^(15D6n#)KXS;OHeUf{Ztj718GT`!1&)jXS4{wDB1mUcz=3B9}P zga_@6Fal+lMXk1l;>A$SbvAAa;mhA`oR!@Y1i~)|yqVT!WEw7mUC<8^6+`l58m6;# z8>hq=7!9G=!#eMW?QF9n7-77h!z_(QTGqoZstk_vdBhT37Pt8Ug7n^uYt3!w|E3ux zI-%FR8MFT;NYnEWUA_5iTJ*UyGp_wqEAHkOb$6O3AvLgr(1o0hcV7Et&W!2=5sQQ3 ziP)oNx2Ikeg4PiED&2DKsP_HN%xV)A?}S`RD_p@^kEkT$f+IKC?BAg&c&_9BWg?ds zKeYDDOtI$442wB~d>`1ouVItvm+mb9t@U<&PKoIX?C!xH5&-|wpbKE^8w|UKra3Bt zzls|^0_BDg!l*WEF2;B_u+N0K0m{1Z5ZUw41_=VX=@8u1Y;$TdiWZwfQIb*6Hy5Yf zj6tT%qxXj5=B8s`jAp7#Ezoa zFM=CN6s6C!^Fd&N81wd~!*~*?mBT$24#B5ow`2KxVvzQG6~ucL?8%@y5ky@Gc8I~4 z%w|8?t6*f75$Tvl`^4Xq8t3s*Er*NAr));DpcZnFHw`f)$E@{V1xU|BoUm-7-JJc5 zV(^G{x)d{vMIRX9_@#k#k_GWG{zHMAnC@E$BHlGNl3uU{LQ$v}>x&no{jOp6p50|N&5c$Z#{&E;n26g3t z7kc2-KEp~uXn;m1pTL-=W7Vm!r6U#i&a!P{n1>v-idAXPsYG0b(q=$jS~#Wz<;8+3 zQs8=8Wr4w1M>b4DQiY|J!%6UnK~YpN)RqI&6IZLqs_o^~{#rX0Q($WB9m>2N86MT> z^eU(f38lbt@Ny|!T8i5Izln#$YIB z2I@qtt3#Jz2J7%)K(M+FMcUc5ai?S>_Ls0#n)fPy1B7kmzr4zSd6oZf9KnCyr~HRp ztRqD(OTf3-%TGt9w_4cFBS*@#gPgSOi<8C-N z9*%CX3-@X)2s|(~u;&IV&nX!bvMfj@s%S^OEAb$Fl%ip4Y!mKvkI98@PIB!I+`hiT zv*h}k%R+p@S~BtsUps5Dnu6FA$|B}1$f5Fv$x`Uqf*fEG#WYP+O*Wv^r8Yk&=hc0x8 zQBNmYm0y4?c76!WbUG-Zs?}YJ>eo{2V8cQKo~M9Ne_EHcwRv3jj)08=2~uQ{-CG(o z+u4S1oD(mhk>cu}a`iJfSeXXNu?bC3{KiJB2I-5VVB7&34{Ck*$yi!OfjPdpVGk_C3ZS>EY;YNObzU zE;#|N2Go!bHHboD7#7A8#bM#U&4PJ-;y@DWkkKDs64oV(uJ$V|pTYHOXhH(3;#%Q= zf2qE~YRB;tD;HQZ4}WmbKY6SrVL%}+uON7c+(9hVFjbv~K@z6t{Y!~cp8_h9v|w%+ zSqT`J8|Dqnr(+*}8knR)dCkJ>+>N)N?c!PICp0gn@+086y>DXs^HeT1{^Vt&cPyj) z;$&dP| za~{tRy*}&y^4!7|5r;4TzQX;@gF{=#~5I-B`E|m)1ED%XA7C{4z7qiNDOu33v7GJCG}46y}n-ie@)nx9cdXiwDAno{$-?mvc=;o$>X?Dyqb= zTzLEhHsZDyu<6fa#%FM4SnV$}lU)H266%J$T(}ttwPmjTTElg<#>-fs6g*qc}zz3c@j00o1k1gPsfhQk(%1gcnyPW#po|$^suaAFZb$H z*tR1#6a-HoF|jb+b8pACn%g&}B=AU}&irZWQ43m7FS1o4)JbC_)uZ;u*(&hP4`3Wn z12p3zR_CcksU;_Ieb9Q1BegfC7du{?yA{&j!&`2(NF}OGL6O)5tvR!DK~`y;&L>cT z8f?po#TRddok-=PzR46!f&``wl0u_eIko_O-oTK`vtx$CM)ylGreW)7jBuE%gb(fR z6^ur|v-A_eln+00=pHQHoN|8WeN*-?6xL<7>lX@(@*+Im~k#kHF2ysm6Hi}IbBUXFXagq$adPvVm)_3M?*c4Np7x&>r*!%x*7e3b{yKC(v<12JUUD#vNF~DD z$E7E{6rtDVAbBnv2$sNNshM6cbnmA61u+G|0RB4q1Ahg#&i##ljr;P_|5@gPryuQ4 zqQhS=cx!I6W1qgY*nQ@RIg|L=ql)lzy4|%i~VSf%6T14hCT4%LS$dpgX~btUY#Ry zk?(1*9$IIro@I*`R_fg?)GU+`=7vp4T&YU72&4|0xOoR`9=J9QLYmFwFHhPOMUC^! zRKWsEIZKtkJ*F8kIgmQ|MT@gfQ-eEGe?qVC-7-@+S>4YvllH3jyF-7cq~e*-AZliG zOxk646m%fG>u*18Pi$t6pI{z!#lhx!4Ku?~%P~Tp z2McI8$p#4Cs8mSw2v|?dJCmA<7iQsSK`CA&t^ok+ zO4X>8uAXOIIT0+1{XFZ6jsEF;-s20kIiSTCV2FwI_aBmN-BE}M^y5B3qX?Fi3c2(N z5lczAo0b>MLuZRhRbr8MVvz&L)R&7li`&$tJBr+78@s~Q0n|15Pt;XA0Ds>UcO(Ye z5`>>?Evf}jS8(RNKuxIz4w?>uqw&KZMel^TZkpEIO;K3FCM;=7ZcOR7r>etG%QVeZ zUm01oq{KaY7vJ$TF1mYhj2NMI!l6RVr^3M;=U7`|Gg!gstXMv{0aeruX5ql%2nv9rN1h*;J|J^t3)6tO8|1Rtw8$H&vz-1lU*0O0;m@Mdt(+* zkp=9A0}Rf;ny!FAC7U-06nh;>o+pflSfK@ra>+N6!P1P< zC@4TB@;5HXA|%$_x?T>Bq^U1q!h$*@U<{tJl4&=%Net0msGv{-ezcF3i*RI_b&C=G zeB1D}(l~4`e7{|Y^taAxu?(VPOh_to>GjnkO<8!c&+}wpO2(|z2NK(J2vMk=_hrWB z4ie=w`CIHwB7HywSZH|bGf{L=MLV{}x$|g5AR0ZY%tU;2s!5#p!zob*|R7F&#eN&Q6XTiff}c zIFZ{;JJ25IU8~s9iM3@A%(NO z5MP$zd2pz1I2{&0eU(Z-$+Or0Gw&-6F14%&^1eU^+zEnt1W=Y%(36cO5 zYGXiEzAiFL%rr8v8|lXzF(39LbkyjkamtfTNML;~j*qY7c1l&Mi8dwym-F^;!;YR% zZ_pqA74Yqy5BTy!!lO)Xrg_6u!2$6lZ-VxCvhuk4tELioA^z)AsC(yjHO|NkX%cn# zIwUON-vxYUr~&f<-?}JcL7@Q<@C7b8SD!WMT99AemX=N!A463{0TVBF?(mi5fjcCaZ@MT5&v7ReLWx-q@ZUWPJ zqZXRgY7xkSA(goTw@MIWyP45g|#wS#VJa1gF+@v8K0YQ3BTA|@-rxZ^`g8gC#-J0A2R z?e3!*6k5lMtw^3!Jtx{@Y}z38V3n&zm=aMIRv_eJlyLxsk;jy?!7EYpo%hwqw)%~$8)~4!#Bo0x`J+DiD3X3&|`RyQ8cdlmTQK_In|G5dC5l|NEVb(-Dka?4Kk`HoD z<5^@~leHN~S@`Su1?1O)NY`ifrJix+=uK0jygfiweuE9 z}Ivjlku;i826c}-AM4S@B{rkM(jhgn%kgqq21xJ^SD?t*mg@&=0^4IxX zd&L;jD6gFcnfYm53{BZ}oFi80xa6XBzX!sc&+}t@VSYZ!M)d&;{kERsfR4_a=KX2&h6@(%JZ0F|<^&e^^r3d0; zJn!5#mDc=>-sb8lOY+>>h!Bnx#_kb^#>e>y>JLx@Oj$y!dZLHuXi;|`7I9LKsvwke z@oKb!Rk6)VyW%#IjY;|45YsbAHpIlKOlT3>iV)58n)Jv)k1T$7zI{SKwzEN^1e=pu zmadx$B-jD40mN)LSWzJWv zEqc4;#AV&|EuNfY5LAr~x`yXAezN*uy1UtI@tO4EzI>BDoEIpp52il@5x$H-I7gcg zxX1jBqu%vYaOFYnQ4nMmL$c=r_;(>gZ9M`^3LU&=54LB`mWYs8K2{}2*yf>R6H20D z(#2f<(yIka1w^dfNMT3JfoB3|kkX03frI3Aagq)qmWbsW#dAW$dEnR_UJuC;jKTnH zG=8erC=?4I)y)7>?b2#Lk5tQj{)SYy>JK$GDE+q1rS2xd#y-`1M}ddET$x)4ITl1j zFRx+O-_M6BW2rDkEE`rfn~R<^74zNq@F2=F=U;s}QJ{I6sG23^L#O1c+I5{sjnZQv zCnDNAi}$RN$=9Rs@^aaRw@|Vn&G%kVFYH5N`6RcIchER?`)%bQl0Z+NvnB3RT|hjC zj#BX9<434iZPZS&h>h0ji-K>Pl&uBUUd4x5z0mfayuktabUCk{kSEPY$F_>{47xu9j2Sf%v%u`8ZiMGGhE z$JTp0uIt(SZ{voh`=c)>{pZIGZA%Y8v#cWH!+O^8Cx6)P%JlZSt|PlSz;g9th~|U*xq{ft)7YFVzk?1 zDHa$qJOF2*-k|KL^MP7^II=?Bj*D~n=%3vWv7OJ;VprYVfqmVd40X={CJYVl9o zkZM3EnVJfU+ENg`i7ZN10rsJl!B!Pu0k#@Bu_fapypaO|@M@ZSP71g9MFARl6?yz4 zC}{Ck-3x}(;a^;uw4PwBJN z(uJhB^43!9BCQ6W><2>2LIiG|W=XdLzWL*}Cq}UhT1-lQ%!pB34k9+ATb0wQHtQ%_ z&>4$Z!gyXIis~xG6hG`sU^Ke+?Lb zBCd>I1BQPq;sOQ?|7jQ3f3=7U;Gco%LSw`HP@%bb!;vFVz)%60A6)+dKLe#)0Q~${ z9Txx?|6INP`Lp2b*Zgna^8a^wxlpHoG$c^OMO4xTinxr;eiU&Tq3XwHh$Sj!{&vJz zunSk9Ms|}3eT-;ueG4vT$qZW&Y3hqIGvmQ?`Jk0;#5du6g_uNr79^++==J#Q9Zn=)H7lZP=NTV@e_CIsk8%^dw@PQ)MS<{wR5 z8GsZo_-~ftxw&})L4JOIL4LllprG)7y#Vh2S(o|0YPkLz1o400BmW~}V1V-{3Xj|E z%7ZTj(#?k-_iP8z=hw`cFe)w94b=~-ngSi_uD9nq)O+VU)R&=>+I$*aw_7ySChfac zMNwf^745AE*kA4@l)d9jUDvHSsL@U>*e2VDv{Z{TR43`%pwH+#@}vr_9XeOFb!BGm zYjf?0R~?%^F)BLIzi9;Cc`P40xYheH+m1i+@K~Am=~A6q*lq(o#dnvlN@gY$kS!M5 zE{)r)Daa$QS)W$>@^0=GVi_1C0{QEk2e{$t>W(=g)w&_0aRtZo+r*whh1;fU-smzr8~vKpkk?xa?E>3F$VeiwUvGqm zLv9Fh?kOah=u&P2fMgk(oEteJAceD=(K^c5Pd>C`CIy9fMTU(B1{og)&;cDy?$Exd z9ram6GyVmLgX1VP(V|@fQmd5)6lf{7u^4N82LU>uSO5f@iy^fQo)@}Bs~L=)OPm42 z&;k*KZnTu?3Mb)99JJ8Q4hcdynTvL7)V%SOocj5IJ>PguvM;&_IcTPy8qT=mp=KGZq6fM(zL(K?4o!M)*kG46rqxx4{0e@Hp8%gw-0|h?yq3N~W|aFE`sG z`F1STN|k^1+u1xTW)tLYjdboqA<&5wyMq&@xF74Gp@+B!gK#>!x$4!S7WoP&wrqcY zakX?n)4QF-gdgafVi>2`iDg8gc=F|?Z1vkucP1E)y}n}pA)YmNOXQ@~Bxne=MWmcw zuyh8yjnm^{Xhkdl>({X%`TF5@;(SxZvokl_3=C%Yr0JKWo&?huo^)e>rn>vq-?x7d zqcUTg2X)mSProkkezG3_>g1>8zJ6zsD{wPCny;~M`pCNouZI?IReg8G{der!QBCjc z53&v0vtQr-eOKnE`%B?Dvnxxz&-y&wwb1{|)YsmeuS}ZCxmT};xBB@f1|$bWSiW@7 z>G8wyh>oQE!-UH*wS%Qh#Yd!5%mWcTt3Qf^*Lq_b=2A08(TjJOWsX` z8(QUV2C{S6*Q6?{Fn)789kBbkMwDil<`g7(0I>Vxn^fl-Y6LiTwHR(cu;sxLu(nw) z*Aj%o*d!ybg9%LuR| z7lwQ9dJP{B_uBVvWv^*qXYoC^2wj(Ae@AJ7$3q^*H&|wrNVHkiV6qo;VJC#`ScRHx zQZQi&w0DXNb!w$p0XP@1@WFO~Z+~Dvfa->gQ>o!FMjIbma};(kzueMS*sCOdWY^|V z8W(uJZp@TYg%ts9R-8E(J!LW6Z4VLhd$(YNlo-NRIFgrs=8^x~akLFLEFLKYgy4vP zafybnWkXwZhYFbjaC^?JHCwAG=+#8NLS>W82++xtB(pu)725!Ljb1{PCWtC@pI8+v zafww9Pea{HrihTM{S6gu{1%ecaM05q-R<8V@#0KJpS$GlTm2fP#L81V5o^>&GYASR= zh1ENU1tv(9(><|JoALUi>g?4KDSkyjXX!42(lFR z>U&9eE}}2E-FiJFI|_t3D}&*gJfjgR82O%*EWe(s7z!#}7AgZLQCj`S@A8;Q4`DnF z>ba#8c{#g4=;ZERb!%L=7D^6V#CDXG>As=OE>+c|zLhQo=Xn~PP!7~$=*{VmOs_f~ zrL+nKCTZK{Qe#1JX-;&LZ=(y{P0ESM2d>4Q^A!&J!j*?_*~(8bQDKSZ8`T!$}zdsL<8oz}u7C|B1zt(!>r zVkwQ}c(vdKCdb;A(~GGw$@P%$Q|8<=xqf(~&EeQ_VlQNDJz`$#e95N^w%OSJo+oz80 zNLG865&0}bg@rspPBkVcBW$3b_ht?@YqBWlATC(&!;be2gH~ZRDp;O?9iJgz=gnSZ zDMndR)IEWOvJ{@ivC?Y;BiY`bY^;-e$k(nY5U@!U8~gAknu$#yb5k~R5PxVCyo^!b zAw_O(%SZ+lBvVr`1!>&C+%>Y~-c=^QR`87{f;{&5;8fq?qk?vhlo@0^6 zXCrjOqNHig=NwD4NF{xfbYV@2U0}&2Gu*&z31gyUbQXIxs1z+Ky-mR1V5;73E4??M z`pX`5p0`K2fF|SpOOySpZtA~J4gGUG`A0nY|Jyj_WhRpM7SA_M5jqX~w*#xc_c(HZ zc4RYy46nh%PX?>3BZe-awg7vrFqdtmBim*}^y++pc(P%bz9SdtkzWl+v5ndZ z8J;u#R7=u9y!S*dSNV3$T^4)4@yU z%)L7s00p+^8sqlV%|S3M@zr|-9(l0Zy&hBL2wL0^R^UYT3=*~M!i0pH*dDj&oumTs zKT59$Eh(WfNUj*Ap*SvQs*DD=^*~sW{B?TKR$@Btg1W{-!!SpH!2_Hu4HD5T-wm)m zMJT}blosHP7^8sgiKRzwReoFs*q*UCI=(imldDoinO(z9f8WY8^z^3)RXq=I2MWYt zAJo`>dU6;MXLT%}uyfzw)6DeKHU03k^jLDEXSNuH4WrrUVDn=^0^KZ70)%8-GBTj- z=|dZUea~~F7p&3^mw{In1+Q&5T1xbw&3Ol?x)k&!2azKtE_4QO9jHx~!V@BUkO@a` zn-Rj5EI!IeOWGy`gmB%?u&vbrBM&z1Iow~_u597H6OkYErK+II!X0j)Yk0gCA=K?u zgK_JRslnslgK3>cx>Q$Ux<1HycZ<13KOw7aZNGK5#T+I5dV_1w6q z)pJkZa3s(SUB>G4*PT|+wwpU|INeley2XD+`@&&T)GX6Y@8GO!;lfwlEAv)Zr@NPV zZ%$ubwc+4rTIui0)1Ni2P|lbv-oEdL8!Q-9p|T@@v}?GbK{OR1-O77|h9@Fe?JSI#`NxYctYK|eis?D!t%I6qge_J_N> zdbK??lGEzE9xUz(auHtcnqxjQVto5L5Kx9fB6VdJx{6u?*wJbl9H`DEY$mb>V!T^OkA~mW7{pD^Kv8B;8PG@gPdem(NKQt`_qDY2mc=UjgXaSj15Es$5*Wv7Txbk zv{E5SfltOz^9t^}BDj#Y)3N8E*%9C+rZ{N825evpMk;*kTydIy#V%}-SLlKkc;4J> zu6BlByPqEDZ-R;PZV!aKBN4`Y{9@U>VDgIS+hv{B-xZ@}ZMsHx48nd2#n;;^LDJUZMd`yYfxBrg7i0LyXaj zXQ2c!2-;l_f>A&p`y$O@s0PsVi}Ds2_1tm~MU^>G;p*6jSi1b_>Ph;^OO8?{5$8IX zh=u8z&zfTNhq4SPLYlLb^vQ)vn4yCE68S1AC@?WvT7lUV1R^`0FVIO`p$>w;Xt4~X z$aV_F3}fw0={23>@;*_)E9yB6#p_9MM!YL5d0?gKB@P!W-lwv21d1FI7kW&JU6{4M zm&BBuaIM3zmjte zNqj$+n4vhkW&D0em=fDzAnP704$QqhT2v!zDPzB2)`R+Tx%LVy_NJ8cgxgO8KR{<$ zamt&t(d`;sO=n%5MF!lPZVC|87bz0$HKBqx(lNn_kP84{5l>Y&kY`!1%#T8Qg6T$c zbPmT*kBuxL$^_sEvz400V||(SC$(e`GtS+)JH0$PZF(7Wan0>#i?GL9c3XjzS3J3K zdr`N>7-!7O3WCg&GYlTOCFJQ*5!-6O`y(mn)ElxrTstlD1Uzqb(+iMskU*&?`d85P3!lsSeLw$%gPUok58AGWj@}It?od z0w-3p3%xjC6;AYb@aE6y3n~Xc=xk1IH6VUkO`N#ipuyF-f^FnxPM8Dw=3~LuWGa_e z5Z53@>U{+Hq$KHyk{&^zsa$Rq8>uBxV@W*-h-@vJ%~mP+j@@K=7w?ZeoPHYqtNnW2 z8;K6Q4W?$G<6~$T8E-zhZrc&H*XkRp7j5_uwSMXt?%RjlC{uwAPQVZ@cTZdAP$QUp zlIz7>;yEjzP6~V{=ew4Cg7OFnc_FcXiWw>h4YCqI*dS#9EuJ_0+})i2vid@TSwCu$ zV$xE72l!#2(kQ+2+I*SO=?S32NRXQ)EZ`Ou=9ZM?ZQm{|6BRYo?+}aY+S+y>Ju2xw zeuUrq>cWMd{=V}=gMAk-4qP3%GBNRBY6{?iU(U?De*5;#|MfRm)OwITA!vk6<->HG zV2)`(MNTf-xMV;|;Xxq^1Qa`s2g@rWs%U`TkRq8FLu^e)EU#Bkg)u0RfZnn5aM(Cg zDfS`ta+%4d^#oi~E2GGMNm@|b5p(Wd)-D9>hbQ*~!v5>Z{x>%j{U5laf3ehmH$s7e zrT@FP23(nE(kwPymWx!g0{y%)6=2!9bOww@=u=^ARG5GdT$#cE7q~LC4yvYOlFi^( zVH7zW;L4!Nh`8kUjI&0=g%wPyng+9W@hHi{CWHK?YsBy-+%b<;p6=I1eo%(v;Qh@{!hN4zidoU z-ajl9a{N;=Gdm5reBJqyX4R(t?IwQhq0eWGnbnkt@+9*kt(QyefQ3SX6hJaN1Clw! z50K23K%fmf|yli4Pp2b%cCs7LB$+TzX>EcL&`GOhB)6p(Faf?hcd7iWCj5 zOi?Hi{feaB*_k5S9_b3R(vgmuBQY1m!Mn{c0i{Hz-7F!EBL&y7TCjzztc+=J$M1)d zEL@-Wf$#T~A)R{1dYgQAch8AlI;oHFG#_}{XC$2QsrM?~)n}bR6ttKlO^rkNi!ZhK zZmZ!)fqXN!z^uipoVGd2^<$-WlUJuyeP<9k--yhh6cCjr$3lO9x*%atSy3>7LY!WQ zIu8jD_5>PiwgS1RZX6`v0TaE-^Py@3V02L_1=6$jf3WxF;ZXm7-}h(su@8pq#y-}N zB&lXBSrTK7vTq?YBwNbNFpPcAC^XhcvP4nRAVQl;NUE`AeWhO$k98IfJ_Lksu!Dt(=}Bgn$#kcM9U22^HG$pr|xtx zSq;0c6g=yqHZJ>lCYi!~DW<*`J1FT~%i5fSKoF)uQnv7IhAzqle98#GOap1xI*wr2G9}@B8Wyz*I6Q)sEUPy^g8JTTreQMpZyl&r)NQ0!Yj?|gqJ zLQM}`)1(sSduGB3b4#rcSDn=cvcP&OU3);u++_|*=6JKxBk#Mc9g=hs>??C4z7xyO zo{WEU-u_;~TU$^vJGR}obAERjMh7Kx@x32!dVal6Sn9iFm$=+6pS$*MWOMV6fkGRL zVqcA3)q{Zt(^*#PCq3Hg#$shJJ`D)Vj@FqU&HW%{JAQxVw4a*=p=K!gLZbimf7wJ;5GDPG&_AMZQF;lZ`TWdt`X1r3LI70~eaSi66G z!=MT}ljV$+>6UJ6+z^eoYM0n~STIBwL&CCYq;%mWoajCRq4IrjbKlr&JIEu^L*GB2 zBk)9#cA59W#f4GdENb)1wm)_=4x!pyuFDr|G*b%#B%rcvc#Z@lQ=&KB(e35_%#f@` zt|1hTpAjkVghmE`)9`el?=Kq`S8~8<`AKSOtF7Zw@Srr?vx~lE`h1XoSqMg{&H|Zj zlp@3EzjF>l6p?4sl;b!M+|Zs7F(VZ?ptSj`0vp97m=3JDGmmxfX_Qe*x=Do1$0gz3 z-xY5=84(x;P?~^!daue!oL+|ZQP%5A%Fc>D3~eSs#Js2v5AJ^!HG=8}>zc#yK$>k9 zP;E2{ce2kzgMZ~Y7(Az3q3dYUc0NGOEX2!oyscy)ENJVsCr+kpq@WgC@PrXT0>4Z{ zPeZWB55Sep$Wm(1+nS+-{`-Ptx+R&wn?Ko)Hz%U@_+G-6TJ>8DShCc@s<3&Zw6JJ0 zk5eL4KoEaeTgktyC0hV`!+8DR)cE-2D+6~uDML@AbTJIaXefW$L614P{gVsVHwWC) z9DaMRWV%#h!UGu=;Z&jdtcrBys@@aXnBwEl-l^7Jd%iC{reC^Xxgb_)VdrK$cH{Gp zZC9>yPi#({$Yg&zaHVJE?#0Adef8h0ZiOfzAzWV_niav`TIvi4%3QTpAfVGQboD*U zXfXZ%MueVc#KT!&hffu+G@T$E9X)f#HUkf}Y6bW|QY^G)Av}7EXCDG+-nh%ylqEAX zCLhL+I*QlWH3WMO_>U@(9d!wCbOtG17PFNfMSvCgi_(8_6*|!MeMvUHJd!oTe0jlH z=Q)M|mZ12yQ^{hld?qg0EjMp^@WwV8H_&EiA;u`1-DPVrh-W5aP1$J3 zOPsKjn`A*BC0)Fm7g>zybLu11;Q|y<_SAlP#~&b@0wJYXHDPxtz_I8sO?5-7jCZ4l zA=6S%3Wgw!rK4f~w-SVJ#ZdW9?;}IzIn(Qg$$%Vz;dMn~+*UW47+|Zeo`@5%0Ei0f zXa%a4OcfNxg~?R-LH20*@@6vG2hn=L+L$V{AO+1zO<`#9F1kRuX|I5)Lf^T1PNqjnmEgFp*`&NEL zRksp`wr`be>p6xEem3Lny*4SfnQ0;qN9N^0qOU~3Y({+`dsmkrb-Fy5GB)NkgNl%; zJbEdS3i05k)Sf3_ib3__ilZ{;1oXe)&M|pf-X{;%3LeI`SHfg5J05Od-@Mqgn28Zt{1!_mxWN!II>I zzCOY;lzZY-i$M{zs_4%z-jEpjHw$B%NxDWa-G_0=+bH#(OKK2a#E+R;o0b|T;T^G> z`Z-WI3NOM#PK#R=Io2s0$4EOKoW_F_uHf(_aWS!YMo5YqgFhd%4jCDpjBE+vnob=`ueQoy#$~WpF;(>$wGtWSMj4G=K4Q6 z>%Tx}{U4F_KM+~}^3Q)Xmow#G7US%~qA9Emo06MXWsw8glrpa)f7_Hh1pnBS1)xoN z^N&r*3)+{F0$_>k+idQ=dg;8V0I*ez)P%gfV?s z2ovR%&3_swg11m@sX9kE_sDDVgkB(0&0b4)K;=$+_lzZMil@Xq*3verAL@yx z5yN=fz`&?-+mbXXz7~?XXI(ZKK<5^LNN9v^o3A2+!jeU1$V9S*OUEJ!e3?a{0HT6O zsN$}e1%mtu@{;#ah@8(J`K}O9hl$Bl!JV{qX{UV-5b2UG=tT(9NlM0a!p%J*#6whBPS8Q|T(x5b$X^&KpGHFFYLE97Ncmc^*A*EYB#JXfZ2(rN$(KW&keCWdOs zcuVZ_lwvgj#+6h>g0iwiL)-EL0O#svcnX=tjRW8*wzsedG8_tNiaFrGs6d&#edTuT zWFC4`ya5|2m7;s-0oeq0M0tenV!cilw=ms={$CL z=$<2Oa-)<@r72PA!doU{y-yz40<}>MD@&z_FZ-K+1K?tIR+XD?S>KCFJSY8}qGT+o#D7Fl3y5wxOsyqQsrd)h8+6)X!}@N(5A@q7zK zhU1gmbI`F+ag>fp1h-mNQk52b(vUuZ!s1rKLm37B+Y4ok$EK$v72 zozgFF@Ju80hECnY!r8+aQB#lMf)9`}RN<{V>We;mz)ZLyYJ#P4y?q*NtSH_OE^i7~ zB`ApbAFDHpl7T?8bkl8ZiFxH^Id|2|S8gdd!k8*URmfq$1+^ zJZ9I8fv!6pnu36z;2|QcM70%`1PLo_O0J7h#4$9PY+)ihL+vsslkhYF6fPMD0-A=- ze(<>qfPfb9fhbn7+`Q8iBj1`5Yo}%?l*wGd2kB*nWckY#j=7jpK$%1kj5i`|!nM-S zcYH;6uq9DL!Gni#mYjlcun|FBx&@idliiuDW(+~?EET~Pf{}DW<*#%DPng1 z$<`^Rq5)*eUV$Kk8ue_Kea8=s)wyo9@X*#uO`{(Y1@HyU@60 zc&_ec=}>AwTXp*dE9Z0WdF0ENq`zqezuI&4!IXJ>T`@Y;>G+k0Z86a~C)TyO9-2?b z9$dbBq5qw&Yva?DrOQ_my277b{F9q>nXbu08mg(d+jPaJCm#W)>raXP|{i$Xz7P1s0GZXG&m5t(jkc8~6b> zj(IVt`Weu|!O=9(q}6T51#tTywvXUHCVL@+>9i9B1)fa-)_hsGEcC~vOLp}$z+NMB zQH=-bI@9k(li8G**+DM|ee3~GKDFz zQgC*0G3R^F)UJT>ZwroJUA|bK(mtZ9Yi!luA?*{VynD9cPlZlviv-7*3?&& zUelL_6}0&JC4sBe`~R029}M6qM;BpkFzg^J`CF<ktS!(G*^l&N$}E>4(*zB#czva?Kypa^fN=~cN`hN9}{`7KPxo}5>|9R1@s zXVY%0?0gKbmC4ib_UGG0jCI4nXE}o@>6F1oM9SUn$NR8=dgRlIM-UF+Ho}h1#(ziqxt2)p)sSEs8-j3$|&q!9%6> zCB|-?P_H;eqFy^q&0dZGOz4*F$@68)(&LzKqJ70R5pL%en zHF7!!EiRI&w}oFpCw^T=P_C*_IxQ$2fs{5nrMZ@Jp%@Tc6<$jd5o7|wlmD;|cLLi0 zEelX!bpH=b9V4Rlb~D|bwkH2#?GT-;EZpraJzcE5-TuYqu?_U!7aV9G8e$(7<`8kj zksRq175)F(`*BO89XOe~;Q)E1jndAu!Lc|> z-)l<$8#cSQx3}-Vk9+jrSk-_6;|N;Ds^zaWEmjyxe;Od6TV~Z-HSmXnbQ7L6OaEi) zwABo!$|+TVvuU-r^VMB`$HWBPW#Ym?Q^!f^UIihUckAlW`Ulm9nTnnxV>Mt*OtoL4 zM#p)V6zAs9>lvRY`aro-XSgh_OrX5x$PjqFKkEg=7gG)!kLJ$-hXDxWZn0Hqz2KK}9XzlZ-QbH%C3{lWr8enmRuK{kn2pi!`XMFTFzv+0U$+ z8_56QxaUmE(S^In9ZBQ2H|f8Y_KE1V`g#1=+w^9Iq?M`qv0A@=v_dDu_d$z)qZg&8 z>9Ri5gq^egtWQMx$QR#jRt>W|%pSM;osg3c))apy`t??X$ufVY$mY9V2(^BQ&b;X@ zh-6D2(vQZCvenu`iJ2x0^Ef}>pUrP(35#1bl~ZV)BvVytO$eA+iJT}v`YQPW1-Gqe zNs>XD_Ce_-Qbnv`r271c{k^?p0^em_C=p$JM^QyeWE>b9f~Ry1u3EN9PqN zsViSV)R0l5ts{u|R?=R{m?z6~`%k$=v6ZtfL_uY%Z~#olFRtU3rX<6iL_ew>%XVM) zvPPP1j({W&q_>z1Pvc{{0{NxeR9A|FmC2@e)Tegx$W*s1h;cnnHgRo4&w1qYN|Y@| zvO~R|)oIws6RW=5o z!$kaGC*Y45=xhQNz}55K;qqU>M+K-fF*t7u6K1QzB|EeHdN22RWgQ&6Q1G@jOd-sa zU%$_&&2#vg9LZC62=gc%akiK;uA5o(a0ph6{lpSWLJV;QTyK7?P~TkESiinMdQD^i zqBZ}Jv46Ua9yz!zfWA7rMg89DoaT1HZ}YnO*56*LS?7FP5D&Tc?G<0V;CHTn>fZ0K zhum|%zj150_x-KZ8NnZmRrh=1-(B|3IsU%z!@VDKJ->S5mio-zB`mW!|b*DAFN*ry?VOZ zLmgiye0U{ygI@KGS3WqOCict^d6a=ryqJ7J+Ju5(6H+&iv)hr+08_8lKCK)`mNgm$ zNn&uccqKI57=aS8d3V9RDROn{3*cbceSxV@6{|yOd>uxEx*#KV9iyYS-v5;8U6Gd6 zMqp!8@KjAf1C+lJ6snA~NDoHRk>)gUmC1*At0k3v{0-t%3W3kr;grr8jTgf(Jr&}mV&v22(v-!Dlg}9vtY(7u!497#_CfRx@?c=biw!wpR)E!H4CH&9< zn!$O;XFfuWEtG29F+h(4MS61R(iq%X-(`B>@i&jox+>vKC3$lh`nE_9B99%f{^mjA z^e@vbRx!R%31_bBa1bU_+JG(CygHyaQgHjJ!CQc6%9e_T@SE6LYDaYMZ65sC_}I-h zZySERwOvVGx{M}q2HmTDByaDWWWlzK0;ra=1CQf9NP*Y7x|z0HUfO|ZUxY8lZYCmm zxV9;H(~22;Q%pY6lj@|ue0sv))qiuMEWvy2<0iIEd@{DIFS9z%+^dSkH_nanKS2!e z(#hl9b%!NxQf8(HJOHF``iY?2%r$f{H0vcOQvBLJ@dToibPKdHgp({04nY|P9o5G> zJ=yR2=r7Mk^1R#}7}&vrl!D74;E|bX9t6=o-G&_siaKFeYlSn>Q;;QGCiV_hyaJEZ z(}Ae_$t7>f!UR9+grU4;{Kb!B?ukcJvwXCmP&?V_Lp|b`TO)6WZ6lnSnq0W-#(t-# zNZ6~l6hjfoqa{;;S?j< zeGP{TQh*&sT#2qc+L?7-zPsH3cK~2^YgSVa^7yo%$mqz1 z+6kfQev2&(SkcU;G8|-GELyMDg4@T3CM0Xw0m+^x~MB3->{ zI@j$3aEh7C=cSh{9ZVHd3w}}rZ_QLKXmbCb(5 zG(BWW!(Q!8J=E$gm2}?2-}^M`Ez;v3r1B;!KHf&6YC4l)YDT@&Q)b45;g}P%aD(z+%4^*t5iIINB>FX+-EA=}vPpdZpa%eVD zbl%3MX|F5ZFG>F45B4YwR4r~#%6r5K9%dS3X69W}OsVkvcS zBLBfei1;l6Jw_s@S-NFLT!->lppBEMPBpF3nO|1bSO18%aK;Jc8ddXDK}lA|x3>ku z^ds`o;e3>*Pa@2&PMnxAA6r9iyK?Ma?8QI@5jgLqJm|GG8vUSiB_zy^TpjiRDqT~x z$w7jsr7tH<0P$1yC{|zTd6*tr+3Do#E&^d@<>3{DhfrXk-YF?G+{_daSYl9n_#(aR z?aw}z&(}}^+Y;Dh?Csnv%6WF!Td!+MCH}q@Liq zUWR`1IuI1yEATl`J#5pnwLV*@|-cp%^~}Y=s)OGe$pKotg#w@ z)5=l7FJk*~ylZLvYz(g|4wK8~5sC4`&IH4I!)GpeC0k;=zlwa`O07zZ{T8(2^drgg zP1KXiUi^_KtFN3CHT-5pQQqS6m^(kEZi4-C-{Dx~^LqRnJ<0YpaVFIEQIJg)jI#|q!1F>y^CEz{#-MBDI3?$Tu z&O?Ev0?-HsLy84A(oGLz0(=CxDJ6Y>J>>mhl>(|cFTfs*maPOqP!RY8r486dM+XD2 zf2wRdF#wQ?QL-tE8&&vS*$TGTw^m!yWp-!CNJiCIXRFwsFJpoW2vdc;M^d(5Qtdls zczE`X`cXCel3hvX`8CFrA5XgzB9)B=s&&{KL7=^3P4$T z1pHG8{{D^U0-|MqdieiVVh^=c_NV=RB7c|If07t~_wcR361xywZvUsmF46TL0ZVLf z)X3;4SYlrS7e;_3Hn`j#EU~Q$!EA{3hDZN+B99y0E`>fD>1dp2_UX@%-Fx{0nBwy2 zyyxx9O;4|o=SIr+Ub*<}THL2M&u(97esO~W>1>@Vhauri+pe~k zx0zcGKflv;>DB#WrDVB%SKD7dszjmP?q2P9JJI5QyJFw9%kQ2xDa}2+wtbZ(Y2;{n0$ad~23|oA@Is0LEb#LEeh~!#$Uk00#0JO; z$o#wS+X&S8PreTV-*`nlKvWp)NP<6)eB==L?kG8xx3KUM#Qy&JpC;ft2t&wYL10im zDRU#zOmB*yqy~1W#kV&@RBum8oWty%Oup@@`~()8lCNU#_<7P{-ljzAAk9N^<2@Jw zxc`0+dVn2;3&1%!-Cde^9Tn}ZiaOghwDq;LboI5i>+72s=>Y_UTb zy6_p0#P!TETWlotOpUjg?c8PoQn{i8Pdw?crc=b`{gFHU63mS3YIJW;t?7uW8!ongVBFK@EM~@yocI;SeY;1g7T*8JiY8gtk z2}|A=k>U`=aEeZMIhMIUHVfo~-Q%-8j^~1lWy4))F$c0^59ausEI8>?lH|h#ueL9( zAb?RC#yCyRERW2p2+U@It7gN?IwHz0A31yVSZ-x}VfBeqbw^lN$(7eYQaiG`C#tp& z>R^s>gZiY7)yQ$+S7A`9}dARG%av3~=xm6vW*wcpslVnHVMpMlu!?(5gD_w@AK z0Arw9DHGKl?DJO#E_4sJ+!+4T;dpbY_jY^V?T-FCS8t788+vekOzaQjf z{~azn@*9}_P0RM)eGG!KgAXR}j!%QQ>_4-zASnA_>cs{t`!^{2aEkNz+3e(tdC>#`2OmGyem~aN*TLJ}CbUc^F7w|oQ97Jw*I&0g z^*vZ-YxS)Z>FpU;rD;tgiZ+GIThr#~UD)*SagQUFGOZ5)P z(7iW_TRsO5BMi9|g*WXQwp4#k7AB;lnxNeg#lL7{SeLc^z~HF?{Alu-t)UYmcRM+^ zWke%i*~tWNYPP>l*V#*Q@Hsco z&wLdRp4%7ln(trKl`zANEy2#Wyxf8kmoR2j=d3St zmGTI@+l3h?7JH!umQv&aO;zL4xtyJj?zP2&)VMTc>~w2yzSg3iMLHl(%-+53HU~>e zqI|l)d!1WPQTq(o+%F2MVg#LDe-=5Pb*x>T2t==^@v1BGSLWy|FZv z5QRK3I=V(@rO6?;gm+Nvq~Tv4du(DJ6F(TMZAkG>+qv=tQIx$h6>uor@To_mwof5> zi}u$f#e$)*^=IFo-+hs(ch61f#ffY0yz}*<8k7rDn%~5pIvDj@g_*7P=IiP39!Wyk z(f4=HmdCvrRx?g=JlHjn`t?d*8go?Yd^H?BXz ztxGSlov>^2eY{&tP9)KW3VXi)`UXOL%4cZ9q&odmUHm}^!sOYd-i*cNBc{W1_74t} zw4>^Vl78k(J=TirMMsTB%B`_Vb@8Se`7J5CjTLy)#oAjQgwsV7eVw6O_AEO^!s@-X~*MpZm?{lR+{McetSd?hlym>#kkq7&A)#e7DwWaqg>)=8RF=*DD(^JvFC09dD?i_w zAqY2>YxgPHU6&7ax-*AHEGglBie_0wb9^h&r_NU}yI(@?$5$MO7$+>Bb!8|a)`$pW zuhEOKvc0kljEXucMSI_O{*>6o^3EPhGsRJu%})jSYxP^3Ld77bdf=)_7nuWFLk}0m zgjFV8jjf?Bx9&DFwNUS;Lf6}AX9jF>T9>}b6FEv^U5zid90}=~?`l4MH`Dtu&05F| z-UcMEcUCC8%+K(Z5bY>1(m`I*fW>QxySW14G%Kem!Fc>#AcS18<(a3>(F8@Yk66=P zKh^hnYK&E+w$u}LghEmn&KKz9RNF8Oc&qr9Wuf} zXmWMI_4|7@E-UR3wBB3esK^>N#0N!WlslD5nT^QnBnW3MO8HCPACf0cD|wHT>%>AJ z4}%*HC-k+Y>d|_I660vQ4?KRNiiz$r*%O-UQZ&C4ag9ce`HCSVkl*n#3Taj5kfd{^k+ z?86kGJO8e0lE~8ofr3Lv#sa*STG9o>zrQ;3ObbYPd)Tvoc0ZfRw!A$Gv0i$9+p_iL z{btIWdpk9lK5k;Egdc_Wi?VHJu{v5YzeJyg7%6!Q1*Ktli9(C168k^1Q((D!oqrM9 z1JurNlXhi1I21B=cJ{(jmgwZ2twB|RNj50u6CAbp&sDa&gBH%#+2Z*7J`0Lr&#b>Y zoqr9R2$Rx&IWZoQVCb;N4CQGNPK@r2Qf%&?9p4}1 z<+RtR?R#x=@cYA4I?{F9?!LA={{2zTxIvAqE(MMTpe(zN^ZS1WJ8YYR0F*ZX>F%2Y zPrpCBn7UOEv5g8b;*2-M+}v>i2(!L+74WxsI4eS0wC6iBc{iftsgx%$q!O9_;Qi-I zmt^0BY&j6`FedZL1|Fgk0JFc%Ai{9eTpI%%>=_%0)alLkSgMxqtS7)OkRy@;%y@nn zrfDrM3q=6k&{Op&)*>W4VjE|&F}djN&^s=l6--)^VY)r0m+Qe+;=ST#DY`C2PET4p zDDp+u4Cbf6%WvK*BXfne^Z5)y=jNP<0Mpz=*@0@!uAZ4rHM$Uh7_p3zQGT(sH@re(3hzbFvf@<%MpvM}~9z5a1%|6%_*3o?8!3(>k<7P`-ecbiw zc>>a8`flvC%`>v6C{U9;Dis-h;zg=u_w4rP6q~i_i6BjPE5L6b4t7F@ImM`m~MDHx+!~r{>UQDF`IqO>NLRk(Z?=p1T z+Ok?szoNWTTCCxMfcDWm;^LLy((P>z*7|p!owFGo+|6k3e zx*UqlIdm8p3a*K95@v_;bW-?-VseoXI61V#kcu4&6_}*rRzu_^Xv&Nb`l;Zz$sYJg zoW@OOO{Ss!Rz;T-+WMu#1~ixTe0Pry*PoRHk7Fi&U(80t%^^{)Aur`k;&fLA zWzV@ztf<%;>|Zq4y%&8D>bfmP#o~;ag>8WEI?=~nB5?Qa}w!w zL+L;BHBU{-Uzkj9dbmp!x%(pwS8e3e&Xw;Y>;2S&zr*kg*B#u0B~Gh3K=m`C+V?aV zS&qBFXE+DKiUdcil&;k?H1I??BS`-XP1njr(qO9*Ggyt0v$mYGgwN$#F5Fg=%#)UAl$7)k)uGwDB znf69$7e2WcnwniU6HsTPx&4|+5{!HGJIiZ5%x|rGAavoMSV~4vegq-?6eC@mCk4lo z!TKzZUlLH=elUxza~|y;easlW5<`3~ujnJNsaB|+vZoV`6aJRJL*+P|;go_CqA$sh z3}yYQB=Oho656USTpR#ILAQTWGf3V;?%k8}kstuuDU z(No5)XHxgOMCX&;EjM+gMmh(|x&vn1Cp^Ps{=CUEyFV3(pbk_x6{wXKoJ2Fy+S4!K z(Oew7JWnuk=uE9d#cuG|SeO}v6mltr#>4U^l3qU_@IGb%%|&IOyUKb(OuufAH?=NO zvp#4QpQ@b>onh^}Tu9%2IjyOl)+~|Xu)WwNUGplf_)LIZQ=l==_w9Gm_i79kFJrTQ zE*Ja0miK>>wdQ+@n!?-Z0toqMNKaL@ZqIozbn4g4siOu}N55s?u5t7l<9UwrF;e80 z8CIV#D(~*ZZyqRlwJM)v%}kaowLHdz6fiTNFmu)G-b~_(LQ0tprL1qcRq8H(MJbU1 z7eK2N6#T)z1wYu1*wFlH+6F2*hAKMSHk7}%_Fu{$l>Hl;A8F`<+~$Tfaz}1Ih}_|Y zH1XbO0f=m|#%$ROb^(w({gJ!;k-Gyo+5t$yAtW&bVI708js;Wu5qlF5`%WP2P9p4* zHu?hK)HB!`0NVjhj#l8*v!{pCU*g|7d_(^S1$qP@@(vC0jR+3}+aK}qkqCPV!k&t7 zNQS!?z}*Yso+Yq=T3FyYSZFisa0~2kD=e%P7JdoLtAj>fgGO~jkM=;1^+Dr@Ac^-t zg&*OPydm;86n>IVDJb$Glt&QC7=$_wNqq)MeE~`102wcV%vV4b7sz@I zjHLHGht*= z^xFDyc^M^m6OVbMMSLVcl= zlku78h7i-`#nt5Lv-Jj`KP-IA#pSqM<&!4kY2IQ{Y)F5esVZ&&r@5%sE9PnNt)|eS z2c-ZFD`O8z;n9*X8T-6?Z%pu*`zw*J?^pwpfy#PR7%@&SMIsOj>1F;be*L^O487f7 z>TpXh6^MHCHITVV2U10Zi*QI0q0tZv)J5{lDT%$Y>eTQwIjT z^ld}UfD5Y@qwu<~vXPCjHxZS@%a3z#NZ}VW>G!4K7xWr{JG|dgWMr3OZ%UYO&h<-_ zC1}7|et8(Gh))W*p>PHY%pl<1xHM5yJyZ)(FcHvMj1XjlMd+fTS zrJXmRBz)yb@VTaysjw^WVxH{1rTuj}dLsPmv$$7HUmKq%etq}#1qG$E%1PN2u{y(0 zySO@=we9`tT%M)QxA~&|5#L@i4_*AWP!{|C+bdR@&UbE2NyPWp_2({re{tceZ_4`!s5~`RC`c{YQR& znLO0|b7eaA!_Tjrw9V_Q^Cd^tzj4nsuYX^>@?rhQ(yh(E);>=h`So-4Rr9a)wXYw* z8`qdO0r+E7z`-qie;r<`KppsL2G1q}LCxX3N9nRK2 zoU0dJsCTSNKekps{+xb7qyEVj{p2n^T9f!9HT{Nl{qw)JfN368Rps^Ps#;naFI~F0aX?(VcIA3^7kD&m914#f zjZTb@O;1lfe?Gl&C~-n`~+90%{-ErMAe;BoNj(?{?u*su!z!=3Mc!56@rr`{if zgGhxyC9p~htiQ}5VZT1Qz{Z-c{!D~q!tZQwOS-}qFdIBdX-LIc*~f=5V0!zElz$C3 zPMAkR87k@8AObn}fU(76iJzdrS48FC933Ck9!lJ_r!APz8K*po=XaG`tn70pb2 ztm;#4BkBdp$*mzBI;k~q%QcQJ#XL_y!UGeZPl1Ed1MOC7N>a|;Y`r)>pt%GHVxfGK z!NSZ`fL|~n(dR1NdM*`vnN;jite=Y5Zq9%6Elrb)Q-oeX`%7J9YdX4JaXlbunLc~# zFkj+mwHbxiBLNj|MBFPnW9iBD(OtSJByfT{8i4lbr$8mQVU)cRrC@xUEnB3+Daj1s zi^v1~>TUh7Cl}1eM%f1-Ml*EYo?(lUM1BoRl4*klWCG#gaf?G26dZeG|46`Pj4m)$ zr(m;>1nIT)8-vegAgE(R@J(CloZh5ET1lNIhhV??J$?5ck)1-o%3ukPu^1Lbo z>aEq7G0%qsD-wd#51kGE$ipq72~NE{n@ao35kdpJfL04wAYwr4sTKrDO>MThc-p%8 z*dO$FJQU%5ILbdfIxsReB=*GbgqlR!F-mHDG9wXmF3T&KXKPN^Hn5sn8(P~hw01Og zbhdPLcUg+`|kbXho$#R%O5^|`kxJ~LDvGZXc;_-3Z=mycn>riX9hs!HKK=ftIRX_w84cT zRDu~o0TU3-!q9lpqGEANx&&Wyn3BGh0h0qqmME(fS?M}K06tZ7Uj8L2%q&d+Yfj;^ z0Uln<6UNusMO0}0PBGdf`!j*(f;t3?bD*j_RJ%*F;@F=LWgy(CyehAq&F;fhMlK_K zL|)!1*8U2uA0@Iag%u-h)>bhJ#N-?p^pA%o`(~h5_AhQ&XlU4mA$H_QB$*ubFSb~G{PBc@#J^)&Xtb0K_X*rK z0fw^t#%q7`wf`=ZJiTE_fqqnZIhe}wZ=oz3j?=kw4I5c3fBQ~8S)4fAWEcPD;!j1Erflxbe2Sg9dz?9Pne(QqSp#r>gh3&A&%*ZKS zR8Q8EDAl_~N>8n(x;~Afw8A4|JS2%i!ZZRsN;wo8LaLQ0m6K(>+{d)cu9CpFux+ZX zj+sapP|O`_b_UZPqCZnZNo_%}4m@cr6a0J@@COhqHxm^d^9?m<08dqy!%9{#u#ioo z?M4q1SpNA3E=Ta0r&56tIbMrTWW27*y}3=tjj`~o7Va3-Z91%D$=LJ5&S;BgwfG9xsvakZr3KZiFwii8i5DWs*8K!bG@xJ!``lqD7iRA}F(9X^ zjHN4-@%UPir1?z|&~1rs1d-ICmg2Z(2f{76lTEJjbS-g!D4Ao`XC;n2_IOAsFmbsE z6V#=!hvccPn67k4mxT*UO(EwU+om#*5AiN*ts)v02L$ffj@36a^(^?TVqu}JfAf@w zsfAN%gpQb+E%{!&LwQVSk(%A{s+2=#H6S(*&(bu^gDZ6u-aM$x5Wg8*txr7tpgON& z3@MIqh)jbCL=vnbBu-$^0uHj#4VDt4V7j7hjb2{;dd>3?jXK%hsl4LLBcY=WNw0;8 z4hPU8=~8Ob!s!8Dg^6ZLJHGMu%jX-knGucOnWxl~Fj3-EvW4OVQS-B}dJ{}|=5X*P za6(;SJGiF7fAYz9imVL|XU31531!G^^2kFSJ9BKsohY-me6zE$yfN)4g4sLhy0AV~ z)EyTGnIU%a`t=j`&g((QF!fDu2^M1yfWGTCCSv04dt^S;!?H1HN`ZvD4^CaDuhHaQ z|0Izd@gq>D19^?ZA8XR;(XHXTMv!G&FsNg;~f>ga3M!1I}Y_gq4 zf-y=p!Eg~VEfmNI=aM{L(X~XMii^<2;D-j0sbp`+Rdy-@>jOJ6IVmhcrBzGPjw1k= zgsG8<2pHHSz0?m5^rjgZ5qP2*1jq#r6LuBCYr*b^O^Ko;bb1ZG3ilDBap=bsYK9>? zrAUH=N)<3q>D)3;Wa(Prji8tu9>`Oft9=jfPA`zAPn?Uh40K@)biMJ=JE1s$`qaqi5fJNxNnQ{ zCBXR1WHJK%?V4UUD+Re7s#pf9Z5#oHmayW-_G+1rH3HZ;Gv)6-a6&W(o@4|ernI0f ztttg{@4VV2PeH|8&EMC;&lXcz77=fOWWI1WLm;D48n+820>up6yOc*|rZ-DtKFY4r zCO}^FSe$uNdW}tobD_cG2K!vkm)&$DMgfERg0*!9BY{uJ8Z@utW z7ZCwS|1J_e!EM|kf3~9kJ#e&p-|pRZMAO}dmc$)<%?P$;M0*Q!aOP;w0S9pG$YIYO zXG6dVJe4}}H0{YV#uO*xIVa=A zY$j(eb9O#!?oAH&P0s7Lxo?*8KYS|u_^I&IO3{~c8io7wv&*`L*=ht8%5vTn~HL3Gs?39Qh6crh?AWV zNXVU9L1DM;in^8(@*=QkvZ+ENut^iCig-F|x=B{rz=Noy{EjB2(zY(3ux;Uvo|T{* zN55vI1m{>#Be3LW;mSio9}f>E+$vEN@5vI5B7okttV8$E2s<^1DJVp;_De_y8T=UA zi2Oe7={^A=3}g;Q5!N#i!8a7JHwy^6Z_8r~LqfO)L6<+Hb87CCs*5G=wDWknG7FVb zGdwfzCN@B#YI~m=8xpP`+3^9!pA+Rxo&CvroC+hbCw68(Xny_o4mu zhW7ZqVSo=TX;z>|{TR&UV7*h{Yu8pYf{ry|t21gT=Q=|5D`VBqwjw`Tv}}_%B8NKPvpQ)co4hB@JgQ8_u%2%d2iQr4-bi{ZA;f zje>t8cCohd`u{`04lKmsT0{yp~l+M)J-kyiXaW4~iB@1G-{`NJRo!G3GM zeLghS^f&e^bM5yV7qDCxc@Gl5y9rEYA%U zK3((FT_sZoiuaziyPh!c2-0?Fw=nx#46#&v{dHflB>_NF$jjfciWK%D^!eDVi;c*8 z%|`Qp&2mZW8hADMp=Tfm=Oz&|)iqI0p5me1mJ;aJ$-u`_T>>4<$ubx>k__a>Gg+}eeEll^FZSL$tf{tJ_noOEKp>$> zC!vO3ML@uW-VMELs3M}$1q4m#ASEEuL=9Cy(4eS@sG)ZRR0Ko?L`1QmVprZXk@tJo z_pNr;+H0+|_qnc}f4qz_A(J`hGoCT-`*$N7YW(QLkhb$ZiZBG8CYRK9Bw1SSEb6hG zt!{iWvdV{0o$C|(GRXCgTpLpqv#48#x;!#vs|rN+2LMVoWs7p14L62GypJ`!IGgxZ zU^%Y~x34Zr6f`sEIUv{A^09j8!K5Yz9$R!`XrYD_|GD91T}9sd`Q$Nlwqi;~Q6c<@ zznie_iTypvr#Ut*ew`o*T|@$wDy}FEO5K^yuU0SdCB2eQd29Z{7m>d-IjowJnI);# zZdhoQ1*y@LlFup1Jga(ZiO(9|0abq6wse*~XZA=_z?Y0JUVVG>?OA2VcCKbyM(o=1 zg~OLVSeRWh`COI#?lyee>!3SGg{Dn}-^f}wc6BwpN5K8(YLC#qrk1n9hhM+%N5pb} z7?71wh#VxI@0cA_y8ildu*w~kk0ZIgb{|LeJ_bdN8GiXWHD=7Ay1rMB=ivIpK}hcU zq@7r8^gW7Y(!G05?gwLh&Aa7MX_F`&uba1>zj$~FgaIqqCYR{*{sn;-=)sn%bk3eI;zs(MPb6B3j#Dg zOPDK#UY`xg#BwQ!g=uigUUuIV_3YgZRgJebZ3~jJ#ii#hR=TT`pol8GG@bp_Q5mZ2 z5w6froO<|F>+{_H<40*vYKc~{&rbRuIH>mhAU4@91*fdx3Qh(uU^1 z@i{d0+hm;tPgoc3jY-8f=sXEkJ|4hVgGZ}c$^QXiB+{V}<*{%m7Ml#iguvx`;F%6h zm*^WcdX-r1Vw3IYQ$qkR55qoCUj|y(iiwm5S_&CsicC;m*mO3p3vmQvrTxMhz zO-u3*S}`Op7ck&cGBVcI4)&J|OXu#%X%AYf9lU&!sWYx1R@fMu_MwO=B4=u&ujkV4 zq}AwyQXc+1I?@9yILVjx6~jnIuD9NJmxRp#c77r}aBQZn3m$W2zE@relDylBTzaLa&H(YT{7s+&`rKExu!klB9bM%}+X%To zGfT<#jf28$B2!l57P95Ivv|&WW`@-b!27FY=g{B)FPxr z_z_GgA`NJRU?CI}6`hD=Oeypno{uDAR|&zftN7)a0H04pCMSu;AE^fs^aleX^&mp! zOc?w_-9+6O;^>THswhc=lMhbfWRBAS$p|FxgXF`9Y@y1R84^3cqPSWZCb2wzXcU|c zJD+(@ETkoMx!1oOB;`4O;oMfg?23^j^$Huav+V8gJZD*KG*E!1qX7O`j}#)2S#QsD z;W-`0PM%~)XtJR2-N|OLELJuSj)$I`zYsc+jHl0#aS+yqshW!)!f7p;|4n?81e_1y zZbgrGEH!smBxk;UZEv0b)eKHqy#-L+uE~Sw7R(mm*1i7Sz$}^I}a9UPPCjnuTtdU z^leDljXRVz5_E=qW0*R1;#9ZYa&^v)k&ufYHcIY|1YfDRF&e%+RQ}BFMN`L(u{ilb z;4`?j`Vl`I&!gk~({9C>>-%`7Lu=Kx(SNRkD;;b7n+}f2bG6>`jT{tZ1~Dw9H|!qI zma6}LzhTez#=S|&p2G%Hth%esQAr1UpS4fjd9t0AWFO{vhl+xsySTDPSA!qUBFu#| zS?48QhiV_4zGY0i(vWoU(3gY<33{E9Ijn;RE_orA)s9@hmwPbg_@l>bhpu;|-2!yu z79M{LzJC4Lt#^r+emwp9_B#8M??F71_7wikgcHjXPOk@eRZ)7KVs8&;tr~Fe9gpq( zrV5vy6Xif+2ZT1>;$iZ7Hx)JUG-wzFK-45YS3Ryj?0fM;*|6dA_I~}5{kK1!{y4X+ z|Hq9n`jd~$A3s+tb|*}fIIP$4{(5PWZ!lTL^QmE{?<<=-?GG-Tjk)M7vAXxE!SvN9 zpIVNddF`iubNb=M&)3fXdJ~fWW9G%7&+S(v-bUZKIrrx6=Ud=ur9z`a_$V;AES|&W za5glAl>)6Of1h`0`;yetxFO49?@yH^E^8kDHh2L1IldUaP~H9gZppFry4^;v+||EN zWo&$6=Kp%>tA6ysyz@j9hVC3V|~w7fty%x;|jV)4xPPz_*@(4 z7Xde`3BL^@ho_gp+2P5l&5>d8+~$~Yb@lkxRuhcf1~a$8k>3AMpB1d}ME-*{o>NT* zu!+Rpq$m7Ux2#>}pP*Yco)|*rx3#M?TgvvV-HpiUf#OYT*EfabvCSF}Q>X;c0jG1pcoTBy z{YJk#!OVo1gEQ9x?oA~f{qp(=JD%&0gZOSX26>G4?ys9b2EFr??LG&I>#reGN$wBN z8hsDNa+?fBuiW;bCV!Z_cT{25nO~njy_)zUi6?X&{r2@y|3lt8$2Wfb{AKwBD%iGf z{t(6|oO%3;92EiuEZUf~S7Z%H>^wP^L&Eh@7O}2=FplDd?oi?9sSr0sxisfstLLZO4!x``rUom@ z7YD|Q1ef{Lfdly9%MHtA2di$>RfOMu85}FM^o>rE(wjd|I%W_~Is4}Z)(nsYK$#){ z7=RwWO+gZle`F*1FXu#mzi0gSH;n%Wj)(p) znE!w1?(|=LTkz%v0r2ycf1+W)4J;`c1Nv6m);5Pn5pV>TBq@ZN6uxUzD0=UE!X?$^;HYKKNG^A!wIVS3aXo&`3U+A@A)uzfijcF!Y4 zcrz-lb#gf#xE23d0#et5UNBW4s@rlf5 zuDjn1_~#qv!2=n%msWvf3&}Fs0$b!vP5$n;!g6`eQy#TBD$jM&`STG>GchimqI-yh zCAOo3=-hi~p;72yt);#rGo8yHsnN47q;YUL*ZH7`)7b-BOMA-c!43CL>%RdqV3uS$ z2Z$ISM@kpo`T`{wskWPdwipxBlAWK-<|Hgu=$Vzr9BPTx;T@WtsEvdbLJE|sQ|Ah# zBH4f#SzsK>6^clcmkP1dfHYghMq8(xLZnV>Qls*}O1ovn6m}25f~tSfB&&{+S~CW4_TPGFPzg7?x*y z_Ki9`&e6^{c-O%uS2gb)91QsA%2Jw)=jx?e3fK=y54~iRk-^eiA1i7$5L0 zw9`jpQ$%@1r7bZWHt0YHRA5U6uZk3(m6w;#7U?F zJ|vi=GLeyvgJ?kF%Z+*>GDWdX&r=#Qmo$?@%|=>6kOCRR-mdz}2rCf*GrxMAgSZCr z07q(N3$GoxLy!uN)}V_sv z$io%qp<;X}R+^I@Q1!l&Fpo2JuqZ;pq?DF)(u7fPYNz<)?`!?2wnfv&?bl&k<8i2S-cbx|Y699s5ZKaS%swC z{1iseCri(n#M?B+=gL?0%qW{`4WI709DMb)m0DD^t=y+VD_J~QImIFwLQ$2Wqk1&A z6^;Zc)s&5yp>(SEGxj^(gH6>0>sH^e-xnKI_S7&~uR(W=dFtN27jvEl5!c4buijJo zvk0)+ECN_y5#aa_MF7~v{_m;-{)+YaSBrrEy%oT3ukpX94*CBul;{6SL;i~^_S2~V^l31-d-S25$CIJh@qL_+jeQws{#9zc{E~E&eo-Uub?$mzlnI@cx6L(uino1*UedI=Z6NJ zx!m+n97D0th3l1pc%fLyt|v0lGcO5(z5<6*0ja%Z&nuH5`b*?#M69gerb=?n^tkM) z{xi|9TAq`H=mH-y_))WnOuVOEDVO`@^%JnAhyhW&MxCK{Ic}5Y|hx)Rj(jM>i z3>n=vzCsVih2m-xUyk>|JdGSV2)>z2B%#{z1)XO@4d}XGuQWhL;kDPU3OH*emlxI7 zTi+Go|V@a_6fFHo6tk*8;cWWwsUww7(1DKOnB6%BYB={8>cEGBI z9aiz#YT4nowE3aH3kxFbj>p?i`)~xp3)5gzvESd;%+4wwzP_&Or+!;)wYPQCR8f3b7km>y!w!=HPn{*U>Te!?UfgI=Jn9!|K2jm7OC+*j)E82%{$;%nXWOGMpOdOe6lO_WE zR6RzwWGZ9992rtGzoRoTjfO;O->>X~29IBdCzXDf@5ApiV|nl%!J1@B$gJqX#j8Wd z%`@qwg%q@qK*cSD@!G=iCXxZsq)xHAe0dV)upJ8`85}(hpkmDyNYEfS zj?Ebl|3g^NAdqcwOtD6j4tS;}no5n$&X}QM*fba1)p)X`6Rn7tNPs}v0P#Ion&cWe z$;$63Vo?Le4GMv|fi4kAEKM#kSksT##WOWdTVhnklku>S%Z0F-0VaSPLZXARYWIBh!6z6P&ZIdLq6LW7Qvr_5ty;o-uaq&>Q1 zF%$`r#n0whrqOvy$-L4unwVFcq%>GHC9w${E3D+|_vUz`X&6qLDhTsn16H17slclo z`w@7kKOQa~r;)*(N#c>Vvf=rO&6J^4#%qOl!}HwE)|Ae39jDqzb>Ja}7VpEHANilD zCn5TR-rz&a`~@Aq*)+SpvAtI0r5dG=z2P`m;s~ufnR-LkUwW|A++^9)iaioMmRSNN z&~$38p&?!$%cn=`bgvJN9!mIldY=2d-X^X)_)6J|Y$q@^I2PNkeP$HyOxn`PBQ`a> zccFVgamH4s3Y5qHWwYAyRe>7#EngMbqXNxJV5}tQQL3#uS6j9_uTb@kPo*Q+1?W<=Ud zsQa52>BkR{v+)C@Z2V`vNGJxlnWKbTh1Ie?^_|218}Uu9NmYWiQA-2cYvYK!@@rM(45 z@nC1=-!G9#6$|+_wTx5(>|5oW+@JR;c}X zlkP(p@4)Ory~D>)sBr4QX?;%(s)Q2W^0fY5b~;AGEu>(FF6R zctikP-}`z*>3UDu)rFtspEUP;Dm%Ay?3~bJ4rbCHm=y*Msgz1hRuddxCi56i&qwjS zvh+*C?#Wz8m70)UNajv1Wo9A^^_W?}*pjfLIZ>cTh_h*$fe1+r_RHWlpYS)=($awl z5xM1Eu{Z=whoxyLxZ8(;K(85}_XQ*Z z{pei5MGUs!C}u*ua7QA6Lx>lS2Y5n8H2X!Q68lolMt!5CLL5i1G{|mZRyP(B$3zN> zSpgP(1!z!>hzn%&30BHGb|ZX4*buar86M1RH=If@tP+}_izxRjn+7Y@lT9YKyWrEg zgghM6pa#?l5@4z!e_6nIn%xT(SIPKzoyS#}s>oq1i=**|Gw8|NqC5yCKy<+NE)iLF zUneT8=~WL7f|@1^OX=JwnbAa1x^6?KN$ChNpO$nEodrKN!y5GcFcHtc>>eC<= z>iM(m-u1r1y$e^KWhkrtT)mm!^lRwy-KGu9@SOz!garVQl}ZlG8V!bZH-Y~4JwVU^ z?Qgr*v47i=tgNmB^2k)ww3Ib;l{NL0v<#HAb}H!@{XL;<%c>0G$&fqTe_NHke!G=@ z{|&>kzv6ZhQO{h?*y%5X8E9KZm`5NiBDQSHlDi`%9Am{?kBK`ch`S`pn0U#Uc}v^v z!`p-y?2Y?pfHN~|Q!`r&(9yh!Hgk6c>1Gal_Ja5Sf97kZ_y>VFGpC@?EtXk8KoDru z0D)!TJ%7{M?0N$3p7*E0+56O{$9ZoBeD7&dce;pYx`Ea}AF z<9Yw4CmM7^(;q^Try(gbXxaqmhDKyB!gF2#xvS9pw@}7AfUyR8qM?N!e%qqgA%8lf zzx-v64o)c!%sL%(;%sovIgpzcoL{~FWc~i4#)IWow$N$+L`Uqu^WU&T{ny@N zb0I_jiCpA`di<%qQnLe*i{>Og?a-V!ko}k=W_swSRbkb@Nj}>~oy^O%-KR7V?GH9)Fs~oX-3}HTxYKzwC%m+ zeck!Vog`(1u}_=hVt+1|`E8#YyO-~tSxq(eZMT2*u(`kIw(k%7cRM>b{t(zz^waU9 z?ZhtErv*QbuyYqIi}#it3;uEDea6(@^5da@G(&_>)2`x5#e4;Ri#b)>nC4t=&bcW9 zZXpY4Jh~o)R8E`Bg$(?m>4LOW3A4pa>0+z#$4?<^Zd#Wg)yJlda+3um>b1~1ubxUj?!s9|I)ZucsqVp(yQYjXt{ z4c4XN^7Wjh8pdcj{=O#9pwj|5RYPS;PIxnw*bimn$@L6rqMz@v!C-SvkA5Wz)CR&MV^<%5Pfl+dK-pIvu?8&DDDeR5l!u$~<^& z&SaGg5WjYwP$dcD-?lxPU3q=wMcoBkKIjVbgep;Z9?~&Ue3~uwg_Fwav|d^4hXba{ z8tU!h5v)$^I++`b*`>@<)t`1c!Gn~kJR$koVYmx_%A+b$P{?O8XSs-`(~-%5 zoaG}3fgVljmGSf-acB=rV?TBtdmRxzHr`0h6c*CMTvZ>t@7FnjT&?V%6kfF(5EbhdGp&{>AKT$h@qCxb0byZ}wP9^Hu0-1n)g4^yYfn3v_-3x$@Cb5iEG3GY z=XpAbhU-{bxGt%1-^3O!Wpx@NrsNGpi^OCsa*uqlQ8s}mu9$pOv%yk2Tixuc(h%E< zCNjAoC2GyrNlS%XoYF2g)8x|g$1S9lmo6@L+)EB;1Mi$}(nWGSq_gm<9v6=GVM>OL zI!5L3e_E2CXXJyH}fDCCl}-KU(1FhTgI!15FB)?cV#%j*?l_@EUuXCHILn(TCsUkgmXg72oM9q>8Ig!o$qg~adR`svcU|R zU&R~IAI_)wt_N!f72bt}hr{KBa7ef;NqACkapUvU^*b*K;m4&t{ZDc(eSDcwD9x{KM@3I#CY)9>eTf^|T_m@jv57&NOi;w z_@FU3rqpFLNDCrL4*p(A{X;p_v({4sru4e}$w+X|wf&lxuf01hD6kJXx^HZ(HY(M+ zqKs4dRZqeYBVD)0kMBoA>+$atrl}nX_n_@=mRc)|QY0xM&$|UFO~r!|F>3q zo2B00>b!qhkIb5+{EUr~WAp#nyB(aBt} zF$7|3KrlgTD+|OBfJGJ95&}sCqoYH=Ni>@n8nEdDLI?hW&iwB!q5ie9>i^K^z-n)T z!lczv$@b;kzm@i+WKNtxgDMk^ItwoAhTZ^4q2Z@UOGGfw) z52@A%Ov=~qx}fFT;70Ur33!_4OFVM1vpseKn7+9~-Sqa+!(Uxd!nFBL1eeq`c4-R- zm|L03ZU80s0C5D*m)gw^EIg&YPj|_bgfUH~2(PVawx+@NS#gybJ<1?x^LxQ=Pv}9U zO$|ftrc>nkcpfLlYdhdG)0w30!Cy3F)3rs1gS+@M$V|UmIRCw5 zlv1Vy2X$`3Qi4L&sEoXX;Tu4Qpcs|enM|sPsLK6<9z<^Lv&l8L3j* zc}#P0s?fSLii3NttfKg!214T4N@X8sgmj&-f7Tm|5M~M{+X9-STh4~(I9>CGo-in% z3Dz^?oUJJ?Cp_BUu)n;(Kd;ZNJp9^1x(_vPL#QHrqetX=qhFrx7oTXTo%`d6Jb9{# z*j&72X!T+lvNnDtVrmt3Fz!8@JGM>3zj<2~{akXcnpc;Yy)5e>|2;nMH`yFH3n5r}+m2xGr|E#lKqsucPp8zD{?@?nv0 z>4v9?A6t5F&Yk(aww{^Jmk?cL?fkR8{`{lf>}celR1ee~{*;zhmihcl8DYh4GmUxnx6cF$et6+V!&ys(2dgDL072PfTTLmn*q ztH+!_SB3oEw4+E#tnvCOqtKTj-ClFEtHFcpKDnE$+gfFR zH{LGg>?CvAP|@w~fv}V1pD)*mTrkgdoqtsRl{JlJoype?l-YZMcJkVnh60cE?o$_+ zi+54q+@(F!pZY^zEZkKtcbqPQc6x4=YOlp$~_203yd zIBts$R8xI+iya3p$RIwh?fMlE5On+2CLjnr=HO^%3k?Lq;{IBnx8^b+8fXg^_j^F| z;lIIo`T`K#9I?-n1eL($D+EjE&ftSz$BByUbHy2GPQ7NAqly6xvOr zh3TKphjvoJW2Ir_R1PaD0}V+_IgQ>y!!in!God6wiUuA3JiXeQP(fu(LhomD%I9b> zF2C3fCP%@o%A8^26+45x^d{KP9~2QRiwzZ{=IOk=Hhb#DCZwOHAHzU}{~;d&G>UDC zi2ptxV#_E7=0ntP8pZx0AEIs3D7Kjo0kTuUe2BJ9qnPp?i7f#*6yO0=rl5YnHh>$b z19jhIi=8GmWD`46GbbB!7ds1gilvv6rI(9c*gl7dLyigYPDzQ*$CAK{!kI>MP0w)8 zKH-ta@W?OlC^+d^P~=%yM&Q-kLD$*A?Oh>V zBm28Y_xIl2-!~aL_~5|sRM_ytu(8>L<8uecA0M22dg%Vb;fG6yA3g`Q?@ z=2jyfzmA-L7q$34>e$>?N5va|7IJw|wd34|XsJ&cnAI%!yStX?=O zg2^GQr9Ewhr}iT0oFSyrZ;W|_xV(K0b{;Fskicb533xy(d8O0^^zh@0`^e>R+ZNWUo z4u-hYfQ`KH)>(w!9L{dud3gUhU@0Kf`!BjubVAtQ1C~;=lR%YqPC=$R9Rbb z>B|KYz% zSK7SCo&F231ltv;L}KNWl}p$Qbg@! z0X&0-!f<0Gi3Rk!6qKUmk_$jHMM0p-IYbQs5(0sOM|#ZabfJ+@bxM+T5flxB(m9`d zt(yY$ojbT6&uA|o1Tbx(%<}NS!IO$fN`?T`#F85?6PuVB&LpR9iQ<6YSf$V4oOaW? zQTrE1X=^;YMNRrEF$5g9g8UFMEwJ!qhsJW6r6=_ z%|Dlxo`cyFD=RO-{xKLl@&5g0%*3ZppTGn2KlDI71ef1UEDk#v#VajaA5Ng9a}ngD z2N;W_ETpRO{Y)_ZGH;uH_D{V?KN?aP?ei6{$xh`)$SRG~*mN_r05XsfvOrG87)pt+ zh2Mh$Xsw+p(`}&C10^9s1qO?xA6$@=@Ru8W5IU8)S3yXJT?s|;U9>SQZ&HT%x&ctSMgm#aNor zaioOTddOt0@EMa@V$6lW{(=D0vu8YF56-e=IHyN%9wGg%EfAny0lYT$fPa9(g)M*0 zPD@?TUjuR_w!&+-KoVen?OzlLFtgSvBEmB<6-=B3Z?&7`2>J>C-15Je5n%2th>Zv- zZUC_nhpOA78v8&4PRymzSk_$-7y;(a#$S7Q^u{AFbQVNK9J~E&GkUi3#b3CG|K(*8 zbq0_D;bUNWQ!>u0m83V0H%Wux5=-$EQU({CV1WmqaC$PIhm~;s1T-6gfrRK)lJGrH zSskoS=DZoQgp1G=uJI9{frIg>HBq}rcTs{AS*$4CbfOyTq(Mzs*gu34uoI^?o_yiT zLx5^D->BR-dfHCL$7%v!o6cu(FLE8_Bs-V}#bw4(CSd6~aUurrrmD^KBtazn_YsEz za6sj6S4S0i(t;PrmRJ-#l;ACA>oi7>RZonx>gqX%Lx`Q6GqQ6AH_9`dYYSG=*B@Jxhr)d@k4ft}yxpLf#8 zTyK@zbza_!Gfh!tmv6U#u>20oV*hpo;g@FU>h zM^u9E29||?R*=NsYK6Z=*ZvEGeGZXCq5%@Swth+AkNCS-Jp?Wp5P-S$TS1|=-h%qK zD<=U^0RSHW0+avUR)c{VQgApDjs%w*Co6A_(%33=R&1~cy zoRv)-Rc%~}6gMSTZxxR{sy_a@&Ov%^AqL)IM!u1n0iik}hjkCe82W|lM;*0MvGLmO z=&>;BxoNcITk%@_k zy}iAQi;Its56M3+-YWj+QL?|ke`siEWMpJ)Y%EC2GfGIYI6${PoM9D}ZX0#NE%DYil|4LsLu&8|00Q?QpvDteB=c}Y>Q07b=jlsvZUk_rWplhnT3{lrB;k` zCt9gpd9A~ldiS&{_slAfj2h3ZI-lGNzJ<+u3s{t@i%zvIF7>T$7jOC%wFZ{AdtL4F zxzX?2F%(+e7gjrXs9`Lkc_ON1DyDTd?)sCswkHWUpC#Q{p|Z#PdL{!09tI81hKxTA znOq2)d3n5ZHGJVi)QhjtFTbbmPR(@8WVkVkJc~~Ip04t%x)^e?C#GjOet0}_JUQ7s zJKOr?Nr&QMN_n|sMTPUZbFNILYh9hkg$v#-Enpv*dgF$FM@JyNTRwe|m^n(!9#_n{ zr<8kNsqm5Vsd<&s=c=b)sWI0yYCdb$ebemh4DRjSKQaA} zmf}}eljwB%i4!M^ii*HotLp0N3l}c5w6wIhw}UvqDcaO*%Is3+((ByU>xJt-ihsO+ zPy75i8MH)#jF8^mUNBZ_Vq)UKg9meSb6_GAxcI(*{~la+!HWx24{ctyTWX+x^ZJ?t z{XhL6JZ>wuE;YdkG&g>z`6qMZR&Jehr6d&0t-~~)KmYf+b^06PHm67v_5YMxXYT+8 z1T~IDe-VFTTXqqDE9r0-p9|ql89dFf{$&fYu99ZS;0D@0=8wa zS*Wb_otNer{lZDk}rKIAHCiu*CIAxaRSyfYlAXzv(_BX zrTe%uHGnVUC6D9T5W;A zdky=&+%t|gM+?i4Y(Evm6_0>?b2rf-0ENvWPH5JM?1SZ|cVF zf7y#Y!azf_E>`TxR|~BAyj~VIZjeyp8wLF)y?cj(%98Dl#av-=i?BrPzRWdYPi$nH z?@MpkndnKj!#!683aYB8>eJ7H(%-1 zIi@0;d68>B7LyL&KTv6+U!=uBo}-$2Xu{>3pJ&OM?|ehTBqnFY^A()r&C`ErzxXlmDRhXxkya~ldk#dU*0do)<3@^hL1+JX#CC&JJ#E5)zQ>;58+Q5Q@CVc5J~39nD~ZIJdgo%-_R*oFJW(53ojj^n5#&0L%NYXC_q z!CnRlP(7|VJ-fx}q78KGqB3<9QTM-*l52WKNF%JkE2JTwA`|T?m9|xxb0dR{4k#{m zWJf}2R-NR{-V>v`2|chJQo!0-r?o)hB@GkCwG4`A=nwup(veFwJAi}KH@pVwrSGrp z`gC7r2OwC{z1t>A%B)98vG&oT^re+f5N}YnlY(9AHpzNJBAKhKuW{l@5FuqnCMjg! z%^9MKh~h%}&}2>F{9|PsSSNIcQ(#r7CkdFNFUTqLegc{%C=^_n%RU_R#KfhE15}7>coig# zgpi}+PMDXThvd}nRXz*Qcxt3^&8)pxFPBW6SM&gYMOM7e#$vM>sen~F970h?T{KHp z4qM;~(CZd`qLH!FZeT*dsr^)i8ibZ9E@ftk3i7BH9VkyBnz62&#mGW$veHP6^E}Gj zvI(3x;Fk+j)OG6JF+dMts{{dz!tesw%psH&J)?1Z58`X7B?4u_V>H*zy*OOK@{-#82r=DfjX%pX;^$J3hz8dmb8Ox88oX#Sl_pic zfEcou;YM$K?bb=aGko4OxA>2B+f+a_k58fnC7pQ>)8jEN}s>+Tk=E{lM5NE!hEpl*6C z_?1Z|;L!Jp7tbT|x3zAQ(nQhtL%M6d*DQuqKFA3Pckd1J@<4Y-3xf(a?s@8n2D>vI zjfF{uQSuDAhmmm_Bf@iWBIUAf+~Gw8NKg0$a=3=2d1NxbP(9rpVUp!`pNeb3Z$JRf z$_(ddDrYkV3KWshd_LQKB|LhSt0-<>i@KapcJmp-qMp?CPnUcr1AGw4m!(97TC#(h z0tm{ULte3j5#NDj&HAJd;#s_v=qGc=g6p zB%zK9y6*_OIx7L<^JW!#gJ@d(iVTnc(Yj{TVtP`|v-2oL;-xwq!31l9$G&-C5?@))Q$|~DdvjCe^^^E2*|IorcX#jks4ME?-bhH4PSWy-FpdjytUpfK%vN6o=;IajD zc{WW|wpeGN{=klQzsu=8B?N=#4WvW^Y{b$tZs`8t<`IJZ4wX|<%W<71%tlV>a4x|t ztQEFT4|~ypE+~V|l+*K@7ZsLmhHGk;4P`WIJiH`H4FeK=H|*(c&v-^ z!6taay&L8jQF|W~d<>tJtlBFuDT%{z*R7CI{Oh*Tl{Cmf{d&pZAYU@l4 zHK%&mhxaxf2H|bugQLm0Q9dW$OzvUBw19zR-6aUG|NUsfN94LgR>M{Dm8H^ag~6g9 zO!bn!!lWMBq0oA`6Yls$z{VS`!W@4oFth^C>$Rpm3w~u-h0B=t_7q>mULGZKR@`HO#Nb?UDgf%GsQCb%!+cj<*D& zp_M*RO|;yK&y7;x&@xx_)x(_@zJFNc0FvXXB^;iWx(gl`9wMRCLa2ylMp=~7?gXfeAPr*Pq)OE8$)}Skt z7*g5>?@xdON=JSR=Z&E9Si5Ka!1HwLU=PW_+#kkAuzA0-V_-N37s~(`+20l9yod9? z2*e6fdAv*gwX9D9Y>r9xZg?k0;mJ%G1bJUWpHG+|;+meSff-=~+%Z8M)>sIedxQpw zC1N>hxhqYv4}}tk;^M*uGmpBchp|1-jwf_$u;M{}hc%JV2wkknIA_V4*~`zI&g>G2PuUMXga!a$ z2zoJO8v2-ldDM?x4>v~BF*6$24|>Sk0B;b(P^gD1zyI(PN8i4A&qrw*PdD64lPriN zGmW`%=pBIf3B?RS_DXpHU4{j?)Nq!A+hGE5ipeQNvnWnilN$%x_l2G(oik%{B4m#* zL%n6!;7x$#LM?Wnp<-SG`LF?TDot)mOXCxf285ppVw~yyiruboT2uez;RYxSg1WZ` z`{q(r@r;|B2^j(~v5ElE9gEW7dPB9jO0mJwxNc%0zaB7BmP?_X=<;zA)Uo{hjC4gf zr_i_#Z!jFUj7HE3?*-^Feb}LTk@hYG;ic|FM-7u-t66gZr!5T+o z8>Mg23Z@yP=%<~;;!EsQ9;50fuxX`hnoVwTv<7iyoMG}bqSuX_=^^``!d+5=t-tR~ zN;t3NXLP!ZC+>>TVcPcBC$pXvNgZnB5Z1-+SHhAgyWp)T=h|}voIXt~zMaYNyXAa# zGR=yWqCfSC~)nBJ%} zPw3FWHBqJY6IpcVz?$ftWw(b6%xzCu#ZKYDdpo-Td9DaE>zcD0?6bi9F-5X%hKA3Z zc=J}lU0-3qZidArCi*^s$!sw@oT?V<1g~4$P+3#D1ZW^hsfbscJSt0KFO@EW8H9i` z>(D`>G|qy^iYZS(vq_RnlOflW(K0uF;^Lv z^TVZ{iFS51F9&eDA~g1BYA#!VuPr)n{S22u>t5UEpAf_F_)p%nW(u%nE9h@zYMizRv zfZGY+CY+{phN(pARJs^>lc(V3^a=bhRex%nn*zRo)$`P4eLjAPbQy?~oIFtSWkJI(qDW3aGsAQ6^)xHF{lqdbP)S ze^GnAI(mKPdiVV7rHc3Y8}*vu>5s@ ze#hY1pMy;Cp(?|n8lR!MoPjfmLya9nO>;vRe-5$4hmTYYUG^EimN?v2G2GrUoMt!N z@pG6hKGJ14((`lZw$I2w#mG>{$jIDCOybCx_~@kJ=zX8jPVUjEiqYAQ(Yd+NqMXqu z;$w@3V@p0`c@@3O6=SbD#$Jbw&CiXkiQoNTc(?uM*m~mKuN8N{Hw}L7xV!Q5E+8>p zkuwJM9S8G!QIGq6Rg7~#9_PV~bNw3UGnx=MGluh>5I!>@YBchv*FP8#{kt&|*z?%z z_HlA^V)?oGgm7YnZ8Az=uMX_eNvLcG8+Fp#NnpPXY__Rdxahlw?({ln=o48w@B@`#mlEvh{n@HD1^)>Jx(_>(p>WWB z$k=or@`CQei7oeGkMO*7f1c>BP#FQ=YPBN( z)YfMZfwJHHu&|w+;Pn5vgUDZ_e=y_@{NSzRI51iWocn`nicRI+l}qh6u7dM@Fc=OT z@q@}cFcNOdfcaMp9H_4V6Mr_7&A+aJ2D<;O(BeP)3j|wi9C&aLMJ0o_9wi^E@e~d^ zH%2NFs4E|^v!7P&!UlXmEiY1 z9kWkpWvYQY*gH3GvuSeux_F41jO|(S-Kyl{xRTnG-Q51_)YEmo zRcXHmU}rMGol12XGmo12vK|~9Rb)Q`Dp%$_W{RrJUF8|5%zG|FhBqXfVedMCH^nm&fnyTx0f@WJo)Rq z=f6Nc|2KF~m>d0VBSq(S5|Mv%JGKAhc0%gLlTqiiQTu@SBm`UvK;vb+n*tz+oM!obBPKpb|C>SaL#va(%EiCP^l%iS7cB7x}hv7Mf03iVS zxrZ`<3@894(W5z18e+nd|I3)AoSdSJj2gXXsiLB;rlyIOHUCG_(*9qomj5Yf`Oj6D zrZycF{wrpw8}x4_%V>Jc@^3s?TboV?>*?wm7#J9vn3$WHSU>x&XGO?JAdBU$;rjp`NDtNgSomA>G>Wa(Vc!4?e6YL--CI2dV6{KczOAHd;9wO z`qSlXetv;;k$6Br5Pd2Z6m&5-I3zgupIM)UZKG5>65(x+lc&;O&EnfdOoaAxkmRWtuBnMq%T{huSpBoa9;F8*KDGD%5) z$>mZ~(*6-7`m0Hlk&%^|nVprDot>SVlarU5o1d4L{||3@VL?GrVPWyV_KC{MDk>@} zEB}!ws;a6NF6T=EwuSA{Zr}~ax>W0v6cz~vCq8wTk1%f zpFI6f9!se5S|^BvzAUWlZizd$FOElW}rM zT_4x_!+(A?9sCE6MbFc0n7;!|<-X7H^og5+`~HehQXcBvd%w~ZP(AI`|NYa(IBiJ4 z+=w}Adx~j!dQw|}&K)$Cw~gEx`TcWnD4pK3)C}RM;HRGw$O!9I@bN1x8p5Y`aH-t+ zH{|qru~t));I5WYM&9d@L}vCler6c^$>D=cg;xc9S-eL*bg84_+cz)&5_hu;bwY&o zvKXpD7J*r~`_FhYmFlzrOpRY$qnDXvreH4YR%@q{a(G5L5>xgWl(v^a21@Hx;nyXe zvzkrZ|3f>b|n8oMXR$cq{*4Ofm{tj8H6a@{m!y6vH=`E+$%vC>K z=meb~QB%dq;4zmbWa8`!0b-p~V^k}~I-z9liryyv=$b`EF0N0uhMVk8&)k_6DGyus z$NJtfj!QS2+DhII9$S?}jLm7kwH!GLqawQu;~{=x+nq*sEILjHKppHM#QH^*_;Qbp z{KCvF?SaMMIYtL*Gm-7GwJzDW437aKGrNnDwyKj{#B5D%_j4mtm#!{Cf~Azt1~up} zFyPKq+3b(PWdpyh4plm01!tFYWY5jZd8Ek?mW1F1pJ~M3rvYyT{%pD*)c6^tK*g?* zM3w@<4+Ql2M$if-0S9wahL^aPF^d%+kmBvmvSvMuHlrwoCsZ;BGrK`Xi`^sWKwrk5 zyp$lb0%#R}v9_y@Mc?Pv(IJ?z$3KwostKVAj#6c!Md2`D=A42Wi!nijg*AxMV(O7< zl@a`YS8#UNJyX6JRss-R#;r{N@xrocaw`z)4?wXGpEHlfFxcfKX-C!?^&CG1 zkA=WwP4R&nQY&CgQZMJT5b@_mq;uZ6n*N?-4%+P0ge-{w_4Ta<)<)_xH4AkIYU#iY zB3}Si4p1UEOU@b2MA?!Y5<&cdQ=OC@C>fCX@tDjx>^myDZIj?rPzN-RWS}qT(XKfl zu6jVyfO9StT!M>2vl}yImrMwp!NcdN_xj0;X%cc9aI1BQ!sQMS0j~ptFC?CWB&7Nr zky7;sTnTd0nU;Zb!i=aH!QRnyW7p4--*raCwsj4B@Rn(e8_%U;itkp6c(A}%$ltVm zbooUO0Owb>2q&iQ*VULgn*4FM&rX4@OMAj4qBt4Anl`^1@PIT z!uq0Qcd{FWx6V!N#o`Hyj&zLpsi~6ub1BdV*6bd+b@)lvs*Xf6DG!Fn0fKt*mZ%x% zjT$X2-=1zmu7_Omy=P&0Y!V>h(X1ovuP3Vl z@gc<3o_PF9lV~Hw}w=bS@o*T1!yATQU6EFAMiHU z+DlGJV{P>qqv<-seSmcRsLXkun$;?Cp!Tr7R0`?&;7mbxRFC9^%`N-<>sfnJHT5ftK)Avqub%2OjDfab`CuZzDFJ`PJRbMjg z462PoW={I*w>CA-_d72joVpcRmE}n_wgSTRd%t*Ay#EWy_jp*Sl>qWbg#B1D{`c zQ8H^+*t(tVaO!g9_LJ<00}$JjbLq-&ggu%aLOwv_#R;sB@}2n zsNM4N_uP3`u&7Q;deu7u0f0mp6tD>kLlw%z<=!N26-1|hjx-$m;H~;|bs?*_`>cBb z|F?}y0^WU?#8c&kf2oXXz|Vff)51yQ%lf;3%#FNCugd1nU0?3kRvQ z|K2sU7+frV_@z}q_+)wO1kSR2uM zmjc(jhOSGyW)H2uG5wYT|QkmISx;)B}^;ZCTe-8=5@_`RkjeXMb*-yn7UP`}bb?pIPn#(|`+Gk-_QIOQpC4-%_CCd4d)X`f z>CH(Xdo?4g%sz7Cn`5Rn7VJRM(n2m$G;&x~i*HCIZDB<`=pUh?sz`wi2iLzpjXQG` zXHpzH`&A;w<1*i_B4dASj!w8^s4JtCL=6j8j*3JlBhN`i%8*d&0<0Y*l&`nA>En3K zNC0Y=bYb!`)>jfOk6!P>}C(J;CoUi*2}DtQ_hrqkTC$w za_?*#QAKYQF+s4iI3Nf%`)Grc%{IwJMG3m2VcHuBRx8Od{V_HvSjUt^dx!HPib6Da z3R;0(yo};B&ueY*+L1evY}(^vw~6S&$7@6=cYC1QNFW(1YM$BB_>#ukF4T#B<;p|l z;&Ig7ka+D)@EjG|c^H&$gZp@>abPX}J6{uyO`Ts(*N(fI7@2^5CDz!iA!8VizHeiP zLywFh7D=IRzb8M5%v?T|Vj!JjHJW+(F?foEgcCtS3&tC}!iH2U&2Q5{@@(@8PBDREBG3G$#F;Geaq!TObQ=lKvAS{DJO-Y?^l?#m!K#Bpf zwj50Qy5XSjLM*V>N zD$S={iQ;JqY_-VmvVv);;ybO#^yj# zATm^^8#*fm);d58V{#tu%hXAs1Sz(~uWY&Xta60|j`&g;iEx=HfYXK%kyYH?)>Zpe z?jvl*NqcOrZ_0f`%W*G)Zql>cn7o~T(6kGBO980sBGW9nzwOSHK7624LaYCm= zoE7wak$4iku&vN%N)BDMK>h%O+}u_?b7;^+1dU0lYb(Z`W5WuEAiE|l+b&5&*@lfp zmhp>}e6=fyh)UjhjJMnOeB1^1e|x>ivGPgF6}5qI9hy)I{uujW#Z&{Q&PXjW-~o5- zqZBqA3#D`gN_`yY$2a*{2Y8%5u%rl66d;QtkWkeH9;wNs6YyNdeMFlV{9#VjrKp+- z<&w$%syGq3d&+rNdn%s3svbW=r6hyKG0AtOOP?Os;`}d_IAY6IA&UeQsy9=+6n~3~ zlA(a^k@Q{ir_S4C9Q%VEFdnhV>L)93V_)5+CM-D zt4TrK_LX3c0;un!Zj(S8&C*CwImNPi)gLvp6E!-u6_d#D`TCKVQ!2VWwgy8+1X^G&*OO|`X6*HzlD|7bEk0No{^f`wyK zHxXAR+qn$ew^VK!__UKF(Zjf!jWTxS{`mKt?c1EUm;7&idUskMhf1-(tsrvy7iZm< z*4qH5+l$X`L*6I<;iSOCV!=)n6q3wXPQe7kAlE3^fY_5<9qh>KEKVKV$P|vB9hjkx z)2f{tS2}Q9oFjm2p#WB16I!Qad8TijAfY4n{4pCEkAy2kPAJvd<#wi7Cisquv%YqC z`Ka^UweG1m(J_jwmrK~RuaFoi-N6SpTi!LJ6WJ15#Pr<6z$*BraqR$ACS^J$O-ov}hef#cwV1{0-ZSx# z?tM^^uXygYlwjIpr!oL~#$T;YN2L)Ap1>oYQ^=3fm zu5sy}Hl^ zA~fiWerN*X1sPRFM0Wy^*RiP`B!9AvY2rl1d*QnZ1_%)Es7VE&S_(~_5UXBe`}O{= z^Ln)2Bl>SgG$Ixl9#JVz>{M7;uU%8KH!i6)s=nFcTv~o_?1$2DPF3KvjO$kec`pgY z$zdA*+mfrV$27{ZKjJ~{q}uyB=zeBd!9=Sm3Iyl{ozH>1@tUyhmiDb6lmtvJ;*bjf zL_z0tDN**({uKw*c-W(RZNIMdG~H#yAh3dnQKI5G67nfEcZ7Lp>$Qu&Y4glm?3BR$ zTHX7h%wVGkp@CyTxsCI&jh4c%ZPEfRFF7eowUH+`NR^jdLl+_Rzi6>(o*1TSc>(ci zYqnB6b#)Onk0HJAl3gOUKGC`Vb8Y6^F#X3}(A@;j;Gm-==F@52wXI^oH7h`VN<7JS?t_c z6jaLr5B0dGfDzCI0ukZj{B_k}bqhX>i&T6bUX4drhZs{(ecz*(RUNTFQ*0>~`3K_o zZ~-h|gM3Vxy^d9HSO9gB&{=r_x|XB;PKoTpMt6|@)NqL3IIIpOc$wI5hiS9+4DP1XLi>XBz)w!8)edp7l4h}fnJIOKZIcKv zsOe%{0%(wgUZQ$m)b2^#4T7@gMFlJx#4W~`*L_zALE~4>&~(=E`_51BH*ccDxi}*D z*+Z%WAPZEa@P62>oAwpuw^f>EY(iH{pTDrN@0(djph+V%L}F*|bc(-!@uTvJ@ij3T zPY>|g%cDCle!6U2YPw$97q#^W(7#Xy7ux_!vVI8Kz`EXIirL&%++;Q16inYdGjf)5 zWE1*g^Q`OE3#KjJTQB*8x5On&MQB^LeOnS5ul5nI6soSvRlSN&-cpQNmbvAl))%Qi zh(zqMJrF=I<{;V!M`e)LTW}5aqp#G1=Zn8PZ+5oZ9++B@By~GL-FS4xJ1!m9ZBtEauGC+%gY^^!6kBcQq94 zaiMv4)~*3k`%}194Zi(sscm&DV8?#Drj!a+Ml}xD;?a#1?2UAv84O||=VpV?UEv({ znV8{j*@)p=N*Uvdr77t0Naa-BM-Qsn>XTh2q!35FsFW)%EvnZ3Z+5dac5+Xcj&-a5 zT**#u&6<>ymE84|c`m!qA28yWFIA7i`sKn&xt0+!^3mhUrdh93P=zIUt7ub$eN>U_ zCnHnC`we=|+j>g{fFc|k!hs#t_+ZqrJgGrLI}IYszwDdvL)!@8VL>6Gwg4MXy;_o9 zv`6;Qiln1JmeJ^E$&<*iEr|p1<}7BYvZ>}|ND`As(Yuj7#WQdHdh8-otHI}sOy3ri znin5E?5Z)x?u*ZxxfNF@6km<%iRg!CI0tyFml(YTJuJXCL<8CYIbMSo8a`ByDQoP2 zS$}U!Y@r{7#y0AX-&bI@^l`BCc!O}}==mVR8kI=&P%b~GOnr#CSg>BA4}2ua&1$vx zrEYESSxy2&m<-bp`GNjdfJQ+}_SH+!=*ZgSV`NBdIeauK9+SkrwHL-x)fG0pQ zG+g3+C^bzM2etGq^ET4!u!acPMeL++j3q(UW8_i@JUZ+%3QTi`3yfVR<8{#uTY<1C zvRzxy_<$LoS(Un0)dGvnE}>2@^Ne1Z&0DKRvr3IbPFuQe%eLM1p7EE7`*}hplPU}K zj$*8GTp3~B9=sCR8N4PokDz?eaqm~7rj6zv-_e7bM)uAMWD@mg&YiOHXT>Mr+OkP33+jTd$t_R^YCIfw?+i{Rt`fs z(f1Jy<`?$NTXI9c)|)HfC@gWDN#xZIGzULAIbOv)F{d92+`=V`hryu5Zxc$^!qy(tzc4L5SK!0UD}0AsHPK=yTE)X$ z>_b|>H2IlwprLkcaWsPVR|WSJ-2TMj+c~+XrniT{bv610*`_dtwjXR>1vNiG3366O zLN~gtOo}-vmS&2T=YVR>rCHXNx#6KU)?5;)h1T|7g@O`QT3dXQ70MJ_^Rsv4>|L(x z^w_(~_Q*Sw8DE{uKS-#wNKeV}>cxrIjMh2&z2$S~uvM3-Tk(58{@DKFU8tgSXd&pb zQ+VH{ka9mm^VrjALY=tjCRnG))F&&k%{%{hs+2`U4lT^}OjIJeXhEtJJ4)z&rWE zq_R)tVfw0fbu5kH$9CZr!9m~p#COWOKZrO%nH$wf=a*}<8wdT1@<;l0D+9fry^Si* zNn&#vW#bYMuzbRK%lZZ&NjAT!S0#7=K3Ue3r}SZeFX&cnqP!hc9gpy=QYA6CQbrk0 zv0i`otV!YfqyU56^b73$Y+*$w4gIeV{UqBvEMKSC0Z&zgj5FD<3t=NSp%?Un#!}1d zew|?38@PD?xl_ENQOAf`ni@$-tWm@(qDT3&FN=dK0aRua6EG}d#!0$`jSB`hI8$cqTcYC2HFsBcSY6gJp@iQWJme6jY%~ zD-kS5#V)Nd$Vt|YdToHX+JqV89d?lw{k~*Y2(xH&T&9<&(Y4UGZd(0}@sFiF1Z9q( z@3xCc7KB$U_qB1xmpdoGN*FS81Cd!aWppo)pzE4HXAbkFW+%9F1oOef8V(@U8`-$q zyRJ6_PvJGbb!f8bjM86O_g{K;h{0FJI*B0}_HtDC{O$xWXFo$CdYc@z$m=H+)$Fzn zy2?}V0tR19^;xOmUoxDNjLrpE83R=pNgzz!DAJ1Pfu)8#@`TAu8w-BoFvbJVoTG*7 zT8UV4$gd;<%_vCMHlp+yf4o>7Q~NgXlX*JRJTKSrl;P*aJXM}o2MTYQSRxjA$o!sK z33#LIDd|eNj#ofeOVpQD|Lm9vAB~&ZoxbKe$V5ohi-ubGaG)E9Y(L*Qet70i)OOx0 z7NJeVR;u4hi_oK2(_c7cv8=h*!*k8uUS$h zK5-%=J>7W}<%)}EfEa;f^Q4SD`-#vYV$$x0$AfwKKqf`~33ez00F?*xlJOI*Klh6A0VK$%PfzLKfzYm>Y+Q`aoI{w3^JJmIIP2Y3-#AuHV4fmoSJ8|8$Xy z(XD@_E~eEQUim?o=Z=)aIm3r}Z10rD;UC9ZOfuo%G;<-@av+?p9=pd;pTHr5K|0Qj zZGrpQCuL4hR6d4@bH88V<2^BFkN_yX{IDa7tH>@{bNG$M-mNQ)DB&n{0uCw-yS>)T zqUA|Iu{M(VXEqkhnE|{HK1&Im?&gYQ5_@r*;TRBBXPqDiC%Lq5JuhV>xh6&O! zx_wC=W$to;oxg^4Ii)Boa$@aM;_X>vCVvL1(%-VL_|bd_eX4+V!dd>%EqaZ+uw}ra zICcQo8xGHYG~*L|tv}fEm305T=dGNCw?WVK6+`c;?`_h2%btJoUVeu^FQeYH_QJ;L z`_M_`#h7^?#3<$4biy%^+m6VBp=V(?S0k@(hiclay;!Za_IV?Lg))iQkoZo zYQU|T*X$Q2HcZ#UA#SZ@%?@(BP|y78tKrz#ko{-&FfDv+M$Nz7eAdS30wNs&bkddnIW$w z(^2cQG0G>WG$|cZMxDAlXb8vfGIi;KiQ>(M@-r=$RqN!v`Qq5l9T9m{v!7iEXG*i; zPkC>WHyhdfArCdYDIpnl)9W~Mj&jzd>>PSaF2?~l6ps;6B7MMs$P~z%cl2kMBp7F^ z+R;$2G=3*N_cij?hKCl3L&JX~k<9@*D@^tgh8kRDun~rPE)kM z=iA+kdWFO9qOrnJ~!%RVbFxIk=L@ykf_#+QE4k4F7 zpDYP-XEZFPFi612K1yO*Eu&n~{5B4wY+oiWa*RCroMkP=aF+?{=>Vd1k~Oecp7mje z7M;zMIIkL2iJT-`VeQWz)r^~o&~g#=jutTSq=*vRxxkiLmhoSQsq;?yjaPG$5UsR# z33b{9?xcIGgRP5vx3f1SEv$>|ru2?d@RFZX9(SOtC_o>F32+I}yJpC7zk$Oc$@pq( zf)&hv>UI$`)=KVP9dpr3+Rq76L)xYZzP~v`Pzs*qtJhN-=hDyM2TOcJQqcK18IquO zoCZ86O@_)ajWpx^b53*CcAfhr_TzJnjOm#I&uR`Q3Ck}M6@7fqpehievof7>_{ihEriLl0q#{GR+vyN z$wdLJozwgU>#E4w>z7oHXv>}|a@rUIPmQaqk<8ie?{)O}5^Jjr?-Sn6fu~R=CD1$- z%J7sZ<9lP;7o!KUb2CpG3fjNGgh__n+=;i?XIaEkc1upY0>JlK?%qxtoB4vb3F$oj zKF+Hd#udq68i|&6GQf|7i7s?>tO#Wk8{iP!}wE9 zQD{7YfLSuB1`J{LN)N<~zTV<)sj&0SUt%|sTn=i`!l3lIx6W_;(oTE5be8q*3az3Km zgmUUxW#@gte3~v>!netkNzMI2Y4h3@Q_@{Yw=GS~d=h49&T5)! z=D;xB5$6q)fgZwKBcU0#j^j%!9ulX}BG3~Qh~Gkr5GhF_1$d!kgzV4>Egvt0KJ&I* zDU#?9)6!INOggK4J^-J1!8eCdgf2fTthsR>BJfQAx(yb?Al>%t*2fYO;4cu3zVWAT z^v6K)bi6JRP@||wXTt$7^?#*Wo&OIr|NnW0{GV`~zX3P8?c-mstFLeP-*jF3|LM9W zCT9OV*9AfWv3~)17XcNZ(*N4;Nw-YU#qIyjZZcka;bJ|lPk6R!6+Wc~8x z^S`-0dgA8a(LK7#?r)nKo#CT1*L2{TPV&*2>;EOpM+fq_Le#YVrR8a^r80jPHW#^3K*8ILgsnb+ajMA&~|?d z(k{vL>PuA)PM6Xv%VvWSx|5G!bwAafcE5j0@p!7&UMBCh`j;S0qVQYw^{2+~Yn zQk}RiJRBZ4ObIR2tTw-+@@coPJ>ccT>vG@r=s;e&=CA9+$A`PqMHcfTcYfXgY|Uo> z8KDDtvevsVZ^zPpVUM7@1PFx9uqFodESXA1Ng8617=4%KJ+Bd9$XpQ(5bLH=w`>ih*GMFp-)rpC;vSMgeh9J%Scjd^BO9)Nf>ft2|g`#-$?M*x^kqls9KuK=o+V z2Xr;#5LC-~9suJ>W1eVk7}R>kf0OoL^C&r9KBbOIwqM3I6?iXJeVHB? zxPmR^<;;>>3*?TiIM&By!a$%nFZQhz9xq)yqw4lT{zC5gbGuda%iz>%1Tc58i+A5d9A23rrz0ji_2n#L?z1Tx`W)7? zyk%;APtW2{X!t|bPh|w@ZVWAX>ttaD9yJ}AxA3Y$M|na4V&rl{Sk{p5lG0#5iD?s= zSGB-8kK*}p*4UKqi>}&yD@nY6V+&t!_=B_h&e4yk`=>p#MMGMAnlim1K9g>(T@UIk zrO!c1>%YIa`t|y+v$9IBjCH9$O)qIREArobHEXRU6IM&AnikQ$cT|cB>&O&H2#%2i zFbKQ%VHWjLwSLQjWSeU+k9UH}CQenlHs48(xGb&ikkSf`qhyH?57xV?uKVBmY9yS; zBOy^F(FP_I(^j?*4Mwv65-4!nH7XMiYlf^qD0lX9-6Vvpu^4k3#7p9pakA)Wk<@4DF`~CoaT7B*Y_vL^rMte(m0za`)e4sP?X~Bj;q+k-{p{iGBTgDmC&a@~x?V=0?yJiZ?6tr!E*ETCC5EdwWvd9D6uVYx}4 z72f3!Qi$0kgCBxWCpT(jw0KUq^;5B{!ekH`IG`B;;?jx~-2Y*aV-do~3U#Ih?$D2i z!%(lfYN3J(v>NjH{<+&vsjYT@h$dd!pR117g@mzyqyU*GErdc*&K-gITOfjdW+pN$lUWr9JL!xCEEpY_5FH&D;Q zba{1imMTAgk<_S^&`fPbaT$VH*B{&?{K3aJSh2>hU@AZ`lN@`T9Y?1xP<9z%dKPi1 zT|T}6MLIxTXY&Y?7iY;ibK8QAe!XfCB?3e-qkwur*tt)`vGY` z$v*N$1HF$;$}AAE>K^Ua)8cN$+&ldVxn6S5UcH}q-;DmDUyBS4QK03{=gf!HIpQKH zi#yKfTi1#X}*rRZpmkh-T3_|)u&dPLb-l(sDUF{7l=39tLJWQ7Uw zwW>##wM>V4tM~??=dP6d=Y9Q9c~JW1qbqcJ~B>l{<*MskB;LX zH})Qf0H|uS3quc?L&9OEW&{_RV5j{TkmD(%1OnY6@Y#T_Py;!mY2 z42=#sVGQnvY(5T;)RqY$0ddn?_ePZniN?!ThxtS03k=@(4b%PBsajsAs>CQv&}P@K zC#k{=D5cy?wo?g-nH9$tvBNc@IH%hyhKXC`JDN4BQCc=s1_i$C)wO!{V#)OcgcUF= zSX=$QN&logRZ7KIr85>mxF@fdOcIaK;TQ>b1qIN4B}ZIaWqq15*1lbE{A|9fYRcuB z=8amf7n)qUq7>*uy*4Ckn>KgUpX0MRQHY!KDw}LTzc?S>lv8JZjh;2`!dXHg4NpRK zFhNTUc9Zb{Z7S+^fx(9NWl4URfYQJF?LNhIeYpK^yMJN3;1{jxe07fpm>Q_YzQ% zS5%hlPT;|2h#bV52)Ti913;uD>8ThET5{&AyUG9`Qf^ZbWOL19Q!(|17ePt0&Gs~i z{lm zG9xA1h?YJ^>fDfql_Zn%q^zOC;eQ@hPrqg#dKC?E%YhBx1}W%wa$+|1@xNq)W_SG$8?QKRpjYq+nh*2$le@kmGD#-d0ei9RvWhnc8Eab6a-hMEdW&yIg;ikK(*a=5dM9UVAwTC3e$7FpHyMzT zzL-bSyTWpt@*ZefH`sV=6?7p1W(tA$6q zVO))gaj`g*AZ^_IilMS!bAr-h03}K%@i6@5+p_7SBu)83D_HQdr}?g2u1adc7U7zF zH#(gUxklCEmCh^oD&^|&y4n_b^@1q8ig4HK+ z1Mx`7{Tc+qc?^occ&W}h)IJTdIB}`LDnD16t<1_V-!&z8SFsKRLCMsBrij%oDd(ZJ z)xS8HX~)$Q2dV7c$yQxzG2k*E-cWabvV8PN;q#B~5{>;+t+b>TIX_!gFhB za4p1o4K(x(PB+{@b4dwt2?bq-nm`$rh~O?O}KZpM&fbBxb5im|CA zd|8H&XK*V8=V1VbW_bvQg3q6Ai_K|5lX2A|1N3a%hLOx< zelnJapp|iy9z+$?q>J%pEPTj!dzy0hnWkwvpA!k))(n~-!Gy5a?>hqcuaItHh%1*6a81CC*fICnoQdKz9!I z2A+WK$_X!Ify@(;0|d;5P)u#$eOy1=E}mD$6=3A0(7aB; zyNyMgGlXar1U3bQKg`>E7l(_iXG=lf4$)y+3reg{bY2PiZp>sEIr8BLBSDJHXsgt_ zfWEMSBlb6*^U!*XG}gS=6mkg<{$SL%p*nD_3T*{Gbh@jv(QvM1RJZ`6l@i}`D7$Bj zneC@e?ZVe_CkBbgN2+%o4^=-cFMZg2XKjs->yfey5jBS|S=vc$2-y7A zC+kP)E`kiDUxnerlk3;+Upkn)8qE>5exG(>^ghXyBesG^)B*MKW~O`K{rC!wN4ttV zvN9elrYU>xpb8gBWtZxXXY4iVeA#3YH)GgWcsDK%uH@h-&RcRkq+{p zgp)~#X($K6G<$|g=;wI5?EVPHw=0@G6gVgH5UiwFUgGra&U@zMEfTt{X5hIKra`U! zV=_-e(pvRL~UW9|Il*qk^9u3ImE$(qz4Wr*c@@R_gNRZr~PCcnd10poWk zjH)2UvJ})H-~<(e+K|$!Tz63nLEYvbkTEkLey5;@j8ES*RhjJycTpzl2MfwRS`d9` zwEn2DBSn+dMrQ|Sw^6s>iaNsv=_NtZ%AGoo7gxm9GFe@6zA~cxm$ZJXguZh*#q|)r zd7ffB&%p0*xg{fmM`^V=nJqX;<~#K6$2>3garB`O9JCnKHJ=wPII!OmwQ*|)m~LR4 z9dpo5A@)%{_P26W=V|-!4HB|<<2EuK&_@_h>RipXed?~y z-pI*I^Ie}u#&f`7W8OSR-rD+}M{R3{<7-ZnH=Vw%IXv&$d@Ca=g$zWRKF2AYmGh9l z>|wr7j3uE>NiIM&vAzY{=>Sh`k*&Vi({v33pRE$QJr$7VRg_KHu{(wvU;pa;B zluD)V+^$We@MaQfNEmz!7Cwis4oXP>c1qUA8QM#S5O zKPdMu2Tm8Q(S9s1pEaMef3tSQX)9GFpkxX4?#m@f7k2{na#zxGMny5n{o&_Q$Pw$; zS`PzgN~(`Z=u2nRUZM~J6KMFlmxXyjg8HPj`OcS04CfUsEgagOgsP+tt>U4!qA&h@ z?6m=3N%GzBGsjegc&CaAA%k<>^|Wv17l3mPTF58D<#korh**~nVw{+!-z0}1Af6KtwBnhh zl6M)y?~*Nd>bq9dWzY955I3))@g(pl{jsO*{agb=iiG@{+LQOx6MnF&c;~}qjt_XD z2kPZC%p=C-*{Y|fZ&}oDT#h9T_Z24&HXT@o&>vKDaT)K(v(KI5CZm2|JeX?CPajgk7XL!NEn{Du=USWp9b`H6?gIAhOPc>E zZT}qhlja`}o@z_dzkI%nIsNWx_#fy4T>X)*-PY12#)paXf=X%PCidIiE)PW%nz zi3gpSC=GYB{s)j(*0DQX&v#Kl-0l{4dmHOrPKcEmJE92+C+8`=n$HPZb8YE|J>G1( zAT~o@UfVAehn}luQxh)KdwMtXv}Tf8SinrVY(px#X;#l*f&t1#<6GOG>EeNL4=y|o zLO$+_UA$GvzCz6Ktm1!n8fL$D7HWTMv{Cq{MRA}D??r4CgiB;Rx!%|-+WAMsyBr}f z5%yT7S<|V%eJgNq^UXpmCUC&+yj+j-njIHJ-gGizbM4|4h1+YJr-DCyO26pK;N#qK zf^Tz-+6XT`!D7*I50N0H4KE&)93uDXP-QwXQreuM)LcC%1bgJk)iE`O0iI9efyu?~ zGMk8p*rj{rORNVpCwVq9jft1fS(7z#Q+>-ZYPR?mg$bAIb^^4F9D*DxQNRoS~#X2rK|97S} z=8dD9+J5FheeS?cU)ebgl+`9{FxCqT6i^d?W3K#mE{P@HciM|Rbg<(Aj*oJDL+UU_ z7q@71>bFOdm(^%vg7Hiy)a!Ic^HGLW%>t_6#DSF@+CBAUw^Ct16=9=5^?@}p zKd8;Od0z7Xg4uBoGRd&y&*8lm7??XsAWVD>dmP54qWF4mJ^eV&vHy5Wv!n?pok;aI29NRgx{FW$fXuqP0E=zqYxvee%1 z4G-@KeLf&TO|>lktS!#?;A1eOF0qsLxv`(i)PpS%s0SG0$4Hd;aS}`(^>f=-?KK#L zy-OR`4z5uv%Pb&F_i2gc9(s)dtQWAZsC1Ul0t!lG( zV?)SdZ6#kz$-7LBqk(&!2_!SYQxsJ2nqdrfU%~tU@Ux`JnN#g)Y#{SJkwX!6<6M`e`4JibE;|%XVb6$M^L0Hi37sk7Y>ss7%e)T^`<~7y;{p=a~e2_ z0uGqeo#6>nZ3cL7;+3))&1m&x3&YvScBv`p`X~KTWeakU0|AUmr2dAV6l{4TLB`>A zj=1wU%;?n_fXA}Z_utJ9VO<_7kE8BSJ50&3cV#ldVoocC9@Wh z2N@vF*F&0t?hPk*y`+iM)Ic>4#nu%D?3u0q!Q6XBHQBxUx=%t1y@cLF4?Xmbp%;hq0AiYTwK~U*ZHGn9#xbwdMvesT>pEb@{(=?YbsXc^K17j~L82vydoBX1T#arEM)@1SF5GOg ztdoH8Dm9(26J=03OLcLt;b;j?JGKXRMecc5grx*I5skH0D+IZXF_@Bt0a69!LOPc- znt}bX0KGRdLPpe@xy-O2BeYf?rx1}JFYHGhUhq+TTYN4xw5=qwXg?)QU?ZJiqVc}C zU;jp9wayQ8@3{%a0vq8>u-W-RAv`Aq-itQ38%+%Lut}3X-T^!{1iR589e8iojYti2FyRF~Ql454NsFk<-iJSAUMtk?g!VB>OoBWv5N= zb~uXi^k4$?+>Z#`jgC1oh7tMaz+NIMPS1ng+HTcpZl$72#vII%1vj&NWW<)n++Ygr zoxfJmx1F9}QjRQ*4fd8NOJXnUpOkI67Rl8_PF;EIQa-qH$>|x>E2rqzia(k=RJ&@Z z`&B89Bk4TavM*oJ74fh3 zW2Rrec*ZOSP_nPggUP8dvKDPqbfkW93bKq1RlTl5kwXce`*d}L=4xE6 zs8_ShGcvXJRorqOGXsk8O>%H!_8XnciVvx(8_r32aE&Y;nI+dOXx2x}Y5kT4KgZJc ztUg&DAK+Cem`S|;tzZENqnxD-C7ua?5>Tx)lz2;$^%(TkR zyZ29|zpZ?Y2x$mLj{ZpcDEi_4HT%wb5oD0Z`RgmkY_^@;hR3?*#k+PeH2Qte{SQCW zG+mtq>cTt94}$|PXbjFBE797e39;6DImUj0;~xqeL)$+7q?ISVYWPSE?{{qMWAuGE z?P$)8yjWPhuNcEjvo+qltw34u^44*lwsAQ7dxe{V?gYH0)cFSIXgcrH-c#ZOb1kFh=?GTsRn~{{( zt1v>px=6Ej7b@++)kJbiZ?}!_Mw_xP2R{VG@5D&rMZ0rzW0?%(Z;FicC7}2>zf%Zx z!wjvIi(`3MtMDE%cy30@1rKmtIxjmTEYLDBi<#h8tsIUiKL_yiwdwHa^K5PO!N2Hy zq|klkPA{lPIo?-ZnGgF?sx{ZxBRh?E%@@9MPm5(!pL$eliBSMi`n+~jPb&~9xPLEP z{wDR<6NwSc7Ig$=x#;K`1B+z8qgp?ItW0P(&2B8}(majIY<7geO+Q`0{wZ=dw&k_z z%UTmX=T%r}3^EBJ)|N)2ZpNrNp%QWxaEZZ-niyDUkpIvcG%i1|LBgCo38Pcy)l%R%u2lwX8;85XB*oY381_JiW$_XNoi{Cpw?!iY)?q?B2O^^c4gUS(J#j9e6wNl0sM zYQHCXHo^%sN}H9ob_zRm<545%0x83j+^3w3{Q+yJo7A*W$EQOyn0n9hgT2DoW-(!5 zjd-r}fvEA0tUrxh%Ic{~# zK5oYJq+jwGg626QbAJ|`&(nv?&h&X|aOqw*zVW>AW|d#H%zpoa zbM93hwa~0!B{8)rVy(>Hmn_!|npZ-ZgSI#)G&cx04@A(&Xj42!4 ztnh1UYMWU$Eo%Hs2Qrl-_2nN{=okGyZ$@C-5pO3vpp=5R5> zWF0finURDME2(F5%M&)?L6q93b95mOmE>xkTeAT3X*&U;NoOgJxjgH+HmXqQjX3J@ zTxI1Yy&!H|%_^n0M92U(!KL+q_fB3kcNcevtaK$Td=+NXI{X-y7WJ!Bowd{S-OPCr zs`DD$?yEV8nBK%`I(B~<9)hw7Mq&e|4m%Asw<8{bJaViML z)1BZ)iSI{)eMtm@i#PXL1U<_L)Pe}utl~owsZ@Oy!Y6o{tI8nkwM_zkL z*<)Lwtht4FFy0;8*l49V&|M&|&WRR>%W>Z1#=`u%6T(LA=VAmvof=Q)G)}q`yo#&m zovY}$DAQ_{?Oefbxe537#nb%lE?1b0D-I8Cns}X2xKSX?dYIunn8SV)l`}_p4>Qwx zt5q;%jwy81$!yf4dt9CS;m7~qKnIAJkad!3P?=_&njk|(QDsy?mf@7(OV8N|eJ zs*!GAw7^<0+M#G7O zk$e|vEwL{a^>8jI{G@Sz&?52`$}THkgHp?(R?8vesWb0bX_5&?yDC@lTOG$J<4?8Z zQ@1`ms@|6PfMK1#{ZJ#-Ju#%14OW#BpqlvYB@Ed*YR;JEI^qCPeEw+%7IFp_B;Y>{ zyd4a24HjU`bhPOUnTZX`3oEa;-{DQbr1)VIjaR^=nFr(24n%RvWhTn??@6+Yct@1D z=1i_1__fCfNWvDjxt1&M>Q+zgOl7;B6uYq}P}U4Guh_jEvt?jao6m|pkrN`a^>-41J6xhW=_!L7x zB`CuL?30zpX|fz@sWO%5CZlQ8+UVBW?WQZeHf%HDxib`|c{6Z=<-3iSw1oIQ>GuG6 zx@Tu4U{f?CZR23m`FoAw{g44?0$v!aqk^EM{SFQs0ijzzU|A|Fe-#0Ik{M5iyTcu2 zzFLaboGT81GOe)scC%Qby(0SGlDo3>8X`+Ot{!Kub0Z7tcR-y?Y#VLvQ}KYz2+VtL z$=(V-&Efq=*MW(|rVle_UeJ?mGbxWBEwkMAEM+_N9GNjQsv^nD1rwo6OEjVG?_GFO zco>=L7}y%U=NW+e}aWrF}d6wtQB;_~fyqNmP0V zo1`47`tmJkVk4RZ1SHnnmWr-9O~1Bx<4n>1LD8>hHYG&}z|`{)V0Ji=YopZCqy*m` z{OxO$k-o22+#JS*35~mj!@p1!7k6LLUg#8_WO8bXXTu{m=M^IuT^nX;^&+!f_{~fo`F&)<9PRHQ?P7H&_`whjEF;h zDGP3oDA@9F(%K(=+)v?5w*naopD9R+xl*1vBt{xd|8|{6@4B={5+k<*ak5=YkQ1@> zFcr1RF?BdwHjpttP;@{rq-7R9Xe?M@bJ?TlmkT}K#4w$N(z@Il#GQP?%JFt(1*KR-R>QiU0Ia+5xVng99>ari9lOT6Yvgx^F%ZWHfh z5~**Q0!4DnT?|npXcceYC@I+I3`6JFVFFZb|LAvNpx;4>ZQm()ut?UCgn(80&5dvqsS_~gvBq$F&-#SH zlu2J3gDlSgBTPc;VUDKC7x7F-h~s_v<9zl#LGR>JThn>{E42DuUqovz%@KFAPjxJd zVO$s-Pb1!SZxqjgcOHq_C48|nYEP@nv?BtAM+k`#7`igSlhWxGqnX)|zQgBMIU{TL zkMiOwX-_3Zpjha3IMfSm4t{r&Lg?$T|7pNYbO0+ot1+3G)>weJJU)9sy(AOis=D&-amZVd~;@n?C6CiYgQZZQfzJ&iS zE%_h#LfuYxKnG7j>Hl`1>BM)cRK@2%(vogtf8h&RncSzbgthr*$V2Bk>VKsr&p&Qi z>i8R9_*D?sh20pd_-9)3U-&`_8a^_<(4!}WS@PmU%=+4Y=w$KSLGLGGrMCaZ7hdT7 z@_ve}lhqFVr%o2u+WYu$8Rhr4PB!dGH(4i}bJX>hPWF;0IW39Znyxv2_xhWj)BVOK za$54;w`~xDg@kEjtOCBCbUnFu>D9$ASEyJJjK!@;;K$FV<$W@~koXe@!hOR5XRO^$ zHz-9L-Q7ORt>SPHE>x8#jlDnF4WNzc!!bvXETkau-EMfsIDbKB!TLtQ2;oqUwRCjG zqxCGsDYlJl)q2~F9L@ZqjhmRn`HftCd$!Fy<6gVXd^1GR<}FM0>CFP$W7e%g9s8!O zqDYTjksMci&(>}4JE-jvo1&)eWdFxbo2A(GkK5&%LjhvZQQK-874gaqAMXl&72k=( zu{p0*roHvusiqR$^S_>D*!-cUfDg4>w`bS9>scJFzFuGXU~9K=HYHFZqqcr*y{RRx ze(xUX{UeEeht`GO<(6(;pZy1ITYHi=Phrb%9}WnL9JJN2-jlK#QU1BqK0UN~&}nEG zBz=C~mwV~a%3kAP7wNL3jLB+|4nO_>R~@fGRGw{PPWrsnr1q+ezQn=|Q)+;2Qt8Uy@e=fe6cmAUIR=)Ffwe74xbMcQA zS3jY4GBdQ*cT-C5p!S;XX`}XWy(IO6xG@>j;iKg#u}`sk&ow?r{b>^J$S>*dx*$t5&cFixee+Kk2 zi2OPlzbpLv!rMQ>B(uF6=l+Bpf5-zkFd%R`PG8==h;dN}%9opPIl?^$$)Q8_J}ptB z9a)Sv79vvwldc{kvoIXGOr>eblJZq0mc@dscXCq_B0N&vIrKP9)9?cA0%h130p9ny zsS<3*RS7r_eSxv`G#dGJro2UzNOfvDb;LSzRT@(KPHKi-=lbQ=#k;tF3|#nfdcY0!9Ruw&YdIM{QfJJ_rGWI{(k`B{RcA__y02D_J0|Esk8 zzoYVGrtQC1?0-M_Yw7;m#@v@g`F{wyb3q9I*|_(=kGd>bAt(Pyo^5z_S1tt1YuefP zmvOI%rTHJ^S#2MQN6oMAIdq_HdtZ!LA4z_C z&buT;11{ktkLm5|Wm5oL1M!yX36sST>ld4&Ud0}Sm{7vKn$rjJKYC_I&n&m)Ofl0*eWgi+*O%j1aveDrBcW`9_tD1mu9gaV5P6Zc_TsY2*&5pt%N~DZh}(jh2=}&eBx6?L!8jz zFcUn2r0^JeVXP9DU+QUGKm zTAnb>YtIAV7(C6wQOuxV0G`b9R0m=NzaF&`Vm6VdXHsb(R>}}$?2*cQzJ-;5h&uBH z10>Eyk7S;hv^H+CR_17klFwtO1V7I@+;A75B|vy@u{H|i{<<8s8mSuy(_!N{qqezt zj?(=NgG?L?f!+R#DV!m=rSBy-XE!iROTlC2F5rAJZw+IBI zpE1wc=hDGLIND{8ifY3#<70_11VuS<2*EipGsqOf!34WvrAUJTWz(rBz(o5KK~o&x7SpPk;%a)o2`lU{x=WgL>>IQ&+b~V(7|HESkgQ zGbL~tJVjQsu7QcT9)l|decTVA#Bi`Ly3Y#at*O(~msw8|jwyHGM8&uJXFL^8} zKPWCu-1pFgk9bX~!9zQVF`N7y2l5kFBK6;SM|^2r_>`fbO~Gr3YS#WbF-2Fs7JX?) z&C)BDK3Nsf4nLB$9S!6$QhvnSpu~6sF?Oj)Gzl*2j!A&9IC*#@i>Ic|y)SO`^KN1w zw*=4((9(;i>Ij?w#!tScc_N%IDnVg0Cv>`o&d8ZDl{Eu`B|R>g{83%Q3CpPWQKLwI zw5X>{)dHdpQu9avV;^XwRZ*BFTOOiE&WVsdi+MS%03|*crT~F({;D!2@dijvYo0o0 zH7%V8!^s#6L>B8rPWdau z3O4ZcZ8Gy$m@)8L!_TT`z=OA59qHo;_-NW*`SNVWoB3^SsfQo{#0a=3{P}do6 zvV4|Jv-IW#h;;{WAn4tn$Gp1m-7Na2{0=Q1e{K0 zB}{n}zjih;Q3bh@^X29AkHk4i0AhuZ5Yo*0xchWjMhBcMwX-A6g?#B55tLu{%)>mO zd?H5X7Uvv-y=y=AK~%%}ce(mqy^E7FQCIH0`xJLu=FRU1F~enr#QbkhcS15^=f>ZE zP0V{TOJ~iO`__#<*`r%B0pJK!@0*U-fAMQ;gmsfB0bU45n4U2+rT>zs{#1Zcv~Vo$ zh9j@QwW5CAcO%{lZ5&W^RJsPeu$Sj-2H+EhF+$J%_Xt3HAHt6tbUaLr5Q3LOl^-k>VBLo zrlGk8FuLo7quxBVdlX0qC0Q2_b&4qCK!dzJ0C%9fSn2wNbX%$2{b=Tvi%OEOq9iXH z6+iJ+`Iwv167f3iywt+SXBhg<*hHSUdo0MCwUfc2k5;5|@ludVa+-y!&44-aO~vuN zV1$x^PNJIhgfR6L92nHPjr(f6Ru=A^xCbB;RT?V;g}R-6IJVJgu`sZg@k{7k{K8K= z9ngUC=VvP-s-|}r z0+qN+5-mhkxJmj1CS+{_NC2ld@~-@&=*DLuo#G@5gZ`DB>DxFD$Mdw)21!c#I3epD zaL>ExO2OPJ;Qg}onf8@lO``&bI2eW=MGtV%hQxP?^E2zPV4u{Lwjf6%VUe2h^6|3q z5hAhHLYuZsE`x6bLCWVh`PxBdRlCLM=02{#{D-V(p|eB2%iD|)20aZ5OJ>9kyT4o{^dQTC%T?KU8uKmUs$up37oPZE@`Mbhch)P?-v{v1nzp+-(5 zaL15jPY)yc4nn^^d!xf}{bpLlm$H0P^9RdiirlJkhlH!fo?&3M;SfpiyWF-f#!<%3x~e^L36%{F^v$72aPLy0n(a*On49og%1<9b}3l9S_>MccW(H zIU>@WG|})FEYt>>tbfL2q{cQOg8SqCCn12I_}K3OB(S#RsICO9~~zGUfy zXIwjCmk_w5@{ySW3k`?3v?#~P9YJRtS(Kxh`)7>6o2)aBoMi-jRl}V~P~c#ZIbAi? zky1@XAi5QVXvxctRLE)2*9pT>AQll@-3X&OrjKdvKrG@k%}K4w>62Un$(Xqf4j5w7 zCHw4Ac{vtd9MrkcFw10lf#|3jzy_IUz6q)nf0^)_^n2iZ=z`6I^ctaMpr^z+~LsUY+eD7 zN3K}VP(iV`^J3#IsC@ijYVoX|)`Hh9bvd>ViH1NWc-ua@C=z6|3Zig_*q&UWV&_M7 zC`%>lmrC5is*L1jUWNp%RRl8wx@fqM5VyuC z^9|bcAYSfeQ58GleVnIW^81MaHJaCwasPhK;% zH5J-Y=le}-?tNoQNmhPr1sBRanr)78bmbqR_E;*H4g68<#>Tcj#v6UK%Hxmd!DCFBbuAT*ZH7$g__ zDN{3Nq0U07fKZv>ngy`cMB&YiX*E?Fu|&j}@3O$fra?iK?nO*hR_gC=)&N0nj z5b>5X1k6IIbJ$v8A&L(k*6RxjVI&L_8TXka6iX5^m?chO+ha^83Q*oY5XJy5_#K>? zCaZB7nr}*SG*T1X8Y`_aDwOKW6Kg9h%=524N^?5VweXRLw?bpk@Nq?s({5jd!AlT_ zya(!t)*pEfhix5|0P1mmqcV`5H~e~|9r~1)@$|Ll4elaaC9y||T? z!`dANMxunxmRMz8P-&0=ZQoP(#S(b?baS^Yuv5r8K#+h}5;SmJLl@Y}2GdDBF z1gkz)y(r#FO6I5z@cSbsYAiI$9d94r+Z2Qls7FjP0g|h*#$`nOvKv3q{n5A^SHA!s z$rJgLxk{1%`=i6HJq<0!J14Sx-%-%&Up8S@yQqYXeP;Mtu&u6< z@-8?k7b3cL>rE|8zY?01=9(J|R7CXiFclRS_L+l4FbMw`w7<)`{9bc^evIpZ70htj zHptTDyl}x|1?R(`ObnjN{u?2yY$;R9{m#PH+YR@)6N5N?V%a|;1F>P>*?^zEve5kU z@ul*S`rv?Rze&$KL@+oE{lYR{KJcXC4+2e*qyi!bkFlXVYkf#F#I$-pDY7!r6;KSM z1tf_O=1MDkUl~WC>`z_DZ?N#v ztgPKIO!CsNnQe$|$d`}=sm^;mc59xnDF@M(Y7{8=6u)$`%8fieyDHsx4hxCOWDhUd zg&7)D2eKndO{=6lo?Q5%ACwoP+%xQ$%2^ox6#|B`>By>s^2WoBoQ}jeZf}0oUg}MBT5xn=)&TL2;6@3uH^5XWL382yx z)E>{lo7QUtYTtD1GZ`5N0aV=}Fn0?{r>CiK_xV^gY||_0VnW=)3z~6T}G(AybB=O?Y={qMkKxy+l;ceW3bz%eiOox9hO`6y=dulyf)bgP5KG4i-<2T7U#j!Z)STaOV!St7 zB%EAM+j;tIJ$E@hcP{z=iN)9Q4&Q^;}vi=9_hZw1Sn(RPU$SSKC+gjxEY)>DUriokptm6ybSdZ zwB@u|&Dma^cm2WD#l!S0_2=wJaZDuUPJ}@BD+AuN<6dAI(n`5e^>gDE%^F5~GfZq# z|N5qO&Zg-_*1JzO&1IM`gtEoOKDT(EWk_PljAgdD!*Y5BsyeeJ^PSa6n^`hf*i9gd zXl%!df1+~a@pSo|u&?LbJ4GMNmcwPZle?w@Xqy!I%^;rSE1kEEAM^;J*9{|ybt?M_ zg#&K5NlB~7h|(tta%{8N2!j1HUhAFZ2_6eJpSoXqm(3Bo9N-{it;Nyus8JVEj=}?{ z5;e?kT?NtHH&j|$R#p#S%PLM&3ZS8DU%Xs0k2_GonL-ShsPU`rQz zp65GFE#Hs!!kUoB_fV4;Urb-eK2kWWue&fnkI-+0UAvX8=r_Ki{6@ekHY|7W`;`Ol znt{9h18mGjll2z$Y}2z}YkmAe7D7KBmXKss@52(}fg~OvXF^sySB`s5&RML=eBy|y z9qUzczOx3#On~CuhR&jgguzgiA53TO2RZ)0xc&@C!HG!7@oTnJ%pnsu;mi2bprK)6 z{3bDu4tiapFi2Vdu!BwO&0QV#6bY4gp1(bi{E-SO2$!5g4JU9Y!Rbv4Dn*X4TCR4J zLZkhbcnNrXt*mlYL9EgIhu(z9m582x81E2IKP&=e{}uCXEa1Zt&ZxAEAyWRz z_{AI3(372RHb8+99^&$v-pmP3O9=%sum)vbu6T$7Eg0?0AM!^&%Yr_`s-u*$8b+@P4+L@@Bo$`q?VSrtvv@#vX(V>W?qD$s zMjI6j_|n1J0e8O3l|yh`w=Ll4Pa%@<3S_uq2~;LuNMXf#PMN*MQt${`bN3gY3>-Pa zefpy(k$#~A-DLtYq}WJLfJs$|n$PTPy2kVc?sm7D+){|{-R2ZgT6r-z0QK#}>~LTx zc?E8L5QL4wz!#szyTJKe&(O1JncRBGQfi)WAMvViWS!|xy?$^>w#(qdOt2BZ8&~By zbB9p9Sw5iHF-gqgPLKB24Ud;8cP9uNbfU!>K`(yiQxhoFk6VRWWLc7lns+5ST5BdW zgBm-s=wtfn0jOq3O^SR?uE3vTjy3^C{)wc=tl!R48bCRzN6Aw0N+B44X4HG3sh#ML zN2|{It5@YygL3W@ncH9d0rRvHC=`q>bzr#2VpP_WSYw%JibwXoQ1p}oe^X{mRpsAy%t?la#&(Yv#bGyEtVWqp1| z?~C;Xm90)mwgJ@+n|2A-N47Q=21>83P3#`oIb0%xS06YQKC*WS8&tA#jwF+3FWs>F zEajdm@_5ntiqcm*e#H65OSXA+kznPa#d~kq4@UG%v~JreWkAJJa5S9Wr^*bzVkyz) z0Zs3U`hyi5+c5m!T${y#c5HPG$ExaW$+ZtAgj7Gn|>ALnLJD9k@Cp`#g)AW-vBa(L3SszSNkJlyVpGc>l{IdGXrtzH=v z5>NW+r60F?J%}iQ>d^i6r@+D0j$>Zp(}Xb;bW}(Q&g$$Hbt#<44#A6#nJXVj`w?K_Y}xaJ!-n$7dLq#ZX4(N)8MGyjqFXP z<>?kl69wTyr|RC{zYX?CbZmmBuOQg zV>MZ&C`~e(@1W5$V?a{|lO+yI8-c3&avakA1US~RrC3(8xXg-jVKIHQz?VL5k|7ol zpT<*P0+9NGA4LRv=mlk;RDKlnRA^fbMzI;|v4JfCnJ66)(%bmgHhbQJm5d**?50JF zJM8YMJH6qmqv1s&zQ8;A3KLTTPxQ+|R7({FZF~|mv^@#$2nwLbB(aQr!{2|3qYp1m zfW2SkyqSJ8e%mFn>hW#Z>il^szchEISqxZ-#UoXa$nnd&UNgjfm!RbNnut;1#e~_UH+w81?n=wVcx-Lis%>*#HQ@m^;Ri<2ekN^WJK^9zs zDKh2x^deQO`{1M@d}AKy?ebfw&$s@iVvNjbhERtvAw^>(f%41G{KW0!F-KovESJ?w ziZmjgVI)=8BZ7l>2Y>>)?TIp3zD5!zMdyFeL|x(_u#Ci*28*XDZ%BPqyOd_?+axde zI|)2YK533ddutO`S*)ZWJadWopMI-+S#RLP1q0B&^J|(nAl+rNWQ1 zspnP0(Os!Y(TJ|S;hN^23PE)zqgH97dng@X0EH?F1mtM|oTLC*T6O)T#GirrrSb)3 zo14N~kY>wEjsvo`3uzGdNRw*z_S+Z z6}$0ihYcq-0fKo^Vc2HfL9liYAM%+E`^dE3_Sr!JP$=s2DnR?4K~erP1V=~is;UWl z0L4Dk7r?#`?!C``L8|-2NZTTvmlxbb+@ypo5^~vRMnF#O6s&@j5GFUBdxgudqR<#5 zczMT#_t&8g_iXKbz{Hrg&FaQ397F`3xfI3LLe1s-^mzh>H%Y$KOUYM>g4KF1 zq@x_e=69j3WQs8X!vTr%Q%fp&bpsYv{E^3Bm;Hz1b>*mPR^EF$vTtW@KDMn9&zZWdO+(Pm2U@B=&<)t@xhgRl4B=V^{k0;Lux~&U#BG z7Mk<57Z&Op-x*$McdWZ45d7L3+Z?5HeIXaj|5f?jw<6Xi@1V!!-`(jL008=yAW#Xw z1l@AM&sj;5);=&Q$VdWDU?Ny`dKGlZ-$nn&F*`?Z+#0{1VlkaqwUTYbX0#)iqjo@0 zg0*Q8nfkqo<}yxhRb^Yns1O7O|82L00z7}kZ^Fp&n-ZX&+-xO-^3GwDj0`n(O?7n5 z^z^Mw4Q$PfoNbL=?QN`$ot-S*Ty2cqolU(iT?h#<4-GWOhM0$kTE)d$UyHZ7e(m3) zX)>NJ=1SPrxJYtP#}1$Bkdf(_lk0Y;+U;(QTUGs~x@Pyr`|eGxUJoC8wsv`SJn??? z+^37+)6?(U+aK_9Dsc35@c5gM>31PBA42EX!rp9!EpB6%K4O=4!dLdg-yMX%Ka5!Y z9J%==>f^Vlo$t{HzoQRHS3dnoPKqZ>hBMNWva(XhUhC@WiU;?b+S=}up>3U=tz=eP zcUQ-gCtc5;_6!d84-NGX4-b%)*Q29DWFXtj^wjIuvu_sW-@jkk*jU@xSl`~>{5Os5 z@bKW%r$aK3?dXU|_F$i$ejzj2$nV(ygGKIftNpXbO+f)cIo8wm|F8SsAkJ}$CMzjY)hkT@(c=yrq)LQ=vt{6#>D850g(5Eh zva8CuR3aBogA8w*^)s$bPmgd2VgDIVs+YB$KBZR1<)V!s^p}r)UatLZSDoOjrbbrk zyjTUQhcfuI%qdrqtB{-g$|x*`u|}UBsVt1Oq!gi{7^PQ;37GBa{wtoOalwo|Et5hh zN`I9RJe&-c@13Jl81F>Kc5FU19fLcptd8~35b2%IU(UUg{T#Q;##uSv`CagP?w-jYL z#vqwVKLtI6+Q&;t67V?m%Sv8q09(3cg#kyM2EI#Og>(+${00Z>l$D7+IxYtnn!pfPw(-Y#5E8|!HNT01Hd-24qrHMT_BTOrs&#&h`+&|mYGzk6PQ{a~~w^e@g z-uI7+mt}hUDsMlZ-DwO9`mu+q`_wz%GORtj-)SNF^AP#^q6QiBU8`~U41ROw^Fx|2 zO@o0a*MA)|iriH{ndaWq+?#Qbo&Hkea!KoJbr8SSw~DI_T4&{%uQb2kE_~HP&Rpi#BxUS>(D;*n!v7gaQw6Q(;8GM;4;hVBp<_7-Ns;acNR%p7 zdrsnwj@3Q%jtX6APSUj#_g&103R7ZEa;(DIM@#Y{P+(5Vl}L|mcYhU5>l}P!$J!PO&2Ss{zR5mJ$Q)gxbba=M5`YTI=Gc$co z)*lj#mE{&Pvb+?8?oXqXm8LSXFGbe0tnMhOC1&Kfb!;>cN0hYUGHynl)YJj#N;>rU zx%LX)b&Ms(2Id)gc9EMx0-Q=FrWyH#otsr?Dy0jd`M1nZYO64urdEiIf(r^;;#MV! zb`R1E???HRx~D5TP30FEc8HW>OU}D#r{6X>*^*7*RP<0!FCJ7By_HL)=p%EhL_2aj zzmoGpz*JhPR>!tdTZux*SX$Y;FQQq5bcOJoTjk0Meks!>=9kUWDwHBWYOJX##G0nv zIj!#Wizk*SUF%Q1D|_;hJeH@L(3@HbRYF~5k3c(H zW%H%hh@9*g>8Q)+ai!LZCR*F=wRX6@LsYs-+o55wPcI}Pk4{6M$$r?mJe z-E*1VYsLK|uu2Kg08%>SJ0=6b4AiOWNb8!aT(Hx(b2D>vx9|#d35xIviSh}*5)c&^ zeC1jg`Sypy$0lc7!)M{ra}vq2@rtVAyS1hF9@anXxYzdRe*5DGkGfkQ^|bXoee~pc z*V7l>FNX*N!+k>|FNenl#wLef%}mTM%)EXx_wK{;`o`+U=GxZw#@_zU-u~{v;r`*L zgU`fI#G}v0CrAH-I}v3)KprTk^ymX8Qh-pd{@5zr1bSBGC`!5YffQB>ED=elOrzlt zKn+Z83zilZ)|QqwR#vvw*0%Qcjt&maj*c!a zE^cmam;M#e^YZZU_VV)a_V)Gh@%Qr!@be1{2nY%c3=Rqk2@Vbo4aJ6qg_GI4SnNO9 zyO%FtiH?rBawRq}P(vp(0va-Ll^X27tDk|>YC6jFbM%h+X)l^rLum7x_Z~TXF{=eAf|DDGB zzZ1pl>+A3DfBEv|z`(%Z;NZ~E(D3l^$jHd(=;+wk*!cMP#NRbJIXN{o_3G8DX>!f{ zRkO3Rb8~a^UM#i%W0+s^#V7m6eru@87@w@K>#_uCA@GuW$TSo12?k zTU*=Y`uOqV&R?~=ySqoO{r&xegM-7r>Jzy>fBsA)5|93>~A3uNo{PpYC@87>kB+~y8p8G%e!TkT{Ddf!}6To5ITG^Kbha+84LUsiy zj122)-mUrkN&LzNO;MuN!#9z1cbZsy=2CM}bcQA(LjODYSCBErMxd`y5g<6GzyP>~ z2iIGV!XVr11U=KOQdT1Z+$zR!%X2FkNo8pdoHa$+-G1QAz`RjgOtPTKeNH!4&%z72D15?AX#h&% z2scGG923h_p<_x(QGycA16wA-bvz0p`ilYg=}`kuyLAT^9$dS7aUH?3ww$qz3X^|c zd1(n=nI&h*P&b0Zqr`p>Ab<rOPPoN! zsyCyG_F=0;CX=)J+kC*iRgi|eATaQ+&Ek3H2BlbP3{CaC02L$|m`!II0_W~?mtZWX zABuu;!$MnuvG<%W8LDkHF?vV%7Syz;NgF-drom8`pz{p|1S+V37bL{|5><`2$29jH zDJm%)u;xq#``(b$$%I13`CCtXniGB4nr+uBhI@`zO?eCHAn5a)-U$0am;Us9>A5BB zrRV;g)&|#VhO|PSln}@{(_wB|-G`ER z5ZyRx&Wq*i;K^k6(6dbCSDT;iwtXRflE8u|QGi=Z{&|`DGi)cVJx&*_1Jk%)B%Zs{ z?r@2Zh5(c5o#;36;fvJb^W?Y9AD1p3P)fG%>DEp0nX?X6+b&H)l*6+Zt7)WfVCw9o z6EMg=p&sLEh~)hjd}phvS3dv#CNCm0x8CXW9c|W=c+QfhwziJ+lQ{g4*`lJCGD$4F z=A7CYxLa=Q<1tMo0H;J84JOC$nSS^-(pva{k(j_3UkUO^Bv3jN6FDZ3A);sb)Pck# zByAOCkp7G4Y+^FXOaP{~gJ5VTrl7MtCLD_q>|;c{>V&|$X^aBz4`M0_(@95X<@plH zb(Cf-ED)AIG9aqX1nAMGKdH8)>6loj-};dTepQ!jkE#-VCvVKBT?GPSFo>R2X8j?@ zp~T=BkxOm599HtfW^Li3Ae(X0ot88H;bBS(+gV%#uH;&if`{N^a*;J9uuf;KMvSt^4Mi^5+%@8oUOPWNA$Lr zi%bTmp-(S-Qx%;w&9wl$C{?}kWM1DvClLrAK|q#)mfRLIUa zOGYM94AR0wW3EGoBuIr_E36}!_kVCG4G*!C_6{L!lK|M*D z8UoOTH)=t3q^=zZAsul2P^!u#N&7Le0dE|04v~a|?6o?#87qT=u}~(!a%o)Kxcb(; z8b5_;0m|CXML8)g-YG9LM1%?Sf38}@!#v@M>4R(`#<+Q+kn`a7`Eq}PwA6}euD0qM zx<{ukrP`4=h;*Y4gTx50H4_A++Q(Y*49d+x#zJc*o=Y~{GAW>HKr4Rh`IAXM<4Q<( z=*~xuTnqM2@^b&E%Z{V4G8fTbHqW7k|WwZ!@~6Bp{8xQDLD3na=~tVfI7(Q zw6GN)5F)U3gea!;L2U?U`1tDHD*g3ELRvtSI+iP~EG>Y_w4##b)@9!|#{B1M$7h!5 z>Yo&X%Bi%U#vsK21D0P9vi7{++cV&pgkiHwMBeZlnLAD@wZKx#8}E~K%Pje)U*s-b zEb09{D+gpvXQV!D9t)IubsNOEjRFFZHkN})b!J}ZrI?v)90ubS9^Eut-b=g?m>$Aw3(B`C3X%^sc>|h>W)&+P@^C!x1W-7-L50 zVjGlN9tsw>abaKhRHW#pb$H)iV=R9eb+3gEpz;@Fm6lLqw0M?lOF^J7^k4k0Eb4Xr z)uKwxv>qKgcVjelpZOt<>Tk-j=479XTqr3kXZSNV>^xM7rrZ-wb?}T0iq<1j%BE457 z(yR2YCiG6I0-}aqq&EQ-LoXtNC}0mwP%NORh>9oveD?Ey_J8lQ&+HfHev=u7FB4XV zbusJytm`7fMjtXyaD3Y2za+g-Tui(T4BMfK9SLcloaT8dQ{`x$Fp1iX7we4LHkkK( z4_{1{Falt$%j!yyiDhFRZkYaSO3&!24m z3Yh-&0hyfYWN@FwR~2!{AaL=8-+o)bOzC0IPO`248nWdOSodk!AYIs?(^rr#nrh!N zrqOPvWDB6{U4XosQi!Fp=aZ57D-zSST611#Xt{Q0F)9^jThj>!SA&~Kv;;RPd24P9 zdB{wL;2+u^O>7lsk0C{>#G=^-wC)${AO{HVzH#-CISL3s{v}u$S^?*Sv|F?n@*#BC zi6(~88Al|T8G}rAG+F=`AYGc5H#A{uDXE6CATbWNm1uEVW*!2dxe7VsPp69jcmrr2 znW*zPM@V71_B8y|boycueyQk0&>`Xk8d(Scg7)BX%G$?|BV$h==zJ{W;iP>vL$bps zI>|muVf_*kcq%VW&y<4X^vxo#8pp-rDmk^P{Dmaco4gzZ3!{1>$JB9#&0Y}{Z^p959G_|~SY6DXQe2={j0KX) zBZ|yc@}mZdPL7bij9VPmv6`>3x@Q#GEk}dCS@eJtLdG1=mU7`~SE8)O3KJ#QIh?F3 zj0x7fuJN{`5jI?+TDc=IRv&i0YA$u8SN6gJlpHl53yIg6Yy8{!}UB}y$Pad8;9gpr9hwbfkbqDxFF zn9aWPWf{6gtf6?8@ixs-ww&qi5v7VxRM^rzMaZ5PmOR@x6J9{Ca8Kf^sdzCVfD;*Y z{m~VU7^yJ_cNZ$kn*=z$=azJ=C-?}@hh!Gwry@31PamD5iv+lI0Th+L7uSTLG&}hg z*3ZzdV_|EsHYx;(g}IPjnETz$2ylJ^s1A#DuHbvN$hT*Vg*}9@`yk6mATCe1+}?Gd z1{w5KYUv3-WyX`Szfx|oGDnP8zdt|#Ec2*A>g3b~4Y>Hw6RvVG-vSt*@T-iIt!SuN zO^{xVR|fF&0Ls|2mTAeq*G|GY23&&Guw$>a==KBKfD>HMNi#l*1J^#oL;(~5+ z%D5v;n`8u9SpL!xa8oQQs-Z@_@Ji><6+d(gq^tH-;YF~%RVbQm(13>H|ZVR>TyFH5v+{kqX`%us5o$bj^**BJmR(ao-gx{XukABtW7k-Moeu;2Gw#@x|AT9y{V- zQ#+I5jN`8SO5-G|x~S%jZez4Rnr@}yDu8+cKvsFZ32N!Y3ZaqRd+ig<9blsBVYm82 zRS)vK6FUhC2Wwz;_N7h^y| znP9ud&AJRyi8pGATy#xl;y{!G-9dXD~aJ7u84Rz_roAC0R1xKYd zdm3psG8Dp4hY=|p<-~RX4B(j_WI%NGyUZI{Z#^Smup>64vRx=3VJ{;e*7x)vJ|< zx9Omp zycb(eUEaiK1@OpHP$irtysf&zT4<29178BVF~TTd+^Dg8bO1k=9yvl(GD%BC*NKz} zu8nACG2^5g-r9{FUvd^p1s zl_BJ)<{PJsl<|tu2{~Rr|Ei+VgbB-F>0o+Hu=|wq#@)wtu`4AO_;{;hE~v+U>%C3os9`4?bU zoJ1t;%8eb+_4GFB&$&AaK^f_ej)s+?BYZ4}oQ3%E>UXwsYy#v8y29`FAERJn?sM;1 z9qb$&E>@KN7P;&`$W`o$XeW(+j8cB2`ycm0*{W;#F83FX{=Yf%UL=zB}< zmx(SF9`f_Ae4(vqRVg^Q$5KCBCTt%^>7vvj#?bRc{r#lbn_OQl^(%#1Sr zOZ;`)A01kx;RYNiKiIXY-nIAKnU!8wW-1viRoz?!DkLGV3m+qsNdT=qXrc?fv~yeDj*k81LqVhNg3f zh?*0R%u9RV;h$^v!m38j-qtu~fnQDzWr}|GgLgh=Kj54rTK*}hk>;so@g#xQIiWF7 z;z{5GgT6f{G?EOfM$-*aJ@cOj-4z48P7kHlnQRNvS?>CVT17W5^tz~~d%MfHyBTYFqO=LHU!&{O>?uEkH?3QYQo5TJ z*C!sag&W4U`ukBK#16kzqtl+(4E<-}~ZHlq@ceH!Rdffyee{{?xGXHqm(ZZkZ zdh^0&WjlUe#XKKv;&kP8HmQIa+Oo%LuCTR!Ut*hn*ZC#pZQ$++JL4Zx@I3^e+%h;~ zeV1|(5jr1(AS#^x6ppLl37(I=7~AEdh~5?7h5-8xo3Fpz2BuO`3e@S2u=t9b@%L7e z9vDk_oqkw@1zn(^Q1`$f9NakKwdv)AoRv6mAzS$ZheZw=b~gcP<8ra*#eq>DXaT_^ z1dNcQV_Dq`)u0>6IO~|xr$s*Q!>fJr3wkH4-UA-#(Yf&sww3@XOgOQM3FM*5bfV%( z0DJ6}wGh(@%gv5YpN4-eYXzabJW*t_I8(viM&W18SDzzBKS6=fiF>Q*d&t?A*Ow1p zH8n7&h!P+>0~VR-sA0y*w!z}6PTG*I#E4+27(z>;ex&0wTEpBj;}+!gX-IE`l| z;7NfypHx7-x6qk85x#m#S%bcC8a>06Mr#~n^ z&Mw)_1*mjK)GL5+MlBKmk-g^98+P)?nt%xW*CMuxHi48PyMaD{i8XEX>JrBcP*a?>YIDo%51PDwR&hJ) zTF7AhNqWaJ6H2~~$sq$l)2K@@MPHVPrE{7#@yt>Ikj=@j6sY4ECXuEwHh#Gh(Ok%C zSX$<`0dfKgP*|-a(FaRx>7AsKu$hiesT3B#Re!w^`(!i)=_IQ)R;JE8AjE$1AVpzz zVpeD*gEmzn!cl?FHArC%wP{6LJpc}1wujzFH{uU_0*)K)|fYGaW`>j(14Ws$~T1#BDHKmb+NI#&#yUFpb`3e&F-stNCdMvWa{W ztA;nc$dRm={2#iYwQF;qwn)YL;xQ|L()~p*Ulaa7qY)z$T*cc%P9O% zAIrX&sVz8XG{>BQ>DyF#3c zVxNc>2Gz`6x9}p&wJhp7b$;3-ET(5-_2+p-bEP+3xE>$?0vP8UVGQlIt>Xf+X_-7oc>0;YBjS_VLJ6l5uk0ofFQXWPo^#VyDdmpAhQdWf!~2`$#RAc%A46{nUv040D*w;#xnPgT<2U! zJz*P}=^7fsW`HJeo&mbB4`%A~g9gqGCu-Dka%cR@PKJXMHoEy8CodV&o?NwL{f z&C94_;JqPXY_aTQKoCwx-?a#<|U{_c#S=7IAG2Gr(h8n7^rU zeACjY2X&5=f`z!=ZcysE?_C%mlT&>HZ7c+4aG>VW-$?!jberpq^oM6M81;t zm#mGp(}byX){@9NX_C}UGO`^Fe#OX~W}kE(t6J-%7WeqV6~d)YHv|9AHT5A@6ajAi|w+CWE60*d5NMqJ(FtUP&jk{0M! z=O`BVfk!wcD6zj=VddOQh1j8=KFy+)3qD_ij12U~HzZEyz5N94=iS#(Iljdu$iTwh z;f;rcs&YTc4(tI?D5&HXbzmUEPC6&w2_R3AOjnY4vhx;?_=PlhmS;CB=0Am}?A zPx-E=i7U3>dx;E7k#=58yZ-g9HQ4l9C+}hpWJXnAOU{sKp;HnNcF&Dk88924wi*~z zROXiBaZA8G)cb6Z^HZTrHiK}`Dc%}$_i2c9nPOkI!>!GsJvQAqQnrXQkQJdPI%E+X z;xlnY^>cfy*;M|=}v z$;wWUWN-XV>=j)xm=RjjK#$pxN)$gNtl}<7*>^-X<3uL+b)O!)VUcp+6Rp_qsgFF) zf`O`k=Gw(a7nZIcKN8*a%%TgQ^TPJ9ym+7gsqgNQ3o7NpB>&lvT$|@u5kwjnZUld# z6+|!F1PmH#*|n?EJ%_+hg!jS_N)^G__Pbx}Ze5qYBm8c^wDSTJ;MHe6O&#&`eu%-V zFy)jRIq>!-@Fl$JkHnkCl9jcBh;ABXVUor-4KNyLpm^Q-6#=xzf|KJa>GuG7VQI;l z9MRt^$by*?)=+CTAfg=Ns5aP+Cqc+y<_+$au1XCGciWqR(0e2r=q>D-CWg>e=E?j` zM~xWGD|* zM*;yA(IEe*>@>~v8%=Ez1lxxJk=EiZ`eky2G;tGxb|=*PyKZV{vJH+hP;q5wrcLYl zn2z@;q8xV}*Ol&>5?B%iQFnJlSRt1(ZvJT8VsqT`VB88aVa+;WBQjyDG+}2jVQ)X- z;633OHQ|&x;aoD|@@Sm!4*&%CCx@FE1OT9D2oPCIONWL8AlUymIoy1(|G-xM{lLEh zw*OBL;AlTl`ri-yD>?iB@Bk1>dnYs|DU$ZW1gKZnm(w&>(KgpQWpPT^&cx8g%E;Bm z*u%l}tg{*3&BEWuDlot*IM^oiyz9ku8lU7w$i!!toy)8A%D?JWaLv20+NZF_x1_wO{Q{?OW?um(zG%Vgw@>DaEtxW481{*{D*rx$L&x^Rbjad_k6 z-OWps*Ox}NGjEI#Ms^5eZ<8n9(|()(tAj`e2p}M2qCS9#l|r~nER1Hu=mZA{l)VuU z8%D|(S4db($AG2yap8D8k%dzVb%4_&;GidXo08-Np&%mTnQ8kso@pQ=o$I?+8X5{D zircjuU_q2oCQ(GD$P7uB4#jQ3+O&hN!DX?Y0PRrlcm$o|l7jb0zYHBQ^vT~3j#gIA zKV<~~p3zLe8fauRjrO7ymsVLCA8lr4POB`frZgb>KWl0m9&Q&AMdP3Y1MtB?USa%7z+T7Gw)6`V^FS_|3wD~XBoY6v|#ii0pPeYq$XD096oBo?M{ouje z^XE@#iPKc-D;nKQOPqf5X8Y}%9U9>L?%nSHOgDf2_U%vN^x@&RpFe;6UwwH0|9vx! zcai@;7=Zmvx&~qOvky$V>t_Ck0ods>Ausm0|AM3KUYlIQ{Y|>I$=A{6|LfgoZ7K%k z`U^*+u4>rbSn2Q_%g&pQ{>QsfPv%B{+q0Wtc+0$hyc?OGtiQN-v%~GqMH(C(%qI6I z>H1sMje)L>sS^zP*Jw%C1~X-fH1Ed2p6!2mH}<|+qIox-x&JTk#((iHw503%w{CuX zMWJ~&THgK#jy~~E()IQq??%fzLtl1RduZN`cW!%-4~yc_TS{!I%MrV=3R{!}8(yOEkqud+i;K^y&nqaFR%(l~va*3x-ncGfbm z=@RRiB4z&TSrQFR>)F!%JOAQcl$QNz-i@!DXx@!qcWB;?Fv(4l0eb+=yHTu}=G{nx zql>MLB)3ZJ9Rs#XoqcKEjqWjTwk~_7OTI4mE(>`54;<|*+xp}R9bjD2uL_|HG}M)a zkhiae49##=Mb5fxUxjQ9HeS1M)LemzY?p)30q8ZxA>geiBXAJ5WyhBQk|ScY(dak2 zF)$2msbXt}faLWb3h+I?`pJ46994uegql4kwOr%cq2d5p!)9D$acN8nSr4$7!fG`~ zAkrdnNRaw;Mywu!f(qgyD@p4DyKNxA5k?aeKnTi$LLgQHgK?w4k-#6EWF3UOI82XM z_YIvn8PEnX*_r(SRaZiH+|C0^rQVr7vPSnFYRrX##N5N=MzerN;ISywnw;)}QJ4Y4 z?uP&(66Kz|kAI%Kfig}fx;r?6fHmFzsO7m zR|WQw>*uOQ5CSWwt`nO`yW#tkXFo6?rNUZV;NwyQj!T~MgxZym*OSM3r+z$($!01?p5an z6YB6a)7|y=3V1!l#zh#cRd^H>W1^7$d_`6N(u+Tak+fwR)`q15h921!rZo-%)4#GHsYNE*&v5s72g= zrQ^ZJop`6}JJRD;+?R9Q3)N+tA*I)On%vHjb<;C z9a^3Hx;ME$?@o2{#%3leQ48<*fEP$^$=>`b0y>1AITizQF$$VApr3h$sx~sJxY|lu z5PsTeU9QAicu?XbfPZKuOY`YtQ06{>TQGdNZ)XDZ%LM289B7T{34X0AHX=~8Mk-xv ztQX({(%vlebe1J7*`a0kBi8{?{&FmSjeaFtjge8mhG`vPr_b22y0B!GMBXrhmwLiq z{?Uuql|k7n&hZY_mtUS{?1PsY@H5EG$$KXF8Hdw3Kmf`qyzel2Jg4YH?Vqk-CXDGovNy!gq+kXA@h%mA}h0%Wq!Wg;_>l ztAaKgFIuk0r-|QqNeZ{%mC%;Y_RnP?%=F$gxV)+``sVEtiytX+vVoHD_Ds`K#CWQj zN#|Dsiv0I+*Yq(4O7pm-dryIavgi88Q_otlFDJMQ=)+2^7B)Jxqc~ra`k-o`^0mpq zJRkm9rm@~Aa%+#@;jWW4Sl8S7V8F|O$(MTI56G2$wm|IgTFQ^egYjC0#+NOZb=8%l9J5EVAo)i!2osMMTc}qlhm6DPyu%JeQpOT7H@PQ_7TPl-7ueBpRclg zxQC@?cCMwtG${0!w1k;HHmx!I_;wdqxu5HwWD2ZRP=X|f66v{6p!|nyWY0i{jc2B( zy<-P8UV9@pozHN?O{evQeDZ>&FZ1NUrt+o>^T+OGVFtoGwP0xlIzQ|!QCM0W)dGo3 zUoR1DO96|8Ve<~Ah3{?|S9lirRhA${e}c+wo_;DzHP{50$D2X3WhElV>*X>`=gu{l zUcRm6BXy_S%uC~Q#X@ca_e%L3MAoiA7TKV5bLM{NOXiA@cl9T4J~TgH`MGKbf>mRa zoDV8fytm+RlVcI*IM6Z9$ZXry!QU5sP(~{2g%$AWSoAT_ExHur_ez) zI&WtOHDG<9q~zR{1kwdo7mQPD*);CWocPUUTCoY_k9X!!;XA}s*g)7{gpB|K_vkN~ zfWsJx&2+q<%o;ZhT2mTO;Q2J}C9a%g~t%!osA5bdO? zBK-I{v8AJVYmaj%zfJE}4avaM3WAIRRiX9BeJST=hsRacnNYp7U6v#=!>Q+a7KH{k z^k&~;24Rei77m&C>y_4bWFd^t50P5+E#}1T-a4mWGu`}a%tdn!t@$3At}850%W2u< z;^UI1wSGX%hm6I_hmM`jcqj}kaXSbn|2l!1NL!tjOzq2Mo3(K(a*ncHt}JtSnppKo z6qR|7M-#{Arv>I7mw=%Q2Gev7ZE~MofG|AYqbgdbW<{!v36x9Bhf6J8yC3yBElv@t zdA!ef&s12We4w1EYLPdV*UamY1nkrS@S!-t>9&Mai=B7ut4qD?VsF2|BM#UY)l!A! zb7nro+{)3gUL6zI?c_c$r$k8Z`pEu>3O#iQRCqJYp~M?4cNe7PNz(9Pxw;5Hqtw6< zxCuu0b}@uj=>Z+=;Q}3G!ti`J{l!Nqn1fs4P&ki`oPriJVvc*wig7$sL;y!jbZsg9#>gA&7*<@?;3!l zt4&Tss@~MQ6n0iJ=7ML$UB&?*zwzj}PJ$9a(jm*hgSNUv{#J7wmx5q5nb5K{Keyb8 z7l{OF)O?rOqOTfo>`LaEP^TRNLuK-KKv4&!7tH? zvZ_QF^14i{D)kpwTLa=xGWXRKXDGb)6xBEarhF|?Zx)2quQq6Qr($UkVS)qWpn{7v{* zp%LRTsDwWMR1(}dA(d->ZDsx1(t}(`Z|LjQ(v$noUNt997`Q$L_mmo(4NSap@Ujxk zkcEP?c$M{#zZ6y_rfzp~6T-6ZT*-OY;shp@1~+%L@t36ZcTyn7#i6cmUul;e5F#I~m)#kFpWD zhUii2&2AgK%DCrgPMMwGexdPVFY08O6KI4nN0DJb_pXu5I^$8&`$_BJr!WTlQqrK; zCT5*vBm)t9qH{utyvfUF#H07Q+&L#a!&KlSPhJ>ReF8ZXXRLEvINB*R;otHl_jeXePpcT33gnSGdXDJtJrJ$O`y@i{)iF&xFqM<4oIhM4w zEr}Eq=%PSQct+tLMx`64Sp$+h?52BrzJvrLADylEiq;rc=+Y#21do zXJ@EogISUu1&f(5pN~9gUaTMc$ku(Et+tav7of_5PlHWk>{KzSRK|Q9axGR+MP2l(ow;J1 zhClY09PEu14D?mJ{Vwn0*F0fo@US;jrjjuEdyBm505~d3)5aFy{D#T3IbSuJ?nb94 zLv5JATB&D%A-71Wy=k_BGukT-5gEFnwL(9k1V{Pxq57A+D6Uugef) zL-$UuurR~WDAnM-4R4+clX^;+hHVx%r(^JUNLEI95~VUfK%vB%>DZdF7T_yKyfW5w z<;PxmP}#|Duc|pTWS&aTZ4DT-_H*AfELJWlWlLf_er!Crg-!vRR+pkrQ0Yq{u4~^R z9k49xzH3vdSEuW*g45K;CYWDko&Ic!i6=8$AHVkV4PTGHO_?YQ1bX_#Ao|Ob>xfFS zRG$%>viM#F3%G^MtWjNkaGi08|D!=Q_fWN;ceRtQ18BBdNTUY0S#88ZeqvA~HBrqmh|2ZvSfDDl@6xDT3}VYyf3V|UJP*1tXxMThomA_urC#V`hwT^)2K%mx$cT~98b~tlN@N7EbGmdsS2{&H1wMfRI zm_=M|!S^~jH)!`f=_u|g#GJBR0+G>o4=bvF-r$^wvlMD?0nUm=W>SN>jRoTOP|8&B zrgh-qQ!Ep+;LMD$&M2Jx5GcG3mhMCug$*^|8bWZ$C7l-X*9>Qa_t72IWx2LrK4P87 z&>&7>toFDl&A8MRzdlg=DQ0Ewvbo{l8U-0N#%M&o`xPXOtmg0^5j0i> z=y8}nb)yv9N_vE!&`(zJ>S~S7>h-^je!wh~SA?$n);p=0q+5JL9CiKT`0dLmby_>_ zeABn-pCza+Kj+k1D#go>NG{C4oWh?|lV)evA4}R~nf9WK$JYxCT#DXSK;fB6(2P}w zgkS3??@^4KR_}6-PWUZdfK3_?q!@gslkqW8Q1vw7Y|%*clDvQ& zSm?%>z}jTkJI~`bWtq|N3!j|qRsC-pXPbe(j9M<Blw9V z6ZOgZb(UPUqyh;o?#)P_N_t;U@RjH2Xf2|W79g>hp8PPaN^0a0d2a6KL^7{v@DBR= z&n$_m(X1r2VL(m}IB&RIx7eI;W*_l|3TMU9{UFTHt;{KY$DDXlM4r}6MeD)_=TnNu z8=UAwN*)Bvaqg*UF+&EDH37n=a(zAB=A(@IMc{c)rd#Iup1Hd6@~6HiW}dYzOn7&( zk5o{tc|63h5h({?Y^DfNu~Ua;Q==6NPkOXpY~Wtv7zS>kE>a)L-M_CQ7ri%t{_!(J zHVYEo$rw!H*lV~wp3aFT&nF*Fd~r6|9^0e!*lz*D~^A=W63iiyZ1N^!nA8dwe!q zT9&hP)%zD-{ZxO2ZK*qa@d{o4>K6CQV{Iw|N`3zx{n3JoI*!<7Aj5~z$YCn;um$YQ zD|L-E&Uxy_(KYt@S6s(ytS_k?-1QF6Fg%9-0&KQzW=;hCHn9-}tzNB3zG>+X%!>&o z(8}VEq?qmIw)Bz>G1hqhw00ZEaHFhjB_BHtQF&E<{_Q6heeKsoLfgxg8=`JFxEo?} zwpz+I*Xb~F^0EytX15F`{0<dV6YxTv~LC4z_BI`pMkI!6lF`aUeu}M}wcKwWet*Wf1_+f)lcPG&HjrW;ay|q?; zbGKKGVe4<%vbOlmLtkHD+^RvbqP}jHzpR-)-EMdMhzt@K$%V{AJwQM7&ZT>Ji}^4< z>i)Vg+h)!8WpBkUw&RoG*&D!YJe^z@UN3D)IWUN{r_jdvG-& zFLPZ@Qqx-w_+bOe0>$wyuYtJ_cYT-9lWT^nR5tFY4>!kFCHv3GIQAY}_neM7Qx4bz zojqgm)!RnpBi(83!!4d@Vpjr&{-U)ry`c!AvQ2aJ3^!{IvU}_}?(Cem zCCtZm(ucA*F7|_i@jB;;5lreH<8S}_Z?Ac(ewnL!xW|WwJ^8>C@J5;S?i*9VE&J13 z&Ivi5`u^$Z{vo&gD|Y-J^oM(_4U$g`%_YHBFEDD7-bH9JHI@bTO@_|*1@_-t8VrK` z=svrG_IR*B#~KF9qr5)~3sv-1E~T*x1jgUDK~K@B%AoU>UEj)KEO-SmpBmy1z@U=Q zU>jC!euC*xM<(UL;AkY>i?1QF3V_=;hECQ5RDJzE?4|`&4TCx%;45K#+otcG2CPQ0 z2Drz_JCd;lp^hxKxfl4OpR?pzEUUVI%08R`h=)P^!hX~LK#gJG6Q?e?rSn_&n zk~(4izIa7hMAdZn_JcH!^8sJf35;4eaKt$&!28Fe{4W98-!KR+iMy|G6%Fg)DxcntlQtXAFy&J>U5{n3!?sl;Oq(a?-8%>hn&h74g&$+K5jeP zs8g+Ouyx^kgh6NQ&GFJ|xKaA|F6!fh{uB4=D99@9O9!OXYd?=ofr0)U9yc-sE&yz1 zVQX@4rua_{&EhLAw5toX%v?j<&+>jbvwgh)fo#lhOJ92?i{so`@YEzQp6jY;B>ucL+ru3A*jBo`|Pi*NvTd zcFOpEyS#3l7~VCzdv9JZ`5NE*>W%srShp$TS-`X4YLN1kV{^$YQszVHt94Ji?`c0t zde=QMuLjREZ(sBTuzn_XHmOhuklW&&+^uLHRkzS+t&~s z9s4k6!%FfpHv$tbk{xhI{Ml|xMg*gq1Z&hGLI^#z=Y>SZfVfOnj1l2e7@=|iePL7; zH-aTR3NOy^EKmY=g}-NgrIlM<~MsE_&nl)!-Ia%PeKNU>&a*T z%xEzWJP&V37>C9TjK{MiB$+J6r5N1wS&D)CqFOuGeu-@(*=V2fp$%Hp#L9eF=@2RJ zrg;M%j&^B1i4*k7$Vy9>WS_tHy0=16vwAMBu|WA4E!%q;n&-c6*M#%|#NW@8l*OI9 z_T+JpTBzxO&48eV&!*ht-rX&Yttbs^UAD)AA8H~(0Fc`}C-YsTsbD?DVSHE)O z?cV@|PP%bZF(EMpCVSnf-P%BXUOya<*DzcR^VO#1Cu}aG?&aov)0%Q)W zhveTk!)8~fPJJq*yWBQYc2J!zFUBb@z9?zR_3nutC(z=O$&yx(tFPkSM>tREy2657 z?^jZf??rW|mQNZqM7Yir1oG;Z>l_H8tA)n})1~Z;4_xE0LpJ#_k7@;=ZyH(}nyb{7 zGD=aBCYlY+Y-$_yl4$|vJ{oqF2^KZFtrD1%D4V?HM~$bTQU<@4m3g~U5S))$%~FQ! z8&n=QTfNw^2+?1x0+BP%Zb;hq4LRhO1#7lVqfhl0n$-E4HM<)Ixjr6R&M|vz=g>Cq zaI4s%fBA8H>_pI+FGI&GHiOXs`}@uArwyHk*_S&r41>?H(QE`j{WZx9TzC#mms!W< z?#p@2XMdws215N1IV5RcLrvF*Wiefq(eKcLyBVK|{TbTIA;CVHZclaC`l{|TMzR9) z1#@xS?eDpRgEZaO*;fWsLZn06hIrLXydcc`GQ0{2?r$AehQ{v5gl<(5w#_z?!X}ic z4$U*4%D(l^JpN#LVPL#lMVx7GdW|k58UgKJC&i)7*Veo4t9_2Er>8 z`0)7{_1&`{L24}^-+32|79QorafiY3>~D}u zJ7Cs_oEWi}8{lI<&WC)&a)ffWAlcEm?u0sWQmZT?HR#YwU}y($ZeVTJ&)bS=$3I-? z8V=~d8&GE3yDW1sQgCjwWArR6^6a-8?l9rU=NH!GtiB3nyXvg-C6Fmn<6SA=dW90C zIgNtR@=RvdI409u=048kJfBBoa7#HHbRJ(|Y)$K}nJFO=p#0iyEYFB1k!7|}X(O7# zum=!OU40JMp-o+P!}Nhidv}uGf!SCkQnfiTzOF0W=D5w1Pma1%l!RIO>J6&5-YvY!;-C@1D2kGVpy=iOKMQ236?=iLYG;#lORIRdw+Hq zRchp2KU?y~yo6%kf62b$WPjro&F}lyqk3zZWz6)mhbDDOY`{EhO*n9K>9EhWvlpEl zG{IOfSGwFD=?;Yf7ir2sC3)v|J6WFY`u0f9m8^pV0hUWzGD%{XX1Gmz)IkAQ`o#mDxOlT_HVjp-4Y_-5}zgtIuoF3 z$?7|@Yv^`%$S4*f1?@?TM<=i3ARyxa)d65K0jNPu(Kbn8@yr)sJsv$VK1veO6HaP< zO(iOkgy8o^^l`CGBoQLovqWs1B^J4{x5K_M)|V!CaMVCdhJws%xrSd`hP zjt&5%2Fsz>It%Rtn}Y$608TD9#YywgOe zUGp!#%g1M?=%?sX7Q8Nv#dOvCL~ScOZ4<;W5t(uAAguOkbhozr4XbT@hAHB9J&6Vq;xAt7>wZ{5;D_cAyz z_poj#?jCvGVpMP(?2>1O78S~WD{EkfaC~+lL!6xM%`ATj+m&!&*4c{?3hKgBgx%u{ zI(PtN39 z%HZb#JkwIB*)5&1LAE|gefub*^Ps4}&V1FJrFF+*q&6i=4J_58;Cilc`o`%%=X?E0 zr^TOI@-&sc=skT~`koGWz!Y;w=@Seb}v>)%%BCRv!3dy_! z3*I}|RdagO>7X<2z~J%>rNLu5Q#yETW(tS|NkA-yk$Zuhy?UCx2F0i(2?+B9+^v&f zoY*`-hJ^1Cxd$MeVTx+SJ%Xi~4DT11L?Nd?U*(QV2WBnaJVddt!cL>I*43|Bovs9p z*fBwE20WFF6O&D@kZhYvjmtCi$UNaYjidDRGWQ_HH{cqbO6E&Sr(F@kwA`GyWVVI$ zx-O_1OPWED73gV}x`%BV3BgWku!tbOi?zO6sMmXfdl=bkDFgzjW^^W9$xKW;PFG zMhr0PA}h-MRkGqzBX)>o=;9Fy!mMev(LXN}kyR*xv}$|&R-djfOyy>0o|p%)CJr(d zmKDE5xUn=7-RBf*ksJ5RxOXG-h4)ILp;M5yKK+&1{shy|3;DPd7V*U*zWw4rrN$y% zNkuMp;XCq z_xGLS_{(3MIcGdyx99y)zEX3_8qjbJP_2EF@loD)`p|ibOsgeo`r7LZnc<_%3rCFh zC?;@k?i2G={zA6zHWY3lP&GxcZ(Rb-$F$_0bDs;9Qx;hk6dy%8+ZB`(^+RB4cx7J! zy0r{=pk4CIon)vMO@4wym1?ojjQ)YAxNpJ|E@PEx%a9TtV+xTjJuF-m}EiVxo6$?v5bFtA$ zc#DIPo2r7FYD~`B=vJREZN5h-?_`*sWC5O(}+*gyw5r zb+ROARmI&{16wX|>14ur?O_I0(v#DhL2J&JndTNSJPcv6;|-jdA)7x_oLMsMf9z&H z$*wfWDlu%NcslAVv_LsFew+Up=W2_U1_4-84MdY~Ock`tFoR<*fd7N7Z5^i^*fFkeH*eo2#{(lPIie zks00zQ}+c!vv!MoIg>cqxapK+sF3tKV(Cy6lkY=!IrU$e$Rw@|?J@jl&^iq`r z$g?Cd4J4q~JkZThN1Rs?G)Af!^eFQ~f8qf5DfAi=s6t4(yygKugaiRVU>z@LA4oEU z_(*VnqE}4^lQg0LxR#ta|NLTnNEUeX_O1vZ4%z2D>FUEY7cWti_?#;)SP?Rtzbgm_ zvs!&zc>fXD2DyId4QBaJpg_Fj4wG;8fo6fEPKm1rAGcjENLwY@6$0~py)UyQaqIYs zFUCR%zB^FAB3a^j3@jRnG93BPbKnI%{m@4`I85}jVeoqBn=g^)*;m$Y?+|vJHgG-S_1Vq!dYsp zu={Q}{D}T0$O3nS);^+h^IxWd|4CRW0QkSi{pWuZ*8l2PFI>3rSMiffSjoY1RgLrH z4(mVtsb8_O<)As*KSjj*2R|WP@#rhXv4gRm4(5n$CS0mH@Qmj$&H^_?BIl0I= zt;{w3j!Q&+^+tZI+kI*(iTd`mifO51&JKk~cNNvL=dQ1dLXemJOMG^AxFwCz<` z$71-S<;cftkx$m6dp4qa$(J#<|Ekaa30(iJTmO#H{*$);?OIc_;?uJeGjfyuPuE&} z=hk1YwYa$WPufcEkp3$|O77bJMO*)hkpAsjYwuN%W4C|1*1tt7IehyM(c0eD*3r@N z=+UE2atinVW?6|(-~Yi`pY6pC9{imp{UewA56(I?@@!;$Xl#PqDE&vG^u;JywEmqa zB`0kj+)3R=?{X0~;^;fL)e?;rA!NN ze`R|Aj+P!BlP&A#&nL&^bSb%7`XABKzlx<~!dmgym4_Grz344MbA>99f=&^Q^S93l zPU29wub8zUoeZx3SZOt_^+M){Ne2 zor?O|yEJKJ6N|3K0bv-El`f6G?2y}#PPp4{rC2EGg>*%-cG=U2diWw%`$Ora(0BsX zqI709(QfWN>E|PN3tAs}afH1u2Nm-Z-9W!Deb7WbCXE&N^QA8(Wj0P_!(7?47%Is9KbzaJX=Ud#@rr?M#a%upMZ6tWz@iY(B&JEo^j90Fn%4cx?5+d9e4MbR#D?J?wK*SDv?+hqCpUVPEDD2;g2gKcX z(uS4bhgbi)@?amv%w?vP2kQ&HR0A6dB$ip6+zlCo}yV=ITVmbgJgVsHo~wNhugFmEfew1OFBv!gA>FU^Dy6nEK_^wLCVx*wr@5(kQ zDyQjcjU9jX;C&?LeN5kx(7xs<3Hf-XMx9ombRV%*d+cO5mR2O5`a6*Mop576+t`vrPD!>e%0lXQE(5D@$vTbzgr zkg)_3m@pwr`6B(?LCB=*!Ak4pOVpYNH!HdsMEDQz1NH;p0prVJt?P2~m&M!%lCS0( zb^1K{1v%A7F`!->RY^+^F_LU=?Rn^-4cHifjO(<1*`;ecZfR-fusWlxXjBF|zaw(*8og{c^~Q*|~HF>Cmec&Z1m`ZQH6!*%E~T}i{O&`PZl&7nR{9uC4t zrFlnZZvjUWdK-sf{Wdau(P%LJKz+k(PBB|Vp(!=c;0|X+bIRk*p_^Ihn_ML;E zZxFnYr%|jy&;#!Iyn&8<%dP28#Iw?mNNFo-u;Y<(Q+Km{EMU5Wi zBo#b7*)q8=7h^(nf@MCWLGuc;JmcV%e!NK1zLb;QUognS0lf7*Nx3D{{j`dqpVi1; z$E0XnBTKfP07Gv|Qzp$fFdQDM%4IR!y!dN~ZFFb&;tyT!6;d-abADVOs+F}RFUokXDZb~Y`s&=BHoD+JUoRHR3(IMLT<`9VitmqyP$>uIVfk31S@ zdbAeDl3%0awN8GL@IoO@ruWm#RY#__Ittz|j55IViVT|PY8>r#uN)ujE>+CU3djWI zU;OP)ciDcDo@b^k1cYN#1BfeXw=1~i%V5c(<=ZW1p|5Fd5kIONxV;=pOObzh-D{FYd>+_*(paGQF<5VRSrVKl4c&8sKjYyA2Bv5u|EAWbty5rAm}Y%k>IRX{%ehePpG%cahi ztS`GkUU~quo1$hIHd8%hPn5hM%TIV0kSv@Advt~rI(3kdp&5A`A zYw`?0_&ocp?Ol8l8NrVZfllbBuw;lo|)!YUXeHRO9N# zIjQm$Ve+B~iWs-twKY)VnMQ-yz^Z7NM9EY~d&eyytUA|^(_x(-ZZfyFEHTvAE$+V2 z0(hMX&era7Yv9T}B0{-NDj&5?f{Q)E?%cmB@kbL2tRlWuuWJkYt5*Xuc-~eyeffGgf5~d& zH~4!@bjg$D4^jHK3h>Gl;_ADQMMx;lf#4-~$yE<0=GXLcbvn{JjKKJ9wGlP@)|&h?dB0DlYRKG~fDIO%0YhI3W!=%BjF1zrl$ zIT%M}`9~qMqb44vK2=M{6Vu0Z=pzVp4;c{NI+q4d6B*oZ&OKBqZxsjW(rc6>vhnZ( z3~XxV0&&O0k5NDOu0d71%jpyqua+uPyec{d{;ixY4x4gc4Fy3`r?1njG^arL)a9Jj z_4`TL>YLfw4%glvLeWRi{WZE0fRqk_!y^WUGz};tDK^o2x>8k14@}N9k-t03`%EQaPeF%ZVRVXimw|Ma+vI#aJ3=DPRj< zVj$B6G(@6LlYf>^l^e6*UHWW||?hS{49v#v6U~I@{^I5(ExN@P3du#A z`t_`e4PAQRE!r^WlEHe*PHRH9hu%W9PH%n0(D%|Q5$)p>6Msgd>Cm!85nb{;ywa$( z;gh1WHP+j?fu-4Q#f+3j?}~2krD(jSytLY7G~RXlSn1Bv%x(P1?XM?gCtY{WbOpaI z(Zj8Dek|RA*p%N_Dq~17xSMhtx?GO1(UJn1QlQG&QFrLVDp-DKlNW-ie+bUhJ7M43 zSbs~@!5~P@)Fn%*I*wL(U;NqaX)jb(oVL0A#L?8q;wggA_pFn^Ek!XdjMGjgqIt$5 z5DJJ39w>{{4#RJ>i>nq_N1gh!7R$01m%o{-Som&+>#o#YmXimvJgp3B8mYlAheQh_ z!|f66l@*DnNy#Hrk@Co^LzVAfT9@uId(Jal?lxl4lx2KZ?QByGQLG`cnb*|lX2&NN zM|9VSPS=!Zr-&}sxr|wJ`QLpQP-|0T93}5!y`vC(ug2%z-58rXu(1^>g@q_u9~)L1 zFIo)`tD_3L9q@&ID1o)OpVdv3jcTAq-I^g{G&+9zR&9)ws+Lo?gOg&gK=?E>)U~0p zut8f(9t5EIYd_ac4FQ+y#>|-- zC^jbccFik{Cv>Of@FNO}&+?ENBFDQ!mH|EH;27qUTx?x`1J}`-@`KC1X zG^cT>h5g*~dyU<74~k3P7uBt`q1y#ragKhc9Ss@Ew>*anR7OamPi1SL0)3AZd_UlQ z@kC$RV+_X8tIgE!n+4)~J|SZbkx=e;43#Qb@W{f`m0*D}0JkD7evlgG9KC@28qKQE zRj~ozhBe>Gd2usY<`0KZ%4gEUwR|cM(KT4Q&}_=AKHti(zC;`~8J+F|_?*Hpln*f) zi6|L0zBs~Fws&B$H7afFqoD9duM?$oAMq`6=GciQ$3{1$QJTo<<}fL;%YzNgYw(IuRAxj{P!J>Kxb ztvl>o?=gGjH#f>*{m5TKQosr9$HSg+M}gKV>VW>f5jVCPSVW0^!1M8IyiQVq5UE4` zoa-(<*9xWHT1t8D)PtPT`Le;!02I^fqKq8XiF~!Js?toKd1~~c(GCM2vj&c+?Vg@v z5dlZDr9HJ-ZW;OBQrN&{!4vBy8+0{Q>i%qy|09HWc(-zCw~m^E8@!ui7V`^EVQHG! z8lGUgU1K%?`&#>&Nc24OG zZnP7JP=&*X!t6B`Eye!1e&Ci^33y=68Pw3audF<-o4y<5K!6M{)aGy#PvU4;~ZxUYKor4Ie(IZo&85iIB; z>#n>H&2pD1b7idQ@-P79QJvRR6cW6EcmVDgPZebKf@(5!drN%=hwC<03k^d(o!T zRo-yBY`KwR$)fw|e)V$muhQS6^uK;0nb=-;t{SyQ>c*836Utuq*%>`H)a#KbujX7C zxlo*UeyDlBY`AP?ima4KB8!zDS7_K)7wwj3IhQAC?>sGAUGvc&xL`PBSMiqf%}%4< zMw!vp$ETc)Ex%8WQ{VDSO8uN=Z6xv(hNyqi`P4xku8;x!CQG@GHjeVt!n|bT2{^wqVu47nga4%Gf1SiMtzSoz0 zuhX<)*t@!7z52!{nV z#o0M-zeGW?;?nX_nMKC9AL+#Q_6J=V7JK-xoxIpC9K16;#?DUjAVl@!T6e@T{=)6_ zy|+WEGu<)^dz^mAJrW^Tpy1;#&b5$(+hMowL|m+jn|8UD@74zPCw%br)S~x#-I0YK zC6q6HXa;8;kVGTiyAc<}5wG za6Su(J{jdcftN8*^1X%`X}?ScF#uxhJ&oYI6F=yoQiqJcl%EvO#cnb(wWq0f|8{Zy z@Z0x84$dQ43ZLpeLYT4W1>zAsbFZTt;&zq^s1zdbRP2sS|HTPLrGDE+x6c~2WCSju-Egp!!edB>`)(1Z{r}c3n#tIcCCOwrX}s zdn8;4;3gz}&nrapmBNU=aK9s>;&7|;{SaW&ycuJf%pAf^@)o1ZVDv`f~RUMq0hMSzYfBz!U8d2CRwfR&D;B~orjUnZo zXDkmZOvYmim<}K+om9;R+`PxGd`Zp0rz8iognh*K&(oZO(!*1J`X7IZhrX@(bw~E8 zbF^bSjY2_Ar}p`XuZs8TQsZ##pCx+hrGS8;ZaZJRfrxYqTbzM>-$lE< zbq*c!L0hx>io?4Kr)Mi8kKwKJdC$_t_itJ;v{c?#XU*<5XY|quMdMXbw%IntIdjj^ z?kzF`r9}|;1+fAm5jIik6&WgQHI&z?}%mj$Q>^4IO z67AM7#Jqqj?q+>PnS^0_5VUAp0qCx@UCda%sLJW&IdUX)ukm@5#4VZUviQn`l zw@E6|^+jkU*!-AhKCyY&7u{%gJQ`2L?ePpH1X#6nmQwm4asJQO2~R9e!+xdR%C>HV zZ#`JAyPJpT6Fn!b|D=erq4k>WS1HFAF?@t1RyN+-HiGzSE^pmqciT4-uy5CQwyKyMK>{a%v^?Z8ahG+RyDRP^Gk!W~DxvrnZ-&?W6p4fF+aV-w0!#$jk za<-cl88vV>FO!m`rY|C3ckOZb39E)f?PeWflb6guhq*|F1O)j^l=)g1Ye#a-&Fv@a z#tVnTq?LXY|CK84U&KUOs@WKqPABB^ouo@IVqmXAhNU3MLl@81X?$0pRN;NF$6T>6 zr~)1}lCn{VO)_$Rq zp{K=camS#G?wtj6t-4-Nb~X3IXsyQ#E#jQbS7EIAr< zySTbtXEj&Lz19b|o!jM!WST37X$n6KJQ)vT+P_Vi;7)#|Vw`G$zVlh`nmYI^KJ8D! z>Y;N!bvoALB*IaNsAJx3lHV~`Gr(W}BRJ(=GmR?!uEwr@P`ByrueWNXDXi2-t>MF~ zfu%}cJWTS)gteEU4tG!J8?SLD&n+UG)&**ll5X>+;Q59}94zWLb|*wksvz(dPh6`_ z!Gn(ZM!&5+)6llb7~Zw}_98&gSry5=^XvSBXxjQK{>~-|NAs8VAcHD^X~RKLrskUo z-p;vQc%K(@6^XyLaHc5Q(cp#FGRFN3c)0$=`+)NOGjC7x4+~Sv?rjI^^;Lf!l&Z^C z5e?2G+X_KrNRGz(4bE@URGxsl3~5KIawaTvGWDXYcTy-CqGoa&=lJ+_rV_~)@zY5| zJRBrqtlS7wau8XMX1nW%O|%YWj(RZv+|icNdSvrs;ZcD@DV=)ItF!d)12x3#iHi06tqA(Ps zIo22LL~$7pd%U;+B%Pn&^IwN!d7wN*tF{3I1Os_TdYnor2jVV#=$Dgsf2u9n{AtQk z42R^JFi&4U!~i~s`HBN44JxRXDUP~Sn+OVXIPz2Uo2bnl#d`!6U8Z9l<(UKC#m9iH z9$PV@CpL>8;GAC!h?WGEzjt(Eg9rT0c`-1q zQXD0V6_Fmh`c_JRSo3MiOC7j#xgk!-lZ#b14ENk<4E;%-KQw?h7 z6EM24ym*o!XDj#-dXC0n&fX)2^LauR+ge5R)uv=X^j2N=iI}urr6#q+O6|D|&ac~o z+todtMcYWYJv-0OF$#qa0^;pd(}P?SEnhw^IX`fjG3yVs%5RQ;OaLHsSpfAdytsXz zCSJanXCE$}Fu;L$tq5pM!#v{(PwcFLrKYB z&4a4fIb*Ej%0uFW&b1UGY6%JVWnH^HMk_>Q@Xo0i0knY!*z=SxztLgqLh<#_{hJZn z7hn;#(O2`!zuE8L*MJ;oLII69kd9-mu8+PoDTtrvuweL17528O#c}A68J7%N!$92B zNb8`GHPy`lwHK6db>X6<$o?=5E~*VO%hTt@gG)GH3KdvqLj0TpV3BX3TJ9|(SY zJrkM~@rFkWbE+PIji(R?P=*Nu53`>}9$Yj1?obq11iB48zp^GIxj2A<@FbxXBXw4j zqR$$?M>g~0*iuf7Gy9%_l?(uPz4QJctP{_qY-0fgt9Wja9v}hCgRGlj>x19w8})p6 zWrogT@c|6dN4eRDaDJFHf@4?egJ`gX$KvQT`T^CK;#pKX@j}36k!BpxY$~Q_xeq<4 z3cR8!?5-MrBn51~S5gePf)!(r=;qK+V-&}I-c|idDTjG-&trt5N9GxiG3mBZd7}Cb zv>r^I>~#NkbBwPd#PgK8kG;-)P5cuByp1A8_O{u*eW` zYQz?+JAE&R7rlV5uH%FddGWC5Qy5LP$dk%vRrk`w#9JlMOhb#wKSAnbNvmg&u2DTW zpqH+XX1UmXJ7(R9ZO)yyaLU0ExNoSoF*Ygi%J4X8ttc7~*R7Sqi5 zov}!)Hg;NPr*kw}19=}imhu`LoXq(}nOAabXeKn{M*8rTLkWK5czEe8*wS#6b9vBN zt=F6s!;}8h5P8K0nC~HwTNvhs1n^qHEDDLL$uN?ZdrAOVP-a0dSYPMx%K#D#P4KX9THpL6JNpF9q@K2R-01RD`M4(tR_%+a^Pls_W zF>DbmTu0gFo)R(w293mr;|;Itv@ni0(|%~CV=vVrOh!y4P-CRO4JI2Z!no^zMg-uV z92TNZJtJp0=L}(DFJa4qP9eu4R3LQLK*AJ{8?uJAK{{B326$m|4kC@>Yf-_}+4pPf z*ClB+7hXpSyn<&%9>${{Cta|YW{#5%u21H(f@#4S{p^|Tgquwcv5yU^cKMrsuK@2` z#Sj4bjiFGMV`&V-!>ruNH}?&p10=)M2m{}knYV$4i=|Hm#6`SS>L+OOUB;57J@W3R zq|DW)Jiy;7OP+-E%UYL(lZa!W^B$k%xn(;#>OHW z)sm`+%vLOfdSEHEHf66*&8F$A@A%b>EHQ2+4JYR2N#QUMZRuFH#uf znTD<!&57SQz59aXFBG<(--Hqn1h<*>DuS}G3L$9kf4wp*G;&&17pl0#DJ42 zeFR0B0KzH4lv_=?BYEn}Q%!$S?wiHLHpeUaz%7MQLP4e~jIWd$P02oAMH7C}Tz#xk zNJ>H+%lOE-E&bUDa*ec@C8KiN%9&G947sM56_keznyu#7$=q6ee8)X~IGLs=XBwfY!=w`b9 zX*xHZn?uC;x0Kl^k$k%kC6T{qr*E;@PiFTk=Bv6`G6$sWkF=+IC$B7Kyje_taY}P1 zrTp=Jd~wgxon%g&eZ0R8HZ=hhnKxh)-kQ#0^kc=5`K0=mY^6Od@J#o0AP-Q9Rge>6v8m;NqpArU|+P{(}WpWvPa)gs0b zMkuCB%`UjKkw}ZP0j+rxUsCcDN_E4MDc2yO_VHmNwLQX2_t$2V@F|vtSAfZ2OUC>_ z>w@4E^x)y^*LJT-$XWXyS^}6lxZ%p(j}X%srfT6^>Mh1{#taJ6%LQxZF+1_s>fa<{ zh;HQxPK8uqKPmk~O)uEOvNss+5%QgtR^I$t9WvyLKYg{yUI;pT`&)vjj9ddU*|!4h zf7ABk+Fn7X6;mbB*NYyUNyv8l++xWdNRH!tWyJ|3Bm{@VG1@~z(D~fvS*+nDm&4#M zt&8%dS9)nv_OqLs1C#uBVra9Vp@6(y8Q7B|fW?k6q6L`g^z!Cs#DdGK94rOuH8_+7 z!g4@T7bdRV!GLZMS~mAs5nt_*Ob*g6T-zwXjxPJnn6=4?x0_1Rz{SGMqQca!!u092 zdz6cEqFsAI@TMa+U>|GQdeK4Ii*Vt1p3S5Q|4t#-`0FEUg(Y3ZA2u#MPrwp~= z0^zo7-}hONoUOTBxYoC9o^RQ6_5Jn%NbZ1Oi!M-iuy=Xf)#ur^I|(p=i%l>Q+X(_N zz4y+1`gQC=H3@ti>hM~qmiyA@w-OjIb>FryF;>6uy_H~$Hx3%}^(|5-PF!&( z{_D=T7?BPKmNY!Fv zceTr5mqI*#H-hAmz~YYGny5CAd#v@MM?_QN9SaW*po>CClD4qt-K%lTc+Xm>*H2LZ z3o*7Xc*~)DFY2m?dPtJOqNgYhj6B_IuKh4awVVBZH@|6*9k_KFm&CoeC&dy6E&R}} zus?Eq4T}ZS^=)5lh8Zk+u{#h`dv_`q+%qNj2Nt}a9=~NFg2ei^Lv9NuapS!jdgE^t zg3~1d5{hwjJ3Et!J}3wpj29JNh8fL-$ybNm`B32=j!$N3Pv zFzXZnSKVMpuy~*Adadt+85e6wpWdjg#@h#bUwvzHVh@@8L_!jd6+Rt9b3RLcI^z2D zDcG;(U&<5$Fb0TUBL`Cd2s{7xQ0jkFQvZmf>Yc;kaDVqv$piCbT&k<9`(J&jjtTw` zUut7bmZjuG>i-j$n!0)aS5WHfL2U%cWxJv<>)`{ z=$-2SETi77t@s;}{>?`l8XEr=qz_sfT3ef1+8h6O9Q7ZRw6nSEN&7!FX>U*0lc$}3 zzm|A>c1k{vyBrs(Jtag;L=>sr_+q22qZzL*lbIFfW%z4&voN8dQMG z`%Bt-zk4gMUW7mG!{zI8++G*YDYpXYf zgDI)L6dnNvl9lslglCLqh>Wk9j5W4iaCZuzmeM~bNtwkkzqN_^yxsI-5-8vOofRv_ zwi5mO#CoDmj=t04$C>$g86vM##59#+>Nme@SainheDUyue2yU3X_GeL;VbCKcqm8x zd%ru4wD3GFe-43VK)d(qvkW(0G38Cgx zVN|b9VxKW=c+;~RWel~h1Acz_c8&P!(3SE08f zkI4NI)(-9}yKy-|=#3ez9pkhV=Mit3f}+l`0Z^wdS6ibjq&1d**`8@gsF zZxBEsJgcigyP!ZF&qj+(QIxiCH!Ta*Fz%CWOl})dRJ2#_qg;+3Ebx&GcG@ruNjHsG zC6#HkR%vG}g)-bJqy_BwN(Ij9Xt<=u#q+;?QvUvy@f3=Vk0O3|zjaDW?Bjjj>?L%F zamHP{Vo8aa43_yee?#=BxU!8#Hu^lZ{k~vpNsU`ZB<0bh$fR>$n#A&x7;N`Yu!$kGjY?$;&?sz*>p@&p_R`9= zP{viL0ZLWn%jl_$>KHYh59?bKII|J+XxWih%R!J5UK9 zJTUml_2ltChUCYvwrP!KoBrYRwR_^q$;#6ho#tNW!Ru7s=R|rKsOC@oEvx8E(j+ha zy1?^p3aoQ%#Epq%p#I>Lq+)~W*g*oNC+D!4xpZ9_&8kIyQLlWB4?&0?UWw_AadYa15$mx-r7@p2q+5+gj3PyLZl#lE_J^^HBx-Uv5bgksnpYx_iMKGnto2x118U zbxx-ZX{%Z-cSQwrbZ$AZo;B0B!Yu%X6C2#8$wN^0;Sy7xeTwNr*d_AmtHQ((_tE0O z9F1D@k5#GYpC6uwT_L7Np7C+F>rg8iT~DL&#<7yzwK!IQs+0jQ!%6R`E0IYf^v9vY z5pJFwY8*1XPXz8UD)`%?PXshXs8l)Pcq*A5jSN)_3%JTq)BOl@lod46PSsq5vQ`$J z6HR?75WN9W1S}3IFCS&HNITxhQhY9~Fi0>7^Qfa#zhZ1ioWPOgDFFkmFei1MvhGSlJeH1&7aCN`GH&w zy;A7-)gQ;?DWFuzM~u4mY@Z89o>XJyyY$Tj{|KcD8h6M}6#p_NE7Mv9=7$s73ZGr| z`r|GF;%=H(r>hT)W%}^}*rB<#! z6TQ?-dmHz;^38;>sHOtI`HLs&Jg>p7!99a`S(t zkRYETW3SJs`U+VgL&k==kubD~)=807#@0E1!5k4yBExbWuu_nLf`TM~4Z%)`ldrnl z8%znfEJ8`e{XAfq0q*QWT0%S@{U}~5rihZ1pj^3@sg6#!%T}~R6dIQXucS!L!pT8X z2cy$ug(S(54|Z27AGohA2;BRkP2m4Rs}KRk#!id7T}Tr)BnM4*L)4eJoJxQrX;)hd zpG%zf(;AK2th%*M#t!XF41@ zaV5crf`t~ZY@H1-z8dldKV^T@c7KTLiMywUWQiTYyQ7t11sjs^KBwi@mN?DPK`QlH znx_GQafLwfc%N!wP{ycXEpRmAw=|uuiVun_bv2pF^`6c?mCKn@XImOYCc^|{KBKqoi~t4UIsvVAjM4qzW8^h1qWTzd_aK*h=0?0Yx1#Kmsh zHlryr=}iX-xDcZ-s;_dZR9wDxsv}UiArARlE6L^0JR+8c1$oQ6Q9R$(on2R51!A^C zo2~dC66J*URH!3pDgkm2QJHP~Ip)>sv=nvfDs3E;QSOzX_K_qRqZ8vjwzLP1vh%!L zoV!W~Zr9cO5>@K>tu)oG$39pqR0~L0XHN;S$@E>8=IHsTB;$c{5>6t69|P_?m-EvT zESqlhhzJY9%?z}dYbu-Dt>Vr2Q<|0mVe{}f$oReDcen211n$K!kH*5i%u&2*_cSNf z*e0L#D=u6w2p`RDh>`TuBEb>j4m{nXx7ct`KX9Tr0>N~JV{oeb(#a)P&OD4U3$*Ac zzh;4SDVCcc#Kby0|i^Y9!4>RL6*Vj)PX#3Qk9RoW#=sIO| zxaL$pJ|;KtM6M~_r8y2tTT0<6o6@2VA$<`R2XRav-as#A7~Bv-jVO*MzjaJ_l6_%h z(b#m1A$2`7;*5{~WTS9+&iGCOp zf&;fJ-I!6kM^yR*5KssoY2>C;Z-b8}_jr zuHFoBm=*Sl18WTjZybt+@CLobM$Qkji*yE^Swx&AElpxmVie4a<eTsu} zR=5|elEi{)AqaH6M_lEJ%utP_afo~_lhXAp)fkZWP?(;8p{sADwHh(M@9u50lQGGw zQeY~+Crq!7Ma0{~_6X2XwF_R0hN%=G>vBfF?dk54Q==z1)QNQ{mhL+a7Va*JG6pnc zv5eZ&#&t@GZ>nLd(cmR@hE0~YUud0>C?^7fvesVpFk~OLaMSY5A9V37Ut&}s^b0h&;?+W`>%`EzO;pRTNF#7bqBxW5} z)xE6ryF03<3~GAo?3a}2gw^c~LNbg(B6*Z@z~#AtYk9a7OeU6&ghiBL9C<79RYvoF zB5z5YUjGqWFouLo^wF^50qyoq=WeN6*4(`0YB&_H9|}Hal6Ow1k0$X{+~mZS!qT|d zqbT*eZ<@S@38biE&}b2-T@qS6wn;yK>te=tt(&{Cb>tx{C4AZU8|kC^t>ld>U0v_f zqQ~D$`bKrym7+BR4f{h&M_4cRP=<^ymA=@O84oRaaZ);GUG{>$?0!ng0;#KP#e;KE zSL^vw8TsnF`cYZbT=6DZnHHH2Q2Kdq(zgn+Xlctm4}h$!(X}EAna%ipG#QiffHUQM zp>sDE+o?+jj1HX96|4$yR3SX?+{kA<1EV<~n&RV$(P;pZndo*Wcv+IcckAT?E{(e;mSmUJJqH!m(Kj1mj#-1_XN#` z7-Yt7Gu)Fwy{kC;ZpouuxjcQl*IEPlquO+~GNSX2FtA20mB3+K{ngFf&85bx-OP-q z*guH*YC?@j7&4ww(C`OHx0`4zTEj0&-#~d=KR!Fxn6)6A)fvS`0)DO*)n#xgii%Oq zmHt5xU2u4Ga%M>*vS3YI4^>^?_w z;99z;ggj42)#U`oiFPLf4|5V41%A(VXWj;Jax=W#&bZG`ZHi{7pEiBEuG@zB3I(>t z;a>eZJtHf{DgiuPd_TQH>XXd9XEE#;i>i5of z1l?8NaRb!&jNXAp%Ol1aWsDT4j4kLo+N;b!%|w3^^%wOCr}H^%!6qKpAFH9 ztzz>TCBXL4IhA298s-@Dkf8laN#3N4@2;vDV>yA}$PodQleWP1Jc6BiM}=btgRJvM z04!PUMmTd-?=xgSh~g!IA$}UuI4eX06sC7W>SG#syx7ed5@b>yBF+h0Rv|QzUH%(^ zdEaQS`ZE{2_G=A$5P}hg#>i`?!Nkf%rKqg*a4dQH?V6w<7<2Fw?Ofs^Nlx=twPiW&dobFNQ zR)X}$cUtzs^t{x>+Gn+f6wHTdkcnZ*7usqx-3g-#02v~+@ft^64&sol(i4QGp2c#2 z?l!TYew82q>5sdH!iY!Gt3pjrZL94b-@L}&VO;n~J^9He8D+h5fhWryFKqd3WfQ>o z1WJ=k<25U?i})nT{l|@&x##`|Nur3YEE$oKW7%jLJ^Fksf$5THkVb zX)h%3Q9)|56&>2E1zetCj8}Q84B4~ilj)=RY9F+pHfS6ad_($z+Q6mp!Yh-Dqb)2z z1@rGj6dFZGSxVi)Pl7I&0C zq@|yG5h`-)Jw{F;G@F%`E)@&lvDT?!qxc~U2qIiM=%b`59!uU9wo6f;?7Q^nNc9S( z-Njux;AC#*q1{3jO)p19Uj#{&wM})>J!ik2MhZJa!p}^e5x|`4pJ)mKx)|moCKp%k z`0p1@>=v3GX}H}w=Vy=bJ)1R{Y+v4Qzh0{MHcrcvpEDi}>?HCS_RNR=m}BFZf3Bg^ zf~&h?w_rgYLYbVL^;qzhD0#0Ny|c6s&|~OlSL%P^RY=)Q_h0j#8I_^GUWIYabx{c zZycHVhb{AIEkATzRwLP%5Ew0Bz?CUhe0_!@?w$!pF$yPOHpciGS^?Pwz{QnSuz6sL zi10<9S{SN&ziW9QV^N4Lbz>hviwPDxs-XexvO@rXO$;tRmDOv#%$Fh_qCivk$2Yvp6HG^MK4+}8>Aro|^(tWH)e%#~O-AcajMs#5jzunx42ju@lc!2g zs9s3Z!G1}86Se(~w?$Q4ivcS} z&2Kdmq?fgAjmsKxCeCl+!3vq8u8FRmT2l32t-Lyb`s@@&wvgRQ@C2R~Fh|pg1UhHI z^_u9a-{aJrT-b>&C3wjDW@aHGZOP%o)>`zY?}4O)fK{cy)m}ROG~SMOD$8T5Gi>$& zSF5Bnb@6kL28Q;F=Xs45T;P}5d%k-0D|koay?>1PAPZX5c`>+6Q)ci%ji8)Gn%w8` zP@o)1E>>_?)xE11>ita1J3QtHz>9%6V$giQP^Ec zNFkvl^rnQ~A@m|*=twj44xxjfLxA zdG|iwKIiPa$Nhfa`7dK+jrC`(vDTdHna@+=*Z;y9<3{0zC9&lMqNitQsBu|C?Wioe zG+)za2pYb&*JI7!3onm7TA?_#ziL~2VUdg^bZK(4_O^1#X@9o|7d&*xk2KG?P+%0z zdRN$M`9j9AuB+l#igC{?wFXVhNiZ43(gtp+6FaY|`>t|7Aow2&Pr9$PDZFI#m^8+T zQ;-7oENf_Zq-b2{EA7i-UyZ&;wySl0^ye5b_!ar^%TRi={~D6E_#>#vQQ4!!An^c2~@G+_t_`N{D@1Dzn3F&EONyuhgM7KToyHA3d#8vIU%OE zEH*(M-$#8-^>;2@=-1VJ>zJW3n$@1_Aag3oj3RyJ7+oP(HpI$V(xEw{`m`WSUv5kJ zE0mE39yDXh~$e z5&%9)TW9l^tP3n}e~>;Om9Y7#-m$S)pZA=~{=DsMwiGw~hJwACr6j3y;fC0I@#2na zzVGFmKpz%!x!b~G6BnjW@ht1$ir7xvkAHO!f?T;6vrRNA<`6@k3}$|1pQ*tWKMl*! z$f=g2SK79-`jz^P*1o$HarPnJ2s!J%T~l_!-m=4%M-o|W`xB{@HZGm_Xebq}U@0fU z5R@Ef+T6EPjxMa-=Ffd3kCevF|J-Ypwf&+X`HECMsl21cV_Iqhqh0GupBeNwPl@N5 z9nIEz0)uXZcz#k>f85?GIj!?-N?pS(KfGO-Gwg8ahMd&J+t%fPeVmc6KIvm=i<+aHQtMS-mz>1*I~(LPu3aB`SY+X zq!649dXvg-GBAcu&M^IUe0a~d8qi&T5&(^`eljnN5{~5eR@;<{Y#P_nZ69b6kSb(u zT7N>WlXCE65mq(!<1?#ArzP-{#uroCIzCa$$ve(Bmh z<;U@#gEvTHVJ&ywt8@;YrX`DB_A4=4O*Z>}`^(Rufvt#mcgzCj((hL3l8XvzM=!qq z3~g=d`1B)KG|*Onm~p<6#Pak)oXS{+)b&zm!A~TaD(W6#GY$>-_Cw~TJCA-idFvKt z)ISX$6f(}SnC5NN2RHasH>F@VQ-0#G*IYN_Q>A$AyvUpJ$OpgcGS4jD^S&HA#>upM znxfO)5%Z3i+tFr8+NtxbPTfX)oio&k)3flVK*2e3fTYTBMG-y{97Iq|Il zb`1yeyX^?lc?X4!(1scThpaNQn{&luCP8Yy5M~mcZKe>Sv4#ndOB~E8w*FD2*3n~g zc0IDl#e|_zA`WK3HRV4y3D#SbI~nxBE@!0Ok9}h=O9yXP{>3xc;A_t*p-%Ojf+QJC zo&YjvZ=Md<+*W>>IxXUTE(^h=OuOm`WWT!xIkIr|-kEKcEhO!J@Xb2&kjGIf3#cd)M_r?suF`a7RL!x#eW^J29j;q^= zXbxRdt7IA3w^K%cPeH0DFLF$)2dC6p1!``lGWDn)uG-NCq!Zy!?-tvK$gP|3ocz+$osSF0qHGJJJmdu|tSLEL^Xz0xHYH z?$0ZpC9v{FunA|&vxhFQ+mS{F3cz{FL8}iy*ikGC_wR>C9 zfRewmwzbGg)LuxI-TjHQ@z)8tGgEG+EmRJ1d`8gkCnD8NiN_=5BH1jcdW#ilnN zsa@rt)_wvAJFcNc7AiRW7ywDhvS-DHGf@UV_zbpYa4_-M!1I^6K2J{nd0CQTQexRA zvE7?h-Ge-n@4>U)3STfaj8>r~pU`o&_jp`|Ei0V9ZG8N6WZ2z>;AX(#EIyg#FnbGZ zNlb)=2tMZN9}tO>i@E>RYfOz3c#b^@2qvJ1kS(uZpUVT&#mVVCAD+G9eChY#uOm?2 z777cCk1;#?Q6=Aqc%&6McHC6iB(>@yVB&h-Gyuy2m0d%eMGuUcGbIDTcAcY!pP>st zVs7IadsOj|pfxs3YH4-bTvXiDKQ>X_gq#mDja#_YY(yQ+2K*4j&qsf|15HVTBmU!8jQCnP zg+GmH(=V7t8eCp_?4rCl)bdt6?VLBvJHjWISD^f&dpHnjgy^1QZEX0iWwgo^0L5>rh;wuYPDsV zT+}t^166YdZ1As14!zuhCP6_^% zDmw$$mj%qR_bww~s&c8Kzv0QnP{IPd%-w{}CC{{Y+RX64S@W#0$1cYXoJ`H>6c{#Q zW3_A_*4BZirOMwCJ1U!=o(q(VtPZ#e)Jc#>u+`V@$_`3c4+>2kzgj8Tq9?=kq#?E? zS!5g(VUY|H0H4QE5%k0hdx_6&50cNJG#W#Ek$Fc51Fsj*@!2+WZppJ;t(8iy_X188oG(oLJ!|k?5#+OwJ7a zB);)tY~pxnUO891Y~}EYWuKm`dJzE-NR-z z552%^5tfxhkAu>N*K4VE-MEW-0(*A*c105kzosbAZ4t z#v1tPp)3tT8du)%w4T&eR)6+5S9XG1puiOIXnq?t&4c}jf3zm@XkBCw3bOsYVDrMd zcQO1HRbKCE8IC^(SzLTbts!%dMn=K_a<`HI_@H6l2#G zIvPDppIZ@32R|3h9Z&DaW6;8E&r9aIu<7I7Vz1t|tCc(wV|Pm0#%1jxcJ>k@7!0bOG1yu z)YnRn*ncd>^CYrL%AE=w(Y_r!_puTKHMH=kMS%JjLX*|G;+zMWBIg`ZTxpUB zHM$`*=r9C!0-Jb-MvYN?{JHGTc(=)REluWby73vBSRD;PW{KHL#Libp80KUXZN+M7 z+Ocr3smm*o87>N!Tm{#s7Ov-?mK1pxS%+tQ4ApArZE-e>7I^e|IleE1Yak}A8j|H{ zLqlt(SZs4$R{{b~6(OeI_A*by@G;4v#*s z<@Uv=*waFHzCMLjzx;UyiD10^UEuShKjo4u`L91OHCkSR%=-R(eR>F81EklW-p3#x zo*rt#P60$v5LjssfQ_!zo+m=KeGJKPL&6#>1FTp~X4CR5eMV$ahDHGiQ1qJY0a>Lb zfxG5awmJZ#v&5~?JlBYSy$3}euC|upg+B|- zM}*4Z*A#3SH*SHsq=O8=|E1HuNa>%*ZtQ% zX1ARUV64t-(kls$u&n^w8fROgoAXw%bB$c~R%qeYp$#=`H0X3qP{>x0)4xE*FrA#7oSj`=T-^Ung83gI z#eWcr;bCEC{zWJc->Jccg~i3CrKRQN<&~9{ zCr|$#&;H9q`Qil=75q0?@IRA+|LvW8|NifU`ac`$d!POu|0|vT4=C_I6M@VN>i>`9 z@8Ub)I#Wge8)Q7nHd93&Oag&dY1G#hBUBJ@Tuq=gPb>>$mN7UtF+Gwe>yQvN&pKI) zk@rs+=6aVrbzSHD+trDGf{YI{YU#DJRVWs&FB3Dq*Jv8&S!z|^*#PL88S5{`3M=wQ zFa_<%PSj&ErXFZQnl(I}2V@D+ti)Pwci+J{rz8Q!v^>5)f!Zdf%2(xe1J>9oHN{`e zLFIh)Guk3e@eWbOj}`mcsm0qDyn^7J;;%$CIRO1*giXC zfU8h~vj~Rr$&wAkMoK+``N~f==QG`eDM;a+=8FS47_8WcirI(ABpAa`E_w4wX-aJ| z!bw51_{l_CfLXQlb>=S24Mc4%27_r@-Ak6o^+T((4?C+>$$9Ndr{6Vodkae0Te>Zy zEkfw~#6>I_Uil|S8hvo6$?NOEY@3CUz9(J!lM`?XF30xA4N)qDNXaJ{4ImdUU!Z7{XXJ#c_FOy<4RLr1!d8`aXY02ZRbv+4EiU;E2NVx;RDQ}NWmNGDxbkRyFiDM^syh1J z^_bpKyNol{VxoT+-<1``FD$SgWuF)~)7w7@vIwx{`!R0mzMrd?UyXQ9`&*Zphn?ND z$ww4;uLbg7wOBDNjdE z)^0Xi9mWKC)*OpIe|VK9fLk?+n2Jxrx`sB;*2K6Ca~9+wd&y2Mcqz=Rw06Z1{SObf zhZ2^`JN!g5B0>E|!lA{+VsrJQAT1finwFC?6|5GarAqE%hEO43Gbeg3t$Zm*s`Euj zPm!)h=7m9S(~=*ZnL_Hs<@#-dmve-xafA}^h@@d1j|Js985AuGMZRq>9@XDczBrKJ z!#11lg9mH6h$qMu-%f=Fs&EgGjL)3)hh)npt3Fp?I~pLy=AP|)YC6)m%{80##Z;wr z)DITPE6fB;LU1jc=Zu+ow_IPhg18ks?Cj4uh*T|DCGJV+sjKd{b+^R@S`SuEqq%cJ zohH#YErwOD3@DcxeN}bE-NF@0Ik(ktNs@@xDpN-M_VEe)t^zipwh%7Z7mCh3fCch% z9eMIMm_LAVIOIK@()U$U0Ua|fcW@}g3>lm9XSfDEkG#I@w6}13>zNYqI*{8G^;+Tb z>oeT2H5#G#Bj;^|$=#@n69t85q;p3`gx!~zd*wrn_}-_hzy<)7ZNCZ^KQCxD0i{>V zZvvv{WO78^+81v&?!;5s1Q>7+k)^oO9Y9V|ECKmtfDv_31+qmf>&@0{$4nv$ZIz0YyJ@#iZ>qbJ4i~Mlf zJHXd?7t991=A;s;#W}~otdQCSrQKg$y83JAzV2aa^Mr494L`el&?nAib#1}ybw0VT z*A8!5@4n!<(>;QB%v93z1%DwV82Tok&zixqxJ;d|ex4bi!K-VNZ+7uv@|<8IpQ#@n z3Irz+gNy?U2@Vt)r(lQnN`b|??0DN#r32s^@qO9MxXM6<5NQ0#pZV<9x{IR~7VGRa z*K_TK=2DqcNCE|F)PD<72^g2#zL#^VUL&NsJDR8VC%80HR3_9|T1{`%0V{?@tn7gYDW{^K-*)h-wvbyp{m zEefZ6{Ob*sx~0yN?xv=CpBuYwGNIM>SEY^(E1c*cX%C(Hh}lv(jIC!F%I+SLb8xkO zyfL(2_&1O26MHoPD?0V^OQOy&$dfzI^~^Nn1g{(27jUdK9T9#+oB2fjlfo^Qc2}jG z2tG1{Xu>%p;gBzkF%l%??mDp*;ENL%)+Am%a6!p0+d6&b+epUzDKeJe0rq6BlKJJL zh$ttVLa92hPP_I!tb#o*aC6}b*!Aik<4ALUOc;PtBY=il1>^j5i_c$9!b{qjp_~n) zo!^A6*C?UJrKJP_x$cCBPb^(%of`CItSa?$N`ic?WiY>5hZ9v%08pDj`VygSL>8k; zy+|2LW4n+XDu3?`ya8&kZfvl{4Y-j}eR!x(tQ2GqPVr?utZcb@`}+xy2Huv}03Be^ z@X4hf%YtEQ7SL>A*@{{%!rm1u$`ZoVSXyNZC3#QSWuJl_pn*;B65moN2h?^$#Jw4T zA_geJ7+y|PyJ&9AWQQWlOibK2Nnix*d%xU6H~?~uy(>7G!$aU zcl1q?NOEfFb_&@7x)hRr&8a|t{CE3PIPzqUd=Fykz=O$NP-ZFy8nPWiEYQO~DlVhz$@;AxB z4&{jJ=2l7Ou$bq{mgIVD<|y6DRsNCd6mXa;%9^L)me)0uyA_>}qFr$s^z5D`CW z4Ia{zT!v!HX99joy7jjp@Knt4Jxfc$T$kKyyHQbj0rv5#MJ-86R|Ks(ZWRIG5SN}J zw^R)eUE3tqI3*YUBgGn-6a1lqfDIj~Nw$qCx)Q%;^fKSf&>`0nO^}K)kMGGAS1(x( zE>3aFO)b$)PgSaSi@VGn%@JEjmGcyA#gzV#KN3`WH2QMI58NHK>jF`wU1~<7ZX~(I z(h`jj)6kexfe!5Byv25vte^lgc2+cw>RH$A6;i=R;K5V|3Z465 zm|ctVAkpJ_A*BrA+jxP}R(jYhUwxP0jZcEUEolG`TWvO2puQqWwfLsP&7My}jE<~F zKgy!`Z!p%ee@S7^I|tX1f{)cEj%%OCL&{^n1^ItN23`nyQzx38PGpU)WK? zEuF19UG@5Vp3rSl#71RjOoK>G!xy2JfRytgd(jYo*IN_kLdN9r;yP3kAOEvxz2z`m za7q}dP+PBI5L>v)zI~2Fh@CVY(TW%0{iO>EzJ5JN`JFhp?!YQy z_U;J@Su~#AYk>{G|uBHIAF^>I`8}T_$bap2<*QNYKt;d8Py}RcwaM=otI+fiU+TL4d7ZN9s+9F)di*xQLBA%(Xj-KySu}t6G zHhEK=HLHHy6_OfH>w~3$1A?!0#X|1vsW$512W8nhNifh_W*F(_q3e9Br5a89Kpc zdj7+2+`I6i-TX(u{&a-mW6{7FPKT)7fR0j$xK$BbaORZIw9zZJ>l?oh?Nv1$=ARtb zaq=Ad>TpfgfJYB>6))0_9l{*PI5;`E@B$?T{AD9F*-~(a6!->FOb?HIp9qJ#y9F+) z8JRkYeCBh~RN zupD{gcr()Yhg`<;3$wY||8=nW49HNoxw^-8{fH4sUeTq8H zXV5UOC+G~hgX0&$BcobwTf?{noPAu>BuyhF{okX`rHKV*`Ej)k`tAZJ5^oC;i`R z9uNavYXZF)ffx?aeM8Z26w%6M&@G}5h{(#__97rIW@mnlbV2Hl1A-R|&dMfp8|2rQ zKlGMrw$9(+tFGWf{dr4iCa|jUVoC|*WtlUzTjAJQg2qvR z(1P62JN@n<{}*#;>zvwZY*@t_96Z^PR@=PnOh0Vyl5Y~fOHRP73CSjUL~bRYmAu_l z$pz=_2=~7ai@6dCrV@_WJ`p#5!mN!;ncKpiNk@RYqxNlMW3ZYnNCII?IQD6ZZN!85 z9o0P~aZehPI+K2T=FP=3YYBvTetSJ(fDH|=%SH{5gO2sb9n}U-c17vkYQF+z>m@?1 z!dUO)1kSo&0+AxQ`Q^OmNChIoJ@W1#5FJ5z&tp!5p2tagbgD>1^Er8}DSudJiIQNn z;}tG^`OyA!C5i9e1<5yUkJ`#WO4aUC9fIb~1xME&8GH9;3CSUdiP}Rt?t$HK zuWxF}-Qc^}efv_M`NhJbv<5QbOsteRz?xfnLu%|Ib(tWWjXaZGo|=xaj)=v5HgtD@ z`Cm>@MWb{NWYcuMCVYr{^c{TjSdYlJ?ws^**0J$tiI*)OXzad?;ys$CR@s%Wi*v=I zh!m+WPoo+u_msLaXT-k?r4YFO>b}@gNK%cpx%oE!rfpo;=0Zp6`1Y|{J6UiB5vAQS-$fw9i^<0i5)CcnAA3Q@{lQc`ihhg*!hna& zbxFlgz875`t>U8tg0C-K9==KW>kxz4|Eia5P%Wba6c8kMv*GvXv`Nt|RUJUx0>1n1 zzA^AXMC+Rsm06;W!YBmw$v(QCbsrU{? zef32X86uv87cliW97@^SQG{bRKsKC=Ou7KQv}iI`PwYfy{_ZAVjm&xz>bST7>TO~~ zpj=Xrl_Q|5anQSx4`&$Cz1y{pM%{abm8ZRi3ZEZC zDs_<*F7ydw?`+yWh{OEU_;vOJ@t#9oWgm+0R5Q!e?Q{r<_?`H2=tPE&>baL*gYjPL zXe^?U7zWNRaeD5QbGz=(?h#_Pk3Gn+W7wp2nBO;6H$Wvv8cEhaCGMsuHUf)8h*8h> zD)cYn2Tz?~f)-(tKej#m@01A$)^;rW-91T19Jx-wep^k9P&~{ShX3|{4qCHC_Ho0=R?IEBu9zaukz@l!gI;Ov0-`JU^c2aFQ;O?{r z#MP2dFrodhja&QUu==Uv-;?I7CFXZQiUx2gjY-PknrX3*rD3M+2$HH{e^MrJ?v%za zwtRaV?bq!Nr%pf%wLb;-P_>&ThfCI-FI#sHf=yqjYr#kE>8E zSKl)u(JBh|J4wsUmPb0>f^Sj_XZ)PFBnku+EiSnERK4!>hRO7x8{bD`3y=9-y1XRaL6?+dN$P|O9b7994 zoK>rfe6X)V0=i!EwQWjSjm#A3%^4-~$qDTF^YR27`WGD0i`~p)WwZpc!>Z{UgT>r$ z$vjpjwRBF8WLFD*4T0jJLaDe0SzbZ#HZk8!Z>6GObh5h&EP1*D!s76uIUx-=q(M_b z^)0u~X~)kdf>a{qAZItO6+K*}5M&1_{hk8r;3L)a%2agZ@e-`FB_W*~4|50cDr$&; zcr{_-mAprFW*f8@HHIq-gkbA*o@0Fb0c5~A1j;_+l$z+esf|9iQm1_p1d>0reS`U2 zH_Ta4Ocw6sC$lE4pBNun`=qFeW(O$9lcaJ>EmKPkg9GpfAU5aPTuu>Ra6g8^_0&0^FYR{1g_ZPV=N2UHOHBfK`xRb;1?D<+Euo_O zCMC98Ln6VKS7DnS=`4mgLc*s*LJehQ2To`*xi$2Z->>fOi43`jtNtPu9Fa4^NVZon zFj1@#Cs_^vrn2%mf)dWJqzd}P_|yD70n0hcn6EF(ZMxrUzeA=louiG0H8fNZ5d#-6 zOr$kbP1w=1V3@TLcW*rPLp?rS8=X`zSz#iTKYB89e=<+O^%Y*8p2SJMgLFEeu(aH^ zj4%iYdVi4k-Jd`-ZQpJ$PcDo5xXEsXViivn99Z($L0$KHmBGDb~9ZxHv<46 zT0q>}ynNR#GIg0F^9~YZ7Oq+r7908?Pe#ij<>DYymw9IX0W23Rfup;$fSJYjXENe9 z(p!k79|Prh$5 z?PJWP&j>@n^?|d%$0DJhauy%>om@VF&V2V%dH5~4=h5ZyU)vtiZ{7LtxwrXU1^xD+ zc%8I#=P91T2i1zIb{BD;ZA=0!Jv)Sq5(Es=siGkji|0Bj)nWzkrhuVw~|zFeZ-rh9_{~3Tg1Q37ap8y2b9>Zv1(7G)d7)r3FXUTp2u`t&?g=S7b|U z{29`3QF^ONtD}=WiK7V*0@=)hbXI!#nYt>h%KoISje)eN%)i zJ+Vv|i}YaZV%{sPD;DC&8mkFw0-|1<(k?xdN~@?NGsDWrPvl3wAEd)vp(v9X7`nYI z;>x?s2O#A=<@~cH;wwpw+Q(nAM^{2_g?Iu?L~N;tUtAYXc_AC*l z!bU_i5KW?Z+p@j;DCfIFjCO2`dY@^0=Ji_Pk`r>F^)J4PoGrBTYdnlBh+^=3M*&#n z+0xbbhs`k~zgr3vuN)h|JR)(Q>Y}pweoBxy^6Gh}>bcTIk1t?nge?aqfz>{7 z5YJZyzZv=8J9pyWpzuG>0+-sfuWF9N6Zh+g=^7c1mYi>=+2crR-WmRTO-pGhWiQ4gJo8;^GQ{Nn#=P zsU0Xl)|D!Rc#y>2k=5YQ!4A6R@Q7qKB6ZzJyYD!UhnlXz9KOn;wVv zyC3#vRj7XdRKXX8u#rbuS!84jux?$tDcq*&6#{+>Vff4E2n&$a=}>)#QVNl*V?p&q zBzttr@rm@M*n^!IV!p*<99XJ$3&c1UVjG)a?>dkygfMA2kqwCV(J-xKm1u4RtQO)$ zhpLgq1ov;o8H@Y2ByDC3%JpT7k)YzS5CjsCjuiLJ&hl0;y^`078BTu^o2I=wFp$^D zX;qmdOj9OMd2vbRgmhH|)RV;FBaR70nHq_VPy`ZO$qCAM>VuqSCn1njqqeBgkcj(` zY({_Ed1JNRp<_8+Y8Beam2Gjo)#~Ds*}w^^L#A$hmCrEB1b-8LXwt78j!~$ku44vW z=n4Ge32I-4oCZPoT&@%2EH>K-VmPQ#OH#kieM&9%gusy9h6O%Sgbji(AdmU+yc)i`cAHec(O zqYjS+Rg07gY$v$sKm>J=A$P|WuoAj|l(#mqM+4}g{togvZv8yIy zoS#e;JpnKlc&=WeiaQnGr$7cl77m-u;$_%_j3Xu>+50kkAe zhl1Vd36J9&A8gVn*&sKa8+_v|UIf}m_|#a2RbjDZaA~SLVEIzSvS(FFZZ_$pMY?t^ zRC({76E>;e!g|4~CeXfH3$Zv-Fnxh7)K@((&g-gCUAVkiZD1jh0o4q$55{%;3B{?{2!oG|>q8~}m>>40J| z^I!R31nBlroPm^-w6wIWysVO8a8||ToPk|{o=@y?zc`~~>c%>@)&{P& zcnyMymb-UXy*4ED6o(Xfgxw&~dn+1BE4EM2) z@bU3A4Y_LJf5p!)ATTh532>b~OZp$3;@G&jc<3oIl#syUoWbIHjm15m#k)k%ElJQL zmER`^LcGC8%;ycf4hg7%1lK`An;_xL8ebP@WCzEEKAvDYSLiLS@H%kxASiws6*r8$ zG6lLaV}6=q8J=hxLA4G~vx&@iica^ubd3;Q?0q@sj8nn|_e|fbc~^)3Ea{uz%(p<+dvNwgP|iM(_XSk& z9Vq4O{d4sFzf!~S6_-qt11o4jRavJS zv%{K;NnORHZhG`UZQOKol0|pg$>Ch9nNr(lm4xjEhs4CBZR#y=?XRk;uBoYI zn(OY|X=!V_%gh|#yVuuBUasjHtm_$OvSW7#COe0xd&g$_AI>oabYo-V4nbLSH-Y z_jdR8Kkk2F5?_0tzwUqe#uUMQ|C{vs@$2_*W?A^}TjdboXZ`n^QNps!qP21)T|oWx z>UishDG$~-?TbZQ^#l`@s`s2|yER#a^M6=&l2C2Kqw(LU6!M7Wr!9+0i#&O+hwaR= zFyV+0yI@ECVuS15)6d_tLup5>Ivd>*z0Z|mu3IWLkAKKff0{`Dl8w z>(0vuflX_BEfH-d`zndYIph^pAgK0LvdD$)S1H(J*>##^VaR%_Y)$QYnquendOB`Y zb|XV$DP$v4XRCH2OaI&U#x*=lZZq42FLX1Fo-d zbp3W^hSuBds%$g)o$7q&(>u3{gX(vfdn@1W+@>eXzpbq>fbf@eS7=v4g;nTUS`D;w%a-`-LTs>rSzAkyj zyw{vD)mHkp5>RyJ+sY~P;~k0)Y}h2l<}xB4CJr~cre_e}^!@Nf{P!FCT)}YVgS907 z!@YS{$-!?xhPnY&G1Y|d{rSa>^sE!b57g6Mmc@L~o$wV_{<&RUq^!PvSMkLGz=n;q z;f}9L%z`@8s~&OOeAE2v!?)zKKR+_yJ~}*oI(QO4K_jDE!LWNh4V^V@Rd+@9S7 z(e6ZH){IJ?er6+cdr;_U%P7C!H&mFcIBC3PlrLlpAOeYCw)A98It6;`eS*-bkWyTu zSf;8pJ8EI2a##dyNVJ%VwY^8|RqO6G5vV*T=2-Mq3j;%S`*N#f`wA)&`^|`SwsOW0 zN|~W7-1$!6EshQyH_VFq`s&Cd*~;H*tvyF-hlDMwQ)Fu_CfyIiDeiPL6`)R4#=@8% z@ot^}F`wBnOKX9rD@-wGB3Lsir#&pohINhx zQwiO&NwXuBji|rD`nKvtx805Vm~p!orwfbmT7%}1t9$k9nt5pcDR1S?<(`b3K#?nt zewAQ3Ue?NC@A##G_$M~{_~WeG$4ERe%w4WI~sliQK3Yv2%0zRVCFwrexRa)U{RY$1KT#lnG2 zxOjz(Pv@DiBYxDDIm!-r- z<*(8nw|v=sWhCO0=eg=;Zi7@4*-(f?=GGGKGH_e7Mf{SRrbBszED0-MK@W+&DutQc>&m z$4zPUAdJJn_)@CW>V>K$Akot8;mYpm0n|n7h^z)2d*NV|z-tZH)6?!aZ#;yQ%nO5d z;xlK;zp>7clMlH>0QM4h)v8s^748}kyAPLtO)Sf3L>1uR!7~OwR2U>W2kk3 zq{w%nf<06p#CG!jC-qD=b4ipx>lr@fA!iTfRCTW9vMoNYuGO3BTOss#D$so6x_R4A zBcVlt$%GqCXm1Zl9q8_QfN5i=kkb_H~d@NDE={HQ0RSE<0bb%uiWE z7;5j;5$Fvs>{~m6YYia$>9e^3M}_57tAF8+_=rK*WSx_>#rBJWYqHj98Y0YkD`i7%`28-%c?$i@9=^4JyL?N1~MsmH2bqVd!h_b z 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 z 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 literal 0 HcmV?d00001 diff --git a/packages/documentation/img/office365-header-icon.png b/packages/documentation/img/office365-header-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..529191ea65d5d78f1964a4ff675c064e3324873c GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^DImwho^t@)Y*6k#k0@(X5g zcy=QV#7XjYcVXyYmGuB}I14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfBQ{AMOSK2Hc=iK@ zR(rZQhFF|_du1cZLh;FKR%UL^VQEdTT15*_xaf=PkVMessuWl!PC{xWt~$(699F$b}j$_ literal 0 HcmV?d00001 diff --git a/packages/documentation/img/pnpjs-common-uml.svg b/packages/documentation/img/pnpjs-common-uml.svg new file mode 100644 index 000000000..4eb24604e --- /dev/null +++ b/packages/documentation/img/pnpjs-common-uml.svg @@ -0,0 +1,220 @@ + + + + +Gyuml_util + +Util + +getCtxCallback +dateAdd +combinePaths +getRandomString +getGUID +isFunc +objectDefinedNotNull +isArray +extend +isUrlAbsolute +stringIsNullOrEmpty +getAttrValueFromString +sanitizeGuid + + +yuml_pnpclientstore + +PnPClientStore + +enabled + +get() +put() +delete() +getOrPut() +deleteExpired() +yuml_pnpclientstoragewrapper + +PnPClientStorageWrapper + +store +defaultTimeoutMinutes +enabled + +get() +put() +delete() +getOrPut() +deleteExpired() +test() +createPersistable() +cacheExpirationHandler() +yuml_pnpclientstore->yuml_pnpclientstoragewrapper + + +yuml_pnpclientstorage + +PnPClientStorage + +_local +_session +local +session + + +yuml_ispfxcontext + +ISPFXContext + +graphHttpClient +pageContext + + +yuml_ispfxgraphhttpclient + +ISPFXGraphHttpClient + + + +fetch() +yuml_fetchclient + +FetchClient + + + +fetch() +yuml_bearertokenfetchclient + +BearerTokenFetchClient + +_token +token + +fetch() +yuml_fetchclient->yuml_bearertokenfetchclient + + +yuml_adalclient + +AdalClient + +clientId +tenant +redirectUri +_authContext +_displayCallback +_loginPromise + +fromSPFxContext() +fetch() +getToken() +ensureAuthContext() +login() +getResource() +yuml_bearertokenfetchclient->yuml_adalclient + + +yuml_httpclientimpl + +HttpClientImpl + + + +fetch() +yuml_httpclientimpl->yuml_fetchclient + + +yuml_requestclient + +RequestClient + + + +fetch() +fetchRaw() +get() +post() +patch() +delete() +yuml_fetchoptions + +FetchOptions + +method +body + + +yuml_configoptions + +ConfigOptions + +headers +mode +credentials +cache + + +yuml_libraryconfiguration + +LibraryConfiguration + +globalCacheDisable +defaultCachingStore +defaultCachingTimeoutSeconds +enableCacheExpiration +cacheExpirationIntervalMilliseconds +spfxContext + + +yuml_runtimeconfigimpl + +RuntimeConfigImpl + +_v +defaultCachingStore +defaultCachingTimeoutSeconds +globalCacheDisable +enableCacheExpiration +cacheExpirationIntervalMilliseconds +spfxContext + +extend() +get() +yuml_error + +Error +yuml_urlexception + +UrlException + + + + +yuml_error->yuml_urlexception + + +yuml_typedhash + +TypedHash + + + + +yuml_dictionary + +Dictionary + +keys +values +count + +get() +add() +merge() +remove() +getKeys() +getValues() +clear() + + diff --git a/packages/documentation/img/pnpjs-config-store-uml.svg b/packages/documentation/img/pnpjs-config-store-uml.svg new file mode 100644 index 000000000..533057b6a --- /dev/null +++ b/packages/documentation/img/pnpjs-config-store-uml.svg @@ -0,0 +1,56 @@ + + + + +Gyuml_iconfigurationprovider + +IConfigurationProvider + + + +getConfiguration() +yuml_default + +default + +wrappedProvider +cacheKey +store + +getWrappedProvider() +getConfiguration() +selectPnPCache() +yuml_iconfigurationprovider->yuml_default + + +yuml_iconfigurationprovider->yuml_default + + +yuml_error + +Error +yuml_nocacheavailableexception + +NoCacheAvailableException + + + + +yuml_error->yuml_nocacheavailableexception + + +yuml_settings + +Settings + +_settings + +add() +addJSON() +apply() +load() +get() +getJSON() + + diff --git a/packages/documentation/img/pnpjs-graph-uml.svg b/packages/documentation/img/pnpjs-graph-uml.svg new file mode 100644 index 000000000..99b260e53 --- /dev/null +++ b/packages/documentation/img/pnpjs-graph-uml.svg @@ -0,0 +1,602 @@ + + + + +Gyuml_requestclient + +RequestClient +yuml_graphhttpclient + +GraphHttpClient + +_impl + +fetch() +fetchRaw() +get() +post() +patch() +delete() +yuml_requestclient->yuml_graphhttpclient + + +yuml_error + +Error +yuml_nographclientavailableexception + +NoGraphClientAvailableException + + + + +yuml_error->yuml_nographclientavailableexception + + +yuml_graphbatchparseexception + +GraphBatchParseException + + + + +yuml_error->yuml_graphbatchparseexception + + +yuml_graphconfiguration + +GraphConfiguration + + + + +yuml_graphconfigurationpart + +GraphConfigurationPart + +graph + + +yuml_graphruntimeconfigimpl + +GraphRuntimeConfigImpl + +headers +fetchClientFactory + + +yuml_graphqueryableinstance + +GraphQueryableInstance + + + +select() +expand() +yuml_user + +User + + + + +yuml_graphqueryableinstance->yuml_user + + +yuml_team + +Team + + + +update() +get() +yuml_graphqueryableinstance->yuml_team + + +yuml_plan + +Plan + + + + +yuml_graphqueryableinstance->yuml_plan + + +yuml_photo + +Photo + + + +getBlob() +getBuffer() +setContent() +yuml_graphqueryableinstance->yuml_photo + + +yuml_section + +Section + + + + +yuml_graphqueryableinstance->yuml_section + + +yuml_notebook + +Notebook + +sections + + +yuml_graphqueryableinstance->yuml_notebook + + +yuml_onenote + +OneNote + +notebooks +sections +pages + + +yuml_graphqueryableinstance->yuml_onenote + + +yuml_member + +Member + + + + +yuml_graphqueryableinstance->yuml_member + + +yuml_me + +Me + +onenote + + +yuml_graphqueryableinstance->yuml_me + + +yuml_group + +Group + +calendar +events +owners +plans +members +conversations +acceptedSenders +rejectedSenders +photo +team + +addFavorite() +createTeam() +getMemberGroups() +delete() +update() +removeFavorite() +resetUnseenCount() +subscribeByMail() +unsubscribeByMail() +getCalendarView() +yuml_graphqueryableinstance->yuml_group + + +yuml_post + +Post + +attachments + +delete() +forward() +reply() +yuml_graphqueryableinstance->yuml_post + + +yuml_thread + +Thread + +posts + +reply() +delete() +yuml_graphqueryableinstance->yuml_thread + + +yuml_conversation + +Conversation + +threads + +update() +delete() +yuml_graphqueryableinstance->yuml_conversation + + +yuml_event + +Event + + + +update() +delete() +yuml_graphqueryableinstance->yuml_event + + +yuml_calendar + +Calendar + +events + + +yuml_graphqueryableinstance->yuml_calendar + + +yuml_attachment + +Attachment + + + + +yuml_graphqueryableinstance->yuml_attachment + + +yuml_graphqueryablecollection + +GraphQueryableCollection + +count + +filter() +select() +expand() +orderBy() +top() +skip() +skipToken() +yuml_users + +Users + + + +getById() +yuml_graphqueryablecollection->yuml_users + + +yuml_plans + +Plans + + + +getById() +yuml_graphqueryablecollection->yuml_plans + + +yuml_pages + +Pages + + + + +yuml_graphqueryablecollection->yuml_pages + + +yuml_sections + +Sections + + + +getById() +add() +yuml_graphqueryablecollection->yuml_sections + + +yuml_notebooks + +Notebooks + + + +getById() +add() +yuml_graphqueryablecollection->yuml_notebooks + + +yuml_members + +Members + + + +add() +getById() +yuml_graphqueryablecollection->yuml_members + + +yuml_groups + +Groups + + + +getById() +add() +yuml_graphqueryablecollection->yuml_groups + + +yuml_graphqueryablesearchablecollection + +GraphQueryableSearchableCollection + + + +search() +yuml_graphqueryablecollection->yuml_graphqueryablesearchablecollection + + +yuml_senders + +Senders + + + +add() +remove() +yuml_graphqueryablecollection->yuml_senders + + +yuml_posts + +Posts + + + +getById() +add() +yuml_graphqueryablecollection->yuml_posts + + +yuml_threads + +Threads + + + +getById() +add() +yuml_graphqueryablecollection->yuml_threads + + +yuml_conversations + +Conversations + + + +add() +getById() +yuml_graphqueryablecollection->yuml_conversations + + +yuml_events + +Events + + + +getById() +add() +yuml_graphqueryablecollection->yuml_events + + +yuml_calendars + +Calendars + + + + +yuml_graphqueryablecollection->yuml_calendars + + +yuml_attachments + +Attachments + + + +getById() +addFile() +yuml_graphqueryablecollection->yuml_attachments + + +yuml_teamproperties + +TeamProperties + +memberSettings +guestSettings +messagingSettings +funSettings + + +yuml_graphendpoints + +GraphEndpoints + +Beta +V1 + +ensure() +yuml_teamcreateresult + +TeamCreateResult + +data +group +team + + +yuml_teamupdateresult + +TeamUpdateResult + +data +team + + +yuml_teams + +Teams + + + +create() +yuml_graphqueryable + +GraphQueryable + + + +as() +toUrlAndQuery() +getParent() +clone() +setEndpoint() +toRequestContext() +yuml_graphqueryable->yuml_graphqueryableinstance + + +yuml_graphqueryable->yuml_graphqueryablecollection + + +yuml_graphrest + +GraphRest + +groups +teams +me +users + +setup() +yuml_graphqueryable->yuml_graphrest + + +yuml_onenotemethods + +OneNoteMethods + +notebooks +sections +pages + + +yuml_onenotemethods->yuml_onenote + + +yuml_sectionaddresult + +SectionAddResult + +data +section + + +yuml_notebookaddresult + +NotebookAddResult + +data +notebook + + +yuml_owners + +Owners + + + + +yuml_members->yuml_owners + + +yuml_groupaddresult + +GroupAddResult + +group +data + + +yuml_odataqueryable + +ODataQueryable +yuml_odataqueryable->yuml_graphqueryable + + +yuml_graphqueryableconstructor + +GraphQueryableConstructor + + + + +yuml_postforwardinfo + +PostForwardInfo + +comment +toRecipients + + +yuml_eventaddresult + +EventAddResult + +data +event + + +yuml_odatabatch + +ODataBatch +yuml_graphbatch + +GraphBatch + +batchUrl + +executeImpl() +formatRequests() +_parseResponse() +yuml_odatabatch->yuml_graphbatch + + + + diff --git a/packages/documentation/img/pnpjs-logging-uml.svg b/packages/documentation/img/pnpjs-logging-uml.svg new file mode 100644 index 000000000..3a16c31ef --- /dev/null +++ b/packages/documentation/img/pnpjs-logging-uml.svg @@ -0,0 +1,59 @@ + + + + +Gyuml_logger + +Logger + +_instance +activeLogLevel +instance +count + +subscribe() +clearSubscribers() +write() +writeJSON() +log() +error() +yuml_logentry + +LogEntry + +message +level +data + + +yuml_loglistener + +LogListener + + + +log() +yuml_functionlistener + +FunctionListener + +method + +log() +yuml_loglistener->yuml_functionlistener + + +yuml_consolelistener + +ConsoleListener + + + +log() +format() +yuml_loglistener->yuml_consolelistener + + + + diff --git a/packages/documentation/img/pnpjs-nodejs-uml.svg b/packages/documentation/img/pnpjs-nodejs-uml.svg new file mode 100644 index 000000000..2cd7c11da --- /dev/null +++ b/packages/documentation/img/pnpjs-nodejs-uml.svg @@ -0,0 +1,85 @@ + + + + +Gyuml_httpclientimpl + +HttpClientImpl +yuml_spfetchclient + +SPFetchClient + +siteUrl +_clientId +_clientSecret +authEnv +_realm +SharePointServicePrincipal +token + +fetch() +getAddInOnlyAccessToken() +getAuthHostUrl() +getRealm() +getAuthUrl() +getFormattedPrincipal() +toDate() +yuml_httpclientimpl->yuml_spfetchclient + + +yuml_adalfetchclient + +AdalFetchClient + +_tenant +_clientId +_secret +_resource +_authority +authContext + +fetch() +acquireToken() +yuml_httpclientimpl->yuml_adalfetchclient + + +yuml_authtoken + +AuthToken + +token_type +expires_in +not_before +expires_on +resource +access_token + + +yuml_aadtoken + +AADToken + +accessToken +expiresIn +expiresOn +isMRRT +resource +tokenType + + +yuml_error + +Error +yuml_authurlexception + +AuthUrlException + + + + +yuml_error->yuml_authurlexception + + + + diff --git a/packages/documentation/img/pnpjs-odata-uml.svg b/packages/documentation/img/pnpjs-odata-uml.svg new file mode 100644 index 000000000..fd1726a34 --- /dev/null +++ b/packages/documentation/img/pnpjs-odata-uml.svg @@ -0,0 +1,253 @@ + + + + +Gyuml_queryable + +Queryable + +_options +_query +_url +_parentUrl +_useCaching +_cachingOptions +query +parentUrl + +toUrlAndQuery() +toUrl() +concat() +configure() +configureFrom() +usingCaching() +getCore() +postCore() +patchCore() +deleteCore() +putCore() +append() +extend() +toRequestContext() +yuml_odataqueryable + +ODataQueryable + +_batch +hasBatch +batch + +inBatch() +toUrl() +get() +getCore() +postCore() +patchCore() +deleteCore() +putCore() +addBatchDependency() +yuml_queryable->yuml_odataqueryable + + +yuml_error + +Error +yuml_alreadyinbatchexception + +AlreadyInBatchException + + + + +yuml_error->yuml_alreadyinbatchexception + + +yuml_processhttpclientresponseexception + +ProcessHttpClientResponseException + +status +statusText +data + + +yuml_error->yuml_processhttpclientresponseexception + + +yuml_requestcontext + +RequestContext + +batch +batchDependency +cachingOptions +hasResult +isBatched +isCached +options +parser +pipeline +requestAbsoluteUrl +requestId +result +verb +clientFactory + + +yuml_pipelinemethods + +PipelineMethods + + + +logStart() +caching() +send() +logEnd() +yuml_odataparser + +ODataParser + +hydrate + +parse() +yuml_lambdaparser + +LambdaParser + +parser + +parse() +yuml_odataparser->yuml_lambdaparser + + +yuml_odataparserbase + +ODataParserBase + + + +parse() +parseImpl() +handleError() +parseODataJSON() +yuml_odataparser->yuml_odataparserbase + + +yuml_cachingparserwrapper + +CachingParserWrapper + +parser +cacheOptions + +parse() +cacheData() +yuml_odataparser->yuml_cachingparserwrapper + + +yuml_bufferparser + +BufferParser + + + +parseImpl() +yuml_odataparserbase->yuml_bufferparser + + +yuml_jsonparser + +JSONParser + + + +parseImpl() +yuml_odataparserbase->yuml_jsonparser + + +yuml_blobparser + +BlobParser + + + +parseImpl() +yuml_odataparserbase->yuml_blobparser + + +yuml_textparser + +TextParser + + + +parseImpl() +yuml_odataparserbase->yuml_textparser + + +yuml_odatadefaultparser + +ODataDefaultParser + + + + +yuml_odataparserbase->yuml_odatadefaultparser + + +yuml_odatabatchrequestinfo + +ODataBatchRequestInfo + +url +method +options +parser +resolve +reject +id + + +yuml_odatabatch + +ODataBatch + +_batchId +_dependencies +_requests +_resolveBatchDependencies +batchId +requests + +add() +addDependency() +addResolveBatchDependency() +execute() +executeImpl() +yuml_icachingoptions + +ICachingOptions + +expiration +storeName +key + + +yuml_cachingoptions + +CachingOptions + +key +storage +expiration +storeName +store + + +yuml_icachingoptions->yuml_cachingoptions + + + + diff --git a/packages/documentation/img/pnpjs-sp-addinhelpers-uml.svg b/packages/documentation/img/pnpjs-sp-addinhelpers-uml.svg new file mode 100644 index 000000000..23ce95ab5 --- /dev/null +++ b/packages/documentation/img/pnpjs-sp-addinhelpers-uml.svg @@ -0,0 +1,48 @@ + + + + +Gyuml_sprest + +SPRest +yuml_sprestaddin + +SPRestAddIn + + + +crossDomainSite() +crossDomainWeb() +_cdImpl() +yuml_sprest->yuml_sprestaddin + + +yuml_httpclientimpl + +HttpClientImpl +yuml_sprequestexecutorclient + +SPRequestExecutorClient + +convertToResponse + +fetch() +yuml_httpclientimpl->yuml_sprequestexecutorclient + + +yuml_error + +Error +yuml_sprequestexecutorundefinedexception + +SPRequestExecutorUndefinedException + + + + +yuml_error->yuml_sprequestexecutorundefinedexception + + + + diff --git a/packages/documentation/img/pnpjs-sp-clientsvc-uml.svg b/packages/documentation/img/pnpjs-sp-clientsvc-uml.svg new file mode 100644 index 000000000..5d4be5a9b --- /dev/null +++ b/packages/documentation/img/pnpjs-sp-clientsvc-uml.svg @@ -0,0 +1,167 @@ + + + + +Gyuml_processqueryparser + +ProcessQueryParser + +op + +parse() +findResult() +getParsedResultById() +yuml_imethodparamsbuilder + +IMethodParamsBuilder + + + +string() +number() +boolean() +objectPath() +toArray() +yuml_methodparams + +MethodParams + +_p + +build() +string() +number() +boolean() +objectPath() +toArray() +a() +yuml_imethodparamsbuilder->yuml_methodparams + + +yuml_iobjectpath + +IObjectPath + +path +actions +id + + +yuml_objectpath + +ObjectPath + +path +actions +id +replaceAfter + + +yuml_iobjectpath->yuml_objectpath + + +yuml_objectpathqueue + +ObjectPathQueue + +_paths +_relationships +_xml +_contextIndex +_siteIndex +_webIndex +last +lastIndex +siteIndex +webIndex +contextIndex + +add() +addChildRelationship() +getChildRelationship() +getChildRelationships() +appendAction() +appendActionToLast() +clone() +toArray() +hash() +toBody() +toIndexedTree() +dirty() +yuml_iclientsvcqueryable + +IClientSvcQueryable + + + +select() +usingCaching() +inBatch() +yuml_clientsvcqueryable + +ClientSvcQueryable + +_objectPaths +_selects +_batch +hasBatch +batch + +select() +inBatch() +toUrlAndQuery() +getSelects() +getChild() +getChildProperty() +send() +sendGet() +sendGetCollection() +invokeMethod() +invokeNonQuery() +invokeMethodCollection() +invokeUpdate() +toRequestContext() +addBatchDependency() +invokeMethodImpl() +yuml_iclientsvcqueryable->yuml_clientsvcqueryable + + +yuml_queryable + +Queryable +yuml_queryable->yuml_clientsvcqueryable + + +yuml_clientsvcqueryableconstructor + +ClientSvcQueryableConstructor + + + + +yuml_iobjectpathbatch + +IObjectPathBatch + + + + +yuml_objectpathbatch + +ObjectPathBatch + +parentUrl + +executeImpl() +yuml_iobjectpathbatch->yuml_objectpathbatch + + +yuml_odatabatch + +ODataBatch +yuml_odatabatch->yuml_objectpathbatch + + + + diff --git a/packages/documentation/img/pnpjs-sp-taxonomy-uml.svg b/packages/documentation/img/pnpjs-sp-taxonomy-uml.svg new file mode 100644 index 000000000..5944a31d3 --- /dev/null +++ b/packages/documentation/img/pnpjs-sp-taxonomy-uml.svg @@ -0,0 +1,458 @@ + + + + +Gyuml_changeinformation + +ChangeInformation + +ItemType +OperationType +StartTime +WithinTimeSpan + + +yuml_changeditem + +ChangedItem + +ChangedBy +ChangedTime +Id +ItemType +Operation +SiteId +TermId +ChangedCustomProperties +ChangedLocalCustomProperties +LcidsForChangedDescriptions +LcidsForChangedLabels +TermSetId +FromGroupId +GroupId +ChangedLanguage +IsDefaultLanguageChanged +IsFullFarmRestore + + +yuml_ilabelmatchinfo + +ILabelMatchInfo + +DefaultLabelOnly +ExcludeKeyword +Lcid +ResultCollectionSize +StringMatchOption +TermLabel +TrimDeprecated +TrimUnavailable + + +yuml_timespan + +TimeSpan + +Days +Hours +Milliseconds +Minutes +Seconds +Ticks +TotalDays +TotalHours +TotalMilliseconds +TotalMinutes +TotalSeconds + + +yuml_itermstore + +ITermStore + +hashTagsTermSet +keywordsTermSet +orphanedTermsTermSet +systemGroup + +addGroup() +addLanguage() +commitAll() +deleteLanguage() +get() +getChanges() +getSiteCollectionGroup() +getTermById() +getTermInTermSet() +getTermGroupById() +getTerms() +getTermSetById() +getTermSetsByName() +rollbackAll() +update() +updateCache() +updateUsedTermsOnSite() +yuml_termstore + +TermStore + +hashTagsTermSet +keywordsTermSet +orphanedTermsTermSet +systemGroup + +get() +getTermSetsByName() +getTermSetById() +getTermById() +getTermInTermSet() +getTermGroupById() +getTerms() +getSiteCollectionGroup() +addLanguage() +addGroup() +commitAll() +deleteLanguage() +rollbackAll() +updateCache() +update() +updateUsedTermsOnSite() +getChanges() +yuml_itermstore->yuml_termstore + + +yuml_clientsvcqueryable + +ClientSvcQueryable +yuml_clientsvcqueryable->yuml_termstore + + +yuml_termstores + +TermStores + + + +get() +getByName() +getById() +yuml_clientsvcqueryable->yuml_termstores + + +yuml_termset + +TermSet + +group +terms + +addStakeholder() +deleteStakeholder() +get() +getTermById() +addTerm() +copy() +update() +yuml_clientsvcqueryable->yuml_termset + + +yuml_termsets + +TermSets + + + +get() +getById() +getByName() +yuml_clientsvcqueryable->yuml_termsets + + +yuml_term + +Term + +labels +parent +pinSourceTermSet +reusedTerms +sourceTerm +termSet +termSets + +createLabel() +deprecate() +get() +setDescription() +setLocalCustomProperty() +update() +yuml_clientsvcqueryable->yuml_term + + +yuml_terms + +Terms + + + +get() +getById() +getByName() +yuml_clientsvcqueryable->yuml_terms + + +yuml_termgroup + +TermGroup + +store +termSets + +addContributor() +addGroupManager() +createTermSet() +get() +update() +yuml_clientsvcqueryable->yuml_termgroup + + +yuml_session + +Session + +termStores + +setup() +createBatch() +getDefaultKeywordTermStore() +getDefaultSiteCollectionTermStore() +yuml_clientsvcqueryable->yuml_session + + +yuml_label + +Label + + + +get() +setAsDefaultForLanguage() +delete() +yuml_clientsvcqueryable->yuml_label + + +yuml_labels + +Labels + + + +getByValue() +get() +yuml_clientsvcqueryable->yuml_labels + + +yuml_itermstores + +ITermStores + + + +get() +getByName() +getById() +yuml_itermstores->yuml_termstores + + +yuml_itermstoredata + +ITermStoreData + +DefaultLanguage +Id +IsOnline +Languages +Name +WorkingLanguage + + +yuml_itermset + +ITermSet + +terms +group + +copy() +get() +getTermById() +addTerm() +update() +yuml_itermset->yuml_termset + + +yuml_itermsets + +ITermSets + + + +getById() +getByName() +get() +yuml_itermsets->yuml_termsets + + +yuml_itermsetdata + +ITermSetData + +Contact +CreatedDate +CustomProperties +CustomSortOrder +Description +Id +IsAvailableForTagging +IsOpenForTermCreation +LastModifiedDate +Name +Names +Owner +Stakeholders + + +yuml_iterm + +ITerm + +labels +parent +pinSourceTermSet +reusedTerms +sourceTerm +termSet +termSets + +createLabel() +deprecate() +get() +setDescription() +setLocalCustomProperty() +update() +yuml_iterm->yuml_term + + +yuml_iterms + +ITerms + + + +get() +getById() +getByName() +yuml_iterms->yuml_terms + + +yuml_itermdata + +ITermData + +CustomProperties +CustomSortOrder +Description +Id +IsAvailableForTagging +IsDeprecated +IsKeyword +IsPinned +IsPinnedRoot +IsReused +IsRoot +IsSourceTerm +LastModifiedDate +LocalCustomProperties +MergedTermIds +Name +Owner +PathOfTerm +TermsCount + + +yuml_itermgroup + +ITermGroup + +store +termSets + +addContributor() +addGroupManager() +createTermSet() +get() +update() +yuml_itermgroup->yuml_termgroup + + +yuml_itermgroupdata + +ITermGroupData + +CreatedDate +Description +Id +IsSiteCollectionGroup +IsSystemGroup +LastModifiedDate +Name + + +yuml_itaxonomysession + +ITaxonomySession + +termStores + +setup() +createBatch() +getDefaultKeywordTermStore() +getDefaultSiteCollectionTermStore() +yuml_itaxonomysession->yuml_session + + +yuml_ilabel + +ILabel + + + +get() +setAsDefaultForLanguage() +delete() +yuml_ilabel->yuml_label + + +yuml_ilabels + +ILabels + + + +getByValue() +get() +yuml_ilabels->yuml_labels + + +yuml_ilabeldata + +ILabelData + +IsDefaultForLanguage +Language +Value + + + + diff --git a/packages/documentation/img/pnpjs-sp-uml.svg b/packages/documentation/img/pnpjs-sp-uml.svg new file mode 100644 index 000000000..f71d83e0a --- /dev/null +++ b/packages/documentation/img/pnpjs-sp-uml.svg @@ -0,0 +1,2833 @@ + + + + +Gyuml_requestclient + +RequestClient +yuml_sphttpclient + +SPHttpClient + +_digestCache +_impl + +fetch() +fetchRaw() +get() +post() +patch() +delete() +yuml_requestclient->yuml_sphttpclient + + +yuml_digestcache + +DigestCache + +_httpClient +_digests + +getDigest() +clear() +yuml_cacheddigest + +CachedDigest + +expiration +value + + +yuml_spconfiguration + +SPConfiguration + + + + +yuml_spconfigurationpart + +SPConfigurationPart + +sp + + +yuml_spruntimeconfigimpl + +SPRuntimeConfigImpl + +headers +baseUrl +fetchClientFactory + + +yuml_sharepointqueryableshareableweb + +SharePointQueryableShareableWeb + + + +shareWith() +shareObject() +shareObjectRaw() +unshareObject() +yuml_web + +Web + +webs +allProperties +webinfos +contentTypes +lists +fields +features +availablefields +navigation +siteUsers +siteGroups +siteUserInfoList +regionalSettings +currentUser +folders +userCustomActions +roleDefinitions +relatedItems +rootFolder +associatedOwnerGroup +associatedMemberGroup +associatedVisitorGroup +customListTemplate + +fromUrl() +getParentWeb() +getSubwebsFilteredForCurrentUser() +createBatch() +getFolderByServerRelativeUrl() +getFolderByServerRelativePath() +getFileByServerRelativeUrl() +getFileByServerRelativePath() +getList() +update() +delete() +applyTheme() +applyWebTemplate() +ensureUser() +availableWebTemplates() +getCatalog() +getChanges() +getUserById() +mapToIcon() +getStorageEntity() +setStorageEntity() +removeStorageEntity() +getAppCatalog() +getClientSideWebParts() +addClientSidePage() +addClientSidePageByPath() +yuml_sharepointqueryableshareableweb->yuml_web + + +yuml_sharepointqueryablecollection + +SharePointQueryableCollection + + + +filter() +select() +expand() +orderBy() +skip() +top() +yuml_webinfos + +WebInfos + + + + +yuml_sharepointqueryablecollection->yuml_webinfos + + +yuml_webs + +Webs + + + +add() +yuml_sharepointqueryablecollection->yuml_webs + + +yuml_webpartdefinitions + +WebPartDefinitions + + + +getById() +getByControlId() +yuml_sharepointqueryablecollection->yuml_webpartdefinitions + + +yuml_viewfields + +ViewFields + + + +getSchemaXml() +add() +move() +removeAll() +remove() +yuml_sharepointqueryablecollection->yuml_viewfields + + +yuml_views + +Views + + + +getById() +getByTitle() +add() +yuml_sharepointqueryablecollection->yuml_views + + +yuml_usercustomactions + +UserCustomActions + + + +getById() +add() +clear() +yuml_sharepointqueryablecollection->yuml_usercustomactions + + +yuml_subscriptions + +Subscriptions + + + +getById() +add() +yuml_sharepointqueryablecollection->yuml_subscriptions + + +yuml_siteusers + +SiteUsers + + + +getByEmail() +getById() +getByLoginName() +removeById() +removeByLoginName() +add() +yuml_sharepointqueryablecollection->yuml_siteusers + + +yuml_sitegroups + +SiteGroups + + + +add() +getByName() +getById() +removeById() +removeByLoginName() +yuml_sharepointqueryablecollection->yuml_sitegroups + + +yuml_roledefinitionbindings + +RoleDefinitionBindings + + + + +yuml_sharepointqueryablecollection->yuml_roledefinitionbindings + + +yuml_roledefinitions + +RoleDefinitions + + + +getById() +getByName() +getByType() +add() +yuml_sharepointqueryablecollection->yuml_roledefinitions + + +yuml_roleassignments + +RoleAssignments + + + +add() +remove() +getById() +yuml_sharepointqueryablecollection->yuml_roleassignments + + +yuml_timezones + +TimeZones + + + +getById() +yuml_sharepointqueryablecollection->yuml_timezones + + +yuml_installedlanguages + +InstalledLanguages + + + + +yuml_sharepointqueryablecollection->yuml_installedlanguages + + +yuml_navigationnodes + +NavigationNodes + + + +getById() +add() +moveAfter() +yuml_sharepointqueryablecollection->yuml_navigationnodes + + +yuml_lists + +Lists + + + +getByTitle() +getById() +add() +ensure() +ensureSiteAssetsLibrary() +ensureSitePagesLibrary() +yuml_sharepointqueryablecollection->yuml_lists + + +yuml_itemversions + +ItemVersions + + + +getById() +yuml_sharepointqueryablecollection->yuml_itemversions + + +yuml_items + +Items + + + +getById() +getItemByStringId() +skip() +getPaged() +getAll() +add() +ensureListItemEntityTypeName() +yuml_sharepointqueryablecollection->yuml_items + + +yuml_forms + +Forms + + + +getById() +yuml_sharepointqueryablecollection->yuml_forms + + +yuml_folders + +Folders + + + +getByName() +add() +yuml_sharepointqueryablecollection->yuml_folders + + +yuml_versions + +Versions + + + +getById() +deleteAll() +deleteById() +recycleByID() +deleteByLabel() +recycleByLabel() +restoreByLabel() +yuml_sharepointqueryablecollection->yuml_versions + + +yuml_files + +Files + + + +getByName() +add() +addChunked() +addTemplateFile() +yuml_sharepointqueryablecollection->yuml_files + + +yuml_fields + +Fields + + + +getByTitle() +getByInternalNameOrTitle() +getById() +createFieldAsXml() +add() +addText() +addCalculated() +addDateTime() +addNumber() +addCurrency() +addMultilineText() +addUrl() +addUser() +addLookup() +addChoice() +addMultiChoice() +addBoolean() +yuml_sharepointqueryablecollection->yuml_fields + + +yuml_features + +Features + + + +getById() +add() +remove() +yuml_sharepointqueryablecollection->yuml_features + + +yuml_fieldlinks + +FieldLinks + + + +getById() +yuml_sharepointqueryablecollection->yuml_fieldlinks + + +yuml_contenttypes + +ContentTypes + + + +getById() +addAvailableContentType() +add() +yuml_sharepointqueryablecollection->yuml_contenttypes + + +yuml_replies + +Replies + + + +add() +yuml_sharepointqueryablecollection->yuml_replies + + +yuml_comments + +Comments + + + +getById() +add() +clear() +yuml_sharepointqueryablecollection->yuml_comments + + +yuml_attachmentfiles + +AttachmentFiles + + + +getByName() +add() +addMultiple() +deleteMultiple() +yuml_sharepointqueryablecollection->yuml_attachmentfiles + + +yuml_appcatalog + +AppCatalog + + + +getAppById() +add() +yuml_sharepointqueryablecollection->yuml_appcatalog + + +yuml_webensureuserresult + +WebEnsureUserResult + +data +user + + +yuml_getcatalogresult + +GetCatalogResult + +data +list + + +yuml_webupdateresult + +WebUpdateResult + +data +web + + +yuml_webaddresult + +WebAddResult + +data +web + + +yuml_sharepointqueryableinstance + +SharePointQueryableInstance + + + +select() +expand() +yuml_webpart + +WebPart + + + + +yuml_sharepointqueryableinstance->yuml_webpart + + +yuml_webpartdefinition + +WebPartDefinition + +webpart + +saveChanges() +moveTo() +close() +open() +delete() +yuml_sharepointqueryableinstance->yuml_webpartdefinition + + +yuml_view + +View + +fields + +update() +delete() +renderAsHtml() +yuml_sharepointqueryableinstance->yuml_view + + +yuml_userprofilequery + +UserProfileQuery + +clientPeoplePickerQuery +profileLoader +editProfileLink +isMyPeopleListPublic +myFollowers +myProperties +trendingTags +ownerUserProfile +userProfile + +amIFollowedBy() +amIFollowing() +getFollowedTags() +getFollowersFor() +getPeopleFollowedBy() +getPropertiesFor() +getUserProfilePropertyFor() +hideSuggestion() +isFollowing() +setMyProfilePic() +setSingleValueProfileProperty() +setMultiValuedProfileProperty() +createPersonalSiteEnqueueBulk() +createPersonalSite() +shareAllSocialData() +clientPeoplePickerResolveUser() +clientPeoplePickerSearchUser() +yuml_sharepointqueryableinstance->yuml_userprofilequery + + +yuml_usercustomaction + +UserCustomAction + + + +update() +delete() +yuml_sharepointqueryableinstance->yuml_usercustomaction + + +yuml_subscription + +Subscription + + + +update() +delete() +yuml_sharepointqueryableinstance->yuml_subscription + + +yuml_mysocialquery + +MySocialQuery + + + +followed() +followedCount() +followers() +suggestions() +yuml_sharepointqueryableinstance->yuml_mysocialquery + + +yuml_socialquery + +SocialQuery + +my + +getFollowedSitesUri() +getFollowedDocumentsUri() +follow() +isFollowed() +stopFollowing() +createSocialActorInfoRequestBody() +yuml_sharepointqueryableinstance->yuml_socialquery + + +yuml_currentuser + +CurrentUser + + + + +yuml_sharepointqueryableinstance->yuml_currentuser + + +yuml_siteuser + +SiteUser + +groups + +update() +delete() +yuml_sharepointqueryableinstance->yuml_siteuser + + +yuml_sitegroup + +SiteGroup + +users + +update() +yuml_sharepointqueryableinstance->yuml_sitegroup + + +yuml_site + +Site + +rootWeb +features +userCustomActions + +getRootWeb() +getContextInfo() +getDocumentLibraries() +getWebUrlFromPageUrl() +createBatch() +openWebById() +yuml_sharepointqueryableinstance->yuml_site + + +yuml_filefoldershared + +FileFolderShared + + + +getShareLink() +checkSharingPermissions() +getSharingInformation() +getObjectSharingSettings() +unshare() +deleteSharingLinkByKind() +unshareLink() +getShareable() +yuml_sharepointqueryableinstance->yuml_filefoldershared + + +yuml_sharepointqueryablesecurable + +SharePointQueryableSecurable + +roleAssignments +firstUniqueAncestorSecurableObject + +getUserEffectivePermissions() +getCurrentUserEffectivePermissions() +breakRoleInheritance() +resetRoleInheritance() +userHasPermissions() +currentUserHasPermissions() +hasPermissions() +yuml_sharepointqueryableinstance->yuml_sharepointqueryablesecurable + + +yuml_searchsuggest + +SearchSuggest + + + +execute() +mapQueryToQueryString() +yuml_sharepointqueryableinstance->yuml_searchsuggest + + +yuml_search + +Search + + + +execute() +fixupProp() +yuml_sharepointqueryableinstance->yuml_search + + +yuml_roledefinition + +RoleDefinition + + + +update() +delete() +yuml_sharepointqueryableinstance->yuml_roledefinition + + +yuml_roleassignment + +RoleAssignment + +groups +bindings + +delete() +yuml_sharepointqueryableinstance->yuml_roleassignment + + +yuml_timezone + +TimeZone + + + +utcToLocalTime() +localTimeToUTC() +yuml_sharepointqueryableinstance->yuml_timezone + + +yuml_regionalsettings + +RegionalSettings + +installedLanguages +globalInstalledLanguages +timeZone +timeZones + + +yuml_sharepointqueryableinstance->yuml_regionalsettings + + +yuml_navigationnode + +NavigationNode + +children + +delete() +yuml_sharepointqueryableinstance->yuml_navigationnode + + +yuml_itemversion + +ItemVersion + + + +delete() +yuml_sharepointqueryableinstance->yuml_itemversion + + +yuml_form + +Form + + + + +yuml_sharepointqueryableinstance->yuml_form + + +yuml_version + +Version + + + +delete() +yuml_sharepointqueryableinstance->yuml_version + + +yuml_field + +Field + + + +update() +delete() +setShowInDisplayForm() +setShowInEditForm() +setShowInNewForm() +yuml_sharepointqueryableinstance->yuml_field + + +yuml_feature + +Feature + + + +deactivate() +yuml_sharepointqueryableinstance->yuml_feature + + +yuml_fieldlink + +FieldLink + + + + +yuml_sharepointqueryableinstance->yuml_fieldlink + + +yuml_contenttype + +ContentType + +fieldLinks +fields +parent +workflowAssociations + +delete() +yuml_sharepointqueryableinstance->yuml_contenttype + + +yuml_comment + +Comment + +replies + +like() +unlike() +delete() +yuml_sharepointqueryableinstance->yuml_comment + + +yuml_attachmentfile + +AttachmentFile + + + +getText() +getBlob() +getBuffer() +getJSON() +setContent() +delete() +getParsed() +yuml_sharepointqueryableinstance->yuml_attachmentfile + + +yuml_app + +App + + + +deploy() +retract() +install() +uninstall() +upgrade() +remove() +yuml_sharepointqueryableinstance->yuml_app + + +yuml_sharepointqueryable + +SharePointQueryable + + + +as() +toUrlAndQuery() +getParent() +clone() +toRequestContext() +yuml_sharepointqueryable->yuml_sharepointqueryablecollection + + +yuml_sharepointqueryable->yuml_sharepointqueryableinstance + + +yuml_limitedwebpartmanager + +LimitedWebPartManager + +webparts + +export() +import() +yuml_sharepointqueryable->yuml_limitedwebpartmanager + + +yuml_utilitymethod + +UtilityMethod + + + +getBaseUrl() +excute() +sendEmail() +getCurrentUserEmailAddresses() +resolvePrincipal() +searchPrincipals() +createEmailBodyForInvitation() +expandGroupsToPrincipals() +createWikiPage() +yuml_sharepointqueryable->yuml_utilitymethod + + +yuml_sharepointqueryableshareable + +SharePointQueryableShareable + + + +getShareLink() +shareWith() +shareObject() +unshareObjectWeb() +checkPermissions() +getSharingInformation() +getObjectSharingSettings() +unshareObject() +deleteLinkByKind() +unshareLink() +getRoleValue() +getShareObjectWeb() +sendShareObjectRequest() +yuml_sharepointqueryable->yuml_sharepointqueryableshareable + + +yuml_relateditemmanagerimpl + +RelatedItemManagerImpl + + + +FromUrl() +getRelatedItems() +getPageOneRelatedItems() +addSingleLink() +addSingleLinkToUrl() +addSingleLinkFromUrl() +deleteSingleLink() +yuml_sharepointqueryable->yuml_relateditemmanagerimpl + + +yuml_navigationservice + +NavigationService + + + +getMenuState() +getMenuNodeKey() +yuml_sharepointqueryable->yuml_navigationservice + + +yuml_navigation + +Navigation + +quicklaunch +topNavigationBar + + +yuml_sharepointqueryable->yuml_navigation + + +yuml_viewupdateresult + +ViewUpdateResult + +view +data + + +yuml_viewaddresult + +ViewAddResult + +view +data + + +yuml_utilitymethods + +UtilityMethods + + + +usingCaching() +inBatch() +sendEmail() +getCurrentUserEmailAddresses() +resolvePrincipal() +searchPrincipals() +createEmailBodyForInvitation() +expandGroupsToPrincipals() +createWikiPage() +yuml_utilitymethods->yuml_utilitymethod + + +yuml_createwikipageresult + +CreateWikiPageResult + +data +file + + +yuml_usercustomactionupdateresult + +UserCustomActionUpdateResult + +data +action + + +yuml_usercustomactionaddresult + +UserCustomActionAddResult + +data +action + + +yuml_likedata + +LikeData + +name +loginName +id +email +creationDate + + +yuml_storageentity + +StorageEntity + +Value +Comment +Description + + +yuml_peoplepickerentitydata + +PeoplePickerEntityData + +AccountName +Department +Email +IsAltSecIdPresent +MobilePhone +ObjectId +OtherMails +PrincipalType +SPGroupID +SPUserID +Title + + +yuml_peoplepickerentity + +PeoplePickerEntity + +Description +DisplayText +EntityData +EntityType +IsResolved +Key +MultipleMatches +ProviderDisplayName +ProviderName + + +yuml_peoplepickerquerysettings + +PeoplePickerQuerySettings + +ExcludeAllUsersOnTenantClaim + + +yuml_clientpeoplepickerqueryparameters + +ClientPeoplePickerQueryParameters + +AllowEmailAddresses +AllowMultipleEntities +AllowOnlyEmailAddresses +AllUrlZones +EnabledClaimProviders +ForceClaims +MaximumEntitySuggestions +PrincipalSource +PrincipalType +QuerySettings +QueryString +SharePointGroupID +UrlZone +UrlZoneSpecified +WebApplicationID + + +yuml_fieldcreationproperties + +FieldCreationProperties + +DefaultFormula +Description +EnforceUniqueValues +FieldTypeKind +Group +Hidden +Indexed +Required +Title +ValidationFormula +ValidationMessage + + +yuml_menunodecollection + +MenuNodeCollection + +FriendlyUrlPrefix +Nodes +SimpleUrl +SPSitePrefix +SPWebPrefix +StartingNodeKey +StartingNodeTitle +Version + + +yuml_menunode + +MenuNode + +CustomProperties +FriendlyUrlSegment +IsDeleted +IsHidden +Key +Nodes +NodeType +SimpleUrl +Title + + +yuml_renderlistdataparameters + +RenderListDataParameters + +AllowMultipleValueFilterForTaxonomyFields +DatesInUtc +ExpandGroups +FirstGroupOnly +FolderServerRelativeUrl +ImageFieldsToTryRewriteToCdnUrls +OverrideViewXml +Paging +RenderOptions +ReplaceGroup +ViewXml + + +yuml_wikipagecreationinformation + +WikiPageCreationInformation + +ServerRelativeUrl +WikiHtmlContent + + +yuml_emailproperties + +EmailProperties + +To +CC +BCC +Subject +Body +AdditionalHeaders +From + + +yuml_sharinginformation + +SharingInformation + +canAddExternalPrincipal +canAddInternalPrincipal +canSendEmail +canUseSimplifiedRoles +hasUniquePermissions +currentRole +requiresAccessApproval +hasPendingAccessRequests +pendingAccessRequestsLink +sharedObjectType +directUrl +webUrl +defaultLinkKind +domainRestrictionMode +RestrictedDomains +anonymousLinkExpirationRestrictionDays +permissionsInformation +pickerSettings + + +yuml_objectsharingsettings + +ObjectSharingSettings + +WebUrl +ListId +ItemId +ItemName +ItemUrl +ObjectSharingInformation +AccessRequestMode +PermissionsOnlyMode +InheritingWebLink +ShareByEmailEnabled +IsGuestUser +HasEditRole +HasReadRole +IsPictureLibrary +CanShareFolder +CanSendEmail +DefaultShareLinkType +SupportsAclPropagation +CanCurrentUserShareInternally +CanCurrentUserShareExternally +CanCurrentUserRetrieveReadonlyLink +CanCurrentUserManageReadonlyLink +CanCurrentUserRetrieveReadWriteLink +CanCurrentUserManageReadWriteLink +CanCurrentUserRetrieveOrganizationReadonlyLink +CanCurrentUserManageOrganizationReadonlyLink +CanCurrentUserRetrieveOrganizationReadWriteLink +CanCurrentUserManageOrganizationReadWriteLink +CanSendLink +ShowExternalSharingWarning +SharingPermissions +SimplifiedRoles +GroupsList +Roles +SharePointSettings +IsUserSiteAdmin +RequiredAnonymousLinkExpirationInDays + + +yuml_sharinginformationrequest + +SharingInformationRequest + +maxPrincipalsToReturn +clientSupportedFeatures + + +yuml_sharingentitypermission + +SharingEntityPermission + +inputEntity +resolvedEntity +hasAccess +role + + +yuml_sharingrecipient + +SharingRecipient + +email +alias + + +yuml_spinvitationcreationresult + +SPInvitationCreationResult + +Succeeded +Email +InvitationLink + + +yuml_usersharingresult + +UserSharingResult + +IsUserKnown +Status +Message +User +DisplayName +Email +CurrentRole +AllowedRoles +InvitationLink + + +yuml_sharingresult + +SharingResult + +PermissionsPageRelativeUrl +UsersWithAccessRequests +StatusCode +ErrorMessage +UniquelyPermissionedUsers +GroupsSharedWith +GroupUsersAddedTo +UsersAddedToGroup +InvitedUsers +Name +Url +IconUrl + + +yuml_sharinglinkinfo + +SharingLinkInfo + +AllowsAnonymousAccess +Created +CreatedBy +Expiration +IsActive +IsEditLink +IsFormsLink +IsUnhealthy +LastModified +LastModifiedBy +LinkKind +ShareId +Url + + +yuml_sharelinkresponse + +ShareLinkResponse + +sharingLinkInfo + + +yuml_sharelinkrequest + +ShareLinkRequest + +peoplePickerInput +createLink +emailData +settings + + +yuml_sharelinksettings + +ShareLinkSettings + +shareId +linkKind +expiration +role +allowAnonymousAccess + + +yuml_sharingemaildata + +SharingEmailData + +subject +body + + +yuml_shareobjectoptions + +ShareObjectOptions + +url +loginNames +role +emailData +group +propagateAcl +includeAnonymousLinkInEmail +useSimplifiedRoles + + +yuml_listformdata + +ListFormData + +ContentType +Title +Author +Editor +Created +Modified +Attachments +ListSchema +FormControlMode +FieldControlModes +WebAttributes +ItemAttributes +ListAttributes +CSRCustomLayout +PostBackRequired +PreviousPostBackHandled +UploadMode +SubmitButtonID +ItemContentTypeName +ItemContentTypeId +JSLinks + + +yuml_renderlistdata + +RenderListData + +Row +FirstRow +FolderPermissions +LastRow +FilterLink +ForceNoHierarchy +HierarchyHasIndention + + +yuml_contextinfo + +ContextInfo + +FormDigestTimeoutSeconds +FormDigestValue +LibraryVersion +SiteFullUrl +SupportedSchemaVersions +WebFullUrl + + +yuml_documentlibraryinformation + +DocumentLibraryInformation + +AbsoluteUrl +Modified +ModifiedFriendlyDisplay +ServerRelativeUrl +Title + + +yuml_principalinfo + +PrincipalInfo + +Department +DisplayName +Email +JobTitle +LoginName +Mobile +PrincipalId +PrincipalType +SIPAddress + + +yuml_useridinfo + +UserIdInfo + +NameId +NameIdIssuer + + +yuml_hashtagcollection + +HashTagCollection + +Items + + +yuml_hashtag + +HashTag + +Name +UseCount + + +yuml_userprofile + +UserProfile + +FollowedContent +AccountName +DisplayName +O15FirstRunExperience +PersonalSite +PersonalSiteCapabilities +PersonalSiteFirstCreationError +PersonalSiteFirstCreationTime +PersonalSiteInstantiationState +PersonalSiteLastCreationTime +PersonalSiteNumberOfRetries +PictureImportEnabled +PublicUrl +UrlToCreatePersonalSite + + +yuml_followedcontent + +FollowedContent + +FollowedDocumentsUrl +FollowedSitesUrl + + +yuml_basepermissions + +BasePermissions + +Low +High + + +yuml_xmlschemafieldcreationinformation + +XmlSchemaFieldCreationInformation + +Options +SchemaXml + + +yuml_listitemformupdatevalue + +ListItemFormUpdateValue + +ErrorMessage +FieldName +FieldValue +HasException + + +yuml_changelogitemquery + +ChangeLogitemQuery + +ChangeToken +Contains +Query +QueryOptions +RowLimit +ViewFields +ViewName + + +yuml_listitemcollectionposition + +ListItemCollectionPosition + +PagingInfo + + +yuml_camlquery + +CamlQuery + +DatesInUtc +FolderServerRelativeUrl +ListItemCollectionPosition +ViewXml + + +yuml_changequery + +ChangeQuery + +Add +Alert +ChangeTokenEnd +ChangeTokenStart +ContentType +DeleteObject +Field +File +Folder +Group +GroupMembershipAdd +GroupMembershipDelete +Item +List +Move +Navigation +Rename +Restore +RoleAssignmentAdd +RoleAssignmentDelete +RoleDefinitionAdd +RoleDefinitionDelete +RoleDefinitionUpdate +SecurityPolicy +Site +SystemUpdate +Update +User +View +Web + + +yuml_changetoken + +ChangeToken + +StringValue + + +yuml_subscriptionupdateresult + +SubscriptionUpdateResult + +subscription +data + + +yuml_subscriptionaddresult + +SubscriptionAddResult + +subscription +data + + +yuml_mysocialquerymethods + +MySocialQueryMethods + + + +get() +followed() +followedCount() +followers() +suggestions() +yuml_mysocialquerymethods->yuml_mysocialquery + + +yuml_socialmethods + +SocialMethods + +my + +getFollowedSitesUri() +getFollowedDocumentsUri() +follow() +isFollowed() +stopFollowing() +yuml_socialmethods->yuml_socialquery + + +yuml_mysocialdata + +MySocialData + +SocialActor +MyFollowedDocumentsUri +MyFollowedSitesUri + + +yuml_socialactor + +SocialActor + +ActorType +Id +Uri +Name +IsFollowed +Status +CanFollow +ImageUri +AccountName +EmailAddress +Title +StatusText +PersonalSiteUri +FollowedContentUri +ContentUri +LibraryUri +TagGuid + + +yuml_socialactorinfo + +SocialActorInfo + +AccountName +ActorType +ContentUri +Id +TagGuid + + +yuml_siteuserprops + +SiteUserProps + +Email +Id +IsHiddenInUI +IsShareByEmailGuestUser +IsSiteAdmin +LoginName +PrincipalType +Title + + +yuml_userupdateresult + +UserUpdateResult + +user +data + + +yuml_sitegroupaddresult + +SiteGroupAddResult + +group +data + + +yuml_groupaddresult + +GroupAddResult + +group +data + + +yuml_groupupdateresult + +GroupUpdateResult + +group +data + + +yuml_openwebbyidresult + +OpenWebByIdResult + +data +web + + +yuml_sharepointqueryableshareablefolder + +SharePointQueryableShareableFolder + + + +shareWith() +yuml_filefoldershared->yuml_sharepointqueryableshareablefolder + + +yuml_sharepointqueryableshareablefile + +SharePointQueryableShareableFile + + + +shareWith() +yuml_filefoldershared->yuml_sharepointqueryableshareablefile + + +yuml_folder + +Folder + +contentTypeOrder +files +folders +listItemAllFields +parentFolder +properties +serverRelativeUrl +uniqueContentTypeOrder + +update() +delete() +recycle() +getItem() +moveTo() +yuml_sharepointqueryableshareablefolder->yuml_folder + + +yuml_file + +File + +listItemAllFields +versions + +approve() +cancelUpload() +checkin() +checkout() +copyTo() +delete() +deny() +getLimitedWebPartManager() +moveTo() +publish() +recycle() +undoCheckout() +unpublish() +getText() +getBlob() +getBuffer() +getJSON() +setContent() +getItem() +setContentChunked() +startUpload() +continueUpload() +finishUpload() +yuml_sharepointqueryableshareablefile->yuml_file + + +yuml_sharepointqueryablesecurable->yuml_sharepointqueryableshareableweb + + +yuml_sharepointqueryableshareableitem + +SharePointQueryableShareableItem + + + +getShareLink() +shareWith() +checkSharingPermissions() +getSharingInformation() +getObjectSharingSettings() +unshare() +deleteSharingLinkByKind() +unshareLink() +yuml_sharepointqueryablesecurable->yuml_sharepointqueryableshareableitem + + +yuml_list + +List + +contentTypes +items +views +fields +forms +defaultView +userCustomActions +effectiveBasePermissions +eventReceivers +relatedFields +informationRightsManagementSettings +subscriptions +rootFolder + +getView() +update() +delete() +getChanges() +getItemsByCAMLQuery() +getListItemChangesSinceToken() +recycle() +renderListData() +renderListDataAsStream() +renderListFormData() +reserveListItemId() +getListItemEntityTypeFullName() +addValidateUpdateItemUsingPath() +yuml_sharepointqueryablesecurable->yuml_list + + +yuml_item + +Item + +attachmentFiles +contentType +comments +effectiveBasePermissions +effectiveBasePermissionsForUI +fieldValuesAsHTML +fieldValuesAsText +fieldValuesForEdit +folder +file +versions + +update() +getLikedBy() +like() +unlike() +delete() +recycle() +getWopiFrameUrl() +validateUpdateListItem() +ensureListItemEntityTypeName() +yuml_sharepointqueryableshareableitem->yuml_item + + +yuml_odataqueryable + +ODataQueryable +yuml_odataqueryable->yuml_sharepointqueryable + + +yuml_sharepointqueryableconstructor + +SharePointQueryableConstructor + + + + +yuml_personalresultsuggestion + +PersonalResultSuggestion + +HighlightedTitle +IsBestBet +Title +TypeId +Url + + +yuml_searchsuggestquery + +SearchSuggestQuery + +querytext +count +personalCount +preQuery +hitHighlighting +capitalize +culture +stemming +includePeople +queryRules +prefixMatch + + +yuml_searchsuggestresult + +SearchSuggestResult + +PeopleNames +PersonalResults +Queries + + +yuml_reorderingrule + +ReorderingRule + +MatchValue +Boost +MatchType + + +yuml_searchpropertyvalue + +SearchPropertyValue + +StrVal +BoolVal +Intval +StrArray +QueryPropertyValueTypeIndex + + +yuml_searchproperty + +SearchProperty + +Name +Value + + +yuml_sort + +Sort + +Property +Direction + + +yuml_resulttable + +ResultTable + +GroupTemplateId +ItemTemplateId +Properties +Table +Refiners +ResultTitle +ResultTitleUrl +RowCount +TableType +TotalRows +TotalRowsIncludingDuplicates + + +yuml_resulttablecollection + +ResultTableCollection + +QueryErrors +QueryId +QueryRuleId +CustomResults +RefinementResults +RelevantResults +SpecialTermResults + + +yuml_searchresponse + +SearchResponse + +ElapsedTime +Properties +PrimaryQueryResult +SecondaryQueryResults +SpellingSuggestion +TriggeredRules + + +yuml_searchresult + +SearchResult + +Rank +DocId +WorkId +Title +Author +Size +Path +Description +Write +LastModifiedTime +CollapsingStatus +HitHighlightedSummary +HitHighlightedProperties +contentclass +PictureThumbnailURL +ServerRedirectedURL +ServerRedirectedEmbedURL +ServerRedirectedPreviewURL +FileExtension +ContentTypeId +ParentLink +ViewsLifeTime +ViewsRecent +SectionNames +SectionIndexes +SiteLogo +SiteDescription +importance +SiteName +IsDocument +FileType +IsContainer +WebTemplate +SPWebUrl +UniqueId +ProgId +OriginalPath +RenderTemplateId +PartitionId +UrlZone +Culture + + +yuml_searchquery + +SearchQuery + +Querytext +QueryTemplate +EnableInterleaving +EnableStemming +TrimDuplicates +EnableNicknames +EnableFQL +EnablePhonetic +BypassResultTypes +ProcessBestBets +EnableQueryRules +EnableSorting +GenerateBlockRankLog +SourceId +RankingModelId +StartRow +RowLimit +RowsPerPage +SelectProperties +Culture +RefinementFilters +Refiners +HiddenConstraints +SortList +Timeout +HitHighlightedProperties +ClientType +PersonalizationData +ResultsUrl +QueryTag +Properties +ProcessPersonalFavorites +QueryTemplatePropertiesUrl +ReorderingRules +HitHighlightedMultivaluePropertyLimit +EnableOrderingHitHighlightedProperty +CollapseSpecification +UIlanguage +DesiredSnippetLength +MaxSnippetLength +SummaryLength + + +yuml_searchbuiltinsourceid + +SearchBuiltInSourceId + +Documents +ItemsMatchingContentType +ItemsMatchingTag +ItemsRelatedToCurrentUser +ItemsWithSameKeywordAsThisItem +LocalPeopleResults +LocalReportsAndDataResults +LocalSharePointResults +LocalVideoResults +Pages +Pictures +Popular +RecentlyChangedItems +RecommendedItems +Wiki + + +yuml_searchresults + +SearchResults + +_url +_query +_raw +_primary +ElapsedTime +RowCount +TotalRows +TotalRowsIncludingDuplicates +RawSearchResults +PrimarySearchResults + +getPage() +formatSearchResults() +yuml_searchquerybuilder + +SearchQueryBuilder + +_query +enableInterleaving +enableStemming +trimDuplicates +enableNicknames +enableFql +enablePhonetic +bypassResultTypes +processBestBets +enableQueryRules +enableSorting +generateBlockRankLog +processPersonalFavorites +enableOrderingHitHighlightedProperty + +create() +text() +template() +sourceId() +trimDuplicatesIncludeId() +rankingModelId() +startRow() +rowLimit() +rowsPerPage() +selectProperties() +culture() +timeZoneId() +refinementFilters() +refiners() +hiddenConstraints() +sortList() +timeout() +hithighlightedProperties() +clientType() +personalizationData() +resultsURL() +queryTag() +properties() +queryTemplatePropertiesUrl() +reorderingRules() +hitHighlightedMultivaluePropertyLimit() +collapseSpecification() +uiLanguage() +desiredSnippetLength() +maxSnippetLength() +summaryLength() +toSearchQuery() +extendQuery() +yuml_roledefinitionaddresult + +RoleDefinitionAddResult + +definition +data + + +yuml_roledefinitionupdateresult + +RoleDefinitionUpdateResult + +definition +data + + +yuml_sprest + +SPRest + +_options +_baseUrl +site +web +profiles +social +navigation +utility + +configure() +setup() +searchSuggest() +search() +createBatch() +create() +yuml_relateditemmanger + +RelatedItemManger + + + +getRelatedItems() +getPageOneRelatedItems() +addSingleLink() +addSingleLinkToUrl() +addSingleLinkFromUrl() +deleteSingleLink() +yuml_relateditemmanger->yuml_relateditemmanagerimpl + + +yuml_relateditem + +RelatedItem + +ListId +ItemId +Url +Title +WebId +IconUrl + + +yuml_inavigationservice + +INavigationService + + + +getMenuState() +getMenuNodeKey() +yuml_inavigationservice->yuml_navigationservice + + +yuml_navigationnodeaddresult + +NavigationNodeAddResult + +data +node + + +yuml_listensureresult + +ListEnsureResult + +list +created +data + + +yuml_listupdateresult + +ListUpdateResult + +list +data + + +yuml_listaddresult + +ListAddResult + +list +data + + +yuml_itemupdateresultdata + +ItemUpdateResultData + +odata.etag + + +yuml_itemupdateresult + +ItemUpdateResult + +item +data + + +yuml_itemaddresult + +ItemAddResult + +item +data + + +yuml_pageditemcollection + +PagedItemCollection + +parent +nextUrl +results +hasNext + +getNext() +yuml_folderupdateresult + +FolderUpdateResult + +folder +data + + +yuml_folderaddresult + +FolderAddResult + +folder +data + + +yuml_clientsidepage + +ClientSidePage + +sections +commentsDisabled + +create() +fromFile() +jsonToEscapedString() +escapedStringToJson() +addSection() +toHtml() +fromHtml() +load() +save() +enableComments() +disableComments() +findControlById() +findControl() +setCommentsOn() +mergePartToTree() +mergeColumnToTree() +updateProperties() +yuml_file->yuml_clientsidepage + + +yuml_fileaddresult + +FileAddResult + +file +data + + +yuml_chunkedfileuploadprogressdata + +ChunkedFileUploadProgressData + +uploadId +stage +blockNumber +totalBlocks +chunkSize +currentPointer +fileSize + + +yuml_fieldupdateresult + +FieldUpdateResult + +data +field + + +yuml_fieldaddresult + +FieldAddResult + +data +field + + +yuml_featureaddresult + +FeatureAddResult + +data +feature + + +yuml_error + +Error +yuml_notsupportedinbatchexception + +NotSupportedInBatchException + + + + +yuml_error->yuml_notsupportedinbatchexception + + +yuml_maxcommentlengthexception + +MaxCommentLengthException + + + + +yuml_error->yuml_maxcommentlengthexception + + +yuml_spbatchparseexception + +SPBatchParseException + + + + +yuml_error->yuml_spbatchparseexception + + +yuml_contenttypeaddresult + +ContentTypeAddResult + +contentType +data + + +yuml_commentinfo + +CommentInfo + +text +mentions + + +yuml_identity + +Identity + +loginName +email +name + + +yuml_commentdata + +CommentData + +author +createdDate +id +isLikedByUser +isReply +itemId +likeCount +listId +mentions +parentId +replyCount +text + + +yuml_commentauthordata + +CommentAuthorData + +email +id +isActive +isExternal +jobTitle +loginName +name +principalType +userId + + +yuml_clientsidepart + +ClientSidePart + + + +remove() +yuml_clientsidewebpart + +ClientSideWebpart + +title +description +propertieJson +webPartId +htmlProperties +serverProcessedContent +canvasDataVersion + +fromComponentDef() +import() +setProperties() +getProperties() +toHtml() +fromHtml() +getControlData() +renderHtmlProperties() +parseJsonProperties() +yuml_clientsidepart->yuml_clientsidewebpart + + +yuml_clientsidetext + +ClientSideText + +_text +text + +getControlData() +toHtml() +fromHtml() +yuml_clientsidepart->yuml_clientsidetext + + +yuml_canvascontrol + +CanvasControl + +controlType +dataVersion +column +order +id +controlData +jsonData + +toHtml() +fromHtml() +getControlData() +yuml_canvascontrol->yuml_clientsidepart + + +yuml_canvascolumn + +CanvasColumn + +section +order +factor +controls + +addControl() +getControl() +toHtml() +fromHtml() +getControlData() +remove() +yuml_canvascontrol->yuml_canvascolumn + + +yuml_clientsidewebpartdata + +ClientSideWebpartData + +dataVersion +description +id +instanceId +properties +title +serverProcessedContent + + +yuml_clientsidecontroldata + +ClientSideControlData + +controlType +id +editorType +position +webPartId +displayMode + + +yuml_clientsidecontrolposition + +ClientSideControlPosition + +controlIndex +sectionFactor +sectionIndex +zoneIndex + + +yuml_serverprocessedcontent + +ServerProcessedContent + +searchablePlainTexts +imageSources +links + + +yuml_clientsidepagecomponent + +ClientSidePageComponent + +ComponentType +Id +Manifest +ManifestType +Name +Status + + +yuml_canvassection + +CanvasSection + +page +order +columns +_memId +defaultColumn + +addColumn() +addControl() +toHtml() +remove() +yuml_odatabatch + +ODataBatch +yuml_spbatch + +SPBatch + +baseUrl + +ParseResponse() +executeImpl() +yuml_odatabatch->yuml_spbatch + + +yuml_attachmentfileaddresult + +AttachmentFileAddResult + +file +data + + +yuml_attachmentfileinfo + +AttachmentFileInfo + +name +content + + +yuml_appaddresult + +AppAddResult + +data +file + + + + diff --git a/packages/documentation/package-structure.md b/packages/documentation/package-structure.md new file mode 100644 index 000000000..4b6cf3d40 --- /dev/null +++ b/packages/documentation/package-structure.md @@ -0,0 +1,43 @@ +# Package Structure + +Each of the packages is published with the same structure, so this article applies to all of the packages. We will use @pnp/common as an example for discussion. + +## Folders + +In addition to the files in the root each package has three folders dist, docs, and src. + +### Root Files + +These files are found at the root of each package. + +|File|Description| +|-|-| +|index.d.ts|Referenced in package.json typings property and provides the TypeScript type information for consumers| +|LICENSE|Package license| +|package.json|npm package definition| +|readme.md|Basic readme referencing the docs site| + +### Dist + +The dist folder contains the transpiled files bundled in various ways. You can choose the best file for your usage as needed. Below the {package} will be +replaced with the name of the package - in our examples case this would be "common" making the file name "{package}.es5.js" = "common.es5.js". All of the *.map +files are the debug mapping files related to the .js file of the same name. + +|File|Description| +|-|-| +|{package}.es5.js|Library packaged in es5 format not wrapped as a module| +|{package}.es5.umd.bundle.js|The library bundled with all dependencies into a single UMD module. Global variable will be "pnp.{package}". Referenced in the main property of package.json| +|{package}.es5.umd.bundle.min.js|Minified version of the bundled umd module| +|{package}.es5.umd.js|The library in es5 bundled as a UMD modules with no included dependencies. They are designed to work with the other *.es5.umd.js files. Referenced in the module property of package.json| +|{package}.es5.umd.min.js|Minified version of the es5 umd module| +|{package}.js|es6 format file of the library. Referenced by es2015 property of package.json| + +### Docs + +This folder contains markdown documentation for the library. All packages will include an index.md which serves as the root of the docs. These files are also used +to build the [public site](https://pnp.github.io/pnpjs/). To edit these files they can be found in the packages/{package}/docs folder. + +### Src + +Contains the TypeScript definition files refrenced by the index.d.ts in the package root. These files serve to provide typing information about the library to +consumers who can process typing information. \ No newline at end of file diff --git a/packages/documentation/packages.md b/packages/documentation/packages.md new file mode 100644 index 000000000..99498aa2e --- /dev/null +++ b/packages/documentation/packages.md @@ -0,0 +1,19 @@ +The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within +the @pnp scope. + +The latest published version is **{{version}}**. + +| || | +| ---| -------------|-------------| +| @pnp/| | | +|| [common](../common/docs/index.md) | Provides shared functionality across all pnp libraries | +|| [config-store](../config-store/docs/index.md) | Provides a way to manage configuration within your application | +|| [graph](../graph/docs/index.md) | Provides a fluent api for working with Microsoft Graph | +|| [logging](../logging/docs/index.md) | Light-weight, subscribable logging framework | +|| [nodejs](../nodejs/docs/index.md) | Provides functionality enabling the @pnp libraries within nodejs | +|| [odata](../odata/docs/index.md) | Provides shared odata functionality and base classes | +|| [pnpjs](../pnpjs/docs/index.md) | Rollup library of core functionality (mimics sp-pnp-js) | +|| [sp](../sp/docs/index.md) | Provides a fluent api for working with SharePoint REST | +|| [sp-addinhelpers](../sp-addinhelpers/docs/index.md) | Provides functionality for working within SharePoint add-ins | +|| [sp-clientsvc](../sp-clientsvc/docs/index.md) | Provides based classes used to create a fluent api for working with SharePoint Managed Metadata | +|| [sp-taxonomy](../sp-taxonomy/docs/index.md) | Provides a fluent api for working with SharePoint Managed Metadata | \ No newline at end of file diff --git a/packages/documentation/polyfill.md b/packages/documentation/polyfill.md new file mode 100644 index 000000000..63cd5fe89 --- /dev/null +++ b/packages/documentation/polyfill.md @@ -0,0 +1,77 @@ +# Polyfills + +These libraries may make use of some features not found in older browsers, mainly fetch, Map, and Proxy. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. There are several ways to include this missing functionality. + +## IE 11 Polyfill package + +We created a package you can use to include the needed functionality without having to determine what polyfills are required. Also, this package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you need to support IE 11. + +### Install + +`npm install --save @pnp/polyfill-ie11` + +### Use + +```TypeScript +import "@pnp/polyfill-ie11"; +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("BigList").items.filter(`ID gt 6000`).get().then(r => { + this.domElement.innerHTML += r.map(l => `${l.Title}
    `); +}); +``` + +### SearchQueryBuilder + +Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version for IE 11 as shown below. + +```TypeScript +import "@pnp/polyfill-ie11"; +import { SearchQueryBuilder } from "@pnp/polyfill-ie11/dist/searchquerybuilder"; +import { sp, ISearchQueryBuilder } from "@pnp/sp"; + +// works in IE11 and other browsers +const builder: ISearchQueryBuilder = SearchQueryBuilder().text("test"); + +sp.search(builder).then(r => { + this.domElement.innerHTML = JSON.stringify(r); +}); +``` + +## Polyfill Service + +If acceptable to your design and security requirements you can use a service to provide missing functionality. This loads scripts from a service outside of your and our +control, so please ensure you understand any associated risks. + +To use this option you need to wrap the code in a function, here called "stuffisloaded". Then you need to add another script tag as shown below that will load what you need from the polyfill service. Note the parameter "callback" takes our function name. + +```HTML + + + + +``` + +## Module Loader + +If you are using a module loader you need to load the following two files as well. You can do this form a CDN or your style library. + +1. Download the **es6-promises** polyfill from https://github.com/stefanpenner/es6-promise and upload it to your style library. +2. Download the **fetch** polyfill from https://github.com/github/fetch and upload it to your style library. +2. Download the **corejs** polyfill from https://github.com/zloirock/core-js and upload it to your style library. +3. Update your module loader to set these files as dependencies before the pnp library is opened. + +One issue you still may see is that you get errors that certain libraries are undefined when you try to run your code. This is because your code is running before +these libraries are loaded. You need to ensure that all dependencies are loaded **before** making use of the pnp libraries. diff --git a/packages/documentation/theme/main.html b/packages/documentation/theme/main.html new file mode 100644 index 000000000..9868e07d0 --- /dev/null +++ b/packages/documentation/theme/main.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block analytics %} + spacer +{% endblock %} diff --git a/packages/documentation/transition-guide.md b/packages/documentation/transition-guide.md new file mode 100644 index 000000000..db55fde01 --- /dev/null +++ b/packages/documentation/transition-guide.md @@ -0,0 +1,102 @@ +# Transition Guide + +These libraries are based on the [sp-pnp-js](https://github.com/SharePoint/PnP-JS-Core) library and our goal was to make transition as easy as possible. The most +obvious difference is the splitting of the library into multiple packages. We have however created a rollup library to help folks make the move - though our +recommendation is to switch to the separate packages. This article outlines transitioning your existing projects from sp-pnp-js to the new libraries, please provide +feedback on how we can improve out guidance. + +## Installing @pnp libraries + +With the separation of the packages we needed a way to indicate how they are related, while making things easy for folks to track and update and we have used peer +dependencies between the packages to do this. With each release we will release all packages so that the version numbers move in lock-step, making it easy to ensure +you are working with compatible versions. One thing to keep in mind with peer dependencies is that they are not automatically installed. The advantage is you +will only have one copy of each library in your project. + +Installing peer dependencies is easy, you can specify each of the packages in a single line, here we are installing everything required to use the @pnp/sp package. + +``` +npm i @pnp/logging @pnp/common @pnp/odata @pnp/sp +``` + +If you do not install all of the peer dependencies you will get a message specifying which ones are missing along with the version expected. + +## Import Simplification + +With the separation of packages we have also simplified the imports, and allowed you more control over what you are importing. Compare these two examples showing +the same set of imports, but one is done via sp-pnp-js and the other using the @pnp libraries. + +### From sp-pnp-js +```TypeScript +import pnp, { + Web, + Util, + Logger, + FunctionListener, + LogLevel, +} from "sp-pnp-js"; +``` + +### From @pnp libraries +```TypeScript +import { + Logger, + LogLevel, + FunctionListener +} from "@pnp/logging"; + +import * as Util from "@pnp/common"; + +import { + sp, + Web +} from "@pnp/sp"; +``` + +In the above example the "sp" import replaces "pnp" and is the root of your method chains. Once we have updated our imports we have a few small code changes to make, +depending on how you have used the library in your applications. Watch this short video discussing the most common updates: + + + +## Updated settings file format + +If you are doing local debugging or testing you have likely created a settings.js from the supplied settings.example.js. Please note the format of that file has changed, +the new format is shown below. + +```JavaScript +var settings = { + + spsave: { + username: "develina.devsson@mydevtenant.onmicrosoft.com", + password: "pass@word1", + siteUrl: "https://mydevtenant.sharepoint.com/" + }, + testing: { + enableWebTests: true, + sp: { + id: "{ client id }", + secret: "{ client secret }", + url: "{ site collection url }", + notificationUrl: "{ notification url }", + }, + graph: { + tenant: "{tenant.onmicrosoft.com}", + id: "{your app id}", + secret: "{your secret}" + }, + } +} +``` + +## HttpClient Renamed + +If you used HttpClient from sp-pnp-js it was renamed to SPHttpClient. A transition to @pnp/sp assumes replacement of: + +```TypeScript +import { HttpClient } from 'sp-pnp-js'; +``` + +to the following import statement: + +```TypeScript +import { SPHttpClient } from '@pnp/sp'; +``` diff --git a/packages/graph/docs/contacts.md b/packages/graph/docs/contacts.md new file mode 100644 index 000000000..b9afe76f9 --- /dev/null +++ b/packages/graph/docs/contacts.md @@ -0,0 +1,197 @@ +# @pnp/graph/contacts + +The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described +you can add and edit both contacts and folders in a users Outlook. + +## Get all of the Contacts + +Using the contacts.get() you can get the users contacts from Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.get(); + +const contacts = await graph.me.contacts.get(); + +``` + +## Add a new Contact + +Using the contacts.add() you can a add Contact to the users Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); + +const addedContact = await graph.me.contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); + +``` + +## Get Contact by Id + +Using the contacts.getById() you can get one of the users Contacts in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId'); + +const contact = await graph.me.contacts.getById('userId'); + +``` +## Delete a Contact + +Using the delete you can remove one of the users Contacts in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId').delete(); + +const delContact = await graph.me.contacts.getById('userId').delete(); + +``` + +## Update a Contact + +Using the update you can update one of the users Contacts in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId').update({birthday: "1986-05-30" }); + +const updContact = await graph.me.contacts.getById('userId').update({birthday: "1986-05-30" }); + +``` + +## Get all of the Contact Folders + +Using the contactFolders.get() you can get the users Contact Folders from Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.get(); + +const contactFolders = await graph.me.contactFolders.get(); + +``` + +## Add a new Contact Folder + +Using the contactFolders.add() you can a add Contact Folder to the users Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add('displayName', ''); + +const addedContactFolder = await graph.me.contactFolders.contactFolders.add('displayName', ''); + +``` + +## Get Contact Folder by Id + +Using the contactFolders.getById() you can get one of the users Contact Folders in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId'); + +const contactFolder = await graph.me.contactFolders.getById('folderId'); + +``` +## Delete a Contact Folder + +Using the delete you can remove one of the users Contact Folders in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId').delete(); + +const delContactFolder = await graph.me.contactFolders.getById('folderId').delete(); + +``` + +## Update a Contact Folder + +Using the update you can update one of the users Contact Folders in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('userId').update({displayName: "value" }); + +const updContactFolder = await graph.me.contactFolders.getById('userId').update({displayName: "value" }); + +``` + +## Get all of the Contacts from the Contact Folder + +Using the contacts.get() in the Contact Folder gets the users Contact from the folder. + +```TypeScript +import { graph } from "@pnp/graph"; + +const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId').contacts.get(); + +const contactsInContactFolder = await graph.me.contactFolders.getById('folderId').contacts.get(); + +``` + +## Get Child Folders of the Contact Folder + +Using the childFolders.get() you can get the Child Folders of the current Contact Folder from Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('').childFolders.get(); + +const childFolders = await graph.me.contactFolders.getById('').childFolders.get(); + +``` + +## Add a new Child Folder + +Using the childFolders.add() you can a add Child Folder in a Contact Folder + +```TypeScript +import { graph } from "@pnp/graph"; + +const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('').childFolders.add('displayName', ''); + +const addedChildFolder = await graph.me.contactFolders.getById('').childFolders.add('displayName', ''); + +``` + +## Get Child Folder by Id + +Using the childFolders.getById() you can get one of the users Child Folders in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('').childFolders.getById('folderId'); + +const childFolder = await graph.me.contactFolders.getById('').childFolders.getById('folderId'); + +``` + +## Add Contact in Child Folder of Contact Folder +Using contacts.add in the Child Folder of a Contact Folder, adds a new Contact to that folder + +```TypeScript +import { graph } from "@pnp/graph"; + +const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('').childFolders.getById('folderId').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); + +const addedContact = await graph.me.contactFolders.getById('').childFolders.getById('folderId').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); + +``` + diff --git a/packages/graph/docs/directoryobjects.md b/packages/graph/docs/directoryobjects.md new file mode 100644 index 000000000..1033fc293 --- /dev/null +++ b/packages/graph/docs/directoryobjects.md @@ -0,0 +1,71 @@ +# @pnp/graph/directoryObjects + + +## The groups and directory roles for the user +```TypeScript +import { graph } from "@pnp/graph"; + +const memberOf = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').memberOf.get(); + +const memberOf = await graph.me.memberOf.get(); + +``` + +## Return all the groups the user, group or directoryObject is a member of +```TypeScript +import { graph } from "@pnp/graph"; + +const memberGroups = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups(); + +const memberGroups = await graph.me.getMemberGroups(); + +const memberGroups = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups(); + +const memberGroups = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups(); + + +``` +## Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. +```TypeScript +import { graph } from "@pnp/graph"; + +const memberObjects = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); + +const memberObjects = await graph.me.getMemberObjects(); + +const memberObjects = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); + +const memberObjects = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); + + +``` +## Check for membership in a specified list of groups +And returns from that list those groups of which the specified user, group, or directory object is a member +```TypeScript +import { graph } from "@pnp/graph"; + +const checkedMembers = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); + +const checkedMembers = await graph.me.checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); + +const checkedMembers = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); + +const checkedMembers = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); +``` + +## Get directoryObject by Id +```TypeScript +import { graph } from "@pnp/graph"; + +const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').get(); + +``` + + +## Delete directoryObject +```TypeScript +import { graph } from "@pnp/graph"; + +const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete() + +``` \ No newline at end of file diff --git a/packages/graph/docs/index.md b/packages/graph/docs/index.md new file mode 100644 index 000000000..fbb51c2c2 --- /dev/null +++ b/packages/graph/docs/index.md @@ -0,0 +1,99 @@ +# @pnp/graph + +[![npm version](https://badge.fury.io/js/%40pnp%2Fgraph.svg)](https://badge.fury.io/js/%40pnp%2Fgraph) + +This package contains the fluent api used to call the graph rest services. + +## Getting Started + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/graph --save` + +Import the library into your application and access the root sp object + +```TypeScript +import { graph } from "@pnp/graph"; + +(function main() { + + // here we will load the current web's properties + graph.groups.get().then(g => { + + console.log(`Groups: ${JSON.stringify(g, null, 4)}`); + }); +})() +``` + +## Getting Started with SharePoint Framework + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/graph --save` + +Import the library into your application, update OnInit, and access the root sp object in render + +```TypeScript +import { graph } from "@pnp/graph"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + + graph.setup({ + spfxContext: this.context + }); + }); +} + +// ... + +public render(): void { + + // A simple loading message + this.domElement.innerHTML = `Loading...`; + + // here we will load the current web's properties + graph.groups.get().then(groups => { + + this.domElement.innerHTML = `Groups:

      ${groups.map(g => `
    • ${g.displayName}
    • `).join("")}
    `; + }); +} +``` + +## Getting Started on Nodejs + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/graph @pnp/nodejs --save` + +Import the library into your application, setup the node client, make a request + +```TypeScript +import { graph } from "@pnp/graph"; +import { AdalFetchClient } from "@pnp/nodejs"; + +// do this once per page load +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient("{tenant}.onmicrosoft.com", "AAD Application Id", "AAD Application Secret"); + }, + }, +}); + +// here we will load the groups information +graph.groups.get().then(g => { + + console.log(`Groups: ${JSON.stringify(g, null, 4)}`); +}); +``` + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-graph-uml.svg) + +Graphical UML diagram of @pnp/graph. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/graph/docs/invitations.md b/packages/graph/docs/invitations.md new file mode 100644 index 000000000..05e3a00b0 --- /dev/null +++ b/packages/graph/docs/invitations.md @@ -0,0 +1,15 @@ +# @pnp/graph/invitations + +The ability invite an external user via the invitation manager + +## Create Invitation + +Using the invitations.create() you can create an Invitation. +We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). + +```TypeScript +import { graph } from "@pnp/graph"; + +const invitationResult = await graph.invitations.create('external.user@emailadress.com', 'https://tenant.sharepoint.com/sites/redirecturi'); + +``` diff --git a/packages/graph/docs/onedrive.md b/packages/graph/docs/onedrive.md new file mode 100644 index 000000000..a0a4dafdc --- /dev/null +++ b/packages/graph/docs/onedrive.md @@ -0,0 +1,204 @@ +# @pnp/graph/onedrive + +The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can manage drives and drive items in Onedrive. + +## Get the default drive + +Using the drive.get() you can get the default drive from Onedrive + +```TypeScript +import { graph } from "@pnp/graph"; + +const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives.get(); + +const drives = await graph.me.drives.get(); + +``` + +## Get all of the drives + +Using the drives.get() you can get the users available drives from Onedrive + +```TypeScript +import { graph } from "@pnp/graph"; + +const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives.get(); + +const drives = await graph.me.drives.get(); + +``` + +## Get drive by Id + +Using the drives.getById() you can get one of the available drives in Outlook + +```TypeScript +import { graph } from "@pnp/graph"; + +const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId'); + +const drive = await graph.me.drives.getById('driveId'); + +``` + +## Get the associated list of a drive + +Using the list.get() you get the associated list + +```TypeScript +import { graph } from "@pnp/graph"; + +const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list.get(); + +const list = await graph.me.drives.getById('driveId').list.get(); + +``` + +## Get the recent files + +Using the recent.get() you get the recent files + +```TypeScript +import { graph } from "@pnp/graph"; + +const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent.get(); + +const files = await graph.me.drives.getById('driveId').recent.get(); + +``` + +## Get the files shared with me + +Using the sharedWithMe.get() you get the files shared with the user + +```TypeScript +import { graph } from "@pnp/graph"; + +const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe.get(); + +const shared = await graph.me.drives.getById('driveId').sharedWithMe.get(); + +``` + +## Get the Root folder + +Using the root.get() you get the root folder + +```TypeScript +import { graph } from "@pnp/graph"; + +const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.get(); + +const root = await graph.me.drives.getById('driveId').root.get(); + +``` + +## Get the Children + +Using the children.get() you get the children + +```TypeScript +import { graph } from "@pnp/graph"; + +const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.get(); + +const rootChildren = await graph.me.drives.getById('driveId').root.children.get(); + +const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children.get(); + +const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children.get(); + +``` + +## Add folder or item +Using the add you can add a folder or an item + +```TypeScript +import { graph } from "@pnp/graph"; +import { DriveItem as IDriveItem } from "@microsoft/microsoft-graph-types"; + +const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', {folder: {}}); + +const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', {folder: {}}); + +``` + +## Search items + +Using the search.get() you can search for items, and optionally select properties + +```TypeScript +import { graph } from "@pnp/graph"; + +const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText').get(); + +const search = await graph.me.drives.getById('driveId')root.search('queryText').get(); + +``` + +## Get specific item in drive + +Using the items.getById() you can get a specific item from the current drive + +```TypeScript +import { graph } from "@pnp/graph"; + +const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId'); + +const item = await graph.me.drives.getById('driveId').items.getById('itemId'); + +``` + +## Get thumbnails + +Using the thumbnails.get() you get the thumbnails + +```TypeScript +import { graph } from "@pnp/graph"; + +const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails.get(); + +const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails.get(); + +``` + +## Delete drive item + +Using the delete() you delete the current item + +```TypeScript +import { graph } from "@pnp/graph"; + +const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete(); + +const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete(); + +``` + +## Update drive item + +Using the update() you update the current item + +```TypeScript +import { graph } from "@pnp/graph"; + +const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: "New Name"}); + +const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: "New Name"}); + +``` + +## Move drive item + +Using the move() you move the current item, and optionally update it + +```TypeScript +import { graph } from "@pnp/graph"; + +// Requires a parentReference to the new folder location +const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"}); + +const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"}); + +``` \ No newline at end of file diff --git a/packages/graph/docs/planner.md b/packages/graph/docs/planner.md new file mode 100644 index 000000000..a8ad1ce81 --- /dev/null +++ b/packages/graph/docs/planner.md @@ -0,0 +1,194 @@ +# @pnp/graph/planner + +The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can add, update and delete items in Planner. + +## Get Plans by Id + +Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const plan = await graph.planner.plans.getById('planId'); + +``` + +## Add new Plan + +Using the planner.plans.add() you can create a new Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const newPlan = await graph.planner.plans.add('groupObjectId', 'title'); + +``` + +## Get Tasks in Plan + +Using the tasks.get() you can get the Tasks in a Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const planTasks = await graph.planner.plans.getById('planId').tasks.get(); + +``` + +## Get Buckets in Plan + +Using the buckets.get() you can get the Buckets in a Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const planBuckets = await graph.planner.plans.getById('planId').buckets.get(); + +``` + +## Get Details in Plan + +Using the details.get() you can get the details in a Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const planDetails = await graph.planner.plans.getById('planId').details.get(); + +``` + +## Delete Plan + +Using the delete() you can get delete a Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const delPlan = await graph.planner.plans.getById('planId').delete(); + +``` + +## Update Plan + +Using the update() you can get update a Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title'}); + +``` + +## Get Task by Id + +Using the planner.tasks.getById() you can get a specific Task. +Planner.tasks is not an available endpoint, you need to get a specific Task. + +```TypeScript +import { graph } from "@pnp/graph"; + +const task = await graph.planner.tasks.getById('taskId'); + +``` + +## Add new Task + +Using the planner.tasks.add() you can create a new Task. + +```TypeScript +import { graph } from "@pnp/graph"; + +const newTask = await graph.planner.tasks.add('planId', 'title'); + +``` + +## Get Details in Task + +Using the details.get() you can get the details in a Task. + +```TypeScript +import { graph } from "@pnp/graph"; + +const taskDetails = await graph.planner.tasks.getById('taskId').details.get(); + +``` + +## Delete Task + +Using the delete() you can get delete a Task. + +```TypeScript +import { graph } from "@pnp/graph"; + +const delTask = await graph.planner.tasks.getById('taskId').delete(); + +``` + +## Update Task + +Using the update() you can get update a Task. + +```TypeScript +import { graph } from "@pnp/graph"; + +const updTask = await graph.planner.tasks.getById('taskId').update({properties}); + +``` + +## Get Buckets by Id + +Using the planner.buckets.getById() you can get a specific Bucket. +planner.buckets is not an available endpoint, you need to get a specific Bucket. + +```TypeScript +import { graph } from "@pnp/graph"; + +const bucket = await graph.planner.buckets.getById('bucketId'); + +``` + +## Add new Bucket + +Using the planner.buckets.add() you can create a new Bucket. + +```TypeScript +import { graph } from "@pnp/graph"; + +const newBucket = await graph.planner.buckets.add('name', 'planId'); + +``` + +## Update Bucket + +Using the update() you can get update a Bucket. + +```TypeScript +import { graph } from "@pnp/graph"; + +const updBucket = await graph.planner.buckets.getById('bucketId').update({name: "Name"}); + +``` + +## Delete Bucket + +Using the delete() you can get delete a Bucket. + +```TypeScript +import { graph } from "@pnp/graph"; + +const delBucket = await graph.planner.buckets.getById('bucketId').delete(); + +``` + +## Get Bucket Tasks + +Using the tasks.get() you can get Tasks in a Bucket. + +```TypeScript +import { graph } from "@pnp/graph"; + +const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks.get(); + +``` \ No newline at end of file diff --git a/packages/graph/docs/subscriptions.md b/packages/graph/docs/subscriptions.md new file mode 100644 index 000000000..ff84e8971 --- /dev/null +++ b/packages/graph/docs/subscriptions.md @@ -0,0 +1,63 @@ +# @pnp/graph/subscriptions + +The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: +* Mail, events, and contacts from Outlook. +* Conversations from Office Groups. +* Drive root items from OneDrive. +* Users and Groups from Azure Active Directory. +* Alerts from the Microsoft Graph Security API. + +## Get all of the Subscriptions + +Using the subscriptions.get(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. + +```TypeScript +import { graph } from "@pnp/graph"; + +const subscriptions = await graph.subscriptions.get(); + +``` + +## Create a new Subscription + +Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. +To learn more about the scopes visit [this](https://docs.microsoft.com/en-us/graph/api/subscription-post-subscriptions?view=graph-rest-1.0) url. + +```TypeScript +import { graph } from "@pnp/graph"; + +const addedSubscription = await graph.subscriptions.add("created,updated", "https://webhook.azurewebsites.net/api/send/myNotifyClient", "me/mailFolders('Inbox')/messages", "2019-11-20T18:23:45.9356913Z"); + +``` + +## Get Subscription by Id + +Using the subscriptions.getById() you can get one of the subscriptions + +```TypeScript +import { graph } from "@pnp/graph"; + +const subscription = await graph.subscriptions.getById('subscriptionId'); + +``` +## Delete a Subscription + +Using the subscriptions.getById().delete() you can remove one of the Subscriptions + +```TypeScript +import { graph } from "@pnp/graph"; + +const delSubscription = await graph.subscription.getById('subscriptionId').delete(); + +``` + +## Update a Subscription + +Using the subscriptions.getById().update() you can update one of the Subscriptions + +```TypeScript +import { graph } from "@pnp/graph"; + +const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: "created,updated,deleted" }); + +``` diff --git a/packages/graph/docs/teams.md b/packages/graph/docs/teams.md new file mode 100644 index 000000000..f14ed2ec8 --- /dev/null +++ b/packages/graph/docs/teams.md @@ -0,0 +1,166 @@ +# @pnp/graph/teams + +The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described +you can add, update and delete items in Teams. + +## Teams the user is a member of +```TypeScript +import { graph } from "@pnp/graph"; + +const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams.get(); + +const myJoinedTeams = await graph.me.joinedTeams.get(); + +``` + +## Get Teams by Id + +Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan. + +```TypeScript +import { graph } from "@pnp/graph"; + +const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').get(); + +``` + +## Create new Group and Team +When you create a new group and add a Team, the group needs to have an Owner. Or else we get an error. +So the owner Id is important, and you could just get the users Ids from + +```TypeScript +import { graph } from "@pnp/graph"; + +const users = await graph.users.get(); +``` +Then create +```TypeSCript +import { graph } from "@pnp/graph"; + +const createdGroupTeam = await graph.teams.create('Groupname', 'description', 'OwnerId',{ +"memberSettings": { + "allowCreateUpdateChannels": true +}, +"messagingSettings": { + "allowUserEditMessages": true, +"allowUserDeleteMessages": true +}, +"funSettings": { + "allowGiphy": true, + "giphyContentRating": "strict" +}}); +``` + +## Create a Team via a specific group +Here we get the group via id and use `createTeam` + +```TypeScript +import { graph } from "@pnp/graph"; + +const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ +"memberSettings": { + "allowCreateUpdateChannels": true +}, +"messagingSettings": { + "allowUserEditMessages": true, +"allowUserDeleteMessages": true +}, +"funSettings": { + "allowGiphy": true, + "giphyContentRating": "strict" +}}); +``` + +## Archive a Team +```TypeScript +import { graph } from "@pnp/graph"; + +const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive(); + +``` +## Unarchive a Team +```TypeScript +import { graph } from "@pnp/graph"; + +const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive(); + +``` + +## Clone a Team +```TypeScript +import { graph } from "@pnp/graph"; + +const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( +'Cloned','description','apps,tabs,settings,channels,members','public'); + +``` +## Get all channels of a Team +```TypeScript +import { graph } from "@pnp/graph"; + +const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.get(); + +``` +## Get channel by Id +```TypeScript +import { graph } from "@pnp/graph"; + +const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').get(); + +``` + +## Create a new Channel +```TypeScript +import { graph } from "@pnp/graph"; + +const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description'); + +``` +## Get installed Apps +```TypeScript +import { graph } from "@pnp/graph"; + +const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.get(); + +``` +## Add an App +```TypeScript +import { graph } from "@pnp/graph"; + +const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a'); + +``` +## Remove an App +```TypeScript +import { graph } from "@pnp/graph"; + +const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove(); + +``` +## Get Tabs from a Channel +```TypeScript +import { graph } from "@pnp/graph"; + +const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs +.get(); + +``` +## Get Tab by Id +```TypeScript +import { graph } from "@pnp/graph"; + +const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs +.getById('Id'); + +``` +## Add a new Tab +```TypeScript +import { graph } from "@pnp/graph"; + +const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',{}); + +``` diff --git a/packages/graph/index.ts b/packages/graph/index.ts new file mode 100644 index 000000000..3e87a0433 --- /dev/null +++ b/packages/graph/index.ts @@ -0,0 +1 @@ +export * from "./src/graph"; diff --git a/packages/graph/package.json b/packages/graph/package.json new file mode 100644 index 000000000..321da1af0 --- /dev/null +++ b/packages/graph/package.json @@ -0,0 +1,28 @@ +{ + "name": "@pnp/graph", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides functionality to query the Microsoft Graph", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "@microsoft/microsoft-graph-types": "1.5.0", + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/logging": "0.0.0-PLACEHOLDER", + "@pnp/odata": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} diff --git a/packages/graph/presets/all.ts b/packages/graph/presets/all.ts new file mode 100644 index 000000000..809997ce1 --- /dev/null +++ b/packages/graph/presets/all.ts @@ -0,0 +1,33 @@ +import "../src/attachments"; +import "../src/calendars"; +import "../src/contacts"; +import "../src/conversations"; +import "../src/directory-objects"; +import "../src/groups"; +import "../src/members"; +import "../src/messages"; +import "../src/onedrive"; +import "../src/onenote"; +import "../src/photos"; +import "../src/planner"; +import "../src/subscriptions"; +import "../src/teams"; +import "../src/users"; + +export * from "../src/attachments"; +export * from "../src/calendars"; +export * from "../src/contacts"; +export * from "../src/conversations"; +export * from "../src/directory-objects"; +export * from "../src/groups"; +export * from "../src/members"; +export * from "../src/messages"; +export * from "../src/onedrive"; +export * from "../src/onenote"; +export * from "../src/photos"; +export * from "../src/planner"; +export * from "../src/subscriptions"; +export * from "../src/teams"; +export * from "../src/users"; + +export { graph, GraphRest } from "../src/rest"; diff --git a/packages/graph/src/attachments/conversations.ts b/packages/graph/src/attachments/conversations.ts new file mode 100644 index 000000000..d31352da4 --- /dev/null +++ b/packages/graph/src/attachments/conversations.ts @@ -0,0 +1,14 @@ +import { _Post } from "../conversations/types"; +import { addProp } from "@pnp/odata"; +import { Attachments, IAttachments } from "./types"; + +declare module "../conversations/types" { + interface _Post { + readonly attachments: IAttachments; + } + interface IPost { + readonly attachments: IAttachments; + } +} + +addProp(_Post, "attachments", Attachments); diff --git a/packages/graph/src/attachments/index.ts b/packages/graph/src/attachments/index.ts new file mode 100644 index 000000000..01a049ebe --- /dev/null +++ b/packages/graph/src/attachments/index.ts @@ -0,0 +1,8 @@ +import "./conversations"; + +export { + Attachment, + Attachments, + IAttachment, + IAttachments, +} from "./types"; diff --git a/packages/graph/src/attachments/types.ts b/packages/graph/src/attachments/types.ts new file mode 100644 index 000000000..88b31219a --- /dev/null +++ b/packages/graph/src/attachments/types.ts @@ -0,0 +1,41 @@ +import { Attachment as IAttachmentType } from "@microsoft/microsoft-graph-types"; +import { body, IGetable } from "@pnp/odata"; +import { _GraphQueryableCollection, _GraphQueryableInstance, IGraphQueryableInstance, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { graphPost } from "../operations"; +import { defaultPath, getById, IGetById } from "../decorators"; +import { type } from "../utils/type"; + +/** + * Attachment + */ +export class _Attachment extends _GraphQueryableInstance { } +export interface IAttachment extends IGetable, IGraphQueryableInstance { } +export interface _Attachment extends IGetable { } +export const Attachment = graphInvokableFactory(_Attachment); + +/** + * Attachments + */ +@defaultPath("attachments") +@getById(Attachment) +export class _Attachments extends _GraphQueryableCollection implements IAttachments { + + /** + * Add attachment to this collection + * + * @param name Name given to the attachment file + * @param bytes File content + */ + public addFile(name: string, bytes: string | Blob): Promise { + + return graphPost(this, body(type("#microsoft.graph.fileAttachment", { + contentBytes: bytes, + name, + }))); + } +} +export interface IAttachments extends IGetable, IGetById, IGraphQueryableCollection { + addFile(name: string, bytes: string | Blob): Promise; +} +export interface _Attachments extends IGetable, IGetById { } +export const Attachments = graphInvokableFactory(_Attachments); diff --git a/packages/graph/src/batch.ts b/packages/graph/src/batch.ts new file mode 100644 index 000000000..cdda391e7 --- /dev/null +++ b/packages/graph/src/batch.ts @@ -0,0 +1,204 @@ +import { Batch, ODataBatchRequestInfo } from "@pnp/odata"; +import { Logger, LogLevel } from "@pnp/logging"; +import { extend, jsS, isUrlAbsolute } from "@pnp/common"; +import { GraphRuntimeConfig } from "./config/graphlibconfig"; +import { GraphHttpClient } from "./net/graphhttpclient"; + +interface GraphBatchRequestFragment { + id: string; + method: string; + url: string; + headers?: string[][] | { + [key: string]: string; + }; + body?: any; +} + +interface GraphBatchRequest { + requests: GraphBatchRequestFragment[]; +} + +interface GraphBatchResponseFragment { + id: string; + status: number; + statusText?: string; + method: string; + url: string; + headers?: string[][] | { + [key: string]: string; + }; + body?: any; +} + +interface GraphBatchResponse { + responses: GraphBatchResponseFragment[]; + nextLink?: string; +} + +export class GraphBatch extends Batch { + + constructor(private batchUrl = "https://graph.microsoft.com/v1.0/$batch", private maxRequests = 20) { + super(); + } + + /** + * Urls come to the batch absolute, but the processor expects relative + * @param url Url to ensure is relative + */ + private static makeUrlRelative(url: string): string { + + if (!isUrlAbsolute(url)) { + // already not absolute, just give it back + return url; + } + + let index = url.indexOf(".com/v1.0/"); + + if (index < 0) { + + index = url.indexOf(".com/beta/"); + + if (index > -1) { + + // beta url + return url.substr(index + 10); + } + + } else { + // v1.0 url + return url.substr(index + 9); + } + + // no idea + return url; + } + + private static formatRequests(requests: ODataBatchRequestInfo[]): GraphBatchRequestFragment[] { + + return requests.map((reqInfo, index) => { + + let requestFragment: GraphBatchRequestFragment = { + id: `${++index}`, + method: reqInfo.method, + url: this.makeUrlRelative(reqInfo.url), + }; + + let headers = {}; + + // merge global config headers + if (GraphRuntimeConfig.headers !== undefined && GraphRuntimeConfig.headers !== null) { + + headers = extend(headers, GraphRuntimeConfig.headers); + } + + if (reqInfo.options !== undefined) { + + // merge per request headers + if (reqInfo.options.headers !== undefined && reqInfo.options.headers !== null) { + headers = extend(headers, reqInfo.options.headers); + } + + // add a request body + if (reqInfo.options.body !== undefined && reqInfo.options.body !== null) { + + requestFragment = extend(requestFragment, { + body: reqInfo.options.body, + }); + } + } + + requestFragment = extend(requestFragment, { + headers: headers, + }); + + return requestFragment; + }); + } + + private static parseResponse(requests: ODataBatchRequestInfo[], graphResponse: GraphBatchResponse): Promise<{ nextLink: string, responses: Response[] }> { + + return new Promise((resolve) => { + + const parsedResponses: Response[] = new Array(requests.length).fill(null); + + for (let i = 0; i < graphResponse.responses.length; ++i) { + + const response = graphResponse.responses[i]; + // we create the request id by adding 1 to the index, so we place the response by subtracting one to match + // the array of requests and make it easier to map them by index + const responseId = parseInt(response.id, 10) - 1; + + if (response.status === 204) { + + parsedResponses[responseId] = new Response(); + } else { + + parsedResponses[responseId] = new Response(JSON.stringify(response.body), response); + } + } + + resolve({ + nextLink: graphResponse.nextLink, + responses: parsedResponses, + }); + }); + } + + protected executeImpl(): Promise { + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Executing batch with ${this.requests.length} requests.`, LogLevel.Info); + + if (this.requests.length < 1) { + Logger.write(`Resolving empty batch.`, LogLevel.Info); + return Promise.resolve(); + } + + const client = new GraphHttpClient(); + + // create a working copy of our requests + const requests = this.requests.slice(); + + // this is the root of our promise chain + const promise = Promise.resolve(); + + while (requests.length > 0) { + + const requestsChunk = requests.splice(0, this.maxRequests); + + const batchRequest: GraphBatchRequest = { + requests: GraphBatch.formatRequests(requestsChunk), + }; + + const batchOptions = { + body: jsS(batchRequest), + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + method: "POST", + }; + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Sending batch request.`, LogLevel.Info); + + client.fetch(this.batchUrl, batchOptions) + .then(r => r.json()) + .then((j) => GraphBatch.parseResponse(requestsChunk, j)) + .then((parsedResponse: { nextLink: string, responses: Response[] }) => { + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Resolving batched requests.`, LogLevel.Info); + + parsedResponse.responses.reduce((chain, response, index) => { + + const request = requestsChunk[index]; + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Resolving batched request ${request.method} ${request.url}.`, LogLevel.Verbose); + + return chain.then(_ => request.parser.parse(response).then(request.resolve).catch(request.reject)); + + }, promise); + }); + } + + return promise; + } +} diff --git a/packages/graph/src/calendars/groups.ts b/packages/graph/src/calendars/groups.ts new file mode 100644 index 000000000..0e5fdb2c0 --- /dev/null +++ b/packages/graph/src/calendars/groups.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Group } from "../groups/types"; +import { Calendar, ICalendar, IEvents, Events } from "./types"; + +declare module "../groups/types" { + interface _Group { + readonly attachmentFiles: ICalendar; + readonly events: IEvents; + } + interface IGroup { + readonly attachmentFiles: ICalendar; + readonly events: IEvents; + } +} + +addProp(_Group, "calendar", Calendar, "calendar"); +addProp(_Group, "events", Events); diff --git a/packages/graph/src/calendars/index.ts b/packages/graph/src/calendars/index.ts new file mode 100644 index 000000000..5c1b14cff --- /dev/null +++ b/packages/graph/src/calendars/index.ts @@ -0,0 +1,13 @@ +import "./groups"; + +export { + Calendar, + Calendars, + Event, + EventAddResult, + Events, + ICalendar, + ICalendars, + IEvent, + IEvents, +} from "./types"; diff --git a/packages/graph/src/calendars/types.ts b/packages/graph/src/calendars/types.ts new file mode 100644 index 000000000..f6c3e50a5 --- /dev/null +++ b/packages/graph/src/calendars/types.ts @@ -0,0 +1,76 @@ +import { IGetable, body } from "@pnp/odata"; +import { Event as IEventType, Calendar as ICalendarType } from "@microsoft/microsoft-graph-types"; +import { _GraphQueryableCollection, _GraphQueryableInstance, IGraphQueryableCollection, IGraphQueryableInstance, graphInvokableFactory } from "../graphqueryable"; +import { defaultPath, IDeleteable, deleteable, IUpdateable, updateable, getById, IGetById } from "../decorators"; +import { graphPost } from "../operations"; + +/** + * Calendars + */ +@defaultPath("calendars") +export class _Calendars extends _GraphQueryableCollection implements ICalendars { } +export interface ICalendars extends IGetable, IGraphQueryableCollection { } +export interface _Calendars extends IGetable { } +export const Calendars = graphInvokableFactory(_Calendars); + +/** + * Calendar + */ +export class _Calendar extends _GraphQueryableInstance implements ICalendar { + + public get events(): IEvents { + return Events(this); + } +} +export interface ICalendar extends IGetable, IGraphQueryableInstance { + readonly events: IEvents; +} +export interface _Calendar extends IGetable { } +export const Calendar = graphInvokableFactory(_Calendar); + +/** + * Event + */ +@deleteable() +@updateable() +export class _Event extends _GraphQueryableInstance implements IEvent { } +export interface IEvent extends IGetable, IDeleteable, IUpdateable, IGraphQueryableInstance { } +export interface _Event extends IGetable, IDeleteable, IUpdateable { } +export const Event = graphInvokableFactory(_Event); + +/** + * Events + */ +@defaultPath("events") +@getById(Event) +export class _Events extends _GraphQueryableCollection { + + /** + * Adds a new event to the collection + * + * @param properties The set of properties used to create the event + */ + public async add(properties: IEventType): Promise { + + const data = await graphPost(this, body(properties)); + + return { + data, + event: this.getById(data.id), + }; + } +} +export interface IEvents extends IGetable, IGetById, IGraphQueryableCollection { + getById(id: string): IEvent; + add(properties: IEventType): Promise; +} +export interface _Events extends IGetable, IGetById { } +export const Events = graphInvokableFactory(_Events); + +/** + * EventAddResult + */ +export interface EventAddResult { + data: IEventType; + event: IEvent; +} diff --git a/packages/graph/src/config/graphlibconfig.ts b/packages/graph/src/config/graphlibconfig.ts new file mode 100644 index 000000000..d633b8461 --- /dev/null +++ b/packages/graph/src/config/graphlibconfig.ts @@ -0,0 +1,52 @@ +import { LibraryConfiguration, TypedHash, RuntimeConfig, IHttpClientImpl, AdalClient } from "@pnp/common"; + +export interface GraphConfigurationPart { + graph?: { + /** + * Any headers to apply to all requests + */ + headers?: TypedHash; + + /** + * Defines a factory method used to create fetch clients + */ + fetchClientFactory?: () => IHttpClientImpl; + }; +} + +export interface GraphConfiguration extends LibraryConfiguration, GraphConfigurationPart { } + +export function setup(config: GraphConfiguration): void { + RuntimeConfig.extend(config); +} + +export class GraphRuntimeConfigImpl { + + public get headers(): TypedHash { + + const graphPart = RuntimeConfig.get("graph"); + if (graphPart !== undefined && graphPart !== null && graphPart.headers !== undefined) { + return graphPart.headers; + } + + return {}; + } + + public get fetchClientFactory(): () => IHttpClientImpl { + + const graphPart = RuntimeConfig.get("graph"); + // use a configured factory firt + if (graphPart !== undefined && graphPart !== null && graphPart.fetchClientFactory !== undefined) { + return graphPart.fetchClientFactory; + } + + // then try and use spfx context if available + if (RuntimeConfig.spfxContext !== undefined) { + return () => AdalClient.fromSPFxContext(RuntimeConfig.spfxContext); + } + + throw Error("There is no Graph Client available, either set one using configuraiton or provide a valid SPFx Context using setup."); + } +} + +export let GraphRuntimeConfig = new GraphRuntimeConfigImpl(); diff --git a/packages/graph/src/contacts/index.ts b/packages/graph/src/contacts/index.ts new file mode 100644 index 000000000..ae919f5e5 --- /dev/null +++ b/packages/graph/src/contacts/index.ts @@ -0,0 +1,14 @@ +import "./users"; + +export { + Contact, + IContactAddResult, + ContactFolder, + IContactFolderAddResult, + ContactFolders, + Contacts, + IContact, + IContactFolder, + IContactFolders, + IContacts, +} from "./types"; diff --git a/packages/graph/src/contacts/types.ts b/packages/graph/src/contacts/types.ts new file mode 100644 index 000000000..efce6ea00 --- /dev/null +++ b/packages/graph/src/contacts/types.ts @@ -0,0 +1,137 @@ +import { _GraphQueryableCollection, IGraphQueryableCollection, _GraphQueryableInstance, IGraphQueryableInstance, graphInvokableFactory } from "../graphqueryable"; +import { TypedHash, extend } from "@pnp/common"; +import { Contact as IContactType, ContactFolder as IContactFolderType, EmailAddress as IEmailAddressType } from "@microsoft/microsoft-graph-types"; +import { defaultPath, updateable, deleteable, IUpdateable, IDeleteable, getById, IGetById } from "../decorators"; +import { graphPost } from "../operations"; +import { body, IGetable } from "@pnp/odata"; + +/** + * Contact + */ +@updateable() +@deleteable() +export class _Contact extends _GraphQueryableInstance implements IContact { } +export interface IContact extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { } +export interface _Contact extends IGetable, IUpdateable, IDeleteable { } +export const Contact = graphInvokableFactory(_Contact); + +/** + * Contacts + */ +@defaultPath("contacts") +@getById(Contact) +export class _Contacts extends _GraphQueryableCollection implements IContacts { + + /** + * Create a new Contact for the user. + * + * @param givenName The contact's given name. + * @param surName The contact's surname. + * @param emailAddresses The contact's email addresses. + * @param businessPhones The contact's business phone numbers. + * @param additionalProperties A plain object collection of additional properties you want to set on the new contact + */ + public async add( + givenName: string, + surName: string, + emailAddresses: IEmailAddressType[], + businessPhones: string[], + additionalProperties: TypedHash = {}): Promise { + + const postBody = extend({ businessPhones, emailAddresses, givenName, surName }, additionalProperties); + + const data = await graphPost(this, body(postBody)); + + return { + contact: this.getById(data.id), + data, + }; + } +} +export interface IContacts extends IGetable, IGetById, IGraphQueryableCollection { + add( + givenName: string, + surName: string, + emailAddresses: IEmailAddressType[], + businessPhones: string[], + additionalProperties: TypedHash): Promise; +} +export interface _Contacts extends IGetable, IGetById { } +export const Contacts = graphInvokableFactory(_Contacts); + +/** + * Contact Folder + */ +@deleteable() +@updateable() +export class _ContactFolder extends _GraphQueryableInstance { + /** + * Gets the contacts in this contact folder + */ + public get contacts(): IContacts { + return Contacts(this); + } + + /** + * Gets the contacts in this contact folder + */ + public get childFolders(): IContactFolders { + return ContactFolders(this, "childFolders"); + } +} +export interface IContactFolder extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { + readonly contacts: IContacts; + readonly childFolders: IContactFolders; +} +export interface _ContactFolder extends IGetable, IUpdateable, IDeleteable { } +export const ContactFolder = graphInvokableFactory(_ContactFolder); + +/** + * Contact Folders + */ +@defaultPath("contactFolders") +@getById(ContactFolder) +export class _ContactFolders extends _GraphQueryableCollection implements IContactFolders { + + /** + * Create a new Contact Folder for the user. + * + * @param displayName The folder's display name. + * @param parentFolderId The ID of the folder's parent folder. + */ + public async add(displayName: string, parentFolderId?: string): Promise { + + const postBody = { + displayName: displayName, + parentFolderId: parentFolderId, + }; + + const data = await graphPost(this, body(postBody)); + + return { + contactFolder: this.getById(data.id), + data, + }; + } +} +export interface IContactFolders extends IGetable, IGetById, IGraphQueryableCollection { + add(displayName: string, parentFolderId?: string): Promise; +} +export interface _ContactFolders extends IGetable, IGetById { } +export const ContactFolders = graphInvokableFactory(_ContactFolders); + +/** + * IContactFolderAddResult + */ +export interface IContactFolderAddResult { + data: IContactFolderType; + contactFolder: IContactFolder; +} + +/** + * IContactAddResult + */ +export interface IContactAddResult { + data: IContactType; + contact: IContact; +} diff --git a/packages/graph/src/contacts/users.ts b/packages/graph/src/contacts/users.ts new file mode 100644 index 000000000..aeb033860 --- /dev/null +++ b/packages/graph/src/contacts/users.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _User } from "../users/types"; +import { IContacts, Contacts, ContactFolders, IContactFolders } from "./types"; + +declare module "../users/types" { + interface _User { + readonly contacts: IContacts; + readonly contactFolders: IContactFolders; + } + interface IUser { + readonly contacts: IContacts; + readonly contactFolders: IContactFolders; + } +} + +addProp(_User, "contacts", Contacts); +addProp(_User, "contactFolders", ContactFolders); diff --git a/packages/graph/src/conversations/groups.ts b/packages/graph/src/conversations/groups.ts new file mode 100644 index 000000000..92926ec6e --- /dev/null +++ b/packages/graph/src/conversations/groups.ts @@ -0,0 +1,20 @@ +import { addProp } from "@pnp/odata"; +import { _Group } from "../groups/types"; +import { Conversations, IConversations, ISenders, Senders } from "./types"; + +declare module "../groups/types" { + interface _Group { + readonly conversations: IConversations; + readonly acceptedSenders: ISenders; + readonly rejectedSenders: ISenders; + } + interface IGroup { + readonly conversations: IConversations; + readonly acceptedSenders: ISenders; + readonly rejectedSenders: ISenders; + } +} + +addProp(_Group, "conversations", Conversations); +addProp(_Group, "acceptedSenders", Senders, "acceptedsenders"); +addProp(_Group, "rejectedSenders", Senders, "rejectedsenders"); diff --git a/packages/graph/src/conversations/index.ts b/packages/graph/src/conversations/index.ts new file mode 100644 index 000000000..acefde6c3 --- /dev/null +++ b/packages/graph/src/conversations/index.ts @@ -0,0 +1,19 @@ +import "./groups"; + +export { + Conversation, + Conversations, + IConversation, + IConversations, + IPost, + IPostForwardInfo, + IPosts, + ISenders, + IThread, + IThreads, + Post, + Posts, + Senders, + Thread, + Threads, +} from "./types"; diff --git a/packages/graph/src/conversations/types.ts b/packages/graph/src/conversations/types.ts new file mode 100644 index 000000000..1e69ac66b --- /dev/null +++ b/packages/graph/src/conversations/types.ts @@ -0,0 +1,185 @@ +import { body, IGetable } from "@pnp/odata"; +import { + ConversationThread as IConversationThreadType, + Post as IPostType, + Recipient as IRecipientType, + Conversation as IConversationType, + User as IUserType, +} from "@microsoft/microsoft-graph-types"; +import { + _GraphQueryableCollection, + IGraphQueryableCollection, + _GraphQueryableInstance, + IGraphQueryableInstance, + graphInvokableFactory, +} from "../graphqueryable"; +import { defaultPath, updateable, IUpdateable, deleteable, IDeleteable, addable, IAddable, getById, IGetById } from "../decorators"; +import { graphPost, graphDelete } from "../operations"; + +/** + * Conversations + */ +@defaultPath("conversations") +@addable() +export class _Conversations extends _GraphQueryableCollection implements IConversations { + /** + * Gets a conversation from this collection by id + * + * @param id Group member's id + */ + public getById(id: string): IConversation { + return Conversation(this, id); + } +} +export interface IConversations extends IGetable, IAddable, IGraphQueryableCollection { + getById(id: string): IConversation; +} +export interface _Conversations extends IGetable, IAddable { } +export const Conversations = graphInvokableFactory(_Conversations); + +/** + * Conversation + */ +@updateable() +@deleteable() +export class _Conversation extends _GraphQueryableInstance { + + /** + * Get all the threads in a group conversation. + */ + public get threads(): IThreads { + return Threads(this); + } +} +export interface IConversation extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { } +export interface _Conversation extends IGetable, IUpdateable, IDeleteable { } +export const Conversation = graphInvokableFactory(_Conversation); + +/** + * Threads + */ +@defaultPath("threads") +@addable() +export class _Threads extends _GraphQueryableCollection implements IThreads { + + /** + * Gets a thread from this collection by id + * + * @param id Group member's id + */ + public getById(id: string): IThread { + return Thread(this, id); + } +} +export interface IThreads extends IGetable, IAddable, IGraphQueryableCollection { + getById(id: string): IThread; +} +export interface _Threads extends IGetable, IAddable { } +export const Threads = graphInvokableFactory(_Threads); + +/** + * Thread + */ +@deleteable() +export class _Thread extends _GraphQueryableInstance { + + /** + * Get all the threads in a group conversation. + */ + public get posts(): IPosts { + return Posts(this); + } + + /** + * Reply to a thread in a group conversation and add a new post to it + * + * @param post Contents of the post + */ + public reply(post: IPostType): Promise { + return graphPost(this.clone(Thread, "reply"), body(post)); + } +} +export interface IThread extends IGetable, IDeleteable, IGraphQueryableInstance { + readonly posts: IPosts; + reply(post: IPostType): Promise; +} +export interface _Thread extends IGetable, IDeleteable { } +export const Thread = graphInvokableFactory(_Thread); + +/** + * Post + */ +@deleteable() +export class _Post extends _GraphQueryableInstance implements IPost { + /** + * Forward a post to a recipient + */ + public forward(info: IPostForwardInfo): Promise { + return graphPost(this.clone(Post, "forward"), body(info)); + } + + /** + * Reply to a thread in a group conversation and add a new post to it + * + * @param post Contents of the post + */ + public reply(post: IPostType): Promise { + return graphPost(this.clone(Post, "reply"), body(post)); + } +} +export interface IPost extends IGetable, IDeleteable, IGraphQueryableInstance { + forward(info: IPostForwardInfo): Promise; + reply(post: IPostType): Promise; +} +export interface _Post extends IGetable, IDeleteable { } +export const Post = graphInvokableFactory(_Post); + +/** + * Posts + */ +@defaultPath("posts") +@addable() +@getById(Post) +export class _Posts extends _GraphQueryableCollection implements IPosts { } +export interface IPosts extends IGetable, IGetById, IAddable, IGraphQueryableCollection { } +export interface _Posts extends IGetable, IGetById, IAddable { } +export const Posts = graphInvokableFactory(_Posts); + +/** + * Senders + */ +export class _Senders extends _GraphQueryableCollection { + + /** + * Add a new user or group to this senders collection + * @param id The full @odata.id value to add (ex: https://graph.microsoft.com/v1.0/users/user@contoso.com) + */ + public add(id: string): Promise { + return graphPost(this.clone(Senders, "$ref"), body({ "@odata.id": id })); + } + + /** + * Removes the entity from the collection + * + * @param id The full @odata.id value to remove (ex: https://graph.microsoft.com/v1.0/users/user@contoso.com) + */ + public remove(id: string): Promise { + const remover = this.clone(Senders, "$ref"); + remover.query.set("$id", id); + return graphDelete(remover); + } +} +export interface ISenders extends IGetable, IGraphQueryableCollection { + add(id: string): Promise; + remove(id: string): Promise; +} +export interface _Senders extends IGetable { } +export const Senders = graphInvokableFactory(_Senders); + +/** + * Information used to forward a post + */ +export interface IPostForwardInfo { + comment?: string; + toRecipients: IRecipientType[]; +} diff --git a/packages/graph/src/decorators.ts b/packages/graph/src/decorators.ts new file mode 100644 index 000000000..660cf7e9b --- /dev/null +++ b/packages/graph/src/decorators.ts @@ -0,0 +1,110 @@ +import { IGraphQueryable } from "./graphqueryable"; +import { graphDelete, graphPatch, graphPost } from "./operations"; +import { body } from "@pnp/odata"; +import { TypedHash } from "@pnp/common"; + +/** + * Decorator used to specify the default path for Queryable objects + * + * @param path + */ +export function defaultPath(path: string) { + + return function (target: T) { + + return class extends target { + constructor(...args: any[]) { + super(args[0], args.length > 1 && args[1] !== undefined ? args[1] : path); + } + }; + }; +} + +/** + * Adds the delete method to the tagged class + */ +export function deleteable() { + return function (target: T) { + + return class extends target { + public delete(this: IGraphQueryable): Promise { + return graphDelete(this); + } + }; + }; +} + +export interface IDeleteable { + /** + * Delete this instance + */ + delete(): Promise; +} + +/** + * Adds the update method to the tagged class + */ +export function updateable() { + return function (target: T) { + + return class extends target { + public update(this: IGraphQueryable, props: TypedHash): Promise { + return graphPatch(this, body(props)); + } + }; + }; +} + +export interface IUpdateable> { + /** + * Update the properties of an event object + * + * @param props Set of properties to update + */ + update(props: T): Promise; +} + +/** + * Adds the add method to the tagged class + */ +export function addable() { + return function (target: T) { + + return class extends target { + public add(this: IGraphQueryable, props: any): Promise { + return graphPost(this, body(props)); + } + }; + }; +} + +export interface IAddable, R = { id: string }> { + /** + * Adds a new item to this collection + * + * @param props properties used to create the new thread + */ + add(props: T): Promise; +} + +/** + * Adds the getById method to a collection + */ +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); + } + }; + }; +} +export interface IGetById { + /** + * Adds a new item to this collection + * + * @param props properties used to create the new thread + */ + getById(id: T): R; +} diff --git a/packages/graph/src/directory-objects/index.ts b/packages/graph/src/directory-objects/index.ts new file mode 100644 index 000000000..de2406f22 --- /dev/null +++ b/packages/graph/src/directory-objects/index.ts @@ -0,0 +1,24 @@ +import { GraphRest } from "../rest"; +import { IDirectoryObjects, DirectoryObjects } from "./types"; + +export { + IDirectoryObject, + DirectoryObjectTypes, + DirectoryObject, + DirectoryObjects, + IDirectoryObjects, +} from "./types"; + +declare module "../rest" { + interface GraphRest { + readonly directoryObjects: IDirectoryObjects; + } +} + +Reflect.defineProperty(GraphRest.prototype, "directoryObjects", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return DirectoryObjects(this); + }, +}); diff --git a/packages/graph/src/directory-objects/types.ts b/packages/graph/src/directory-objects/types.ts new file mode 100644 index 000000000..15b22ee57 --- /dev/null +++ b/packages/graph/src/directory-objects/types.ts @@ -0,0 +1,92 @@ +import { _GraphQueryableCollection, IGraphQueryableCollection, IGraphQueryableInstance, _GraphQueryableInstance, graphInvokableFactory } from "../graphqueryable"; +import { DirectoryObject as IDirectoryObjectType } from "@microsoft/microsoft-graph-types"; +import { defaultPath, getById, IGetById, deleteable, IDeleteable } from "../decorators"; +import { IGetable, body } from "@pnp/odata"; +import { graphPost } from "../operations"; + +/** + * Represents a Directory Object entity + */ +@deleteable() +export class _DirectoryObject extends _GraphQueryableInstance implements IDirectoryObject { + + /** + * Returns all the groups and directory roles that the specified Directory Object is a member of. The check is transitive + * + * @param securityEnabledOnly + */ + public getMemberObjects(securityEnabledOnly = false): Promise<{ value: string[] }> { + return graphPost(this.clone(DirectoryObject, "getMemberObjects"), body({ securityEnabledOnly })); + } + + /** + * Returns all the groups that the specified Directory Object is a member of. The check is transitive + * + * @param securityEnabledOnly + */ + public getMemberGroups(securityEnabledOnly = false): Promise<{ value: string[] }> { + return graphPost(this.clone(DirectoryObject, "getMemberGroups"), body({ securityEnabledOnly })); + } + + /** + * Check for membership in a specified list of groups, and returns from that list those groups of which the specified user, group, or directory object is a member. + * This function is transitive. + * @param groupIds A collection that contains the object IDs of the groups in which to check membership. Up to 20 groups may be specified. + */ + public checkMemberGroups(groupIds: String[]): Promise<{ value: string[] }> { + return graphPost(this.clone(DirectoryObject, "checkMemberGroups"), body({ groupIds })); + } +} +export interface IDirectoryObject extends IGetable, IDeleteable, IGraphQueryableInstance { + getMemberObjects(securityEnabledOnly?: boolean): Promise<{ value: string[] }>; + getMemberGroups(securityEnabledOnly?: boolean): Promise<{ value: string[] }>; + checkMemberGroups(groupIds: String[]): Promise<{ value: string[] }>; + } +export interface _DirectoryObject extends IGetable, IDeleteable { } +export const DirectoryObject = graphInvokableFactory(_DirectoryObject); + +/** + * Describes a collection of Directory Objects + * + */ +@defaultPath("directoryObjects") +@getById(DirectoryObject) +export class _DirectoryObjects extends _GraphQueryableCollection implements IDirectoryObjects { + public getByIds(ids: string[], type: DirectoryObjectTypes = DirectoryObjectTypes.directoryObject): Promise { + return graphPost(this.clone(DirectoryObjects, "getByIds"), body({ ids, type })); + } +} +export interface IDirectoryObjects extends IGetable, IGetById, IGraphQueryableCollection { + /** + * Returns the directory objects specified in a list of ids. NOTE: The directory objects returned are the full objects containing all their properties. + * The $select query option is not available for this operation. + * + * @param ids A collection of ids for which to return objects. You can specify up to 1000 ids. + * @param type A collection of resource types that specifies the set of resource collections to search. Default is directoryObject. + */ + getByIds(ids: string[], type?: DirectoryObjectTypes): Promise; +} +export interface _DirectoryObjects extends IGetable, IGetById { } +export const DirectoryObjects = graphInvokableFactory(_DirectoryObjects); + +/** + * DirectoryObjectTypes + */ +export enum DirectoryObjectTypes { + /** + * Directory Objects + */ + directoryObject, + /** + * User + */ + user, + /** + * Group + */ + group, + /** + * Device + */ + device, +} diff --git a/packages/graph/src/graph.ts b/packages/graph/src/graph.ts new file mode 100644 index 000000000..9514b2155 --- /dev/null +++ b/packages/graph/src/graph.ts @@ -0,0 +1,24 @@ +export { graph, GraphRest } from "./rest"; + +export { + GraphBatch, +} from "./batch"; + +export { + IGraphQueryableCollection, + IGraphQueryableInstance, + IGraphQueryableSearchableCollection, + GraphQueryable, + IGraphQueryable, + GraphQueryableCollection, + GraphQueryableInstance, + IGraphQueryableConstructor, + GraphQueryableSearchableCollection, +} from "./graphqueryable"; + +export { + GraphConfiguration, + GraphConfigurationPart, +} from "./config/graphlibconfig"; + +export * from "./types"; diff --git a/packages/graph/src/graphqueryable.ts b/packages/graph/src/graphqueryable.ts new file mode 100644 index 000000000..3509180df --- /dev/null +++ b/packages/graph/src/graphqueryable.ts @@ -0,0 +1,294 @@ +import { combine, isUrlAbsolute, IFetchOptions } from "@pnp/common"; +import { Queryable, invokableFactory, IGetable, IQueryable } from "@pnp/odata"; +import { GraphEndpoints } from "./types"; +import { graphGet } from "./operations"; + +export interface IGraphQueryableConstructor { + new(baseUrl: string | IGraphQueryable, path?: string): T; +} + +export const graphInvokableFactory = (f: IGraphQueryableConstructor) => (baseUrl: string | IGraphQueryable, path?: string): T => { + return invokableFactory(f)(baseUrl, path); +}; + +/** + * Queryable Base Class + * + */ +export class _GraphQueryable extends Queryable implements IGraphQueryable { + + /** + * Creates a new instance of the Queryable class + * + * @constructor + * @param baseUrl A string or Queryable that should form the base part of the url + * + */ + constructor(baseUrl: string | IGraphQueryable, path?: string) { + + let url = ""; + let parentUrl = ""; + const query = new Map(); + + if (typeof baseUrl === "string") { + parentUrl = baseUrl; + url = combine(parentUrl, path); + } else { + parentUrl = baseUrl.toUrl(); + url = combine(parentUrl, path); + } + + super({ + parentUrl, + query, + url, + }); + + // post init actions + if (typeof baseUrl !== "string") { + this.configureFrom(baseUrl); + } + } + + /** + * Choose which fields to return + * + * @param selects One or more fields to return + */ + public select(...selects: string[]): this { + if (selects.length > 0) { + this.query.set("$select", selects.join(",")); + } + return this; + } + + /** + * Expands fields such as lookups to get additional data + * + * @param expands The Fields for which to expand the values + */ + public expand(...expands: string[]): this { + if (expands.length > 0) { + this.query.set("$expand", expands.join(",")); + } + return this; + } + + public defaultAction(options?: IFetchOptions): Promise { + return graphGet(this, options); + } + + /** + * Gets the full url with query information + * + */ + public toUrlAndQuery(): string { + + let url = this.toUrl(); + + if (!isUrlAbsolute(url)) { + url = combine("https://graph.microsoft.com", url); + } + + if (this.query.size > 0) { + const char = url.indexOf("?") > -1 ? "&" : "?"; + url += `${char}${Array.from(this.query).map((v: [string, string]) => v[0] + "=" + v[1]).join("&")}`; + } + + return url; + } + + /** + * Gets a parent for this instance as specified + * + * @param factory The contructor for the class to create + */ + protected getParent( + factory: IGraphQueryableConstructor, + baseUrl: string | IGraphQueryable = this.parentUrl, + path?: string): T { + + return new factory(baseUrl, path); + } + + /** + * Clones this queryable into a new queryable instance of T + * @param factory Constructor used to create the new instance + * @param additionalPath Any additional path to include in the clone + * @param includeBatch If true this instance's batch will be added to the cloned instance + */ + protected clone(factory: (...args: any[]) => T, additionalPath?: string, includeBatch = true): T { + + return super.cloneTo(factory(this, additionalPath), { includeBatch }); + } + + protected setEndpoint(endpoint: string): this { + this.data.url = GraphEndpoints.ensure(this.data.url, endpoint); + return this; + } +} + +export interface IGraphQueryable extends IGetable, IQueryable { + + /** + * Choose which fields to return + * + * @param selects One or more fields to return + */ + select(...selects: string[]): this; + + /** + * Expands fields such as lookups to get additional data + * + * @param expands The Fields for which to expand the values + */ + expand(...expands: string[]): this; + + defaultAction(options?: IFetchOptions): Promise; + + /** + * Gets the full url with query information + * + */ + toUrlAndQuery(): string; + +} +export interface _GraphQueryable extends IGetable { } +export const GraphQueryable = graphInvokableFactory(_GraphQueryable); + +/** + * Represents a REST collection which can be filtered, paged, and selected + * + */ +export class _GraphQueryableCollection extends _GraphQueryable implements IGraphQueryableCollection { + + /** + * + * @param filter The string representing the filter query + */ + public filter(filter: string): this { + this.query.set("$filter", filter); + return this; + } + + /** + * Orders based on the supplied fields + * + * @param orderby The name of the field on which to sort + * @param ascending If false DESC is appended, otherwise ASC (default) + */ + public orderBy(orderBy: string, ascending = true): this { + const o = "$orderby"; + const query = this.query.has(o) ? this.query.get(o).split(",") : []; + query.push(`${orderBy} ${ascending ? "asc" : "desc"}`); + this.query.set(o, query.join(",")); + return this; + } + + /** + * Limits the query to only return the specified number of items + * + * @param top The query row limit + */ + public top(top: number): this { + this.query.set("$top", top.toString()); + return this; + } + + /** + * Skips a set number of items in the return set + * + * @param num Number of items to skip + */ + public skip(num: number): this { + this.query.set("$skip", num.toString()); + return this; + } + + /** + * To request second and subsequent pages of Graph data + */ + public skipToken(token: string): this { + this.query.set("$skiptoken", token); + return this; + } + + /** + * Retrieves the total count of matching resources + */ + public get count(): this { + this.query.set("$count", "true"); + return this; + } +} + +export interface IGraphQueryableCollection extends IGetable, IGraphQueryable { + + /** + * Retrieves the total count of matching resources + */ + count: this; + + /** + * + * @param filter The string representing the filter query + */ + filter(filter: string): this; + + /** + * Orders based on the supplied fields + * + * @param orderby The name of the field on which to sort + * @param ascending If false DESC is appended, otherwise ASC (default) + */ + orderBy(orderBy: string, ascending?: boolean): this; + + /** + * Limits the query to only return the specified number of items + * + * @param top The query row limit + */ + top(top: number): this; + + /** + * Skips a set number of items in the return set + * + * @param num Number of items to skip + */ + skip(num: number): this; + + /** + * To request second and subsequent pages of Graph data + */ + skipToken(token: string): this; +} +export interface _GraphQueryableCollection extends IGetable { } +export const GraphQueryableCollection = graphInvokableFactory(_GraphQueryableCollection); + +export class _GraphQueryableSearchableCollection extends _GraphQueryableCollection implements IGraphQueryableSearchableCollection { + + /** + * To request second and subsequent pages of Graph data + */ + public search(query: string): this { + this.query.set("$search", query); + return this; + } +} + +export interface IGraphQueryableSearchableCollection extends IGetable, IGraphQueryable { + search(query: string): this; +} +export interface _GraphQueryableSearchableCollection extends IGetable { } +export const GraphQueryableSearchableCollection = graphInvokableFactory(_GraphQueryableSearchableCollection); + + +/** + * Represents an instance that can be selected + * + */ +export class _GraphQueryableInstance extends _GraphQueryable { } + +export interface IGraphQueryableInstance extends IGetable, IGraphQueryable { } +export interface _GraphQueryableInstance extends IGetable { } +export const GraphQueryableInstance = graphInvokableFactory(_GraphQueryableInstance); diff --git a/packages/graph/src/groups/index.ts b/packages/graph/src/groups/index.ts new file mode 100644 index 000000000..014df0b07 --- /dev/null +++ b/packages/graph/src/groups/index.ts @@ -0,0 +1,25 @@ +import { GraphRest } from "../rest"; +import { IGroups, Groups } from "./types"; + +export { + Group, + GroupType, + Groups, + IGroup, + IGroupAddResult, + IGroups, +} from "./types"; + +declare module "../rest" { + interface GraphRest { + readonly groups: IGroups; + } +} + +Reflect.defineProperty(GraphRest.prototype, "groups", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return Groups(this); + }, +}); diff --git a/packages/graph/src/groups/types.ts b/packages/graph/src/groups/types.ts new file mode 100644 index 000000000..9f0f71495 --- /dev/null +++ b/packages/graph/src/groups/types.ts @@ -0,0 +1,149 @@ +import { extend, TypedHash } from "@pnp/common"; +import { Event as IEventType, Group as IGroupType } from "@microsoft/microsoft-graph-types"; +import { body, IGetable } from "@pnp/odata"; +import { _GraphQueryableInstance, _GraphQueryableCollection, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { defaultPath, deleteable, IDeleteable, updateable, IUpdateable, getById, IGetById } from "../decorators"; +import { graphPost } from "../operations"; +import { _DirectoryObject, IDirectoryObject, _DirectoryObjects } from "../directory-objects/types"; + +export enum GroupType { + /** + * Office 365 (aka unified group) + */ + Office365, + /** + * Dynamic membership + */ + Dynamic, + /** + * Security + */ + Security, +} + +/** + * Represents a group entity + */ +@deleteable() +@updateable() +export class _Group extends _DirectoryObject implements IGroup { + + public addFavorite(): Promise { + return graphPost(this.clone(Group, "addFavorite")); + } + + public removeFavorite(): Promise { + return graphPost(this.clone(Group, "removeFavorite")); + } + + public resetUnseenCount(): Promise { + return graphPost(this.clone(Group, "resetUnseenCount")); + } + + public subscribeByMail(): Promise { + return graphPost(this.clone(Group, "subscribeByMail")); + } + + public unsubscribeByMail(): Promise { + return graphPost(this.clone(Group, "unsubscribeByMail")); + } + + public getCalendarView(start: Date, end: Date): Promise { + + const view = this.clone(Group, "calendarView"); + view.query.set("startDateTime", start.toISOString()); + view.query.set("endDateTime", end.toISOString()); + return view(); + } +} +export interface IGroup extends IGetable, IDeleteable, IUpdateable, IDirectoryObject { + /** + * Add the group to the list of the current user's favorite groups. Supported for only Office 365 groups + */ + addFavorite(): Promise; + + /** + * Remove the group from the list of the current user's favorite groups. Supported for only Office 365 groups + */ + removeFavorite(): Promise; + + /** + * Reset the unseenCount of all the posts that the current user has not seen since their last visit + */ + resetUnseenCount(): Promise; + + /** + * Calling this method will enable the current user to receive email notifications for this group, + * about new posts, events, and files in that group. Supported for only Office 365 groups + */ + subscribeByMail(): Promise; + + /** + * Calling this method will prevent the current user from receiving email notifications for this group + * about new posts, events, and files in that group. Supported for only Office 365 groups + */ + unsubscribeByMail(): Promise; + + /** + * Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group + * + * @param start Start date and time of the time range + * @param end End date and time of the time range + */ + getCalendarView(start: Date, end: Date): Promise; +} +export interface _Group extends IGetable, IDeleteable, IUpdateable { } +export const Group = graphInvokableFactory(_Group); + +/** + * Describes a collection of Field objects + * + */ +@defaultPath("groups") +@getById(Group) +export class _Groups extends _GraphQueryableCollection implements IGroups { + + /** + * Create a new group as specified in the request body. + * + * @param name Name to display in the address book for the group + * @param mailNickname Mail alias for the group + * @param groupType Type of group being created + * @param additionalProperties A plain object collection of additional properties you want to set on the new group + */ + public async add(name: string, mailNickname: string, groupType: GroupType, additionalProperties: TypedHash = {}): Promise { + + let postBody = extend({ + displayName: name, + mailEnabled: groupType === GroupType.Office365, + mailNickname: mailNickname, + securityEnabled: groupType !== GroupType.Office365, + }, additionalProperties); + + // include a group type if required + if (groupType !== GroupType.Security) { + + postBody = extend(postBody, { + groupTypes: groupType === GroupType.Office365 ? ["Unified"] : ["DynamicMembership"], + }); + } + + const data = await graphPost(this, body(postBody)); + + return { + data, + group: this.getById(data.id), + }; + } +} +export interface IGroups extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _Groups extends IGetable, IGetById { } +export const Groups = graphInvokableFactory(_Groups); + +/** + * IGroupAddResult + */ +export interface IGroupAddResult { + group: IGroup; + data: any; +} diff --git a/packages/graph/src/invitations/index.ts b/packages/graph/src/invitations/index.ts new file mode 100644 index 000000000..94316074c --- /dev/null +++ b/packages/graph/src/invitations/index.ts @@ -0,0 +1,22 @@ +import { GraphRest } from "../rest"; +import { IInvitations, Invitations } from "./types"; + +export { + IInvitationAddResult, + IInvitations, + Invitations, +} from "./types"; + +declare module "../rest" { + interface GraphRest { + readonly invitations: IInvitations; + } +} + +Reflect.defineProperty(GraphRest.prototype, "invitations", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return Invitations(this); + }, +}); diff --git a/packages/graph/src/invitations/types.ts b/packages/graph/src/invitations/types.ts new file mode 100644 index 000000000..228290e66 --- /dev/null +++ b/packages/graph/src/invitations/types.ts @@ -0,0 +1,41 @@ +import { TypedHash, extend } from "@pnp/common"; +import { body, IGetable } from "@pnp/odata"; +import { Invitation as IInvitationType } from "@microsoft/microsoft-graph-types"; +import { _GraphQueryableCollection, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { defaultPath } from "../decorators"; +import { graphPost } from "../operations"; + +/** + * Invitations + */ +@defaultPath("invitations") +export class _Invitations extends _GraphQueryableCollection implements IInvitations { + + /** + * Create a new Invitation via invitation manager. + * + * @param invitedUserEmailAddress The email address of the user being invited. + * @param inviteRedirectUrl The URL user should be redirected to once the invitation is redeemed. + * @param additionalProperties A plain object collection of additional properties you want to set in the invitation + */ + public async create(invitedUserEmailAddress: string, inviteRedirectUrl: string, additionalProperties: TypedHash = {}): Promise { + + const postBody = extend({ inviteRedirectUrl, invitedUserEmailAddress }, additionalProperties); + + const data = await graphPost(this, body(postBody)); + + return { data }; + } +} +export interface IInvitations extends IGetable, IGraphQueryableCollection { + create(invitedUserEmailAddress: string, inviteRedirectUrl: string, additionalProperties: TypedHash): Promise; +} +export interface _Invitations extends IGetable { } +export const Invitations = graphInvokableFactory(_Invitations); + +/** + * IInvitationAddResult + */ +export interface IInvitationAddResult { + data: IInvitationType; +} diff --git a/packages/graph/src/members/groups.ts b/packages/graph/src/members/groups.ts new file mode 100644 index 000000000..23af265c8 --- /dev/null +++ b/packages/graph/src/members/groups.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Group } from "../groups/types"; +import { IMembers, Members } from "./types"; + +declare module "../groups/types" { + interface _Group { + readonly owners: IMembers; + readonly members: IMembers; + } + interface IGroup { + readonly owners: IMembers; + readonly members: IMembers; + } +} + +addProp(_Group, "owners", Members, "owners"); +addProp(_Group, "members", Members); diff --git a/packages/graph/src/members/index.ts b/packages/graph/src/members/index.ts new file mode 100644 index 000000000..6e86c5b24 --- /dev/null +++ b/packages/graph/src/members/index.ts @@ -0,0 +1,8 @@ +import "./groups"; + +export { + IMember, + IMembers, + Member, + Members, +} from "./types"; diff --git a/packages/graph/src/members/types.ts b/packages/graph/src/members/types.ts new file mode 100644 index 000000000..394beb9d8 --- /dev/null +++ b/packages/graph/src/members/types.ts @@ -0,0 +1,42 @@ +import { IGetable, body } from "@pnp/odata"; +import { User as IMemberType } from "@microsoft/microsoft-graph-types"; +import { _GraphQueryableCollection, IGraphQueryableInstance, _GraphQueryableInstance, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { defaultPath, getById, IGetById } from "../decorators"; +import { graphDelete, graphPost } from "../operations"; + +/** + * Member + */ +export class _Member extends _GraphQueryableInstance implements IMember { + /** + * Removes this Member + */ + public remove(): Promise { + return graphDelete(this.clone(Member, "$ref")); + } +} +export interface IMember extends IGetable, IGraphQueryableInstance { } +export interface _Member extends IGetable { } +export const Member = graphInvokableFactory(_Member); + +/** + * Members + */ +@defaultPath("members") +@getById(Member) +export class _Members extends _GraphQueryableCollection implements IMembers { + + /** + * Use this API to add a member to an Office 365 group, a security group or a mail-enabled security group through + * the members navigation property. You can add users or other groups. + * Important: You can add only users to Office 365 groups. + * + * @param id Full @odata.id of the directoryObject, user, or group object you want to add (ex: https://graph.microsoft.com/v1.0/directoryObjects/${id}) + */ + public add(id: string): Promise { + return graphPost(this.clone(Members, "$ref"), body({ "@odata.id": id })); + } +} +export interface IMembers extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _Members extends IGetable, IGetById { } +export const Members = graphInvokableFactory(_Members); diff --git a/packages/graph/src/messages/index.ts b/packages/graph/src/messages/index.ts new file mode 100644 index 000000000..85b09d55e --- /dev/null +++ b/packages/graph/src/messages/index.ts @@ -0,0 +1,14 @@ +import "./users"; + +export { + IMailFolder, + IMessage, + IMailFolders, + IMailboxSettings, + IMessages, + MailFolder, + MailFolders, + MailboxSettings, + Message, + Messages, +} from "./types"; diff --git a/packages/graph/src/messages/types.ts b/packages/graph/src/messages/types.ts new file mode 100644 index 000000000..93dc93884 --- /dev/null +++ b/packages/graph/src/messages/types.ts @@ -0,0 +1,52 @@ +import { Message as IMessageType, MailFolder as IMailFolderType, MailboxSettings as IMailboxSettingsType } from "@microsoft/microsoft-graph-types"; +import { IGetable } from "@pnp/odata"; +import { _GraphQueryableCollection, _GraphQueryableInstance, IGraphQueryableInstance, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { defaultPath, getById, addable, IGetById, IAddable, updateable, IUpdateable } from "../decorators"; + +/** + * Message + */ +export class _Message extends _GraphQueryableInstance implements IMessage { } +export interface IMessage extends IGetable, IGraphQueryableInstance { } +export interface _Message extends IGetable { } +export const Message = graphInvokableFactory(_Message); + +/** + * Messages + */ +@defaultPath("messages") +@getById(Message) +@addable() +export class _Messages extends _GraphQueryableCollection implements IMessages { } +export interface IMessages extends IGetable, IGetById, IAddable, IGraphQueryableInstance { } +export interface _Messages extends IGetable, IGetById, IAddable { } +export const Messages = graphInvokableFactory(_Messages); + +/** + * MailFolder + */ +export class _MailFolder extends _GraphQueryableInstance implements IMailFolder { } +export interface IMailFolder extends IGetable, IGraphQueryableInstance { } +export interface _MailFolder extends IGetable { } +export const MailFolder = graphInvokableFactory(_MailFolder); + +/** + * MailFolders + */ +@defaultPath("mailFolders") +@getById(MailFolder) +@addable() +export class _MailFolders extends _GraphQueryableCollection implements IMailFolders {} +export interface IMailFolders extends IGetable, IGetById, IAddable, IGraphQueryableCollection { } +export interface _MailFolders extends IGetable, IGetById, IAddable { } +export const MailFolders = graphInvokableFactory(_MailFolders); + +/** + * MailboxSettings + */ +@defaultPath("mailboxSettings") +@updateable() +export class _MailboxSettings extends _GraphQueryableInstance implements IMailboxSettings {} +export interface IMailboxSettings extends IGetable, IUpdateable, IGraphQueryableInstance { } +export interface _MailboxSettings extends IGetable, IUpdateable { } +export const MailboxSettings = graphInvokableFactory(_MailboxSettings); diff --git a/packages/graph/src/messages/users.ts b/packages/graph/src/messages/users.ts new file mode 100644 index 000000000..8179aa548 --- /dev/null +++ b/packages/graph/src/messages/users.ts @@ -0,0 +1,27 @@ +import { addProp, body } from "@pnp/odata"; +import { _User, User } from "../users/types"; +import { IMessages, Messages, IMailboxSettings, MailboxSettings, IMailFolders, MailFolders, IMessage } from "./types"; +import { graphPost } from "../operations"; + +declare module "../users/types" { + interface _User { + readonly messages: IMessages; + readonly mailboxSettings: IMailboxSettings; + readonly mailFolders: IMailFolders; + sendMail(message: IMessage): Promise; + } + interface IUser { + readonly messages: IMessages; + readonly mailboxSettings: IMailboxSettings; + readonly mailFolders: IMailFolders; + sendMail(message: IMessage): Promise; + } +} + +addProp(_User, "messages", Messages); +addProp(_User, "mailboxSettings", MailboxSettings); +addProp(_User, "mailFolders", MailFolders); + +_User.prototype.sendMail = function (this: _User, message: IMessage): Promise { + return graphPost(this.clone(User, "sendMail"), body(message)); +}; diff --git a/packages/graph/src/net/graphhttpclient.ts b/packages/graph/src/net/graphhttpclient.ts new file mode 100644 index 000000000..72b80e21d --- /dev/null +++ b/packages/graph/src/net/graphhttpclient.ts @@ -0,0 +1,114 @@ +import { + extend, + IRequestClient, + mergeHeaders, + IFetchOptions, + IHttpClientImpl, + getCtxCallback, +} from "@pnp/common"; +import { GraphRuntimeConfig } from "../config/graphlibconfig"; + +export class GraphHttpClient implements IRequestClient { + + private _impl: IHttpClientImpl; + + constructor() { + + this._impl = GraphRuntimeConfig.fetchClientFactory(); + } + + public fetch(url: string, options: IFetchOptions = {}): Promise { + + const headers = new Headers(); + + // first we add the global headers so they can be overwritten by any passed in locally to this call + mergeHeaders(headers, GraphRuntimeConfig.headers); + + // second we add the local options so we can overwrite the globals + mergeHeaders(headers, options.headers); + + if (!headers.has("Content-Type")) { + headers.append("Content-Type", "application/json"); + } + + const opts = extend(options, { headers: headers }); + + return this.fetchRaw(url, opts); + } + + public fetchRaw(url: string, options: IFetchOptions = {}): Promise { + + // here we need to normalize the headers + const rawHeaders = new Headers(); + mergeHeaders(rawHeaders, options.headers); + options = extend(options, { headers: rawHeaders }); + + const retry = (ctx: RetryContext): void => { + + this._impl.fetch(url, options).then((response) => ctx.resolve(response)).catch((response) => { + + // Check if request was throttled - http status code 429 + // Check if request failed due to server unavailable - http status code 503 + if (response.status !== 429 && response.status !== 503) { + ctx.reject(response); + } + + // grab our current delay + const delay = ctx.delay; + + // Increment our counters. + ctx.delay *= 2; + ctx.attempts++; + + // If we have exceeded the retry count, reject. + if (ctx.retryCount <= ctx.attempts) { + ctx.reject(response); + } + + // Set our retry timeout for {delay} milliseconds. + setTimeout(getCtxCallback(this, retry, ctx), delay); + }); + }; + + return new Promise((resolve, reject) => { + + const retryContext: RetryContext = { + attempts: 0, + delay: 100, + reject: reject, + resolve: resolve, + retryCount: 7, + }; + + retry.call(this, retryContext); + }); + } + + public get(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "GET" }); + return this.fetch(url, opts); + } + + public post(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "POST" }); + return this.fetch(url, opts); + } + + public patch(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "PATCH" }); + return this.fetch(url, opts); + } + + public delete(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "DELETE" }); + return this.fetch(url, opts); + } +} + +interface RetryContext { + attempts: number; + delay: number; + reject: (reason?: any) => void; + resolve: (value?: Response | PromiseLike) => void; + retryCount: number; +} diff --git a/packages/graph/src/onedrive/index.ts b/packages/graph/src/onedrive/index.ts new file mode 100644 index 000000000..54ca3f1be --- /dev/null +++ b/packages/graph/src/onedrive/index.ts @@ -0,0 +1,16 @@ +import "./users"; + +export { + Drive, + DriveItem, + DriveItems, + Drives, + IDrive, + IDriveItem, + IDriveItemAddResult, + IDriveItemVersionInfo, + IDriveItems, + IDrives, + IRoot, + Root, +} from "./types"; diff --git a/packages/graph/src/onedrive/types.ts b/packages/graph/src/onedrive/types.ts new file mode 100644 index 000000000..829d246eb --- /dev/null +++ b/packages/graph/src/onedrive/types.ts @@ -0,0 +1,164 @@ +import { + GraphQueryableInstance, + GraphQueryableCollection, + _GraphQueryableInstance, + IGraphQueryableInstance, + IGraphQueryableCollection, + _GraphQueryableCollection, + graphInvokableFactory, +} from "../graphqueryable"; +import { Drive as IDriveType } from "@microsoft/microsoft-graph-types"; +import { extend, combine } from "@pnp/common"; +import { defaultPath, getById, IGetById, deleteable, IDeleteable, updateable, IUpdateable } from "../decorators"; +import { IGetable, body } from "@pnp/odata"; +import { graphPatch, graphGet, graphPut } from "../operations"; + +/** + * Describes a Drive instance + * + */ +@defaultPath("drive") +export class _Drive extends _GraphQueryableInstance implements IDrive { + + public get root(): IRoot { + return Root(this); + } + + public get list(): IGraphQueryableInstance { + return GraphQueryableInstance(this, "list"); + } + + public get recent(): IDriveItems { + return DriveItems(this, "recent"); + } + + public get sharedWithMe(): IDriveItems { + return DriveItems(this, "sharedWithMe"); + } + + public getItemById(id: string): IDriveItem { + return DriveItem(this, combine("items", id)); + } +} +export interface IDrive extends IGetable, IGraphQueryableInstance { + readonly root: IRoot; + readonly list: IGraphQueryableInstance; + readonly recent: IDriveItems; + readonly sharedWithMe: IDriveItems; +} +export interface _Drive extends IGetable { } +export const Drive = graphInvokableFactory(_Drive); + +/** + * Describes a collection of Drive objects + * + */ +@defaultPath("drives") +@getById(Drive) +export class _Drives extends _GraphQueryableCollection implements IDrives { } +export interface IDrives extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _Drives extends IGetable, IGetById { } +export const Drives = graphInvokableFactory(_Drives); + +/** + * Describes a Root instance + * + */ +@defaultPath("root") +export class _Root extends _GraphQueryableInstance implements IRoot { + + public get children(): IDriveItems { + return DriveItems(this, "children"); + } + + public search(query: string): Promise { + const searcher = this.clone(Root); + searcher.query.set("search", `'${query}'`); + return searcher(); + } + + public get thumbnails(): IGraphQueryableCollection { + return GraphQueryableCollection(this, "thumbnails"); + } +} +export interface IRoot extends IGetable, IGraphQueryableInstance { + readonly children: IDriveItems; + readonly thumbnails: IGraphQueryableCollection; + search(query: string): Promise; +} +export interface _Root extends IGetable { } +export const Root = graphInvokableFactory(_Root); + +/** + * Describes a Drive Item instance + * + */ +@deleteable() +@updateable() +export class _DriveItem extends _GraphQueryableInstance implements IDriveItem { + + public get children(): IDriveItems { + return DriveItems(this, "children"); + } + + public get thumbnails(): IGraphQueryableCollection { + return GraphQueryableCollection(this, "thumbnails"); + } + + public get versions(): IGraphQueryableCollection { + return GraphQueryableCollection(this, "versions"); + } + + public move(parentReference: { id: "string" }, name: string): Promise { + return graphPatch(this, body(extend(parentReference, { name }))); + } + + public getContent(): Promise { + return graphGet(this.clone(DriveItem, "content")); + } + + public setContent(content: any): Promise<{ id: string, name: string, size: number }> { + return graphPut(this.clone(DriveItem, "content"), { + body: content, + }); + } +} +export interface IDriveItem extends IGetable, IDeleteable, IUpdateable, IGraphQueryableInstance { + readonly children: IDriveItems; + readonly thumbnails: IGraphQueryableCollection; + readonly versions: IGraphQueryableCollection; + move(parentReference: { id: "string" }, name: string): Promise; + getContent(): Promise; +} +export interface _DriveItem extends IGetable, IDeleteable, IUpdateable { } +export const DriveItem = graphInvokableFactory(_DriveItem); + +/** + * Describes a collection of Drive Item objects + * + */ +@getById(DriveItem) +export class _DriveItems extends _GraphQueryableCollection implements IDriveItems { } +export interface IDriveItems extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _DriveItems extends IGetable, IGetById { } +export const DriveItems = graphInvokableFactory(_DriveItems); + +/** + * IDriveItemAddResult + */ +export interface IDriveItemAddResult { + data: any; + driveItem: IDriveItem; +} + +export interface IDriveItemVersionInfo { + id: string; + lastModifiedBy: { + user: { + id: string; + displayName: string; + }, + }; + lastModifiedDateTime: string; + size: number; +} diff --git a/packages/graph/src/onedrive/users.ts b/packages/graph/src/onedrive/users.ts new file mode 100644 index 000000000..3cbcb6e90 --- /dev/null +++ b/packages/graph/src/onedrive/users.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _User } from "../users/types"; +import { IDrive, Drive, IDrives, Drives } from "./types"; + +declare module "../users/types" { + interface _User { + readonly drive: IDrive; + readonly drives: IDrives; + } + interface IUser { + readonly drive: IDrive; + readonly drives: IDrives; + } +} + +addProp(_User, "drive", Drive); +addProp(_User, "drives", Drives); diff --git a/packages/graph/src/onenote/index.ts b/packages/graph/src/onenote/index.ts new file mode 100644 index 000000000..eb926a34d --- /dev/null +++ b/packages/graph/src/onenote/index.ts @@ -0,0 +1,16 @@ +import "./users"; + +export { + INotebook, + INotebookAddResult, + INotebooks, + IOneNote, + ISection, + ISectionAddResult, + ISections, + Notebook, + Notebooks, + OneNote, + Section, + Sections, +} from "./types"; diff --git a/packages/graph/src/onenote/types.ts b/packages/graph/src/onenote/types.ts new file mode 100644 index 000000000..17889b830 --- /dev/null +++ b/packages/graph/src/onenote/types.ts @@ -0,0 +1,135 @@ +import { IGetable, body } from "@pnp/odata"; +import { Notebook as INotebookType, Onenote as IOnenoteType, OnenoteSection as ISectionType, OnenotePage as IOnenotePageType } from "@microsoft/microsoft-graph-types"; +import { + GraphQueryableCollection, + _GraphQueryableInstance, + _GraphQueryableCollection, + IGraphQueryableCollection, + IGraphQueryableInstance, + graphInvokableFactory, +} from "../graphqueryable"; +import { defaultPath, getById, IGetById } from "../decorators"; +import { graphPost } from "../operations"; + +/** + * Represents a onenote entity + */ +@defaultPath("onenote") +export class _OneNote extends _GraphQueryableInstance implements IOneNote { + + public get notebooks(): INotebooks { + return Notebooks(this); + } + + public get sections(): ISections { + return Sections(this); + } + + public get pages(): IGraphQueryableCollection { + return GraphQueryableCollection(this, "pages"); + } +} +export interface IOneNote extends IGetable, IGraphQueryableInstance { + readonly notebooks: INotebooks; + readonly sections: ISections; + readonly pages: IGraphQueryableCollection; +} +export interface _OneNote extends IGetable { } +export const OneNote = graphInvokableFactory(_OneNote); + + +/** + * Describes a notebook instance + * + */ +export class _Notebook extends _GraphQueryableInstance implements INotebook { + public get sections(): ISections { + return Sections(this); + } +} +export interface INotebook extends IGetable, IGraphQueryableInstance { + readonly sections: ISections; +} +export interface _Notebook extends IGetable { } +export const Notebook = graphInvokableFactory(_Notebook); + +/** + * Describes a collection of Notebook objects + * + */ +@defaultPath("notebooks") +@getById(Notebook) +export class _Notebooks extends _GraphQueryableCollection implements INotebooks { + /** + * Create a new notebook as specified in the request body. + * + * @param displayName Notebook display name + */ + public async add(displayName: string): Promise { + + const data = await graphPost(this, body({ displayName })); + + return { + data, + notebook: this.getById(data.id), + }; + } +} +export interface INotebooks extends IGetable, IGetById, IGraphQueryableCollection { + add(displayName: string): Promise; +} +export interface _Notebooks extends IGetable, IGetById { } +export const Notebooks = graphInvokableFactory(_Notebooks); + + +/** + * Describes a sections instance + */ +export class _Section extends _GraphQueryableInstance implements ISection { } +export interface ISection extends IGetable, IGraphQueryableInstance { } +export interface _Section extends IGetable { } +export const Section = graphInvokableFactory(_Section); + +/** + * Describes a collection of Sections objects + * + */ +@defaultPath("sections") +@getById(Section) +export class _Sections extends _GraphQueryableCollection implements ISections { + /** + * Adds a new section + * + * @param displayName New section display name + */ + public async add(displayName: string): Promise { + + const data = await graphPost(this, body({ displayName })); + + return { + data, + section: this.getById(data.id), + }; + } +} +export interface ISections extends IGetable, IGetById, IGraphQueryableCollection { + add(displayName: string): Promise; +} +export interface _Sections extends IGetable, IGetById { } +export const Sections = graphInvokableFactory(_Sections); + +/** + * INotebookAddResult + */ +export interface INotebookAddResult { + data: any; + notebook: INotebook; +} + +/** + * ISectionAddResult + */ +export interface ISectionAddResult { + data: any; + section: ISection; +} diff --git a/packages/graph/src/onenote/users.ts b/packages/graph/src/onenote/users.ts new file mode 100644 index 000000000..2690e8527 --- /dev/null +++ b/packages/graph/src/onenote/users.ts @@ -0,0 +1,14 @@ +import { addProp } from "@pnp/odata"; +import { _User } from "../users/types"; +import { IOneNote, OneNote } from "./types"; + +declare module "../users/types" { + interface _User { + readonly onenote: IOneNote; + } + interface IUser { + readonly onenote: IOneNote; + } +} + +addProp(_User, "onenote", OneNote); diff --git a/packages/graph/src/operations.ts b/packages/graph/src/operations.ts new file mode 100644 index 000000000..55988d716 --- /dev/null +++ b/packages/graph/src/operations.ts @@ -0,0 +1,33 @@ +import { IFetchOptions, mergeOptions, objectDefinedNotNull } from "@pnp/common"; +import { defaultPipelineBinder, cloneQueryableData, IOperation } from "@pnp/odata"; +import { GraphHttpClient } from "./net/graphhttpclient"; +import { IGraphQueryable } from "./graphqueryable"; + +const graphClientBinder = defaultPipelineBinder(() => new GraphHttpClient()); + +const send = (operation: IOperation): (o: IGraphQueryable, options?: IFetchOptions) => Promise => { + + return async function (o: IGraphQueryable, options?: IFetchOptions): Promise { + + const data = cloneQueryableData(o.data); + const batchDependency = objectDefinedNotNull(data.batch) !== null ? data.batch.addDependency() : () => { return; }; + const url = o.toUrlAndQuery(); + + mergeOptions(data.options, options); + + return operation(Object.assign({}, data, { + batchDependency, + url, + })); + }; +}; + +export const graphGet = (o: IGraphQueryable, options?: IFetchOptions): Promise => send(graphClientBinder("GET"))(o, options); + +export const graphPost = (o: IGraphQueryable, options?: IFetchOptions): Promise => send(graphClientBinder("POST"))(o, options); + +export const graphDelete = (o: IGraphQueryable, options?: IFetchOptions): Promise => send(graphClientBinder("DELETE"))(o, options); + +export const graphPatch = (o: IGraphQueryable, options?: IFetchOptions): Promise => send(graphClientBinder("PATCH"))(o, options); + +export const graphPut = (o: IGraphQueryable, options?: IFetchOptions): Promise => send(graphClientBinder("PUT"))(o, options); diff --git a/packages/graph/src/photos/groups.ts b/packages/graph/src/photos/groups.ts new file mode 100644 index 000000000..b696ae27a --- /dev/null +++ b/packages/graph/src/photos/groups.ts @@ -0,0 +1,14 @@ +import { addProp } from "@pnp/odata"; +import { _Group } from "../groups/types"; +import { Photo, IPhoto } from "./types"; + +declare module "../groups/types" { + interface _Group { + readonly photo: IPhoto; + } + interface IGroup { + readonly photo: IPhoto; + } +} + +addProp(_Group, "photo", Photo); diff --git a/packages/graph/src/photos/index.ts b/packages/graph/src/photos/index.ts new file mode 100644 index 000000000..42a3d8217 --- /dev/null +++ b/packages/graph/src/photos/index.ts @@ -0,0 +1,6 @@ +import "./groups"; + +export { + IPhoto, + Photo, +} from "./types"; diff --git a/packages/graph/src/photos/types.ts b/packages/graph/src/photos/types.ts new file mode 100644 index 000000000..4fad83064 --- /dev/null +++ b/packages/graph/src/photos/types.ts @@ -0,0 +1,38 @@ +import { _GraphQueryableInstance, IGraphQueryableInstance, graphInvokableFactory } from "../graphqueryable"; +import { BlobParser, BufferParser, IGetable } from "@pnp/odata"; +import { Photo as IPhotoType } from "@microsoft/microsoft-graph-types"; +import { defaultPath } from "../decorators"; +import { graphPatch } from "../operations"; + +@defaultPath("photo") +export class _Photo extends _GraphQueryableInstance { + /** + * Gets the image bytes as a blob (browser) + */ + public getBlob(): Promise { + return this.clone(Photo, "$value", false).usingParser(new BlobParser())(); + } + + /** + * Gets the image file byets as a Buffer (node.js) + */ + public getBuffer(): Promise { + return this.clone(Photo, "$value", false).usingParser(new BufferParser())(); + } + + /** + * Sets the file bytes + * + * @param content Image file contents, max 4 MB + */ + public setContent(content: ArrayBuffer | Blob): Promise { + return graphPatch(this.clone(Photo, "$value", false), { body: content }); + } +} +export interface IPhoto extends IGetable, IGraphQueryableInstance { + getBlob(): Promise; + getBuffer(): Promise; + setContent(content: ArrayBuffer | Blob): Promise; +} +export interface _Photo extends IGetable { } +export const Photo = graphInvokableFactory(_Photo); diff --git a/packages/graph/src/planner/groups.ts b/packages/graph/src/planner/groups.ts new file mode 100644 index 000000000..308498793 --- /dev/null +++ b/packages/graph/src/planner/groups.ts @@ -0,0 +1,14 @@ +import { addProp } from "@pnp/odata"; +import { _Group } from "../groups/types"; +import { IPlans, Plans } from "./types"; + +declare module "../groups/types" { + interface _Group { + readonly plans: IPlans; + } + interface IGroup { + readonly plans: IPlans; + } +} + +addProp(_Group, "plans", Plans, "planner/plans"); diff --git a/packages/graph/src/planner/index.ts b/packages/graph/src/planner/index.ts new file mode 100644 index 000000000..f4ec91240 --- /dev/null +++ b/packages/graph/src/planner/index.ts @@ -0,0 +1,39 @@ +import { GraphRest } from "../rest"; +import { IPlanner, Planner } from "./types"; + +import "./groups"; +import "./users"; + +export { + Bucket, + Buckets, + IBucket, + IBucketAddResult, + IBuckets, + IPlan, + IPlanAddResult, + IPlanner, + IPlans, + ITask, + ITaskAddResult, + ITasks, + Plan, + Planner, + Plans, + Task, + Tasks, +} from "./types"; + +declare module "../rest" { + interface GraphRest { + readonly planner: IPlanner; + } +} + +Reflect.defineProperty(GraphRest.prototype, "planner", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return Planner(this); + }, +}); diff --git a/packages/graph/src/planner/types.ts b/packages/graph/src/planner/types.ts new file mode 100644 index 000000000..c48fd1f1b --- /dev/null +++ b/packages/graph/src/planner/types.ts @@ -0,0 +1,207 @@ +import { TypedHash, extend } from "@pnp/common"; +import { + PlannerPlan as IPlannerPlanType, + PlannerTask as IPlannerTaskType, + PlannerBucket as IPlannerBucketType, + Planner as IPlannerType, +} from "@microsoft/microsoft-graph-types"; +import { IGetable, body } from "@pnp/odata"; +import { _GraphQueryableInstance, IGraphQueryableInstance, _GraphQueryableCollection, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { updateable, IUpdateable, deleteable, IDeleteable, getById, IGetById } from "../decorators"; +import { graphPost } from "../operations"; +import { defaultPath } from "../decorators"; + +/** + * Planner + */ +@defaultPath("planner") +export class _Planner extends _GraphQueryableInstance implements IPlanner { + + // Should Only be able to get by id, or else error occur + public get plans(): IPlans { + return Plans(this); + } + + // Should Only be able to get by id, or else error occur + public get tasks(): ITasks { + return Tasks(this); + } + + // Should Only be able to get by id, or else error occur + public get buckets(): IBuckets { + return Buckets(this); + } +} +export interface IPlanner { + readonly plans: IPlans; + readonly tasks: ITasks; + readonly buckets: IBuckets; +} +export interface _Planner extends IGetable { } +export const Planner = graphInvokableFactory(_Planner); + +/** + * Plan + */ +@updateable() +@deleteable() +export class _Plan extends _GraphQueryableInstance { + + public get tasks(): ITasks { + return Tasks(this); + } + + public get buckets(): IBuckets { + return Buckets(this); + } +} +export interface IPlan extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { + readonly tasks: ITasks; + readonly buckets: IBuckets; +} +export interface _Plan extends IGetable, IUpdateable, IDeleteable { } +export const Plan = graphInvokableFactory(_Plan); + +@defaultPath("plans") +@getById(Plan) +export class _Plans extends _GraphQueryableCollection implements IPlans { + /** + * Create a new Planner Plan. + * + * @param owner Id of Group object. + * @param title The Title of the Plan. + */ + public async add(owner: string, title: string): Promise { + + const data = await graphPost(this, body({ owner, title })); + + return { + data, + plan: this.getById(data.id), + }; + } +} +export interface IPlans extends IGetable, IGetById, IGraphQueryableCollection { + add(owner: string, title: string): Promise; +} +export interface _Plans extends IGetable, IGetById { } +export const Plans = graphInvokableFactory(_Plans); + +/** + * Task + */ +@updateable() +@deleteable() +export class _Task extends _GraphQueryableInstance implements ITask { } +export interface ITask extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { } +export interface _Task extends IGetable, IUpdateable, IDeleteable { } +export const Task = graphInvokableFactory(_Task); + +/** + * Tasks + */ +@defaultPath("tasks") +@getById(Task) +export class _Tasks extends _GraphQueryableCollection implements ITasks { + /** + * Create a new Planner Task. + * + * @param planId Id of Plan. + * @param title The Title of the Task. + * @param assignments Assign the task + * @param bucketId Id of Bucket + */ + public async add(planId: string, title: string, assignments?: TypedHash, bucketId?: string): Promise { + + let postBody = extend({ + planId, + title, + }, assignments); + + if (bucketId) { + postBody = extend(postBody, { + bucketId: bucketId, + }); + } + + const data = await graphPost(this, body(postBody)); + + return { + data, + task: this.getById(data.id), + }; + } +} +export interface ITasks extends IGetable, IGetById, IGraphQueryableCollection { + add(planId: string, title: string, assignments?: TypedHash, bucketId?: string): Promise; +} +export interface _Tasks extends IGetable, IGetById { } +export const Tasks = graphInvokableFactory(_Tasks); + + +/** + * Bucket + */ +@updateable() +@deleteable() +export class _Bucket extends _GraphQueryableInstance implements IBucket { + public get tasks(): ITasks { + return Tasks(this); + } +} +export interface IBucket extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { + readonly tasks: ITasks; +} +export interface _Bucket extends IGetable, IUpdateable, IDeleteable { } +export const Bucket = graphInvokableFactory(_Bucket); + + +/** + * Buckets + */ +@defaultPath("buckets") +@getById(Bucket) +export class _Buckets extends _GraphQueryableCollection implements IBuckets { + /** + * Create a new Bucket. + * + * @param name Name of Bucket object. + * @param planId The Id of the Plan. + * @param oderHint Hint used to order items of this type in a list view. + */ + public async add(name: string, planId: string, orderHint?: string): Promise { + + const postBody = { + name: name, + orderHint: orderHint ? orderHint : "", + planId: planId, + }; + + const data = await graphPost(this, body(postBody)); + + return { + bucket: this.getById(data.id), + data, + }; + } +} +export interface IBuckets extends IGetable, IGetById, IGraphQueryableCollection { + add(name: string, planId: string, orderHint?: string): Promise; +} +export interface _Buckets extends IGetable, IGetById { } +export const Buckets = graphInvokableFactory(_Buckets); + +export interface IBucketAddResult { + data: IPlannerBucketType; + bucket: IBucket; +} + +export interface IPlanAddResult { + data: IPlannerPlanType; + plan: IPlan; +} + +export interface ITaskAddResult { + data: IPlannerTaskType; + task: ITask; +} diff --git a/packages/graph/src/planner/users.ts b/packages/graph/src/planner/users.ts new file mode 100644 index 000000000..38dfad6a5 --- /dev/null +++ b/packages/graph/src/planner/users.ts @@ -0,0 +1,14 @@ +import { addProp } from "@pnp/odata"; +import { _User } from "../users/types"; +import { ITasks, Tasks } from "./types"; + +declare module "../users/types" { + interface _User { + readonly tasks: ITasks; + } + interface IUser { + readonly tasks: ITasks; + } +} + +addProp(_User, "tasks", Tasks, "planner/tasks"); diff --git a/packages/graph/src/rest.ts b/packages/graph/src/rest.ts new file mode 100644 index 000000000..e9b267eca --- /dev/null +++ b/packages/graph/src/rest.ts @@ -0,0 +1,19 @@ +import { _GraphQueryable } from "./graphqueryable"; +import { + setup as _setup, + GraphConfiguration, +} from "./config/graphlibconfig"; +import { GraphBatch } from "./batch"; + +export class GraphRest extends _GraphQueryable { + + public createBatch(): GraphBatch { + return new GraphBatch(); + } + + public setup(config: GraphConfiguration) { + _setup(config); + } +} + +export let graph = new GraphRest("v1.0"); diff --git a/packages/graph/src/subscriptions/index.ts b/packages/graph/src/subscriptions/index.ts new file mode 100644 index 000000000..cd65dea5b --- /dev/null +++ b/packages/graph/src/subscriptions/index.ts @@ -0,0 +1,24 @@ +import { GraphRest } from "../rest"; +import { Subscriptions, ISubscriptions } from "./types"; + +export { + ISubscription, + ISubAddResult, + ISubscriptions, + Subscription, + Subscriptions, +} from "./types"; + +declare module "../rest" { + interface GraphRest { + readonly subscriptions: ISubscriptions; + } +} + +Reflect.defineProperty(GraphRest.prototype, "subscriptions", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return Subscriptions(this); + }, +}); diff --git a/packages/graph/src/subscriptions/types.ts b/packages/graph/src/subscriptions/types.ts new file mode 100644 index 000000000..02fdb8700 --- /dev/null +++ b/packages/graph/src/subscriptions/types.ts @@ -0,0 +1,63 @@ +import { _GraphQueryableInstance, IGraphQueryableInstance, _GraphQueryableCollection, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { extend } from "@pnp/common"; +import { IGetable, body } from "@pnp/odata"; +import { Subscription as ISubscriptionType } from "@microsoft/microsoft-graph-types"; +import { defaultPath, deleteable, IDeleteable, IUpdateable, updateable, getById, IGetById } from "../decorators"; +import { graphPost } from "../operations"; + +/** + * Subscription + */ +@deleteable() +@updateable() +export class _Subscription extends _GraphQueryableInstance implements ISubscription { } +export interface ISubscription extends IGetable, IDeleteable, IUpdateable, IGraphQueryableInstance { } +export interface _Subscription extends IGetable, IDeleteable, IUpdateable { } +export const Subscription = graphInvokableFactory(_Subscription); + +/** + * Subscriptions + */ +@defaultPath("subscriptions") +@getById(Subscription) +export class _Subscriptions extends _GraphQueryableCollection { + public async add(changeType: string, notificationUrl: string, resource: string, expirationDateTime: string, props: ISubscriptionType = {}): Promise { + + const postBody = extend({ + changeType, + expirationDateTime, + notificationUrl, + resource, + }, props); + + const data = await graphPost(this, body(postBody)); + + return { + data, + subscription: this.getById(data.id), + }; + } +} +export interface ISubscriptions extends IGetable, IGetById, IGraphQueryableCollection { + /** + * Create a new Subscription. + * + * @param changeType Indicates the type of change in the subscribed resource that will raise a notification. The supported values are: created, updated, deleted. + * @param notificationUrl The URL of the endpoint that will receive the notifications. This URL must make use of the HTTPS protocol. + * @param resource Specifies the resource that will be monitored for changes. Do not include the base URL (https://graph.microsoft.com/v1.0/). + * @param expirationDateTime Specifies the date and time when the webhook subscription expires. The time is in UTC. + * @param props A plain object collection of additional properties you want to set on the new subscription + * + */ + add(changeType: string, notificationUrl: string, resource: string, expirationDateTime: string, props: ISubscriptionType): Promise; +} +export interface _Subscriptions extends IGetable, IGetById { } +export const Subscriptions = graphInvokableFactory(_Subscriptions); + +/** + * ISubAddResult + */ +export interface ISubAddResult { + data: ISubscriptionType; + subscription: ISubscription; +} diff --git a/packages/graph/src/teams/index.ts b/packages/graph/src/teams/index.ts new file mode 100644 index 000000000..b38775401 --- /dev/null +++ b/packages/graph/src/teams/index.ts @@ -0,0 +1,66 @@ +import { addProp, body } from "@pnp/odata"; +import { GraphRest } from "../rest"; +import { _Group, Group } from "../groups/types"; +import { ITeamCreateResult, ITeamProperties, ITeam, Team, ITeams, Teams } from "./types"; +import { graphPut } from "../operations"; + +import "./users"; + +export { + Channel, + Channels, + IChannel, + IChannelCreateResult, + IChannels, + ITab, + ITabCreateResult, + ITabUpdateResult, + ITabs, + ITabsConfiguration, + ITeam, + ITeamCreateResult, + ITeamProperties, + ITeamUpdateResult, + ITeams, + Tab, + Tabs, + Team, + Teams, +} from "./types"; + +declare module "../groups/types" { + interface _Group { + readonly team: ITeam; + createTeam(properties: ITeamProperties): Promise; + } + interface IGroup { + readonly team: ITeam; + createTeam(properties: ITeamProperties): Promise; + } +} + +addProp(_Group, "team", Team); + +_Group.prototype.createTeam = async function (this: _Group, props: ITeamProperties): Promise { + + const data = await graphPut(this.clone(Group, "team"), body(props)); + + return { + data, + team: this.team, + }; +}; + +declare module "../rest" { + interface GraphRest { + readonly teams: ITeams; + } +} + +Reflect.defineProperty(GraphRest.prototype, "teams", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return Teams(this); + }, +}); diff --git a/packages/graph/src/teams/types.ts b/packages/graph/src/teams/types.ts new file mode 100644 index 000000000..4538e4f7c --- /dev/null +++ b/packages/graph/src/teams/types.ts @@ -0,0 +1,238 @@ +import { _GraphQueryableInstance, IGraphQueryableInstance, IGraphQueryableCollection, _GraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { IGetable, body } from "@pnp/odata"; +import { extend } from "@pnp/common"; +import { updateable, IUpdateable, getById, IGetById, deleteable, IDeleteable } from "../decorators"; +import { graphPost } from "../operations"; +import { defaultPath } from "../decorators"; + +/** + * Represents a Microsoft Team + */ +@defaultPath("team") +@updateable() +export class _Team extends _GraphQueryableInstance implements ITeam { + + public get channels(): IChannels { + return Channels(this); + } + + /** + * Archives this Team + * + * @param shouldSetSpoSiteReadOnlyForMembers Should members have Read-only in associated Team Site + */ + public archive(shouldSetSpoSiteReadOnlyForMembers = false): Promise { + return graphPost(this.clone(Team, "archive"), body({ shouldSetSpoSiteReadOnlyForMembers })); + } + + /** + * Unarchives this Team + */ + public unarchive(): Promise { + return graphPost(this.clone(Team, "unarchive")); + } + + /** + * Clones this Team + * @param name The name of the new Group + * @param description Optional description of the group + * @param partsToClone Parts to clone ex: apps,tabs,settings,channels,members + * @param visibility Set visibility to public or private + */ + public cloneTeam(name: string, description = "", partsToClone = "apps,tabs,settings,channels,members", visibility: "public" | "private" = "private"): Promise { + + const postBody = { + description: description ? description : "", + displayName: name, + mailNickname: name, + partsToClone, + visibility, + }; + + // TODO:: we need to get the Location header from the response and return an operation + // instance that folks can query to see if/when this is complete + // it could just have a single method getResult (or whatever) that returns a promise that + // resolves when the operation is successful or rejects when it is not + return graphPost(this.clone(Team, "clone"), body(postBody)); + } +} +export interface ITeam extends IGetable, IUpdateable, IGraphQueryableInstance { + readonly channels: IChannels; + archive(shouldSetSpoSiteReadOnlyForMembers?: boolean): Promise; + unarchive(): Promise; + cloneTeam(name: string, description?: string, partsToClone?: string, visibility?: string): Promise; +} +export interface _Team extends IGetable, IUpdateable { } +export const Team = graphInvokableFactory(_Team); + +/** + * Teams + */ +@defaultPath("teams") +@getById(Team) +export class _Teams extends _GraphQueryableCollection implements ITeams { } +export interface ITeams extends IGetable, IGetById, IGraphQueryableCollection { +} +export interface _Teams extends IGetable, IGetById { } +export const Teams = graphInvokableFactory(_Teams); + +/** + * Channel + */ +export class _Channel extends _GraphQueryableInstance implements IChannel { + public get tabs(): ITabs { + return Tabs(this); + } +} +export interface IChannel extends IGetable, IGraphQueryableInstance { + readonly tabs: ITabs; +} +export interface _Channel extends IGetable { } +export const Channel = graphInvokableFactory(_Channel); + +/** + * Channels + */ +@defaultPath("channels") +@getById(Channel) +export class _Channels extends _GraphQueryableCollection implements IChannels { + + /** + * Creates a new Channel in the Team + * @param displayName The display name of the new channel + * @param description Optional description of the channel + * + */ + public async add(displayName: string, description = ""): Promise { + + const postBody = { + description, + displayName, + }; + + const data = await graphPost(this, body(postBody)); + + return { + channel: this.getById(data.id), + data, + }; + } +} +export interface IChannels extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _Channels extends IGetable, IGetById { } +export const Channels = graphInvokableFactory(_Channels); + +/** + * Tab + */ +@defaultPath("tab") +@updateable() +@deleteable() +export class _Tab extends _GraphQueryableInstance implements ITab { } +export interface ITab extends IGetable, IUpdateable, IDeleteable, IGraphQueryableInstance { } +export interface _Tab extends IGetable, IUpdateable, IDeleteable { } +export const Tab = graphInvokableFactory(_Tab); + +/** + * Tabs + */ +@defaultPath("tabs") +@getById(Tab) +export class _Tabs extends _GraphQueryableCollection implements ITabs { + + /** + * Adds a tab to the cahnnel + * @param name The name of the new Tab + * @param appUrl The url to an app ex: https://graph.microsoft.com/beta/appCatalogs/teamsApps/12345678-9abc-def0-123456789a + * @param tabsConfiguration visit https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/teamstab_add for reference + */ + public async add(name: string, appUrl: string, properties: ITabsConfiguration): Promise { + + const postBody = extend({ + name, + "teamsApp@odata.bind": appUrl, + }, properties); + + const data = await graphPost(this, body(postBody)); + + return { + data, + tab: this.getById(data.id), + }; + } +} +export interface ITabs extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _Tabs extends IGetable, IGetById { } +export const Tabs = graphInvokableFactory(_Tabs); + +export interface ITeamUpdateResult { + data: any; + team: ITeam; +} + + + +export interface IChannelCreateResult { + data: any; + channel: IChannel; +} + +export interface ITabCreateResult { + data: any; + tab: ITab; +} + +export interface ITabUpdateResult { + data: any; + tab: ITab; +} + +/** + * Defines the properties for a Team + * + * TODO:: remove this once typings are present in graph types package + */ +export interface ITeamProperties { + + memberSettings?: { + "allowCreateUpdateChannels"?: boolean; + "allowDeleteChannels"?: boolean; + "allowAddRemoveApps"?: boolean; + "allowCreateUpdateRemoveTabs"?: boolean; + "allowCreateUpdateRemoveConnectors"?: boolean; + }; + + guestSettings?: { + "allowCreateUpdateChannels"?: boolean; + "allowDeleteChannels"?: boolean; + }; + + messagingSettings?: { + "allowUserEditMessages"?: boolean; + "allowUserDeleteMessages"?: boolean; + "allowOwnerDeleteMessages"?: boolean; + "allowTeamMentions"?: boolean; + "allowChannelMentions"?: boolean; + }; + + funSettings?: { + "allowGiphy"?: boolean; + "giphyContentRating"?: "strict" | string, + "allowStickersAndMemes"?: boolean; + "allowCustomMemes"?: boolean; + }; +} + +export interface ITabsConfiguration { + configuration: { + "entityId": string; + "contentUrl": string; + "websiteUrl": string; + "removeUrl": string; + }; +} + +export interface ITeamCreateResult { + data: any; + team: ITeam; +} diff --git a/packages/graph/src/teams/users.ts b/packages/graph/src/teams/users.ts new file mode 100644 index 000000000..b64d67171 --- /dev/null +++ b/packages/graph/src/teams/users.ts @@ -0,0 +1,14 @@ +import { addProp } from "@pnp/odata"; +import { _User } from "../users/types"; +import { ITeams, Teams } from "./types"; + +declare module "../users/types" { + interface _User { + readonly joinedTeams: ITeams; + } + interface IUser { + readonly joinedTeams: ITeams; + } +} + +addProp(_User, "joinedTeams", Teams, "joinedTeams"); diff --git a/packages/graph/src/types.ts b/packages/graph/src/types.ts new file mode 100644 index 000000000..7e28ebd20 --- /dev/null +++ b/packages/graph/src/types.ts @@ -0,0 +1,17 @@ +export class GraphEndpoints { + + public static Beta = "beta"; + public static V1 = "v1.0"; + + /** + * + * @param url The url to set the endpoint + */ + public static ensure(url: string, endpoint: string): string { + const all = [GraphEndpoints.Beta, GraphEndpoints.V1]; + let regex = new RegExp(endpoint, "i"); + const replaces = all.filter(s => !regex.test(s)).map(s => s.replace(".", "\\.")); + regex = new RegExp(`/?(${replaces.join("|")})/`, "ig"); + return url.replace(regex, `/${endpoint}/`); + } +} diff --git a/packages/graph/src/users/index.ts b/packages/graph/src/users/index.ts new file mode 100644 index 000000000..da04ad2c6 --- /dev/null +++ b/packages/graph/src/users/index.ts @@ -0,0 +1,32 @@ +import { GraphRest } from "../rest"; +import { IUser, User, IUsers, Users } from "./types"; + +export { + IUser, + IUsers, + User, + Users, +} from "./types"; + +declare module "../rest" { + interface GraphRest { + readonly me: IUser; + readonly users: IUsers; + } +} + +Reflect.defineProperty(GraphRest.prototype, "me", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return User(this, "me"); + }, +}); + +Reflect.defineProperty(GraphRest.prototype, "users", { + configurable: true, + enumerable: true, + get: function (this: GraphRest) { + return Users(this); + }, +}); diff --git a/packages/graph/src/users/types.ts b/packages/graph/src/users/types.ts new file mode 100644 index 000000000..87df83faf --- /dev/null +++ b/packages/graph/src/users/types.ts @@ -0,0 +1,37 @@ +import { _GraphQueryableCollection, IGraphQueryableCollection, graphInvokableFactory } from "../graphqueryable"; +import { + User as IUserType, +} from "@microsoft/microsoft-graph-types"; +import { _DirectoryObject, IDirectoryObject, DirectoryObjects, IDirectoryObjects } from "../directory-objects/types"; +import { defaultPath, updateable, deleteable, IUpdateable, IDeleteable, getById, IGetById } from "../decorators"; +import { IGetable } from "@pnp/odata"; + +/** + * Represents a user entity + */ +@updateable() +@deleteable() +export class _User extends _DirectoryObject implements IUser { + /** + * The groups and directory roles associated with the user + */ + public get memberOf(): IDirectoryObjects { + return DirectoryObjects(this, "memberOf"); + } +} +export interface IUser extends IGetable, IUpdateable, IDeleteable, IDirectoryObject { + readonly memberOf: IDirectoryObjects; + } +export interface _User extends IGetable, IUpdateable, IDeleteable { } +export const User = graphInvokableFactory(_User); + +/** + * Describes a collection of Users objects + * + */ +@defaultPath("users") +@getById(User) +export class _Users extends _GraphQueryableCollection {} +export interface IUsers extends IGetable, IGetById, IGraphQueryableCollection { } +export interface _Users extends IGetable, IGetById { } +export const Users = graphInvokableFactory(_Users); diff --git a/packages/graph/src/utils/type.ts b/packages/graph/src/utils/type.ts new file mode 100644 index 000000000..4bf2f33b6 --- /dev/null +++ b/packages/graph/src/utils/type.ts @@ -0,0 +1,3 @@ +export function type(n: string, a: T): T & { "@odata.type": string} { + return Object.assign({ "@odata.type": n }, a); +} diff --git a/packages/graph/tsconfig.es5.json b/packages/graph/tsconfig.es5.json new file mode 100644 index 000000000..0c56c2b8a --- /dev/null +++ b/packages/graph/tsconfig.es5.json @@ -0,0 +1,30 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "strictNullChecks": false + }, + "types": [ + "adal" + ], + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + }, + { + "path": "../odata/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/graph/tsconfig.json b/packages/graph/tsconfig.json new file mode 100644 index 000000000..69c18a7cd --- /dev/null +++ b/packages/graph/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strictNullChecks": false + }, + "types": [ + "adal" + ], + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + }, + { + "path": "../odata" + } + ] +} \ No newline at end of file diff --git a/packages/index.md b/packages/index.md new file mode 100644 index 000000000..e906153c4 --- /dev/null +++ b/packages/index.md @@ -0,0 +1,56 @@ +![SharePoint Patterns and Practices Logo](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) + +PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community. + +![Fluent API in action](documentation/img/PnPJS_FluentAPI.gif) +_Animation of the library in use, note intellisense help in building your queries_ + +## General Guidance + +These articles provide general guidance for working with the libraries. If you are migrating from _sp-pnp-js_ please review the [transition guide](documentation/transition-guide.md). + +* **[Getting Started](documentation/getting-started.md)** +* [Getting Started Contributing](documentation/getting-started-dev.md) +* [Documentation](documentation/documentation.md) +* [Gulp Commands](documentation/gulp-commands.md) +* [Debugging](documentation/debugging.md) +* [Deployment](documentation/deployment.md) +* [Install Beta Versions](documentation/beta-versions.md) +* [Polyfills](documentation/polyfill.md) +* [Package Structure](documentation/package-structure.md) + +## Packages + +Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. + +The latest published version is **{{version}}**. + +| || | +| ---| -------------|-------------| +| @pnp/| | | +|| [common](common/docs/index.md) | Provides shared functionality across all pnp libraries | +|| [config-store](config-store/docs/index.md) | Provides a way to manage configuration within your application | +|| [graph](graph/docs/index.md) | Provides a fluent api for working with Microsoft Graph | +|| [logging](logging/docs/index.md) | Light-weight, subscribable logging framework | +|| [nodejs](nodejs/docs/index.md) | Provides functionality enabling the @pnp libraries within nodejs | +|| [odata](odata/docs/index.md) | Provides shared odata functionality and base classes | +|| [pnpjs](pnpjs/docs/index.md) | Rollup library of core functionality (mimics sp-pnp-js) | +|| [sp](sp/docs/index.md) | Provides a fluent api for working with SharePoint REST | +|| [sp-addinhelpers](sp-addinhelpers/docs/index.md) | Provides functionality for working within SharePoint add-ins | +|| [sp-clientsvc](sp-clientsvc/docs/index.md) | Provides based classes used to create a fluent api for working with SharePoint Managed Metadata | +|| [sp-taxonomy](sp-taxonomy/docs/index.md) | Provides a fluent api for working with SharePoint Managed Metadata | + +## Issues, Questions, Ideas + +Please [log an issue](https://github.com/pnp/pnpjs/issues) using our template as a guide. This will let us track your request and ensure we respond. We appreciate any contructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project. + + +## Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### "Sharing is Caring" + +Please use [http://aka.ms/sppnp](http://aka.ms/sppnp) for the latest updates around the whole *SharePoint Patterns and Practices (PnP) program*. + +### Disclaimer +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** diff --git a/packages/logging/__tests/logging.test.ts b/packages/logging/__tests/logging.test.ts new file mode 100644 index 000000000..f210d8d3e --- /dev/null +++ b/packages/logging/__tests/logging.test.ts @@ -0,0 +1,68 @@ +import { expect } from "chai"; +import { Logger, LogLevel, FunctionListener } from ".."; + +describe("Logging", () => { + + describe("Logger", () => { + + const logger = Logger; + + beforeEach(() => { + logger.clearSubscribers(); + }); + + it("Can create an Logger instance and subscribe an ILogListener", () => { + const message = "Test message"; + let message2 = ""; + logger.subscribe(new FunctionListener((e) => { + message2 = e.message; + })); + logger.write(message, LogLevel.Warning); + expect(message2).to.eq(message); + }); + + it("Can create an Logger instance and log a simple object", () => { + let message2 = ""; + let level2 = LogLevel.Verbose; + logger.subscribe(new FunctionListener((e) => { + level2 = e.level; + message2 = e.message; + })); + logger.log({ level: LogLevel.Error, message: "Test message" }); + expect(message2).to.eq("Test message"); + expect(level2).to.eql(LogLevel.Error); + }); + + it("Should return an accurate count of subscribers", () => { + logger.subscribe(new FunctionListener(() => { return; })); + logger.subscribe(new FunctionListener(() => { return; })); + logger.subscribe(new FunctionListener(() => { return; })); + expect(logger.count).to.eq(3); + }); + + it("Should allow multiple subscribes to be added in one call", () => { + logger.subscribe( + new FunctionListener(() => { return; }), + new FunctionListener(() => { return; }), + new FunctionListener(() => { return; }), + ); + expect(logger.count).to.eq(3); + }); + + it("Should correctly log to multiple listeners", () => { + let message1 = ""; + let message2 = ""; + let message3 = ""; + logger.subscribe( + new FunctionListener((e) => { message1 = e.message; }), + new FunctionListener((e) => { message2 = e.message; }), + new FunctionListener((e) => { message3 = e.message; }), + ); + logger.activeLogLevel = LogLevel.Verbose; + logger.write("Test message"); + expect(message1).to.eq("Test message"); + expect(message2).to.eq("Test message"); + expect(message3).to.eq("Test message"); + }); + }); +}); diff --git a/packages/logging/docs/index.md b/packages/logging/docs/index.md new file mode 100644 index 000000000..961263865 --- /dev/null +++ b/packages/logging/docs/index.md @@ -0,0 +1,174 @@ +# @pnp/logging + +[![npm version](https://badge.fury.io/js/%40pnp%2Flogging.svg)](https://badge.fury.io/js/%40pnp%2Flogging) + +The logging module provides light weight subscribable and extensiable logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers. + +## Getting Started + +Install the logging module, it has no other dependencies + +`npm install @pnp/logging --save` + +## Understanding the Logging Framework + +The logging framework is based on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the _LogListener_ interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface. + +```TypeScript +/** + * Interface that defines a log listner + * + */ +export interface LogListener { + /** + * Any associated data that a given logging listener may choose to log or ignore + * + * @param entry The information to be logged + */ + log(entry: LogEntry): void; +} + +/** + * Interface that defines a log entry + * + */ +export interface LogEntry { + /** + * The main message to be logged + */ + message: string; + /** + * The level of information this message represents + */ + level: LogLevel; + /** + * Any associated data that a given logging listener may choose to log or ignore + */ + data?: any; +} +``` + +### Log Levels + +```TypeScript +export const enum LogLevel { + Verbose = 0, + Info = 1, + Warning = 2, + Error = 3, + Off = 99, +} +``` + +## Writing to the Logger + +To write information to a logger you can use either write, writeJSON, or log. + +```TypeScript +import { + Logger, + LogLevel +} from "@pnp/logging"; + +// write logs a simple string as the message value of the LogEntry +Logger.write("This is logging a simple string"); + +// optionally passing a level, default level is Verbose +Logger.write("This is logging a simple string", LogLevel.Error); + +// this will convert the object to a string using JSON.stringify and set the message with the result +Logger.writeJSON({ name: "value", name2: "value2"}); + +// optionally passing a level, default level is Verbose +Logger.writeJSON({ name: "value", name2: "value2"}, LogLevel.Warn); + +// specify the entire LogEntry interface using log +Logger.log({ + data: { name: "value", name2: "value2"}, + level: LogLevel.Warning, + message: "This is my message" +}); +``` + +## Log an error + +There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error +instance pased in, the level will be Error, and the message will be the Error instance message. + +```TypeScript +const e = new Error("An Error"); + +Logger.error(e); +``` + +## Subscribing a Listener + +By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. + +```TypeScript +import { + Logger, + ConsoleListener, + LogLevel +} from "@pnp/logging"; + +// subscribe a listener +Logger.subscribe(new ConsoleListener()); + +// set the active log level +Logger.activeLogLevel = LogLevel.Info; +``` + +## Available Listeners + +There are two listeners included in the library, ConsoleListener and FunctionListener. + +### ConsoleListener + +This listener outputs information to the console and works in Node as well as within browsers. It takes no settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Usage is shown in the example above. + +### FunctionListener + +The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. + +```TypeScript +import { + Logger, + FunctionListener, + LogEntry +} from "@pnp/logging"; + +let listener = new FunctionListener((entry: LogEntry) => { + + // pass all logging data to an existing framework + MyExistingCompanyLoggingFramework.log(entry.message); +}); + +Logger.subscribe(listener); +``` + +### Create a Custom Listener + +If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the LogListener interface. + +```TypeScript +import { + Logger, + LogListener, + LogEntry +} from "@pnp/logging"; + +class MyListener implements LogListener { + + log(entry: LogEntry): void { + // here you would do something with the entry + } +} + +Logger.subscribe(new MyListener()); +``` + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-logging-uml.svg) + +Graphical UML diagram of @pnp/logging. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/logging/index.ts b/packages/logging/index.ts new file mode 100644 index 000000000..c7a2f18a2 --- /dev/null +++ b/packages/logging/index.ts @@ -0,0 +1 @@ +export * from "./src/logging"; diff --git a/packages/logging/package.json b/packages/logging/package.json new file mode 100644 index 000000000..8a8998e7e --- /dev/null +++ b/packages/logging/package.json @@ -0,0 +1,22 @@ +{ + "name": "@pnp/logging", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - light-weight, subscribable logging framework", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/packages/logging/src/listeners.ts b/packages/logging/src/listeners.ts new file mode 100644 index 000000000..fcab5c6f7 --- /dev/null +++ b/packages/logging/src/listeners.ts @@ -0,0 +1,74 @@ +import { ILogEntry, LogLevel, ILogListener } from "./types"; + +/** + * Implementation of LogListener which logs to the console + * + */ +export class ConsoleListener implements ILogListener { + + /** + * Any associated data that a given logging listener may choose to log or ignore + * + * @param entry The information to be logged + */ + public log(entry: ILogEntry): void { + + const msg = this.format(entry); + + switch (entry.level) { + case LogLevel.Verbose: + case LogLevel.Info: + console.log(msg); + break; + case LogLevel.Warning: + console.warn(msg); + break; + case LogLevel.Error: + console.error(msg); + break; + } + } + + /** + * Formats the message + * + * @param entry The information to format into a string + */ + private format(entry: ILogEntry): string { + const msg = []; + msg.push("Message: " + entry.message); + if (entry.data !== undefined) { + try { + msg.push(" Data: " + JSON.stringify(entry.data)); + } catch (e) { + msg.push(` Data: Error in stringify of supplied data ${e}`); + } + } + + return msg.join(""); + } +} + +/** + * Implementation of LogListener which logs to the supplied function + * + */ +export class FunctionListener implements ILogListener { + + /** + * Creates a new instance of the FunctionListener class + * + * @constructor + * @param method The method to which any logging data will be passed + */ + constructor(private method: (entry: ILogEntry) => void) { } + + /** + * Any associated data that a given logging listener may choose to log or ignore + * + * @param entry The information to be logged + */ + public log(entry: ILogEntry): void { + this.method(entry); + } +} diff --git a/packages/logging/src/logger.ts b/packages/logging/src/logger.ts new file mode 100644 index 000000000..d5e2f77e3 --- /dev/null +++ b/packages/logging/src/logger.ts @@ -0,0 +1,118 @@ +import { ILogListener, ILogEntry, LogLevel } from "./types"; + +/** + * Class used to subscribe ILogListener and log messages throughout an application + * + */ +export class Logger { + + private static _instance: LoggerImpl; + + /** + * Gets or sets the active log level to apply for log filtering + */ + public static get activeLogLevel(): LogLevel { + return Logger.instance.activeLogLevel; + } + + public static set activeLogLevel(value: LogLevel) { + Logger.instance.activeLogLevel = value; + } + + private static get instance(): LoggerImpl { + if (Logger._instance === undefined || Logger._instance === null) { + Logger._instance = new LoggerImpl(); + } + return Logger._instance; + } + + /** + * Adds ILogListener instances to the set of subscribed listeners + * + * @param listeners One or more listeners to subscribe to this log + */ + public static subscribe(...listeners: ILogListener[]): void { + listeners.forEach(listener => Logger.instance.subscribe(listener)); + } + + /** + * Clears the subscribers collection, returning the collection before modifiction + */ + public static clearSubscribers(): ILogListener[] { + return Logger.instance.clearSubscribers(); + } + + /** + * Gets the current subscriber count + */ + public static get count(): number { + return Logger.instance.count; + } + + /** + * Writes the supplied string to the subscribed listeners + * + * @param message The message to write + * @param level [Optional] if supplied will be used as the level of the entry (Default: LogLevel.Info) + */ + public static write(message: string, level: LogLevel = LogLevel.Info) { + Logger.instance.log({ level: level, message: message }); + } + + /** + * Writes the supplied string to the subscribed listeners + * + * @param json The json object to stringify and write + * @param level [Optional] if supplied will be used as the level of the entry (Default: LogLevel.Info) + */ + public static writeJSON(json: any, level: LogLevel = LogLevel.Info) { + this.write(JSON.stringify(json), level); + } + + /** + * Logs the supplied entry to the subscribed listeners + * + * @param entry The message to log + */ + public static log(entry: ILogEntry) { + Logger.instance.log(entry); + } + + /** + * Logs an error object to the subscribed listeners + * + * @param err The error object + */ + public static error(err: Error) { + Logger.instance.log({ data: err, level: LogLevel.Error, message: err.message }); + } +} + +class LoggerImpl { + + constructor(public activeLogLevel: LogLevel = LogLevel.Warning, private subscribers: ILogListener[] = []) { } + + public subscribe(listener: ILogListener): void { + this.subscribers.push(listener); + } + + public clearSubscribers(): ILogListener[] { + const s = this.subscribers.slice(0); + this.subscribers.length = 0; + return s; + } + + public get count(): number { + return this.subscribers.length; + } + + public write(message: string, level: LogLevel = LogLevel.Info) { + this.log({ level: level, message: message }); + } + + public log(entry: ILogEntry) { + if (entry !== undefined && this.activeLogLevel <= entry.level) { + this.subscribers.map(subscriber => subscriber.log(entry)); + } + } +} diff --git a/packages/logging/src/logging.ts b/packages/logging/src/logging.ts new file mode 100644 index 000000000..1ff6bdac5 --- /dev/null +++ b/packages/logging/src/logging.ts @@ -0,0 +1,3 @@ +export { Logger } from "./logger"; +export { ConsoleListener, FunctionListener } from "./listeners"; +export * from "./types"; diff --git a/packages/logging/src/types.ts b/packages/logging/src/types.ts new file mode 100644 index 000000000..645f1a39d --- /dev/null +++ b/packages/logging/src/types.ts @@ -0,0 +1,42 @@ +/** + * A set of logging levels + */ +export const enum LogLevel { + Verbose = 0, + Info = 1, + Warning = 2, + Error = 3, + Off = 99, +} + +/** + * Interface that defines a log entry + * + */ +export interface ILogEntry { + /** + * The main message to be logged + */ + message: string; + /** + * The level of information this message represents + */ + level: LogLevel; + /** + * Any associated data that a given logging listener may choose to log or ignore + */ + data?: any; +} + +/** + * Interface that defines a log listner + * + */ +export interface ILogListener { + /** + * Any associated data that a given logging listener may choose to log or ignore + * + * @param entry The information to be logged + */ + log(entry: ILogEntry): void; +} diff --git a/packages/logging/tsconfig.es5.json b/packages/logging/tsconfig.es5.json new file mode 100644 index 000000000..1d457e7dd --- /dev/null +++ b/packages/logging/tsconfig.es5.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.es5.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [] +} \ No newline at end of file diff --git a/packages/logging/tsconfig.json b/packages/logging/tsconfig.json new file mode 100644 index 000000000..97a3c5c30 --- /dev/null +++ b/packages/logging/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [] +} \ No newline at end of file diff --git a/packages/nodejs/docs/adal-fetch-client.md b/packages/nodejs/docs/adal-fetch-client.md new file mode 100644 index 000000000..8f194c949 --- /dev/null +++ b/packages/nodejs/docs/adal-fetch-client.md @@ -0,0 +1,28 @@ +# @pnp/nodejs/adalfetchclient + +The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. + +```TypeScript +import { AdalFetchClient } from "@pnp/nodejs"; +import { graph } from "@pnp/graph"; + +// setup the client using graph setup function +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient("{tenant}", "{app id}", "{app secret}"); + }, + }, +}); + +// execute a library request as normal +graph.groups.get().then(g => { + + console.log(JSON.stringify(g, null, 4)); + +}).catch(e => { + + console.error(e); +}); +``` \ No newline at end of file diff --git a/packages/nodejs/docs/bearer-token-fetch-client.md b/packages/nodejs/docs/bearer-token-fetch-client.md new file mode 100644 index 000000000..15f7597f3 --- /dev/null +++ b/packages/nodejs/docs/bearer-token-fetch-client.md @@ -0,0 +1,27 @@ +# @pnp/nodejs/BearerTokenFetchClient + +The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. + +```TypeScript +import { BearerTokenFetchClient } from "@pnp/nodejs"; +import { graph } from "@pnp/graph"; + +// setup the client using graph setup function +graph.setup({ + graph: { + fetchClientFactory: () => { + return new BearerTokenFetchClient("{Bearer Token}"); + }, + }, +}); + +// execute a library request as normal +graph.groups.get().then(g => { + + console.log(JSON.stringify(g, null, 4)); + +}).catch(e => { + + console.error(e); +}); +``` \ No newline at end of file diff --git a/packages/nodejs/docs/index.md b/packages/nodejs/docs/index.md new file mode 100644 index 000000000..c54b18e28 --- /dev/null +++ b/packages/nodejs/docs/index.md @@ -0,0 +1,22 @@ +# @pnp/nodejs + +[![npm version](https://badge.fury.io/js/%40pnp%2Fnodejs.svg)](https://badge.fury.io/js/%40pnp%2Fnodejs) + +This package supplies helper code when using the @pnp libraries within the context of nodejs. This removes the node specific functionality from any of the packages. +Primarily these consist of clients to enable use of the libraries in nodejs. + +## Getting Started + +Install the library and required dependencies. You will also need to install other libraries such as [@pnp/sp](../../sp/docs/index.md) or [@pnp/graph](../../graph/docs/index.md) to use the +exported functionality. + +`npm install @pnp/logging @pnp/common @pnp/nodejs --save` + +* [AdalFetchClient](adal-fetch-client.md) +* [SPFetchClient](sp-fetch-client.md) +* [BearerTokenFetchClient](bearer-token-fetch-client.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-nodejs-uml.svg) + +Graphical UML diagram of @pnp/nodejs. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/nodejs/docs/provider-hosted-app.md b/packages/nodejs/docs/provider-hosted-app.md new file mode 100644 index 000000000..7f8ce761c --- /dev/null +++ b/packages/nodejs/docs/provider-hosted-app.md @@ -0,0 +1,43 @@ +# @pnp/nodejs/providerhostedrequestcontext + +_Added in 1.2.7_ + +The ProviderHostedRequestcontext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. + +The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. + +**Note**: To support concurrent requests by different users and/or add-ins on different tenants, do not use the `SPFetchClient` class. Instead, use the more generic `NodeFetchClient` class. The downside is that you have to manually configure each request to use the desired user/app context. + +```TypeScript +import { sp, SPRest } from "@pnp/sp"; +import { NodeFetchClient, ProviderHostedRequestContext } from "@pnp/nodejs"; + +// configure your node options +sp.setup({ + sp: { + fetchClientFactory: () => { + return new NodeFetchClient(); + }, + }, +}); + +// get request data generated by /_layouts/15/AppRedirect.aspx +const spAppToken = request.body.SPAppToken; +const spSiteUrl = request.body.SPSiteUrl; + +// create a context based on the add-in details and SPAppToken +const ctx = await ProviderHostedRequestContext.create(spSiteUrl, "{client id}", "{client secret}", spAppToken); + +// create an SPRest object configured to use our context +// this is used in place of the global sp object +const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl); +const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl); + +// make a request on behalf of the user +const user = await userSP.web.currentUser.get(); +console.log(`Hello ${user.Title}`); + +// make an add-in only request +const app = await addinSP.web.currentUser.get(); +console.log(`Add-in principal: ${app.Title}`); +``` \ No newline at end of file diff --git a/packages/nodejs/docs/sp-fetch-client.md b/packages/nodejs/docs/sp-fetch-client.md new file mode 100644 index 000000000..71c536243 --- /dev/null +++ b/packages/nodejs/docs/sp-fetch-client.md @@ -0,0 +1,106 @@ +# @pnp/nodejs/spfetchclient + +The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practive to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. + +```TypeScript +import { SPFetchClient } from "@pnp/nodejs"; +import { sp } from "@pnp/sp"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}"); + }, + }, +}); + +// execute a library request as normal +sp.web.get().then(w => { + + console.log(JSON.stringify(w, null, 4)); + +}).catch(e => { + + console.error(e); +}); +``` + +## Set Authentication Environment + +_Added in 1.1.2_ + +For some areas such as Germany, China, and US Gov clouds you need to specifiy a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. + +- SPO : (default) for all *.sharepoint.com urls +- China: for China hosted cloud +- Germany: for Germany local cloud +- USDef: USA Defense cloud +- USGov: USA Government cloud + +```TypeScript +import { sp } from "@pnp/sp"; +import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.China); + }, + }, +}); +``` + + +## Set Realm + +In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to "https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx" and copying the GUID value that appears after the "@" - this is the realm id. + +**As of version 1.1.2 the realm parameter is now the 5th parameter in the constructor.** + +```TypeScript +import { sp } from "@pnp/sp"; +import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.SPO, "{realm}"); + }, + }, +}); +``` + +## Creating a client id and secret + +This section outlines how to register for a client id and secret for use in the above code. + +### Register An Add-In + +Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you [configure your farm accordingly](https://msdn.microsoft.com/en-us/library/office/dn155905.aspx). + +1. Navigation to {site url}/_layouts/appregnew.aspx +2. Click "Generate" for both the Client Id and Secret values +3. Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions +4. Provide a fake value for app domain and redirect uri, you can use the values shown in the examples +5. Click "Create" +6. Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article. + +### Grant Your Add-In Permissions + +Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to [register add-ins with certain permissions in the admin site](https://msdn.microsoft.com/en-us/pnp_articles/how-to-provide-add-in-app-only-tenant-administrative-permissions-in-sharepoint-online). + +1. Navigate to {admin site url}/_layouts/appinv.aspx +2. Paste your client id from the above section into the Add Id box and click "Lookup" +3. You should see the information populated into the form from the last section, if not ensure you have the correct id value +4. Paste the below XML into the permissions request xml box and hit "Create" +5. You should get a confirmation message. + +```XML + + + + + +``` + +**Note that the above XML will grant full tenant control, you should grant only those permissions necessary for your application** diff --git a/packages/nodejs/index.ts b/packages/nodejs/index.ts new file mode 100644 index 000000000..07ef4f570 --- /dev/null +++ b/packages/nodejs/index.ts @@ -0,0 +1 @@ +export * from "./src/nodejs"; diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json new file mode 100644 index 000000000..9a9e76988 --- /dev/null +++ b/packages/nodejs/package.json @@ -0,0 +1,29 @@ +{ + "name": "@pnp/nodejs", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides functionality enabling the @pnp libraries within nodejs", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "adal-node": "0.1.28", + "jsonwebtoken": "8.3.0", + "node-fetch": "2.2.0", + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/logging": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} diff --git a/packages/nodejs/src/net/adalfetchclient.ts b/packages/nodejs/src/net/adalfetchclient.ts new file mode 100644 index 000000000..9495102a6 --- /dev/null +++ b/packages/nodejs/src/net/adalfetchclient.ts @@ -0,0 +1,63 @@ +declare var require: (path: string) => any; +import { AuthenticationContext } from "adal-node"; +const nodeFetch = require("node-fetch").default; +import { AADToken } from "../types"; +import { + combine, + objectDefinedNotNull, + IHttpClientImpl, + isUrlAbsolute, + extend, +} from "@pnp/common"; + +export class AdalFetchClient implements IHttpClientImpl { + + private authContext: any; + + constructor(private _tenant: string, + private _clientId: string, + private _secret: string, + private _resource = "https://graph.microsoft.com", + private _authority = "https://login.windows.net") { + + this.authContext = new AuthenticationContext(combine(this._authority, this._tenant)); + } + + public fetch(url: string, options: any): Promise { + + if (!objectDefinedNotNull(options)) { + options = { + headers: new Headers(), + }; + } else if (!objectDefinedNotNull(options.headers)) { + options = extend(options, { + headers: new Headers(), + }); + } + + if (!isUrlAbsolute(url)) { + url = combine(this._resource, url); + } + + return this.acquireToken().then(token => { + + options.headers.set("Authorization", `${token.tokenType} ${token.accessToken}`); + + return nodeFetch(url, options); + }); + } + + public acquireToken(): Promise { + return new Promise((resolve, reject) => { + + this.authContext.acquireTokenWithClientCredentials(this._resource, this._clientId, this._secret, (err: any, token: AADToken) => { + + if (err) { + reject(err); + } else { + resolve(token); + } + }); + }); + } +} diff --git a/packages/nodejs/src/net/bearertokenfetchclient.ts b/packages/nodejs/src/net/bearertokenfetchclient.ts new file mode 100644 index 000000000..3ba4de099 --- /dev/null +++ b/packages/nodejs/src/net/bearertokenfetchclient.ts @@ -0,0 +1,33 @@ +declare var require: (path: string) => any; + +import { IHttpClientImpl, mergeHeaders, IFetchOptions } from "@pnp/common"; +const nodeFetch = require("node-fetch").default; + +/** + * Makes requests using the fetch API adding the supplied token to the Authorization header + */ +export class BearerTokenFetchClient implements IHttpClientImpl { + + constructor(private _token: string | null) { } + + public get token() { + return this._token || ""; + } + + public set token(token: string) { + this._token = token; + } + + public fetch(url: string, options: IFetchOptions = {}): Promise { + + const headers = new Headers(); + + mergeHeaders(headers, options.headers); + + headers.set("Authorization", `Bearer ${this._token}`); + + options.headers = headers; + + return nodeFetch(url, options); + } +} diff --git a/packages/nodejs/src/net/index.ts b/packages/nodejs/src/net/index.ts new file mode 100644 index 000000000..525e3f6dd --- /dev/null +++ b/packages/nodejs/src/net/index.ts @@ -0,0 +1,4 @@ +export { AdalFetchClient } from "./adalfetchclient"; +export { BearerTokenFetchClient} from "./bearertokenfetchclient"; +export { NodeFetchClient } from "./nodefetchclient"; +export { SPFetchClient } from "./spfetchclient"; diff --git a/packages/nodejs/src/net/nodefetchclient.ts b/packages/nodejs/src/net/nodefetchclient.ts new file mode 100644 index 000000000..054c12424 --- /dev/null +++ b/packages/nodejs/src/net/nodefetchclient.ts @@ -0,0 +1,122 @@ +declare var require: (path: string) => any; +import { IHttpClientImpl } from "@pnp/common"; +import { Logger, LogLevel } from "@pnp/logging"; +const nodeFetch = require("node-fetch").default; + +/** + * Payload from transient errors + */ +interface IRetryData { + retryCount: number; + error: any; + retryInterval: number; +} + +/** + * Fetch client that encapsulates the node-fetch library and also adds retry logic + * when encountering transient errors. + */ +export class NodeFetchClient implements IHttpClientImpl { + + /** + * + * @param retryCount: number - Maximum number of transient failure retries before throwing the error + * @param retryInterval: number - Starting interval to delay the first retry attempt + * @param minRetryInterval: number - Minimum retry delay boundary as retry intervals are randomly recalculated + * @param maxRetryInterval: number - Maximum retry delay boundary as retry intervals are radnomaly recalculated + */ + constructor(private retryCount = 3, private retryInterval = 3000, private minRetryInterval = 3000, private maxRetryInterval = 90000) { } + + public async fetch(url: string, options?: any): Promise { + + const wrapper = async (retryData: any): Promise => { + + try { + + // Try to make the request... + return await nodeFetch(url, options || {}); + + } catch (err) { + + // Get the latest retry information. + const retry = this.updateRetryData(retryData, err); + + // If there is no error code, this wasn't a transient error + // so we throw immediately. + if (!err.code) { throw err; } + + // Watching for specific error codes. + if (["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNREFUSED", "ECONNRESET"].indexOf(err.code.toUpperCase()) > -1) { + + Logger.write(`Attempt #${retry.retryCount} - Retrying error code: ${err.code}...`, LogLevel.Verbose); + + // If current amount of retries is less than the max amount, + // try again + if (this.shouldRetry(retry)) { + await this.delay(retry.retryInterval); + return await wrapper(retry); + } else { // max amount of retries reached, so throw the error + throw err; + } + } + } + }; + + return await wrapper(null); + } + + private async delay(ms: number): Promise { + + return new Promise((resolve: any) => { + setTimeout(() => { + resolve(); + }, ms); + }); + + } + + private updateRetryData(retryData: IRetryData, err: any): IRetryData { + + const data: IRetryData = retryData || { + error: null, + retryCount: 0, + retryInterval: 0, + }; + + const newError = err || null; + + // Keep track of errors from previous retries + // if they exist + if (newError) { + + if (data.error) { + newError.innerError = data.error; + } + + data.error = newError; + + } + + // Adjust retry interval and cap based on the min and max intervals specified + let incrementDelta = Math.pow(2, data.retryCount) - 1; + const boundedRandDelta = this.retryInterval * 0.8 + + Math.floor(Math.random() * (this.retryInterval * 1.2 - this.retryInterval * 0.8)); + incrementDelta *= boundedRandDelta; + + // Adjust retry count + data.retryCount++; + data.retryInterval = Math.min(this.minRetryInterval + incrementDelta, this.maxRetryInterval); + + return data; + } + + private shouldRetry(retryData: IRetryData): boolean { + + if (!retryData) { + throw new Error("ERROR: retryData cannot be null."); + } + + const currentCount = (retryData && retryData.retryCount); + return (currentCount < this.retryCount); + } +} diff --git a/packages/nodejs/src/net/spfetchclient.ts b/packages/nodejs/src/net/spfetchclient.ts new file mode 100644 index 000000000..a0945e7f7 --- /dev/null +++ b/packages/nodejs/src/net/spfetchclient.ts @@ -0,0 +1,86 @@ +declare var global: any; +import { IHttpClientImpl, combine, isUrlAbsolute } from "@pnp/common"; +import { NodeFetchClient } from "./nodefetchclient"; +import { getAddInOnlyAccessToken } from "../sptokenutils"; +import { SPOAuthEnv, AuthToken } from "../types"; + +/** + * Fetch client for use within nodejs, requires you register a client id and secret with app only permissions + */ +export class SPFetchClient implements IHttpClientImpl { + + protected token: AuthToken | null = null; + + constructor( + public siteUrl: string, + protected _clientId: string, + protected _clientSecret: string, + public authEnv: SPOAuthEnv = SPOAuthEnv.SPO, + protected _realm = "", + protected _fetchClient: IHttpClientImpl = new NodeFetchClient()) { + + global._spPageContextInfo = { + webAbsoluteUrl: siteUrl, + }; + } + + public async fetch(url: string, options: any = {}): Promise { + + const realm = await this.getRealm(); + const authUrl = await this.getAuthUrl(realm); + const token = await getAddInOnlyAccessToken(this.siteUrl, this._clientId, this._clientSecret, realm, authUrl); + + options.headers.set("Authorization", `Bearer ${token.access_token}`); + + const uri = !isUrlAbsolute(url) ? combine(this.siteUrl, url) : url; + + return this._fetchClient.fetch(uri, options); + } + + public getAuthHostUrl(env: SPOAuthEnv): string { + switch (env) { + case SPOAuthEnv.China: + return "accounts.accesscontrol.chinacloudapi.cn"; + case SPOAuthEnv.Germany: + return "login.microsoftonline.de"; + default: + return "accounts.accesscontrol.windows.net"; + } + } + + private async getRealm(): Promise { + + if (this._realm.length > 0) { + return Promise.resolve(this._realm); + } + + const url = combine(this.siteUrl, "_vti_bin/client.svc"); + + const r = await this._fetchClient.fetch(url, { + "headers": { + "Authorization": "Bearer ", + }, + "method": "POST", + }); + + const data: string = r.headers.get("www-authenticate") || ""; + const index = data.indexOf("Bearer realm=\""); + this._realm = data.substring(index + 14, index + 50); + return this._realm; + } + + private async getAuthUrl(realm: string): Promise { + + const url = `https://${this.getAuthHostUrl(this.authEnv)}/metadata/json/1?realm=${realm}`; + + const r = await this._fetchClient.fetch(url, { method: "GET"}); + const json: { endpoints: { protocol: string, location: string }[] } = await r.json(); + + const eps = json.endpoints.filter(ep => ep.protocol === "OAuth2"); + if (eps.length > 0) { + return eps[0].location; + } + + throw Error("Auth URL Endpoint could not be determined from data."); + } +} diff --git a/packages/nodejs/src/nodejs.ts b/packages/nodejs/src/nodejs.ts new file mode 100644 index 000000000..a733302d3 --- /dev/null +++ b/packages/nodejs/src/nodejs.ts @@ -0,0 +1,22 @@ +declare var global: any; +declare var require: (path: string) => any; +const NodeFetch = require("node-fetch"); + +(function (g) { + + // patch these globally for nodejs + if (!g.Headers) { + g.Headers = NodeFetch.Headers; + } + if (!g.Request) { + g.Request = NodeFetch.Request; + } + if (!g.Response) { + g.Response = NodeFetch.Response; + } + +})(global); + +export { AADToken, SPOAuthEnv } from "./types"; +export { ProviderHostedRequestContext } from "./providerhosted"; +export * from "./net/index"; diff --git a/packages/nodejs/src/providerhosted.ts b/packages/nodejs/src/providerhosted.ts new file mode 100644 index 000000000..3d7e924f0 --- /dev/null +++ b/packages/nodejs/src/providerhosted.ts @@ -0,0 +1,32 @@ +import { AuthToken, ProviderHostedConfigurationOptions } from "./types"; +import { validateProviderHostedRequestToken, getAddInOnlyAccessToken, getUserAccessToken } from "./sptokenutils"; + +export class ProviderHostedRequestContext { + + constructor(private siteUrl: string, private clientId: string, private clientSecret: string, + private realm: string, private refreshToken: string, private stsUri: string, private cacheKey: string) { } + + public static async create(siteUrl: string, clientId: string, clientSecret: string, spAppToken: string): Promise { + + const payload = await validateProviderHostedRequestToken(spAppToken, clientSecret); + const appctx = JSON.parse(payload.appctx); + + return new ProviderHostedRequestContext(siteUrl, clientId, clientSecret, payload.iss.split("@")[1], payload.refreshtoken, appctx.SecurityTokenServiceUri, appctx.CacheKey); + } + + public async getAddInOnlyConfig(): Promise { + return this.getConfigOptions(await getAddInOnlyAccessToken(this.siteUrl, this.clientId, this.clientSecret, this.realm, this.stsUri)); + } + + public async getUserConfig(): Promise { + return this.getConfigOptions(await getUserAccessToken(this.siteUrl, this.clientId, this.clientSecret, this.refreshToken, this.realm, this.stsUri, this.cacheKey)); + } + + private getConfigOptions(token: AuthToken): ProviderHostedConfigurationOptions { + return { + headers: { + "Authorization": `Bearer ${token.access_token}`, + }, + }; + } +} diff --git a/packages/nodejs/src/sptokenutils.ts b/packages/nodejs/src/sptokenutils.ts new file mode 100644 index 000000000..56505ffc6 --- /dev/null +++ b/packages/nodejs/src/sptokenutils.ts @@ -0,0 +1,115 @@ +declare var require: (path: string) => any; +const u: any = require("url"); +const nodeFetch = require("node-fetch").default; +import * as jwt from "jsonwebtoken"; +import { TypedHash } from "@pnp/common"; +import { AuthToken, SharePointServicePrincipal, ITokenCacheManager } from "./types"; + +class MapCacheManager implements ITokenCacheManager { + + private map: Map = new Map(); + + public getAccessToken(realm: string, cacheKey: string) { + return this.map.get(this.buildKey(realm, cacheKey)); + } + + public setAccessToken(realm: string, cacheKey: string, token: AuthToken) { + this.map.set(this.buildKey(realm, cacheKey), token); + } + + private buildKey(realm: string, cacheKey: string) { + return `${realm}:${cacheKey}`; + } +} + +const tokenCache: ITokenCacheManager = new MapCacheManager(); + +export async function validateProviderHostedRequestToken(requestToken: string, clientSecret: string): Promise> { + + return new Promise>((resolve, reject) => { + + const secret = Buffer.from(clientSecret, "base64"); + + jwt.verify(requestToken, secret, (err: jwt.VerifyErrors, decoded: any) => { + err ? reject(err) : resolve(decoded); + }); + }); +} + +/** + * Gets an add-in only authentication token based on the supplied site url, client id and secret + */ +export async function getAddInOnlyAccessToken(siteUrl: string, clientId: string, clientSecret: string, realm: string, stsUri: string): Promise { + return getTokenInternal({ siteUrl, clientId, clientSecret, refreshToken: null, realm, stsUri, cacheKey: `addinonly:${clientId}` }); +} + +/** + * Gets a user authentication token based on the supplied site url, client id, client secret, and refresh token + */ +// tslint:disable-next-line: max-line-length +export function getUserAccessToken(siteUrl: string, clientId: string, clientSecret: string, refreshToken: string, realm: string, stsUri: string, cacheKey: string): Promise { + return getTokenInternal({ siteUrl, clientId, clientSecret, refreshToken, realm, stsUri, cacheKey: `user:${cacheKey}` }); +} + +interface GetTokenInternalParams { + siteUrl: string; + clientId: string; + clientSecret: string; + refreshToken: string; + realm: string; + stsUri: string; + cacheKey: string; +} + +async function getTokenInternal(params: GetTokenInternalParams): Promise { + + let accessToken = tokenCache.getAccessToken(params.realm, params.cacheKey); + if (accessToken && new Date() < toDate(accessToken.expires_on)) { + return accessToken; + } + + const resource = getFormattedPrincipal(SharePointServicePrincipal, u.parse(params.siteUrl).hostname, params.realm); + const formattedClientId = getFormattedPrincipal(params.clientId, "", params.realm); + + const body: string[] = []; + if (params.refreshToken) { + body.push("grant_type=refresh_token"); + body.push(`refresh_token=${encodeURIComponent(params.refreshToken)}`); + } else { + body.push("grant_type=client_credentials"); + } + body.push(`client_id=${formattedClientId}`); + body.push(`client_secret=${encodeURIComponent(params.clientSecret)}`); + body.push(`resource=${resource}`); + + const r = await nodeFetch(params.stsUri, { + body: body.join("&"), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); + + accessToken = await r.json(); + tokenCache.setAccessToken(params.realm, params.cacheKey, accessToken); + return accessToken; +} + +function getFormattedPrincipal(principalName: string, hostName: string, realm: string): string { + let resource = principalName; + if (hostName !== null && hostName !== "") { + resource += "/" + hostName; + } + resource += "@" + realm; + return resource; +} + +function toDate(epoch: string): Date { + let tmp = parseInt(epoch, 10); + if (tmp < 10000000000) { + tmp *= 1000; + } + const d = new Date(); + d.setTime(tmp); + return d; +} diff --git a/packages/nodejs/src/types.ts b/packages/nodejs/src/types.ts new file mode 100644 index 000000000..920c4d3bf --- /dev/null +++ b/packages/nodejs/src/types.ts @@ -0,0 +1,38 @@ +export const SharePointServicePrincipal = "00000003-0000-0ff1-ce00-000000000000"; + +export interface AADToken { + accessToken: string; + expiresIn: number; + expiresOn: string | Date; + isMRRT?: boolean; + resource: string; + tokenType: string; +} + +export enum SPOAuthEnv { + SPO, + China, + Germany, + USDef, + USGov, +} + +export interface AuthToken { + token_type: string; + expires_in: string; + not_before: string; + expires_on: string; + resource: string; + access_token: string; +} + +export interface ProviderHostedConfigurationOptions { + headers: { + Authorization: string; + }; +} + +export interface ITokenCacheManager { + getAccessToken(realm: string, cacheKey: string): AuthToken; + setAccessToken(realm: string, cacheKey: string, token: AuthToken): void; +} diff --git a/packages/nodejs/tsconfig.es5.json b/packages/nodejs/tsconfig.es5.json new file mode 100644 index 000000000..df878f2c9 --- /dev/null +++ b/packages/nodejs/tsconfig.es5.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.es5.json", + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/nodejs/tsconfig.json b/packages/nodejs/tsconfig.json new file mode 100644 index 000000000..b56db1008 --- /dev/null +++ b/packages/nodejs/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + } + ] +} \ No newline at end of file diff --git a/packages/odata/docs/caching.md b/packages/odata/docs/caching.md new file mode 100644 index 000000000..97a01f012 --- /dev/null +++ b/packages/odata/docs/caching.md @@ -0,0 +1,163 @@ +# @pnp/odata/caching + +Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. + +The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/odata base classes, such as @pnp/graph. + +## Basic example + +You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The below code will get the items from the list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() should always be the last method in the chain before the get() (OR if you are using [[batching]] these methods can be transposed, more details below). + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("Tasks").items.usingCaching().get().then(r => { + console.log(r) +}); + +sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching().get().then(r => { + console.log(r) +}); +``` + +## Globally Configure Cache Settings + +If you would like to not use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.setup({ + defaultCachingStore: "session", // or "local" + defaultCachingTimeoutSeconds: 30, + globalCacheDisable: false // or true to disable caching in case of debugging/testing +}); + +sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching().get().then(r => { + console.log(r) +}); +``` + +## Per Call Configuration + +If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. + +```TypeScript +export interface ICachingOptions { + expiration?: Date; + storeName?: "session" | "local"; + key: string; +} +``` + +```TypeScript +import { sp } from "@pnp/sp"; +import { dateAdd } from "@pnp/common"; + +sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching({ + expiration: dateAdd(new Date(), "minute", 20), + key: "My Key", + storeName: "local" +}).get().then(r => { + console.log(r) +}); +``` + +## Using [Batching](odata-batch.md) with Caching + +You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. + +```TypeScript +import { sp } from "@pnp/sp"; + +let batch = sp.createBatch(); + +sp.web.lists.inBatch(batch).usingCaching().get().then(r => { + console.log(r) +}); + +sp.web.lists.getByTitle("Tasks").items.usingCaching().inBatch(batch).get().then(r => { + console.log(r) +}); + +batch.execute().then(() => console.log("All done!")); +``` + +## Implement Custom Caching + +You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achive this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern [as discussed here](https://github.com/pnp/pnpjs/issues/371). + +### Implement caching helper method: + +We create a map to act as our cache storage and a function to wrap the request caching logic + +```TypeScript +const map = new Map(); + +async function staleWhileRevalidate(key: string, p: Promise): Promise { + + if (map.has(key)) { + + // In Cache + p.then(u => { + // Update Cache once we have a result + map.set(key, u); + }); + + // Return from Cache + return map.get(key); + } + + // Not In Cache so we need to wait for the value + const r = await p; + + // Set Cache + map.set(key, r); + + // Return from Promise + return r; +} +``` + +### Usage + +> Don't call usingCaching just apply the helper method + +```TypeScript +// this one will wait for the request to finish +const r1 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description").get()); + +console.log(JSON.stringify(r1, null, 2)); + +// this one will return the result from cache and then update the cache in the background +const r2 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description").get()); + +console.log(JSON.stringify(r2, null, 2)); +``` + +### Wrapper Function + +You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. + +```TypeScript +interface WebData { + Title: string; + Description: string; +} + +function getWebData(): Promise { + + return staleWhileRevalidate("test1", sp.web.select("Title", "Description").get()); +} + + +// this one will wait for the request to finish +const r1 = await getWebData(); + +console.log(JSON.stringify(r1, null, 2)); + +// this one will return the result from cache and then update the cache in the background +const r2 = await getWebData(); + +console.log(JSON.stringify(r2, null, 2)); +``` diff --git a/packages/odata/docs/core.md b/packages/odata/docs/core.md new file mode 100644 index 000000000..cd375b5a1 --- /dev/null +++ b/packages/odata/docs/core.md @@ -0,0 +1,52 @@ +# @pnp/odata/core + +This modules contains shared interfaces and abstract classes used within, and by inheritors of, the @pnp/odata package. + +## ProcessHttpClientResponseException + +The exception thrown when a response is returned and cannot be processed. + +## interface ODataParser + +Base interface used to descibe a class that that will parse incoming responses. It takes a single type parameter representing the type of the +value to be returned. It has two methods, one is optional: + +* parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T +* hydrate?: (d: any) => T - optional method used when getting an object from the [cache](caching.md) if it requires calling a constructor + +## ODataParserBase + +The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper +methods. + +### Create a custom parser from ODataParserBase + +You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most +cases. + +```TypeScript +class MyParser extends ODataParserBase { + + // we need to override the parse method to do our custom stuff + public parse(r: Response): Promise { + + // we wrap everything in a promise + return new Promise((resolve, reject) => { + + // lets use the default error handling which returns true for no error + // and will call reject with an error if one exists + if (this.handleError(r, reject)) { + + // now we add our custom parsing here + r.text().then(txt => { + // here we call a madeup function to parse the result + // this is where we would do our parsing as required + myCustomerUnencode(txt).then(v => { + resolve(v); + }); + }); + } + }); + } +} +``` diff --git a/packages/odata/docs/index.md b/packages/odata/docs/index.md new file mode 100644 index 000000000..9f24ee142 --- /dev/null +++ b/packages/odata/docs/index.md @@ -0,0 +1,27 @@ +# @pnp/odata + +[![npm version](https://badge.fury.io/js/%40pnp%2Fodata.svg)](https://badge.fury.io/js/%40pnp%2Fodata) + +This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata +library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure +the core code is solid and well tested, with any updates benefitting all inheriting libraries. + +## Getting Started + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata --save` + +## Library Topics + +* [caching](caching.md) +* [core](core.md) +* [OData Batching](odata-batch.md) +* [Parsers](parsers.md) +* [Pipeline](pipeline.md) +* [Queryable](queryable.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-odata-uml.svg) + +Graphical UML diagram of @pnp/odata. Right-click the diagram and open in new tab if it is too small. \ No newline at end of file diff --git a/packages/odata/docs/odata-batch.md b/packages/odata/docs/odata-batch.md new file mode 100644 index 000000000..cdcfb0c17 --- /dev/null +++ b/packages/odata/docs/odata-batch.md @@ -0,0 +1,13 @@ +# @pnp/odata/odatabatch + +This module contains an abstract class used as a base when inheriting libraries support batching. + +## ODataBatchRequestInfo + +This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will +be responsible for processing that info by implementing the abstract executeImpl method. + +## ODataBatch + +Base class for building batching support for a library inheriting from @pnp/odata. You can see implementations of this abstract class in the @pnp/sp +and @pnp/graph modules. diff --git a/packages/odata/docs/parsers.md b/packages/odata/docs/parsers.md new file mode 100644 index 000000000..f94a44473 --- /dev/null +++ b/packages/odata/docs/parsers.md @@ -0,0 +1,74 @@ +# @pnp/odata/parsers + +This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need. + +## ODataDefaultParser + +The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. + +```TypeScript +import { sp } from "@pnp/sp"; +import { JSONParser } from "@pnp/odata"; + +try { + + const parser = new JSONParser(); + + // this always throws a 404 error + await sp.web.getList("doesn't exist").get(parser); + +} catch (e) { + + // we can check for the property "isHttpRequestError" to see if this is an instance of our class + // this gets by all the many limitations of subclassing Error and type detection in JavaScript + if (e.hasOwnProperty("isHttpRequestError")) { + + console.log("e is HttpRequestError"); + + // now we can access the various properties and make use of the response object. + // at this point the body is unread + console.log(`status: ${e.status}`); + console.log(`statusText: ${e.statusText}`); + + const json = await e.response.clone().json(); + console.log(JSON.stringify(json)); + const text = await e.response.clone().text(); + console.log(text); + const headers = e.response.headers; + } + + console.error(e); +} +``` + +## TextParser + +Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files. + +## BlobParser + +Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files. + +## JSONParser + +Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files. + +## BufferParser + +Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files. + +## LambdaParser + +Allows you to pass in any handler function you want, called if the request does not result in an error that ransforms the raw, unread request into the result type. + +```TypeScript +import { LambdaParser } from "@pnp/odata"; +import { sp } from "@pnp/sp"; + +// here a simple parser duplicating the functionality of the JSONParser +const parser = new LambdaParser((r: Response) => r.json()); + +const webDataJson = await sp.web.get(parser); + +console.log(webDataJson); +``` diff --git a/packages/odata/docs/pipeline.md b/packages/odata/docs/pipeline.md new file mode 100644 index 000000000..920bdef57 --- /dev/null +++ b/packages/odata/docs/pipeline.md @@ -0,0 +1,57 @@ +# @pnp/odata/pipeline + +All of the odata requests processed by @pnp/odata pass through an extensible request pipeline. Each request is executed in a specific request context defined by +the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the +pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline. + +## interface RequestContext + +The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This +allows full control over the methods called during a request, and allows for the insertion of any custom methods required. + +```TypeScript +interface RequestContext { + batch: ODataBatch; + batchDependency: () => void; + cachingOptions: ICachingOptions; + hasResult?: boolean; + isBatched: boolean; + isCached: boolean; + options: FetchOptions; + parser: ODataParser; + pipeline: Array<(c: RequestContext) => Promise>>; + requestAbsoluteUrl: string; + requestId: string; + result?: T; + verb: string; + clientFactory: () => RequestClient; +} +``` + +## requestPipelineMethod decorator + +The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you +would like your method to always run regardless of the existance of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument +of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. + +```TypeScript +@requestPipelineMethod(true) +public static myPipelineMethod(context: RequestContext): Promise> { + + return new Promise>(resolve => { + + // do something + + resolve(context); + }); +} +``` + +## Default Pipeline + +1. logs the start of the request +2. checks the cache for a value based on the context's cache settings +3. sends the request if no value from found in the cache +4. logs the end of the request + + diff --git a/packages/odata/docs/queryable.md b/packages/odata/docs/queryable.md new file mode 100644 index 000000000..6e19bafe7 --- /dev/null +++ b/packages/odata/docs/queryable.md @@ -0,0 +1,79 @@ +# @pnp/odata/queryable + +The Queryable class is the base class for all of the libraries building fluent request apis. + +## abstract class ODataQueryable + +This class takes a single type parameter represnting the type of the batch implementation object. If your api will not support batching +you can create a dummy class here and simply not use the batching calls. + +## properties + +### query + +Provides access to the query string builder for this url + +## public methods + +### concat + +Directly concatonates the supplied string to the current url, not normalizing "/" chars + +### configure + +Sets custom options for current object and all derived objects accessible via chaining + +```TypeScript +import { ConfigOptions } from "@pnp/odata"; +import { sp } from "@pnp/sp"; + +const headers: ConfigOptions = { + Accept: 'application/json;odata=nometadata' +}; + +// here we use configure to set the headers value for all child requests of the list instance +const list = sp.web.lists.getByTitle("List1").configure({ headers }); + +// this will use the values set in configure +list.items.get().then(items => console.log(JSON.stringify(items, null, 2)); +``` + +For reference the ConfigOptions interface is shown below: +```TypeScript +export interface ConfigOptions { + headers?: string[][] | { [key: string]: string } | Headers; + mode?: "navigate" | "same-origin" | "no-cors" | "cors"; + credentials?: "omit" | "same-origin" | "include"; + cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached"; +} +``` + +### configureFrom + +Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance. + +### usingCaching + +Enables caching for this request. See [caching](caching.md) for more details. + +```TypeScript +import { sp } from "@pnp/sp" + +sp.web.usingCaching().get().then(...); +``` + +### inBatch + +Adds this query to the supplied batch + +### toUrl + +Gets the currentl url + +### abstract toUrlAndQuery() + +When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request + +## get + +Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object intances. diff --git a/packages/odata/index.ts b/packages/odata/index.ts new file mode 100644 index 000000000..c3cd28998 --- /dev/null +++ b/packages/odata/index.ts @@ -0,0 +1 @@ +export * from "./src/odata"; diff --git a/packages/odata/package.json b/packages/odata/package.json new file mode 100644 index 000000000..35084f0d1 --- /dev/null +++ b/packages/odata/package.json @@ -0,0 +1,26 @@ +{ + "name": "@pnp/odata", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides shared odata functionality and base classes", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/logging": "0.0.0-PLACEHOLDER", + "@pnp/common": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/packages/odata/src/batch.ts b/packages/odata/src/batch.ts new file mode 100644 index 000000000..cfbce7c13 --- /dev/null +++ b/packages/odata/src/batch.ts @@ -0,0 +1,108 @@ +import { IFetchOptions, getGUID } from "@pnp/common"; +import { IODataParser } from "./parsers"; + +export interface ODataBatchRequestInfo { + url: string; + method: string; + options: IFetchOptions; + parser: IODataParser; + resolve: ((d: any) => void) | null; + reject: ((error: any) => void) | null; + id: string; +} + +export abstract class Batch { + + protected _deps: Promise[]; + protected _reqs: ODataBatchRequestInfo[]; + protected _rDeps: Promise[]; + + constructor(private _batchId = getGUID()) { + this._reqs = []; + this._deps = []; + this._rDeps = []; + } + + public get batchId(): string { + return this._batchId; + } + + /** + * The requests contained in this batch + */ + protected get requests(): ODataBatchRequestInfo[] { + return this._reqs; + } + + /** + * + * @param url Request url + * @param method Request method (GET, POST, etc) + * @param options Any request options + * @param parser The parser used to handle the eventual return from the query + * @param id An identifier used to track a request within a batch + */ + public add(url: string, method: string, options: IFetchOptions, parser: IODataParser, id: string): Promise { + + const info: ODataBatchRequestInfo = { + id, + method: method.toUpperCase(), + options, + parser, + reject: null, + resolve: null, + url, + }; + + const p = new Promise((resolve, reject) => { + info.resolve = resolve; + info.reject = reject; + }); + + this._reqs.push(info); + + return p; + } + + /** + * Adds a dependency insuring that some set of actions will occur before a batch is processed. + * MUST be cleared using the returned resolve delegate to allow batches to run + */ + public addDependency(): () => void { + + let resolver: () => void = () => void (0); + + this._deps.push(new Promise((resolve) => { + resolver = resolve; + })); + + return resolver; + } + + /** + * The batch's execute method will not resolve util any promises added here resolve + * + * @param p The dependent promise + */ + public addResolveBatchDependency(p: Promise): void { + this._rDeps.push(p); + } + + /** + * Execute the current batch and resolve the associated promises + * + * @returns A promise which will be resolved once all of the batch's child promises have resolved + */ + public execute(): Promise { + + // we need to check the dependencies twice due to how different engines handle things. + // We can get a second set of promises added during the first set resolving + return Promise.all(this._deps) + .then(() => Promise.all(this._deps)) + .then(() => this.executeImpl()) + .then(() => Promise.all(this._rDeps)) + .then(() => void (0)); + } + + protected abstract executeImpl(): Promise; +} diff --git a/packages/odata/src/caching.ts b/packages/odata/src/caching.ts new file mode 100644 index 000000000..669fb8dca --- /dev/null +++ b/packages/odata/src/caching.ts @@ -0,0 +1,42 @@ +import { IODataParser } from "./parsers"; +import { RuntimeConfig, dateAdd, IPnPClientStore, PnPClientStorage } from "@pnp/common"; + +export interface ICachingOptions { + expiration?: Date; + storeName?: "session" | "local"; + key: string; +} + +export class CachingOptions implements ICachingOptions { + + protected static storage = new PnPClientStorage(); + + constructor( + public key: string, + public storeName: "session" | "local" = RuntimeConfig.defaultCachingStore, + public expiration = dateAdd(new Date(), "second", RuntimeConfig.defaultCachingTimeoutSeconds)) { } + + public get store(): IPnPClientStore { + if (this.storeName === "local") { + return CachingOptions.storage.local; + } else { + return CachingOptions.storage.session; + } + } +} + +export class CachingParserWrapper implements IODataParser { + + constructor(public parser: IODataParser, public cacheOptions: CachingOptions) { } + + public parse(response: Response): Promise { + return this.parser.parse(response).then(r => this.cacheData(r)); + } + + protected cacheData(data: any): any { + if (this.cacheOptions.store !== null) { + this.cacheOptions.store.put(this.cacheOptions.key, data, this.cacheOptions.expiration); + } + return data; + } +} diff --git a/packages/odata/src/errors.ts b/packages/odata/src/errors.ts new file mode 100644 index 000000000..532b6b0f5 --- /dev/null +++ b/packages/odata/src/errors.ts @@ -0,0 +1,13 @@ +export class HttpRequestError extends Error { + + public isHttpRequestError = true; + + constructor(message: string, public response: Response, public status = response.status, public statusText = response.statusText) { + super(message); + } + + public static async init(r: Response): Promise { + const t = await r.clone().text(); + return new HttpRequestError(`Error making HttpClient request in queryable [${r.status}] ${r.statusText} ::> ${t}`, r.clone()); + } +} diff --git a/packages/odata/src/extenders.ts b/packages/odata/src/extenders.ts new file mode 100644 index 000000000..c8137296f --- /dev/null +++ b/packages/odata/src/extenders.ts @@ -0,0 +1,10 @@ +export function addProp(target: { prototype: any }, name: string, factory: (arg: U, p?: string) => T, path?: string): void { + + Reflect.defineProperty(target.prototype, name, { + configurable: true, + enumerable: true, + get: function (this: U) { + return factory(this, path); + }, + }); +} diff --git a/packages/odata/src/invokable.ts b/packages/odata/src/invokable.ts new file mode 100644 index 000000000..beb4bd3db --- /dev/null +++ b/packages/odata/src/invokable.ts @@ -0,0 +1,57 @@ +import { IQueryable } from "./queryable"; +import { RequestContext } from "./pipeline"; +import { IFetchOptions, RuntimeConfig } from "@pnp/common"; + +export type IHybrid> = T & { + (this: T, ...args: any[]): R; +}; + +export type IInvoker = (this: T, ...args: any[]) => R; + +export type IHybridConstructor = (...args: any[]) => IHybrid; + +const invokableBinder = (invoker: IInvoker) => (constructor: T): IHybridConstructor => { + + return (...args: any[]) => { + + const factory = (as: any[]) => { + const r = Object.assign(function (...ags: any[]) { return invoker.apply(r, ags); }, new constructor(...as)); + Reflect.setPrototypeOf(r, constructor.prototype); + return r; + }; + + if (RuntimeConfig.ie11) { + return factory(args); + } else { + return new Proxy>(factory(args), { + apply: (target: any, _thisArg: any, argArray?: any) => { + return Reflect.apply(target, _thisArg, argArray); + }, + get: (target: any, p: PropertyKey, receiver: any) => { + return Reflect.get(target, p, receiver); + }, + has: (target: any, p: PropertyKey) => { + return Reflect.has(target, p); + }, + set: (target: any, p: PropertyKey, value: any, receiver: any) => { + return Reflect.set(target, p, value, receiver); + }, + }); + } + }; +}; + +function defaultAction(this: IQueryable, options?: IFetchOptions): Promise { + return this.defaultAction(options); +} + +// @ts-ignore (reason: there is not a great way to describe the "this" type for this operation) +export const invokable = invokableBinder(defaultAction); + +export interface IGetable { + (options?: Partial>): Promise; +} + +export const invokableFactory = (f: { new(...args: any[]): T }) => (...args: any[]): T => { + return invokable(f)(...args); +}; diff --git a/packages/odata/src/odata.ts b/packages/odata/src/odata.ts new file mode 100644 index 000000000..ef3e1fae7 --- /dev/null +++ b/packages/odata/src/odata.ts @@ -0,0 +1,10 @@ +export * from "./batch"; +export * from "./caching"; +export * from "./errors"; +export * from "./extenders"; +export * from "./invokable"; +export * from "./operation-binder"; +export * from "./parsers"; +export * from "./pipeline"; +export * from "./queryable"; +export * from "./request-builders"; diff --git a/packages/odata/src/operation-binder.ts b/packages/odata/src/operation-binder.ts new file mode 100644 index 000000000..325b38beb --- /dev/null +++ b/packages/odata/src/operation-binder.ts @@ -0,0 +1,63 @@ +import { IODataParser, ODataParser } from "./parsers"; +import { IFetchOptions, IRequestClient, getGUID } from "@pnp/common"; +import { IQueryableData, cloneQueryableData } from "./queryable"; +import { PipelineMethod, pipe, getDefaultPipeline } from "./pipeline"; + +/** + * Methods which operate on queryables + */ + +export interface IRequestOptions extends IFetchOptions { + parser: IODataParser; +} + +export interface IClientFactoryBinder { + (clientFactory: () => IRequestClient): IMethodBinder; +} + +export interface IPipelineBinder { + (pipeline: PipelineMethod[]): IClientFactoryBinder; +} + +export interface IMethodBinder { + (method: string): IOperation; +} + +export interface IOperation { + (o: Partial>): Promise; +} + +export function operationBinder(pipes: PipelineMethod[]): IClientFactoryBinder { + + return function (clientFactory: () => IRequestClient): IMethodBinder { + + return function (method: string): IOperation { + + return function (o: Partial>): Promise { + + // send the IQueryableData down the pipeline + return pipe(Object.assign({}, { + batch: null, + batchDependency: null, + cachingOptions: null, + clientFactory, + cloneParentCacheOptions: null, + cloneParentWasCaching: false, + hasResult: false, + isBatched: typeof o.batch !== "undefined" && o.batch !== null, + method, + options: null, + parentUrl: "", + parser: new ODataParser(), + pipes: pipes.slice(0), + query: new Map(), + requestId: getGUID(), + url: "", + useCaching: /^get$/i.test(o.method) && o.useCaching, + }, cloneQueryableData(o))); + }; + }; + }; +} + +export const defaultPipelineBinder: IClientFactoryBinder = operationBinder(getDefaultPipeline()); diff --git a/packages/odata/src/parsers.ts b/packages/odata/src/parsers.ts new file mode 100644 index 000000000..55cd8b998 --- /dev/null +++ b/packages/odata/src/parsers.ts @@ -0,0 +1,110 @@ +import { isFunc, hOP } from "@pnp/common"; +import { HttpRequestError } from "./errors"; + +export interface IODataParser { + hydrate?: (d: any) => T; + parse(r: Response): Promise; +} + +export class ODataParser implements IODataParser { + + public parse(r: Response): Promise { + + return new Promise((resolve, reject) => { + if (this.handleError(r, reject)) { + this.parseImpl(r, resolve, reject); + } + }); + } + + protected parseImpl(r: Response, resolve: (value?: T | PromiseLike) => void, reject: (reason?: Error) => void): void { + if ((r.headers.has("Content-Length") && parseFloat(r.headers.get("Content-Length")!) === 0) || r.status === 204) { + resolve({}); + } else { + + // patch to handle cases of 200 response with no or whitespace only bodies (#487 & #545) + r.text() + .then(txt => txt.replace(/\s/ig, "").length > 0 ? JSON.parse(txt) : {}) + .then(json => resolve(this.parseODataJSON(json))) + .catch(e => reject(e)); + } + } + + /** + * Handles a response with ok === false by parsing the body and creating a ProcessHttpClientResponseException + * which is passed to the reject delegate. This method returns true if there is no error, otherwise false + * + * @param r Current response object + * @param reject reject delegate for the surrounding promise + */ + protected handleError(r: Response, reject: (err?: Error) => void): boolean { + if (!r.ok) { + HttpRequestError.init(r).then(reject); + } + + return r.ok; + } + + /** + * Normalizes the json response by removing the various nested levels + * + * @param json json object to parse + */ + protected parseODataJSON(json: any): U { + let result = json; + if (hOP(json, "d")) { + if (hOP(json.d, "results")) { + result = json.d.results; + } else { + result = json.d; + } + } else if (hOP(json, "value")) { + result = json.value; + } + return result; + } +} + +export class TextParser extends ODataParser { + + protected parseImpl(r: Response, resolve: (value: any) => void): void { + r.text().then(resolve); + } +} + +export class BlobParser extends ODataParser { + + protected parseImpl(r: Response, resolve: (value: any) => void): void { + r.blob().then(resolve); + } +} + +export class JSONParser extends ODataParser { + + protected parseImpl(r: Response, resolve: (value: any) => void): void { + r.json().then(resolve); + } +} + +export class BufferParser extends ODataParser { + + protected parseImpl(r: Response, resolve: (value: any) => void): void { + + if (isFunc(r.arrayBuffer)) { + r.arrayBuffer().then(resolve); + } else { + (r).buffer().then(resolve); + } + } +} + +export class LambdaParser extends ODataParser { + + constructor(private parser: (r: Response) => Promise) { + super(); + } + + protected parseImpl(r: Response, resolve: (value: any) => void): void { + this.parser(r).then(resolve); + } +} diff --git a/packages/odata/src/pipeline.ts b/packages/odata/src/pipeline.ts new file mode 100644 index 000000000..67ad44344 --- /dev/null +++ b/packages/odata/src/pipeline.ts @@ -0,0 +1,263 @@ +import { IRequestClient, extend, isFunc, hOP } from "@pnp/common"; +import { LogLevel, Logger } from "@pnp/logging"; +import { CachingOptions, CachingParserWrapper } from "./caching"; +import { IQueryableData } from "./queryable"; + +/** + * Defines the context for a given request to be processed in the pipeline + */ +export interface RequestContext extends IQueryableData { + result?: ReturnType; + clientFactory: () => IRequestClient; + hasResult: boolean; + isBatched: boolean; + requestId: string; + method: string; +} + +export type PipelineMethod = (c: RequestContext) => Promise>; + +/** + * Resolves the context's result value + * + * @param context The current context + */ +function returnResult(context: RequestContext): Promise { + + Logger.log({ + data: Logger.activeLogLevel === LogLevel.Verbose ? context.result : {}, + level: LogLevel.Info, + message: `[${context.requestId}] (${(new Date()).getTime()}) Returning result from pipeline. Set logging to verbose to see data.`, + }); + + return Promise.resolve(context.result!); +} + +/** + * Sets the result on the context + */ +export function setResult(context: RequestContext, value: any): Promise> { + + return new Promise>((resolve) => { + + context.result = value; + context.hasResult = true; + resolve(context); + }); +} + +/** + * Invokes the next method in the provided context's pipeline + * + * @param c The current request context + */ +function next(c: RequestContext): Promise> { + + if (c.pipes.length > 0) { + return c.pipes.shift()!(c); + } else { + return Promise.resolve(c); + } +} + +/** + * Executes the current request context's pipeline + * + * @param context Current context + */ +export function pipe(context: RequestContext): Promise { + + if (context.pipes.length < 1) { + Logger.write(`[${context.requestId}] (${(new Date()).getTime()}) Request pipeline contains no methods!`, LogLevel.Error); + throw Error("Request pipeline contains no methods!"); + } + + const promise = next(context).then(ctx => returnResult(ctx)).catch((e: Error) => { + Logger.error(e); + throw e; + }); + + if (context.isBatched) { + // this will block the batch's execute method from returning until the child requets have been resolved + context.batch.addResolveBatchDependency(promise); + } + + return promise; +} + +/** + * decorator factory applied to methods in the pipeline to control behavior + */ +export function requestPipelineMethod(alwaysRun = false) { + + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + + const method = descriptor.value; + + descriptor.value = function (...args: any[]) { + + // if we have a result already in the pipeline, pass it along and don't call the tagged method + if (!alwaysRun && args.length > 0 && hOP(args[0], "hasResult") && args[0].hasResult) { + Logger.write(`[${args[0].requestId}] (${(new Date()).getTime()}) Skipping request pipeline method ${propertyKey}, existing result in pipeline.`, LogLevel.Verbose); + return Promise.resolve(args[0]); + } + + // apply the tagged method + Logger.write(`[${args[0].requestId}] (${(new Date()).getTime()}) Calling request pipeline method ${propertyKey}.`, LogLevel.Verbose); + + // then chain the next method in the context's pipeline - allows for dynamic pipeline + return method.apply(target, args).then((ctx: RequestContext) => next(ctx)); + }; + }; +} + +/** + * Contains the methods used within the request pipeline + */ +export class PipelineMethods { + + /** + * Logs the start of the request + */ + @requestPipelineMethod(true) + public static logStart(context: RequestContext): Promise> { + return new Promise>(resolve => { + + Logger.log({ + data: Logger.activeLogLevel === LogLevel.Info ? {} : context, + level: LogLevel.Info, + message: `[${context.requestId}] (${(new Date()).getTime()}) Beginning ${context.method} request (${context.url})`, + }); + + resolve(context); + }); + } + + /** + * Handles caching of the request + */ + @requestPipelineMethod() + public static caching(context: RequestContext): Promise> { + + return new Promise>(resolve => { + + // handle caching, if applicable + if (context.useCaching) { + + Logger.write(`[${context.requestId}] (${(new Date()).getTime()}) Caching is enabled for request, checking cache...`, LogLevel.Info); + + let cacheOptions = new CachingOptions(context.url.toLowerCase()); + if (context.cachingOptions !== undefined) { + cacheOptions = extend(cacheOptions, context.cachingOptions); + } + + // we may not have a valid store + if (cacheOptions.store !== null) { + // check if we have the data in cache and if so resolve the promise and return + let data = cacheOptions.store.get(cacheOptions.key); + if (data !== null) { + // ensure we clear any held batch dependency we are resolving from the cache + Logger.log({ + data: Logger.activeLogLevel === LogLevel.Info ? {} : data, + level: LogLevel.Info, + message: `[${context.requestId}] (${(new Date()).getTime()}) Value returned from cache.`, + }); + if (isFunc(context.batchDependency)) { + context.batchDependency(); + } + // handle the case where a parser needs to take special actions with a cached result + if (hOP(context.parser, "hydrate")) { + data = context.parser.hydrate(data); + } + return setResult(context, data).then(ctx => resolve(ctx)); + } + } + + Logger.write(`[${context.requestId}] (${(new Date()).getTime()}) Value not found in cache.`, LogLevel.Info); + + // if we don't then wrap the supplied parser in the caching parser wrapper + // and send things on their way + context.parser = new CachingParserWrapper(context.parser, cacheOptions); + } + + return resolve(context); + }); + } + + /** + * Sends the request + */ + @requestPipelineMethod() + public static send(context: RequestContext): Promise> { + + return new Promise>((resolve, reject) => { + // send or batch the request + if (context.isBatched) { + + // we are in a batch, so add to batch, remove dependency, and resolve with the batch's promise + const p = context.batch.add(context.url, context.method, context.options, context.parser, context.requestId); + + // we release the dependency here to ensure the batch does not execute until the request is added to the batch + if (isFunc(context.batchDependency)) { + context.batchDependency(); + } + + Logger.write(`[${context.requestId}] (${(new Date()).getTime()}) Batching request in batch ${context.batch.batchId}.`, LogLevel.Info); + + // we set the result as the promise which will be resolved by the batch's execution + resolve(setResult(context, p)); + + } else { + + Logger.write(`[${context.requestId}] (${(new Date()).getTime()}) Sending request.`, LogLevel.Info); + + // we are not part of a batch, so proceed as normal + const client = context.clientFactory(); + const opts = extend(context.options || {}, { method: context.method }); + client.fetch(context.url, opts) + .then(response => context.parser.parse(response)) + .then(result => setResult(context, result)) + .then(ctx => resolve(ctx)) + .catch(e => reject(e)); + } + }); + } + + /** + * Logs the end of the request + */ + @requestPipelineMethod(true) + public static logEnd(context: RequestContext): Promise> { + + return new Promise>(resolve => { + + if (context.isBatched) { + + Logger.log({ + data: Logger.activeLogLevel === LogLevel.Info ? {} : context, + level: LogLevel.Info, + message: `[${context.requestId}] (${(new Date()).getTime()}) ${context.method} request will complete in batch ${context.batch.batchId}.`, + }); + + } else { + + Logger.log({ + data: Logger.activeLogLevel === LogLevel.Info ? {} : context, + level: LogLevel.Info, + message: `[${context.requestId}] (${(new Date()).getTime()}) Completing ${context.method} request.`, + }); + } + + resolve(context); + }); + } +} + +export function getDefaultPipeline() { + return [ + PipelineMethods.logStart, + PipelineMethods.caching, + PipelineMethods.send, + PipelineMethods.logEnd, + ].slice(0); +} diff --git a/packages/odata/src/queryable.ts b/packages/odata/src/queryable.ts new file mode 100644 index 000000000..fcb066c09 --- /dev/null +++ b/packages/odata/src/queryable.ts @@ -0,0 +1,309 @@ +import { + combine, + RuntimeConfig, + IFetchOptions, + IConfigOptions, + mergeOptions, + objectDefinedNotNull, + IRequestClient, +} from "@pnp/common"; +import { ICachingOptions } from "./caching"; +import { Batch } from "./batch"; +import { PipelineMethod } from "./pipeline"; +import { IODataParser, ODataParser } from "./parsers"; + +export function cloneQueryableData(source: Partial): Partial { + + const s = JSON.stringify(source, (key: string, value: any) => { + + switch (key) { + case "query": + return JSON.stringify([...(>value)]); + case "batch": + return "-"; + case "batchDependency": + return "-"; + case "cachingOptions": + return "-"; + case "clientFactory": + return "-"; + case "parser": + return "-"; + default: + return value; + } + }, 0); + + return JSON.parse(s, (key: any, value: any) => { + switch (key) { + case "query": + return new Map(JSON.parse(value)); + case "batch": + return source.batch; + case "batchDependency": + return source.batchDependency; + case "cachingOptions": + return source.cachingOptions; + case "clientFactory": + return source.clientFactory; + case "parser": + return source.parser; + default: + return value; + } + }); +} + +export interface IQueryableData { + batch: Batch | null; + batchDependency: () => void | null; + cachingOptions: ICachingOptions | null; + cloneParentCacheOptions: ICachingOptions | null; + cloneParentWasCaching: boolean; + query: Map; + options: IFetchOptions | null; + url: string; + parentUrl: string; + useCaching: boolean; + pipes?: PipelineMethod[]; + parser?: IODataParser; + clientFactory?: () => IRequestClient; + method?: string; +} + +export interface IQueryable { + data: Partial>; + query: Map; + append(pathPart: string): void; + inBatch(batch: Batch): this; + addBatchDependency(): () => void; + toUrlAndQuery(): string; + toUrl(): string; + concat(pathPart: string): this; + configure(options: IConfigOptions): this; + configureFrom(o: IQueryable): this; + usingCaching(options?: ICachingOptions): this; + usingParser(parser: IODataParser): this; + withPipeline(pipeline: PipelineMethod[]): this; + defaultAction(options?: IFetchOptions): Promise; +} + +export abstract class Queryable implements IQueryable { + + private _data: Partial>; + + constructor(dataSeed: Partial> = {}) { + + this._data = Object.assign({}, { + cloneParentWasCaching: false, + options: {}, + parentUrl: "", + parser: new ODataParser(), + query: new Map(), + url: "", + useCaching: false, + }, cloneQueryableData(dataSeed)); + } + + public get data(): Partial> { + return this._data; + } + + public set data(value: Partial>) { + this._data = Object.assign({}, cloneQueryableData(this.data), cloneQueryableData(value)); + } + + /** + * Gets the full url with query information + * + */ + public abstract toUrlAndQuery(): string; + + /** + * The default action for this + */ + public abstract defaultAction(options?: IFetchOptions): Promise; + + /** + * Gets the currentl url + * + */ + public toUrl(): string { + return this.data.url; + } + + /** + * Directly concatonates the supplied string to the current url, not normalizing "/" chars + * + * @param pathPart The string to concatonate to the url + */ + public concat(pathPart: string): this { + this.data.url += pathPart; + return this; + } + + /** + * Provides access to the query builder for this url + * + */ + public get query(): Map { + return this.data.query; + } + + /** + * Sets custom options for current object and all derived objects accessible via chaining + * + * @param options custom options + */ + public configure(options: IConfigOptions): this { + mergeOptions(this.data.options, options); + return this; + } + + /** + * Configures this instance from the configure options of the supplied instance + * + * @param o Instance from which options should be taken + */ + public configureFrom(o: IQueryable): this { + mergeOptions(this.data.options, o.data.options); + return this; + } + + /** + * Enables caching for this request + * + * @param options Defines the options used when caching this request + */ + public usingCaching(options?: ICachingOptions): this { + if (!RuntimeConfig.globalCacheDisable) { + this.data.useCaching = true; + if (options !== undefined) { + this.data.cachingOptions = options; + } + } + return this; + } + + public usingParser(parser: IODataParser): this { + this.data.parser = parser; + return this; + } + + /** + * Allows you to set a request specific processing pipeline + * + * @param pipeline The set of methods, in order, to execute a given request + */ + public withPipeline(pipeline: PipelineMethod[]): this { + this.data.pipes = pipeline.slice(0); + return this; + } + + /** + * Appends the given string and normalizes "/" chars + * + * @param pathPart The string to append + */ + public append(pathPart: string): void { + this.data.url = combine(this.data.url, pathPart); + } + + /** + * Adds this query to the supplied batch + * + * @example + * ``` + * + * let b = pnp.sp.createBatch(); + * pnp.sp.web.inBatch(b).get().then(...); + * b.execute().then(...) + * ``` + */ + public inBatch(batch: Batch): this { + + if (this.batch !== null) { + throw Error("This query is already part of a batch."); + } + + if (objectDefinedNotNull(batch)) { + this.data.batch = batch; + } + + return this; + } + + /** + * Blocks a batch call from occuring, MUST be cleared by calling the returned function + */ + public addBatchDependency(): () => void { + if (this.data.batch !== null) { + return this.data.batch.addDependency(); + } + + return () => null; + } + + /** + * Indicates if the current query has a batch associated + * + */ + protected get hasBatch(): boolean { + return objectDefinedNotNull(this.data.batch); + } + + /** + * The batch currently associated with this query or null + * + */ + protected get batch(): Batch | null { + return this.hasBatch ? this.data.batch : null; + } + + /** + * Gets the parent url used when creating this instance + * + */ + protected get parentUrl(): string { + return this.data.parentUrl; + } + + /** + * Extends this queryable from the provided parent + * + * @param parent Parent queryable from which we will derive a base url + * @param path Additional path + */ + protected extend(parent: IQueryable, path?: string) { + this.data.parentUrl = parent.data.url; + this.data.url = combine(this.data.parentUrl, path || ""); + this.configureFrom(parent); + } + + /** + * Clones this instance's data to target + * + * @param target Instance to which data is written + * @param settings [Optional] Settings controlling how clone is applied + */ + protected cloneTo>(target: T, settings: { includeBatch: boolean } = { includeBatch: true }): T { + + target.data = Object.assign({}, cloneQueryableData(this.data), { + cloneParentCacheOptions: null, + cloneParentWasCaching: false, + }, cloneQueryableData(target.data)); + + target.configureFrom(this); + + if (settings.includeBatch) { + target.inBatch(this.batch); + } + + if (this.data.useCaching) { + target.data.cloneParentWasCaching = true; + target.data.cloneParentCacheOptions = this.data.cachingOptions; + } + + return target; + } +} diff --git a/packages/odata/src/request-builders.ts b/packages/odata/src/request-builders.ts new file mode 100644 index 000000000..12b966535 --- /dev/null +++ b/packages/odata/src/request-builders.ts @@ -0,0 +1,9 @@ +import { jsS, TypedHash } from "@pnp/common"; + +export function body(o: U, previous?: T): T & { body: string } { + return Object.assign({ body: jsS(o) }, previous); +} + +export function headers = {}>(o: U, previous?: T): T & { headers: U } { + return Object.assign({ headers: o }, previous); +} diff --git a/packages/odata/tsconfig.es5.json b/packages/odata/tsconfig.es5.json new file mode 100644 index 000000000..1e4f3465d --- /dev/null +++ b/packages/odata/tsconfig.es5.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.es5.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/odata/tsconfig.json b/packages/odata/tsconfig.json new file mode 100644 index 000000000..05c360eb6 --- /dev/null +++ b/packages/odata/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + } + ] +} \ No newline at end of file diff --git a/packages/pnpjs/docs/index.md b/packages/pnpjs/docs/index.md new file mode 100644 index 000000000..ed299d2fb --- /dev/null +++ b/packages/pnpjs/docs/index.md @@ -0,0 +1,59 @@ +# @pnp/pnpjs + +[![npm version](https://badge.fury.io/js/%40pnp%2Fpnpjs.svg)](https://badge.fury.io/js/%40pnp%2Fpnpjs) + +The pnpjs library is a rollup of the core libraries across the @pnp scope and is designed only as a bridge to help folks transition from sp-pnp-js, primarily +in scenarios where a single file is being imported via a script tag. **It is recommended to not use this rollup library where possible and [migrate to the +individual libraries](../../documentation/transition-guide.md)**. + +## Getting Started + +There are two approaches to using this libary: the first is to import, the second is to manually extract the bundled file for use in your project. + +### Install + +`npm install @pnp/pnpjs --save` + +You can then make use of the pnpjs rollup library within your application. It's structure matches sp-pnp-js, though some things may have changed based on the rolled-up dependencies. + +```TypeScript +import pnp from "@pnp/pnpjs"; + +pnp.sp.web.get().then(w => { + + console.log(JSON.stringify(w, null, 4)); +}); +``` + +### Grab Bundle File + +This method is useful if you are primarily working within a script editor web part or similar case where you are not using a build pipeline to bundle your application. + +Install only this library. + +`npm install @pnp/pnpjs` + +Browse to _./node_modules/@pnp/pnpjs/dist_ and grab either _pnpjs.es5.umd.bundle.js_ or _pnpjs.es5.umd.bundle.min.js_ depending on your needs. You can then add a script tag referencing this file and you will have a global variable "pnp". + +For example you could paste the following into a script editor web part: + +```HTML +

    Script Editor is on page.

    + + +``` + +Alternatively to serve the script from the project at "https://localhost:8080/assets/pnp.js" you can use: + +`gulp serve --p pnpjs` + +This will allow you to test your changes to the entire bundle live while making updates. diff --git a/packages/pnpjs/index.ts b/packages/pnpjs/index.ts new file mode 100644 index 000000000..d0e7d36e8 --- /dev/null +++ b/packages/pnpjs/index.ts @@ -0,0 +1,5 @@ +export * from "./src/pnpjs"; + +import pnp from "./src/pnpjs"; + +export default pnp; diff --git a/packages/pnpjs/package.json b/packages/pnpjs/package.json new file mode 100644 index 000000000..5538365df --- /dev/null +++ b/packages/pnpjs/package.json @@ -0,0 +1,29 @@ +{ + "name": "@pnp/pnpjs", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - rollup library of core functionality (mimics sp-pnp-js)", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "@pnp/logging": "0.0.0-PLACEHOLDER", + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/odata": "0.0.0-PLACEHOLDER", + "@pnp/sp": "0.0.0-PLACEHOLDER", + "@pnp/graph": "0.0.0-PLACEHOLDER", + "@pnp/config-store": "0.0.0-PLACEHOLDER", + "@pnp/sp-addinhelpers": "0.0.0-PLACEHOLDER", + "tslib": "1.9.3" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/packages/pnpjs/src/config/pnplibconfig.ts b/packages/pnpjs/src/config/pnplibconfig.ts new file mode 100644 index 000000000..c47bfcec8 --- /dev/null +++ b/packages/pnpjs/src/config/pnplibconfig.ts @@ -0,0 +1,9 @@ +import { LibraryConfiguration, RuntimeConfig } from "@pnp/common"; +import { SPConfigurationPart } from "@pnp/sp"; +import { GraphConfigurationPart } from "@pnp/graph"; + +export interface PnPConfiguration extends LibraryConfiguration, SPConfigurationPart, GraphConfigurationPart { } + +export function setup(config: PnPConfiguration): void { + RuntimeConfig.extend(config); +} diff --git a/packages/pnpjs/src/pnpjs.ts b/packages/pnpjs/src/pnpjs.ts new file mode 100644 index 000000000..e54692b31 --- /dev/null +++ b/packages/pnpjs/src/pnpjs.ts @@ -0,0 +1,125 @@ +import { Logger } from "@pnp/logging"; +import { + PnPClientStorage, + dateAdd, + combine, + getCtxCallback, + getRandomString, + getGUID, + isFunc, + objectDefinedNotNull, + isArray, + extend, + isUrlAbsolute, + stringIsNullOrEmpty, + getAttrValueFromString, + sanitizeGuid, +} from "@pnp/common"; +import { Settings } from "@pnp/config-store"; +import { GraphRest, graph as _graph } from "@pnp/graph"; +import { sp as _sp, SPRestAddIn } from "@pnp/sp-addinhelpers"; +import { setup as _setup, PnPConfiguration } from "./config/pnplibconfig"; + +/** + * Root class of the Patterns and Practices namespace, provides an entry point to the library + */ + +/** + * Re-export everything from the dependencies to match the previous pattern + */ +export * from "@pnp/sp"; +export * from "@pnp/graph"; +export * from "@pnp/common"; +export * from "@pnp/logging"; +export * from "@pnp/config-store"; +export * from "@pnp/odata"; + +/** + * Utility methods + */ +export const util = { + combine, + dateAdd, + extend, + getAttrValueFromString, + getCtxCallback, + getGUID, + getRandomString, + isArray, + isFunc, + isUrlAbsolute, + objectDefinedNotNull, + sanitizeGuid, + stringIsNullOrEmpty, +}; + +/** + * Provides access to the SharePoint REST interface + */ +export const sp = _sp; + +/** + * Provides access to the Microsoft Graph REST interface + */ +export const graph = _graph; + +/** + * Provides access to local and session storage + */ +export const storage: PnPClientStorage = new PnPClientStorage(); + +/** + * Global configuration instance to which providers can be added + */ +export const config = new Settings(); + +/** + * Global logging instance to which subscribers can be registered and messages written + */ +export const log = Logger; + +/** + * Allows for the configuration of the library + */ +export const setup: (config: PnPConfiguration) => void = _setup; + +// /** +// * Expose a subset of classes from the library for public consumption +// */ + +// creating this class instead of directly assigning to default fixes issue #116 +const Def = { + /** + * Global configuration instance to which providers can be added + */ + config: config, + /** + * Provides access to the Microsoft Graph REST interface + */ + graph: graph, + /** + * Global logging instance to which subscribers can be registered and messages written + */ + log: log, + /** + * Provides access to local and session storage + */ + setup: setup, + /** + * Provides access to the REST interface + */ + sp: sp, + /** + * Provides access to local and session storage + */ + storage: storage, + /** + * Utility methods + */ + util: util, +}; + +/** + * Enables use of the import pnp from syntax + */ +export default Def; diff --git a/packages/pnpjs/tsconfig.es5.json b/packages/pnpjs/tsconfig.es5.json new file mode 100644 index 000000000..58c787ff7 --- /dev/null +++ b/packages/pnpjs/tsconfig.es5.json @@ -0,0 +1,49 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "types": [ + "sharepoint" + ] + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts", + "../graph/index.ts", + "../graph/src/**/*.ts", + "../config-store/index.ts", + "../config-store/src/**/*.ts", + "../sp-addinhelpers/index.ts", + "../sp-addinhelpers/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + }, + { + "path": "../odata/tsconfig.es5.json" + }, + { + "path": "../sp/tsconfig.es5.json" + }, + { + "path": "../graph/tsconfig.es5.json" + }, + { + "path": "../config-store/tsconfig.es5.json" + }, + { + "path": "../sp-addinhelpers/tsconfig.es5.json" + } + ] +} diff --git a/packages/pnpjs/tsconfig.json b/packages/pnpjs/tsconfig.json new file mode 100644 index 000000000..5e4847698 --- /dev/null +++ b/packages/pnpjs/tsconfig.json @@ -0,0 +1,49 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [ + "sharepoint" + ] + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts", + "../graph/index.ts", + "../graph/src/**/*.ts", + "../config-store/index.ts", + "../config-store/src/**/*.ts", + "../sp-addinhelpers/index.ts", + "../sp-addinhelpers/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + }, + { + "path": "../odata" + }, + { + "path": "../sp" + }, + { + "path": "../graph" + }, + { + "path": "../config-store" + }, + { + "path": "../sp-addinhelpers" + } + ] +} diff --git a/packages/readme.md b/packages/readme.md new file mode 100644 index 000000000..9976c0cab --- /dev/null +++ b/packages/readme.md @@ -0,0 +1,22 @@ +![SharePoint Patterns and Practices](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) + +The SharePoint Patterns and Practices client side libraries were created to help enable developers to do their best work, without worrying about the exact +REST api details. Built with feedback from the community they represent a way to simplify your day-to-day dev cycle while relying on tested and proven +patterns. + +Please use [http://aka.ms/sppnp](http://aka.ms/sppnp) for the latest updates around the whole *SharePoint Patterns and Practices (PnP) initiative*. + +## Source & Docs + +This code is managed within the [main pnp repo](https://github.com/pnp/pnp), please report issues and submit pull requests there. + +Please see the public site for [package documentation](https://pnp.github.io/pnpjs/). + +### Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### "Sharing is Caring" + +### Disclaimer +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** \ No newline at end of file diff --git a/packages/sp-addinhelpers/docs/index.md b/packages/sp-addinhelpers/docs/index.md new file mode 100644 index 000000000..48aa4ff4f --- /dev/null +++ b/packages/sp-addinhelpers/docs/index.md @@ -0,0 +1,46 @@ +# @pnp/sp-addinhelpers + +[![npm version](https://badge.fury.io/js/%40pnp%2Fsp-addinhelpers.svg)](https://badge.fury.io/js/%40pnp%2Fsp-addinhelpers) + +This module contains classes to allow use of the libraries within a SharePoint add-in. + +## Getting Started + +Install the library and all dependencies, + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/sp-addinhelpers --save` + +Now you can make requests to the host web from your add-in using the crossDomainWeb method. + +```TypeScript +// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers"; + +// this only needs to be done once within your application +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPRequestExecutorClient(); + } + } +}); + +// now we need to use the crossDomainWeb method to make our requests to the host web +const addInWenUrl = "{The add-in web url, likely from the query string}"; +const hostWebUrl = "{The host web url, likely from the query string}"; + +// make requests into the host web via the SP.RequestExecutor +sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +``` + +#Libary Topics + +* [SPRequestExecutorClient](sp-request-executor-client.md) +* [SPRestAddIn](sp-rest-addin.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-sp-addinhelpers-uml.svg) + +Graphical UML diagram of @pnp/sp-addinhelpers. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/sp-addinhelpers/docs/sp-request-executor-client.md b/packages/sp-addinhelpers/docs/sp-request-executor-client.md new file mode 100644 index 000000000..7e28a94e3 --- /dev/null +++ b/packages/sp-addinhelpers/docs/sp-request-executor-client.md @@ -0,0 +1,32 @@ +# @pnp/sp-addinhelpers/sprequestexecutorclient + +The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on +the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request. + +## Setup + +To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a +SharePoint add-in web. + +```TypeScript +// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers"; + +// this only needs to be done once within your application +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPRequestExecutorClient(); + } + } +}); + +// now we need to use the crossDomainWeb method to make our requests to the host web +const addInWenUrl = "{The add-in web url, likely from the query string}"; +const hostWebUrl = "{The host web url, likely from the query string}"; + +// make requests into the host web via the SP.RequestExecutor +sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +``` diff --git a/packages/sp-addinhelpers/docs/sp-rest-addin.md b/packages/sp-addinhelpers/docs/sp-rest-addin.md new file mode 100644 index 000000000..526a7239d --- /dev/null +++ b/packages/sp-addinhelpers/docs/sp-rest-addin.md @@ -0,0 +1,26 @@ +# @pnp/sp-addinhelpers/sprestaddin + +This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls + +```TypeScript +// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers"; + +// this only needs to be done once within your application +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPRequestExecutorClient(); + } + } +}); + +// now we need to use the crossDomainWeb method to make our requests to the host web +const addInWenUrl = "{The add-in web url, likely from the query string}"; +const hostWebUrl = "{The host web url, likely from the query string}"; + +// make requests into the host web via the SP.RequestExecutor +sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +``` diff --git a/packages/sp-addinhelpers/index.ts b/packages/sp-addinhelpers/index.ts new file mode 100644 index 000000000..ead425e66 --- /dev/null +++ b/packages/sp-addinhelpers/index.ts @@ -0,0 +1 @@ +export * from "./src/addinhelpers"; diff --git a/packages/sp-addinhelpers/package.json b/packages/sp-addinhelpers/package.json new file mode 100644 index 000000000..07d072517 --- /dev/null +++ b/packages/sp-addinhelpers/package.json @@ -0,0 +1,27 @@ +{ + "name": "@pnp/sp-addinhelpers", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides functionality for working within SharePoint add-ins", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3", + "@types/sharepoint": "2016.1.2" + }, + "peerDependencies": { + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/sp": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} diff --git a/packages/sp-addinhelpers/src/addinhelpers.ts b/packages/sp-addinhelpers/src/addinhelpers.ts new file mode 100644 index 000000000..8afca0e51 --- /dev/null +++ b/packages/sp-addinhelpers/src/addinhelpers.ts @@ -0,0 +1,2 @@ +export * from "./sprequestexecutorclient"; +export * from "./sprestaddin"; diff --git a/packages/sp-addinhelpers/src/sprequestexecutorclient.ts b/packages/sp-addinhelpers/src/sprequestexecutorclient.ts new file mode 100644 index 000000000..1c2210377 --- /dev/null +++ b/packages/sp-addinhelpers/src/sprequestexecutorclient.ts @@ -0,0 +1,79 @@ +import { extend, IHttpClientImpl } from "@pnp/common"; + +/** + * Makes requests using the SP.RequestExecutor library. + */ +export class SPRequestExecutorClient implements IHttpClientImpl { + /** + * Fetches a URL using the SP.RequestExecutor library. + */ + public fetch(url: string, options: any): Promise { + if (SP === undefined || SP.RequestExecutor === undefined) { + throw Error("SP.RequestExecutor is undefined. Load the SP.RequestExecutor.js library (/_layouts/15/SP.RequestExecutor.js) before loading the PnP JS Core library."); + } + + const addinWebUrl = url.substring(0, url.indexOf("/_api")), + executor = new SP.RequestExecutor(addinWebUrl); + + let headers: { [key: string]: string; } = {}, + iterator: IterableIterator<[string, string]>, + temp: IteratorResult<[string, string]>; + + if (options.headers && options.headers instanceof Headers) { + iterator = >options.headers.entries(); + temp = iterator.next(); + while (!temp.done) { + headers[temp.value[0]] = temp.value[1]; + temp = iterator.next(); + } + } else { + headers = options.headers; + } + + return new Promise((resolve, reject) => { + + let requestOptions = { + error: (error: SP.ResponseInfo) => { + reject(this.convertToResponse(error)); + }, + headers: headers, + method: options.method, + success: (response: SP.ResponseInfo) => { + resolve(this.convertToResponse(response)); + }, + url: url, + }; + + if (options.body) { + requestOptions = extend(requestOptions, { body: options.body }); + } else { + requestOptions = extend(requestOptions, { binaryStringRequestBody: true }); + } + executor.executeAsync(requestOptions); + }); + } + + /** + * Converts a SharePoint REST API response to a fetch API response. + */ + private convertToResponse = (spResponse: SP.ResponseInfo): Response => { + const responseHeaders = new Headers(); + + if (spResponse.headers !== undefined) { + for (const h in spResponse.headers) { + if (spResponse.headers[h]) { + responseHeaders.append(h, spResponse.headers[h]); + } + } + } + + // Cannot have an empty string body when creating a Response with status 204 + const body = spResponse.statusCode === 204 ? null : spResponse.body; + + return new Response(body, { + headers: responseHeaders, + status: spResponse.statusCode, + statusText: spResponse.statusText, + }); + } +} diff --git a/packages/sp-addinhelpers/src/sprestaddin.ts b/packages/sp-addinhelpers/src/sprestaddin.ts new file mode 100644 index 000000000..db576c337 --- /dev/null +++ b/packages/sp-addinhelpers/src/sprestaddin.ts @@ -0,0 +1,67 @@ +import { + SPRest, +} from "@pnp/sp"; + +import { Web, IWeb } from "@pnp/sp/src/webs"; +import { Site, ISite } from "@pnp/sp/src/sites"; + +import { + isUrlAbsolute, + combine, +} from "@pnp/common"; + +import { ISharePointQueryable } from "@pnp/sp"; + +export class SPRestAddIn extends SPRest { + + /** + * Begins a cross-domain, host site scoped REST request, for use in add-in webs + * + * @param addInWebUrl The absolute url of the add-in web + * @param hostWebUrl The absolute url of the host web + */ + public crossDomainSite(addInWebUrl: string, hostWebUrl: string): ISite { + return this._cdImpl(Site, addInWebUrl, hostWebUrl, "site"); + } + + /** + * Begins a cross-domain, host web scoped REST request, for use in add-in webs + * + * @param addInWebUrl The absolute url of the add-in web + * @param hostWebUrl The absolute url of the host web + */ + public crossDomainWeb(addInWebUrl: string, hostWebUrl: string): IWeb { + return this._cdImpl(Web, addInWebUrl, hostWebUrl, "web"); + } + + /** + * Implements the creation of cross domain REST urls + * + * @param factory The constructor of the object to create Site | Web + * @param addInWebUrl The absolute url of the add-in web + * @param hostWebUrl The absolute url of the host web + * @param urlPart String part to append to the url "site" | "web" + */ + private _cdImpl( + factory: (...args: any[]) => T, + addInWebUrl: string, + hostWebUrl: string, + urlPart: string): T { + + if (!isUrlAbsolute(addInWebUrl)) { + throw Error("The addInWebUrl parameter must be an absolute url."); + } + + if (!isUrlAbsolute(hostWebUrl)) { + throw Error("The hostWebUrl parameter must be an absolute url."); + } + + const url = combine(addInWebUrl, "_api/SP.AppContextSite(@target)"); + + const instance = factory(url, urlPart); + instance.query.set("@target", "'" + encodeURIComponent(hostWebUrl) + "'"); + return instance.configure(this._options); + } +} + +export const sp = new SPRestAddIn(); diff --git a/packages/sp-addinhelpers/tsconfig.es5.json b/packages/sp-addinhelpers/tsconfig.es5.json new file mode 100644 index 000000000..f95b98256 --- /dev/null +++ b/packages/sp-addinhelpers/tsconfig.es5.json @@ -0,0 +1,28 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "types": [ + "sharepoint" + ] + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../sp/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/sp-addinhelpers/tsconfig.json b/packages/sp-addinhelpers/tsconfig.json new file mode 100644 index 000000000..7cf2616a9 --- /dev/null +++ b/packages/sp-addinhelpers/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [ + "sharepoint" + ] + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../sp" + } + ] +} \ No newline at end of file diff --git a/packages/sp-clientsvc/docs/index.md b/packages/sp-clientsvc/docs/index.md new file mode 100644 index 000000000..70755be7d --- /dev/null +++ b/packages/sp-clientsvc/docs/index.md @@ -0,0 +1,8 @@ +# @pnp/sp-clientsvc + +This library provides base classes for working with the legacy SharePoint client.svc/ProcessQuery endpoint. The base classes support most of the possibilities for types of query calls, as well as supporting fluent batching and caching. They are based on the same @pnp/odata foundation as the other libraries so should feel familiar when extending. You can see [@pnp/sp-taxonomy](../../sp-taxonomy/docs/index.md) for an example showing how to extend these base classes into a functional fluent model. + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-sp-clientsvc-uml.svg) + +Graphical UML diagram of @pnp/sp-clientsvc. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/sp-clientsvc/index.ts b/packages/sp-clientsvc/index.ts new file mode 100644 index 000000000..593b7953c --- /dev/null +++ b/packages/sp-clientsvc/index.ts @@ -0,0 +1 @@ +export * from "./src/clientsvc"; diff --git a/packages/sp-clientsvc/package.json b/packages/sp-clientsvc/package.json new file mode 100644 index 000000000..848a18587 --- /dev/null +++ b/packages/sp-clientsvc/package.json @@ -0,0 +1,28 @@ +{ + "name": "@pnp/sp-clientsvc", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - Provides core functionality to interact with the legacy client.svc SharePoint endpoint", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/logging": "0.0.0-PLACEHOLDER", + "@pnp/odata": "0.0.0-PLACEHOLDER", + "@pnp/sp": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/packages/sp-clientsvc/src/batch.ts b/packages/sp-clientsvc/src/batch.ts new file mode 100644 index 000000000..a4a6c0c82 --- /dev/null +++ b/packages/sp-clientsvc/src/batch.ts @@ -0,0 +1,196 @@ +import { LogLevel, Logger } from "@pnp/logging"; +import { CachingParserWrapper, Batch, ODataBatchRequestInfo } from "@pnp/odata"; +import { ClientSvcQueryable } from "./clintsvcqueryable"; +import { ObjectPath, ObjectPathQueue, opSetId, opSetParentId, opSetPathId, opSetPathParamId } from "./objectpath"; +import { objectPath } from "./opactionbuilders"; +import { staticMethod } from "./opbuilders"; +import { ProcessQueryParser } from "./parsers"; +import { writeObjectPathBody } from "./utils"; + +export interface IObjectPathBatch extends Batch { + +} + +/** + * Implements ODataBatch for use with the ObjectPath framework + */ +export class ObjectPathBatch extends Batch implements IObjectPathBatch { + + constructor(protected parentUrl: string, _batchId?: string) { + super(_batchId); + } + + protected executeImpl(): Promise { + + // if we don't have any requests, don't bother sending anything + // this could be due to caching further upstream, or just an empty batch + if (this.requests.length < 1) { + Logger.write(`Resolving empty batch.`, LogLevel.Info); + return Promise.resolve(); + } + + const executor = new BatchExecutor(this.parentUrl, this.batchId); + executor.appendRequests(this.requests); + return executor.execute(); + } +} + +class BatchExecutor extends ClientSvcQueryable { + + private _builderIndex: number; + private _requests: ODataBatchRequestInfo[]; + + constructor(parentUrl: string, public batchId: string) { + super(parentUrl); + + this._requests = []; + this._builderIndex = 1; + + // we add our session object path and hard code in the IDs so we can reference it + const method = staticMethod("GetTaxonomySession", "{981cbc68-9edc-4f8d-872f-71146fcbb84f}"); + method.path = opSetId("0", method.path); + method.actions.push(opSetId("1", opSetPathId("0", objectPath()))); + + this._objectPaths.add(method); + } + + public appendRequests(requests: ODataBatchRequestInfo[]): void { + + requests.forEach(request => { + + // grab the special property we added to options when we created the batch info + const pathQueue: ObjectPathQueue = (request.options).clientsvc_ObjectPaths; + + let paths = pathQueue.toArray(); + + // getChildRelationships + if (paths.length < 0) { + return; + } + + let indexMappingFunction = (n: number) => n; + + if (/GetTaxonomySession/i.test(paths[0].path)) { + + // drop the first thing as it is a get session object path, which we add once for the entire batch + paths = paths.slice(1); + + // replace the next item's parent id with 0, which will be the id of the session call at the root of this request + paths[0].path = opSetParentId("0", paths[0].path); + + indexMappingFunction = (n: number) => n - 1; + } + + let lastOpId = -1; + const idIndexMap: number[] = []; + + paths.map((op, index, arr) => { + + // rewrite the path string + const opId = ++this._builderIndex; + + // track the array index => opId relationship + idIndexMap.push(opId); + + let path = opSetPathParamId(idIndexMap, opSetId(opId.toString(), op.path), indexMappingFunction); + if (lastOpId >= 0) { + path = opSetParentId(lastOpId.toString(), path); + } + + // rewrite actions with placeholders replaced + const opActions = op.actions.map(a => { + const actionId = ++this._builderIndex; + return opSetId(actionId.toString(), opSetPathId(opId.toString(), a)); + }); + + // handle any specific child relationships + // the childIndex is reduced by 1 because we are removing the Session Path + pathQueue.getChildRelationship(index + 1).map(i => i - 1).forEach(childIndex => { + // set the parent id for our non-immediate children + arr[childIndex].path = opSetParentId(opId.toString(), arr[childIndex].path); + }); + + // and remember our last object path id for the parent replace above + lastOpId = opId; + + // return our now substituted path and actions as a new object path instance + return new ObjectPath(path, opActions); + + }).forEach(op => this._objectPaths.add(op)); + + // get this once + const obPaths = this._objectPaths.toArray(); + + // create a new parser to handle finding the result based on the path + const parser = new ProcessQueryParser(obPaths[obPaths.length - 1]); + + if (request.parser instanceof CachingParserWrapper) { + // handle special case of caching + request.parser = new ProcessQueryCachingParserWrapper(parser, request.parser); + } else { + request.parser = parser; + } + + // add the request to our batch requests + this._requests.push(request); + + // remove the temp property + delete (request.options).clientsvc_ObjectPaths; + }); + } + + public execute(): Promise { + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Executing batch with ${this._requests.length} requests.`, LogLevel.Info); + + // create our request body from all the merged object paths + const options = { + body: writeObjectPathBody(this._objectPaths.toArray()), + }; + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Sending batch request.`, LogLevel.Info); + + // send the batch + return super.postCore(options, new BatchParser()).then((rawResponse: any) => { + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Resolving batched requests.`, LogLevel.Info); + + return this._requests.reduce((chain, request) => { + + Logger.write(`[${request.id}] (${(new Date()).getTime()}) Resolving request in batch ${this.batchId}.`, LogLevel.Info); + + return chain.then(_ => (request.parser).findResult(rawResponse).then(request.resolve).catch(request.reject)); + + }, Promise.resolve()); + }); + } +} + +/** + * Used to return the raw results from parsing the batch + */ +class BatchParser extends ProcessQueryParser { + + constructor() { + super(null); + } + + public findResult(json: any): Promise { + // we leave it to the individual request parsers to find their results in the raw json body + return json; + } +} + +/** + * Handles processing batched results that are also cached + */ +class ProcessQueryCachingParserWrapper extends CachingParserWrapper { + + constructor(parser: ProcessQueryParser, wrapper: CachingParserWrapper) { + super(parser, wrapper.cacheOptions); + } + + public findResult(json: any): Promise { + return (this.parser).findResult(json).then((d: any) => this.cacheData(d)); + } +} diff --git a/packages/sp-clientsvc/src/clientsvc.ts b/packages/sp-clientsvc/src/clientsvc.ts new file mode 100644 index 000000000..a4a55bea1 --- /dev/null +++ b/packages/sp-clientsvc/src/clientsvc.ts @@ -0,0 +1,8 @@ +export { IObjectPathBatch, ObjectPathBatch } from "./batch"; +export * from "./clintsvcqueryable"; +export * from "./objectpath"; +export * from "./opactionbuilders"; +export * from "./opbuilders"; +export * from "./parsers"; +export * from "./types"; +export * from "./utils"; diff --git a/packages/sp-clientsvc/src/clintsvcqueryable.ts b/packages/sp-clientsvc/src/clintsvcqueryable.ts new file mode 100644 index 000000000..3241efbfc --- /dev/null +++ b/packages/sp-clientsvc/src/clintsvcqueryable.ts @@ -0,0 +1,398 @@ +import { + FetchOptions, + combine, + extend, + getGUID, + mergeHeaders, + mergeOptions, + objectDefinedNotNull, + hOP, + getHashCode, + stringIsNullOrEmpty, +} from "@pnp/common"; +import { CachingOptions, ICachingOptions, ODataParser, Queryable, RequestContext } from "@pnp/odata"; +import { SPHttpClient, toAbsoluteUrl } from "@pnp/sp"; +import { IObjectPathBatch } from "./batch"; +import { ObjectPathQueue } from "./objectpath"; +import { methodAction, objectPath, objectProperties, opQuery } from "./opactionbuilders"; +import { IMethodParamsBuilder, method, property } from "./opbuilders"; +import { ProcessQueryParser } from "./parsers"; + +export interface IClientSvcQueryable { + select(...selects: string[]): this; + usingCaching(options?: ICachingOptions): this; + inBatch(batch: IObjectPathBatch): this; +} + +export interface ClientSvcQueryableConstructor { + new(baseUrl: string | ClientSvcQueryable, objectPaths?: ObjectPathQueue): T; +} + +const ProcessQueryPath = "_vti_bin/client.svc/ProcessQuery"; + +export class ClientSvcQueryable extends Queryable implements IClientSvcQueryable { + + /** + * Collection of select fields + */ + protected _selects: string[]; + + /** + * Tracks the batch of which this query may be part + */ + protected _batch: IObjectPathBatch | null; + + /** + * Allows us to properly block batch execution until everything is loaded + */ + protected _batchDependency: () => void | null; + + constructor(parent: ClientSvcQueryable | string = "", protected _objectPaths: ObjectPathQueue | null = null) { + super(); + + this._selects = []; + this._batch = null; + this._batchDependency = null; + + if (typeof parent === "string") { + + // we assume the parent here is an absolute url to a web + this._parentUrl = parent; + this._url = combine(parent.replace(ProcessQueryPath, ""), ProcessQueryPath); + if (!objectDefinedNotNull(this._objectPaths)) { + this._objectPaths = new ObjectPathQueue(); + } + + } else { + this._parentUrl = parent._parentUrl; + this._url = combine(parent._parentUrl, ProcessQueryPath); + if (!objectDefinedNotNull(_objectPaths)) { + this._objectPaths = parent._objectPaths.clone(); + } + this.configureFrom(parent); + } + } + + /** + * Choose which fields to return + * + * @param selects One or more fields to return + */ + public select(...selects: string[]): this { + [].push.apply(this._selects, selects); + return this; + } + + /** + * Adds this query to the supplied batch + * + */ + public inBatch(batch: IObjectPathBatch): this { + + if (this.batch !== null) { + throw Error("This query is already part of a batch."); + } + + if (objectDefinedNotNull(batch)) { + this._batch = batch; + this._batchDependency = batch.addDependency(); + } + + return this; + } + + /** + * Gets the full url with query information + * + */ + public toUrlAndQuery(): string { + return `${super.toUrl()}?${Array.from(this.query).map((v: [string, string]) => v[0] + "=" + v[1]).join("&")}`; + } + + protected getSelects(): string[] { + return objectDefinedNotNull(this._selects) ? this._selects : []; + } + + /** + * Gets a child object based on this instance's paths and the supplied paramters + * + * @param factory Instance factory of the child type + * @param methodName Name of the method used to load the child + * @param params Parameters required by the method to load the child + */ + protected getChild(factory: ClientSvcQueryableConstructor, methodName: string, params: IMethodParamsBuilder | null): T { + + const objectPaths = this._objectPaths.copy(); + + objectPaths.add(method(methodName, params, + // actions + objectPath())); + + return new factory(this, objectPaths); + } + + /** + * Gets a property of the current instance + * + * @param factory Instance factory of the child type + * @param propertyName Name of the property to load + */ + protected getChildProperty(factory: ClientSvcQueryableConstructor, propertyName: string): T { + + const objectPaths = this._objectPaths.copy(); + + objectPaths.add(property(propertyName)); + + return new factory(this, objectPaths); + } + + /** + * Sends a request + * + * @param op + * @param options + * @param parser + */ + protected send(objectPaths: ObjectPathQueue, options: FetchOptions = {}, parser: ODataParser = null): Promise { + + // here we need to create a clone because all the string indexes and references + // will be updated and all need to relate for this operation being sent. The parser + // and the postCore method need to share an independent value of the objectPaths + // See for https://github.com/pnp/pnpjs/issues/419 for details + const clonedOps = objectPaths.clone(); + + if (!objectDefinedNotNull(parser)) { + // we assume here that we want to return for this index path + parser = new ProcessQueryParser(clonedOps.last); + } + + if (this.hasBatch) { + + // this is using the options variable to pass some extra information downstream to the batch + options = extend(options, { + clientsvc_ObjectPaths: clonedOps, + }); + + } else { + + if (!hOP(options, "body")) { + options = extend(options, { + body: clonedOps.toBody(), + }); + } + } + + return super.postCore(options, parser); + } + + /** + * Sends the request, merging the result data with a new instance of factory + */ + protected sendGet(factory: ClientSvcQueryableConstructor): Promise<(DataType & FactoryType)> { + + const ops = this._objectPaths.copy().appendActionToLast(opQuery(this.getSelects())); + + return this.send(ops).then(r => extend(new factory(this), r)); + } + + /** + * Sends the request, merging the result data array with a new instances of factory + */ + protected sendGetCollection(factory: (d: DataType) => FactoryType): Promise<(DataType & FactoryType)[]> { + + const ops = this._objectPaths.copy().appendActionToLast(opQuery([], this.getSelects())); + + return this.send(ops).then(r => r.map(d => extend(factory(d), d))); + } + + /** + * Invokes the specified method on the server and returns the result + * + * @param methodName Name of the method to invoke + * @param params Method parameters + * @param actions Any additional actions to execute in addition to the method invocation (set property for example) + */ + protected invokeMethod(methodName: string, params: IMethodParamsBuilder | null = null, ...actions: string[]): Promise { + return this.invokeMethodImpl(methodName, params, actions, opQuery([], null)); + } + + /** + * Invokes a method action that returns a single result and does not have an associated query (ex: GetDescription on Term) + * + * @param methodName Name of the method to invoke + * @param params Method parameters + * @param actions Any additional actions to execute in addition to the method invocation (set property for example) + */ + protected invokeMethodAction(methodName: string, params: IMethodParamsBuilder | null = null, ...actions: string[]): Promise { + return this.invokeMethodImpl(methodName, params, actions, null, true); + } + + /** + * Invokes the specified non-query method on the server + * + * @param methodName Name of the method to invoke + * @param params Method parameters + * @param actions Any additional actions to execute in addition to the method invocation (set property for example) + */ + protected invokeNonQuery(methodName: string, params: IMethodParamsBuilder | null = null, ...actions: string[]): Promise { + // by definition we are not returning anything from these calls so we should not be caching the results + this._useCaching = false; + return this.invokeMethodImpl(methodName, params, actions, null, true); + } + + /** + * Invokes the specified method on the server and returns the resulting collection + * + * @param methodName Name of the method to invoke + * @param params Method parameters + * @param actions Any additional actions to execute in addition to the method invocation (set property for example) + */ + protected invokeMethodCollection(methodName: string, params: IMethodParamsBuilder | null = null, ...actions: string[]): Promise { + return this.invokeMethodImpl(methodName, params, actions, opQuery([], [])); + } + + /** + * Updates this instance, returning a copy merged with the updated data after the update + * + * @param properties Plain object of the properties and values to update + * @param factory Factory method use to create a new instance of FactoryType + */ + protected invokeUpdate(properties: any, factory: ClientSvcQueryableConstructor): Promise { + + const ops = this._objectPaths.copy(); + // append setting all the properties to this instance + objectProperties(properties).map(a => ops.appendActionToLast(a)); + ops.appendActionToLast(opQuery([], null)); + return this.send(ops).then(r => extend(new factory(this), r)); + } + + /** + * Converts the current instance to a request context + * + * @param verb The request verb + * @param options The set of supplied request options + * @param parser The supplied ODataParser instance + * @param pipeline Optional request processing pipeline + */ + protected toRequestContext( + verb: string, + options: FetchOptions, + parser: ODataParser, + pipeline: Array<(c: RequestContext) => Promise>>): Promise> { + + return toAbsoluteUrl(this.toUrlAndQuery()).then(url => { + + mergeOptions(options, this._options); + + const headers = new Headers(); + + mergeHeaders(headers, options.headers); + + mergeHeaders(headers, { + "accept": "*/*", + "content-type": "text/xml", + }); + + options = extend(options, { headers }); + + // we need to do some special cache handling to ensure we have a good key + if (this._useCaching) { + + let keyStr = options.body; + + if (stringIsNullOrEmpty(keyStr)) { + + if (hOP(options, "clientsvc_ObjectPaths")) { + // if we are using caching and batching together we need to create our string from the paths stored for the + // batching operation (see: https://github.com/pnp/pnpjs/issues/449) but not update the ones passed to + // the batch as they will be indexed during the batch creation process + keyStr = (<{ clientsvc_ObjectPaths: ObjectPathQueue }>options).clientsvc_ObjectPaths.clone().toBody(); + } else { + // this case shouldn't happen + keyStr = ""; + } + } + + // because all the requests use the same url they would collide in the cache we use a special key + const cacheKey = `PnPjs.ProcessQueryClient(${getHashCode(keyStr)})`; + + if (objectDefinedNotNull(this._cachingOptions)) { + // if our key ends in the ProcessQuery url we overwrite it + if (/\/client\.svc\/ProcessQuery\?$/i.test(this._cachingOptions.key)) { + this._cachingOptions.key = cacheKey; + } + } else { + this._cachingOptions = new CachingOptions(cacheKey); + } + } + + const dependencyDispose = this.hasBatch ? this._batchDependency : () => { return; }; + + // build our request context + const context: RequestContext = { + batch: this.batch, + batchDependency: dependencyDispose, + cachingOptions: this._cachingOptions, + clientFactory: () => new SPHttpClient(), + hasResult: false, + isBatched: this.hasBatch, + isCached: this._useCaching, + options: options, + parser: parser, + pipeline: pipeline, + requestId: getGUID(), + url: url, + verb: verb, + }; + + return context; + }); + } + + /** + * Blocks a batch call from occuring, MUST be cleared by calling the returned function + */ + protected addBatchDependency(): () => void { + if (this._batch !== null) { + return this._batch.addDependency(); + } + + return () => null; + } + + /** + * Indicates if the current query has a batch associated + * + */ + protected get hasBatch(): boolean { + return objectDefinedNotNull(this._batch); + } + + /** + * The batch currently associated with this query or null + * + */ + protected get batch(): IObjectPathBatch { + return this.hasBatch ? this._batch : null; + } + + /** + * Executes the actual invoke method call + * + * @param methodName Name of the method to invoke + * @param params Method parameters + * @param queryAction Specifies the query action to take + */ + private invokeMethodImpl(methodName: string, params: IMethodParamsBuilder | null, actions: string[], queryAction: string, isAction = false): Promise { + + const ops = this._objectPaths.copy(); + + if (isAction) { + ops.appendActionToLast(methodAction(methodName, params)); + } else { + ops.add(method(methodName, params, ...[objectPath(), ...actions, queryAction])); + } + + return this.send(ops); + } +} diff --git a/packages/sp-clientsvc/src/objectpath.ts b/packages/sp-clientsvc/src/objectpath.ts new file mode 100644 index 000000000..3dfc0fde3 --- /dev/null +++ b/packages/sp-clientsvc/src/objectpath.ts @@ -0,0 +1,316 @@ +import { TypedHash, extend, objectDefinedNotNull } from "@pnp/common"; +import { objectPath } from "./opactionbuilders"; +import { property, staticProperty } from "./opbuilders"; +import { writeObjectPathBody } from "./utils"; + +/** + * Defines the properties and method of an ObjectPath + */ +export interface IObjectPath { + /** + * The ObjectPath xml node + */ + path: string; + /** + * Collection of xml action nodes + */ + actions: string[]; + /** + * The id of this object path, used for processing, not set directly + */ + id: number | undefined; +} + +/** + * Represents an ObjectPath used when querying ProcessQuery + */ +export class ObjectPath implements IObjectPath { + constructor(public path: string, public actions: string[] = [], public id = -1, public replaceAfter: IObjectPath[] = []) { } +} + +/** + * Replaces all found instance of the $$ID$$ placeholder in the supplied xml string + * + * @param id New value to be insterted + * @param xml The existing xml fragment in which the replace should occur + */ +export function opSetId(id: string, xml: string): string { + return xml.replace(/\$\$ID\$\$/g, id); +} + +/** + * Replaces all found instance of the $$PATH_ID$$ placeholder in the supplied xml string + * + * @param id New value to be insterted + * @param xml The existing xml fragment in which the replace should occur + */ +export function opSetPathId(id: string, xml: string): string { + return xml.replace(/\$\$PATH_ID\$\$/g, id); +} + +/** + * Replaces all found instance of the $$PARENT_ID$$ placeholder in the supplied xml string + * + * @param id New value to be insterted + * @param xml The existing xml fragment in which the replace should occur + */ +export function opSetParentId(id: string, xml: string): string { + return xml.replace(/\$\$PARENT_ID\$\$/g, id); +} + +/** + * Replaces all found instance of the $$OP_PARAM_ID$$ placeholder in the supplied xml string + * + * @param map A mapping where [index] = replaced_object_path_id + * @param xml The existing xml fragment in which the replace should occur + * @param indexMapper Used when creating batches, not meant for direct use external to this library + */ +export function opSetPathParamId(map: number[], xml: string, indexMapper: (n: number) => number = (n) => n): string { + + // this approach works because input params must come before the things that need them + // meaning the right id will always be in the map + const matches = /\$\$OP_PARAM_ID_(\d+)\$\$/ig.exec(xml); + if (matches !== null) { + for (let i = 1; i < matches.length; i++) { + const index = parseInt(matches[i], 10); + const regex = new RegExp(`\\$\\$OP_PARAM_ID_${index}\\$\\$`, "ig"); + xml = xml.replace(regex, map[indexMapper(index)].toString()); + } + } + + return xml; +} + +/** + * Represents a collection of IObjectPaths + */ +export class ObjectPathQueue { + + private _xml: string | null; + private _contextIndex = -1; + private _siteIndex = -1; + private _webIndex = -1; + + constructor(protected _paths: IObjectPath[] = [], protected _relationships: TypedHash = {}) { } + + /** + * Adds an object path to the queue + * + * @param op The action to add + * @returns The index of the added object path + */ + public add(op: IObjectPath): number { + + this.dirty(); + this._paths.push(op); + return this.lastIndex; + } + + public addChildRelationship(parentIndex: number, childIndex: number) { + if (objectDefinedNotNull(this._relationships[`_${parentIndex}`])) { + this._relationships[`_${parentIndex}`].push(childIndex); + } else { + this._relationships[`_${parentIndex}`] = [childIndex]; + } + } + + public getChildRelationship(parentIndex: number): number[] { + if (objectDefinedNotNull(this._relationships[`_${parentIndex}`])) { + return this._relationships[`_${parentIndex}`]; + } else { + return []; + } + } + + public getChildRelationships(): TypedHash { + return this._relationships; + } + + /** + * Appends an action to the supplied IObjectPath, replacing placeholders + * + * @param op IObjectPath to which the action will be appended + * @param action The action to append + */ + public appendAction(op: IObjectPath, action: string): this { + + this.dirty(); + op.actions.push(action); + return this; + } + + /** + * Appends an action to the last IObjectPath in the collection + * + * @param action + */ + public appendActionToLast(action: string): this { + + return this.appendAction(this.last, action); + } + + /** + * Creates a linked copy of this ObjectPathQueue + */ + public copy(): ObjectPathQueue { + const copy = new ObjectPathQueue(this.toArray(), extend({}, this._relationships)); + copy._contextIndex = this._contextIndex; + copy._siteIndex = this._siteIndex; + copy._webIndex = this._webIndex; + return copy; + } + + /** + * Creates an independent clone of this ObjectPathQueue + */ + public clone(): ObjectPathQueue { + const clone = new ObjectPathQueue(this.toArray().map(p => Object.assign({}, p)), extend({}, this._relationships)); + clone._contextIndex = this._contextIndex; + clone._siteIndex = this._siteIndex; + clone._webIndex = this._webIndex; + return clone; + } + + /** + * Gets a copy of this instance's paths + */ + public toArray(): IObjectPath[] { + return this._paths.slice(0); + } + + /** + * The last IObjectPath instance added to this collection + */ + public get last(): IObjectPath { + + if (this._paths.length < 1) { + return null; + } + + return this._paths[this.lastIndex]; + } + + /** + * Index of the last IObjectPath added to the queue + */ + public get lastIndex(): number { + return this._paths.length - 1; + } + + /** + * Gets the index of the current site in the queue + */ + public get siteIndex(): number { + + if (this._siteIndex < 0) { + + // this needs to be here in case we create it + const contextIndex = this.contextIndex; + + this._siteIndex = this.add(property("Site", + // actions + objectPath())); + + this.addChildRelationship(contextIndex, this._siteIndex); + } + + return this._siteIndex; + } + + /** + * Gets the index of the current web in the queue + */ + public get webIndex(): number { + + if (this._webIndex < 0) { + + // this needs to be here in case we create it + const contextIndex = this.contextIndex; + + this._webIndex = this.add(property("Web", + // actions + objectPath())); + + this.addChildRelationship(contextIndex, this._webIndex); + } + + return this._webIndex; + } + + /** + * Gets the index of the Current context in the queue, can be used to establish parent -> child rels + */ + public get contextIndex(): number { + if (this._contextIndex < 0) { + this._contextIndex = this.add(staticProperty("Current", "{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}", + // actions + objectPath())); + } + + return this._contextIndex; + } + + public toBody(): string { + + if (objectDefinedNotNull(this._xml)) { + return this._xml; + } + + // create our xml payload + this._xml = writeObjectPathBody(this.toIndexedTree()); + + return this._xml; + } + + /** + * Conducts the string replacements for id, parent id, and path id + * + * @returns The tree with all string replacements made + */ + public toIndexedTree(): IObjectPath[] { + + let builderIndex = -1; + let lastOpId = -1; + const idIndexMap: number[] = []; + + return this.toArray().map((op, index, arr) => { + + const opId = ++builderIndex; + + // track the array index => opId relationship + idIndexMap.push(opId); + + // do path replacements + op.path = opSetPathParamId(idIndexMap, opSetId(opId.toString(), op.path)); + + if (lastOpId >= 0) { + // if we have a parent do the replace + op.path = opSetParentId(lastOpId.toString(), op.path); + } + + // rewrite actions with placeholders replaced + op.actions = op.actions.map(a => { + const actionId = ++builderIndex; + return opSetId(actionId.toString(), opSetPathId(opId.toString(), a)); + }); + + // handle any specific child relationships + this.getChildRelationship(index).forEach(childIndex => { + // set the parent id for our non-immediate children, thus removing the token so it isn't overwritten + arr[childIndex].path = opSetParentId(opId.toString(), arr[childIndex].path); + }); + + // and remember our last object path id for the parent replace above + lastOpId = opId; + + return op; + }); + } + + /** + * Dirties this queue clearing any cached data + */ + protected dirty(): void { + this._xml = null; + } +} diff --git a/packages/sp-clientsvc/src/opactionbuilders.ts b/packages/sp-clientsvc/src/opactionbuilders.ts new file mode 100644 index 000000000..492d88e1a --- /dev/null +++ b/packages/sp-clientsvc/src/opactionbuilders.ts @@ -0,0 +1,99 @@ +import { PropertyType } from "./types"; +import { IMethodParamsBuilder } from "./opbuilders"; + +export function objectPath(): string { + return ``; +} + +export function identityQuery(): string { + return ``; +} + +export function opQuery(selectProperties: string[] = null, childSelectProperties: string[] | null = null): string { + + // this is fairly opaque behavior, but is the simplest way to convey the required information. + // if selectProperties.length === 0 or null then select all + // else select indicated properties + + // if childSelectProperties === null do not include that block + // if childSelectProperties.length === 0 then select all + // else select indicated properties + + const builder = []; + builder.push(``); + if (selectProperties === null || selectProperties.length < 1) { + builder.push(``); + builder.push(``); + builder.push(``); + } else { + builder.push(``); + builder.push(``); + [].push.apply(builder, selectProperties.map(p => ``)); + builder.push(``); + builder.push(``); + } + + if (childSelectProperties !== null) { + if (childSelectProperties.length < 1) { + builder.push(``); + builder.push(``); + builder.push(``); + } else { + builder.push(``); + builder.push(``); + [].push.apply(builder, childSelectProperties.map(p => ``)); + builder.push(``); + builder.push(``); + } + } + + builder.push(``); + + return builder.join(""); +} + +export function setProperty(name: string, type: PropertyType, value: string): string { + const builder = []; + builder.push(``); + builder.push(`${value}`); + builder.push(``); + return builder.join(""); +} + +export function methodAction(name: string, params: IMethodParamsBuilder | null): string { + + const builder = []; + builder.push(``); + + if (params !== null) { + const arrParams = params.toArray(); + if (arrParams.length < 1) { + builder.push(``); + } else { + builder.push(``); + [].push.apply(builder, arrParams.map(p => `${p.value}`)); + builder.push(``); + } + } + + builder.push(""); + + return builder.join(""); +} + +export function objectProperties(o: any): string[] { + + return Object.getOwnPropertyNames(o).map((name) => { + + const value = o[name]; + if (typeof value === "boolean") { + return setProperty(name, "Boolean", `${value}`); + } else if (typeof value === "number") { + return setProperty(name, "Number", `${value}`); + } else if (typeof value === "string") { + return setProperty(name, "String", `${value}`); + } + + return ""; + }, []); +} diff --git a/packages/sp-clientsvc/src/opbuilders.ts b/packages/sp-clientsvc/src/opbuilders.ts new file mode 100644 index 000000000..974be6f15 --- /dev/null +++ b/packages/sp-clientsvc/src/opbuilders.ts @@ -0,0 +1,97 @@ +import { ObjectPath, IObjectPath } from "./objectpath"; +import { PropertyType } from "./types"; + +export function property(name: string, ...actions: string[]): IObjectPath { + return new ObjectPath(``, actions); +} + +export function staticMethod(name: string, typeId: string, ...actions: string[]): IObjectPath { + return new ObjectPath(``, actions); +} + +export function staticProperty(name: string, typeId: string, ...actions: string[]): IObjectPath { + return new ObjectPath(``, actions); +} + +export function objConstructor(typeId: string, ...actions: string[]): IObjectPath { + return new ObjectPath(``, actions); +} + +export interface IMethodParamsBuilder { + string(value: string): this; + number(value: number): this; + boolean(value: boolean): this; + strArray(values: string[]): this; + objectPath(inputIndex: number): this; + toArray(): { type: PropertyType, value: string }[]; +} + +/** + * Used to build parameters when calling methods + */ +export class MethodParams implements IMethodParamsBuilder { + + constructor(private _p: { type: PropertyType, value: string }[] = []) { } + + public static build(initValues: { type: PropertyType, value: string }[] = []): IMethodParamsBuilder { + const params = new MethodParams(); + [].push.apply(params._p, initValues); + return params; + } + + public string(value: string): this { + return this.a("String", value); + } + + public number(value: number): this { + return this.a("Number", value.toString()); + } + + public boolean(value: boolean): this { + return this.a("Boolean", value.toString()); + } + + public strArray(values: string[]): this { + return this.a("Array", values.map(v => `${v}`).join("")); + } + + public objectPath(inputIndex: number): this { + return this.a("ObjectPath", inputIndex.toString()); + } + + public toArray(): { type: PropertyType, value: string }[] { + return this._p; + } + + private a(type: PropertyType, value: string): this { + this._p.push({ type, value }); + return this; + } +} + +export function method(name: string, params: IMethodParamsBuilder, ...actions: string[]): IObjectPath { + const builder = []; + builder.push(``); + + if (params !== null) { + const arrParams = params.toArray(); + if (arrParams.length < 1) { + builder.push(``); + } else { + builder.push(``); + [].push.apply(builder, arrParams.map(p => { + + if (p.type === "ObjectPath") { + return ``; + } + + return `${p.value}`; + })); + builder.push(``); + } + } + + builder.push(""); + + return new ObjectPath(builder.join(""), actions); +} diff --git a/packages/sp-clientsvc/src/parsers.ts b/packages/sp-clientsvc/src/parsers.ts new file mode 100644 index 000000000..a699dc0de --- /dev/null +++ b/packages/sp-clientsvc/src/parsers.ts @@ -0,0 +1,101 @@ +import { getAttrValueFromString, jsS, hOP } from "@pnp/common"; +import { IObjectPath } from "./objectpath"; + +/** + * Used within the request pipeline to parse ProcessQuery results + */ +export class ProcessQueryParser { + + constructor(protected op: IObjectPath) { } + + /** + * Parses the response checking for errors + * + * @param r Response object + */ + public parse(r: Response): Promise { + + return r.text().then(t => { + + if (!r.ok) { + throw Error(t); + } + + try { + return JSON.parse(t); + } catch (e) { + // special case in ProcessQuery where we got an error back, but it is not in json format + throw Error(t); + } + + }).then((parsed: any[]) => { + + // here we need to check for an error body + if (parsed.length > 0 && hOP(parsed[0], "ErrorInfo") && parsed[0].ErrorInfo !== null) { + throw Error(jsS(parsed[0].ErrorInfo)); + } + + return this.findResult(parsed); + }); + } + + public findResult(json: any): Promise { + + for (let i = 0; i < this.op.actions.length; i++) { + + const a = this.op.actions[i]; + + // let's see if the result is null based on the ObjectPath action, if it exists + // + if (/^(json, parseInt(getAttrValueFromString(a, "Id"), 10)); + if (!result || (result && result.IsNull)) { + return Promise.resolve(null); + } + } + + // let's see if we have a query result + // + if (/^(parsed: any[], id: number): R { + + for (let i = 0; i < parsed.length; i++) { + + if (parsed[i] === id) { + return parsed[i + 1]; + } + } + + return null; + } +} diff --git a/packages/sp-clientsvc/src/types.ts b/packages/sp-clientsvc/src/types.ts new file mode 100644 index 000000000..4eba2cc64 --- /dev/null +++ b/packages/sp-clientsvc/src/types.ts @@ -0,0 +1 @@ +export type PropertyType = "Boolean" | "String" | "Number" | "ObjectPath" | "Array"; diff --git a/packages/sp-clientsvc/src/utils.ts b/packages/sp-clientsvc/src/utils.ts new file mode 100644 index 000000000..8be080d74 --- /dev/null +++ b/packages/sp-clientsvc/src/utils.ts @@ -0,0 +1,29 @@ +import { IObjectPath } from "./objectpath"; + +/** + * Transforms an array of object paths into a request xml body. Does not do placeholder substitutions. + * + * @param objectPaths The object paths for which we want to generate a body + */ +export function writeObjectPathBody(objectPaths: IObjectPath[]): string { + + const actions: string[] = []; + const paths: string[] = []; + + objectPaths.forEach(op => { + paths.push(op.path); + actions.push(...op.actions); + }); + + // create our xml payload + return [ + ``, + "", + actions.join(""), + "", + "", + paths.join(""), + "", + "", + ].join(""); +} diff --git a/packages/sp-clientsvc/tsconfig.es5.json b/packages/sp-clientsvc/tsconfig.es5.json new file mode 100644 index 000000000..f473c0c1c --- /dev/null +++ b/packages/sp-clientsvc/tsconfig.es5.json @@ -0,0 +1,32 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "strictNullChecks": false + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + }, + { + "path": "../odata/tsconfig.es5.json" + }, + { + "path": "../sp/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/sp-clientsvc/tsconfig.json b/packages/sp-clientsvc/tsconfig.json new file mode 100644 index 000000000..24cc124f2 --- /dev/null +++ b/packages/sp-clientsvc/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strictNullChecks": false + }, + "include": [ + "index.ts", + "src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + }, + { + "path": "../odata" + }, + { + "path": "../sp" + } + ] +} \ No newline at end of file diff --git a/packages/sp-taxonomy/__tests/batch.test.ts b/packages/sp-taxonomy/__tests/batch.test.ts new file mode 100644 index 000000000..65c4fa324 --- /dev/null +++ b/packages/sp-taxonomy/__tests/batch.test.ts @@ -0,0 +1,78 @@ +import { expect } from "chai"; +import { taxonomy } from ".."; +import { testSettings } from "../../../test/main"; + +describe("Batching", () => { + + if (testSettings.enableWebTests) { + + it("Should execute batches in the expected order for a single request", () => { + + const order: number[] = []; + + const batch = taxonomy.createBatch(); + + taxonomy.termStores.inBatch(batch).get().then(_ => { + order.push(1); + }); + + return expect(batch.execute().then(_ => { + order.push(2); + return order; + + })).to.eventually.be.fulfilled.and.eql([1, 2]); + }); + + it("Should execute batches in the expected order for an even number of requests", () => { + + const order: number[] = []; + + const batch = taxonomy.createBatch(); + + taxonomy.termStores.inBatch(batch).get().then(_ => { + order.push(1); + }); + + taxonomy.termStores.select("Name").inBatch(batch).get().then(_ => { + order.push(2); + }); + + taxonomy.getDefaultSiteCollectionTermStore().getSiteCollectionGroup(false).inBatch(batch).get().then(_ => { + order.push(3); + }); + + taxonomy.getDefaultSiteCollectionTermStore().inBatch(batch).get().then(_ => { + order.push(4); + }); + + return expect(batch.execute().then(_ => { + order.push(5); + return order; + })).to.eventually.be.fulfilled.and.eql([1, 2, 3, 4, 5]); + }); + + it("Should execute batches in the expected order for an odd number of requests", () => { + + const order: number[] = []; + + const batch = taxonomy.createBatch(); + + taxonomy.termStores.inBatch(batch).get().then(_ => { + order.push(1); + }); + + taxonomy.termStores.select("Name").inBatch(batch).get().then(_ => { + order.push(2); + }); + + taxonomy.getDefaultSiteCollectionTermStore().getSiteCollectionGroup(false).inBatch(batch).get().then(_ => { + order.push(3); + }); + + return expect(batch.execute().then(_ => { + order.push(4); + return order; + })).to.eventually.be.fulfilled.and.eql([1, 2, 3, 4]); + }); + } +}); diff --git a/packages/sp-taxonomy/__tests/session.test.ts b/packages/sp-taxonomy/__tests/session.test.ts new file mode 100644 index 000000000..f57d84dc7 --- /dev/null +++ b/packages/sp-taxonomy/__tests/session.test.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import { taxonomy } from ".."; +import { testSettings } from "../../../test/main"; + +describe("Taxonomy", () => { + + if (testSettings.enableWebTests) { + + let defaultTermStoreName: string | null = null; + let defaultTermStoreId: string | null = null; + + before((done) => { + + // we are going to grab the default term store id and name for use later in loading things + + taxonomy.getDefaultSiteCollectionTermStore().select("Name", "Id").get().then(ts => { + + defaultTermStoreId = ts.Id; + defaultTermStoreName = ts.Name; + done(); + }); + }); + + describe("session", () => { + + it("Should getDefaultKeywordTermStore", () => { + + return expect(taxonomy.getDefaultKeywordTermStore().get()).to.eventually.be.fulfilled; + }); + + it("Should getDefaultSiteCollectionTermStore", () => { + + return expect(taxonomy.getDefaultSiteCollectionTermStore().get()).to.eventually.be.fulfilled; + }); + }); + + describe("termstores", () => { + + it("Should load termstores data", () => { + + const tests = [ + expect(taxonomy.termStores.get()).to.eventually.be.fulfilled, + expect(taxonomy.termStores.select("Name", "Id").get()).to.eventually.be.fulfilled.and.be.an.instanceOf(Array), + ]; + + return Promise.all(tests); + }); + + it("Should load a term store by id", () => { + + return expect(taxonomy.termStores.getById(defaultTermStoreId).get()).to.eventually.be.fulfilled; + }); + + it("Should load a term store by name", () => { + + return expect(taxonomy.termStores.getByName(defaultTermStoreName).get()).to.eventually.be.fulfilled; + }); + }); + } +}); diff --git a/packages/sp-taxonomy/__tests/termstore.test.ts b/packages/sp-taxonomy/__tests/termstore.test.ts new file mode 100644 index 000000000..bf0c21fbd --- /dev/null +++ b/packages/sp-taxonomy/__tests/termstore.test.ts @@ -0,0 +1,111 @@ +import { dateAdd } from "@pnp/common"; +import { expect } from "chai"; +import { ChangedItemType, taxonomy } from ".."; +import { testSettings } from "../../../test/main"; + +describe("TermStore", () => { + + if (testSettings.enableWebTests) { + + let storeId: string | null = null; + let termSetId: string | null = null; + let termSetName: string | null = null; + let termId: string | null = null; + + before((done) => { + + // we load these once for use several times below + taxonomy.getDefaultSiteCollectionTermStore().get().then(store => { + + storeId = store.Id; + + return store.getSiteCollectionGroup(false).get().then(group => { + + return group.termSets.select("Id", "Name").get().then(sets => { + + termSetId = sets[0].Id; + termSetName = sets[0].Name; + + return store.getTermSetById(termSetId).terms.select("Id").get().then(terms => { + + if (terms.length > 0) { + + termId = terms[0].Id; + } + }); + }); + }); + }).then(done).catch(e => done(e)); + }); + + it("Should get changes", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().getChanges({ + ItemType: ChangedItemType.Term, + StartTime: dateAdd(new Date(), "week", -2).toISOString(), + }); + + return expect(p).to.eventually.be.fulfilled; + }); + + it("Should get changes", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().getChanges({ + ItemType: ChangedItemType.Term, + StartTime: dateAdd(new Date(), "week", -2).toISOString(), + }); + + return expect(p).to.eventually.be.fulfilled; + }); + + it("Should get site collection group", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().getSiteCollectionGroup(false).get(); + + return expect(p).to.eventually.be.fulfilled; + }); + + it("Should get groups", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().groups.get(); + + return expect(p).to.eventually.be.fulfilled; + }); + + it("Should get a term by id", () => { + + if (termId !== null) { + return expect(taxonomy.termStores.getById(storeId).getTermById(termId).get()).to.eventually.be.fulfilled; + } + }); + + it("Should get a term in a termset", () => { + + if (termId !== null) { + return expect(taxonomy.termStores.getById(storeId).getTermInTermSet(termId, termSetId).get()).to.eventually.be.fulfilled; + } + }); + + it("Should get a terms using label match info", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().getTerms({ + TermLabel: "label", + TrimUnavailable: true, + }).get(); + + return expect(p).to.eventually.be.fulfilled; + }); + + it("Should get a termset by id", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().getTermSetById(termSetId).get(); + return expect(p).to.eventually.be.fulfilled; + }); + + it("Should get a termset by name", () => { + + const p = taxonomy.getDefaultSiteCollectionTermStore().getTermSetsByName(termSetName, 1033).get(); + return expect(p).to.eventually.be.fulfilled; + }); + } +}); diff --git a/packages/sp-taxonomy/docs/index.md b/packages/sp-taxonomy/docs/index.md new file mode 100644 index 000000000..1cedb5fb7 --- /dev/null +++ b/packages/sp-taxonomy/docs/index.md @@ -0,0 +1,74 @@ +# @pnp/sp-taxonomy + +[![npm version](https://badge.fury.io/js/%40pnp%2Fsp-taxonomy.svg)](https://badge.fury.io/js/%40pnp%2Fsp-taxonomy) + +This module provides a fluent interface for working with the SharePoint term store. It does not rely on SP.taxonomy.js or other dependencies outside the @pnp scope. It is designed to function in a similar manner and present a similar feel to the other data retrieval libraries. It works by calling the "/\_vti_bin/client.svc/ProcessQuery" endpoint. + +## Getting Started + +You will need to install the @pnp/sp-taxonomy package as well as the packages it requires to run. + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/sp-taxonomy @pnp/sp-clientsvc --save` + +## Root Object + +All fluent taxonomy operations originate from the Taxonomy object. You can access it in several ways. + +### Import existing instance + +This method will grab an existing instance of the Taxonomy class and allow you to immediately chain additional methods. + +```TypeScript +import { taxonomy } from "@pnp/sp-taxonomy"; + +await taxonomy.termStores.get(); +``` + +### Import class and create instance + +You can also import the Taxonomy class and create a new instance. This useful in those cases where you want to work with taxonomy in another web than the current web. + +```TypeScript +import { Session } from "@pnp/sp-taxonomy"; + +const taxonomy = new Session("https://mytenant.sharepoint.com/sites/dev"); + +await taxonomy.termStores.get(); +``` + +## Setup + +Because the sp-taxonomy library uses the same @pnp/odata request pipeline as the other libraries you can call the setup method with the same options used for the @pnp/sp library. The setup method is provided as shorthand and avoids the need to import anything from @pnp/sp if you do not need to. A call to this setup method is equivilent to calling the sp.setup method and the configuration is shared between the libraries within your application. + +In the below example all requests for the @pnp/sp-taxonomy library _and_ the @pnp/sp library will be routed through the specified SPFetchClient. Sharing the configuration like this handles the most common scenario of working on the same web easily. You can set other values here as well such as baseUrl and they will be respected by both libraries. + +```TypeScript +import { taxonomy } from "@pnp/sp-taxonomy"; +import { SPFetchClient } from "@pnp/nodejs"; + +// example for setting up the node client using setup method +// we also set a custom header, as an example +taxonomy.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{url}", "{client id}", "{client secret}"); + }, + headers: { + "X-Custom-Header": "A Great Value", + }, + }, +}); +``` + +## Library Topics + +* [Term Stores](term-stores.md) +* [Term Groups](term-groups.md) +* [Term Sets](term-sets.md) +* [Terms](terms.md) +* [Labels](labels.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-sp-taxonomy-uml.svg) + +Graphical UML diagram of @pnp/sp-taxonomy. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/sp-taxonomy/docs/labels.md b/packages/sp-taxonomy/docs/labels.md new file mode 100644 index 000000000..a100b9e9b --- /dev/null +++ b/packages/sp-taxonomy/docs/labels.md @@ -0,0 +1,49 @@ +# @pnp/sp-taxonomy/labels + +## Load labels + +You can load labels by accessing the labels property of a [term](terms.md). + +```TypeScript +import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = + +// load the terms merged with data +const labelsWithData: (ILabel & ILabelData)[] = await term.labels.get(); + + +// get a label by value +const label: ILabel = term.labels.getByValue("term value"); + +// get a label merged with data +const label2: ILabel & ILabelData = term.labels.getByValue("term value").get(); +``` + +## Label Properties and Methods + +### setAsDefaultForLanguage + +Sets this labels as the default for the language + +```TypeScript +import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = + +// get a label by value +await term.labels.getByValue("term value").setAsDefaultForLanguage(); +``` + +### delete + +Deletes this label + +```TypeScript +import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = + +// get a label by value +await term.labels.getByValue("term value").delete(); +``` diff --git a/packages/sp-taxonomy/docs/term-groups.md b/packages/sp-taxonomy/docs/term-groups.md new file mode 100644 index 000000000..8262c453e --- /dev/null +++ b/packages/sp-taxonomy/docs/term-groups.md @@ -0,0 +1,74 @@ +# @pnp/sp-taxonomy/termgroups + +Term groups are used as a container for terms within a term store. + +## Load a term group + +Term groups are loaded from a [term store](term-stores.md) + +```TypeScript +import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` + +## Term Group methods and properties + +### addContributor + +Adds a contributor to the Group + +```TypeScript +import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await group.addContributor("i:0#.f|membership|person@tenant.com"); +``` + +### addGroupManager + +Adds a group manager to the Group + +```TypeScript +import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await group.addGroupManager("i:0#.f|membership|person@tenant.com"); +``` + +### createTermSet + +Creates a new [term set](term-sets.md) + +```TypeScript +import { taxonomy, ITermStore, ITermGroup, ITermSet, ITermSetData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const set: ITermSet & ITermSetData = await group.createTermSet("name", 1031); + +// you can optionally supply the term set id, if you do not we create a new id for you +const set2: ITermSet & ITermSetData = await group.createTermSet("name", 1031, "0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` + +### get + +Gets this term group's data + +```TypeScript +import { taxonomy, ITermStore, ITermGroupData, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup & ITermGroupData = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").get(); +``` diff --git a/packages/sp-taxonomy/docs/term-sets.md b/packages/sp-taxonomy/docs/term-sets.md new file mode 100644 index 000000000..a9eedca38 --- /dev/null +++ b/packages/sp-taxonomy/docs/term-sets.md @@ -0,0 +1,123 @@ +# @pnp/sp-taxonomy/termsets + +Term sets contain terms within the taxonomy heirarchy. + +## Load a term set + +You load a term set directly from a term store. + +```TypeScript +import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` + +Or you can load a term set from a collection - though if you know the id it is more efficient to get the term set directly. + +```TypeScript +import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set = store.getTermSetsByName("my set", 1031).getById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const setWithData = store.getTermSetsByName("my set", 1031).getByName("my set").get(); +``` + + +## Term set methods and properties + +### addStakeholder + +Adds a stakeholder to the TermSet + +```TypeScript +import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await set.addStakeholder("i:0#.f|membership|person@tenant.com"); +``` + +### deleteStakeholder + +Deletes a stakeholder to the TermSet + +```TypeScript +import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await set.deleteStakeholder("i:0#.f|membership|person@tenant.com"); +``` + +### get + +Gets the data for this TermSet + +```TypeScript +import { taxonomy, ITermStore, ITermSet, ITermSetData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const setWithData: ITermSet & ITermSetData = await set.get(); +``` + +### terms + +Provides access to the [terms](terms.md) collection for this termset + +```TypeScript +import { taxonomy, ITermStore, ITermSet, ITerms, ITermData, ITerm } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const terms: ITerms = set.terms; + +// load the data into the terms instances +const termsWithData: (ITermData & ITerm)[] = set.terms.get(); +``` + +### getTermById + +Gets a term by id from this set + +```TypeScript +import { taxonomy, ITermStore, ITermSet, ITermData, ITerm } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const term: ITerm = set.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +// load the data into the term instances +const termWithData: ITermData & ITerm = term.get(); +``` + +### addTerm + +Adds a term to a term set + +```TypeScript +import { taxonomy, ITermStore, ITermSet, ITermData, ITerm } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const term: ITerm & ITermData = await set.addTerm("name", 1031, true); + +// you can optionally set the id when you create the term +const term2: ITerm & ITermData = await set.addTerm("name", 1031, true, "0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` diff --git a/packages/sp-taxonomy/docs/term-stores.md b/packages/sp-taxonomy/docs/term-stores.md new file mode 100644 index 000000000..497e3f97c --- /dev/null +++ b/packages/sp-taxonomy/docs/term-stores.md @@ -0,0 +1,204 @@ +# @pnp/sp-taxonomy/termstores + +Term stores contain term groups, term sets, and terms. This article describes how to work find, load, and use a term store to access the terms inside. + +## List term stores + +You can access a list of all term stores via the _termstores_ property of the Taxonomy class. + +```TypeScript +// get a list of term stores and return all properties +const stores = await taxonomy.termStores.get(); + +// you can also select the fields to return for the term stores using the select operator. +const stores2 = await taxonomy.termStores.select("Name").get(); +``` + +## Load a term store + +To load a specific term store you can use the _getByName_ or _getById_ methods. Using the _get_ method executes the request to the server. + +```TypeScript +const store = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").get(); + +const store2 = await taxonomy.termStores.getById("f6112509-fba7-4544-b2ed-ce6c9396b646").get(); + +// you can use select as well with either method to choose the fields to return +const store3 = await taxonomy.termStores.getById("f6112509-fba7-4544-b2ed-ce6c9396b646").select("Name").get(); +``` + +For term stores and all other objects data is returned as a merger of the data and a new instance of the representative class. Allowing you to immediately begin acting on the object. IF you do not need the data, skip the get call until you do. + +```TypeScript +// no data loaded yet, store is an instance of TermStore class +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +// I can call subsequent methods on the same object and will now have an object with data +// I could have called get above as well - this is just an example +const store2: ITermStore & ITermStoreData = await store.get(); + +// log the Name property +console.log(store2.Name); + +// call another TermStore method on the same object +await store2.addLanguage(1031); +``` + +## Term store methods and properties + +### get + +Loads the data for this term store + +```TypeScript +import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").get(); +``` + +### getTermSetsByName + +Gets the collection of term sets with a matching name + +```TypeScript +import { taxonomy, ITermSets } from "@pnp/sp-taxonomy"; + +const sets: ITermSets = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").getTermSetsByName("My Set", 1033); +``` + +### getTermSetById + +Gets the [term set](term-sets.md) with a matching id + +```TypeScript +import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +// note that you can also use instances if you wanted to conduct multiple operations on a single store +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +// we will handle normalizing guids for you as well :) +const set2: ITermSet = store.getTermSetById("{a63aefc9-359d-42b7-a0d2-cb1809acd260}"); +``` + +### getTermById + +Gets a [term](terms.md) by id + +```TypeScript +import { taxonomy, ITermStore, ITerm, ITermData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const term: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +const termWithData: ITerm & ITermData = await term.get(); +``` + +### getTermsById + +_Added in 1.2.6_ + +```TypeScript +import { taxonomy, ITermStore, ITerms, ITermData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const terms: ITerms = store.getTermsById("0ba6845c-1468-4ec5-a5a8-718f1fb05431", "0ba6845c-1468-4ec5-a5a8-718f1fb05432"); +const termWithData: (ITerm & ITermData)[] = await term.get(); +``` + +### getTermGroupById + +Gets a [term group](term-groups.md) by id + +```TypeScript +import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` + +### getTerms + +Gets [terms](terms.md) that match the provided criteria. Please see [this article](https://msdn.microsoft.com/en-us/library/hh626704%28v=office.12%29.aspx) for details on valid querys. + +```TypeScript +import { taxonomy, ITermStore, ILabelMatchInfo, ITerm, ITermData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const terms: ITerms = store.getTerms({ + TermLabel: "test label", + TrimUnavailable: true, + }); + +// load the data based on the above query +const termsWithData: (ITerm & ITermData)[] = terms.get(); + +// select works here too :) +const termsWithData2: (ITerm & ITermData)[] = terms.select("Name").get(); +``` + +### addLanguage + +Adds a language to the term store by LCID + +```TypeScript +import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.addLanguage(1031); +``` + +### addGroup + +Adds a [term group](term-groups.md) to the term store + +```TypeScript +import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup & ITermGroupData = await store.addGroup("My Group Name"); + +// you can optionally specify the guid of the group, if you don't we just create a new guid for you +const groups: ITermGroup & ITermGroupData = await store.addGroup("My Group Name", "0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` + +### commitAll + +Commits all updates to the database that have occurred since the last commit or rollback. + +```TypeScript +import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.commitAll(); +``` + +### deleteLanguage + +Delete a working language from the TermStore + +```TypeScript +import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.deleteLanguage(1031); +``` + +### rollbackAll + +Discards all updates that have occurred since the last commit or rollback. It is unlikely you will need to call this method through this library due to how things are structured. + +```TypeScript +import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.rollbackAll(); +``` + diff --git a/packages/sp-taxonomy/docs/terms.md b/packages/sp-taxonomy/docs/terms.md new file mode 100644 index 000000000..112ca2130 --- /dev/null +++ b/packages/sp-taxonomy/docs/terms.md @@ -0,0 +1,176 @@ +# @pnp/sp-taxonomy/terms + +Terms are the individual entries with a term set. + +## Load Terms + +You can load a collection of terms through a [term set](term-sets.md) or [term store](term-stores.md). + +```TypeScript +import { + taxonomy, + ITermStore, + ITerms, + ILabelMatchInfo, + ITerm, + ITermData +} from "@pnp/sp-taxonomy"; + +const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const labelMatchInfo: ILabelMatchInfo = { + TermLabel: "My Label", + TrimUnavailable: true, +}; + +const terms: ITerms = store.getTerms(labelMatchInfo); + +// get term instances merged with data +const terms2: (ITermData & ITerm)[] = await store.getTerms(labelMatchInfo).get(); + +const terms3: ITerms = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").terms; + +// get terms merged with data from a term set +const terms4: (ITerm & ITermData)[] = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").terms.get(); +``` + +## Load Single Term + +You can get a single term a variety of ways as shown below. The "best" way will be determined by what information is available to do the lookup but ultimately will result in the same end product. + +```TypeScript +import { + taxonomy, + ITermStore, + ITerms, + ILabelMatchInfo, + ITerm, + ITermData +} from "@pnp/sp-taxonomy"; + +const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +// get a single term by id +const term: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +// get single get merged with data +const term2: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").get(); + +// use select to choose which fields to return +const term3: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").select("Name").get(); + +// get a term from a term set +const term4: ITerm = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +``` + +## Term methods and properties + +### labels + +Accesses the [labels](labels.md) collection for this term + +```TypeScript +import { taxonomy, ITermStore, ITerm, ILabels } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +const labels: ILabels = term.labels; + +// labels merged with data +const labelsWithData = term.labels.get(); +``` + +### createLabel + +Creates a new label for this Term + +```TypeScript +import { taxonomy, ITermStore, ITerm, ILabelData, ILabel } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +const label: ILabelData & ILabel = term.createLabel("label text", 1031); + +// optionally specify this is the default label +const label2: ILabelData & ILabel = term.createLabel("label text", 1031, true); +``` + +### deprecate + +Sets the deprecation flag on a term + +```TypeScript +import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +await term.deprecate(true); +``` + +### get + +Loads the term data + +```TypeScript +import { ITerm, ITermData } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +// load term instance merged with data +const term2: ITerm & ITermData = await term.get(); +``` + +### getDescription + +Sets the description + +```TypeScript +import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +// load term instance merged with data +const description = await term.getDescription(1031); +``` + +### setDescription + +Sets the description + +```TypeScript +import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +// load term instance merged with data +await term.setDescription("the description", 1031); +``` + +### setLocalCustomProperty + +Sets a custom property on this term + +```TypeScript +import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = ; + +// load term instance merged with data +await term.setLocalCustomProperty("name", "value"); +``` + +### addTerm + +_Added in 1.2.8_ + +Adds a child term to an existing term instance. + +```TypeScript +import { ITerm } from "@pnp/sp-taxonomy"; + +const parentTerm: ITerm = ; + +await parentTerm.addTerm("child 1", 1033); + +await parentTerm.addTerm("child 2", 1033); +``` diff --git a/packages/sp-taxonomy/docs/utilities.md b/packages/sp-taxonomy/docs/utilities.md new file mode 100644 index 000000000..998d1e619 --- /dev/null +++ b/packages/sp-taxonomy/docs/utilities.md @@ -0,0 +1,47 @@ +# @pnp/sp-taxonomy/utilities + +These are a collection of helper methods you may find useful + +## setItemMetaDataField + +Allows you to easily set the value of a metadata field in a list item + +```TypeScript +import { sp } from "@pnp/sp"; +import { taxonomy, setItemMetaDataMultiField } from "@pnp/sp-taxonomy"; + +// create a new item, or load an existing +const itemResult = await sp.web.lists.getByTitle("TaxonomyList").items.add({ + Title: "My Title", +}); + +// get a term +const term = await taxonomy.getDefaultSiteCollectionTermStore().getTermById("99992696-1111-1111-1111-15e65b221111").get(); + +setItemMetaDataMultiField(itemResult.item, "MetaDataFieldName", term); +``` + +## setItemMetaDataMultiField + +Allows you to easily set the value of a multi-value metadata field in a list item + +```TypeScript +import { sp } from "@pnp/sp"; +import { taxonomy, setItemMetaDataMultiField } from "@pnp/sp-taxonomy"; + +// create a new item, or load an existing +const itemResult = await sp.web.lists.getByTitle("TaxonomyList").items.add({ + Title: "My Title", +}); + +// get a term +const term = await taxonomy.getDefaultSiteCollectionTermStore().getTermById("99992696-1111-1111-1111-15e65b221111").get(); + +// get another term +const term2 = await taxonomy.getDefaultSiteCollectionTermStore().getTermById("99992696-1111-1111-1111-15e65b221112").get(); + +// get yet another term +const term3 = await taxonomy.getDefaultSiteCollectionTermStore().getTermById("99992696-1111-1111-1111-15e65b221113").get(); + +setItemMetaDataMultiField(itemResult.item, "MultiValueMetaDataFieldName", term, term2, term3); +``` diff --git a/packages/sp-taxonomy/index.ts b/packages/sp-taxonomy/index.ts new file mode 100644 index 000000000..9f5fc2d14 --- /dev/null +++ b/packages/sp-taxonomy/index.ts @@ -0,0 +1 @@ +export * from "./src/taxonomy"; diff --git a/packages/sp-taxonomy/package.json b/packages/sp-taxonomy/package.json new file mode 100644 index 000000000..1cf8a4571 --- /dev/null +++ b/packages/sp-taxonomy/package.json @@ -0,0 +1,29 @@ +{ + "name": "@pnp/sp-taxonomy", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - Provides a fluent API to work with SharePoint taxonomy", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/logging": "0.0.0-PLACEHOLDER", + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/odata": "0.0.0-PLACEHOLDER", + "@pnp/sp": "0.0.0-PLACEHOLDER", + "@pnp/sp-clientsvc": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnp/issues" + }, + "homepage": "https://github.com/pnp/pnp", + "repository": { + "type": "git", + "url": "git://github.com/pnp/pnp" + } +} \ No newline at end of file diff --git a/packages/sp-taxonomy/src/labels.ts b/packages/sp-taxonomy/src/labels.ts new file mode 100644 index 000000000..c1073ddfe --- /dev/null +++ b/packages/sp-taxonomy/src/labels.ts @@ -0,0 +1,122 @@ +import { + ClientSvcQueryable, + IClientSvcQueryable, + MethodParams, + ObjectPathQueue, + property, +} from "@pnp/sp-clientsvc"; +import { stringIsNullOrEmpty } from "@pnp/common"; + +/** + * Represents a collection of labels + */ +export interface ILabels extends IClientSvcQueryable { + /** + * Gets a label from the collection by its value + * + * @param value The value to retrieve + */ + getByValue(value: string): ILabel; + /** + * Loads the data and merges with with the ILabel instances + */ + get(): Promise<(ILabel & ILabelData)[]>; +} + +/** + * Represents a collection of labels + */ +export class Labels extends ClientSvcQueryable implements ILabels { + + constructor(parent: ClientSvcQueryable | string = "", _objectPaths: ObjectPathQueue | null = null) { + super(parent, _objectPaths); + + this._objectPaths.add(property("Labels")); + } + + /** + * Gets a label from the collection by its value + * + * @param value The value to retrieve + */ + public getByValue(value: string): ILabel { + + const params = MethodParams.build().string(value); + return this.getChild(Label, "GetByValue", params); + } + + /** + * Loads the data and merges with with the ILabel instances + */ + public get(): Promise<(ILabel & ILabelData)[]> { + return this.sendGetCollection((d: ILabelData) => { + + if (!stringIsNullOrEmpty(d.Value)) { + return this.getByValue(d.Value); + } + throw Error("Could not find Value in Labels.get(). You must include at least one of these in your select fields."); + }); + } +} + +/** + * Represents the data contained in a label + */ +export interface ILabelData { + /** + * Is this the default label for this language + */ + IsDefaultForLanguage?: boolean; + /** + * LCID language id + */ + Language?: number; + /** + * Label value + */ + Value?: string; +} + +/** + * Represents a label instance + */ +export interface ILabel extends IClientSvcQueryable { + /** + * Gets the data for this Label + */ + get(): Promise; + /** + * Sets this label as the default + */ + setAsDefaultForLanguage(): Promise; + /** + * Deletes this label + */ + delete(): Promise; +} + +/** + * Represents a label instance + */ +export class Label extends ClientSvcQueryable implements ILabel { + /** + * Gets the data for this Label + */ + public get(): Promise { + return this.sendGet(Label); + } + + /** + * Sets this label as the default + */ + public setAsDefaultForLanguage(): Promise { + return this.invokeNonQuery("SetAsDefaultForLanguage"); + } + + /** + * Deletes this label + */ + public delete(): Promise { + return this.invokeNonQuery("DeleteObject"); + } +} diff --git a/packages/sp-taxonomy/src/session.ts b/packages/sp-taxonomy/src/session.ts new file mode 100644 index 000000000..9b2a865e8 --- /dev/null +++ b/packages/sp-taxonomy/src/session.ts @@ -0,0 +1,88 @@ +import { SPConfiguration, sp } from "@pnp/sp"; +import { ClientSvcQueryable, IObjectPathBatch, ObjectPathBatch, objectPath, staticMethod } from "@pnp/sp-clientsvc"; +import { ITermStore, ITermStores, TermStore, TermStores } from "./termstores"; + +/** + * Defines the publicly visible members of Taxonomy + */ +export interface ITaxonomySession { + + /** + * The collection of term stores + */ + termStores: ITermStores; + + /** + * Provides access to sp.setup from @pnp/sp + * + * @param config Configuration + */ + setup(config: SPConfiguration): void; + + /** + * Creates a new batch + */ + createBatch(): IObjectPathBatch; + + /** + * Gets the default keyword termstore for this session + */ + getDefaultKeywordTermStore(): ITermStore; + + /** + * Gets the default site collection termstore for this session + */ + getDefaultSiteCollectionTermStore(): ITermStore; +} + +/** + * The root taxonomy object + */ +export class Session extends ClientSvcQueryable implements ITaxonomySession { + + constructor(webUrl = "") { + super(webUrl); + + // everything starts with the session + this._objectPaths.add(staticMethod("GetTaxonomySession", "{981cbc68-9edc-4f8d-872f-71146fcbb84f}", + // actions + objectPath())); + } + + /** + * The collection of term stores + */ + public get termStores(): ITermStores { + return new TermStores(this); + } + + /** + * Provides access to sp.setup from @pnp/sp + * + * @param config Configuration + */ + public setup(config: SPConfiguration): void { + sp.setup(config); + } + + /** + * Creates a new batch + */ + public createBatch(): IObjectPathBatch { + return new ObjectPathBatch(this.toUrl()); + } + + /** + * Gets the default keyword termstore for this session + */ + public getDefaultKeywordTermStore(): ITermStore { + return this.getChild(TermStore, "GetDefaultKeywordsTermStore", null); + } + + /** + * Gets the default site collection termstore for this session + */ + public getDefaultSiteCollectionTermStore(): ITermStore { + return this.getChild(TermStore, "GetDefaultSiteCollectionTermStore", null); + } +} diff --git a/packages/sp-taxonomy/src/taxonomy.ts b/packages/sp-taxonomy/src/taxonomy.ts new file mode 100644 index 000000000..a4e9490a0 --- /dev/null +++ b/packages/sp-taxonomy/src/taxonomy.ts @@ -0,0 +1,13 @@ +import { Session, ITaxonomySession } from "./session"; + +// export an existing session instance +export const taxonomy: ITaxonomySession = new Session(); + +export * from "./labels"; +export * from "./session"; +export * from "./termgroup"; +export * from "./terms"; +export * from "./termsets"; +export * from "./termstores"; +export * from "./types"; +export * from "./utilities"; diff --git a/packages/sp-taxonomy/src/termgroup.ts b/packages/sp-taxonomy/src/termgroup.ts new file mode 100644 index 000000000..772eabb60 --- /dev/null +++ b/packages/sp-taxonomy/src/termgroup.ts @@ -0,0 +1,195 @@ +import { extend, getGUID, sanitizeGuid, stringIsNullOrEmpty } from "@pnp/common"; +import { ClientSvcQueryable, IClientSvcQueryable, MethodParams, ObjectPathQueue } from "@pnp/sp-clientsvc"; +import { ITermSet, ITermSetData, ITermSets, TermSets } from "./termsets"; +import { ITermStore, TermStore } from "./termstores"; + +export interface ITermGroups extends IClientSvcQueryable { + get(): Promise<(ITermGroupData & ITermGroup)[]>; + getById(id: string): ITermGroup; + getByName(name: string): ITermGroup; +} + +export interface ITermGroupData { + CreatedDate?: string; + Description?: string; + Id?: string; + IsSiteCollectionGroup?: boolean; + IsSystemGroup?: boolean; + LastModifiedDate?: string; + Name?: string; +} + +export interface ITermGroup extends IClientSvcQueryable { + + /** + * ITermStore containing this TermGroup + */ + readonly store: ITermStore | null; + + /** + * Gets the collection of term sets in this group + */ + readonly termSets: ITermSets; + + /** + * Adds a contributor to the Group + * + * @param principalName The login name of the user to be added as a contributor + */ + addContributor(principalName: string): Promise; + /** + * Adds a group manager to the Group + * + * @param principalName The login name of the user to be added as a group manager + */ + addGroupManager(principalName: string): Promise; + /** + * Creates a new TermSet in this Group using the provided language and unique identifier + * + * @param name The name of the new TermSet being created + * @param lcid The language that the new TermSet name is in + * @param id The unique identifier of the new TermSet being created (optional) + */ + createTermSet(name: string, lcid: number, id?: string): Promise; + /** + * Gets this term store's data + */ + get(): Promise<(ITermGroupData & ITermGroup)>; + /** + * Updates the specified properties of this term set, not all properties can be updated + * + * @param properties Plain object representing the properties and new values to update + */ + update(properties: TermGroupUpdateProps): Promise; +} + +export type TermGroupUpdateProps = { + Description?: string, +}; + +/** + * Term Groups collection in Term Store + */ +export class TermGroups extends ClientSvcQueryable implements ITermGroups { + /** + * Gets the groups in this collection + */ + public get(): Promise<(ITermGroupData & ITermGroup)[]> { + return this.sendGetCollection((d: ITermGroupData) => { + if (!stringIsNullOrEmpty(d.Name)) { + return this.getByName(d.Name); + } else if (!stringIsNullOrEmpty(d.Id)) { + return this.getById(d.Id); + } + throw Error("Could not find Name or Id in TermGroups.get(). You must include at least one of these in your select fields."); + }); + } + + /** + * Gets a TermGroup from this collection by id + * + * @param id TermGroup id + */ + public getById(id: string): ITermGroup { + + const params = MethodParams.build() + .string(sanitizeGuid(id)); + + return this.getChild(TermGroup, "GetById", params); + } + + /** + * Gets a TermGroup from this collection by name + * + * @param name TErmGroup name + */ + public getByName(name: string): ITermGroup { + + const params = MethodParams.build() + .string(name); + + return this.getChild(TermGroup, "GetByName", params); + } +} + +/** + * Represents a group in the taxonomy heirarchy + */ +export class TermGroup extends ClientSvcQueryable implements ITermGroup { + + /** + * ITermStore containing this TermGroup + */ + public readonly store: ITermStore | null; + + constructor(parent: ClientSvcQueryable | string = "", _objectPaths?: ObjectPathQueue) { + super(parent, _objectPaths); + + // this should mostly be true + this.store = parent instanceof TermStore ? parent : null; + } + + /** + * Gets the collection of term sets in this group + */ + public get termSets(): ITermSets { + return this.getChildProperty(TermSets, "TermSets"); + } + + /** + * Adds a contributor to the Group + * + * @param principalName The login name of the user to be added as a contributor + */ + public addContributor(principalName: string): Promise { + + const params = MethodParams.build().string(principalName); + return this.invokeNonQuery("AddContributor", params); + } + + /** + * Adds a group manager to the Group + * + * @param principalName The login name of the user to be added as a group manager + */ + public addGroupManager(principalName: string): Promise { + + const params = MethodParams.build().string(principalName); + return this.invokeNonQuery("AddGroupManager", params); + } + + /** + * Creates a new TermSet in this Group using the provided language and unique identifier + * + * @param name The name of the new TermSet being created + * @param lcid The language that the new TermSet name is in + * @param id The unique identifier of the new TermSet being created (optional) + */ + public createTermSet(name: string, lcid: number, id = getGUID()): Promise { + + const params = MethodParams.build() + .string(name) + .string(sanitizeGuid(id)) + .number(lcid); + + this._useCaching = false; + return this.invokeMethod("CreateTermSet", params) + .then(r => extend(this.store.getTermSetById(r.Id), r)); + } + + /** + * Gets this term store's data + */ + public get(): Promise { + return this.sendGet(TermGroup); + } + + /** + * Updates the specified properties of this term set, not all properties can be updated + * + * @param properties Plain object representing the properties and new values to update + */ + public update(properties: TermGroupUpdateProps): Promise { + return this.invokeUpdate(properties, TermGroup); + } +} diff --git a/packages/sp-taxonomy/src/terms.ts b/packages/sp-taxonomy/src/terms.ts new file mode 100644 index 000000000..6d9b65ef2 --- /dev/null +++ b/packages/sp-taxonomy/src/terms.ts @@ -0,0 +1,231 @@ +import { extend, sanitizeGuid, stringIsNullOrEmpty, getGUID } from "@pnp/common"; +import { + ClientSvcQueryable, + IClientSvcQueryable, + MethodParams, + setProperty, +} from "@pnp/sp-clientsvc"; +import { ILabelData, ILabel, ILabels, Labels } from "./labels"; +import { ITermSet, TermSet, ITermSets, TermSets } from "./termsets"; + +export interface ITerms extends IClientSvcQueryable { + get(): Promise<(ITermData & ITerm)[]>; + getById(id: string): ITerm; + getByName(name: string): ITerm; +} + +export interface ITermData { + CustomProperties?: any; + CustomSortOrder?: any | null; + Description?: string; + Id?: string; + IsAvailableForTagging?: boolean; + IsDeprecated?: boolean; + IsKeyword?: boolean; + IsPinned?: boolean; + IsPinnedRoot?: boolean; + IsReused?: boolean; + IsRoot?: boolean; + IsSourceTerm?: boolean; + LastModifiedDate?: string; + LocalCustomProperties?: any; + MergedTermIds?: any[]; + Name?: string; + Owner?: string; + PathOfTerm?: string; + TermsCount?: number; +} + +export interface ITerm extends IClientSvcQueryable { + readonly labels: ILabels; + readonly parent: ITerm; + readonly pinSourceTermSet: ITermSet; + readonly reusedTerms: ITerms; + readonly sourceTerm: ITerm; + readonly terms: ITerms; + readonly termSet: ITermSet; + readonly termSets: ITermSets; + createLabel(name: string, lcid: number, isDefault?: boolean): Promise; + deprecate(doDeprecate: boolean): Promise; + get(): Promise<(ITermData & ITerm)>; + addTerm(name: string, lcid: number, isAvailableForTagging?: boolean, id?: string): Promise; + getDescription(lcid: number): Promise; + setDescription(description: string, lcid: number): Promise; + setLocalCustomProperty(name: string, value: string): Promise; + update(properties: { Name: string }): Promise; +} + +export class Terms extends ClientSvcQueryable implements ITerms { + + /** + * Gets the terms in this collection + */ + public get(): Promise<(ITermData & ITerm)[]> { + return this.sendGetCollection((d: ITermData) => { + + if (!stringIsNullOrEmpty(d.Name)) { + return this.getByName(d.Name); + } else if (!stringIsNullOrEmpty(d.Id)) { + return this.getById(d.Id); + } + throw Error("Could not find Name or Id in Terms.get(). You must include at least one of these in your select fields."); + }); + } + + /** + * Gets a term by id + * + * @param id The id of the term + */ + public getById(id: string): ITerm { + const params = MethodParams.build() + .string(sanitizeGuid(id)); + + return this.getChild(Term, "GetById", params); + } + + /** + * Gets a term by name + * + * @param name Term name + */ + public getByName(name: string): ITerm { + + const params = MethodParams.build() + .string(name); + + return this.getChild(Term, "GetByName", params); + } +} + +/** + * Represents the operations available on a given term + */ +export class Term extends ClientSvcQueryable implements ITerm { + + public addTerm(name: string, lcid: number, isAvailableForTagging = true, id = getGUID()): Promise { + + const params = MethodParams.build() + .string(name) + .number(lcid) + .string(sanitizeGuid(id)); + + this._useCaching = false; + return this.invokeMethod("CreateTerm", params, + setProperty("IsAvailableForTagging", "Boolean", `${isAvailableForTagging}`)) + .then(r => extend(this.termSet.getTermById(r.Id), r)); + } + + public get terms(): ITerms { + return this.getChildProperty(Terms, "Terms"); + } + + public get labels(): ILabels { + return new Labels(this); + } + + public get parent(): ITerm { + return this.getChildProperty(Term, "Parent"); + } + + public get pinSourceTermSet(): ITermSet { + return this.getChildProperty(TermSet, "PinSourceTermSet"); + } + + public get reusedTerms(): ITerms { + return this.getChildProperty(Terms, "ReusedTerms"); + } + + public get sourceTerm(): ITerm { + return this.getChildProperty(Term, "SourceTerm"); + } + + public get termSet(): ITermSet { + return this.getChildProperty(TermSet, "TermSet"); + } + + public get termSets(): ITermSets { + return this.getChildProperty(TermSets, "TermSets"); + } + + /** + * Creates a new label for this Term + * + * @param name label value + * @param lcid language code + * @param isDefault Is the default label + */ + public createLabel(name: string, lcid: number, isDefault = false): Promise { + + const params = MethodParams.build() + .string(name) + .number(lcid) + .boolean(isDefault); + + this._useCaching = false; + return this.invokeMethod("CreateLabel", params) + .then(r => extend(this.labels.getByValue(name), r)); + } + + /** + * Sets the deprecation flag on a term + * + * @param doDeprecate New value for the deprecation flag + */ + public deprecate(doDeprecate: boolean): Promise { + + const params = MethodParams.build().boolean(doDeprecate); + return this.invokeNonQuery("Deprecate", params); + } + + /** + * Loads the term data + */ + public get(): Promise<(ITermData & ITerm)> { + return this.sendGet(Term); + } + + /** + * Gets the appropriate description for a term + * + * @param lcid Language code + */ + public getDescription(lcid: number): Promise { + + const params = MethodParams.build().number(lcid); + return this.invokeMethodAction("GetDescription", params); + } + + /** + * Sets the description + * + * @param description Term description + * @param lcid Language code + */ + public setDescription(description: string, lcid: number): Promise { + + const params = MethodParams.build().string(description).number(lcid); + return this.invokeNonQuery("SetDescription", params); + } + + /** + * Sets a custom property on this term + * + * @param name Property name + * @param value Property value + */ + public setLocalCustomProperty(name: string, value: string): Promise { + + const params = MethodParams.build().string(name).string(value); + return this.invokeNonQuery("SetLocalCustomProperty", params); + } + + /** + * Updates the specified properties of this term, not all properties can be updated + * + * @param properties Plain object representing the properties and new values to update + */ + public update(properties: { Name: string }): Promise { + return this.invokeUpdate(properties, Term); + } +} diff --git a/packages/sp-taxonomy/src/termsets.ts b/packages/sp-taxonomy/src/termsets.ts new file mode 100644 index 000000000..3c6e53413 --- /dev/null +++ b/packages/sp-taxonomy/src/termsets.ts @@ -0,0 +1,183 @@ +import { extend, getGUID, sanitizeGuid, stringIsNullOrEmpty } from "@pnp/common"; +import { ClientSvcQueryable, IClientSvcQueryable, MethodParams, setProperty } from "@pnp/sp-clientsvc"; +import { ITermGroup, TermGroup } from "./termgroup"; +import { ITerm, ITermData, ITerms, Term, Terms } from "./terms"; + +export interface ITermSets extends IClientSvcQueryable { + getById(id: string): ITermSet; + getByName(name: string): ITermSet; + get(): Promise<(ITermSetData & ITermSet)[]>; +} + +export interface ITermSetData { + Contact?: string; + CreatedDate?: string; + CustomProperties?: any; + CustomSortOrder?: any | null; + Description?: string; + Id?: string; + IsAvailableForTagging?: boolean; + IsOpenForTermCreation?: boolean; + LastModifiedDate?: string; + Name?: string; + Names?: { [key: number]: string }; + Owner?: string; + Stakeholders?: string[]; +} + +export class TermSets extends ClientSvcQueryable implements ITermSets { + + /** + * Gets the termsets in this collection + */ + public get(): Promise<(ITermSetData & ITermSet)[]> { + return this.sendGetCollection((d: ITermSetData) => { + if (!stringIsNullOrEmpty(d.Name)) { + return this.getByName(d.Name); + } else if (!stringIsNullOrEmpty(d.Id)) { + return this.getById(d.Id); + } + throw Error("Could not find Value in Labels.get(). You must include at least one of these in your select fields."); + }); + } + + /** + * Gets a TermSet from this collection by id + * + * @param id TermSet id + */ + public getById(id: string): ITermSet { + + const params = MethodParams.build() + .string(sanitizeGuid(id)); + + return this.getChild(TermSet, "GetById", params); + } + + /** + * Gets a TermSet from this collection by name + * + * @param name TermSet name + */ + public getByName(name: string): ITermSet { + + const params = MethodParams.build() + .string(name); + + return this.getChild(TermSet, "GetByName", params); + } +} + +export interface ITermSet extends IClientSvcQueryable { + readonly terms: ITerms; + readonly group: ITermGroup; + copy(): Promise; + get(): Promise<(ITermSetData & ITermSet)>; + getTermById(id: string): ITerm; + addTerm(name: string, lcid: number, isAvailableForTagging?: boolean, id?: string): Promise; + update(properties: TermSetUpdateProps): Promise; +} + +export type TermSetUpdateProps = { + Contact?: string, + Description?: string, + IsOpenForTermCreation?: boolean, +}; + +export class TermSet extends ClientSvcQueryable implements ITermSet { + + /** + * Gets the group containing this Term set + */ + public get group(): ITermGroup { + return this.getChildProperty(TermGroup, "Group"); + } + + /** + * Access all the terms in this termset + */ + public get terms(): ITerms { + return this.getChild(Terms, "GetAllTerms", null); + } + + /** + * Adds a stakeholder to the TermSet + * + * @param stakeholderName The login name of the user to be added as a stakeholder + */ + public addStakeholder(stakeholderName: string): Promise { + const params = MethodParams.build() + .string(stakeholderName); + + return this.invokeNonQuery("DeleteStakeholder", params); + } + + /** + * Deletes a stakeholder to the TermSet + * + * @param stakeholderName The login name of the user to be added as a stakeholder + */ + public deleteStakeholder(stakeholderName: string): Promise { + const params = MethodParams.build() + .string(stakeholderName); + + return this.invokeNonQuery("AddStakeholder", params); + } + + /** + * Gets the data for this TermSet + */ + public get(): Promise { + return this.sendGet(TermSet); + } + + /** + * Get a term by id + * + * @param id Term id + */ + public getTermById(id: string): ITerm { + + const params = MethodParams.build() + .string(sanitizeGuid(id)); + + return this.getChild(Term, "GetTerm", params); + } + + /** + * Adds a term to this term set + * + * @param name Name for the term + * @param lcid Language code + * @param isAvailableForTagging set tagging availability (default: true) + * @param id GUID id for the term (optional) + */ + public addTerm(name: string, lcid: number, isAvailableForTagging = true, id = getGUID()): Promise { + + const params = MethodParams.build() + .string(name) + .number(lcid) + .string(sanitizeGuid(id)); + + this._useCaching = false; + return this.invokeMethod("CreateTerm", params, + setProperty("IsAvailableForTagging", "Boolean", `${isAvailableForTagging}`)) + .then(r => extend(this.getTermById(r.Id), r)); + } + + /** + * Copies this term set immediately + */ + public copy(): Promise { + return this.invokeMethod("Copy", null); + } + + /** + * Updates the specified properties of this term set, not all properties can be updated + * + * @param properties Plain object representing the properties and new values to update + */ + public update(properties: TermSetUpdateProps): Promise { + return this.invokeUpdate(properties, TermSet); + } +} diff --git a/packages/sp-taxonomy/src/termstores.ts b/packages/sp-taxonomy/src/termstores.ts new file mode 100644 index 000000000..b189ea996 --- /dev/null +++ b/packages/sp-taxonomy/src/termstores.ts @@ -0,0 +1,392 @@ +import { extend, getGUID, sanitizeGuid, stringIsNullOrEmpty } from "@pnp/common"; +import { ClientSvcQueryable, IClientSvcQueryable, MethodParams, ObjectPathQueue, method, objConstructor, objectPath, objectProperties, opQuery, property } from "@pnp/sp-clientsvc"; +import { ITermGroup, ITermGroupData, TermGroup, ITermGroups, TermGroups } from "./termgroup"; +import { ITerm, ITerms, Term, Terms } from "./terms"; +import { ITermSet, ITermSets, TermSet, TermSets } from "./termsets"; +import { ChangeInformation, ChangedItem, ILabelMatchInfo } from "./types"; + +/** + * Defines the visible members of the term store + */ +export interface ITermStores extends IClientSvcQueryable { + get(): Promise<(ITermStoreData & ITermStore)[]>; + getByName(name: string): ITermStore; + getById(id: string): ITermStore; +} + +/** + * Represents the set of available term stores and the collection methods + */ +export class TermStores extends ClientSvcQueryable implements ITermStores { + + constructor(parent: ClientSvcQueryable | string = "") { + super(parent); + + this._objectPaths.add(property("TermStores", + // actions + objectPath())); + } + + /** + * Gets the term stores + */ + public get(): Promise<(ITermStoreData & ITermStore)[]> { + return this.sendGetCollection((d: ITermStoreData): ITermStore => { + + if (!stringIsNullOrEmpty(d.Name)) { + return this.getByName(d.Name); + } else if (!stringIsNullOrEmpty(d.Id)) { + return this.getById(d.Id); + } + throw Error("Could not find Name or Id in TermStores.get(). You must include at least one of these in your select fields."); + }); + } + + /** + * Returns the TermStore specified by its index name + * + * @param name The index name of the TermStore to be returned + */ + public getByName(name: string): ITermStore { + return this.getChild(TermStore, "GetByName", MethodParams.build().string(name)); + } + + /** + * Returns the TermStore specified by its GUID index + * + * @param id The GUID index of the TermStore to be returned + */ + public getById(id: string): ITermStore { + return this.getChild(TermStore, "GetById", MethodParams.build().string(sanitizeGuid(id))); + } +} + +/** + * Defines the term store object + */ +export interface ITermStore extends IClientSvcQueryable { + readonly hashTagsTermSet: ITermSet; + readonly keywordsTermSet: ITermSet; + readonly orphanedTermsTermSet: ITermSet; + readonly systemGroup: ITermGroup; + readonly groups: ITermGroups; + addGroup(name: string, id?: string): Promise; + addLanguage(lcid: number): Promise; + commitAll(): Promise; + deleteLanguage(lcid: number): Promise; + get(): Promise<(ITermStoreData & ITermStore)>; + getChanges(info: ChangeInformation): Promise; + getSiteCollectionGroup(createIfMissing?: boolean): ITermGroup; + getTermById(id: string): ITerm; + getTermInTermSet(termId: string, termSetId: string): ITerm; + getTermGroupById(id: string): ITermGroup; + getTerms(info: ILabelMatchInfo): ITerms; + getTermsById(...ids: string[]): any; + getTermSetById(id: string): ITermSet; + getTermSetsByName(name: string, lcid: number): ITermSets; + rollbackAll(): Promise; + update(properties: TermStoreUpdateProps): Promise; + updateCache(): Promise; + updateUsedTermsOnSite(): Promise; +} + +/** + * Defines the term store object + */ +export interface ITermStoreData { + DefaultLanguage?: number; + Id?: string; + IsOnline?: boolean; + Languages?: string[]; + Name?: string; + WorkingLanguage?: number; +} + +export type TermStoreUpdateProps = { + DefaultLanguage?: number, + WorkingLanguage?: number, +}; + +export class TermStore extends ClientSvcQueryable implements ITermStore { + + constructor(parent: ClientSvcQueryable | string = "", _objectPaths: ObjectPathQueue | null = null) { + super(parent, _objectPaths); + } + + public get hashTagsTermSet(): ITermSet { + return this.getChildProperty(TermSet, "HashTagsTermSet"); + } + + public get keywordsTermSet(): ITermSet { + return this.getChildProperty(TermSet, "KeywordsTermSet"); + } + + public get orphanedTermsTermSet(): ITermSet { + return this.getChildProperty(TermSet, "OrphanedTermsTermSet"); + } + + public get systemGroup(): ITermGroup { + return this.getChildProperty(TermGroup, "SystemGroup"); + } + + public get groups(): ITermGroups { + return this.getChildProperty(TermGroups, "Groups"); + } + + /** + * Gets the term store data + */ + public get(): Promise<(ITermStoreData & ITermStore)> { + return this.sendGet(TermStore); + } + + /** + * Gets term sets + * + * @param name + * @param lcid + */ + public getTermSetsByName(name: string, lcid: number): ITermSets { + + const params = MethodParams.build() + .string(name) + .number(lcid); + + return this.getChild(TermSets, "GetTermSetsByName", params); + } + + /** + * Provides access to an ITermSet by id + * + * @param id + */ + public getTermSetById(id: string): ITermSet { + + const params = MethodParams.build().string(sanitizeGuid(id)); + return this.getChild(TermSet, "GetTermSet", params); + } + + /** + * Provides access to an ITermSet by id + * + * @param id + */ + public getTermById(id: string): ITerm { + + const params = MethodParams.build().string(sanitizeGuid(id)); + return this.getChild(Term, "GetTerm", params); + } + + /** + * Provides access to an ITermSet by id + * + * @param id + */ + public getTermsById(...ids: string[]): ITerms { + + const params = MethodParams.build().strArray(ids.map(id => sanitizeGuid(id))); + return this.getChild(Terms, "GetTermsById", params); + } + + /** + * Gets a term from a term set based on the supplied ids + * + * @param termId Term Id + * @param termSetId Termset Id + */ + public getTermInTermSet(termId: string, termSetId: string): ITerm { + + const params = MethodParams.build().string(sanitizeGuid(termId)).string(sanitizeGuid(termSetId)); + return this.getChild(Term, "GetTermInTermSet", params); + } + + /** + * This method provides access to a ITermGroup by id + * + * @param id The group id + */ + public getTermGroupById(id: string): ITermGroup { + + const params = MethodParams.build() + .string(sanitizeGuid(id)); + + return this.getChild(TermGroup, "GetGroup", params); + } + + /** + * Gets the terms by the supplied information (see: https://msdn.microsoft.com/en-us/library/hh626704%28v=office.12%29.aspx) + * + * @param info + */ + public getTerms(info: ILabelMatchInfo): ITerms { + + const objectPaths = this._objectPaths.copy(); + + // this will be the parent of the GetTerms call, but we need to create the input param first + const parentIndex = objectPaths.lastIndex; + + // this is our input object + const input = objConstructor("{61a1d689-2744-4ea3-a88b-c95bee9803aa}", + // actions + objectPath(), + ...objectProperties(info), + ); + + // add the input object path + const inputIndex = objectPaths.add(input); + + // this sets up the GetTerms call + const params = MethodParams.build().objectPath(inputIndex); + + // call the method + const methodIndex = objectPaths.add(method("GetTerms", params, + // actions + objectPath())); + + // setup the parent relationship even though they are seperated in the collection + objectPaths.addChildRelationship(parentIndex, methodIndex); + + return new Terms(this, objectPaths); + } + + /** + * Gets the site collection group associated with the current site + * + * @param createIfMissing If true the group will be created, otherwise null (default: false) + */ + public getSiteCollectionGroup(createIfMissing = false): ITermGroup { + + const objectPaths = this._objectPaths.copy(); + const methodParent = objectPaths.lastIndex; + const siteIndex = objectPaths.siteIndex; + + const params = MethodParams.build().objectPath(siteIndex).boolean(createIfMissing); + + const methodIndex = objectPaths.add(method("GetSiteCollectionGroup", params, + // actions + objectPath(), + )); + + // the parent of this method call is this instance, not the current/site + objectPaths.addChildRelationship(methodParent, methodIndex); + + return new TermGroup(this, objectPaths); + } + + /** + * Adds a working language to the TermStore + * + * @param lcid The locale identifier of the working language to add + */ + public addLanguage(lcid: number): Promise { + + const params = MethodParams.build().number(lcid); + return this.invokeNonQuery("AddLanguage", params); + } + + /** + * Creates a new Group in this TermStore + * + * @param name The name of the new Group being created + * @param id The ID (Guid) that the new group should have + */ + public addGroup(name: string, id = getGUID()): Promise { + + const params = MethodParams.build() + .string(name) + .string(sanitizeGuid(id)); + + this._useCaching = false; + return this.invokeMethod("CreateGroup", params) + .then(r => extend(this.getTermGroupById(r.Id), r)); + } + + /** + * Commits all updates to the database that have occurred since the last commit or rollback + */ + public commitAll(): Promise { + return this.invokeNonQuery("CommitAll"); + } + + /** + * Delete a working language from the TermStore + * + * @param lcid locale ID for the language to be deleted + */ + public deleteLanguage(lcid: number): Promise { + + const params = MethodParams.build().number(lcid); + return this.invokeNonQuery("DeleteLanguage", params); + } + + /** + * Discards all updates that have occurred since the last commit or rollback + */ + public rollbackAll(): Promise { + return this.invokeNonQuery("RollbackAll"); + } + + /** + * Updates the cache + */ + public updateCache(): Promise { + return this.invokeNonQuery("UpdateCache"); + } + + /** + * Updates the specified properties of this term set, not all properties can be updated + * + * @param properties Plain object representing the properties and new values to update + */ + public update(properties: TermStoreUpdateProps): Promise { + return this.invokeUpdate(properties, TermStore); + } + + /** + * This method makes sure that this instance is aware of all child terms that are used in the current site collection + */ + public updateUsedTermsOnSite(): Promise { + + const objectPaths = this._objectPaths.copy(); + const methodParent = objectPaths.lastIndex; + const siteIndex = objectPaths.siteIndex; + + const params = MethodParams.build().objectPath(siteIndex); + + const methodIndex = objectPaths.add(method("UpdateUsedTermsOnSite", params)); + + // the parent of this method call is this instance, not the current context/site + objectPaths.addChildRelationship(methodParent, methodIndex); + + return this.send(objectPaths); + } + + /** + * Gets a list of changes + * + * @param info Lookup information + */ + public getChanges(info: ChangeInformation): Promise { + + const objectPaths = this._objectPaths.copy(); + const methodParent = objectPaths.lastIndex; + + const inputIndex = objectPaths.add(objConstructor("{1f849fb0-4fcb-4a54-9b01-9152b9e482d3}", + // actions + objectPath(), + ...objectProperties(info), + )); + + const params = MethodParams.build().objectPath(inputIndex); + + const methodIndex = objectPaths.add(method("GetChanges", params, + // actions + objectPath(), + opQuery([], this.getSelects()), + )); + + objectPaths.addChildRelationship(methodParent, methodIndex); + + return this.send(objectPaths); + } +} diff --git a/packages/sp-taxonomy/src/types.ts b/packages/sp-taxonomy/src/types.ts new file mode 100644 index 000000000..82b56a852 --- /dev/null +++ b/packages/sp-taxonomy/src/types.ts @@ -0,0 +1,90 @@ +export enum StringMatchOption { + StartsWith = 0, + ExactMatch = 1, +} + +export interface TimeSpan { + Days: number; + Hours: number; + Milliseconds: number; + Minutes: number; + Seconds: number; + Ticks: number; + TotalDays: number; + TotalHours: number; + TotalMilliseconds: number; + TotalMinutes: number; + TotalSeconds: number; +} + +export interface ILabelMatchInfo { + DefaultLabelOnly?: boolean; + ExcludeKeyword?: boolean; + Lcid?: number; + ResultCollectionSize?: number; + StringMatchOption?: StringMatchOption; + TermLabel: string; + TrimDeprecated?: boolean; + TrimUnavailable?: boolean; +} + +export enum ChangedItemType { + Unknown, + Term, + TermSet, + Group, + TermStore, + Site, +} + +export enum ChangedOperationType { + Unknown, + Add, + Edit, + DeleteObject, + Move, + Copy, + PathChange, + Merge, + ImportObject, + Restore, +} + +export interface ChangedItem { + ChangedBy: string; + ChangedTime: string; + Id: string; + ItemType: ChangedItemType; + Operation: ChangedOperationType; + + // Changed Site + SiteId?: string; + TermId?: string; + + // Changed Term + ChangedCustomProperties?: string[]; + ChangedLocalCustomProperties?: string[]; + LcidsForChangedDescriptions?: number[]; + LcidsForChangedLabels?: number[]; + + // Changed Term & Site + TermSetId?: string; + + // Changed Termset + FromGroupId?: string; + + // Changed Termset and Term + GroupId?: string; + + // Changed TermStore + ChangedLanguage?: number; + IsDefaultLanguageChanged?: boolean; + IsFullFarmRestore?: boolean; +} + +export interface ChangeInformation { + ItemType?: ChangedItemType; + OperationType?: ChangedOperationType; + StartTime?: string; + WithinTimeSpan?: TimeSpan; +} diff --git a/packages/sp-taxonomy/src/utilities.ts b/packages/sp-taxonomy/src/utilities.ts new file mode 100644 index 000000000..b3b904c48 --- /dev/null +++ b/packages/sp-taxonomy/src/utilities.ts @@ -0,0 +1,35 @@ +import { sanitizeGuid, TypedHash, objectDefinedNotNull } from "@pnp/common"; +import { IItem, IItemUpdateResult } from "@pnp/sp/src/items/types"; +import { ITermData } from "./terms"; + +export function setItemMetaDataField(item: IItem, fieldName: string, term: ITermData): Promise { + + if (!objectDefinedNotNull(term)) { + return Promise.resolve(null); + } + + const postData: TypedHash = {}; + postData[fieldName] = { + "Label": term.Name, + "TermGuid": sanitizeGuid(term.Id), + "WssId": "-1", + "__metadata": { "type": "SP.Taxonomy.TaxonomyFieldValue" }, + }; + + return item.update(postData); +} + +export function setItemMetaDataMultiField(item: IItem, fieldName: string, ...terms: ITermData[]): Promise { + + if (terms.length < 1) { + return Promise.resolve(null); + } + + return item.list.fields.getByTitle(`${fieldName}_0`).select("InternalName").get<{ InternalName: string}>().then(i => { + + const postData: TypedHash = {}; + postData[i.InternalName] = terms.map(term => `-1;#${term.Name}|${sanitizeGuid(term.Id)};#`).join(""); + + return item.update(postData); + }); +} diff --git a/packages/sp-taxonomy/tsconfig.es5.json b/packages/sp-taxonomy/tsconfig.es5.json new file mode 100644 index 000000000..ef0ffb284 --- /dev/null +++ b/packages/sp-taxonomy/tsconfig.es5.json @@ -0,0 +1,37 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "strictNullChecks": false + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts", + "../sp-clientsvc/index.ts", + "../sp-clientsvc/src/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + }, + { + "path": "../odata/tsconfig.es5.json" + }, + { + "path": "../sp/tsconfig.es5.json" + }, + { + "path": "../sp-clientsvc/tsconfig.es5.json" + } + ] +} diff --git a/packages/sp-taxonomy/tsconfig.json b/packages/sp-taxonomy/tsconfig.json new file mode 100644 index 000000000..4bdb1cf04 --- /dev/null +++ b/packages/sp-taxonomy/tsconfig.json @@ -0,0 +1,37 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strictNullChecks": false + }, + "include": [ + "index.ts", + "src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "../sp/index.ts", + "../sp/src/**/*.ts", + "../sp-clientsvc/index.ts", + "../sp-clientsvc/src/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + }, + { + "path": "../odata" + }, + { + "path": "../sp" + }, + { + "path": "../sp-clientsvc" + } + ] +} diff --git a/packages/sp/__tests/alias.test.ts b/packages/sp/__tests/alias.test.ts new file mode 100644 index 000000000..497ed8905 --- /dev/null +++ b/packages/sp/__tests/alias.test.ts @@ -0,0 +1,58 @@ +import { expect } from "chai"; +import { testSettings } from "../../../test/main"; +import { sp, Web, IWeb } from "../presets/legacy"; +import { combine } from "@pnp/common"; + +describe("Alias Parameters", () => { + + let webRelativeUrl = ""; + let web: IWeb; + + before((done) => { + + // we need to take some steps to ensure we are operating on the correct web here + // due to the url manipulation in the library for sharing + web = Web(testSettings.sp.webUrl); + + web.select("ServerRelativeUrl", "Url").get().then(u => { + + // make sure we have the correct server relative url + webRelativeUrl = u.ServerRelativeUrl; + + // we need a doc lib with a file and folder in it + web.lists.ensure("AliasTestLib", "Used to test alias parameters", 101).then(ler => { + + // add a file and folder + Promise.all([ + ler.list.rootFolder.folders.add("MyTestFolder"), + ler.list.rootFolder.files.add("text.txt", "Some file content!"), + ]).then(_ => { + done(); + }).catch(_ => { + done(); + }); + }).catch(_ => { + done(); + }); + }); + }); + + if (testSettings.enableWebTests) { + + it("Should allow aliasing for folders", () => { + + return expect(sp.web.getFolderByServerRelativeUrl(`!@p1::/${combine(webRelativeUrl, "AliasTestLib/MyTestFolder")}`).get()).to.eventually.be.fulfilled; + }); + + it("Should allow aliasing for files", () => { + + return expect(sp.web.getFileByServerRelativeUrl(`!@p1::/${combine(webRelativeUrl, "AliasTestLib/text.txt")}`).get()).to.eventually.be.fulfilled; + }); + + it("Should allow aliasing for sub-parameters", () => { + + const folder = sp.web.getFolderByServerRelativeUrl(`!@p1::/${combine(webRelativeUrl, "AliasTestLib/MyTestFolder")}`); + return expect(folder.files.add("!@p2::myfilename.txt", "new file content")).to.eventually.be.fulfilled; + }); + } +}); diff --git a/packages/sp/__tests/batch.test.ts b/packages/sp/__tests/batch.test.ts new file mode 100644 index 000000000..2cb4bda8e --- /dev/null +++ b/packages/sp/__tests/batch.test.ts @@ -0,0 +1,109 @@ +import { expect } from "chai"; +import { Web } from "../"; +import { testSettings } from "../../../test/main"; + +describe("Batching", () => { + + if (testSettings.enableWebTests) { + + it("Should execute batches in the expected order for a single request", () => { + + const web = new Web(testSettings.sp.webUrl); + + const order: number[] = []; + + const batch = web.createBatch(); + + web.inBatch(batch).get().then(_ => { + order.push(1); + }); + + return expect(batch.execute().then(_ => { + order.push(2); + return order; + })).to.eventually.be.fulfilled.and.eql([1, 2]); + }); + + it("Should execute batches in the expected order for an even number of requests", () => { + + const web = new Web(testSettings.sp.webUrl); + + const order: number[] = []; + + const batch = web.createBatch(); + + web.inBatch(batch).get().then(_ => { + order.push(1); + }); + + web.lists.inBatch(batch).get().then(_ => { + order.push(2); + }); + + web.lists.top(2).inBatch(batch).get().then(_ => { + order.push(3); + }); + + web.lists.select("Title").inBatch(batch).get().then(_ => { + order.push(4); + }); + + return expect(batch.execute().then(_ => { + order.push(5); + return order; + })).to.eventually.be.fulfilled.and.eql([1, 2, 3, 4, 5]); + }); + + it("Should execute batches in the expected order for an odd number of requests", () => { + + const web = new Web(testSettings.sp.webUrl); + + const order: number[] = []; + + const batch = web.createBatch(); + + web.inBatch(batch).get().then(_ => { + order.push(1); + }); + + web.lists.inBatch(batch).get().then(_ => { + order.push(2); + }); + + web.lists.top(2).inBatch(batch).get().then(_ => { + order.push(3); + }); + + return expect(batch.execute().then(_ => { + order.push(4); + return order; + })).to.eventually.be.fulfilled.and.eql([1, 2, 3, 4]); + }); + + it("Should execute batches that have internally cloned requests", () => { + + const web = new Web(testSettings.sp.webUrl); + + const order: number[] = []; + + const batch = web.createBatch(); + + return expect(web.lists.ensure("BatchItemAddTest").then(ler => { + + const list = ler.list; + + return list.getListItemEntityTypeFullName().then(ent => { + + list.items.inBatch(batch).add({ Title: "Hello 1" }, ent).then(_ => order.push(1)); + + list.items.inBatch(batch).add({ Title: "Hello 2" }, ent).then(_ => order.push(2)); + + return batch.execute().then(_ => { + order.push(3); + return order; + }); + }); + })).to.eventually.eql([1, 2, 3]); + }); + } +}); diff --git a/packages/sp/__tests/clientsidepages.test.ts b/packages/sp/__tests/clientsidepages.test.ts new file mode 100644 index 000000000..64a52dd55 --- /dev/null +++ b/packages/sp/__tests/clientsidepages.test.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import { sp } from "../"; +import { testSettings } from "../../../test/main"; +import { getRandomString, combine } from "@pnp/common"; +import { + ClientSideText, + ClientSideWebpart, + ClientSideWebpartPropertyTypes, + ClientSidePage, +} from "../src/clientsidepages"; + +describe("Client-side Page", () => { + + if (testSettings.enableWebTests) { + + describe("create", () => { + + it("Should create a new page", () => { + return expect(sp.web.addClientSidePage(`TestingAdd_${getRandomString(4)}.aspx`)).to.eventually.be.fulfilled; + }); + }); + + describe("load", function () { + + const pageFileName = `TestingAdd_${getRandomString(4)}.aspx`; + + before(done => { + sp.web.addClientSidePage(pageFileName).then(_ => { + done(); + }); + }); + + it("Should load from an existing file", () => { + + // need to make the path relative + const rel = testSettings.sp.webUrl.substr(testSettings.sp.webUrl.indexOf("/sites/")); + const promise = ClientSidePage.fromFile(sp.web.getFileByServerRelativeUrl(combine("/", rel, "SitePages", pageFileName))); + return expect(promise).to.eventually.be.fulfilled; + }); + }); + + describe("save", () => { + + it("Should update a pages content with a text control", () => { + return sp.web.addClientSidePage(`TestingAdd_${getRandomString(4)}.aspx`).then(page => { + + page.addSection().addControl(new ClientSideText("This is test text!!!")); + + return expect(page.save()).to.eventually.be.fulfilled; + }); + }); + + it("Should update a pages content with an embed control", () => { + return sp.web.getClientSideWebParts().then(parts => { + + sp.web.addClientSidePage(`TestingAdd_${getRandomString(4)}.aspx`).then(page => { + + const part = ClientSideWebpart.fromComponentDef(parts.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa")[0]); + + part.setProperties({ + embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg", + }); + + page.addSection().addControl(part); + + return expect(page.save()).to.eventually.be.fulfilled; + }); + }); + }); + }); + + describe("Page comments", () => { + + let page: ClientSidePage; + + before(done => { + sp.web.addClientSidePage(`TestingAdd_${getRandomString(4)}.aspx`).then(p => { + page = p; + done(); + }); + }); + + it("Should disable", () => { + return expect(page.disableComments()).to.eventually.be.fulfilled; + }); + + it("Should enable", () => { + return expect(page.enableComments()).to.eventually.be.fulfilled; + }); + }); + } +}); diff --git a/packages/sp/__tests/configure.test.ts b/packages/sp/__tests/configure.test.ts new file mode 100644 index 000000000..9611f9050 --- /dev/null +++ b/packages/sp/__tests/configure.test.ts @@ -0,0 +1,185 @@ +import { expect } from "chai"; +import { sp } from "../"; +import { testSettings } from "../../../test/main"; +import { SPFetchClient } from "@pnp/nodejs"; +import { MockFetchClient } from "./mock-fetchclient"; + +describe("Custom options", () => { + const mockFetch = new MockFetchClient(); + const headerName = "my-header"; + const headerValue = "my header value"; + const headers: any = {}; + headers[headerName] = headerValue; + headers["X-RequestDigest"] = "test"; + + before(() => { + sp.setup({ + sp: { + fetchClientFactory: () => { + return mockFetch; + }, + }, + }); + }); + + after(() => { + if (testSettings.enableWebTests) { + sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient(testSettings.sp.webUrl, testSettings.sp.id, testSettings.sp.secret); + }, + }, + }); + } + }); + + it("Should set header when getting a web and configuring global SPRests", () => { + return sp.configure({ + headers: headers, + }).web.get() + .then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal(headerValue); + }); + }); + + it("Should set header when making a post request using getParent method", () => { + return sp.configure({ + headers: headers, + }).web.features.getById("test").deactivate() + .then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal(headerValue); + }); + }); + + it("Should set header when getting a web and applying headers for web only", () => { + return sp.web.configure({ + headers: headers, + }).get() + .then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal(headerValue); + }); + }); + + it("Should override header when setting headers on a web", () => { + const webHeaders: any = {}; + webHeaders[headerName] = "web's value"; + return sp.configure(headers).web.configure({ + headers: webHeaders, + }).get() + .then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal("web's value"); + }); + }); + + it("Should add another header when setting headers on a web", () => { + const webHeaders: any = {}; + webHeaders["new-header"] = "web's value"; + return sp.configure(headers).web.configure({ + headers: webHeaders, + }).get() + .then(() => { + const header = mockFetch.options.headers.get("new-header"); + expect(header).to.equal("web's value"); + }); + }); + + it("Should use the same header for all requests", () => { + const sp2 = sp.configure({ + headers: headers, + }); + const validate = () => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal(headerValue); + mockFetch.options = null; + }; + return sp2.site.get() + .then(() => { + validate(); + return sp2.web.get(); + }) + .then(() => { + validate(); + return sp2.web.fields.add("test", "Text", { FieldTypeKind: 8 }); + }) + .then(() => { + validate(); + }); + }); + + it("Should use different headers for requests", () => { + const webHeaders: any = {}; + webHeaders["new-header"] = "web's value"; + const sp2 = sp.configure({ + headers: headers, + }); + + return sp2.site.get() + .then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal(headerValue); + return sp.web.get(); + }) + .then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.be.null; + }); + }); + + it("Should set correct options when getting a web and configuring global SPRests", () => { + return sp.configure({ + cache: "no-store", + credentials: "omit", + mode: "cors", + }).web.get() + .then(() => { + const mode = mockFetch.options.mode; + const cache = mockFetch.options.cache; + const creds = mockFetch.options.credentials; + + expect(mode).to.equal("cors"); + expect(cache).to.equal("no-store"); + expect(creds).to.equal("omit"); + }); + }); + + it("Should override options when applying on child objects", () => { + return sp.configure({ + cache: "no-store", + credentials: "omit", + mode: "cors", + }).web.configure({ + cache: "default", + mode: "navigate", + }).get() + .then(() => { + + const mode = mockFetch.options.mode; + const cache = mockFetch.options.cache; + + expect(mode).to.equal("navigate"); + expect(cache).to.equal("default"); + }); + }); + + it("Should set options when using clone method", () => { + const webHeaders: any = {}; + webHeaders[headerName] = "myvalue"; + webHeaders["X-RequestDigest"] = "1234"; + + return sp.configure({ + headers: webHeaders, + }).utility.sendEmail({ + Body: "pnpjs", + Subject: "test mail", + To: ["some@mail.com"], + }).then(() => { + const header = mockFetch.options.headers.get(headerName); + expect(header).to.equal("myvalue"); + }); + }); +}); diff --git a/packages/sp/__tests/contenttypes.test.ts b/packages/sp/__tests/contenttypes.test.ts new file mode 100644 index 000000000..10e5cfbde --- /dev/null +++ b/packages/sp/__tests/contenttypes.test.ts @@ -0,0 +1,70 @@ +import { expect } from "chai"; +import { ContentTypes, ContentType } from "../src/contenttypes"; +import { sp } from "../"; +import { testSettings } from "../../../test/main"; +import { toMatchEndRegex } from "./utils"; + +describe("ContentTypes", () => { + it("Should be an object", () => { + const contenttypes = new ContentTypes("_api/web/lists/getByTitle('Tasks')"); + expect(contenttypes).to.be.a("object"); + }); + describe("url", () => { + it("Should return _api/web/lists/getByTitle('Tasks')/contenttypes", () => { + const contenttypes = new ContentTypes("_api/web/lists/getByTitle('Tasks')"); + expect(contenttypes.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/contenttypes")); + }); + }); + describe("getById", () => { + it("Should return _api/web/lists/getByTitle('Tasks')/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')", () => { + const contenttypes = new ContentTypes("_api/web/lists/getByTitle('Tasks')"); + const ct = contenttypes.getById("0x0101000BB1B729DCB7414A9344ED650D3C05B3").toUrl(); + expect(ct).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')")); + }); + }); + + if (testSettings.enableWebTests) { + + describe("getById", () => { + it("Should return the item content type", () => { + return expect(sp.web.contentTypes.getById("0x01").get()).to.eventually.be.fulfilled; + }); + }); + } +}); + +describe("ContentType", () => { + let contentType: ContentType; + + beforeEach(() => { + contentType = new ContentType("_api/web", "contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')"); + }); + + it("Should be an object", () => { + expect(contentType).to.be.an("object"); + }); + + describe("fieldLinks", () => { + it("Should return _api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/fieldLinks", () => { + expect(contentType.fieldLinks.toUrl()).to.match(toMatchEndRegex("_api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/fieldlinks")); + }); + }); + + describe("fields", () => { + it("Should return _api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/fields", () => { + expect(contentType.fields.toUrl()).to.match(toMatchEndRegex("_api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/fields")); + }); + }); + + describe("parent", () => { + it("Should return _api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/parent", () => { + expect(contentType.parent.toUrl()).to.match(toMatchEndRegex("_api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/parent")); + }); + }); + + describe("workflowAssociations", () => { + it("Should return _api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/workflowAssociations", () => { + expect(contentType.workflowAssociations.toUrl()).to.match(toMatchEndRegex("_api/web/contenttypes('0x0101000BB1B729DCB7414A9344ED650D3C05B3')/workflowAssociations")); + }); + }); +}); diff --git a/packages/sp/__tests/errors.test.ts b/packages/sp/__tests/errors.test.ts new file mode 100644 index 000000000..559f6b93a --- /dev/null +++ b/packages/sp/__tests/errors.test.ts @@ -0,0 +1,51 @@ +// this file tests that we are actually producing errors where we should be producing errors +import { expect } from "chai"; +import { testSettings } from "../../../test/main"; +import { sp } from "../"; + +describe("Errors", () => { + + before(function (done) { + + if (testSettings.enableWebTests) { + + // setup a list with a single item we know we can try and update + sp.web.lists.ensure("ErrorTestingList").then(result => { + + result.list.items.add({ + Title: "An Item", + }).then(_ => { + done(); + }).catch(_ => { + done(); + }); + + }).catch(_ => { + done(); + }); + + } else { + + done(); + } + }); + + if (testSettings.enableWebTests) { + + describe("List", () => { + it("Add should fail and produce a catchable error", () => { + + return expect(sp.web.lists.getByTitle("ErrorTestingList").items.add({ + Titttle: "This is a fake value for a fake field", + })).to.eventually.be.rejected; + }); + + it("Update should fail and produce a catchable error", () => { + + return expect(sp.web.lists.getByTitle("ErrorTestingList").items.getById(1).update({ + Titttle: "This is a fake value for a fake field", + })).to.eventually.be.rejected; + }); + }); + } +}); diff --git a/packages/sp/__tests/fields.test.ts b/packages/sp/__tests/fields.test.ts new file mode 100644 index 000000000..3adfbbcc9 --- /dev/null +++ b/packages/sp/__tests/fields.test.ts @@ -0,0 +1,53 @@ +import { combine } from "@pnp/common"; +import { expect } from "chai"; +import { Fields, Field } from "../src/fields"; +import { toMatchEndRegex } from "./utils"; + +describe("Fields", () => { + + const basePath = "_api/web/lists/getByTitle('Tasks')"; + let fields: Fields; + + beforeEach(() => { + fields = new Fields(basePath); + }); + + it("Should be an object", () => { + expect(fields).to.be.a("object"); + }); + + describe("url", () => { + const path: string = combine(basePath, "fields"); + it("Should return " + path, () => { + expect(fields.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("getByTitle", () => { + const path: string = combine(basePath, "fields/getByTitle('Title')"); + it("Should return " + path, () => { + expect(fields.getByTitle("Title").toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("getById", () => { + const path: string = combine(basePath, "fields('cc1322c5-376d-4b8a-87cb-1e21330c6df2')"); + it("Should return " + path, () => { + expect(fields.getById("cc1322c5-376d-4b8a-87cb-1e21330c6df2").toUrl()).to.match(toMatchEndRegex(path)); + }); + }); +}); + +describe("Field", () => { + + const basePath = "_api/web/lists/getByTitle('Tasks')/fields/getByTitle('Title')"; + let field: Field; + + beforeEach(() => { + field = new Field(basePath); + }); + + it("Should be an object", () => { + expect(field).to.be.a("object"); + }); +}); diff --git a/packages/sp/__tests/files.test.ts b/packages/sp/__tests/files.test.ts new file mode 100644 index 000000000..22934aa64 --- /dev/null +++ b/packages/sp/__tests/files.test.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import { Files, File, Versions, Version } from "../src/files"; +import { toMatchEndRegex } from "./utils"; + +describe("Files", () => { + + let files: Files; + + beforeEach(() => { + files = new Files("_api/web"); + }); + + it("Should be an object", () => { + expect(files).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/files", () => { + expect(files.toUrl()).to.match(toMatchEndRegex("_api/web/files")); + }); + }); + + describe("getByName", () => { + it("Should return _api/web/files('Doug Baldwin')", () => { + const file = files.getByName("Doug Baldwin"); + expect(file.toUrl()).to.match(toMatchEndRegex("_api/web/files('Doug Baldwin')")); + }); + }); +}); + +describe("File", () => { + + let file: File; + + beforeEach(() => { + file = new File("_api/web/files", "getByName('Thomas Rawls')"); + }); + + it("Should be an object", () => { + expect(file).to.be.a("object"); + }); + + describe("listItemAllFields", () => { + it("Should return _api/web/files/getByName('Thomas Rawls')/listItemAllFields", () => { + expect(file.listItemAllFields.toUrl()).to.match(toMatchEndRegex("_api/web/files/getByName('Thomas Rawls')/listItemAllFields")); + }); + }); + + describe("versions", () => { + it("Should return _api/web/files/getByName('Thomas Rawls')/versions", () => { + expect(file.versions.toUrl()).to.match(toMatchEndRegex("_api/web/files/getByName('Thomas Rawls')/versions")); + }); + }); +}); + +describe("Versions", () => { + + let versions: Versions; + + beforeEach(() => { + versions = new Versions("_api/web/getFileByServerRelativeUrl('Earl Thomas')"); + }); + + it("Should be an object", () => { + expect(versions).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/getFileByServerRelativeUrl('Earl Thomas')/versions", () => { + expect(versions.toUrl()).to.match(toMatchEndRegex("_api/web/getFileByServerRelativeUrl('Earl Thomas')/versions")); + }); + }); + + describe("getById", () => { + it("Should return _api/web/getFileByServerRelativeUrl('Earl Thomas')/versions(1)", () => { + const version = versions.getById(1); + expect(version.toUrl()).to.match(toMatchEndRegex("_api/web/getFileByServerRelativeUrl('Earl Thomas')/versions(1)")); + }); + }); +}); + +describe("Version", () => { + + let version: Version; + + beforeEach(() => { + version = new Version("_api/web/getFileByServerRelativeUrl('Richard Sherman')", "versions(1)"); + }); + + it("Should be an object", () => { + expect(version).to.be.a("object"); + }); +}); diff --git a/packages/sp/__tests/folders.test.ts b/packages/sp/__tests/folders.test.ts new file mode 100644 index 000000000..a711b6a01 --- /dev/null +++ b/packages/sp/__tests/folders.test.ts @@ -0,0 +1,94 @@ +import { expect } from "chai"; +import { Folders, Folder } from "../src/folders"; +import { toMatchEndRegex } from "./utils"; + +describe("Folders", () => { + + let folders: Folders; + + beforeEach(() => { + folders = new Folders("_api/web"); + }); + + it("Should be an object", () => { + expect(folders).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/folders", () => { + expect(folders.toUrl()).to.match(toMatchEndRegex("_api/web/folders")); + }); + }); + + describe("getByName", () => { + it("Should return _api/web/folders('Russell Wilson')", () => { + const folder = folders.getByName("Russell Wilson"); + expect(folder.toUrl()).to.match(toMatchEndRegex("_api/web/folders('Russell Wilson')")); + }); + }); +}); + +describe("Folder", () => { + + let folder: Folder; + + beforeEach(() => { + folder = new Folder("_api/web/folders", "getByName('Marshawn Lynch')"); + }); + + it("Should be an object", () => { + expect(folder).to.be.a("object"); + }); + + describe("contentTypeOrder", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/contentTypeOrder", () => { + expect(folder.contentTypeOrder.toUrl()) + .to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/contentTypeOrder")); + }); + }); + + describe("files", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/files", () => { + expect(folder.files.toUrl()).to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/files")); + }); + }); + + describe("folders", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/folders", () => { + expect(folder.folders.toUrl()).to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/folders")); + }); + }); + + describe("listItemAllFields", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/listItemAllFields", () => { + expect(folder.listItemAllFields.toUrl()) + .to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/listItemAllFields")); + }); + }); + + describe("parentFolder", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/parentFolder", () => { + expect(folder.parentFolder.toUrl()).to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/parentFolder")); + }); + }); + + describe("properties", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/properties", () => { + expect(folder.properties.toUrl()).to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/properties")); + }); + }); + + describe("serverRelativeUrl", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/serverRelativeUrl", () => { + expect(folder.serverRelativeUrl.toUrl()) + .to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/serverRelativeUrl")); + }); + }); + + describe("uniqueContentTypeOrder", () => { + it("Should return _api/web/folders/getByName('Marshawn Lynch')/uniqueContentTypeOrder", () => { + expect(folder.uniqueContentTypeOrder.toUrl()) + .to.match(toMatchEndRegex("_api/web/folders/getByName('Marshawn Lynch')/uniqueContentTypeOrder")); + }); + }); +}); diff --git a/packages/sp/__tests/items.test.ts b/packages/sp/__tests/items.test.ts new file mode 100644 index 000000000..d9a0b6bb7 --- /dev/null +++ b/packages/sp/__tests/items.test.ts @@ -0,0 +1,101 @@ +import { combine } from "@pnp/common"; +import { expect } from "chai"; +import { Items, Item } from "../"; +import { toMatchEndRegex } from "./utils"; + +describe("Items", () => { + + const basePath = "_api/web/lists/getByTitle('Tasks')"; + let items: Items; + + beforeEach(() => { + items = new Items(basePath); + }); + + it("Should be an object", () => { + expect(items).to.be.a("object"); + }); + + describe("url", () => { + const path = combine(basePath, "items"); + it("Should return " + path, () => { + expect(items.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + describe("getById", () => { + const path = combine(basePath, "items(1)"); + it("Should return " + path, () => { + expect(items.getById(1).toUrl()).to.match(toMatchEndRegex(path)); + }); + }); +}); + +describe("Item", () => { + + const basePath = "_api/web/lists/getByTitle('Tasks')/items(1)"; + let item: Item; + + beforeEach(() => { + item = new Item(basePath); + }); + + it("Should be an object", () => { + expect(item).to.be.a("object"); + }); + + describe("attachmentFiles", () => { + const path = combine(basePath, "AttachmentFiles"); + it("Should return " + path, () => { + expect(item.attachmentFiles.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("contentType", () => { + const path = combine(basePath, "ContentType"); + it("Should return " + path, () => { + expect(item.contentType.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("effectiveBasePermissions", () => { + const path = combine(basePath, "EffectiveBasePermissions"); + it("Should return " + path, () => { + expect(item.effectiveBasePermissions.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("effectiveBasePermissionsForUI", () => { + const path = combine(basePath, "EffectiveBasePermissionsForUI"); + it("Should return " + path, () => { + expect(item.effectiveBasePermissionsForUI.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("fieldValuesAsHTML", () => { + const path = combine(basePath, "FieldValuesAsHTML"); + it("Should return " + path, () => { + expect(item.fieldValuesAsHTML.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("fieldValuesAsText", () => { + const path = combine(basePath, "FieldValuesAsText"); + it("Should return " + path, () => { + expect(item.fieldValuesAsText.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("fieldValuesForEdit", () => { + const path = combine(basePath, "FieldValuesForEdit"); + it("Should return " + path, () => { + expect(item.fieldValuesForEdit.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); + + describe("folder", () => { + const path = combine(basePath, "folder"); + it("Should return " + path, () => { + expect(item.folder.toUrl()).to.match(toMatchEndRegex(path)); + }); + }); +}); diff --git a/packages/sp/__tests/lists.test.ts b/packages/sp/__tests/lists.test.ts new file mode 100644 index 000000000..8b1a1f55d --- /dev/null +++ b/packages/sp/__tests/lists.test.ts @@ -0,0 +1,352 @@ +import { getRandomString } from "@pnp/common"; +import { expect } from "chai"; +import { ControlMode, List, Lists, PageType, sp } from "../"; +import { testSettings } from "../../../test/main"; +import { toMatchEndRegex } from "./utils"; + +describe("Lists", () => { + + let lists: Lists; + + beforeEach(() => { + lists = new Lists("_api/web"); + }); + + it("Should be an object", () => { + expect(lists).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/lists", () => { + expect(lists.toUrl()).to.match(toMatchEndRegex("_api/web/lists")); + }); + }); + + describe("getByTitle", () => { + it("Should return _api/web/lists/getByTitle('Tasks')", () => { + const list = lists.getByTitle("Tasks"); + expect(list.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')")); + }); + }); + + describe("getById", () => { + it("Should return _api/web/lists('4FC65058-FDDE-4FAD-AB21-2E881E1CF527')", () => { + const list = lists.getById("4FC65058-FDDE-4FAD-AB21-2E881E1CF527"); + expect(list.toUrl()).to.match(toMatchEndRegex("_api/web/lists('4FC65058-FDDE-4FAD-AB21-2E881E1CF527')")); + }); + }); + + describe("getById with {}", () => { + it("Should return _api/web/lists('{4FC65058-FDDE-4FAD-AB21-2E881E1CF527}')", () => { + const list = lists.getById("{4FC65058-FDDE-4FAD-AB21-2E881E1CF527}"); + expect(list.toUrl()).to.match(toMatchEndRegex("_api/web/lists('{4FC65058-FDDE-4FAD-AB21-2E881E1CF527}')")); + }); + }); + + if (testSettings.enableWebTests) { + + describe("getByTitle", () => { + it("Should get a list by title with the expected title", () => { + + // we are expecting that the OOTB list exists + return expect(sp.web.lists.getByTitle("Documents").get()).to.eventually.have.property("Title", "Documents"); + }); + }); + + describe("getById", () => { + it("Should get a list by id with the expected title", () => { + return expect(sp.web.lists.getByTitle("Documents").select("ID").get<{ Id: string }>().then((list) => { + return sp.web.lists.getById(list.Id).select("Title").get(); + })).to.eventually.have.property("Title", "Documents"); + }); + }); + + describe("add", () => { + it("Should add a list with the expected title", () => { + + const title = `Test_ListAdd_${getRandomString(8)}`; + + return expect(sp.web.lists.add(title).then(() => { + return sp.web.lists.getByTitle(title).select("Title").get(); + })).to.eventually.have.property("Title", title); + }); + }); + + describe("ensure", () => { + it("Should ensure a list with the expected title", () => { + return expect(sp.web.lists.ensure("pnp testing ensure").then(() => { + return sp.web.lists.getByTitle("pnp testing ensure").select("Title").get(); + })).to.eventually.have.property("Title", "pnp testing ensure"); + }); + }); + + describe("ensureSiteAssetsLibrary", () => { + it("Should ensure that the site assets library exists", () => { + return expect(sp.web.lists.ensureSiteAssetsLibrary()).to.eventually.be.fulfilled; + }); + }); + + describe("ensureSitePagesLibrary", () => { + it("Should ensure that the site pages library exists", () => { + return expect(sp.web.lists.ensureSitePagesLibrary()).to.eventually.be.fulfilled; + }); + }); + } +}); + +describe("List", () => { + + let list: List; + + beforeEach(() => { + list = new List("_api/web/lists", "getByTitle('Tasks')"); + }); + + it("Should be an object", () => { + expect(list).to.be.a("object"); + }); + + describe("contentTypes", () => { + it("should return _api/web/lists/getByTitle('Tasks')/contenttypes", () => { + expect(list.contentTypes.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/contenttypes")); + }); + }); + + describe("items", () => { + it("should return _api/web/lists/getByTitle('Tasks')/items", () => { + expect(list.items.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/items")); + }); + }); + + describe("views", () => { + it("should return _api/web/lists/getByTitle('Tasks')/views", () => { + expect(list.views.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/views")); + }); + }); + + describe("fields", () => { + it("should return _api/web/lists/getByTitle('Tasks')/fields", () => { + expect(list.fields.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/fields")); + }); + }); + + describe("defaultView", () => { + it("should return _api/web/lists/getByTitle('Tasks')/DefaultView", () => { + expect(list.defaultView.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/DefaultView")); + }); + }); + + describe("effectiveBasePermissions", () => { + it("should return _api/web/lists/getByTitle('Tasks')/EffectiveBasePermissions", () => { + expect(list.effectiveBasePermissions.toUrl()) + .to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/EffectiveBasePermissions")); + }); + }); + + describe("eventReceivers", () => { + it("should return _api/web/lists/getByTitle('Tasks')/EventReceivers", () => { + expect(list.eventReceivers.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/EventReceivers")); + }); + }); + + describe("relatedFields", () => { + it("should return _api/web/lists/getByTitle('Tasks')/getRelatedFields", () => { + expect(list.relatedFields.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/getRelatedFields")); + }); + }); + + describe("informationRightsManagementSettings", () => { + it("should return _api/web/lists/getByTitle('Tasks')/InformationRightsManagementSettings", () => { + expect(list.informationRightsManagementSettings.toUrl()) + .to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/InformationRightsManagementSettings")); + }); + }); + + describe("userCustomActions", () => { + it("should return _api/web/lists/getByTitle('Tasks')/usercustomactions", () => { + expect(list.userCustomActions.toUrl()) + .to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/usercustomactions")); + }); + }); + + describe("getView", () => { + it("should return _api/web/lists/getByTitle('Tasks')/getView('b81b1b32-ed0a-4b80-bd16-67c99a4f3c1c')", () => { + expect(list.getView("b81b1b32-ed0a-4b80-bd16-67c99a4f3c1c").toUrl()) + .to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/getView('b81b1b32-ed0a-4b80-bd16-67c99a4f3c1c')")); + }); + }); + + if (testSettings.enableWebTests) { + + describe("contentTypes", () => { + it("should return a list of content types on the list", () => { + return expect(sp.web.lists.getByTitle("Documents").contentTypes.get()).to.eventually.be.fulfilled; + }); + }); + + describe("items", () => { + it("should return items from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").items.get()).to.eventually.be.fulfilled; + }); + }); + + describe("views", () => { + it("should return views from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").views.get()).to.eventually.be.fulfilled; + }); + }); + + describe("fields", () => { + it("should return fields from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").fields.get()).to.eventually.be.fulfilled; + }); + }); + + describe("defaultView", () => { + it("should return the default view from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").defaultView.get()).to.eventually.be.fulfilled; + }); + }); + + describe("userCustomActions", () => { + it("should return the user custom actions from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").userCustomActions.get()).to.eventually.be.fulfilled; + }); + }); + + describe("effectiveBasePermissions", () => { + it("should return the effective base permissions from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").effectiveBasePermissions.get()).to.eventually.be.fulfilled; + }); + }); + + describe("eventReceivers", () => { + it("should return the event receivers from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").eventReceivers.get()).to.eventually.be.fulfilled; + }); + }); + + describe("relatedFields", () => { + it("should return the related fields from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").relatedFields.get()).to.eventually.be.fulfilled; + }); + }); + + describe("informationRightsManagementSettings", () => { + it("should return the information rights management settings from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").informationRightsManagementSettings.get()) + .to.eventually.be.fulfilled; + }); + }); + + describe("getView", () => { + it("should return the default view by id from the list", () => { + return expect(sp.web.lists.getByTitle("Documents").defaultView.select("Id").get().then(v => { + return sp.web.lists.getByTitle("Documents").getView(v.Id).get(); + })).to.eventually.be.fulfilled; + }); + }); + + describe("update", () => { + it("should create a new list, update the title, and then ensure it is set as expected", () => { + const newTitle = "I have a new title"; + return expect(sp.web.lists.ensure("pnp testing list update").then(result => { + return result.list.update({ + Title: newTitle, + }).then(result2 => { + return result2.list.select("Title").get(); + }); + })).to.eventually.have.property("Title", newTitle); + }); + }); + + describe("delete", () => { + it("should create a new list, delete it, and then ensure it is gone", () => { + return expect(sp.web.lists.ensure("pnp testing list delete").then(result => { + return result.list.delete().then(() => { + return result.list.select("Title").get(); + }); + })).to.eventually.be.rejected; + }); + }); + + describe("getChanges", () => { + it("should get a list of changes", () => { + return expect(sp.web.lists.getByTitle("Documents").getChanges({ + Add: true, + DeleteObject: true, + Restore: true, + })).to.eventually.be.fulfilled; + }); + }); + + /* tslint:disable */ + describe("getItemsByCAMLQuery", () => { + it("should get items based on the supplied CAML query", () => { + let caml = { + ViewXml: "5" + }; + return expect(sp.web.lists.getByTitle("Documents").getItemsByCAMLQuery(caml, "RoleAssignments")).to.eventually.be.fulfilled; + }); + }); + /* tslint:enable */ + + describe("getListItemChangesSinceToken", () => { + it("should get items based on the supplied change query"); + }); + + describe("recycle", () => { + it("should create a new list, recycle it, and then ensure it is gone", () => { + return expect(sp.web.lists.ensure("pnp testing list recycle").then(result => { + return result.list.recycle().then(recycleResponse => { + if (typeof recycleResponse !== "string") { + throw Error("Expected a string returned from recycle."); + } + return result.list.select("Title").get(); + }); + })).to.eventually.be.rejected; + }); + }); + + describe("renderListData", () => { + it("should return a set of data which can be used to render an html view of the list", () => { + // create a list, add some items, render a view + return expect(sp.web.lists.ensure("pnp testing renderListData").then(result => { + return Promise.all([ + result.list.items.add({ Title: "Item 1" }), + result.list.items.add({ Title: "Item 2" }), + result.list.items.add({ Title: "Item 3" }), + ]).then(() => { + return result.list; + }); + }).then(l => { + return l.renderListData("5"); + })).to.eventually.have.property("Row").that.is.not.empty; + }); + }); + + describe("renderListFormData", () => { + it("should return a set of data which can be used to render an html view of the list", () => { + // create a list, add an item, get the form, render that form + return expect(sp.web.lists.ensure("pnp testing renderListFormData").then(result => { + return result.list.items.add({ Title: "Item 1" }).then(() => { + return result.list; + }); + }).then(l => { + return l.forms.select("Id").filter(`FormType eq ${PageType.DisplayForm}`).get().then(f => { + return l.renderListFormData(1, f[0].Id, ControlMode.Display); + }); + })).to.eventually.have.property("ListData").that.is.not.null; + }); + }); + + describe("reserveListItemId", () => { + it("should return a number", () => { + // create a list, reserve an item id + return expect(sp.web.lists.ensure("pnp testing reserveListItemId").then(result => { + return result.list.reserveListItemId(); + })).to.eventually.be.a("number"); + }); + }); + } +}); diff --git a/packages/sp/__tests/mock-fetchclient.ts b/packages/sp/__tests/mock-fetchclient.ts new file mode 100644 index 000000000..099da73a6 --- /dev/null +++ b/packages/sp/__tests/mock-fetchclient.ts @@ -0,0 +1,24 @@ +const nodeFetch = require("node-fetch"); +declare var global: any; + +import { FetchOptions, HttpClientImpl } from "@pnp/common"; + +export class MockFetchClient implements HttpClientImpl { + + public options: any; + + constructor() { + global.Headers = nodeFetch.Headers; + global.Request = nodeFetch.Request; + global.Response = nodeFetch.Response; + } + + public fetch(_0: string, options: FetchOptions): Promise { + this.options = options; + const response = new Response("{}", { + status: 200, + }); + + return Promise.resolve(response); + } +} diff --git a/packages/sp/__tests/navigation.test.ts b/packages/sp/__tests/navigation.test.ts new file mode 100644 index 000000000..83db2bdd1 --- /dev/null +++ b/packages/sp/__tests/navigation.test.ts @@ -0,0 +1,16 @@ +import { expect } from "chai"; +import { Navigation } from "../src/navigation"; +import { toMatchEndRegex } from "./utils"; + +describe("Navigation", () => { + it("Should be an object", () => { + const navigation = new Navigation("_api/web"); + expect(navigation).to.be.a("object"); + }); + describe("url", () => { + it("Should return _api/web/Navigation", () => { + const navigation = new Navigation("_api/web"); + expect(navigation.toUrl()).to.match(toMatchEndRegex("_api/web/navigation")); + }); + }); +}); diff --git a/packages/sp/__tests/roles.test.ts b/packages/sp/__tests/roles.test.ts new file mode 100644 index 000000000..4bcc4d614 --- /dev/null +++ b/packages/sp/__tests/roles.test.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import { + RoleAssignment, + RoleAssignments, + RoleDefinitions, +} from "../src/roles"; +import { toMatchEndRegex } from "./utils"; + +describe("RoleAssignments", () => { + it("Should be an object", () => { + const roleAssignments = new RoleAssignments("_api/web"); + expect(roleAssignments).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/roleassignments", () => { + const roleAssignments = new RoleAssignments("_api/web"); + expect(roleAssignments.toUrl()).to.match(toMatchEndRegex("_api/web/roleassignments")); + }); + }); + + describe("getById", () => { + it("Should return _api/web/roleassignments(1)", () => { + const roleAssignments = new RoleAssignments("_api/web"); + expect(roleAssignments.getById(1).toUrl()).to.match(toMatchEndRegex("_api/web/roleassignments(1)")); + }); + }); +}); + +describe("RoleAssignment", () => { + + const baseUrl = "_api/web/roleassignments(1)"; + + it("Should be an object", () => { + const roleAssignment = new RoleAssignment(baseUrl); + expect(roleAssignment).to.be.a("object"); + }); + + describe("groups", () => { + it("Should return " + baseUrl + "/groups", () => { + const roleAssignment = new RoleAssignment(baseUrl); + expect(roleAssignment.groups.toUrl()).to.match(toMatchEndRegex(baseUrl + "/groups")); + }); + }); + + describe("bindings", () => { + it("Should return " + baseUrl + "/roledefinitionbindings", () => { + const roleAssignment = new RoleAssignment(baseUrl); + expect(roleAssignment.bindings.toUrl()).to.match(toMatchEndRegex(baseUrl + "/roledefinitionbindings")); + }); + }); +}); + +describe("RoleDefinitions", () => { + + const baseUrl = "_api/web"; + + let roleDefinitions: RoleDefinitions; + + beforeEach(() => { + roleDefinitions = new RoleDefinitions(baseUrl); + }); + + it("Should be an object", () => { + expect(roleDefinitions).to.be.a("object"); + }); + + describe("getById", () => { + it("Should return " + baseUrl + "/roledefinitions/getbyid(1)", () => { + expect(roleDefinitions.getById(1).toUrl()).to.match(toMatchEndRegex(baseUrl + "/roledefinitions/getbyid(1)")); + }); + }); + + describe("getByName", () => { + it("Should return " + baseUrl + "/getbyname('name')", () => { + expect(roleDefinitions.getByName("name").toUrl()).to.match(toMatchEndRegex(baseUrl + "/roledefinitions/getbyname('name')")); + }); + }); + + describe("getByType", () => { + it("Should return " + baseUrl + "/getbytype(1)", () => { + expect(roleDefinitions.getByType(1).toUrl()).to.match(toMatchEndRegex(baseUrl + "/roledefinitions/getbytype(1)")); + }); + }); +}); diff --git a/packages/sp/__tests/search.test.ts b/packages/sp/__tests/search.test.ts new file mode 100644 index 000000000..247567b87 --- /dev/null +++ b/packages/sp/__tests/search.test.ts @@ -0,0 +1,9 @@ +import { expect } from "chai"; +import { Search } from "../src/search"; + +describe("Search", () => { + it("Should be an object", () => { + const searchquery = new Search("_api"); + expect(searchquery).to.be.a("object"); + }); +}); diff --git a/packages/sp/__tests/sharing.inactive.ts b/packages/sp/__tests/sharing.inactive.ts new file mode 100644 index 000000000..b1581e840 --- /dev/null +++ b/packages/sp/__tests/sharing.inactive.ts @@ -0,0 +1,348 @@ +import { expect } from "chai"; +import { testSettings } from "../../../test/main"; +import { combine } from "@pnp/common"; +import { File, Folder, Item, SharingRole, Web } from "../"; + +describe("Sharing", () => { + + let webAbsUrl = ""; + let webRelativeUrl = ""; + let web: Web; + + before((done) => { + + // we need to take some steps to ensure we are operating on the correct web here + // due to the url manipulation in the library for sharing + web = new Web(testSettings.sp.webUrl); + + web.select("ServerRelativeUrl", "Url").get().then(u => { + + // make sure we have the correct server relative url + webRelativeUrl = u.ServerRelativeUrl; + webAbsUrl = u.Url; + + // we need a doc lib with a file and folder in it + web.lists.ensure("SharingTestLib", "Used to test sharing", 101).then(ler => { + + // add a file and folder + Promise.all([ + ler.list.rootFolder.folders.add("MyTestFolder"), + ler.list.rootFolder.files.add("text.txt", "Some file content!"), + ]).then(_ => { + done(); + }).catch(_ => { + done(); + }); + }).catch(_ => { + done(); + }); + }); + }); + + if (testSettings.enableWebTests) { + + describe("can operate on folders", () => { + + let folder: Folder = null; + + before(() => { + + folder = web.getFolderByServerRelativeUrl("/" + combine(webRelativeUrl, "SharingTestLib/MyTestFolder")); + }); + + // // these tests cover share link + it("Should get a sharing link with default settings.", () => { + + return expect(folder.getShareLink()) + .to.eventually.be.fulfilled + .and.have.property("sharingLinkInfo") + .and.have.deep.property("Url").that.is.not.null; + }); + + // it("Should get a sharing link with a specified kind.", () => { + // return expect(folder.getShareLink(SharingLinkKind.AnonymousView)) + // .to.eventually.be.fulfilled + // .and.have.property("sharingLinkInfo") + // .and.have.deep.property("Url").that.is.not.null; + // }); + + // it("Should get a sharing link with a specified kind and expiration.", () => { + // return expect(folder.getShareLink(SharingLinkKind.AnonymousView, Util.dateAdd(new Date(), "day", 5))) + // .to.eventually.be.fulfilled + // .and.have.property("sharingLinkInfo") + // .and.have.deep.property("Url").that.is.not.null; + // }); + + it("Should allow sharing to a person with default settings.", () => { + + return expect(folder.shareWith("c:0(.s|true")) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role.", () => { + + return expect(folder.shareWith("c:0(.s|true", SharingRole.Edit)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role and share all content.", () => { + + return expect(folder.shareWith("c:0(.s|true", SharingRole.Edit, true)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow for checking of sharing permissions.", () => { + + return expect(folder.checkSharingPermissions([{ alias: "c:0(.s|true" }])) + .to.eventually.be.fulfilled; + }); + + it("Should allow getting Sharing Information.", () => { + + return expect(folder.getSharingInformation()) + .to.eventually.be.fulfilled; + }); + + it("Should allow getting Object Sharing Settings.", () => { + + return expect(folder.getObjectSharingSettings(true)) + .to.eventually.be.fulfilled; + }); + + it("Should allow unsharing.", () => { + + return expect(folder.unshare()) + .to.eventually.be.fulfilled; + }); + + // it("Should allow deleting a link by kind.", () => { + + // return expect(folder.getShareLink(SharingLinkKind.AnonymousView).then(_ => { + + // return folder.deleteSharingLinkByKind(SharingLinkKind.AnonymousView); + // })).to.eventually.be.fulfilled; + // }); + + // it("Should allow unsharing a link by kind.", () => { + + // return expect(folder.getShareLink(SharingLinkKind.AnonymousView).then(response => { + + // return folder.unshareLink(SharingLinkKind.AnonymousView, response.sharingLinkInfo.ShareId); + // })).to.eventually.be.fulfilled; + // }); + }); + + describe("can operate on files", () => { + + let file: File = null; + + before(() => { + + file = web.getFileByServerRelativeUrl("/" + combine(webRelativeUrl, "SharingTestLib/text.txt")); + }); + + it("Should get a sharing link with default settings.", () => { + + return expect(file.getShareLink()) + .to.eventually.be.fulfilled + .and.have.property("sharingLinkInfo") + .and.have.deep.property("Url").that.is.not.null; + }); + + // it("Should get a sharing link with a specified kind.", () => { + // return expect(file.getShareLink(SharingLinkKind.AnonymousView)) + // .to.eventually.be.fulfilled + // .and.have.property("sharingLinkInfo") + // .and.have.deep.property("Url").that.is.not.null; + // }); + + // it("Should get a sharing link with a specified kind and expiration.", () => { + // return expect(file.getShareLink(SharingLinkKind.AnonymousView, Util.dateAdd(new Date(), "day", 5))) + // .to.eventually.be.fulfilled + // .and.have.property("sharingLinkInfo") + // .and.have.deep.property("Url").that.is.not.null; + // }); + + it("Should allow sharing to a person with default settings.", () => { + + return expect(file.shareWith("c:0(.s|true")) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role.", () => { + + return expect(file.shareWith("c:0(.s|true", SharingRole.Edit)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role and require sign-in.", () => { + + return expect(file.shareWith("c:0(.s|true", SharingRole.View, true)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow for checking of sharing permissions.", () => { + + return expect(file.checkSharingPermissions([{ alias: "c:0(.s|true" }])) + .to.eventually.be.fulfilled; + }); + + it("Should allow getting Sharing Information.", () => { + + return expect(file.getSharingInformation()) + .to.eventually.be.fulfilled; + }); + + it("Should allow getting Object Sharing Settings.", () => { + + return expect(file.getObjectSharingSettings(true)) + .to.eventually.be.fulfilled; + }); + + it("Should allow unsharing.", () => { + + return expect(file.unshare()) + .to.eventually.be.fulfilled; + }); + + // it("Should allow deleting a link by kind.", () => { + + // return expect(file.getShareLink(SharingLinkKind.AnonymousView).then(_ => { + + // return file.deleteSharingLinkByKind(SharingLinkKind.AnonymousView); + // })).to.eventually.be.fulfilled; + // }); + + // it("Should allow unsharing a link by kind.", () => { + + // return expect(file.getShareLink(SharingLinkKind.AnonymousView).then(response => { + + // return file.unshareLink(SharingLinkKind.AnonymousView, response.sharingLinkInfo.ShareId); + // })).to.eventually.be.fulfilled; + // }); + }); + + describe("can operate on items", () => { + + let item: Item = null; + + before(() => { + + item = web.lists.getByTitle("SharingTestLib").items.getById(1); + }); + + it("Should get a sharing link with default settings.", () => { + + return expect(item.getShareLink()) + .to.eventually.be.fulfilled + .and.have.property("sharingLinkInfo") + .and.have.deep.property("Url").that.is.not.null; + }); + + // it("Should get a sharing link with a specified kind.", () => { + // return expect(item.getShareLink(SharingLinkKind.AnonymousView)) + // .to.eventually.be.fulfilled + // .and.have.property("sharingLinkInfo") + // .and.have.deep.property("Url").that.is.not.null; + // }); + + // it("Should get a sharing link with a specified kind and expiration.", () => { + // return expect(item.getShareLink(SharingLinkKind.AnonymousView, Util.dateAdd(new Date(), "day", 5))) + // .to.eventually.be.fulfilled + // .and.have.property("sharingLinkInfo") + // .and.have.deep.property("Url").that.is.not.null; + // }); + + it("Should allow sharing to a person with default settings.", () => { + + return expect(item.shareWith("c:0(.s|true")) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role.", () => { + + return expect(item.shareWith("c:0(.s|true", SharingRole.Edit)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role and require sign-in.", () => { + + return expect(item.shareWith("c:0(.s|true", SharingRole.View, true)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow sharing to a person with the edit role and require sign-in.", () => { + + return expect(item.shareWith("c:0(.s|true", SharingRole.View, true)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow for checking of sharing permissions.", () => { + + return expect(item.checkSharingPermissions([{ alias: "c:0(.s|true" }])) + .to.eventually.be.fulfilled; + }); + + it("Should allow getting Sharing Information.", () => { + + return expect(item.getSharingInformation()) + .to.eventually.be.fulfilled; + }); + + it("Should allow getting Object Sharing Settings.", () => { + + return expect(item.getObjectSharingSettings(true)) + .to.eventually.be.fulfilled; + }); + + it("Should allow unsharing.", () => { + + return expect(item.unshare()) + .to.eventually.be.fulfilled; + }); + + // it("Should allow deleting a link by kind.", () => { + + // return expect(item.getShareLink(SharingLinkKind.AnonymousView).then(_ => { + + // return item.deleteSharingLinkByKind(SharingLinkKind.AnonymousView); + // })).to.eventually.be.fulfilled; + // }); + + // it("Should allow unsharing a link by kind.", () => { + + // return expect(item.getShareLink(SharingLinkKind.AnonymousView).then(response => { + + // return item.unshareLink(SharingLinkKind.AnonymousView, response.sharingLinkInfo.ShareId); + // })).to.eventually.be.fulfilled; + // }); + }); + + describe("can operate on webs", () => { + + it("Should allow you to share a web with a person using default settings", () => { + + return expect(web.shareWith("c:0(.s|true")) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + + it("Should allow you to share an object by url", () => { + + return expect(web.shareObject(combine(webAbsUrl, "SharingTestLib/test.txt"), "c:0(.s|true", SharingRole.View)) + .to.eventually.be.fulfilled + .and.have.property("ErrorMessage").that.is.null; + }); + }); + } +}); diff --git a/packages/sp/__tests/site.test.ts b/packages/sp/__tests/site.test.ts new file mode 100644 index 000000000..1c4a5145e --- /dev/null +++ b/packages/sp/__tests/site.test.ts @@ -0,0 +1,57 @@ +import { expect } from "chai"; +import { sp } from "../"; +import { testSettings } from "../../../test/main"; +import { combine } from "@pnp/common"; + +describe("Site", () => { + + if (testSettings.enableWebTests) { + + describe("rootWeb", () => { + it("should return the root web", () => { + return expect(sp.site.rootWeb.get()).to.eventually.have.property("Title"); + }); + }); + + describe("userCustomActions", () => { + it("should return the set of userCustomActions", () => { + return expect(sp.site.userCustomActions.get()).to.eventually.be.fulfilled; + }); + }); + + describe("getContextInfo", () => { + it("should get the site's context info", () => { + return expect(sp.site.getContextInfo()).to.eventually.be.fulfilled; + }); + }); + + describe("getDocumentLibraries", () => { + it("should get the site's document libraries", () => { + return expect(sp.site.getDocumentLibraries(testSettings.sp.webUrl)).to.eventually.not.be.empty; + }); + }); + + describe("getWebUrlFromPageUrl", () => { + it("should get the site's url from the pages url", () => { + const pageUrl = combine(testSettings.sp.webUrl, "/SitePages/Home.aspx"); + return expect(sp.site.getWebUrlFromPageUrl(pageUrl)).to.eventually.equal(testSettings.sp.webUrl.replace(/\/$/, "")); + }); + }); + + describe("openWebById", () => { + it("should get a web by id", () => { + + const chain = sp.web.select("Id").get().then(w => { + + return sp.site.openWebById(w.Id).then(ww => { + + // prove that we can successfully chain from the Web instance + return ww.web.select("Title").get(); + }); + }); + + return expect(chain).to.eventually.be.fulfilled; + }); + }); + } +}); diff --git a/packages/sp/__tests/sitegroups.test.ts b/packages/sp/__tests/sitegroups.test.ts new file mode 100644 index 000000000..33f392659 --- /dev/null +++ b/packages/sp/__tests/sitegroups.test.ts @@ -0,0 +1,59 @@ +import { expect } from "chai"; +import { SiteGroup, SiteGroups } from "../src/sitegroups"; +import { toMatchEndRegex } from "./utils"; + +describe("SiteGroups", () => { + + let siteGroups: SiteGroups; + + beforeEach(() => { + siteGroups = new SiteGroups("_api/web"); + }); + + it("Should be an object", () => { + expect(siteGroups).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/sitegroups", () => { + expect(siteGroups.toUrl()).to.match(toMatchEndRegex("_api/web/sitegroups")); + }); + }); + + describe("getById", () => { + it("should return _api/web/sitegroups(12)", () => { + expect(siteGroups.getById(12).toUrl()).to.match(toMatchEndRegex("_api/web/sitegroups(12)")); + }); + }); + + describe("getByName", () => { + it("should return _api/web/sitegroups/getByName('Group Name')", () => { + expect(siteGroups.getByName("Group Name").toUrl()).to.match(toMatchEndRegex("_api/web/sitegroups/getByName('Group Name')")); + }); + }); +}); + +describe("SiteGroup", () => { + + let group: SiteGroup; + + beforeEach(() => { + group = new SiteGroups("_api/web").getById(1); + }); + + it("Should be an object", () => { + expect(group).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/sitegroups(1)", () => { + expect(group.toUrl()).to.match(toMatchEndRegex("_api/web/sitegroups(1)")); + }); + }); + + describe("users", () => { + it("Should return _api/web/sitegroups", () => { + expect(group.users.toUrl()).to.match(toMatchEndRegex("_api/web/sitegroups(1)/users")); + }); + }); +}); diff --git a/packages/sp/__tests/siteusers.test.ts b/packages/sp/__tests/siteusers.test.ts new file mode 100644 index 000000000..536a4265b --- /dev/null +++ b/packages/sp/__tests/siteusers.test.ts @@ -0,0 +1,68 @@ +import { expect } from "chai"; +import { SiteUser, SiteUsers } from "../src/siteusers"; +import { toMatchEndRegex } from "./utils"; + +describe("SiteUsers", () => { + + let users: SiteUsers; + + beforeEach(() => { + users = new SiteUsers("_api/web"); + }); + + it("Should be an object", () => { + expect(users).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/siteusers", () => { + expect(users.toUrl()).to.match(toMatchEndRegex("_api/web/siteusers")); + }); + }); + + describe("getByEmail", () => { + it("Should return _api/web/siteusers/getByEmail('user@user.com')", () => { + const user = users.getByEmail("user@user.com"); + expect(user.toUrl()).to.match(toMatchEndRegex("_api/web/siteusers/getByEmail('user@user.com')")); + }); + }); + + describe("getById", () => { + it("Should return _api/web/siteusers/getbyid(12)", () => { + const user = users.getById(12); + expect(user.toUrl()).to.match(toMatchEndRegex("_api/web/siteusers/getbyid(12)")); + }); + }); + + describe("getByLoginName", () => { + it("Should return _api/web/siteusers(@v)?@v='i%3A0%23.f%7Cmembership%7Cuser%40tenant.com'", () => { + const user = users.getByLoginName("i:0#.f|membership|user@tenant.com"); + expect(user.toUrlAndQuery()).to.match(toMatchEndRegex("_api/web/siteusers(@v)?@v='i%3A0%23.f%7Cmembership%7Cuser%40tenant.com'")); + }); + }); +}); + +describe("SiteUser", () => { + + let user: SiteUser; + + beforeEach(() => { + user = new SiteUsers("_api/web").getById(2); + }); + + it("Should be an object", () => { + expect(user).to.be.a("object"); + }); + + describe("url", () => { + it("Should return _api/web/siteusers/getbyid(2)", () => { + expect(user.toUrl()).to.match(toMatchEndRegex("_api/web/siteusers/getbyid(2)")); + }); + }); + + describe("groups", () => { + it("Should return _api/web/siteusers/getbyid(2)/groups", () => { + expect(user.groups.toUrl()).to.match(toMatchEndRegex("_api/web/siteusers/getbyid(2)/groups")); + }); + }); +}); diff --git a/packages/sp/__tests/subscriptions.test.ts b/packages/sp/__tests/subscriptions.test.ts new file mode 100644 index 000000000..8617ebee4 --- /dev/null +++ b/packages/sp/__tests/subscriptions.test.ts @@ -0,0 +1,102 @@ +import { expect } from "chai"; +import { sp, Lists } from "../"; +import { testSettings } from "../../../test/main"; + +describe("Lists", () => { + + let lists: Lists; + + before(function (done) { + + if (testSettings.enableWebTests) { + + const now = new Date(); + const expirationDate = new Date(now.setDate(now.getDate() + 90)).toISOString(); + sp.web.lists.getByTitle("Documents").subscriptions.add(testSettings.sp.notificationUrl, expirationDate).then(_ => { + done(); + }).catch(_ => { + done(); + }); + + } else { + + done(); + } + }); + + beforeEach(() => { + lists = new Lists("_api/web"); + }); + + it("Should be an object", () => { + expect(lists).to.be.a("object"); + }); + + if (testSettings.enableWebTests) { + + describe("getByTitle", () => { + it("Should get a list by title with the expected title", () => { + + // we are expecting that the OOTB list exists + return expect(sp.web.lists.getByTitle("Documents").get()).to.eventually.have.property("Title", "Documents"); + }); + }); + + describe("getSubscriptions", () => { + it("Should return the subscriptions of the current list", () => { + const expectVal = expect(sp.web.lists.getByTitle("Documents").subscriptions.get()); + return expectVal.to.eventually.be.fulfilled; + }); + }); + + describe("createSubscription", () => { + it("Should be able to create a new webhook subscription in the current list", () => { + const now = new Date(); + const expirationDate = new Date(now.setDate(now.getDate() + 90)).toISOString(); + const expectVal = expect(sp.web.lists.getByTitle("Documents").subscriptions.add(testSettings.sp.notificationUrl, expirationDate)); + return expectVal.to.eventually.have.property("subscription"); + }); + }); + + describe("getSubscriptionsById", () => { + it("Should return the subscription by its ID of the current list", () => { + sp.web.lists.getByTitle("Documents").subscriptions.get().then((data) => { + if (data !== null) { + if (data.length > 0) { + const expectVal = expect(sp.web.lists.getByTitle("Documents").subscriptions.getById(data[0].id).get()); + return expectVal.to.eventually.have.property("id", data[0].id); + } + } + }); + }); + }); + + describe("updateSubscription", () => { + it("Should be able to update an existing webhook subscription in the current list", () => { + sp.web.lists.getByTitle("Documents").subscriptions.get().then((data) => { + if (data !== null) { + if (data.length > 0) { + const now = new Date(); + const expirationDate = new Date(now.setDate(now.getDate() + 90)).toISOString(); + const expectVal = expect(sp.web.lists.getByTitle("Documents").subscriptions.getById(data[0].id).update(expirationDate)); + return expectVal.to.eventually.have.property("subscription"); + } + } + }); + }); + }); + + describe("deleteSubscription", () => { + it("Should be able to delete an existing webhook subscription in the current list", () => { + sp.web.lists.getByTitle("Documents").subscriptions.get().then((data) => { + if (data !== null) { + if (data.length > 0) { + const expectVal = expect(sp.web.lists.getByTitle("Documents").subscriptions.getById(data[0].id).delete()); + return expectVal.to.eventually.be.fulfilled; + } + } + }); + }); + }); + } +}); diff --git a/packages/sp/__tests/utilities.test.ts b/packages/sp/__tests/utilities.test.ts new file mode 100644 index 000000000..39cacba3f --- /dev/null +++ b/packages/sp/__tests/utilities.test.ts @@ -0,0 +1,33 @@ +import { expect } from "chai"; +import { sp } from "../"; + +describe("Utilities", () => { + describe("containsInvalidFileFolderChars", () => { + it("should return true for file/folder name with invalid characters", () => { + const name = "test?test.txt"; + return expect(sp.utility.containsInvalidFileFolderChars(name)).to.be.true; + }); + + it("should return false for file/folder name with no invalid characters", () => { + const name = "test.txt"; + return expect(sp.utility.containsInvalidFileFolderChars(name)).to.be.false; + }); + }); + + describe("stripInvalidFileFolderChars", () => { + it("should strip invalid characters from file/folder name (online)", () => { + const name = "a\"#%*:<>?/\\|b.txt"; + return expect(sp.utility.stripInvalidFileFolderChars(name)).to.equal("a#%b.txt"); + }); + + it("should strip invalid characters from file/folder name (onpremise)", () => { + const name = "a\"#%*:<>?/\\|b.txt"; + return expect(sp.utility.stripInvalidFileFolderChars(name, "", true)).to.equal("ab.txt"); + }); + + it("should replace invalid characters with custom replacer if provided", () => { + const name = "a*b.txt"; + return expect(sp.utility.stripInvalidFileFolderChars(name, "c")).to.equal("acb.txt"); + }); + }); +}); diff --git a/packages/sp/__tests/utils.ts b/packages/sp/__tests/utils.ts new file mode 100644 index 000000000..f729dc6c1 --- /dev/null +++ b/packages/sp/__tests/utils.ts @@ -0,0 +1,11 @@ +/** + * Used to create an escaped regex for the non-SharePoint request tests + * + */ +export function toMatchEndRegex(s: string): RegExp { + let s2 = s.replace(/\(/g, "\\("); + s2 = s2.replace(/\)/g, "\\)"); + s2 = s2.replace(/\?/g, "\\?"); + s2 = s2.replace(/\$/g, "\\$"); + return new RegExp(`${s2}$`, "i"); +} diff --git a/packages/sp/__tests/views.test.ts b/packages/sp/__tests/views.test.ts new file mode 100644 index 000000000..601872e7e --- /dev/null +++ b/packages/sp/__tests/views.test.ts @@ -0,0 +1,24 @@ +import { expect } from "chai"; +import { Views } from "../src/views"; +import { toMatchEndRegex } from "./utils"; + +describe("Views", () => { + it("Should be an object", () => { + const views = new Views("_api/web/lists/getByTitle('Tasks')"); + expect(views).to.be.a("object"); + }); + describe("url", () => { + it("Should return _api/web/lists/getByTitle('Tasks')/Views", () => { + const views = new Views("_api/web/lists/getByTitle('Tasks')"); + expect(views.toUrl()).to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/views")); + }); + }); + describe("getById", () => { + it("Should return _api/web/lists/getByTitle('Tasks')/Views('7b7c777e-b749-4f58-a825-53084f2941b0')", () => { + const views = new Views("_api/web/lists/getByTitle('Tasks')"); + const view = views.getById("7b7c777e-b749-4f58-a825-53084f2941b0"); + expect(view.toUrl()) + .to.match(toMatchEndRegex("_api/web/lists/getByTitle('Tasks')/views('7b7c777e-b749-4f58-a825-53084f2941b0')")); + }); + }); +}); diff --git a/packages/sp/docs/alias-parameters.md b/packages/sp/docs/alias-parameters.md new file mode 100644 index 000000000..bdb1d6a66 --- /dev/null +++ b/packages/sp/docs/alias-parameters.md @@ -0,0 +1,66 @@ +# @pnp/sp - Aliased Parameters + +Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the +url length limits when working with files and folders. + +To alias a parameter you include the label name, a separator ("::") and the value in the string. You also need to prepend a "!" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a "@" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use "@p1" you should use "@p2" for a second parameter alias in the same query. + +### Construct a parameter alias + +Pattern: !@{label name}::{value} + +Example: "!@p1::\sites\dev" or "!@p2::\text.txt" + +### Example without aliasing + +```TypeScript +import { sp } from "@pnp/sp"; +// still works as expected, no aliasing +const query = sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/").files.select("Title").top(3); + +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 + +query.get().then(r => { + + console.log(r); +}); +``` + +### Example with aliasing + +```TypeScript +import { sp } from "@pnp/sp"; +// same query with aliasing +const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3); + +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 + +query.get().then(r => { + + console.log(r); +}); +``` + +### Example with aliasing and batching + +Aliasing is supported with batching as well: + +```TypeScript +import { sp } from "@pnp/sp"; +// same query with aliasing and batching +const batch = sp.web.createBatch(); + +const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3); + +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 + +query.inBatch(batch).get().then(r => { + + console.log(r); +}); + +batch.execute(); +``` diff --git a/packages/sp/docs/alm.md b/packages/sp/docs/alm.md new file mode 100644 index 000000000..837724343 --- /dev/null +++ b/packages/sp/docs/alm.md @@ -0,0 +1,120 @@ +# @pnp/sp/appcatalog + +The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions. + +### Understanding the App Catalog Heirarchy + +Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. [Read more in the official documentation](https://docs.microsoft.com/en-us/sharepoint/dev/apis/alm-api-for-spfx-add-ins). + +## Reference an App Catalog + +There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. + +```TypeScript +import { sp } from "@pnp/sp"; +// get the curren't context web's app catalog +const catalog = sp.web.getAppCatalog(); + +// you can also chain off the app catalog +pnp.sp.web.getAppCatalog().get().then(console.log); +``` + +```TypeScript +import { sp } from "@pnp/sp"; +// you can get the tenant app catalog (or any app catalog) by passing in a url + +// get the tenant app catalog +const tenantCatalog = sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/appcatalog"); + +// get a different app catalog +const catalog = sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/anothersite"); +``` + +```TypeScript +// alternatively you can create a new app catalog instance directly by importing the AppCatalog class +import { AppCatalog } from "@pnp/sp"; + +const catalog = new AppCatalog("https://mytenant.sharepoint.com/sites/dev"); +``` + +```TypeScript +// and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web +import { Web, AppCatalog } from "@pnp/sp"; + +const web = new Web("https://mytenant.sharepoint.com/sites/dev"); +const catalog = new AppCatalog(web); +``` + +The following examples make use of a variable "catalog" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity. + +## List Available Apps + +The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. + +```TypeScript +// get available apps +catalog.get().then(console.log); + +// get available apps selecting two fields +catalog.select("Title", "Deployed").get().then(console.log); +``` + +## Add an App + +This action must be performed in the context of the tenant app catalog + +```TypeScript +// this represents the file bytes of the app package file +const blob = new Blob(); + +// there is an optional third argument to control overwriting existing files +catalog.add("myapp.app", blob).then(r => { + + // this is at its core a file add operation so you have access to the response data as well + // as a File isntance representing the created file + + console.log(JSON.stringify(r.data, null, 4)); + + // all file operations are available + r.file.select("Name").get().then(console.log); +}); +``` + +## Get an App + +You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions + +```TypeScript +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").get().then(console.log); +``` + +## Perform app actions + +Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success using then and catch. + +```TypeScript + +// deploy +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").deploy().then(console.log).catch(console.error); + +// retract +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").retract().then(console.log).catch(console.error); + +// install +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").install().then(console.log).catch(console.error); + +// uninstall +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").uninstall().then(console.log).catch(console.error); + +// upgrade +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").upgrade().then(console.log).catch(console.error); + +// remove +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").remove().then(console.log).catch(console.error); + +``` + + +## Notes + +* The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so. diff --git a/packages/sp/docs/attachments.md b/packages/sp/docs/attachments.md new file mode 100644 index 000000000..192e4d5c8 --- /dev/null +++ b/packages/sp/docs/attachments.md @@ -0,0 +1,173 @@ +# @pnp/sp/attachments + +The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below. + +## Get attachments + +```TypeScript +import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +// get all the attachments +item.attachmentFiles.get().then(v => { + + console.log(v); +}); + +// get a single file by file name +item.attachmentFiles.getByName("file.txt").get().then(v => { + + console.log(v); +}); + +// select specific properties using odata operators +item.attachmentFiles.select("ServerRelativeUrl").get().then(v => { + + console.log(v); +}); +``` + +## Add an Attachment + +You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. + +```TypeScript +import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.add("file2.txt", "Here is my content").then(v => { + + console.log(v); +}); +``` + +## Add Multiple + +This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. + +```TypeScript +const list = sp.web.lists.getByTitle("MyList"); + +var fileInfos: AttachmentFileInfo[] = []; + +fileInfos.push({ + name: "My file name 1", + content: "string, blob, or array" +}); + +fileInfos.push({ + name: "My file name 2", + content: "string, blob, or array" +}); + +list.items.getById(2).attachmentFiles.addMultiple(fileInfos).then(r => { + + console.log(r); +}); +``` + +## Delete Multiple + +```TypeScript +const list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(2).attachmentFiles.deleteMultiple("1.txt","2.txt").then(r => { + console.log(r); +}); +``` + +## Read Attachment Content + +You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. + +```TypeScript +import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file.txt").getText().then(v => { + + console.log(v); +}); + +// use this in the browser, does not work in nodejs +item.attachmentFiles.getByName("file.mp4").getBlob().then(v => { + + console.log(v); +}); + +// use this in nodejs +item.attachmentFiles.getByName("file.mp4").getBuffer().then(v => { + + console.log(v); +}); + +// file must be valid json +item.attachmentFiles.getByName("file.json").getJSON().then(v => { + + console.log(v); +}); +``` + +## Update Attachment Content + +You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. + +```TypeScript +import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file2.txt").setContent("My new content!!!").then(v => { + + console.log(v); +}); +``` + +## Delete Attachment + +```TypeScript +import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file2.txt").delete().then(v => { + + console.log(v); +}); +``` + +## Recycle Attachment + +Added in _1.2.4_ + +Delete the attachment and send it to recycle bin + +```TypeScript +import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file2.txt").recycle().then(v => { + + console.log(v); +}); +``` + +## Recycle Multiple Attachments + +Added in _1.2.4_ + +Delete multiple attachments and send them to recycle bin +```TypeScript + +import { sp } from "@pnp/sp"; + +const list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(2).attachmentFiles.recycleMultiple("1.txt","2.txt").then(r => { + console.log(r); +}); +``` \ No newline at end of file diff --git a/packages/sp/docs/client-side-pages.md b/packages/sp/docs/client-side-pages.md new file mode 100644 index 000000000..55236af3c --- /dev/null +++ b/packages/sp/docs/client-side-pages.md @@ -0,0 +1,220 @@ +# @pnp/sp/clientsidepages + +The ability to manage client-side pages is a capability introduced in version 1.0.2 of @pnp/sp. Through the methods described +you can add and edit "modern" pages in SharePoint sites. + +## Add Client-side page + +Using the addClientSidePage you can add a new client side page to a site, specifying the filename. + +```TypeScript +import { sp } from "@pnp/sp"; + +const page = await sp.web.addClientSidePage(`MyFirstPage.aspx`); +``` + +Added in 1.0.5 you can also add a client side page using the list path. This gets around potential language issues with list title. You must specify the list path when calling this method in addition to the new page's filename. + +```TypeScript +import { sp } from "@pnp/sp"; + +const page = await sp.web.addClientSidePageByPath(`MyFirstPage.aspx`, "/sites/dev/SitePages"); +``` + +## Load Client-side page + +You can also load an existing page based on the file representing that page. Note that the static fromFile returns a promise which +resolves so the loaded page. Here we are showing use of the getFileByServerRelativeUrl method to get the File instance, but any of the ways +of [getting a File instance](files.md) will work. Also note we are passing the File instance, not the file content. + +```TypeScript +import { + sp, + ClientSidePage, +} from "@pnp/sp"; + +const page = await ClientSidePage.fromFile(sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/ExistingFile.aspx")); +``` + +**The remaining examples below reference a variable "page" which is assumed to be a ClientSidePage instance loaded through one of the above means.** + +## Add Controls + +A client-side page is made up of sections, which have columns, which contain controls. A new page will have none of these and an existing page may have +any combination of these. There are a few rules to understand how sections and columns layout on a page for display. A section is a horizontal piece of +a page that extends 100% of the page width. A page with multiple sections will stack these sections based on the section's order property - a 1 based index. + +Within a section you can have one or more columns. Each column is ordered left to right based on the column's order property. The width of each column is +controlled by the factor property whose value is one of 0, 2, 4, 6, 8, 10, or 12. The columns in a section should have factors that add up to 12. Meaning +if you wanted to have two equal columns you can set a factor of 6 for each. A page can have empty columns. + +```TypeScript +import { + sp, + ClientSideText, +} from "@pnp/sp"; + +// this code adds a section, and then adds a control to that section. The control is added to the section's defaultColumn, and if there are no columns a single +// column of factor 12 is created as a default. Here we add the ClientSideText part +page.addSection().addControl(new ClientSideText("@pnp/sp is a great library!")); + +// here we add a section, add two columns, and add a text control to the second section so it will appear on the right of the page +// add and get a reference to a new section +const section = page.addSection(); + +// add a column of factor 6 +section.addColumn(6); + +// add and get a reference to a new column of factor 6 +const column = section.addColumn(6); + +// add a text control to the second new column +column.addControl(new ClientSideText("Be sure to check out the @pnp docs at https://pnp.github.io/pnpjs/")); + +// we need to save our content changes +await page.save(); +``` + +## Add Client-side Web Parts + +Beyond the text control above you can also add any of the available client-side web parts in a given site. To find out what web parts are available you +first call the web's getClientSideWebParts method. Once you have a list of parts you need to find the defintion you want to use, here we get the Embed web part +whose's id is "490d7c76-1824-45b2-9de3-676421c997fa" (at least in one farm, your mmv). + +```TypeScript +import { + sp, + ClientSideWebpart, + ClientSideWebpartPropertyTypes, +} from "@pnp/sp"; + +// this will be a ClientSidePageComponent array +// this can be cached on the client in production scenarios +const partDefs = await sp.web.getClientSideWebParts(); + +// find the definition we want, here by id +const partDef = partDefs.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa"); + +// optionally ensure you found the def +if (partDef.length < 1) { + // we didn't find it so we throw an error + throw new Error("Could not find the web part"); +} + +// create a ClientWebPart instance from the definition +const part = ClientSideWebpart.fromComponentDef(partDef[0]); + +// set the properties on the web part. Here we have imported the ClientSideWebpartPropertyTypes module and can use that to type +// the available settings object. You can use your own types or help us out and add some typings to the module :). +// here for the embed web part we only have to supply an embedCode - in this case a youtube video. +part.setProperties({ + embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg", +}); + +// we add that part to a new section +page.addSection().addControl(part); + +// save our content changes back to the server +await page.save(); +``` + +## Find Controls + +Added in _1.0.3_ + +You can use the either of the two available method to locate controls within a page. These method search through all sections, columns, and controls returning the first instance that meets the supplied criteria. + +```TypeScript +import { ClientSideWebPart } from "@pnp/sp"; + +// find a control by instance id +const control1 = page.findControlById("b99bfccc-164e-4d3d-9b96-da48db62eb78"); + +// type the returned control +const control2 = page.findControlById("c99bfccc-164e-4d3d-9b96-da48db62eb78"); +const control3 = page.findControlById("a99bfccc-164e-4d3d-9b96-da48db62eb78"); + +// use any predicate to find a control +const control4 = page2.findControl((c: CanvasControl) => { + + // any logic you wish can be used on the control here + // return true to return that control + return c.order > 3; +}); +``` + +## Control Comments + +You can choose to enable or disable comments on a page using these methods + +```TypeScript +// indicates if comments are disabled, not valid until the page is loaded (Added in _1.0.3_) +page.commentsDisabled + +// enable comments +await page.enableComments(); + +// disable comments +await page.disableComments(); +``` + +## Like/Unlike Client-side page, get like information about page + +Added in _1.2.4_ + +You can like or unlike a modern page. You can also get information about the likes (i.e like Count and which users liked the page) + +```TypeScript +// Like a Client-side page (Added in _1.2.4_) +await page.like(); + +// Unlike a Client-side page +await page.unlike(); + +// Get liked by information such as like count and user's who liked the page +await page.getLikedByInformation(); +``` + +## Sample + +The below sample shows the process to add a Yammer feed webpart to the page. The properties required as well as the data version are found by adding the part using the UI and reviewing the values. Some or all of these may be discoverable using [Yammer APIs](https://developer.microsoft.com/en-us/yammer/docs). An identical process can be used to add web parts of any type by adjusting the definition, data version, and properties appropriately. + +```TypeScript +// get webpart defs +const defs = await sp.web.getClientSideWebParts(); + +// this is the id of the definition in my farm +const yammerPartDef = defs.filter(d => d.Id === "31e9537e-f9dc-40a4-8834-0e3b7df418bc")[0]; + +// page file +const file = sp.web.getFileByServerRelativePath("/sites/dev/SitePages/Testing_kVKF.aspx"); + +// create page instance +const page = await ClientSidePage.fromFile(file); + +// create part instance from definition +const part = ClientSideWebpart.fromComponentDef(yammerPartDef); + +// update data version +part.dataVersion = "1.5"; + +// set the properties required +part.setProperties({ + feedType: 0, + isSuiteConnected: false, + mode: 2, + networkId: 9999999, + yammerEmbedContainerHeight: 400, + yammerFeedURL: "", + yammerGroupId: -1, + yammerGroupMugshotUrl: "https://mug0.assets-yammer.com/mugshot/images/{width}x{height}/all_company.png", + yammerGroupName: "All Company", + yammerGroupUrl: "https://www.yammer.com/{tenant}/#/threads/company?type=general", +}); + +// add to the section/column you want +page.sections[0].addControl(part); + +// persist changes +page.save(); +``` diff --git a/packages/sp/docs/comments-likes.md b/packages/sp/docs/comments-likes.md new file mode 100644 index 000000000..b4c0011cd --- /dev/null +++ b/packages/sp/docs/comments-likes.md @@ -0,0 +1,117 @@ +# @pnp/sp/comments and likes + +Likes and comments in the context of modern sites are based on list items, meaning the operations branch from the Item class. To load an item you can refer to the guidance in the [items article](items.md). If you want to set the likes or comments on a modern page and don't know the item id but do know the url you can first load the file and then use the getItem method to get an item instance: + +_These APIs are currently in BETA and are subject to change or may not work on all tenants._ + +```TypeScript +import { sp } from "@pnp/sp"; + +const item = await sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/Test_8q5L.aspx").getItem(); + +// as an example, or any of the below options +await item.like(); +``` + +The below examples use a variable named "item" which is taken to represent an instance of the Item class. + +## Comments + +### Get Comments + +```TypeScript +const comments = await item.comments.get(); +``` + +You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: + +```TypeScript +import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray(Comment)); + +// these will be Comment instances in the array +comments[0].replies.add({ text: "#PnPjs is pretty ok!" }); + +//load the top 20 replies and comments for an item including likedBy information +const comments = await item.comments.expand("replies", "likedBy", "replies/likedBy").top(20).get(); +``` + +### Add Comment + +```TypeScript +// you can add a comment as a string +item.comments.add("string comment"); + +// or you can add it as an object to include mentions +item.comments.add({ text: "comment from object property" }); +``` + +### Delete a Comment + +```TypeScript +import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray(Comment)); + +// these will be Comment instances in the array +comments[0].delete() +``` + +### Like Comment + +```TypeScript +import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray(Comment)); + +// these will be Comment instances in the array +comments[0].like() +``` + +### Unlike Comment + +```TypeScript +import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray(Comment)); + +comments[0].unlike() +``` + +### Reply to a Comment + +```TypeScript +import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray(Comment)); + +const comment: Comment & CommentData = await comments[0].replies.add({ text: "#PnPjs is pretty ok!" }); +``` + +### Load Replies to a Comment + +```TypeScript +import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray(Comment)); + +const replies = await comments[0].replies.get(); +``` + +## Like + +You can like items and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. + +```TypeScript +import { LikeData } from "@pnp/sp"; + +// like an item +await item.like(); + +// unlike an item +await item.unlike(); + +// get the liked by information +const likedByData: LikeData[] = await item.getLikedBy(); +``` diff --git a/packages/sp/docs/content-types.md b/packages/sp/docs/content-types.md new file mode 100644 index 000000000..669a3dd47 --- /dev/null +++ b/packages/sp/docs/content-types.md @@ -0,0 +1,28 @@ +# @pnp/sp/content types + +## Set Folder Unique Content Type Order + +```TypeScript +interface OrderData { + ContentTypeOrder: { StringValue: string }[]; + UniqueContentTypeOrder?: { StringValue: string }[]; +} + +const folder = sp.web.lists.getById("{list id guid}").rootFolder; + +// here you need to see if there are unique content type orders already or just the default +const existingOrders = await folder.select("ContentTypeOrder", "UniqueContentTypeOrder").get(); + +const activeOrder = existingOrders.UniqueContentTypeOrder ? existingOrders.UniqueContentTypeOrder : existingOrders.ContentTypeOrder; + +// manipulate the order here however you want (I am just reversing the array as an example) +const newOrder = activeOrder.reverse(); + +// update the content type order thusly: +await folder.update({ + UniqueContentTypeOrder: { + __metadata: { type: "Collection(SP.ContentTypeId)" }, + results: newOrder, + }, +}); +``` diff --git a/packages/sp/docs/entity-merging.md b/packages/sp/docs/entity-merging.md new file mode 100644 index 000000000..0b6a406ba --- /dev/null +++ b/packages/sp/docs/entity-merging.md @@ -0,0 +1,66 @@ +# @pnp/sp - entity merging + +Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its represending type we make use of the _spODataEntity_ and _spODataEntityArray_ parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples. + +## Request a single entity + +If we are loading a single entity we use the _spODataEntity_ method. Here we show loading a list item using the Item class and a simple get query. + +```TypeScript +import { sp, spODataEntity, Item } from "@pnp/sp"; + +// interface defining the returned properites +interface MyProps { + Id: number; +} + +try { + + // get a list item laoded with data and merged into an instance of Item + const item = await sp.web.lists.getByTitle("ListTitle").items.getById(1).get(spODataEntity(Item)); + + // log the item id, all properties specified in MyProps will be type checked + Logger.write(`Item id: ${item.Id}`); + + // now we can call update because we have an instance of the Item type to work with as well + await item.update({ + Title: "New title.", + }); + +} catch (e) { + Logger.error(e); +} +``` + +## Request a collection + +The same pattern works when requesting a collection of objects with the exception of using the _spODataEntityArray_ method. + +```TypeScript +import { sp, spODataEntityArray, Item } from "@pnp/sp"; + +// interface defining the returned properites +interface MyProps { + Id: number; + Title: string; +} + +try { + + // get a list item laoded with data and merged into an instance of Item + const items = await sp.web.lists.getByTitle("ListTitle").items.select("Id", "Title").get(spODataEntityArray(Item)); + + Logger.write(`Item id: ${items.length}`); + + Logger.write(`Item id: ${items[0].Title}`); + + // now we can call update because we have an instance of the Item type to work with as well + await items[0].update({ + Title: "New title.", + }); + +} catch (e) { + + Logger.error(e); +} +``` diff --git a/packages/sp/docs/features.md b/packages/sp/docs/features.md new file mode 100644 index 000000000..aa07d5a83 --- /dev/null +++ b/packages/sp/docs/features.md @@ -0,0 +1,70 @@ +# @pnp/sp/features + +Features are used by SharePoint to package a set of functionality and either enable (activate) or disable (deactivate) that functionality based on requirements for a specific site. You can manage feature activation using the library as shown below. Note that the features collection only contains _active_ features. + +## List all Features + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +// get all the active features +web.features.get().then(f => { + + console.log(f); +}); + +// select properties using odata operators +web.features.select("DisplayName", "DefinitionId").get().then(f => { + + console.log(f); +}); + +// get a particular feature by id +web.features.getById("87294c72-f260-42f3-a41b-981a2ffce37a").select("DisplayName", "DefinitionId").get().then(f => { + + console.log(f); +}); + +// get features using odata operators +web.features.filter("DisplayName eq 'MDSFeature'").get().then(f => { + + console.log(f); +}); +``` + +## Activate a Feature + +To activate a feature you must know the feature id. You can optionally force activation - if you aren't sure don't use force. + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +// activate the minimum download strategy feature +web.features.add("87294c72-f260-42f3-a41b-981a2ffce37a").then(f => { + + console.log(f); +}); +``` + +## Deactivate a Feature + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +web.features.remove("87294c72-f260-42f3-a41b-981a2ffce37a").then(f => { + + console.log(f); +}); + +// you can also deactivate a feature but going through the collection's remove method is faster +web.features.getById("87294c72-f260-42f3-a41b-981a2ffce37a").deactivate().then(f => { + + console.log(f); +}); +``` \ No newline at end of file diff --git a/packages/sp/docs/fields.md b/packages/sp/docs/fields.md new file mode 100644 index 000000000..e6dad9897 --- /dev/null +++ b/packages/sp/docs/fields.md @@ -0,0 +1,205 @@ +# @pnp/sp/fields + +Fields allow you to store typed information within a SharePoint list. There are many types of fields and the library seeks to simplify working with the most common types. Fields exist in both site collections (site columns) or lists (list columns) and you can add/modify/delete them at either of these levels. + +## Get Fields + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +// get all the fields in a web +web.fields.get().then(f => { + + console.log(f); +}); + +// you can use odata operators on the fields collection +web.fields.select("Title", "InternalName", "TypeAsString").top(10).orderBy("Id").get().then(f => { + + console.log(f); +}); + +// get all the available fields in a web (includes parent web's fields) +web.availablefields.get().then(f => { + + console.log(f); +}); + +// get the fields in a list +web.lists.getByTitle("MyList").fields.get().then(f => { + + console.log(f); +}); + +// you can also get individual fields using getById, getByTitle, or getByInternalNameOrTitle +web.fields.getById("dee9c205-2537-44d6-94e2-7c957e6ebe6e").get().then(f => { + + console.log(f); +}); + +web.fields.getByTitle("MyField4").get().then(f => { + + console.log(f); +}); + +web.fields.getByInternalNameOrTitle("MyField4").get().then(f => { + + console.log(f); +}); +``` + +## Filtering Fields + +Sometimes you only want a subset of fields from the collection. Below are some examples of using the filter operator with the fields collection. + +```TypeScript +import { sp } from '@pnp/sp'; + +const list = sp.web.lists.getByTitle('Custom'); + +// Fields which can be updated +const filter1 = `Hidden eq false and ReadOnlyField eq false`; +list.fields.select('InternalName').filter(filter1).get().then(fields => { + console.log(`Can be updated: ${fields.map(f => f.InternalName).join(', ')}`); + // Title, ...Custom, ContentType, Attachments +}); + +// Only custom field +const filter2 = `Hidden eq false and CanBeDeleted eq true`; +list.fields.select('InternalName').filter(filter2).get().then(fields => { + console.log(`Custom fields: ${fields.map(f => f.InternalName).join(', ')}`); + // ...Custom +}); + +// Application specific fields +const includeFields = [ 'Title', 'Author', 'Editor', 'Modified', 'Created' ]; +const filter3 = `Hidden eq false and (ReadOnlyField eq false or (${ + includeFields.map(field => `InternalName eq '${field}'`).join(' or ') +}))`; +list.fields.select('InternalName').filter(filter3).get().then(fields => { + console.log(`Application specific: ${fields.map(f => f.InternalName).join(', ')}`); + // Title, ...Custom, ContentType, Modified, Created, Author, Editor, Attachments +}); + +// Fields in a view +list.defaultView.fields.select('Items').get().then(f => { + const fields = (f as any).Items.results || (f as any).Items; + console.log(`Fields in a view: ${fields.join(', ')}`); +}); +``` + +## Add Fields + +You can add fields using the add, createFieldAsXml, or one of the type specific methods. Functionally there is no difference, however one method may be easier given a certain scenario. + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +// if you use add you _must_ include the correct FieldTypeKind in the extended properties +web.fields.add("MyField1", "SP.FieldText", { + Group: "~Example", + FieldTypeKind: 2, + Filterable: true, + Hidden: false, + EnforceUniqueValues: true, +}).then(f => { + + console.log(f); +}); + +// you can also use the addText or any of the other type specific methods on the collection +web.fields.addText("MyField2", 75, { + Group: "~Example" +}).then(f => { + + console.log(f); +}); + +// if you have the field schema (for example from an old elements file) you can use createFieldAsXml +let xml = ``; + +web.fields.createFieldAsXml(xml).then(f => { + + console.log(f); +}); + +// the same operations work on a list's fields collection +web.lists.getByTitle("MyList").fields.addText("MyField5", 100).then(f => { + + console.log(f); +}); + +// Create a lookup field, and a dependent lookup field +web.lists.getByTitle("MyList").fields.addLookup("MyLookup", "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx", "MyLookupTargetField").then(f => { + console.log(f); + + // Create the dependent lookup field + return web.lists.getByTitle("MyList").fields.addDependentLookupField("MyLookup_ID", f.Id, "ID"); +}).then(fDep => { + console.log(fDep); +}); +``` + +### Adding Multiline Text Fields with FullHtml + +Because the RichTextMode property is not exposed to the clients we cannot set this value via the API directly. The work around is to use the createFieldAsXml method as shown below + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +const fieldAddResult = await web.fields.createFieldAsXml(``); +``` + +## Update a Field + +You can also update the properties of a field in both webs and lists, but not all properties are able to be updated after creation. You can review [this list](https://msdn.microsoft.com/en-us/library/office/dn600182.aspx#bk_FieldProperties) for details. + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +web.fields.getByTitle("MyField4").update({ + Description: "A new description", + }).then(f => { + + console.log(f); +}); +``` + +### Update a Url/Picture Field + +When updating a URL or Picture field you need to include the __metadata descriptor as shown below. + +```TypeScript +import { sp } from "@pnp/sp"; + +const data = { + "My_Field_Name": { + "__metadata": { "type": "SP.FieldUrlValue" }, + "Description": "A Pretty picture", + "Url": "https://tenant.sharepoint.com/sites/dev/Style%20Library/DSC_0024.JPG", + }, +}; + +await sp.web.lists.getByTitle("MyListTitle").items.getById(1).update(data); +``` + +## Delete a Field + +```TypeScript +import { sp } from "@pnp/sp"; + +let web = sp.web; + +web.fields.getByTitle("MyField4").delete().then(f => { + + console.log(f); +}); +``` diff --git a/packages/sp/docs/files.md b/packages/sp/docs/files.md new file mode 100644 index 000000000..859304c9a --- /dev/null +++ b/packages/sp/docs/files.md @@ -0,0 +1,269 @@ +# @pnp/sp/files + +One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below. + +## Reading Files + +Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your [own parser](../../odata/docs/parsers.md). + +```typescript +import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBlob().then((blob: Blob) => {}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBuffer().then((buffer: ArrayBuffer) => {}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.json").getJSON().then((json: any) => {}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.txt").getText().then((text: string) => {}); + +// all of these also work from a file object no matter how you access it +sp.web.getFolderByServerRelativeUrl("/sites/dev/documents").files.getByName("file.txt").getText().then((text: string) => {}); +``` + +## Adding Files + +Likewise you can add files using one of two methods, add or addChunked. The second is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. + +```typescript +declare var require: (s: string) => any; + +import { ConsoleListener, Web, Logger, LogLevel, ODataRaw } from "@pnp/sp"; +import { auth } from "./auth"; +let $ = require("jquery"); + +let siteUrl = "https://mytenant.sharepoint.com/sites/dev"; + +// comment this out for non-node execution +// auth(siteUrl); + +Logger.subscribe(new ConsoleListener()); +Logger.activeLogLevel = LogLevel.Verbose; + +let web = new Web(siteUrl); + +$(() => { + $("#testingdiv").append(""); + + $("#thebuttontodoit").on('click', (e) => { + + e.preventDefault(); + + let input = document.getElementById("thefileinput"); + let file = input.files[0]; + + // you can adjust this number to control what size files are uploaded in chunks + if (file.size <= 10485760) { + + // small upload + web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true).then(_ => Logger.write("done")); + } else { + + // large upload + web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.addChunked(file.name, file, data => { + + Logger.log({ data: data, level: LogLevel.Verbose, message: "progress" }); + + }, true).then(_ => Logger.write("done!")); + } + }); +}); +``` +### Setting Associated Item Values +You can also update the file properties of a newly uploaded file using code similar to the below snippet: + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true).then(f => { + + f.file.getItem().then(item => { + + item.update({ + Title: "A Title", + OtherField: "My Other Value" + }); + }); +}); +``` + +## Update File Content + +You can of course use similar methods to update existing files as shown below: + +```typescript +import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.txt").setContent("New string content for the file."); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.mp4").setContentChunked(file); +``` +## Check in, Check out, and Approve & Deny + +The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below. + +### Check In + +Check in takes two optional arguments, comment and check in type. + +```TypeScript +import { sp, CheckinType } from "@pnp/sp"; + +// default options with empty comment and CheckinType.Major +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin().then(_ => { + + console.log("File checked in!"); +}); + +// supply a comment (< 1024 chars) and using default check in type CheckinType.Major +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment").then(_ => { + + console.log("File checked in!"); +}); + +// Supply both comment and check in type +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment", CheckinType.Overwrite).then(_ => { + + console.log("File checked in!"); +}); +``` + +### Check Out + +Check out takes no arguments. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkout().then(_ => { + + console.log("File checked out!"); +}); +``` + +### Approve and Deny + +You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").approve("Approval Comment").then(_ => { + + console.log("File approved!"); +}); + +// deny with no comment +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny().then(_ => { + + console.log("File denied!"); +}); + +// deny with a supplied comment. +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny("Deny comment").then(_ => { + + console.log("File denied!"); +}); +``` + +## Publish and Unpublish + +You can both publish and unpublish a file using the library. Both methods take an optional comment argument. + +```TypeScript +import { sp } from "@pnp/sp"; +// publish with no comment +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish().then(_ => { + + console.log("File published!"); +}); + +// publish with a supplied comment. +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish("Publish comment").then(_ => { + + console.log("File published!"); +}); + +// unpublish with no comment +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish().then(_ => { + + console.log("File unpublished!"); +}); + +// unpublish with a supplied comment. +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish("Unpublish comment").then(_ => { + + console.log("File unpublished!"); +}); +``` + +## Advanced Upload Options + +Both the addChunked and setContentChunked methods support options beyond just supplying the file content. + +### progress function + +A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: + +`(data: ChunkedFileUploadProgressData) => void` + +The data interface is: + +```typescript +export interface ChunkedFileUploadProgressData { + stage: "starting" | "continue" | "finishing"; + blockNumber: number; + totalBlocks: number; + chunkSize: number; + currentPointer: number; + fileSize: number; +} +``` + +### chunkSize + +This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts. + +## getItem + +This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem().then(item => { + + console.log(item); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem("Title", "Modified").then(item => { + + console.log(item); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem().then(item => { + + // you can also chain directly off this item instance + item.getCurrentUserEffectivePermissions().then(perms => { + + console.log(perms); + }); +}); +``` + +You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. + +```TypeScript +import { sp } from "@pnp/sp"; +// also supports typing the objects so your type will be a union type +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem<{ Id: number, Title: string }>("Id", "Title").then(item => { + + // You get intellisense and proper typing of the returned object + console.log(`Id: ${item.Id} -- ${item.Title}`); + + // You can also chain directly off this item instance + item.getCurrentUserEffectivePermissions().then(perms => { + + console.log(perms); + }); +}); diff --git a/packages/sp/docs/index.md b/packages/sp/docs/index.md new file mode 100644 index 000000000..02c3aac72 --- /dev/null +++ b/packages/sp/docs/index.md @@ -0,0 +1,121 @@ +# @pnp/sp + +[![npm version](https://badge.fury.io/js/%40pnp%2Fsp.svg)](https://badge.fury.io/js/%40pnp%2Fsp) + +This package contains the fluent api used to call the SharePoint rest services. + +## Getting Started + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp --save` + +Import the library into your application and access the root sp object + +```TypeScript +import { sp } from "@pnp/sp"; + +(function main() { + + // here we will load the current web's title + sp.web.select("Title").get().then(w => { + + console.log(`Web Title: ${w.Title}`); + }); +})() +``` + +## Getting Started: SharePoint Framework + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp --save` + +Import the library into your application, update OnInit, and access the root sp object in render + +```TypeScript +import { sp } from "@pnp/sp"; + +// ... + +public onInit(): Promise { + + return super.onInit().then(_ => { + + // other init code may be present + + sp.setup({ + spfxContext: this.context + }); + }); +} + +// ... + +public render(): void { + + // A simple loading message + this.domElement.innerHTML = `Loading...`; + + sp.web.select("Title").get().then(w => { + + this.domElement.innerHTML = `Web Title: ${w.Title}`; + }); +} +``` + +## Getting Started: Nodejs + +Install the library and required dependencies + +`npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp @pnp/nodejs --save` + +Import the library into your application, setup the node client, make a request + +```TypeScript +import { sp } from "@pnp/sp"; +import { SPFetchClient } from "@pnp/nodejs"; + +// do this once per page load +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{your site url}", "{your client id}", "{your client secret}"); + }, + }, +}); + +// now make any calls you need using the configured client +sp.web.select("Title").get().then(w => { + + console.log(`Web Title: ${w.Title}`); +}); +``` + +## Library Topics + +* [Alias Parameters](alias-parameters.md) +* [ALM api](alm.md) +* [Attachments](attachments.md) +* [Client-side Pages](client-side-pages.md) +* [Features](features.md) +* [Fields](fields.md) +* [Files](files.md) +* [List Items](items.md) +* [Navigation Service](navigation-service.md) +* [Permissions](permissions.md) +* [Related Items](related-items.md) +* [Search](search.md) +* [Sharing](sharing.md) +* [Site Designs](sitedesigns.md) +* [Social](social.md) +* [SP.Utilities.Utility](sp-utilities-utility.md) +* [Tenant Properties](tenant-properties.md) +* [Views](views.md) +* [Webs](webs.md) +* [Comments and Likes](comments-likes.md) + +## UML +![Graphical UML diagram](../../documentation/img/pnpjs-sp-uml.svg) + +Graphical UML diagram of @pnp/sp. Right-click the diagram and open in new tab if it is too small. diff --git a/packages/sp/docs/items.md b/packages/sp/docs/items.md new file mode 100644 index 000000000..53258782f --- /dev/null +++ b/packages/sp/docs/items.md @@ -0,0 +1,382 @@ +# @pnp/sp/items + +## GET + +Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions. + +### Basic Get + +```TypeScript +import { sp } from "@pnp/sp"; + +// get all the items from a list +sp.web.lists.getByTitle("My List").items.get().then((items: any[]) => { + console.log(items); +}); + +// get a specific item by id +sp.web.lists.getByTitle("My List").items.getById(1).get().then((item: any) => { + console.log(item); +}); + +// use odata operators for more efficient queries +sp.web.lists.getByTitle("My List").items.select("Title", "Description").top(5).orderBy("Modified", true).get().then((items: any[]) => { + console.log(items); +}); +``` + +### Get Paged Items + +Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. + +```TypeScript +import { sp } from "@pnp/sp"; + +// basic case to get paged items form a list +let items = await sp.web.lists.getByTitle("BigList").items.getPaged(); + +// you can also provide a type for the returned values instead of any +let items = await sp.web.lists.getByTitle("BigList").items.getPaged<{Title: string}[]>(); + +// the query also works with select to choose certain fields and top to set the page size +let items = await sp.web.lists.getByTitle("BigList").items.select("Title", "Description").top(50).getPaged<{Title: string}[]>(); + +// the results object will have two properties and one method: + +// the results property will be an array of the items returned +if (items.results.length > 0) { + console.log("We got results!"); + + for (let i = 0; i < items.results.length; i++) { + // type checking works here if we specify the return type + console.log(items.results[i].Title); + } +} + +// the hasNext property is used with the getNext method to handle paging +// hasNext will be true so long as there are additional results +if (items.hasNext) { + + // this will carry over the type specified in the original query for the results array + items = await items.getNext(); + console.log(items.results.length); +} +``` + +### getListItemChangesSinceToken + +The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. + +```TypeScript +import { sp } from "@pnp/sp"; + +// Using RowLimit. Enables paging +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({RowLimit: '5'}); + +// Use QueryOptions to make a XML-style query. +// Because it's XML we need to escape special characters +// Instead of & we use & in the query +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({QueryOptions: ''}); + +// Get everything. Using null with ChangeToken gets everything +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({ChangeToken: null}); + +``` + +### Get All Items + +_Added in 1.0.2_ + +Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should +be used. + +```TypeScript +import { sp } from "@pnp/sp"; +// basic usage +sp.web.lists.getByTitle("BigList").items.getAll().then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); + +// set page size +sp.web.lists.getByTitle("BigList").items.getAll(4000).then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); + +// use select and top. top will set page size and override the any value passed to getAll +sp.web.lists.getByTitle("BigList").items.select("Title").top(4000).getAll().then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); + +// we can also use filter as a supported odata operation, but this will likely fail on large lists +sp.web.lists.getByTitle("BigList").items.select("Title").filter("Title eq 'Test'").getAll().then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); +``` + +### Retrieving Lookup Fields + +When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("LookupList").items.select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup").get().then((items: any[]) => { + console.log(items); +}); + +sp.web.lists.getByTitle("LookupList").items.getById(1).select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup").get().then((item: any) => { + console.log(item); +}); +``` + +### 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. + +```TypeScript +import { Web } from "@pnp/sp"; + +const w = new Web("https://{publishing site url}"); + +w.lists.getByTitle("Pages").items + .select("Title", "FileRef", "FieldValuesAsText/MetaInfo") + .expand("FieldValuesAsText") + .get().then(r => { + + // look through the returned items. + for (var i = 0; i < r.length; i++) { + + // the title field value + console.log(r[i].Title); + + // find the value in the MetaInfo string using regex + const matches = /PublishingPageImage:SW\|(.*?)\r\n/ig.exec(r[i].FieldValuesAsText.MetaInfo); + if (matches !== null && matches.length > 1) { + + // this wil be the value of the PublishingPageImage field + console.log(matches[1]); + } + } + }).catch(e => { console.error(e); }); +``` + +## Add Items + +There are several ways to add items to a list. The simplest just uses the _add_ method of the items collection passing in the properties as a plain object. + +```TypeScript +import { sp, ItemAddResult } from "@pnp/sp"; + +// add an item to the list +sp.web.lists.getByTitle("My List").items.add({ + Title: "Title", + Description: "Description" +}).then((iar: ItemAddResult) => { + console.log(iar); +}); +``` + +### Content Type + +You can also set the content type id when you create an item as shown in the example below: + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.lists.getById("4D5A36EA-6E84-4160-8458-65C436DB765C").items.add({ + Title: "Test 1", + ContentTypeId: "0x01030058FD86C279252341AB303852303E4DAF" +}); +``` + +### User Fields + +There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with "Id" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. + +Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a "results" property and an array. Examples for both are shown below. + +```TypeScript +import { sp } from "@pnp/sp"; +import { getGUID } from "@pnp/common"; + +sp.web.lists.getByTitle("PeopleFields").items.add({ + Title: getGUID(), + User1Id: 9, // allows a single user + User2Id: { + results: [ 16, 45 ] // allows multiple users + } +}).then(i => { + console.log(i); +}); +``` + +If you want to update or add user field values when using **validateUpdateListItem** you need to use the form shown below. You can specify multiple values in the array. + +```TypeScript +import { sp } from "@pnp/sp"; + +const result = await sp.web.lists.getByTitle("UserFieldList").items.getById(1).validateUpdateListItem([{ + FieldName: "UserField", + FieldValue: JSON.stringify([{ "Key": "i:0#.f|membership|person@tenant.com" }]), +}, +{ + FieldName: "Title", + FieldValue: "Test - Updated", +}]); +``` + +### Lookup Fields + +What is said for User Fields is, in general, relevant to Lookup Fields: +- Lookup Field types: + - Single-valued lookup + - Multiple-valued lookup +- `Id` suffix should be appended to the end of lookup's `EntityPropertyName` in payloads +- Numeric Ids for lookups' items should be passed as values + +```TypeScript +import { sp } from "@pnp/sp"; +import { getGUID } from "@pnp/common"; + +sp.web.lists.getByTitle("LookupFields").items.add({ + Title: getGUID(), + LookupFieldId: 2, // allows a single lookup value + MuptiLookupFieldId: { + results: [ 1, 56 ] // allows multiple lookup value + } +}).then(console.log).catch(console.log); +``` + +### Add Multiple Items + +```TypeScript +import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("rapidadd"); + +list.getListItemEntityTypeFullName().then(entityTypeFullName => { + + let batch = sp.web.createBatch(); + + list.items.inBatch(batch).add({ Title: "Batch 6" }, entityTypeFullName).then(b => { + console.log(b); + }); + + list.items.inBatch(batch).add({ Title: "Batch 7" }, entityTypeFullName).then(b => { + console.log(b); + }); + + batch.execute().then(d => console.log("Done")); +}); +``` + +## Update + +The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. + +```TypeScript +import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(1).update({ + Title: "My New Title", + Description: "Here is a new description" +}).then(i => { + console.log(i); +}); +``` + +### Getting and updating a collection using filter + +```TypeScript +import { sp } from "@pnp/sp"; + +// you are getting back a collection here +sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'").get().then((items: any[]) => { + // see if we got something + if (items.length > 0) { + sp.web.lists.getByTitle("MyList").items.getById(items[0].Id).update({ + Title: "Updated Title", + }).then(result => { + // here you will have updated the item + console.log(JSON.stringify(result)); + }); + } +}); +``` + +### Update Multiple Items + +This approach avoids multiple calls for the same list's entity type name. + +```TypeScript +import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("rapidupdate"); + +list.getListItemEntityTypeFullName().then(entityTypeFullName => { + + let batch = sp.web.createBatch(); + + // note requirement of "*" eTag param - or use a specific eTag value as needed + list.items.getById(1).inBatch(batch).update({ Title: "Batch 6" }, "*", entityTypeFullName).then(b => { + console.log(b); + }); + + list.items.getById(2).inBatch(batch).update({ Title: "Batch 7" }, "*", entityTypeFullName).then(b => { + console.log(b); + }); + + batch.execute().then(d => console.log("Done")); +}); +``` + +## Delete + +Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. + +```TypeScript +import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(1).delete().then(_ => {}); +``` + +## Resolving field names + +It's a very common mistake trying wrong field names in the requests. +Field's `EntityPropertyName` value should be used. + +The easiest way to get know EntityPropertyName is to use the following snippet: + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.lists + .getByTitle('[Lists_Title]') + .fields + .select('Title, EntityPropertyName') + .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) + .get() + .then(response => { + console.log(response.map(field => { + return { + Title: field.Title, + EntityPropertyName: field.EntityPropertyName + }; + })); + }) + .catch(console.log); +``` + +Lookup fields' names should be ended with additional `Id` suffix. E.g. for `Editor` EntityPropertyName `EditorId` should be used. diff --git a/packages/sp/docs/navigation-service.md b/packages/sp/docs/navigation-service.md new file mode 100644 index 000000000..f0dab9a3e --- /dev/null +++ b/packages/sp/docs/navigation-service.md @@ -0,0 +1,53 @@ +# @pnp/sp/navigation service + +The global navigation service located at "_api/navigation" provides access to the SiteMapProvider instances available in a given site collection. + +## getMenuState + +The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. + +The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma seperated string of property names like: property1,property2,property3\\,containingcomma + +NOTE: the , seperator can be escaped using the \ as escape character as done in the example above. The string above would split like: +* property1 +* property2 +* property3,containingcomma + +```TypeScript +import { sp } from "@pnp/sp"; + +// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. +sp.navigation.getMenuState().then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); + +// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 +sp.navigation.getMenuState("1002", 5).then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); + +// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 +sp.navigation.getMenuState(null, 5, "CurrentNavSiteMapProviderNoEncode").then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); +``` + +## getMenuNodeKey + +Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.navigation.getMenuNodeKey("/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx").then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); +``` diff --git a/packages/sp/docs/permissions.md b/packages/sp/docs/permissions.md new file mode 100644 index 000000000..a5936af15 --- /dev/null +++ b/packages/sp/docs/permissions.md @@ -0,0 +1,85 @@ +# @pnp/sp - permissions + +A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. + +Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables. + +## Get Role Assignments + +This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. + +```TypeScript +import { sp } from "@pnp/sp"; +import { Logger } from "@pnp/logging"; + +sp.web.roleAssignments.get().then(roles => { + + Logger.writeJSON(roles); +}); +``` + +## First Unique Ancestor Securable Object + +This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. + +```TypeScript +import { sp } from "@pnp/sp"; +import { Logger } from "@pnp/logging"; + +sp.web.firstUniqueAncestorSecurableObject.get().then(obj => { + + Logger.writeJSON(obj); +}); +``` + +## User Effective Permissions + +This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. + +```TypeScript +import { sp } from "@pnp/sp"; +import { Logger } from "@pnp/logging"; + +sp.web.getUserEffectivePermissions("i:0#.f|membership|user@site.com").then(perms => { + + Logger.writeJSON(perms); +}); + +sp.web.getCurrentUserEffectivePermissions().then(perms => { + + Logger.writeJSON(perms); +}); +``` + +## User Has Permissions + +Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. + +```TypeScript +import { sp, PermissionKind } from "@pnp/sp"; + +sp.web.userHasPermissions("i:0#.f|membership|user@site.com", PermissionKind.ApproveItems).then(perms => { + + console.log(perms); +}); + +sp.web.currentUserHasPermissions(PermissionKind.ApproveItems).then(perms => { + + console.log(perms); +}); +``` + +## Has Permissions + +If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. + +```TypeScript +import { sp, PermissionKind } from "@pnp/sp"; + +sp.web.getCurrentUserEffectivePermissions().then(perms => { + + if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) { + // ... + } +}); +``` \ No newline at end of file diff --git a/packages/sp/docs/profiles.md b/packages/sp/docs/profiles.md new file mode 100644 index 000000000..a5016c108 --- /dev/null +++ b/packages/sp/docs/profiles.md @@ -0,0 +1,125 @@ +# @pnp/sp/profiles + +The profile services allows to to work with the SharePoint User Profile Store. + +# Profiles + +Profiles is accessed directly from the root sp object. +``` TypeScript +import { sp } from "@pnp/sp"; +``` + +## GET + +### Get profile properties for a specific user +``` getPropertiesFor(loginName: string): Promise; ``` + +``` TypeScript +sp + .profiles + .getPropertiesFor(loginName).then((profile: any) => { + + console.log(profile.DisplayName); + console.log(profile.Email); + console.log(profile.Title); + console.log(profile.UserProfileProperties.length); + + // Properties are stored in inconvenient Key/Value pairs, + // so parse into an object called userProperties + var properties = {}; + profile.UserProfileProperties.forEach(function(prop) { + properties[prop.Key] = prop.Value; + }); + profile.userProperties = properties; + +} +``` + +### Get a specific property for a specific user +``` getUserProfilePropertyFor(loginName: string, propertyName: string): Promise; ``` + +``` TypeScript +sp + .profiles + .getUserProfilePropertyFor(loginName, propName).then((prop: string) => { + console.log(prop); +}; +``` + + +### Find whether a user is following another user +``` isFollowing(follower: string, followee: string): Promise; ``` + +``` TypeScript +sp + .profiles + .isFollowing(follower, followee).then((followed: boolean) => { + console.log(followed); +}; +``` + + +### Find out who a user is following +``` getPeopleFollowedBy(loginName: string): Promise; ``` + +``` TypeScript +sp + .profiles + .getPeopleFollowedBy(loginName).then((followed: any[]) => { + console.log(followed.length); +}; +``` + +### Find out if the current user is followed by another user +``` amIFollowedBy(loginName: string): Promise; ``` + +Returns a boolean indicating if the current user is followed by the user with loginName. +Get a specific property for the specified user. + +``` TypeScript +sp + .profiles + .amIFollowedBy(loginName).then((followed: boolean) => { + console.log(followed); +}; +``` + +### Get the people who are following the specified user +``` getFollowersFor(loginName: string): Promise; ``` + +``` TypeScript +sp + .profiles + .getFollowersFor(loginName).then((followed: any) => { + console.log(followed.length); +}; +``` + + +## SET + +### Set a single value property value +``` setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string) ``` + +Set a user's user profile property. + +``` TypeScript +sp + .profiles + .setSingleValueProfileProperty(accountName, propertyName, propertyValue); +``` + +### Set multi valued User Profile property +``` setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise; ``` + +``` TypeScript +sp + .profiles + .setSingleValueProfileProperty(accountName, propertyName, propertyValues); +``` + +### Upload and set the user profile picture +Users can upload a picture to their own profile only). Not supported for batching. +Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB + +``` setMyProfilePic(profilePicSource: Blob): Promise; ``` \ No newline at end of file diff --git a/packages/sp/docs/related-items.md b/packages/sp/docs/related-items.md new file mode 100644 index 000000000..b14485eac --- /dev/null +++ b/packages/sp/docs/related-items.md @@ -0,0 +1,96 @@ +# @pnp/sp/relateditems + +Related items are used in Task and Workflow lists (as well as others) to track items that have relationships similar to database relationships. + +All methods chain off the Web's relatedItems property as shown below: + +## getRelatedItems + +Expects the named library to exist within the contextual web. + +```TypeScript +import { sp, RelatedItem } from "@pnp/sp"; + +sp.web.relatedItems.getRelatedItems("Documents", 1).then((result: RelatedItem[]) => { + + console.log(result); +}); +``` + +## getPageOneRelatedItems + +Expects the named library to exist within the contextual web. + +```TypeScript +import { sp, RelatedItem } from "@pnp/sp"; + +sp.web.relatedItems.getPageOneRelatedItems("Documents", 1).then((result: RelatedItem[]) => { + + console.log(result); +}); +``` +## addSingleLink + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.relatedItems.addSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev").then(_ => { + + // ... return is void +}); + +sp.web.relatedItems.addSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev", true).then(_ => { + + // ... return is void +}); +``` + +## addSingleLinkToUrl + +Adds a related item link from an item specified by list name and item id, to an item specified by url + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.relatedItems.addSingleLinkToUrl("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt").then(_ => { + + // ... return is void +}); + +sp.web.relatedItems.addSingleLinkToUrl("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", true).then(_ => { + // ... return is void +}); +``` + +## addSingleLinkFromUrl + +Adds a related item link from an item specified by url, to an item specified by list name and item id + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.relatedItems.addSingleLinkFromUrl("https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", "RelatedItemsList1", 2).then(_ => { + // ... return is void +}); + +sp.web.relatedItems.addSingleLinkFromUrl("https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", "RelatedItemsList1", 2, true).then(_ => { + + // ... return is void +}); +``` + +## deleteSingleLink + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.relatedItems.deleteSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev").then(_ => { + + // ... return is void +}); + +sp.web.relatedItems.deleteSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev", true).then(_ => { + + // ... return is void +}); +``` diff --git a/packages/sp/docs/search.md b/packages/sp/docs/search.md new file mode 100644 index 000000000..dde6bb31d --- /dev/null +++ b/packages/sp/docs/search.md @@ -0,0 +1,157 @@ +# @pnp/sp/search + +Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and search suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier. + +# Search + +Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the SearchQuery interface, or a SearchQueryBuilder instance. The first two are shown below. + +```TypeScript +import { sp, SearchQuery, SearchResults } from "@pnp/sp"; + +// text search using SharePoint default values for other parameters +sp.search("test").then((r: SearchResults) => { + + console.log(r.ElapsedTime); + console.log(r.RowCount); + console.log(r.PrimarySearchResults); +}); + +// define a search query object matching the SearchQuery interface +sp.search({ + Querytext: "test", + RowLimit: 10, + EnableInterleaving: true, +}).then((r: SearchResults) => { + + console.log(r.ElapsedTime); + console.log(r.RowCount); + console.log(r.PrimarySearchResults); +}); +``` + +## Search Result Caching + +_Added in 1.1.5_ + +As of version 1.1.5 you can also use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace "search" with "searchWithCaching" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options + +```TypeScript +import { sp, SearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp"; + +sp.searchWithCaching({ + Querytext: "test", + RowLimit: 10, + EnableInterleaving: true, +}).then((r: SearchResults) => { + + console.log(r.ElapsedTime); + console.log(r.RowCount); + console.log(r.PrimarySearchResults); +}); + + +const builder = SearchQueryBuilder().text("test").rowLimit(3); + +// supply a search query builder and caching options +sp.searchWithCaching(builder, { key: "mykey", expiration: dateAdd(new Date(), "month", 1) }).then(r2 => { + + console.log(r2.TotalRows); +}); +``` + + +## Paging with SearchResults.getPage + +Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. + +```TypeScript +import { sp, SearchQueryBuilder, SearchResults } from "@pnp/sp"; + +// this will hold our current results +let currentResults: SearchResults = null; +let page = 1; + +// triggered on page load through some means +function onStart() { + + // construct our query that will be throughout the paging process, likely from user input + const q = SearchQueryBuilder.create("test").rowLimit(5); + sp.search(q).then((r: SearchResults) => { + + currentResults = r; // update the current results + page = 1; // reset if needed + // update UI with data... + }); +} + +// triggered by an event +function next() { + currentResults.getPage(++page).then((r: SearchResults) => { + + currentResults = r; // update the current results + // update UI with data... + }); +} + +// triggered by an event +function prev() { + currentResults.getPage(--page).then((r: SearchResults) => { + + currentResults = r; // update the current results + // update UI with data... + }); +} +``` + +## SearchQueryBuilder + +The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. + +```TypeScript +import { SearchQueryBuilder } from "@pnp/sp"; + +// basic usage +let q = SearchQueryBuilder().text("test").rowLimit(4).enablePhonetic; + +sp.search(q).then(h => { /* ... */ }); + +// provide a default query text in the create() +let q2 = SearchQueryBuilder("text").rowLimit(4).enablePhonetic; + +sp.search(q2).then(h => { /* ... */ }); + +// provide query text and a template + +// shared settings across queries +const appSearchSettings: SearchQuery = { + EnablePhonetic: true, + HiddenConstraints: "reports" +}; + +let q3 = SearchQueryBuilder("test", appSearchSettings).enableQueryRules; +let q4 = SearchQueryBuilder("financial data", appSearchSettings).enableSorting.enableStemming; +sp.search(q3).then(h => { /* ... */ }); +sp.search(q4).then(h => { /* ... */ }); +``` + +# Search Suggest + +Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches SearchSuggestQuery. + +```TypeScript +import { sp, SearchSuggestQuery, SearchSuggestResult } from "@pnp/sp"; + +sp.searchSuggest("test").then((r: SearchSuggestResult) => { + + console.log(r); +}); + +sp.searchSuggest({ + querytext: "test", + count: 5, +}).then((r: SearchSuggestResult) => { + + console.log(r); +}); +``` diff --git a/packages/sp/docs/sharing.md b/packages/sp/docs/sharing.md new file mode 100644 index 000000000..be2fea749 --- /dev/null +++ b/packages/sp/docs/sharing.md @@ -0,0 +1,230 @@ +# @pnp/sp/sharing + +**_Note: This API is still considered "beta" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online._** + +One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and override anything you may supply as an argument to these methods. If you receive an _InvalidOperationException_ when using these methods please check your tenant sharing settings to ensure sharing is not blocked before submitting an issue. + +## getShareLink + +**Applies to: Item, Folder, File** + +Creates a sharing link for the given resource with an optional expiration. + +```TypeScript +import { sp , SharingLinkKind, ShareLinkResponse } from "@pnp/sp"; +import { dateAdd } from "@pnp/common"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView).then(((result: ShareLinkResponse) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), "day", 5)).then((result: ShareLinkResponse) => { + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## shareWith + +**Applies to: Item, Folder, File, Web** + +Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter "shareEverything" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. + +```TypeScript +import { sp , SharingResult, SharingRole } from "@pnp/sp"; + +sp.web.shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit, true, true).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## shareObject & shareObjectRaw + +**Applies to: Web** + +Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. + +```TypeScript +import { sp , SharingResult, SharingRole } from "@pnp/sp"; + +sp.web.shareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt", "i:0#.f|membership|user@site.com", SharingRole.View).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.shareObjectRaw({ + url: "https://mysite.sharepoint.com/sites/dev/Docs/test.txt", + peoplePickerInput: [{ Key: "i:0#.f|membership|user@site.com" }], + roleValue: "role: 1973741327", + groupId: 0, + propagateAcl: false, + sendEmail: true, + includeAnonymousLinkInEmail: false, + emailSubject: "subject", + emailBody: "body", + useSimplifiedRoles: true, +}); +``` + +## unshareObject + +**Applies to: Web** + +```TypeScript +import { sp , SharingResult } from "@pnp/sp"; + +sp.web.unshareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## checkSharingPermissions + +**Applies to: Item, Folder, File** + +Checks Permissions on the list of Users and returns back role the users have on the Item. + +```TypeScript +import { sp , SharingEntityPermission } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").checkSharingPermissions([{ alias: "i:0#.f|membership|user@site.com" }]).then((result: SharingEntityPermission[]) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## getSharingInformation + +**Applies to: Item, Folder, File** + +Get Sharing Information. + +```TypeScript +import { sp , SharingInformation } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation().then((result: SharingInformation) => { + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## getObjectSharingSettings + +**Applies to: Item, Folder, File** + +Gets the sharing settings + +```TypeScript +import { sp , ObjectSharingSettings } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getObjectSharingSettings().then((result: ObjectSharingSettings) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## unshare + +**Applies to: Item, Folder, File** + +Unshares a given resource + +```TypeScript +import { sp , SharingResult } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshare().then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## deleteSharingLinkByKind + +**Applies to: Item, Folder, File** + +```TypeScript +import { sp , SharingLinkKind, SharingResult } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +``` + +## unshareLink + +**Applies to: Item, Folder, File** + +```TypeScript +import { sp , SharingLinkKind } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit).then(_ => { + + console.log("done"); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit, "12345").then(_ => { + + console.log("done"); +}).catch(e => { + console.error(e); +}); +``` diff --git a/packages/sp/docs/sitedesigns.md b/packages/sp/docs/sitedesigns.md new file mode 100644 index 000000000..33b8dc55d --- /dev/null +++ b/packages/sp/docs/sitedesigns.md @@ -0,0 +1,118 @@ +# @pnp/sp/sitedesigns + +You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. +Check out [SharePoint site design and site script overview](https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/site-design-overview) for more information. + +# Site Designs + +## Create a new site design +```TypeScript +import { sp } from "@pnp/sp"; + +// WebTemplate: 64 Team site template, 68 Communication site template +const siteDesign = await sp.siteDesigns.createSiteDesign({ + SiteScriptIds: ["884ed56b-1aab-4653-95cf-4be0bfa5ef0a"], + Title: "SiteDesign001", + WebTemplate: "64", +}); + +console.log(siteDesign.Title); +``` + +## Applying a site design to a site +```TypeScript +import { sp } from "@pnp/sp"; + +await sp.siteDesigns.applySiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8","https://contoso.sharepoint.com/sites/teamsite-pnpjs001"); +``` + +## Retrieval +```TypeScript +import { sp } from "@pnp/sp"; + +// Retrieving all site designs +const allSiteDesigns = await sp.siteDesigns.getSiteDesigns(); +console.log(`Total site designs: ${allSiteDesigns.length}`); + +// Retrieving a single site design by Id +const siteDesign = await sp.siteDesigns.getSiteDesignMetadata("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +console.log(siteDesign.Title); +``` + +## Update and delete +```TypeScript +import { sp } from "@pnp/sp"; + +// Update +const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: "75b9d8fe-4381-45d9-88c6-b03f483ae6a8", Title: "SiteDesignUpdatedTitle001" }); + +// Delete +await sp.siteDesigns.deleteSiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +``` + +## Setting Rights/Permissions +```TypeScript +import { sp } from "@pnp/sp"; + +// Get +const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +console.log(rights.length > 0 ? rights[0].PrincipalName : ""); + +// Grant +await sp.siteDesigns.grantSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]); + +// Revoke +await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]); + +// Reset all view rights +const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", rights.map(u => u.PrincipalName)); +``` + +# Site Scripts + +## Create a new site script +```TypeScript +import { sp } from "@pnp/sp"; + +const sitescriptContent = { + "$schema": "schema.json", + "actions": [ + { + "themeName": "Theme Name 123", + "verb": "applyTheme", + }, + ], + "bindata": {}, + "version": 1, +}; + +const siteScript = await sp.siteScripts.createSiteScript("Title", "description", sitescriptContent); + +console.log(siteScript.Title); +``` + +## Retrieval +```TypeScript +import { sp } from "@pnp/sp"; + +// Retrieving all site scripts +const allSiteScripts = await sp.siteScripts.getSiteScripts(); +console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : ""); + +// Retrieving a single site script by Id +const siteScript = await sp.siteScripts.getSiteScriptMetadata("884ed56b-1aab-4653-95cf-4be0bfa5ef0a"); +console.log(siteScript.Title); +``` + +## Update and delete +```TypeScript +import { sp } from "@pnp/sp"; + +// Update +const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: "884ed56b-1aab-4653-95cf-4be0bfa5ef0a", Title: "New Title" }); +console.log(updatedSiteScript.Title); + +// Delete +await sp.siteScripts.deleteSiteScript("884ed56b-1aab-4653-95cf-4be0bfa5ef0a"); +``` \ No newline at end of file diff --git a/packages/sp/docs/sites.md b/packages/sp/docs/sites.md new file mode 100644 index 000000000..de980b9d7 --- /dev/null +++ b/packages/sp/docs/sites.md @@ -0,0 +1,222 @@ +# @pnp/sp/site - Site properties + +Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types. + +## Get context information for the current site collection + +Using the library, you can get the context information of the current site collection + +```Typescript + +import { sp } from "@pnp/sp"; + +sp.site.getContextInfo().then(d =>{ + console.log(d.FormDigestValue); +}); + +``` + +## Get document libraries of a web + +Using the library, you can get a list of the document libraries present in the a given web. + +**Note:** Works only in SharePoint online + +```Typescript +import { sp } from "@pnp/sp"; + +sp.site.getDocumentLibraries("https://tenant.sharepoint.com/sites/test/subsite").then((d:DocumentLibraryInformation[]) => { + // iterate over the array of doc lib +}); + +``` + +## Open Web By Id + +Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. + +```TypeScript +sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e").then(w => { + + //we got all the data from the web as well + console.log(w.data); + + // we can chain + w.web.select("Title").get().then(w2 => { + // ... + }); +}); +``` + +## Get site collection url from page + +Using the library, you can get the site collection url by providing a page url + +```TypeScript + +import { sp } from "@pnp/sp"; + +sp.site.getWebUrlFromPageUrl("https://tenant.sharepoint.com/sites/test/Pages/test.aspx").then(d => { + console.log(d); +}); + +``` + + +## Join a hub site + +Added in _1.2.4_ + +**Note:** Works only in SharePoint online + +Join the current site collection to a hub site collection + +```TypeScript + +import { sp, Site } from "@pnp/sp"; + +var site = new Site("https://tenant.sharepoint.com/sites/HubSite/"); + +var hubSiteID = ""; + +site.select("ID").get().then(d => { + // get ID of the hub site collection + hubSiteID = d.Id; + + // associate the current site collection the hub site collection + sp.site.joinHubSite(hubSiteID).then(d => { + console.log(d); + }); + +}); + +``` + +## Disassociate the current site collection from a hub site collection + +Added in _1.2.4_ + +**Note:** Works only in SharePoint online + +```TypeScript + +import { sp } from "@pnp/sp"; + +sp.site.joinHubSite("00000000-0000-0000-0000-000000000000").then(d => { + console.log(d); +}); + +``` + +## Register a hub site + +Added in _1.2.4_ + +**Note:** Works only in SharePoint online + +Registers the current site collection as a hub site collection + +```TypeScript + +import { sp } from "@pnp/sp"; + +sp.site.registerHubSite().then(d => { + console.log(d); +}); + +``` + +## Un-Register a hub site + +Added in _1.2.4_ + +**Note:** Works only in SharePoint online + +Un-Registers the current site collection as a hub site collection + +```TypeScript + +import { sp } from "@pnp/sp"; + +sp.site.unRegisterHubSite().then(d => { + console.log(d); +}); + +``` + +## Create a modern communication site + +Added in _1.2.6_ + +**Note:** Works only in SharePoint online + +Creates a modern communication site. + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| Title | string | yes | The title of the site to create. | +| lcid | number | yes | The default language to use for the site. | +| shareByEmailEnabled | boolean | yes | If set to true, it will enable sharing files via Email. By default it is set to false | +| url | string | yes | The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site. | +| description | string | no | The description of the communication site. | +| classification | string | no | The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information +| siteDesignId | string | no | The Guid of the site design to be used. +||||You can use the below default OOTB GUIDs: +||||Topic: null +|||| Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 +|||| Blank: f6cc5403-0d63-442e-96c0-285923709ffc +|||| + +```TypeScript + +import { sp } from "@pnp/sp"; + +sp.site.createCommunicationSite( + "Title", + 1033, + true, + "https://tenant.sharepoint.com/sites/commSite", + "Description", + "HBI", + "f6cc5403-0d63-442e-96c0-285923709ffc").then(d => { + console.log(d); + }); + +``` + +## Create a modern team site + +Added in _1.2.6_ + +**Note:** Works only in SharePoint online. It wont work with App only tokens + +Creates a modern team site backed by O365 group. + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| displayName | string | yes | The title/displayName of the site to be created. | +| alias | string | yes | Alias of the underlying Office 365 Group. | +| isPublic | boolean | yes | Defines whether the Office 365 Group will be public (default), or private. | +| lcid | number | yes | The language to use for the site. If not specified will default to English (1033). | +| description | string | no | The description of the modern team site. | +| classification | string | no | The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information +| owners | string array (string[]) | no | The Owners of the site to be created + + +```TypeScript + +import { sp } from "@pnp/sp"; + +sp.site.createModernTeamSite( + "displayName", + "alias", + true, + 1033, + "description", + "HBI", + ["user1@tenant.onmicrosoft.com","user2@tenant.onmicrosoft.com","user3@tenant.onmicrosoft.com"]) + .then(d => { + console.log(d); + }); + +``` \ No newline at end of file diff --git a/packages/sp/docs/social.md b/packages/sp/docs/social.md new file mode 100644 index 000000000..98f16da3c --- /dev/null +++ b/packages/sp/docs/social.md @@ -0,0 +1,150 @@ +# @pnp/sp/social + +The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not +with app-only permissions. + +## getFollowedSitesUri + +Gets a URI to a site that lists the current user's followed sites. + +```TypeScript +import { sp } from "@pnp/sp"; + +const uri = await sp.social.getFollowedSitesUri(); +``` + +## getFollowedDocumentsUri + +Gets a URI to a site that lists the current user's followed documents. + +```TypeScript +import { sp } from "@pnp/sp"; + +const uri = await sp.social.getFollowedDocumentsUri(); +``` + +## follow + +Makes the current user start following a user, document, site, or tag + +```TypeScript +import { sp, SocialActorType } from "@pnp/sp"; + +// follow a site +const r1 = await sp.social.follow({ + ActorType: SocialActorType.Site, + ContentUri: "htts://tenant.sharepoint.com/sites/site", +}); + +// follow a person +const r2 = await sp.social.follow({ + AccountName: "i:0#.f|membership|person@tenant.com", + ActorType: SocialActorType.User, +}); + +// follow a doc +const r3 = await sp.social.follow({ + ActorType: SocialActorType.Document, + ContentUri: "https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx", +}); + +// follow a tag +// You need the tag GUID to start following a tag. +// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. +// See How to get a tag's GUID based on the tag's name by using the JavaScript object model. +// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid +const r4 = await sp.social.follow({ + ActorType: SocialActorType.Tag, + TagGuid: "19a4a484-c1dc-4bc5-8c93-bb96245ce928", +}); +``` + +## isFollowed + +Indicates whether the current user is following a specified user, document, site, or tag + +```TypeScript +import { sp, SocialActorType } from "@pnp/sp"; + +// pass the same social actor struct as shown in follow example for each type +const r = await sp.social.isFollowed({ + AccountName: "i:0#.f|membership|person@tenant.com", + ActorType: SocialActorType.User, +}); +``` + +## stopFollowing + +Makes the current user stop following a user, document, site, or tag + +```TypeScript +import { sp, SocialActorType } from "@pnp/sp"; + +// pass the same social actor struct as shown in follow example for each type +const r = await sp.social.stopFollowing({ + AccountName: "i:0#.f|membership|person@tenant.com", + ActorType: SocialActorType.User, +}); +``` + +## my + +### get + +Gets this user's social information + +```TypeScript +import { sp } from "@pnp/sp"; + +const r = await sp.social.my.get(); +``` + +### followed + +Gets users, documents, sites, and tags that the current user is following based on the supplied flags. + +```TypeScript +import { sp, SocialActorTypes } from "@pnp/sp"; + +// get all the followed documents +const r1 = await sp.social.my.followed(SocialActorTypes.Document); + +// get all the followed documents and sites +const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site); + +// get all the followed sites updated in the last 24 hours +const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours); +``` + +### followedCount + +Works as followed but returns on the count of actors specifed by the query + +```TypeScript +import { sp, SocialActorTypes } from "@pnp/sp"; + +// get the followed documents count +const r = await sp.social.my.followedCount(SocialActorTypes.Document); +``` + +### followers + +Gets the users who are following the current user. + +```TypeScript +import { sp } from "@pnp/sp"; + +// get the followed documents count +const r = await sp.social.my.followers(); +``` + +### suggestions + +Gets users who the current user might want to follow. + +```TypeScript +import { sp } from "@pnp/sp"; + +// get the followed documents count +const r = await sp.social.my.suggestions(); +``` diff --git a/packages/sp/docs/sp-utilities-utility.md b/packages/sp/docs/sp-utilities-utility.md new file mode 100644 index 000000000..92d408595 --- /dev/null +++ b/packages/sp/docs/sp-utilities-utility.md @@ -0,0 +1,187 @@ +# @pnp/sp/utilities + +Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching. + +## sendEmail + +This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below). + +### EmailProperties + +```TypeScript +export interface EmailProperties { + + To: string[]; + CC?: string[]; + BCC?: string[]; + Subject: string; + Body: string; + AdditionalHeaders?: TypedHash; + From?: string; +} +``` + +### Usage + +You must define the To, Subject, and Body values - the remaining are optional. + +```TypeScript +import { sp, EmailProperties } from "@pnp/sp"; + +const emailProps: EmailProperties = { + To: ["user@site.com"], + CC: ["user2@site.com", "user3@site.com"], + Subject: "This email is about...", + Body: "Here is the body. It supports html", +}; + +sp.utility.sendEmail(emailProps).then(_ => { + + console.log("Email Sent!"); +}); +``` + +## getCurrentUserEmailAddresses + +This method returns the current user's email addresses known to SharePoint. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.utility.getCurrentUserEmailAddresses().then((addressString: string) => { + + console.log(addressString); +}); +``` + +## resolvePrincipal + +Gets information about a principal that matches the specified Search criteria + +```TypeScript +import { sp , PrincipalType, PrincipalSource, PrincipalInfo } from "@pnp/sp"; + +sp.utility.resolvePrincipal("user@site.com", + PrincipalType.User, + PrincipalSource.All, + true, + false).then((principal: PrincipalInfo) => { + + + console.log(principal); + }); +``` + +## searchPrincipals + +Gets information about the principals that match the specified Search criteria. + +```TypeScript +import { sp , PrincipalType, PrincipalSource, PrincipalInfo } from "@pnp/sp"; + +sp.utility.searchPrincipals("john", + PrincipalType.User, + PrincipalSource.All, + "", + 10).then((principals: PrincipalInfo[]) => { + + console.log(principals); + }); +``` + +## createEmailBodyForInvitation + +Gets the external (outside the firewall) URL to a document or resource in a site. + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.utility.createEmailBodyForInvitation("https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx").then((r: string) => { + + console.log(r); +}); +``` + +## expandGroupsToPrincipals + +Resolves the principals contained within the supplied groups + +```TypeScript +import { sp , PrincipalInfo } from "@pnp/sp"; + +sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"]).then((principals: PrincipalInfo[]) => { + + console.log(principals); +}); + +// optionally supply a max results count. Default is 30. +sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"], 10).then((principals: PrincipalInfo[]) => { + + console.log(principals); +}); +``` + +## createWikiPage + +```TypeScript +import { sp , CreateWikiPageResult } from "@pnp/sp"; + +sp.utility.createWikiPage({ + ServerRelativeUrl: "/sites/dev/SitePages/mynewpage.aspx", + WikiHtmlContent: "This is my page content. It supports rich html.", +}).then((result: CreateWikiPageResult) => { + + // result contains the raw data returned by the service + console.log(result.data); + + // result contains a File instance you can use to further update the new page + result.file.get().then(f => { + + console.log(f); + }); +}); +``` + +## containsInvalidFileFolderChars + +Checks if file or folder name contains invalid characters + +```TypeScript +import { sp } from "@pnp/sp"; + +const isInvalid = sp.utility.containsInvalidFileFolderChars("Filename?.txt"); +console.log(isInvalid); // true +``` + +## stripInvalidFileFolderChars + +Removes invalid characters from file or folder name + +```TypeScript +import { sp } from "@pnp/sp"; + +const validName = sp.utility.stripInvalidFileFolderChars("Filename?.txt"); +console.log(validName); // Filename.txt +``` + +## Call Other Methods + +Even if a method does not have an explicit implementation on the utility api you can still call it using the UtilityMethod class. In this example we will show calling the GetLowerCaseString method, but the technique works for any of the utility methods. + +```TypeScript +import { UtilityMethod } from "@pnp/sp"; + +// the first parameter is the web url. You can use an empty string for the current web, +// or specify it to call other web's. The second parameter is the method name. +const method = new UtilityMethod("", "GetLowerCaseString"); + +// you must supply the correctly formatted parameters to the execute method which +// is generic and types the result as the supplied generic type parameter. +method.excute({ + sourceValue: "HeRe IS my StrINg", + lcid: 1033, +}).then((s: string) => { + + console.log(s); +}); +``` diff --git a/packages/sp/docs/tenant-properties.md b/packages/sp/docs/tenant-properties.md new file mode 100644 index 000000000..d77016053 --- /dev/null +++ b/packages/sp/docs/tenant-properties.md @@ -0,0 +1,43 @@ +# @pnp/sp/web - tenant properties + +You can set, read, and remove tenant properties using the methods shown below: + +## setStorageEntity + +This method MUST be called in the context of the app catalog web or you will get an access denied message. + +```TypeScript +import { Web } from "@pnp/sp"; + +const w = new Web("https://tenant.sharepoint.com/sites/appcatalog/"); + +// specify required key and value +await w.setStorageEntity("Test1", "Value 1"); + +// specify optional description and comments +await w.setStorageEntity("Test2", "Value 2", "description", "comments"); +``` + +## getStorageEntity + +This method can be used from any web to retrieve values previsouly set. + +```TypeScript +import { sp, StorageEntity } from "@pnp/sp"; + +const prop: StorageEntity = await sp.web.getStorageEntity("Test1"); + +console.log(prop.Value); +``` + +## removeStorageEntity + +This method MUST be called in the context of the app catalog web or you will get an access denied message. + +```TypeScript +import { Web } from "@pnp/sp"; + +const w = new Web("https://tenant.sharepoint.com/sites/appcatalog/"); + +await w.removeStorageEntity("Test1"); +``` diff --git a/packages/sp/docs/views.md b/packages/sp/docs/views.md new file mode 100644 index 000000000..ede538ef8 --- /dev/null +++ b/packages/sp/docs/views.md @@ -0,0 +1,91 @@ +# @pnp/sp/views + +Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view. + +## Get a View's Properties + +To get a views properties you need to know it's id or title. You can use the standard OData operators as expected to select properties. For a list of the properties, please see [this article](https://msdn.microsoft.com/en-us/library/office/dn531433.aspx#bk_View). + +```TypeScript +import { sp } from "@pnp/sp"; +// know a view's GUID id +sp.web.lists.getByTitle("Documents").getView("2B382C69-DF64-49C4-85F1-70FB9CECACFE").select("Title").get().then(v => { + + console.log(v); +}); + +// get by the display title of the view +sp.web.lists.getByTitle("Documents").views.getByTitle("All Documents").select("Title").get().then(v => { + + console.log(v); +}); +``` + +## Add a View + +To add a view you use the add method of the views collection. You must supply a title and can supply other parameters as well. + +```TypeScript +import { sp, ViewAddResult } from "@pnp/sp"; +// create a new view with default fields and properties +sp.web.lists.getByTitle("Documents").views.add("My New View").then(v => { + + console.log(v); +}); + +// create a new view with specific properties +sp.web.lists.getByTitle("Documents").views.add("My New View 2", false, { + + RowLimit: 10, + ViewQuery: "", +}).then((v: ViewAddResult) => { + + // manipulate the view's fields + v.view.fields.removeAll().then(_ => { + + Promise.all([ + v.view.fields.add("Title"), + v.view.fields.add("Modified"), + ]).then(_ =>{ + + console.log("View created"); + }); + }); +}); +``` + +## Update a View + +```TypeScript +import { sp, ViewUpdateResult } from "@pnp/sp"; + +sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").update({ + RowLimit: 20, +}).then((v: ViewUpdateResult) => { + + console.log(v); +}); +``` + +## Set View XML + +_Added in 1.2.6_ + +```TypeScript +import { sp } from "@pnp/sp"; + +const viewXml: string = "..."; + +await sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").setViewXml(viewXml); +``` + +## Delete a View + +```TypeScript +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").delete().then(_ => { + + console.log("View deleted"); +}); +``` diff --git a/packages/sp/index.ts b/packages/sp/index.ts new file mode 100644 index 000000000..5ae3db53e --- /dev/null +++ b/packages/sp/index.ts @@ -0,0 +1 @@ +export * from "./src/sp"; diff --git a/packages/sp/package.json b/packages/sp/package.json new file mode 100644 index 000000000..9c5c72232 --- /dev/null +++ b/packages/sp/package.json @@ -0,0 +1,28 @@ +{ + "name": "@pnp/sp", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - provides a fluent api for working with SharePoint REST", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "peerDependencies": { + "@pnp/common": "0.0.0-PLACEHOLDER", + "@pnp/logging": "0.0.0-PLACEHOLDER", + "@pnp/odata": "0.0.0-PLACEHOLDER" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } + } + \ No newline at end of file diff --git a/packages/sp/presets/all.ts b/packages/sp/presets/all.ts new file mode 100644 index 000000000..3ab6a89f6 --- /dev/null +++ b/packages/sp/presets/all.ts @@ -0,0 +1,70 @@ +import { SPRest } from "../src/rest"; + +import "../src/appcatalog"; +import "../src/attachments"; +import "../src/clientsidepages"; +import "../src/comments"; +import "../src/content-types"; +import "../src/features"; +import "../src/fields"; +import "../src/files"; +import "../src/folders"; +import "../src/forms"; +import "../src/hubsites"; +import "../src/items"; +import "../src/lists"; +import "../src/navigation"; +import "../src/profiles"; +import "../src/regional-settings"; +import "../src/related-items"; +import "../src/search"; +import "../src/security"; +import "../src/sharing"; +import "../src/site-designs"; +import "../src/site-groups"; +import "../src/site-scripts"; +import "../src/site-users"; +import "../src/sites"; +import "../src/social"; +import "../src/sputilities"; +import "../src/subscriptions"; +import "../src/user-custom-actions"; +import "../src/views"; +import "../src/webparts"; +import "../src/webs"; + +export * from "../src/appcatalog"; +export * from "../src/attachments"; +export * from "../src/clientsidepages"; +export * from "../src/comments"; +export * from "../src/content-types"; +export * from "../src/features"; +export * from "../src/fields"; +export * from "../src/files"; +export * from "../src/folders"; +export * from "../src/forms"; +export * from "../src/hubsites"; +export * from "../src/items"; +export * from "../src/lists"; +export * from "../src/navigation"; +export * from "../src/profiles"; +export * from "../src/regional-settings"; +export * from "../src/related-items"; +export * from "../src/search"; +export * from "../src/security"; +export * from "../src/sharing"; +export * from "../src/site-designs"; +export * from "../src/site-groups"; +export * from "../src/site-scripts"; +export * from "../src/site-users"; +export * from "../src/sites"; +export * from "../src/social"; +export * from "../src/sputilities"; +export * from "../src/subscriptions"; +export * from "../src/user-custom-actions"; +export * from "../src/views"; +export * from "../src/webparts"; +export * from "../src/webs"; +export * from "../src/sp"; + +export const sp = new SPRest(); diff --git a/packages/sp/presets/core.ts b/packages/sp/presets/core.ts new file mode 100644 index 000000000..1d674d6eb --- /dev/null +++ b/packages/sp/presets/core.ts @@ -0,0 +1,14 @@ +import { SPRest } from "../src/rest"; + +import "../src/items"; +import "../src/lists"; +import "../src/sites"; +import "../src/webs"; + +export * from "../src/items"; +export * from "../src/lists"; +export * from "../src/sites"; +export * from "../src/webs"; +export * from "../src/sp"; + +export const sp = new SPRest(); diff --git a/packages/sp/src/appcatalog/index.ts b/packages/sp/src/appcatalog/index.ts new file mode 100644 index 000000000..1ab3a8803 --- /dev/null +++ b/packages/sp/src/appcatalog/index.ts @@ -0,0 +1,27 @@ +import { SPRest } from "../rest"; +import { IWeb, Web } from "../webs/types"; + +import "./web"; +import { SharePointQueryable } from "@pnp/sp/presets/all"; + +export { + AppAddResult, + IApp, + IAppCatalog, + App, + AppCatalog, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + getTenantAppCatalogWeb(): Promise; + } +} + +SPRest.prototype.getTenantAppCatalogWeb = async function (this: SPRest): Promise { + const data: { CorporateCatalogUrl: string } = await SharePointQueryable("/", "_api/SP_TenantSettings_Current")(); + return Web(data.CorporateCatalogUrl); +}; diff --git a/packages/sp/src/appcatalog/types.ts b/packages/sp/src/appcatalog/types.ts new file mode 100644 index 000000000..2add25584 --- /dev/null +++ b/packages/sp/src/appcatalog/types.ts @@ -0,0 +1,128 @@ +import { IGetable } from "@pnp/odata"; +import { + ISharePointQueryable, + _SharePointQueryableInstance, + ISharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { spPost } from "../operations"; +import { odataUrlFrom } from "../odata"; +import { extractWebUrl } from "../utils/extractweburl"; +import { File, IFile } from "../files/types"; + +/** + * Represents an app catalog + */ +export class _AppCatalog extends _SharePointQueryableCollection implements IAppCatalog { + + constructor(baseUrl: string | ISharePointQueryable, path = "_api/web/tenantappcatalog/AvailableApps") { + super(extractWebUrl(typeof baseUrl === "string" ? baseUrl : baseUrl.toUrl()), path); + } + + /** + * Get details of specific app from the app catalog + * @param id - Specify the guid of the app + */ + public getAppById(id: string): IApp { + return App(this, `getById('${id}')`); + } + + /** + * Uploads an app package. Not supported for batching + * + * @param filename Filename to create. + * @param content app package data (eg: the .app or .sppkg file). + * @param shouldOverWrite Should an app with the same name in the same location be overwritten? (default: true) + * @returns Promise + */ + public async add(filename: string, content: string | ArrayBuffer | Blob, shouldOverWrite = true): Promise { + + // you don't add to the availableapps collection + const adder = AppCatalog(extractWebUrl(this.toUrl()), `_api/web/tenantappcatalog/add(overwrite=${shouldOverWrite},url='${filename}')`); + + const r = await spPost(adder, { body: content }); + + return { + data: r, + file: File(odataUrlFrom(r)), + }; + } +} + +export interface IAppCatalog extends IGetable, ISharePointQueryableCollection { + add(filename: string, content: string | ArrayBuffer | Blob, shouldOverWrite?: boolean): Promise; + getAppById(id: string): IApp; +} +export interface _AppCatalog extends IGetable { } +export const AppCatalog = spInvokableFactory(_AppCatalog); + +/** + * Represents the actions you can preform on a given app within the catalog + */ +export class _App extends _SharePointQueryableInstance implements IApp { + + /** + * This method deploys an app on the app catalog. It must be called in the context + * of the tenant app catalog web or it will fail. + * + * @param skipFeatureDeployment Deploy the app to the entire tenant + */ + public deploy(skipFeatureDeployment = false): Promise { + return spPost(this.clone(App, `Deploy(${skipFeatureDeployment})`)); + } + + /** + * This method retracts a deployed app on the app catalog. It must be called in the context + * of the tenant app catalog web or it will fail. + */ + public retract(): Promise { + return spPost(this.clone(App, "Retract")); + } + + /** + * This method allows an app which is already deployed to be installed on a web + */ + public install(): Promise { + return spPost(this.clone(App, "Install")); + } + + /** + * This method allows an app which is already insatlled to be uninstalled on a web + */ + public uninstall(): Promise { + return spPost(this.clone(App, "Uninstall")); + } + + /** + * This method allows an app which is already insatlled to be upgraded on a web + */ + public upgrade(): Promise { + return spPost(this.clone(App, "Upgrade")); + } + + /** + * This method removes an app from the app catalog. It must be called in the context + * of the tenant app catalog web or it will fail. + */ + public remove(): Promise { + return spPost(this.clone(App, "Remove")); + } +} + +export interface _App extends IGetable { } +export interface IApp extends IGetable, ISharePointQueryableInstance { + deploy(skipFeatureDeployment?: boolean): Promise; + retract(): Promise; + install(): Promise; + uninstall(): Promise; + upgrade(): Promise; + remove(): Promise; +} +export const App = spInvokableFactory(_App); + +export interface AppAddResult { + data: any; + file: IFile; +} diff --git a/packages/sp/src/appcatalog/web.ts b/packages/sp/src/appcatalog/web.ts new file mode 100644 index 000000000..eee2130da --- /dev/null +++ b/packages/sp/src/appcatalog/web.ts @@ -0,0 +1,21 @@ +import { _Web } from "../webs/types"; +import { AppCatalog, IAppCatalog } from "./types"; + +declare module "../webs/types" { + interface _Web { + getAppCatalog(url?: string | _Web): 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) + */ + getAppCatalog(url?: string | _Web): IAppCatalog; + } +} + +_Web.prototype.getAppCatalog = function (this: _Web, url?: string | _Web): IAppCatalog { + return AppCatalog(url || this); +}; diff --git a/packages/sp/src/attachments/index.ts b/packages/sp/src/attachments/index.ts new file mode 100644 index 000000000..06dfd5419 --- /dev/null +++ b/packages/sp/src/attachments/index.ts @@ -0,0 +1,11 @@ +// extend everything if they include the root +import "./item"; + +export { + IAttachment, + IAttachments, + Attachment, + Attachments, + AttachmentAddResult, + AttachmentFileInfo, +} from "./types"; diff --git a/packages/sp/src/attachments/item.ts b/packages/sp/src/attachments/item.ts new file mode 100644 index 000000000..957c9dbae --- /dev/null +++ b/packages/sp/src/attachments/item.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Item } from "../items/types"; +import { Attachments, IAttachments } from "./types"; + +/** +* Extend Web +*/ +declare module "../items/types" { + interface _Item { + readonly attachmentFiles: IAttachments; + } + interface IItem { + readonly attachmentFiles: IAttachments; + } +} + +addProp(_Item, "attachmentFiles", Attachments); diff --git a/packages/sp/src/attachments/types.ts b/packages/sp/src/attachments/types.ts new file mode 100644 index 000000000..ca957b456 --- /dev/null +++ b/packages/sp/src/attachments/types.ts @@ -0,0 +1,172 @@ +import { defaultPath, deleteableWithETag, IDeleteableWithETag } from "../decorators"; +import { spPost } from "../operations"; +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { TextParser, BlobParser, JSONParser, BufferParser, ODataParser, IGetable, headers } from "@pnp/odata"; + +export interface AttachmentFileInfo { + name: string; + content: string | Blob | ArrayBuffer; +} + +/** + * Describes a collection of Attachment objects + * + */ +@defaultPath("AttachmentFiles") +export class _Attachments extends _SharePointQueryableCollection implements IAttachments { + + /** + * Gets a Attachment File by filename + * + * @param name The name of the file, including extension. + */ + public getByName(name: string): IAttachment { + const f = Attachment(this); + f.concat(`('${name}')`); + return f; + } + + /** + * Adds a new attachment to the collection. Not supported for batching. + * + * @param name The name of the file, including extension. + * @param content The Base64 file content. + */ + public async add(name: string, content: string | Blob | ArrayBuffer): Promise { + const response = await spPost(this.clone(Attachments, `add(FileName='${name}')`, false), { body: content }); + return { + data: response, + file: this.getByName(name), + }; + } + + /** + * Adds multiple new attachment to the collection. Not supported for batching. + * + * @param files The collection of files to add + */ + public addMultiple(files: AttachmentFileInfo[]): Promise { + + // add the files in series so we don't get update conflicts + return files.reduce((chain, file) => chain.then(() => spPost(this.clone(Attachments, `add(FileName='${file.name}')`, false), { + body: file.content, + })), Promise.resolve()); + } + + /** + * Delete multiple attachments from the collection. Not supported for batching. + * + * @param files The collection of files to delete + */ + public deleteMultiple(...files: string[]): Promise { + return files.reduce((chain, file) => chain.then(() => this.getByName(file).delete()), Promise.resolve()); + } + + /** + * Delete multiple attachments from the collection and send to recycle bin. Not supported for batching. + * + * @param files The collection of files to be deleted and sent to recycle bin + */ + public recycleMultiple(...files: string[]): Promise { + return files.reduce((chain, file) => chain.then(() => this.getByName(file).recycle()), Promise.resolve()); + } +} + +export interface IAttachments extends IGetable, ISharePointQueryableCollection { + getByName(name: string): IAttachment; + add(name: string, content: string | Blob | ArrayBuffer): Promise; + addMultiple(files: AttachmentFileInfo[]): Promise; + deleteMultiple(...files: string[]): Promise; + recycleMultiple(...files: string[]): Promise; +} +export interface _Attachments extends IGetable { } +export const Attachments = spInvokableFactory(_Attachments); + +/** + * Describes a single attachment file instance + * + */ +@deleteableWithETag() +export class _Attachment extends _SharePointQueryableInstance implements IAttachment { + + /** + * Gets the contents of the file as text + * + */ + public getText(): Promise { + return this.getParsed(new TextParser()); + } + + /** + * Gets the contents of the file as a blob, does not work in Node.js + * + */ + public getBlob(): Promise { + return this.getParsed(new BlobParser()); + } + + /** + * Gets the contents of a file as an ArrayBuffer, works in Node.js + */ + public getBuffer(): Promise { + return this.getParsed(new BufferParser()); + } + + /** + * Gets the contents of a file as an ArrayBuffer, works in Node.js + */ + public getJSON(): Promise { + return this.getParsed(new JSONParser()); + } + + /** + * Sets the content of a file. Not supported for batching + * + * @param content The value to set for the file contents + */ + public async setContent(content: string | ArrayBuffer | Blob): Promise { + + await spPost(this.clone(Attachment, "$value", false), headers({ "X-HTTP-Method": "PUT" }, { + body: content, + })); + return Attachment(this); + } + + /** + * Delete this attachment file and send it to recycle bin + * + * @param eTag Value used in the IF-Match header, by default "*" + */ + public recycle(eTag = "*"): Promise { + return spPost(this.clone(Attachment, "recycleObject"), headers({ + "IF-Match": eTag, + "X-HTTP-Method": "DELETE", + })); + } + + private getParsed(parser: ODataParser): Promise { + return this.clone(Attachment, "$value", false).usingParser(parser)(); + } +} + +export interface IAttachment extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag { + getText(): Promise; + getBlob(): Promise; + getBuffer(): Promise; + getJSON(): Promise; + setContent(content: string | ArrayBuffer | Blob): Promise; + recycle(eTag?: string): Promise; +} +export interface _Attachment extends IGetable, IDeleteableWithETag { } +export const Attachment = spInvokableFactory(_Attachment); + +export interface AttachmentAddResult { + file: IAttachment; + data: any; +} diff --git a/packages/sp/src/batch.ts b/packages/sp/src/batch.ts new file mode 100644 index 000000000..33375e748 --- /dev/null +++ b/packages/sp/src/batch.ts @@ -0,0 +1,231 @@ +import { Batch } from "@pnp/odata"; +import { getGUID, isUrlAbsolute, combine, mergeHeaders, hOP } from "@pnp/common"; +import { Logger, LogLevel } from "@pnp/logging"; +import { SPHttpClient } from "./net/sphttpclient"; +import { SPRuntimeConfig } from "./config/splibconfig"; +import { toAbsoluteUrl } from "./utils/toabsoluteurl"; + +/** + * Manages a batch of OData operations + */ +export class SPBatch extends Batch { + + constructor(private baseUrl: string) { + super(); + } + + /** + * Parses the response from a batch request into an array of Response instances + * + * @param body Text body of the response from the batch request + */ + public static ParseResponse(body: string): Promise { + return new Promise((resolve, reject) => { + const responses: Response[] = []; + const header = "--batchresponse_"; + // Ex. "HTTP/1.1 500 Internal Server Error" + const statusRegExp = new RegExp("^HTTP/[0-9.]+ +([0-9]+) +(.*)", "i"); + const lines = body.split("\n"); + let state = "batch"; + let status: number; + let statusText: string; + for (let i = 0; i < lines.length; ++i) { + const line = lines[i]; + switch (state) { + case "batch": + if (line.substr(0, header.length) === header) { + state = "batchHeaders"; + } else { + if (line.trim() !== "") { + throw Error(`Invalid response, line ${i}`); + } + } + break; + case "batchHeaders": + if (line.trim() === "") { + state = "status"; + } + break; + case "status": + const parts = statusRegExp.exec(line); + if (parts.length !== 3) { + throw Error(`Invalid status, line ${i}`); + } + status = parseInt(parts[1], 10); + statusText = parts[2]; + state = "statusHeaders"; + break; + case "statusHeaders": + if (line.trim() === "") { + state = "body"; + } + break; + case "body": + responses.push((status === 204) ? new Response() : new Response(line, { status: status, statusText: statusText })); + state = "batch"; + break; + } + } + if (state !== "status") { + reject(Error("Unexpected end of input")); + } + resolve(responses); + }); + } + + protected executeImpl(): Promise { + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Executing batch with ${this.requests.length} requests.`, LogLevel.Info); + + // if we don't have any requests, don't bother sending anything + // this could be due to caching further upstream, or just an empty batch + if (this.requests.length < 1) { + Logger.write(`Resolving empty batch.`, LogLevel.Info); + return Promise.resolve(); + } + + // creating the client here allows the url to be populated for nodejs client as well as potentially + // any other hacks needed for other types of clients. Essentially allows the absoluteRequestUrl + // below to be correct + const client = new SPHttpClient(); + + // due to timing we need to get the absolute url here so we can use it for all the individual requests + // and for sending the entire batch + return toAbsoluteUrl(this.baseUrl).then(absoluteRequestUrl => { + + // build all the requests, send them, pipe results in order to parsers + const batchBody: string[] = []; + + let currentChangeSetId = ""; + + for (let i = 0; i < this.requests.length; i++) { + const reqInfo = this.requests[i]; + + if (reqInfo.method === "GET") { + + if (currentChangeSetId.length > 0) { + // end an existing change set + batchBody.push(`--changeset_${currentChangeSetId}--\n\n`); + currentChangeSetId = ""; + } + + batchBody.push(`--batch_${this.batchId}\n`); + + } else { + + if (currentChangeSetId.length < 1) { + // start new change set + currentChangeSetId = getGUID(); + batchBody.push(`--batch_${this.batchId}\n`); + batchBody.push(`Content-Type: multipart/mixed; boundary="changeset_${currentChangeSetId}"\n\n`); + } + + batchBody.push(`--changeset_${currentChangeSetId}\n`); + } + + // common batch part prefix + batchBody.push(`Content-Type: application/http\n`); + batchBody.push(`Content-Transfer-Encoding: binary\n\n`); + + // these are the per-request headers + const headers = new Headers(); + + // this is the url of the individual request within the batch + const url = isUrlAbsolute(reqInfo.url) ? reqInfo.url : combine(absoluteRequestUrl, reqInfo.url); + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Adding request ${reqInfo.method} ${url} to batch.`, LogLevel.Verbose); + + if (reqInfo.method !== "GET") { + + let method = reqInfo.method; + + const castHeaders: any = reqInfo.options.headers; + if (hOP(reqInfo, "options") && hOP(reqInfo.options, "headers") && castHeaders["X-HTTP-Method"] !== undefined) { + + method = castHeaders["X-HTTP-Method"]; + delete castHeaders["X-HTTP-Method"]; + } + + batchBody.push(`${method} ${url} HTTP/1.1\n`); + + headers.set("Content-Type", "application/json;odata=verbose;charset=utf-8"); + + } else { + batchBody.push(`${reqInfo.method} ${url} HTTP/1.1\n`); + } + + // merge global config headers + mergeHeaders(headers, SPRuntimeConfig.headers); + + // merge per-request headers + if (reqInfo.options) { + mergeHeaders(headers, reqInfo.options.headers); + } + + // lastly we apply any default headers we need that may not exist + if (!headers.has("Accept")) { + headers.append("Accept", "application/json"); + } + + if (!headers.has("Content-Type")) { + headers.append("Content-Type", "application/json;odata=verbose;charset=utf-8"); + } + + if (!headers.has("X-ClientService-ClientTag")) { + headers.append("X-ClientService-ClientTag", "PnPCoreJS:@pnp-$$Version$$"); + } + + // write headers into batch body + headers.forEach((value: string, name: string) => { + batchBody.push(`${name}: ${value}\n`); + }); + + batchBody.push("\n"); + + if (reqInfo.options.body) { + batchBody.push(`${reqInfo.options.body}\n\n`); + } + } + + if (currentChangeSetId.length > 0) { + // Close the changeset + batchBody.push(`--changeset_${currentChangeSetId}--\n\n`); + currentChangeSetId = ""; + } + + batchBody.push(`--batch_${this.batchId}--\n`); + + const batchOptions = { + "body": batchBody.join(""), + "headers": { + "Content-Type": `multipart/mixed; boundary=batch_${this.batchId}`, + }, + "method": "POST", + }; + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Sending batch request.`, LogLevel.Info); + + return client.fetch(combine(absoluteRequestUrl, "/_api/$batch"), batchOptions) + .then(r => r.text()) + .then(SPBatch.ParseResponse) + .then((responses: Response[]) => { + + if (responses.length !== this.requests.length) { + throw Error("Could not properly parse responses to match requests in batch."); + } + + Logger.write(`[${this.batchId}] (${(new Date()).getTime()}) Resolving batched requests.`, LogLevel.Info); + + return responses.reduce((chain, response, index) => { + + const request = this.requests[index]; + + Logger.write(`[${request.id}] (${(new Date()).getTime()}) Resolving request in batch ${this.batchId}.`, LogLevel.Info); + + return chain.then(_ => request.parser.parse(response).then(request.resolve).catch(request.reject)); + + }, Promise.resolve()); + }); + }); + } +} diff --git a/packages/sp/src/clientsidepages/funcs.ts b/packages/sp/src/clientsidepages/funcs.ts new file mode 100644 index 000000000..da20aff58 --- /dev/null +++ b/packages/sp/src/clientsidepages/funcs.ts @@ -0,0 +1,178 @@ +import { hOP, objectDefinedNotNull, jsS } from "@pnp/common"; + +/** + * Converts a json object to an escaped string appropriate for use in attributes when storing client-side controls + * + * @param json The json object to encode into a string + */ +export function jsonToEscapedString(json: any): string { + + return jsS(json) + .replace(/"/g, """) + .replace(/:/g, ":") + .replace(/{/g, "{") + .replace(/}/g, "}") + .replace(/\[/g, "\[") + .replace(/\]/g, "\]") + .replace(/\*/g, "\*") + .replace(/\$/g, "\$") + .replace(/\./g, "\."); +} + +/** + * Converts an escaped string from a client-side control attribute to a json object + * + * @param escapedString + */ +export function escapedStringToJson(escapedString: string): T { + const unespace = (escaped: string): string => { + return [ + [/"/g, "\""], + [/:/g, ":"], + [/{/g, "{"], + [/}/g, "}"], + [/\\\\/g, "\\"], + [/\\\?/g, "?"], + [/\\\./g, "."], + [/\\\[/g, "["], + [/\\\]/g, "]"], + [/\\\(/g, "("], + [/\\\)/g, ")"], + [/\\\|/g, "|"], + [/\\\+/g, "+"], + [/\\\*/g, "*"], + [/\\\$/g, "$"], + ].reduce((r, m) => r.replace(m[0], m[1] as string), escaped); + }; + + return objectDefinedNotNull(escapedString) ? JSON.parse(unespace(escapedString)) : null; +} + +/** + * Gets the next order value 1 based for the provided collection + * + * @param collection Collection of orderable things + */ +export function getNextOrder(collection: { order: number }[]): number { + + if (collection.length < 1) { + return 1; + } + + return Math.max.apply(null, collection.map(i => i.order)) + 1; +} + +/** + * Finds bounded blocks of markup bounded by divs, ensuring to match the ending div even with nested divs in the interstitial markup + * + * @param html HTML to search + * @param boundaryStartPattern The starting pattern to find, typically a div with attribute + * @param collector A func to take the found block and provide a way to form it into a useful return that is added into the return array + */ +export function getBoundedDivMarkup(html: string, boundaryStartPattern: RegExp | string, collector: (s: string) => T): T[] { + + const blocks: T[] = []; + + if (html === undefined || html === null) { + return blocks; + } + + // remove some extra whitespace if present + const cleanedHtml = html.replace(/[\t\r\n]/g, ""); + + // find the first div + let startIndex = regexIndexOf.call(cleanedHtml, boundaryStartPattern); + + if (startIndex < 0) { + // we found no blocks in the supplied html + return blocks; + } + + // this loop finds each of the blocks + while (startIndex > -1) { + + // we have one open div counting from the one found above using boundaryStartPattern so we need to ensure we find it's close + let openCounter = 1; + let searchIndex = startIndex + 1; + let nextDivOpen = -1; + let nextCloseDiv = -1; + + // this loop finds the tag that matches the opening of the control + while (true) { + + // find both the next opening and closing div tags from our current searching index + nextDivOpen = regexIndexOf.call(cleanedHtml, /]*>/i, searchIndex); + nextCloseDiv = regexIndexOf.call(cleanedHtml, /<\/div>/i, searchIndex); + + if (nextDivOpen < 0) { + // we have no more opening divs, just set this to simplify checks below + nextDivOpen = cleanedHtml.length + 1; + } + + // determine which we found first, then increment or decrement our counter + // and set the location to begin searching again + if (nextDivOpen < nextCloseDiv) { + openCounter++; + searchIndex = nextDivOpen + 1; + } else if (nextCloseDiv < nextDivOpen) { + openCounter--; + searchIndex = nextCloseDiv + 1; + } + + // once we have no open divs back to the level of the opening control div + // meaning we have all of the markup we intended to find + if (openCounter === 0) { + + // get the bounded markup, +6 is the size of the ending tag + const markup = cleanedHtml.substring(startIndex, nextCloseDiv + 6).trim(); + + // save the control data we found to the array + blocks.push(collector(markup)); + + // get out of our while loop + break; + } + + if (openCounter > 1000 || openCounter < 0) { + // this is an arbitrary cut-off but likely we will not have 1000 nested divs + // something has gone wrong above and we are probably stuck in our while loop + // let's get out of our while loop and not hang everything + throw Error("getBoundedDivMarkup exceeded depth parameters."); + } + } + + // get the start of the next control + startIndex = regexIndexOf.call(cleanedHtml, boundaryStartPattern, nextCloseDiv); + } + + return blocks; +} + +/** + * Normalizes the order value for all the sections, columns, and controls to be 1 based and stepped (1, 2, 3...) + * + * @param collection The collection to normalize + */ +export function reindex(collection: { order: number, columns?: { order: number }[], controls?: { order: number }[] }[]): void { + + for (let i = 0; i < collection.length; i++) { + collection[i].order = i + 1; + if (hOP(collection[i], "columns")) { + reindex(collection[i].columns); + } else if (hOP(collection[i], "controls")) { + reindex(collection[i].controls); + } + } +} + +/** + * After https://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr/274094#274094 + * + * @param this Types the called context this to a string in which the search will be conducted + * @param regex A regex or string to match + * @param startpos A starting position from which the search will begin + */ +function regexIndexOf(this: string, regex: RegExp | string, startpos = 0) { + const indexOf = this.substring(startpos).search(regex); + return (indexOf >= 0) ? (indexOf + (startpos)) : indexOf; +} diff --git a/packages/sp/src/clientsidepages/index.ts b/packages/sp/src/clientsidepages/index.ts new file mode 100644 index 000000000..23701ae51 --- /dev/null +++ b/packages/sp/src/clientsidepages/index.ts @@ -0,0 +1,23 @@ +import "./web"; + +export { + ClientSidePage, + IClientSidePage, + CreateClientSidePage, + LoadClientSidePage, + CanvasColumn, + CanvasColumnFactorType, + CanvasControl, + CanvasSection, + ClientSidePageLayoutType, + ClientSidePart, + ClientSideText, + ClientSideWebpart, + IClientSideControlData, + IClientSideControlPosition, + IClientSidePageComponent, + IClientSideWebpartData, + IServerProcessedContent, + PromotedState, + ClientSideWebpartPropertyTypes, +} from "./types"; diff --git a/packages/sp/src/clientsidepages/types.ts b/packages/sp/src/clientsidepages/types.ts new file mode 100644 index 000000000..56ed0e0ba --- /dev/null +++ b/packages/sp/src/clientsidepages/types.ts @@ -0,0 +1,985 @@ +import "../folders/list"; +import "../files/folder"; +import "../files/item"; +import { IList } from "../lists/types"; +import { TemplateFileType, _File, IFile } from "../files/types"; +import { Item, IItemUpdateResult } from "../items/types"; +import { getBoundedDivMarkup, getNextOrder, reindex, jsonToEscapedString, escapedStringToJson } from "./funcs"; +import { TypedHash, extend, combine, getGUID, getAttrValueFromString, hOP } from "@pnp/common"; +import { IGetable, invokableFactory } from "@pnp/odata"; + +/** + * Page promotion state + */ +export const enum PromotedState { + /** + * Regular client side page + */ + NotPromoted = 0, + /** + * Page that will be promoted as news article after publishing + */ + PromoteOnPublish = 1, + /** + * Page that is promoted as news article + */ + Promoted = 2, +} + +/** + * Type describing the available page layout types for client side "modern" pages + */ +export type ClientSidePageLayoutType = "Article" | "Home"; + +/** + * Column size factor. Max value is 12 (= one column), other options are 8,6,4 or 0 + */ +export type CanvasColumnFactorType = 0 | 2 | 4 | 6 | 8 | 12; + +/** + * Represents the data and methods associated with client side "modern" pages + */ +export class _ClientSidePage extends _File implements IClientSidePage { + + /** + * Creates a new instance of the ClientSidePage class + * + * @param baseUrl The url or SharePointQueryable which forms the parent of this web collection + * @param commentsDisabled Indicates if comments are disabled, not valid until load is called + */ + constructor(file: _File, public sections: CanvasSection[] = [], public commentsDisabled = false) { + super(file); + } + + /** + * Add a section to this page + */ + public addSection(): CanvasSection { + const section = new CanvasSection(this, getNextOrder(this.sections)); + this.sections.push(section); + return section; + } + + /** + * Converts this page's content to html markup + */ + public toHtml(): string { + + // trigger reindex of the entire tree + reindex(this.sections); + + const html: string[] = []; + + html.push("
    "); + + for (let i = 0; i < this.sections.length; i++) { + html.push(this.sections[i].toHtml()); + } + + html.push("
    "); + + return html.join(""); + } + + /** + * Loads this page instance's content from the supplied html + * + * @param html html string representing the page's content + */ + public fromHtml(html: string): this { + + // reset sections + this.sections = []; + + // gather our controls from the supplied html + getBoundedDivMarkup(html, /]*data-sp-canvascontrol[^>]*?>/i, markup => { + + // get the control type + const ct = /controlType":(\d*?),/i.exec(markup); + + // if no control type is present this is a column which we give type 0 to let us process it + const controlType = ct == null || ct.length < 2 ? 0 : parseInt(ct[1], 10); + + let control: CanvasControl = null; + + switch (controlType) { + case 0: + // empty canvas column + control = new CanvasColumn(null, 0); + control.fromHtml(markup); + this.mergeColumnToTree(control); + break; + case 3: + // client side webpart + control = new ClientSideWebpart(""); + control.fromHtml(markup); + this.mergePartToTree(control); + break; + case 4: + // client side text + control = new ClientSideText(); + control.fromHtml(markup); + this.mergePartToTree(control); + break; + } + }); + + // refresh all the orders within the tree + reindex(this.sections); + + return this; + } + + /** + * Loads this page's content from the server + */ + public async load(): Promise { + const item = await this.getItem<{ CanvasContent1: string; CommentsDisabled: boolean; }>("CanvasContent1", "CommentsDisabled"); + this.fromHtml(item.CanvasContent1); + this.commentsDisabled = item.CommentsDisabled; + } + + /** + * Persists the content changes (sections, columns, and controls) + */ + public save(): Promise { + return this.updateProperties({ CanvasContent1: this.toHtml() }); + } + + /** + * Enables comments on this page + */ + public async enableComments(): Promise { + const r = await this.setCommentsOn(true); + this.commentsDisabled = false; + return r; + } + + /** + * Disables comments on this page + */ + public async disableComments(): Promise { + const r = await this.setCommentsOn(false); + this.commentsDisabled = true; + return r; + } + + /** + * Finds a control by the specified instance id + * + * @param id Instance id of the control to find + */ + public findControlById(id: string): T { + return this.findControl((c) => c.id === id); + } + + /** + * Finds a control within this page's control tree using the supplied predicate + * + * @param predicate Takes a control and returns true or false, if true that control is returned by findControl + */ + public findControl(predicate: (c: ClientSidePart) => boolean): T { + // check all sections + for (let i = 0; i < this.sections.length; i++) { + // check all columns + for (let j = 0; j < this.sections[i].columns.length; j++) { + // check all controls + for (let k = 0; k < this.sections[i].columns[j].controls.length; k++) { + // check to see if the predicate likes this control + if (predicate(this.sections[i].columns[j].controls[k])) { + return this.sections[i].columns[j].controls[k]; + } + } + } + } + + // we found nothing so give nothing back + return null; + } + + /** + * Like the modern site page + */ + public async like(): Promise { + const i = await this.getItem(); + return i.like(); + } + + /** + * Unlike the modern site page + */ + public async unlike(): Promise { + const i = await this.getItem(); + return i.unlike(); + } + + /** + * Get the liked by information for a modern site page + */ + public getLikedByInformation(): Promise { + return this.getItem().then(i => { + return i.getLikedByInformation(); + }); + } + + /** + * Sets the comments flag for a page + * + * @param on If true comments are enabled, false they are disabled + */ + private async setCommentsOn(on: boolean): Promise { + const i = await this.getItem(); + return Item(i, `SetCommentsDisabled(${!on})`).update({}); + } + + /** + * Merges the control into the tree of sections and columns for this page + * + * @param control The control to merge + */ + private mergePartToTree(control: ClientSidePart): void { + + let section: CanvasSection = null; + let column: CanvasColumn = null; + let sectionFactor: CanvasColumnFactorType = 12; + let sectionIndex = 0; + let zoneIndex = 0; + + // handle case where we don't have position data + if (hOP(control.controlData, "position")) { + if (hOP(control.controlData.position, "zoneIndex")) { + zoneIndex = control.controlData.position.zoneIndex; + } + if (hOP(control.controlData.position, "sectionIndex")) { + sectionIndex = control.controlData.position.sectionIndex; + } + if (hOP(control.controlData.position, "sectionFactor")) { + sectionFactor = control.controlData.position.sectionFactor; + } + } + + const sections = this.sections.filter(s => s.order === zoneIndex); + if (sections.length < 1) { + section = new CanvasSection(this, zoneIndex); + this.sections.push(section); + } else { + section = sections[0]; + } + + const columns = section.columns.filter(c => c.order === sectionIndex); + if (columns.length < 1) { + column = new CanvasColumn(section, sectionIndex, sectionFactor); + section.columns.push(column); + } else { + column = columns[0]; + } + + control.column = column; + column.addControl(control); + } + + /** + * Merges the supplied column into the tree + * + * @param column Column to merge + * @param position The position data for the column + */ + private mergeColumnToTree(column: CanvasColumn): void { + + const order = hOP(column.controlData, "position") && hOP(column.controlData.position, "zoneIndex") ? column.controlData.position.zoneIndex : 0; + let section: CanvasSection = null; + const sections = this.sections.filter(s => s.order === order); + + if (sections.length < 1) { + section = new CanvasSection(this, order); + this.sections.push(section); + } else { + section = sections[0]; + } + + column.section = section; + section.columns.push(column); + } + + /** + * Updates the properties of the underlying ListItem associated with this ClientSidePage + * + * @param properties Set of properties to update + * @param eTag Value used in the IF-Match header, by default "*" + */ + private async updateProperties(properties: TypedHash, eTag = "*"): Promise { + const i = await this.getItem(); + return await i.update(properties, eTag); + } +} + +export interface IClientSidePage extends IGetable, IFile { + + sections: CanvasSection[]; + + commentsDisabled: boolean; + + /** + * Add a section to this page + */ + addSection(): CanvasSection; + + /** + * Converts this page's content to html markup + */ + toHtml(): string; + + /** + * Loads this page instance's content from the supplied html + * + * @param html html string representing the page's content + */ + fromHtml(html: string): this; + + /** + * Loads this page's content from the server + */ + load(): Promise; + + /** + * Persists the content changes (sections, columns, and controls) + */ + save(): Promise; + + /** + * Enables comments on this page + */ + enableComments(): Promise; + + /** + * Disables comments on this page + */ + disableComments(): Promise; + + /** + * Finds a control by the specified instance id + * + * @param id Instance id of the control to find + */ + findControlById(id: string): T; + + /** + * Finds a control within this page's control tree using the supplied predicate + * + * @param predicate Takes a control and returns true or false, if true that control is returned by findControl + */ + findControl(predicate: (c: ClientSidePart) => boolean): T; + + /** + * Like the modern site page + */ + like(): Promise; + + /** + * Unlike the modern site page + */ + unlike(): Promise; + + /** + * Get the liked by information for a modern site page + */ + getLikedByInformation(): Promise; +} + +export interface _ClientSidePage extends IGetable { } +export const ClientSidePage: (file: IFile, sections?: CanvasSection[], commentsDisabled?: boolean) => IClientSidePage = invokableFactory(_ClientSidePage); + +/** + * Creates a new blank page within the supplied library + * + * @param library The library in which to create the page + * @param pageName Filename of the page, such as "page.aspx" + * @param title The display title of the page + * @param pageLayoutType Layout type of the page to use + */ +export const CreateClientSidePage = async function (library: IList, pageName: string, title: string, pageLayoutType?: ClientSidePageLayoutType): Promise { + + // see if file exists, if not create it + const fs = await library.rootFolder.files.select("Name").filter(`Name eq '${pageName}'`).top(1)(); + if (fs.length > 0) { + throw Error(`A file with the name '${pageName}' already exists in the library '${library.toUrl()}'.`); + } + const path = await library.rootFolder.select("ServerRelativePath")(); + const pageServerRelPath = combine("/", path.ServerRelativePath.DecodedUrl, pageName); + const far = await library.rootFolder.files.addTemplateFile(pageServerRelPath, TemplateFileType.ClientSidePage); + const i = await far.file.getItem(); + const iar = await i.update({ + BannerImageUrl: { + Url: "/_layouts/15/images/sitepagethumbnail.png", + }, + CanvasContent1: "", + ClientSideApplicationId: "b6917cb1-93a0-4b97-a84d-7cf49975d4ec", + ContentTypeId: "0x0101009D1CB255DA76424F860D91F20E6C4118", + PageLayoutType: pageLayoutType, + PromotedState: PromotedState.NotPromoted, + Title: title, + }); + return ClientSidePage((<_File>iar.item.file), (iar.item).CommentsDisabled); +}; + +export const LoadClientSidePage = async function (file: IFile): Promise { + const page = ClientSidePage(file); + await page.load(); + return page; +}; + +export class CanvasSection { + + /** + * Used to track this object inside the collection at runtime + */ + private _memId: string; + + constructor(public page: IClientSidePage, public order: number, public columns: CanvasColumn[] = []) { + this._memId = getGUID(); + } + + /** + * Default column (this.columns[0]) for this section + */ + public get defaultColumn(): CanvasColumn { + + if (this.columns.length < 1) { + this.addColumn(12); + } + + return this.columns[0]; + } + + /** + * Adds a new column to this section + */ + public addColumn(factor: CanvasColumnFactorType): CanvasColumn { + + const column = new CanvasColumn(this, getNextOrder(this.columns), factor); + this.columns.push(column); + return column; + } + + /** + * Adds a control to the default column for this section + * + * @param control Control to add to the default column + */ + public addControl(control: ClientSidePart): this { + this.defaultColumn.addControl(control); + return this; + } + + public toHtml(): string { + + const html = []; + + for (let i = 0; i < this.columns.length; i++) { + html.push(this.columns[i].toHtml()); + } + + return html.join(""); + } + + /** + * Removes this section and all contained columns and controls from the collection + */ + public remove(): void { + this.page.sections = this.page.sections.filter(section => section._memId !== this._memId); + reindex(this.page.sections); + } +} + +export abstract class CanvasControl { + + constructor( + protected controlType: number, + public dataVersion: string, + public column: CanvasColumn = null, + public order = 1, + public id: string = getGUID(), + public controlData: IClientSideControlData = null) { } + + /** + * Value of the control's "data-sp-controldata" attribute + */ + public get jsonData(): string { + return jsonToEscapedString(this.getControlData()); + } + + public abstract toHtml(index: number): string; + + public fromHtml(html: string): void { + this.controlData = escapedStringToJson(getAttrValueFromString(html, "data-sp-controldata")); + this.dataVersion = getAttrValueFromString(html, "data-sp-canvasdataversion"); + this.controlType = this.controlData.controlType; + this.id = this.controlData.id; + } + + protected abstract getControlData(): IClientSideControlData; +} + +export class CanvasColumn extends CanvasControl { + + constructor( + public section: CanvasSection, + public order: number, + public factor: CanvasColumnFactorType = 12, + public controls: ClientSidePart[] = [], + dataVersion = "1.0") { + super(0, dataVersion); + } + + public addControl(control: ClientSidePart): this { + control.column = this; + this.controls.push(control); + return this; + } + + public getControl(index: number): T { + return this.controls[index]; + } + + public toHtml(): string { + const html = []; + + if (this.controls.length < 1) { + + html.push(`
    `); + + } else { + + for (let i = 0; i < this.controls.length; i++) { + html.push(this.controls[i].toHtml(i + 1)); + } + } + + return html.join(""); + } + + public fromHtml(html: string): void { + super.fromHtml(html); + + this.controlData = escapedStringToJson(getAttrValueFromString(html, "data-sp-controldata")); + if (hOP(this.controlData, "position")) { + if (hOP(this.controlData.position, "sectionFactor")) { + this.factor = this.controlData.position.sectionFactor; + } + if (hOP(this.controlData.position, "sectionIndex")) { + this.order = this.controlData.position.sectionIndex; + } + } + } + + public getControlData(): IClientSideControlData { + return { + displayMode: 2, + position: { + sectionFactor: this.factor, + sectionIndex: this.order, + zoneIndex: this.section.order, + }, + }; + } + + /** + * Removes this column and all contained controls from the collection + */ + public remove(): void { + this.section.columns = this.section.columns.filter(column => column.id !== this.id); + reindex(this.column.controls); + } +} + +/** + * Abstract class with shared functionality for parts + */ +export abstract class ClientSidePart extends CanvasControl { + + /** + * Removes this column and all contained controls from the collection + */ + public remove(): void { + this.column.controls = this.column.controls.filter(control => control.id !== this.id); + reindex(this.column.controls); + } +} + +export class ClientSideText extends ClientSidePart { + + private _text: string; + + constructor(text = "") { + super(4, "1.0"); + + this.text = text; + } + + /** + * The text markup of this control + */ + public get text(): string { + return this._text; + } + + public set text(text: string) { + + if (!text.startsWith("

    ")) { + text = `

    ${text}

    `; + } + + this._text = text; + } + + public getControlData(): IClientSideControlData { + + return { + controlType: this.controlType, + editorType: "CKEditor", + id: this.id, + position: { + controlIndex: this.order, + sectionFactor: this.column.factor, + sectionIndex: this.column.order, + zoneIndex: this.column.section.order, + }, + }; + } + + public toHtml(index: number): string { + + // set our order to the value passed in + this.order = index; + + const html: string[] = []; + + html.push(`
    `); + html.push("
    "); + html.push(`${this.text}`); + html.push("
    "); + html.push("
    "); + + return html.join(""); + } + + public fromHtml(html: string): void { + + super.fromHtml(html); + + this.text = ""; + + getBoundedDivMarkup(html, /]*data-sp-rte[^>]*>/i, (s: string) => { + + // now we need to grab the inner text between the divs + const match = /]*data-sp-rte[^>]*>(.*?)<\/div>$/i.exec(s); + + this.text = match.length > 1 ? match[1] : ""; + }); + } +} + +export class ClientSideWebpart extends ClientSidePart { + + constructor(public title: string, + public description = "", + public propertieJson: TypedHash = {}, + public webPartId = "", + protected htmlProperties = "", + protected serverProcessedContent: IServerProcessedContent = null, + protected canvasDataVersion = "1.0") { + super(3, "1.0"); + } + + public static fromComponentDef(definition: IClientSidePageComponent): ClientSideWebpart { + const part = new ClientSideWebpart(""); + part.import(definition); + return part; + } + + public import(component: IClientSidePageComponent): void { + this.webPartId = component.Id.replace(/^\{|\}$/g, "").toLowerCase(); + const manifest: IClientSidePageComponentManifest = JSON.parse(component.Manifest); + this.title = manifest.preconfiguredEntries[0].title.default; + this.description = manifest.preconfiguredEntries[0].description.default; + this.dataVersion = "1.0"; + this.propertieJson = this.parseJsonProperties(manifest.preconfiguredEntries[0].properties); + } + + public setProperties(properties: T): this { + this.propertieJson = extend(this.propertieJson, properties); + return this; + } + + public getProperties(): T { + return this.propertieJson; + } + + public toHtml(index: number): string { + + // set our order to the value passed in + this.order = index; + + // will form the value of the data-sp-webpartdata attribute + const data = { + dataVersion: this.dataVersion, + description: this.description, + id: this.webPartId, + instanceId: this.id, + properties: this.propertieJson, + serverProcessedContent: this.serverProcessedContent, + title: this.title, + }; + + const html: string[] = []; + + html.push(`
    `); + + html.push(`
    `); + + html.push(`
    `); + html.push(this.webPartId); + html.push("
    "); + + html.push(`
    `); + html.push(this.renderHtmlProperties()); + html.push("
    "); + + html.push("
    "); + html.push("
    "); + + return html.join(""); + } + + public fromHtml(html: string): void { + + super.fromHtml(html); + + const webPartData = escapedStringToJson(getAttrValueFromString(html, "data-sp-webpartdata")); + + this.title = webPartData.title; + this.description = webPartData.description; + this.webPartId = webPartData.id; + this.canvasDataVersion = getAttrValueFromString(html, "data-sp-canvasdataversion").replace(/\\\./, "."); + this.dataVersion = getAttrValueFromString(html, "data-sp-webpartdataversion").replace(/\\\./, "."); + this.setProperties(webPartData.properties); + + if (webPartData.serverProcessedContent !== undefined) { + this.serverProcessedContent = webPartData.serverProcessedContent; + } + + // get our html properties + const htmlProps = getBoundedDivMarkup(html, /]*data-sp-htmlproperties[^>]*?>/i, markup => { + return markup.replace(/^]*data-sp-htmlproperties[^>]*?>/i, "").replace(/<\/div>$/i, ""); + }); + + this.htmlProperties = htmlProps.length > 0 ? htmlProps[0] : ""; + } + + public getControlData(): IClientSideControlData { + + return { + controlType: this.controlType, + id: this.id, + position: { + controlIndex: this.order, + sectionFactor: this.column.factor, + sectionIndex: this.column.order, + zoneIndex: this.column.section.order, + }, + webPartId: this.webPartId, + }; + + } + + protected renderHtmlProperties(): string { + + const html: string[] = []; + + if (this.serverProcessedContent === undefined || this.serverProcessedContent === null) { + + html.push(this.htmlProperties); + + } else if (this.serverProcessedContent !== undefined) { + + if (this.serverProcessedContent.searchablePlainTexts !== undefined) { + + const keys = Object.keys(this.serverProcessedContent.searchablePlainTexts); + for (let i = 0; i < keys.length; i++) { + html.push(`
    `); + html.push(this.serverProcessedContent.searchablePlainTexts[keys[i]]); + html.push("
    "); + } + } + + if (this.serverProcessedContent.imageSources !== undefined) { + + const keys = Object.keys(this.serverProcessedContent.imageSources); + for (let i = 0; i < keys.length; i++) { + html.push(``); + } + } + + if (this.serverProcessedContent.links !== undefined) { + + const keys = Object.keys(this.serverProcessedContent.links); + for (let i = 0; i < keys.length; i++) { + html.push(`
    `); + } + } + } + + return html.join(""); + } + + protected parseJsonProperties(props: TypedHash): any { + + // If the web part has the serverProcessedContent property then keep this one as it might be needed as input to render the web part HTML later on + if (props.webPartData !== undefined && props.webPartData.serverProcessedContent !== undefined) { + this.serverProcessedContent = props.webPartData.serverProcessedContent; + } else if (props.serverProcessedContent !== undefined) { + this.serverProcessedContent = props.serverProcessedContent; + } else { + this.serverProcessedContent = null; + } + + if (props.webPartData !== undefined && props.webPartData.properties !== undefined) { + return props.webPartData.properties; + } else if (props.properties !== undefined) { + return props.properties; + } else { + return props; + } + } +} + +/** + * Client side webpart object (retrieved via the _api/web/GetClientSideWebParts REST call) + */ +export interface IClientSidePageComponent { + /** + * Component type for client side webpart object + */ + ComponentType: number; + /** + * Id for client side webpart object + */ + Id: string; + /** + * Manifest for client side webpart object + */ + Manifest: string; + /** + * Manifest type for client side webpart object + */ + ManifestType: number; + /** + * Name for client side webpart object + */ + Name: string; + /** + * Status for client side webpart object + */ + Status: number; +} + +interface IClientSidePageComponentManifest { + alias: string; + componentType: "WebPart" | "" | null; + disabledOnClassicSharepoint: boolean; + hiddenFromToolbox: boolean; + id: string; + imageLinkPropertyNames: any; + isInternal: boolean; + linkPropertyNames: boolean; + loaderConfig: any; + manifestVersion: number; + preconfiguredEntries: { + description: { default: string }; + group: { default: string }; + groupId: string; + iconImageUrl: string; + officeFabricIconFontName: string; + properties: TypedHash; + title: { default: string }; + + }[]; + preloadComponents: any | null; + requiredCapabilities: any | null; + searchablePropertyNames: any | null; + supportsFullBleed: boolean; + version: string; +} + +export interface IServerProcessedContent { + searchablePlainTexts: TypedHash; + imageSources: TypedHash; + links: TypedHash; +} + +export interface IClientSideControlPosition { + controlIndex?: number; + sectionFactor: CanvasColumnFactorType; + sectionIndex: number; + zoneIndex: number; +} + +export interface IClientSideControlData { + controlType?: number; + id?: string; + editorType?: string; + position: IClientSideControlPosition; + webPartId?: string; + displayMode?: number; +} + +export interface IClientSideWebpartData { + dataVersion: string; + description: string; + id: string; + instanceId: string; + properties: any; + title: string; + serverProcessedContent?: IServerProcessedContent; +} + +export module ClientSideWebpartPropertyTypes { + + /** + * Propereties for Embed (component id: 490d7c76-1824-45b2-9de3-676421c997fa) + */ + export interface IEmbed { + embedCode: string; + cachedEmbedCode?: string; + shouldScaleWidth?: boolean; + tempState?: any; + } + + /** + * Properties for Bing Map (component id: e377ea37-9047-43b9-8cdb-a761be2f8e09) + */ + export interface IBingMap { + center: { + altitude?: number; + altitudeReference?: number; + latitude: number; + longitude: number; + }; + mapType: "aerial" | "birdseye" | "road" | "streetside"; + maxNumberOfPushPins?: number; + pushPins?: { + location: { + latitude: number; + longitude: number; + altitude?: number; + altitudeReference?: number; + }; + address?: string; + defaultAddress?: string; + defaultTitle?: string; + title?: string; + }[]; + shouldShowPushPinTitle?: boolean; + zoomLevel?: number; + } +} diff --git a/packages/sp/src/clientsidepages/web.ts b/packages/sp/src/clientsidepages/web.ts new file mode 100644 index 000000000..e8574d659 --- /dev/null +++ b/packages/sp/src/clientsidepages/web.ts @@ -0,0 +1,35 @@ +import "../lists/web"; +import { _Web } from "../webs/types"; +import { IClientSidePageComponent, CreateClientSidePage, IClientSidePage } from "./types"; +import { _SharePointQueryableCollection, SharePointQueryableCollection } from "../sharepointqueryable"; + +declare module "../webs/types" { + interface _Web { + getClientSideWebParts(): Promise; + addClientSidePage(pageName: string, title?: string, libraryTitle?: string): Promise; + } + interface IWeb { + + /** + * Gets the collection of available client side web parts for this web instance + */ + getClientSideWebParts(): Promise; + + /** + * Creates a new client side page + * + * @param pageName Name of the new page + * @param title Display title of the new page + * @param libraryTitle Title of the library in which to create the new page. Default: "Site Pages" + */ + addClientSidePage(pageName: string, title?: string, libraryTitle?: string): Promise; + } +} + +_Web.prototype.getClientSideWebParts = function (): Promise { + return this.clone(SharePointQueryableCollection, "GetClientSideWebParts").get(); +}; + +_Web.prototype.addClientSidePage = function (this: _Web, pageName: string, title = pageName.replace(/\.[^/.]+$/, ""), libraryTitle = "Site Pages"): Promise { + return CreateClientSidePage(this.lists.getByTitle(libraryTitle), pageName, title); +}; diff --git a/packages/sp/src/comments/index.ts b/packages/sp/src/comments/index.ts new file mode 100644 index 000000000..84c61c0a2 --- /dev/null +++ b/packages/sp/src/comments/index.ts @@ -0,0 +1,13 @@ +import "./item"; + +export { + Comment, + Comments, + IComment, + ICommentAuthorData, + ICommentData, + IComments, + IReplies, + ICommentInfo, + Replies, +} from "./types"; diff --git a/packages/sp/src/comments/item.ts b/packages/sp/src/comments/item.ts new file mode 100644 index 000000000..de853c79a --- /dev/null +++ b/packages/sp/src/comments/item.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Item } from "../items/types"; +import { Comments, IComments } from "./types"; + +/** +* Extend Web +*/ +declare module "../items/types" { + interface _Item { + readonly comments: IComments; + } + interface IItem { + readonly comments: IComments; + } +} + +addProp(_Item, "comments", Comments); diff --git a/packages/sp/src/comments/types.ts b/packages/sp/src/comments/types.ts new file mode 100644 index 000000000..0847ae6ec --- /dev/null +++ b/packages/sp/src/comments/types.ts @@ -0,0 +1,171 @@ +import { defaultPath } from "../decorators"; +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { extend } from "@pnp/common"; +import { odataUrlFrom } from "../odata"; +import { metadata } from "../utils/metadata"; +import { IGetable, body } from "@pnp/odata"; +import { spPost } from "../operations"; + +export interface ICommentAuthorData { + email: string; + id: number; + isActive: boolean; + isExternal: boolean; + jobTitle: string | null; + loginName: string; + name: string; + principalType: number; + userId: any | null; +} + +export interface ICommentData { + author: ICommentAuthorData; + createdDate: string; + id: string; + isLikedByUser: boolean; + isReply: boolean; + itemId: number; + likeCount: number; + listId: string; + mentions: any | null; + parentId: string; + replyCount: number; + text: string; +} + +export interface ICommentInfo { + text: string; + mentions?: { + loginName: string; + email: string; + name: string; + }; +} + +/** + * Represents a Collection of comments + */ +@defaultPath("comments") +export class _Comments extends _SharePointQueryableCollection implements IComments { + + /** + * Adds a new comment to this collection + * + * @param info Comment information to add + */ + public async add(info: string | ICommentInfo): Promise { + + if (typeof info === "string") { + info = { text: info }; + } + + const postBody = body(extend(metadata("Microsoft.SharePoint.Comments.comment"), info)); + + const d = await spPost(this.clone(Comments, null), postBody); + + return extend(this.getById(d.id), d); + } + + /** + * Gets a comment by id + * + * @param id Id of the comment to load + */ + public getById(id: string | number): IComment { + const c = Comment(this); + c.concat(`(${id})`); + return c; + } + + /** + * Deletes all the comments in this collection + */ + public clear(): Promise { + return spPost(this.clone(Comments, "DeleteAll")); + } +} + +export interface IComments extends IGetable, ISharePointQueryableCollection { + add(info: string | ICommentInfo): Promise; + getById(id: string | number): IComment; + clear(): Promise; +} +export interface _Comments extends IGetable { } +export const Comments = spInvokableFactory(_Comments); + +/** + * Represents a comment + */ +export class _Comment extends _SharePointQueryableInstance { + + public get replies(): IReplies { + return Replies(this); + } + + /** + * Likes the comment as the current user + */ + public like(): Promise { + return spPost(this.clone(Comment, "Like")); + } + + /** + * Unlikes the comment as the current user + */ + public unlike(): Promise { + return spPost(this.clone(Comment, "Unlike")); + } + + /** + * Deletes this comment + */ + public delete(): Promise { + return spPost(this.clone(Comment, "DeleteComment")); + } +} + +export interface IComment extends IGetable, ISharePointQueryableInstance { + readonly replies: IReplies; + like(): Promise; + unlike(): Promise; + delete(): Promise; +} +export interface _Comment extends IGetable { } +export const Comment = spInvokableFactory(_Comment); + +/** + * Represents a Collection of comments + */ +@defaultPath("replies") +export class _Replies extends _SharePointQueryableCollection implements IReplies { + + /** + * Adds a new reply to this collection + * + * @param info Comment information to add + */ + public async add(info: string | ICommentInfo): Promise { + + if (typeof info === "string") { + info = { text: info }; + } + + const postBody = body(extend(metadata("Microsoft.SharePoint.Comments.comment"), info)); + + const d = await spPost(this.clone(Replies, null), postBody); + + return extend(Comment(odataUrlFrom(d)), d); + } +} + +export interface IReplies extends IGetable, ISharePointQueryableCollection { + add(info: string | ICommentInfo): Promise; +} +export interface _Replies extends IGetable { } +export const Replies = spInvokableFactory(_Replies); diff --git a/packages/sp/src/config/splibconfig.ts b/packages/sp/src/config/splibconfig.ts new file mode 100644 index 000000000..5a48081b4 --- /dev/null +++ b/packages/sp/src/config/splibconfig.ts @@ -0,0 +1,72 @@ +import { + LibraryConfiguration, + TypedHash, + RuntimeConfig, + IHttpClientImpl, + FetchClient, + objectDefinedNotNull, +} from "@pnp/common"; + +export interface SPConfigurationPart { + sp?: { + /** + * Any headers to apply to all requests + */ + headers?: TypedHash; + + /** + * The base url used for all requests + */ + baseUrl?: string; + + /** + * Defines a factory method used to create fetch clients + */ + fetchClientFactory?: () => IHttpClientImpl; + }; +} + +export interface SPConfiguration extends LibraryConfiguration, SPConfigurationPart { } + +export function setup(config: SPConfiguration): void { + RuntimeConfig.extend(config); +} + +export class SPRuntimeConfigImpl { + + public get headers(): TypedHash { + + const spPart = RuntimeConfig.get("sp"); + if (spPart !== undefined && spPart.headers !== undefined) { + return spPart.headers; + } + + return {}; + } + + public get baseUrl(): string | null { + + const spPart = RuntimeConfig.get("sp"); + if (spPart !== undefined && spPart.baseUrl !== undefined) { + return spPart.baseUrl; + } + + if (objectDefinedNotNull(RuntimeConfig.spfxContext)) { + return RuntimeConfig.spfxContext.pageContext.web.absoluteUrl; + } + + return null; + } + + public get fetchClientFactory(): () => IHttpClientImpl { + + const spPart = RuntimeConfig.get("sp"); + if (spPart !== undefined && spPart.fetchClientFactory !== undefined) { + return spPart.fetchClientFactory; + } else { + return () => new FetchClient(); + } + } +} + +export let SPRuntimeConfig = new SPRuntimeConfigImpl(); diff --git a/packages/sp/src/content-types/index.ts b/packages/sp/src/content-types/index.ts new file mode 100644 index 000000000..5c059059d --- /dev/null +++ b/packages/sp/src/content-types/index.ts @@ -0,0 +1,15 @@ +import "./web"; +import "./item"; +import "./list"; + +export { + ContentType, + ContentTypes, + IContentType, + IContentTypes, + ContentTypeAddResult, + FieldLink, + FieldLinks, + IFieldLink, + IFieldLinks, +} from "./types"; diff --git a/packages/sp/src/content-types/item.ts b/packages/sp/src/content-types/item.ts new file mode 100644 index 000000000..a1325f257 --- /dev/null +++ b/packages/sp/src/content-types/item.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Item } from "../items/types"; +import { ContentType, IContentType } from "./types"; + +/** +* Extend Item +*/ +declare module "../items/types" { + interface _Item { + readonly contentType: IContentType; + } + interface IItem { + readonly contentType: IContentType; + } +} + +addProp(_Item, "contentType", ContentType, "ContentType"); diff --git a/packages/sp/src/content-types/list.ts b/packages/sp/src/content-types/list.ts new file mode 100644 index 000000000..c1e7d7217 --- /dev/null +++ b/packages/sp/src/content-types/list.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { ContentTypes, IContentTypes } from "./types"; + +/** +* Extend List +*/ +declare module "../lists/types" { + interface _List { + readonly contentTypes: IContentTypes; + } + interface IList { + readonly contentTypes: IContentTypes; + } +} + +addProp(_List, "contentTypes", ContentTypes); diff --git a/packages/sp/src/content-types/types.ts b/packages/sp/src/content-types/types.ts new file mode 100644 index 000000000..544ec4ce3 --- /dev/null +++ b/packages/sp/src/content-types/types.ts @@ -0,0 +1,160 @@ +import { TypedHash } from "@pnp/common"; +import { IGetable, body } from "@pnp/odata"; +import { + SharePointQueryableCollection, + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { defaultPath, deleteable, IDeleteable } from "../decorators"; +import { metadata } from "../utils/metadata"; +import { spPost } from "../operations"; + +/** + * Describes a collection of content types + * + */ +@defaultPath("contenttypes") +export class _ContentTypes extends _SharePointQueryableCollection implements IContentTypes { + + /** + * Adds an existing contenttype to a content type collection + * + * @param contentTypeId in the following format, for example: 0x010102 + */ + public async addAvailableContentType(contentTypeId: string): Promise { + + const data = await spPost(this.clone(ContentTypes, "addAvailableContentType"), body({ "contentTypeId": contentTypeId })); + return { + contentType: this.getById(data.id), + data: data, + }; + } + + /** + * Gets a ContentType by content type id + */ + public getById(id: string): IContentType { + return ContentType(this).concat(`('${id}')`); + } + + /** + * Adds a new content type to the collection + * + * @param id The desired content type id for the new content type (also determines the parent content type) + * @param name The name of the content type + * @param description The description of the content type + * @param group The group in which to add the content type + * @param additionalSettings Any additional settings to provide when creating the content type + * + */ + public async add( + id: string, + name: string, + description = "", + group = "Custom Content Types", + additionalSettings: TypedHash = {}): Promise { + + const postBody = body(Object.assign(metadata("SP.ContentType"), { + "Description": description, + "Group": group, + "Id": { "StringValue": id }, + "Name": name, + }, additionalSettings)); + + const data = await spPost(this, postBody); + return { contentType: this.getById(data.id), data }; + } +} + +export interface IContentTypes extends IGetable, ISharePointQueryableCollection { + addAvailableContentType(contentTypeId: string): Promise; + getById(id: string): IContentType; + add(id: string, name: string, description?: string, group?: string, additionalSettings?: TypedHash): Promise; +} +export interface _ContentTypes extends IGetable { } +export const ContentTypes = spInvokableFactory(_ContentTypes); + +/** + * Describes a single ContentType instance + * + */ +@deleteable() +export class _ContentType extends _SharePointQueryableInstance implements IContentType { + + /** + * Gets the column (also known as field) references in the content type. + */ + public get fieldLinks(): IFieldLinks { + return FieldLinks(this); + } + + /** + * Gets a value that specifies the collection of fields for the content type. + */ + public get fields(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "fields"); + } + + /** + * Gets the parent content type of the content type. + */ + public get parent(): IContentType { + return ContentType(this, "parent"); + } + + /** + * Gets a value that specifies the collection of workflow associations for the content type. + */ + public get workflowAssociations(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "workflowAssociations"); + } +} + +export interface IContentType extends IGetable, ISharePointQueryableInstance, IDeleteable { + readonly fieldLinks: IFieldLinks; + readonly fields: ISharePointQueryableCollection; + readonly parent: IContentType; + readonly workflowAssociations: ISharePointQueryableCollection; +} +export interface _ContentType extends IGetable, IDeleteable {} +export const ContentType = spInvokableFactory(_ContentType); + +export interface ContentTypeAddResult { + contentType: IContentType; + data: any; +} + +/** + * Represents a collection of field link instances + */ +@defaultPath("fieldlinks") +export class _FieldLinks extends _SharePointQueryableCollection implements IFieldLinks { + /** + * Gets a FieldLink by GUID id + * + * @param id The GUID id of the field link + */ + public getById(id: string): IFieldLink { + return FieldLink(this).concat(`(guid'${id}')`); + } +} + +export interface IFieldLinks extends IGetable, ISharePointQueryableCollection { + getById(id: string): IFieldLink; +} +export interface _FieldLinks extends IGetable { } +export const FieldLinks = spInvokableFactory(_FieldLinks); + +/** + * Represents a field link instance + */ +export class _FieldLink extends _SharePointQueryableInstance implements IFieldLink { } + +export interface IFieldLink extends IGetable, _SharePointQueryableInstance { + +} +export interface _FieldLink extends IGetable { } +export const FieldLink = spInvokableFactory(_FieldLink); diff --git a/packages/sp/src/content-types/web.ts b/packages/sp/src/content-types/web.ts new file mode 100644 index 000000000..39c7952a2 --- /dev/null +++ b/packages/sp/src/content-types/web.ts @@ -0,0 +1,18 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { ContentTypes, IContentTypes } from "./types"; + +declare module "../webs/types" { + interface _Web { + readonly contentTypes: IContentTypes; + } + interface IWeb { + + /** + * Content types contained in this web + */ + readonly contentTypes: IContentTypes; + } +} + +addProp(_Web, "contentTypes", ContentTypes); diff --git a/packages/sp/src/decorators.ts b/packages/sp/src/decorators.ts new file mode 100644 index 000000000..7696ba593 --- /dev/null +++ b/packages/sp/src/decorators.ts @@ -0,0 +1,119 @@ +import { headers } from "@pnp/odata"; +import { stringIsNullOrEmpty } from "@pnp/common"; +import { spPostDelete } from "./operations"; +import { ISharePointQueryable } from "./sharepointqueryable"; + +/** + * Class Decorators + */ + +/** + * Decorator used to specify the default path for SharePointQueryable objects + * + * @param path + */ +export function defaultPath(path: string) { + + return function (target: T) { + + return class extends target { + constructor(...args: any[]) { + super(args[0], args.length > 1 && args[1] !== undefined ? args[1] : path); + } + }; + }; +} + +/** + * Adds the a delete method to the tagged class taking no parameters and calling spPostDelete + */ +export function deleteable() { + return function (target: T) { + + return class extends target { + public delete(this: ISharePointQueryable): Promise { + return spPostDelete(this); + } + }; + }; +} + +export interface IDeleteable { + /** + * Delete this instance + */ + delete(): Promise; +} + +export function deleteableWithETag() { + return function (target: T) { + + return class extends target { + public delete(this: ISharePointQueryable, eTag = "*"): Promise { + return spPostDelete(this, headers({ + "IF-Match": eTag, + "X-HTTP-Method": "DELETE", + })); + } + }; + }; +} + +export interface IDeleteableWithETag { + /** + * Delete this instance + * + * @param eTag Value used in the IF-Match header, by default "*" + */ + delete(eTag?: string): Promise; +} + +/** + * Method Decorators + */ + +/** + * Includes this method name in the X-ClientService-ClientTag used to record pnpjs usage + * + * @param name Method name, displayed in the + */ +export function clientTagMethod(name: string) { + return function (target: any, key: string, descriptor: PropertyDescriptor) { + + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key); + } + const originalMethod = descriptor.value; + + descriptor.value = async function (this: ISharePointQueryable, ...args: any[]) { + + this.configure(headers({ "X-PnPjs-Tracking": name })); + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} +clientTagMethod.getClientTag = (h: Headers, deleteFromCollection = true): string => { + if (h.has("X-PnPjs-Tracking")) { + const methodName = h.get("X-PnPjs-Tracking"); + if (deleteFromCollection) { + h.delete("X-PnPjs-Tracking"); + } + if (!stringIsNullOrEmpty(methodName)) { + return methodName; + } + } + return ""; +}; +clientTagMethod.configure = (o: T, name: string): T => { + return o.configure(headers({ "X-PnPjs-Tracking": name })); +}; + + // TODO::? +// performance tracking method decorator +// redirect to graph api + + + + diff --git a/packages/sp/src/features/index.ts b/packages/sp/src/features/index.ts new file mode 100644 index 000000000..9e5bbfe16 --- /dev/null +++ b/packages/sp/src/features/index.ts @@ -0,0 +1,10 @@ +import "./site"; +import "./web"; + +export { + Feature, + IFeature, + Features, + IFeatures, + FeatureAddResult, +} from "./types"; diff --git a/packages/sp/src/features/site.ts b/packages/sp/src/features/site.ts new file mode 100644 index 000000000..6ae5a1591 --- /dev/null +++ b/packages/sp/src/features/site.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Site } from "../sites/types"; +import { Features, IFeatures } from "./types"; + +/** + * Extend Site + */ +declare module "../sites/types" { + interface _Site { + readonly features: IFeatures; + } + interface ISite { + readonly features: IFeatures; + } +} + +addProp(_Site, "features", Features); diff --git a/packages/sp/src/features/types.ts b/packages/sp/src/features/types.ts new file mode 100644 index 000000000..cdf576c32 --- /dev/null +++ b/packages/sp/src/features/types.ts @@ -0,0 +1,104 @@ +import { IGetable, body } from "@pnp/odata"; +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { defaultPath } from "../decorators"; +import { spPost } from "../operations"; +import { SPBatch } from "../batch"; + +/** + * Describes a collection of List objects + * + */ +@defaultPath("features") +export class _Features extends _SharePointQueryableCollection implements IFeatures { + + /** + * Adds a new list to the collection + * + * @param id The Id of the feature (GUID) + * @param force If true the feature activation will be forced + */ + public async add(id: string, force = false): Promise { + + const data = await spPost(this.clone(Features, "add"), body({ + featdefScope: 0, + featureId: id, + force: force, + })); + + return { + data: data, + feature: this.getById(id), + }; + } + + /** + * Gets a list from the collection by guid id + * + * @param id The Id of the feature (GUID) + */ + public getById(id: string): IFeature { + const feature = Feature(this); + feature.concat(`('${id}')`); + return feature; + } + + /** + * Removes (deactivates) a feature from the collection + * + * @param id The Id of the feature (GUID) + * @param force If true the feature deactivation will be forced + */ + public remove(id: string, force = false): Promise { + + return spPost(this.clone(Features, "remove"), body({ + featureId: id, + force: force, + })); + } +} + +export interface IFeatures extends IGetable, ISharePointQueryableCollection { + add(id: string, force?: boolean): Promise; + getById(id: string): IFeature; + remove(id: string, force?: boolean): Promise; +} +export interface _Features extends IGetable { } +export const Features = spInvokableFactory(_Features); + +export class _Feature extends _SharePointQueryableInstance implements IFeature { + + /** + * Removes (deactivates) a feature from the collection + * + * @param force If true the feature deactivation will be forced + */ + public async deactivate(force = false): Promise { + + const removeDependency = this.addBatchDependency(); + + const feature = await Feature(this).select("DefinitionId")<{ DefinitionId: string; }>(); + + const promise = this.getParent(_Features, this.parentUrl, "", this.batch).remove(feature.DefinitionId, force); + + removeDependency(); + + return promise; + } +} + +export interface IFeature extends IGetable, ISharePointQueryableInstance { + deactivate(force?: boolean): Promise; +} +export interface _Feature extends IGetable { } +export const Feature = spInvokableFactory(_Feature); + +export interface FeatureAddResult { + data: any; + feature: IFeature; +} diff --git a/packages/sp/src/features/web.ts b/packages/sp/src/features/web.ts new file mode 100644 index 000000000..2d8463322 --- /dev/null +++ b/packages/sp/src/features/web.ts @@ -0,0 +1,18 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { Features, IFeatures } from "./types"; + +declare module "../webs/types" { + interface _Web { + readonly features: IFeatures; + } + interface IWeb { + + /** + * This web's collection of features + */ + readonly features: IFeatures; + } +} + +addProp(_Web, "features", Features); diff --git a/packages/sp/src/fields/index.ts b/packages/sp/src/fields/index.ts new file mode 100644 index 000000000..7e4612dfb --- /dev/null +++ b/packages/sp/src/fields/index.ts @@ -0,0 +1,23 @@ +// extend everything if they include the root +import "./web"; +import "./list"; +import "./web"; + +export { + IFields, + Fields, + IField, + Field, + FieldAddResult, + FieldUpdateResult, + AddFieldOptions, + CalendarType, + ChoiceFieldFormatType, + DateTimeFieldFormatType, + DateTimeFieldFriendlyFormatType, + FieldTypes, + FieldUserSelectionMode, + IFieldCreationProperties, + UrlFieldFormatType, + XmlSchemaFieldCreationInformation, +} from "./types"; diff --git a/packages/sp/src/fields/list.ts b/packages/sp/src/fields/list.ts new file mode 100644 index 000000000..937a29756 --- /dev/null +++ b/packages/sp/src/fields/list.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { Fields, IFields } from "./types"; + +/** +* Extend List +*/ +declare module "../lists/types" { + interface _List { + readonly fields: IFields; + } + interface IList { + readonly fields: IFields; + } +} + +addProp(_List, "fields", Fields); diff --git a/packages/sp/src/fields/types.ts b/packages/sp/src/fields/types.ts new file mode 100644 index 000000000..ee1d3e030 --- /dev/null +++ b/packages/sp/src/fields/types.ts @@ -0,0 +1,674 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { extend, TypedHash } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { IGetable, body, headers } from "@pnp/odata"; +import { defaultPath, deleteable, IDeleteable } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Describes a collection of Field objects + * + */ +@defaultPath("fields") +export class _Fields extends _SharePointQueryableCollection implements IFields { + + /** + * Gets a field from the collection by id + * + * @param id The Id of the list + */ + public getById(id: string): IField { + return Field(this).concat(`('${id}')`); + } + + /** + * Gets a field from the collection by title + * + * @param title The case-sensitive title of the field + */ + public getByTitle(title: string): IField { + return Field(this, `getByTitle('${title}')`); + } + + /** + * Gets a field from the collection by using internal name or title + * + * @param name The case-sensitive internal name or title of the field + */ + public getByInternalNameOrTitle(name: string): IField { + return Field(this, `getByInternalNameOrTitle('${name}')`); + } + + /** + * Creates a field based on the specified schema + */ + public async createFieldAsXml(xml: string | XmlSchemaFieldCreationInformation): Promise { + + if (typeof xml === "string") { + xml = { SchemaXml: xml }; + } + + const postBody = body({ + "parameters": + extend(metadata("SP.XmlSchemaFieldCreationInformation"), xml), + }); + + const data = await spPost<{ Id: string; }>(this.clone(Fields, "createfieldasxml"), postBody); + + return { + data: data, + field: this.getById(data.Id), + }; + } + + /** + * Adds a new field to the collection + * + * @param title The new field's title + * @param fieldType The new field's type (ex: SP.FieldText) + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public async add(title: string, fieldType: string, properties: IFieldCreationProperties & { FieldTypeKind: number }): Promise { + + const postBody = body(Object.assign(metadata(fieldType), { + "Title": title, + }, properties)); + + const data = await spPost<{ Id: string; }>(this.clone(Fields, null), postBody); + + return { + data: data, + field: this.getById(data.Id), + }; + } + + /** + * Adds a new SP.FieldText to the collection + * + * @param title The field title + * @param maxLength The maximum number of characters allowed in the value of the field. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addText(title: string, maxLength = 255, properties?: IFieldCreationProperties): Promise { + + const props: { FieldTypeKind: number, MaxLength: number } = { + FieldTypeKind: 2, + MaxLength: maxLength, + }; + + return this.add(title, "SP.FieldText", extend(props, properties)); + } + + /** + * Adds a new SP.FieldCalculated to the collection + * + * @param title The field title. + * @param formula The formula for the field. + * @param dateFormat The date and time format that is displayed in the field. + * @param outputType Specifies the output format for the field. Represents a FieldType value. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addCalculated( + title: string, + formula: string, + dateFormat: DateTimeFieldFormatType, + outputType: FieldTypes = FieldTypes.Text, + properties?: IFieldCreationProperties): Promise { + + const props: { + DateFormat: DateTimeFieldFormatType; + FieldTypeKind: number; + Formula: string; + OutputType: FieldTypes; + } = { + DateFormat: dateFormat, + FieldTypeKind: 17, + Formula: formula, + OutputType: outputType, + }; + + return this.add(title, "SP.FieldCalculated", extend(props, properties)); + } + + /** + * Adds a new SP.FieldDateTime to the collection + * + * @param title The field title + * @param displayFormat The format of the date and time that is displayed in the field. + * @param calendarType Specifies the calendar type of the field. + * @param friendlyDisplayFormat The type of friendly display format that is used in the field. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addDateTime( + title: string, + displayFormat: DateTimeFieldFormatType = DateTimeFieldFormatType.DateOnly, + calendarType: CalendarType = CalendarType.Gregorian, + friendlyDisplayFormat: DateTimeFieldFriendlyFormatType = DateTimeFieldFriendlyFormatType.Unspecified, + properties?: IFieldCreationProperties): Promise { + + const props = { + DateTimeCalendarType: calendarType, + DisplayFormat: displayFormat, + FieldTypeKind: 4, + FriendlyDisplayFormat: friendlyDisplayFormat, + }; + + return this.add(title, "SP.FieldDateTime", extend(props, properties)); + } + + /** + * Adds a new SP.FieldNumber to the collection + * + * @param title The field title + * @param minValue The field's minimum value + * @param maxValue The field's maximum value + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addNumber( + title: string, + minValue?: number, + maxValue?: number, + properties?: IFieldCreationProperties): Promise { + + let props: { FieldTypeKind: number } = { FieldTypeKind: 9 }; + + if (minValue !== undefined) { + props = extend({ MinimumValue: minValue }, props); + } + + if (maxValue !== undefined) { + props = extend({ MaximumValue: maxValue }, props); + } + + return this.add(title, "SP.FieldNumber", extend(props, properties)); + } + + /** + * Adds a new SP.FieldCurrency to the collection + * + * @param title The field title + * @param minValue The field's minimum value + * @param maxValue The field's maximum value + * @param currencyLocalId Specifies the language code identifier (LCID) used to format the value of the field + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addCurrency( + title: string, + minValue?: number, + maxValue?: number, + currencyLocalId = 1033, + properties?: IFieldCreationProperties): Promise { + + let props: { CurrencyLocaleId: number; FieldTypeKind: number; } = { + CurrencyLocaleId: currencyLocalId, + FieldTypeKind: 10, + }; + + if (minValue !== undefined) { + props = extend({ MinimumValue: minValue }, props); + } + + if (maxValue !== undefined) { + props = extend({ MaximumValue: maxValue }, props); + } + + return this.add(title, "SP.FieldCurrency", extend(props, properties)); + } + + /** + * Adds a new SP.FieldMultiLineText to the collection + * + * @param title The field title + * @param numberOfLines Specifies the number of lines of text to display for the field. + * @param richText Specifies whether the field supports rich formatting. + * @param restrictedMode Specifies whether the field supports a subset of rich formatting. + * @param appendOnly Specifies whether all changes to the value of the field are displayed in list forms. + * @param allowHyperlink Specifies whether a hyperlink is allowed as a value of the field. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + * + */ + public addMultilineText( + title: string, + numberOfLines = 6, + richText = true, + restrictedMode = false, + appendOnly = false, + allowHyperlink = true, + properties?: IFieldCreationProperties): Promise { + + const props = { + AllowHyperlink: allowHyperlink, + AppendOnly: appendOnly, + FieldTypeKind: 3, + NumberOfLines: numberOfLines, + RestrictedMode: restrictedMode, + RichText: richText, + }; + + return this.add(title, "SP.FieldMultiLineText", extend(props, properties)); + } + + /** + * Adds a new SP.FieldUrl to the collection + * + * @param title The field title + */ + public addUrl( + title: string, + displayFormat: UrlFieldFormatType = UrlFieldFormatType.Hyperlink, + properties?: IFieldCreationProperties): Promise { + + const props = { + DisplayFormat: displayFormat, + FieldTypeKind: 11, + }; + + return this.add(title, "SP.FieldUrl", extend(props, properties)); + } + + /** Adds a user field to the colleciton + * + * @param title The new field's title + * @param selectionMode The selection mode of the field + * @param selectionGroup Value that specifies the identifier of the SharePoint group whose members can be selected as values of the field + * @param properties + */ + public addUser(title: string, + selectionMode: FieldUserSelectionMode, + properties?: IFieldCreationProperties): Promise { + + const props = { + FieldTypeKind: 20, + SelectionMode: selectionMode, + }; + + return this.add(title, "SP.FieldUser", extend(props, properties)); + } + + /** + * Adds a SP.FieldLookup to the collection + * + * @param title The new field's title + * @param lookupListId The guid id of the list where the source of the lookup is found + * @param lookupFieldName The internal name of the field in the source list + * @param properties Set of additional properties to set on the new field + */ + public async addLookup( + title: string, + lookupListId: string, + lookupFieldName: string, + properties?: IFieldCreationProperties): Promise { + + const props = extend({ + FieldTypeKind: 7, + LookupFieldName: lookupFieldName, + LookupListId: lookupListId, + Title: title, + }, properties); + + const postBody = body({ + "parameters": + extend(metadata("SP.FieldCreationInformation"), props), + }); + + const data = await spPost<{ Id: string; }>(this.clone(Fields, "addfield"), postBody); + + return { + data: data, + field: this.getById(data.Id), + }; + } + + /** + * Adds a new SP.FieldChoice to the collection + * + * @param title The field title. + * @param choices The choices for the field. + * @param format The display format of the available options for the field. + * @param fillIn Specifies whether the field allows fill-in values. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addChoice( + title: string, + choices: string[], + format: ChoiceFieldFormatType = ChoiceFieldFormatType.Dropdown, + fillIn?: boolean, + properties?: IFieldCreationProperties): Promise { + + const props = { + Choices: { + results: choices, + }, + EditFormat: format, + FieldTypeKind: 6, + FillInChoice: fillIn, + }; + + return this.add(title, "SP.FieldChoice", extend(props, properties)); + } + + /** + * Adds a new SP.FieldMultiChoice to the collection + * + * @param title The field title. + * @param choices The choices for the field. + * @param fillIn Specifies whether the field allows fill-in values. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addMultiChoice( + title: string, + choices: string[], + fillIn?: boolean, + properties?: IFieldCreationProperties): Promise { + + const props = { + Choices: { + results: choices, + }, + FieldTypeKind: 15, + FillInChoice: fillIn, + }; + + return this.add(title, "SP.FieldMultiChoice", extend(props, properties)); + } + + /** + * Adds a new SP.FieldBoolean to the collection + * + * @param title The field title. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addBoolean( + title: string, + properties?: IFieldCreationProperties): Promise { + + const props = { + FieldTypeKind: 8, + }; + + return this.add(title, "SP.Field", extend(props, properties)); + } + + /** + * Creates a secondary (dependent) lookup field, based on the Id of the primary lookup field. + * + * @param displayName The display name of the new field. + * @param primaryLookupFieldId The guid of the primary Lookup Field. + * @param showField Which field to show from the lookup list. + */ + public async addDependentLookupField( + displayName: string, + primaryLookupFieldId: string, + showField: string, + ): Promise { + + const path = `adddependentlookupfield(displayName='${displayName}', primarylookupfieldid='${primaryLookupFieldId}', showfield='${showField}')`; + + const data = await spPost(this.clone(Fields, path)); + + return { + data, + field: this.getById(data.Id), + }; + } + + /** + * Adds a new SP.FieldLocation to the collection + * + * @param title The field title. + * @param properties Differ by type of field being created (see: https://msdn.microsoft.com/en-us/library/office/dn600182.aspx) + */ + public addLocation( + title: string, + properties?: IFieldCreationProperties): Promise { + + const props = { FieldTypeKind: 33 }; + + return this.add(title, "SP.FieldLocation", extend(props, properties)); + } +} + +export interface IFields extends IGetable, ISharePointQueryableCollection { + getById(id: string): IField; + getByTitle(title: string): IField; + getByInternalNameOrTitle(name: string): IField; + createFieldAsXml(xml: string | XmlSchemaFieldCreationInformation): Promise; + add(title: string, fieldType: string, properties: IFieldCreationProperties & { FieldTypeKind: number }): Promise; + addText(title: string, maxLength?: number, properties?: IFieldCreationProperties): Promise; + addCalculated(title: string, formula: string, dateFormat: DateTimeFieldFormatType, outputType?: FieldTypes, properties?: IFieldCreationProperties): Promise; + addDateTime( + title: string, + displayFormat?: DateTimeFieldFormatType, + calendarType?: CalendarType, + friendlyDisplayFormat?: DateTimeFieldFriendlyFormatType, + properties?: IFieldCreationProperties): Promise; + addNumber(title: string, minValue?: number, maxValue?: number, properties?: IFieldCreationProperties): Promise; + addCurrency(title: string, minValue?: number, maxValue?: number, currencyLocalId?: number, properties?: IFieldCreationProperties): Promise; + addMultilineText( + title: string, + numberOfLines?: number, + richText?: boolean, + restrictedMode?: boolean, + appendOnly?: boolean, + allowHyperlink?: boolean, + properties?: IFieldCreationProperties): Promise; + addUrl(title: string, displayFormat?: UrlFieldFormatType, properties?: IFieldCreationProperties): Promise; + addUser(title: string, selectionMode: FieldUserSelectionMode, properties?: IFieldCreationProperties): Promise; + addLookup(title: string, lookupListId: string, lookupFieldName: string, properties?: IFieldCreationProperties): Promise; + addChoice(title: string, choices: string[], format?: ChoiceFieldFormatType, fillIn?: boolean, properties?: IFieldCreationProperties): Promise; + addMultiChoice(title: string, choices: string[], fillIn?: boolean, properties?: IFieldCreationProperties): Promise; + addBoolean(title: string, properties?: IFieldCreationProperties): Promise; + addDependentLookupField(displayName: string, primaryLookupFieldId: string, showField: string): Promise; + addLocation(title: string, properties?: IFieldCreationProperties): Promise; +} +export interface _Fields extends IGetable { } +export const Fields = spInvokableFactory(_Fields); + +/** + * Describes a single of Field instance + * + */ +@deleteable() +export class _Field extends _SharePointQueryableInstance implements IField { + + /** + * Updates this field intance with the supplied properties + * + * @param properties A plain object hash of values to update for the list + * @param fieldType The type value, required to update child field type properties + */ + public async update(properties: TypedHash, fieldType = "SP.Field"): Promise { + + const req = body(extend(metadata(fieldType), properties), headers({ "X-HTTP-Method": "MERGE" })); + + const data = await spPost(this, req); + + return { + data, + field: this, + }; + } + + /** + * Sets the value of the ShowInDisplayForm property for this field. + */ + public setShowInDisplayForm(show: boolean): Promise { + return spPost(this.clone(Field, `setshowindisplayform(${show})`)); + } + + /** + * Sets the value of the ShowInEditForm property for this field. + */ + public setShowInEditForm(show: boolean): Promise { + return spPost(this.clone(Field, `setshowineditform(${show})`)); + } + + /** + * Sets the value of the ShowInNewForm property for this field. + */ + public setShowInNewForm(show: boolean): Promise { + return spPost(this.clone(Field, `setshowinnewform(${show})`)); + } +} + +export interface IField extends IGetable, ISharePointQueryableInstance, IDeleteable { + update(properties: TypedHash, fieldType?: string): Promise; + setShowInDisplayForm(show: boolean): Promise; + setShowInEditForm(show: boolean): Promise; + setShowInNewForm(show: boolean): Promise; +} +export interface _Field extends IGetable, IDeleteable { } +export const Field = spInvokableFactory(_Field); + +/** + * This interface defines the result of adding a field + */ +export interface FieldAddResult { + data: any; + field: IField; +} + +export interface FieldUpdateResult { + data: any; + field: IField; +} + +/** + * Specifies the type of the field. + */ +export enum FieldTypes { + Invalid = 0, + Integer = 1, + Text = 2, + Note = 3, + DateTime = 4, + Counter = 5, + Choice = 6, + Lookup = 7, + Boolean = 8, + Number = 9, + Currency = 10, + URL = 11, + Computed = 12, + Threading = 13, + Guid = 14, + MultiChoice = 15, + GridChoice = 16, + Calculated = 17, + File = 18, + Attachments = 19, + User = 20, + Recurrence = 21, + CrossProjectLink = 22, + ModStat = 23, + Error = 24, + ContentTypeId = 25, + PageSeparator = 26, + ThreadIndex = 27, + WorkflowStatus = 28, + AllDayEvent = 29, + WorkflowEventType = 30, +} + +export enum DateTimeFieldFormatType { + DateOnly = 0, + DateTime = 1, +} + +export enum DateTimeFieldFriendlyFormatType { + Unspecified = 0, + Disabled = 1, + Relative = 2, +} + +/** + * Specifies the control settings while adding a field. + */ +export enum AddFieldOptions { + /** + * Specify that a new field added to the list must also be added to the default content type in the site collection + */ + DefaultValue = 0, + /** + * Specify that a new field added to the list must also be added to the default content type in the site collection. + */ + AddToDefaultContentType = 1, + /** + * Specify that a new field must not be added to any other content type + */ + AddToNoContentType = 2, + /** + * Specify that a new field that is added to the specified list must also be added to all content types in the site collection + */ + AddToAllContentTypes = 4, + /** + * Specify adding an internal field name hint for the purpose of avoiding possible database locking or field renaming operations + */ + AddFieldInternalNameHint = 8, + /** + * Specify that a new field that is added to the specified list must also be added to the default list view + */ + AddFieldToDefaultView = 16, + /** + * Specify to confirm that no other field has the same display name + */ + AddFieldCheckDisplayName = 32, +} + +export interface XmlSchemaFieldCreationInformation { + Options?: AddFieldOptions; + SchemaXml: string; +} + +export enum CalendarType { + Gregorian = 1, + Japan = 3, + Taiwan = 4, + Korea = 5, + Hijri = 6, + Thai = 7, + Hebrew = 8, + GregorianMEFrench = 9, + GregorianArabic = 10, + GregorianXLITEnglish = 11, + GregorianXLITFrench = 12, + KoreaJapanLunar = 14, + ChineseLunar = 15, + SakaEra = 16, + UmAlQura = 23, +} + +export enum UrlFieldFormatType { + Hyperlink = 0, + Image = 1, +} + +export enum FieldUserSelectionMode { + PeopleAndGroups = 1, + PeopleOnly = 0, +} + +export interface IFieldCreationProperties extends TypedHash { + DefaultFormula?: string; + Description?: string; + EnforceUniqueValues?: boolean; + FieldTypeKind?: number; + Group?: string; + Hidden?: boolean; + Indexed?: boolean; + Required?: boolean; + Title?: string; + ValidationFormula?: string; + ValidationMessage?: string; +} + +export enum ChoiceFieldFormatType { + Dropdown, + RadioButtons, +} diff --git a/packages/sp/src/fields/web.ts b/packages/sp/src/fields/web.ts new file mode 100644 index 000000000..df539e131 --- /dev/null +++ b/packages/sp/src/fields/web.ts @@ -0,0 +1,25 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { Fields, IFields } from "./types"; + +declare module "../webs/types" { + interface _Web { + readonly fields: IFields; + readonly availablefields: IFields; + } + interface IWeb { + + /** + * This web's colleciton of fields + */ + readonly fields: IFields; + + /** + * This web's colleciton of available fields + */ + readonly availablefields: IFields; + } +} + +addProp(_Web, "fields", Fields); +addProp(_Web, "availablefields", Fields, "availablefields"); diff --git a/packages/sp/src/files/folder.ts b/packages/sp/src/files/folder.ts new file mode 100644 index 000000000..4aa920f9e --- /dev/null +++ b/packages/sp/src/files/folder.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Folder } from "../folders/types"; +import { IFiles, Files } from "./types"; + +/** +* Extend List +*/ +declare module "../folders/types" { + interface _Folder { + readonly files: IFiles; + } + interface IFolder { + readonly files: IFiles; + } +} + +addProp(_Folder, "files", Files); diff --git a/packages/sp/src/files/index.ts b/packages/sp/src/files/index.ts new file mode 100644 index 000000000..78424e58e --- /dev/null +++ b/packages/sp/src/files/index.ts @@ -0,0 +1,19 @@ +import "./folder"; +import "./item"; +import "./web"; + +export { + File, + IFile, + Files, + IFiles, + IFileAddResult as FileAddResult, + IFileUploadProgressData, + CheckinType, + MoveOperations, + TemplateFileType, + IVersion, + IVersions, + Version, + Versions, +} from "./types"; diff --git a/packages/sp/src/files/item.ts b/packages/sp/src/files/item.ts new file mode 100644 index 000000000..7130575a8 --- /dev/null +++ b/packages/sp/src/files/item.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Item } from "../items/types"; +import { File, IFile } from "./types"; + +/** +* Extend Item +*/ +declare module "../items/types" { + interface _Item { + readonly file: IFile; + } + interface IItem { + readonly file: IFile; + } +} + +addProp(_Item, "file", File, "file"); diff --git a/packages/sp/src/files/types.ts b/packages/sp/src/files/types.ts new file mode 100644 index 000000000..3a0ad7186 --- /dev/null +++ b/packages/sp/src/files/types.ts @@ -0,0 +1,546 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { TextParser, BlobParser, JSONParser, BufferParser, IGetable, headers } from "@pnp/odata"; +import { extend, getGUID } from "@pnp/common"; +import { Item, IItem } from "../items"; +import { odataUrlFrom } from "../odata"; +import { defaultPath, IDeleteableWithETag, deleteableWithETag } from "../decorators"; +import { spPost } from "../operations"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +export interface IFileUploadProgressData { + uploadId: string; + stage: "starting" | "continue" | "finishing"; + blockNumber: number; + totalBlocks: number; + chunkSize: number; + currentPointer: number; + fileSize: number; +} + +/** + * Describes a collection of File objects + * + */ +@defaultPath("files") +export class _Files extends _SharePointQueryableCollection implements IFiles { + + /** + * Gets a File by filename + * + * @param name The name of the file, including extension. + */ + public getByName(name: string): IFile { + return File(this).concat(`('${name}')`); + } + + /** + * Uploads a file. Not supported for batching + * + * @param url The folder-relative url of the file. + * @param content The file contents blob. + * @param shouldOverWrite Should a file with the same name in the same location be overwritten? (default: true) + * @returns The new File and the raw response. + */ + public async add(url: string, content: string | ArrayBuffer | Blob, shouldOverWrite = true): Promise { + const response = spPost(Files(this, `add(overwrite=${shouldOverWrite},url='${escapeQueryStrValue(url)}')`), { + body: content, + }); + return { + data: response, + file: this.getByName(url), + }; + } + + /** + * Uploads a file. Not supported for batching + * + * @param url The folder-relative url of the file. + * @param content The Blob file content to add + * @param progress A callback function which can be used to track the progress of the upload + * @param shouldOverWrite Should a file with the same name in the same location be overwritten? (default: true) + * @param chunkSize The size of each file slice, in bytes (default: 10485760) + * @returns The new File and the raw response. + */ + public async addChunked(url: string, content: Blob, progress?: (data: IFileUploadProgressData) => void, shouldOverWrite = true, chunkSize = 10485760): Promise { + + await spPost(this.clone(Files, `add(overwrite=${shouldOverWrite},url='${escapeQueryStrValue(url)}')`, false)); + const file = this.getByName(url); + return await file.setContentChunked(content, progress, chunkSize); + } + + /** + * Adds a ghosted file to an existing list or document library. Not supported for batching. + * + * @param fileUrl The server-relative url where you want to save the file. + * @param templateFileType The type of use to create the file. + * @returns The template file that was added and the raw response. + */ + public async addTemplateFile(fileUrl: string, templateFileType: TemplateFileType): Promise { + const response = await spPost(this.clone(Files, `addTemplateFile(urloffile='${escapeQueryStrValue(fileUrl)}',templatefiletype=${templateFileType})`, false)); + return { + data: response, + file: this.getByName(fileUrl), + }; + } +} + +export interface IFiles extends IGetable, ISharePointQueryableCollection { + getByName(name: string): IFile; + add(url: string, content: string | ArrayBuffer | Blob, shouldOverWrite?: boolean): Promise; + addChunked(url: string, content: Blob, progress?: (data: IFileUploadProgressData) => void, shouldOverWrite?: boolean, chunkSize?: number): Promise; + addTemplateFile(fileUrl: string, templateFileType: TemplateFileType): Promise; +} +export interface _Files extends IGetable { } +export const Files = spInvokableFactory(_Files); + +/** + * Describes a single File instance + * + */ +@deleteableWithETag() +export class _File extends _SharePointQueryableInstance implements IFile { + + /** + * Gets a value that specifies the list item field values for the list item corresponding to the file. + * + */ + public get listItemAllFields(): _SharePointQueryableInstance { + return new _SharePointQueryableInstance(this, "listItemAllFields"); + } + + /** + * Gets a collection of versions + * + */ + public get versions(): IVersions { + return Versions(this); + } + + /** + * Approves the file submitted for content approval with the specified comment. + * Only documents in lists that are enabled for content approval can be approved. + * + * @param comment The comment for the approval. + */ + public approve(comment = ""): Promise { + return spPost(this.clone(File, `approve(comment='${escapeQueryStrValue(comment)}')`)); + } + + /** + * Stops the chunk upload session without saving the uploaded data. Does not support batching. + * If the file doesn’t already exist in the library, the partially uploaded file will be deleted. + * Use this in response to user action (as in a request to cancel an upload) or an error or exception. + * Use the uploadId value that was passed to the StartUpload method that started the upload session. + * This method is currently available only on Office 365. + * + * @param uploadId The unique identifier of the upload session. + */ + public cancelUpload(uploadId: string): Promise { + return spPost(this.clone(File, `cancelUpload(uploadId=guid'${uploadId}')`, false)); + } + + /** + * Checks the file in to a document library based on the check-in type. + * + * @param comment A comment for the check-in. Its length must be <= 1023. + * @param checkinType The check-in type for the file. + */ + public checkin(comment = "", checkinType = CheckinType.Major): Promise { + + if (comment.length > 1023) { + throw Error("The maximum comment length is 1023 characters."); + } + + return spPost(this.clone(File, `checkin(comment='${escapeQueryStrValue(comment)}',checkintype=${checkinType})`)); + } + + /** + * Checks out the file from a document library. + */ + public checkout(): Promise { + return spPost(this.clone(File, "checkout")); + } + + /** + * Copies the file to the destination url. + * + * @param url The absolute url or server relative url of the destination file path to copy to. + * @param shouldOverWrite Should a file with the same name in the same location be overwritten? + */ + public copyTo(url: string, shouldOverWrite = true): Promise { + return spPost(this.clone(File, `copyTo(strnewurl='${escapeQueryStrValue(url)}',boverwrite=${shouldOverWrite})`)); + } + + /** + * Denies approval for a file that was submitted for content approval. + * Only documents in lists that are enabled for content approval can be denied. + * + * @param comment The comment for the denial. + */ + public deny(comment = ""): Promise { + if (comment.length > 1023) { + throw Error("The maximum comment length is 1023 characters."); + } + return spPost(this.clone(File, `deny(comment='${escapeQueryStrValue(comment)}')`)); + } + + /** + * Moves the file to the specified destination url. + * + * @param url The absolute url or server relative url of the destination file path to move to. + * @param moveOperations The bitwise MoveOperations value for how to move the file. + */ + public moveTo(url: string, moveOperations = MoveOperations.Overwrite): Promise { + return spPost(this.clone(File, `moveTo(newurl='${escapeQueryStrValue(url)}',flags=${moveOperations})`)); + } + + /** + * Submits the file for content approval with the specified comment. + * + * @param comment The comment for the published file. Its length must be <= 1023. + */ + public publish(comment = ""): Promise { + if (comment.length > 1023) { + throw Error("The maximum comment length is 1023 characters."); + } + return spPost(this.clone(File, `publish(comment='${escapeQueryStrValue(comment)}')`)); + } + + /** + * Moves the file to the Recycle Bin and returns the identifier of the new Recycle Bin item. + * + * @returns The GUID of the recycled file. + */ + public recycle(): Promise { + return spPost(this.clone(File, "recycle")); + } + + /** + * Reverts an existing checkout for the file. + * + */ + public undoCheckout(): Promise { + return spPost(this.clone(File, "undoCheckout")); + } + + /** + * Removes the file from content approval or unpublish a major version. + * + * @param comment The comment for the unpublish operation. Its length must be <= 1023. + */ + public unpublish(comment = ""): Promise { + if (comment.length > 1023) { + throw Error("The maximum comment length is 1023 characters."); + } + return spPost(this.clone(File, `unpublish(comment='${escapeQueryStrValue(comment)}')`)); + } + + /** + * Gets the contents of the file as text. Not supported in batching. + * + */ + public getText(): Promise { + + return this.clone(File, "$value", false).usingParser(new TextParser())(headers({ "binaryStringResponseBody": "true" })); + } + + /** + * Gets the contents of the file as a blob, does not work in Node.js. Not supported in batching. + * + */ + public getBlob(): Promise { + + return this.clone(File, "$value", false).usingParser(new BlobParser())(headers({ "binaryStringResponseBody": "true" })); + } + + /** + * Gets the contents of a file as an ArrayBuffer, works in Node.js. Not supported in batching. + */ + public getBuffer(): Promise { + + return this.clone(File, "$value", false).usingParser(new BufferParser())(headers({ "binaryStringResponseBody": "true" })); + } + + /** + * Gets the contents of a file as an ArrayBuffer, works in Node.js. Not supported in batching. + */ + public getJSON(): Promise { + + return this.clone(File, "$value", false).usingParser(new JSONParser())(headers({ "binaryStringResponseBody": "true" })); + } + + /** + * Sets the content of a file, for large files use setContentChunked. Not supported in batching. + * + * @param content The file content + * + */ + public async setContent(content: string | ArrayBuffer | Blob): Promise { + + await spPost(this.clone(File, "$value", false), { + body: content, + headers: { + "X-HTTP-Method": "PUT", + }, + }); + return File(this); + } + + /** + * Gets the associated list item for this folder, loading the default properties + */ + public getItem(...selects: string[]): Promise { + + const q = this.listItemAllFields; + return q.select.apply(q, selects)().then((d: any) => { + + return extend(Item(odataUrlFrom(d)), d); + }); + } + + /** + * Sets the contents of a file using a chunked upload approach. Not supported in batching. + * + * @param file The file to upload + * @param progress A callback function which can be used to track the progress of the upload + * @param chunkSize The size of each file slice, in bytes (default: 10485760) + */ + public setContentChunked(file: Blob, progress?: (data: IFileUploadProgressData) => void, chunkSize = 10485760): Promise { + + if (progress === undefined) { + progress = () => null; + } + + const fileSize = file.size; + const blockCount = parseInt((file.size / chunkSize).toString(), 10) + ((file.size % chunkSize === 0) ? 1 : 0); + const uploadId = getGUID(); + + // start the chain with the first fragment + progress({ uploadId, blockNumber: 1, chunkSize, currentPointer: 0, fileSize, stage: "starting", totalBlocks: blockCount }); + + let chain = this.startUpload(uploadId, file.slice(0, chunkSize)); + + // skip the first and last blocks + for (let i = 2; i < blockCount; i++) { + chain = chain.then(pointer => { + progress({ uploadId, blockNumber: i, chunkSize, currentPointer: pointer, fileSize, stage: "continue", totalBlocks: blockCount }); + return this.continueUpload(uploadId, pointer, file.slice(pointer, pointer + chunkSize)); + }); + } + + return chain.then(pointer => { + progress({ uploadId, blockNumber: blockCount, chunkSize, currentPointer: pointer, fileSize, stage: "finishing", totalBlocks: blockCount }); + return this.finishUpload(uploadId, pointer, file.slice(pointer)); + }); + } + + /** + * Starts a new chunk upload session and uploads the first fragment. + * The current file content is not changed when this method completes. + * The method is idempotent (and therefore does not change the result) as long as you use the same values for uploadId and stream. + * The upload session ends either when you use the CancelUpload method or when you successfully + * complete the upload session by passing the rest of the file contents through the ContinueUpload and FinishUpload methods. + * The StartUpload and ContinueUpload methods return the size of the running total of uploaded data in bytes, + * so you can pass those return values to subsequent uses of ContinueUpload and FinishUpload. + * This method is currently available only on Office 365. + * + * @param uploadId The unique identifier of the upload session. + * @param fragment The file contents. + * @returns The size of the total uploaded data in bytes. + */ + protected async startUpload(uploadId: string, fragment: ArrayBuffer | Blob): Promise { + let n = await spPost(this.clone(File, `startUpload(uploadId=guid'${uploadId}')`, false), { body: fragment }); + if (typeof n === "object") { + // When OData=verbose the payload has the following shape: + // { StartUpload: "10485760" } + n = (n as any).StartUpload; + } + return parseFloat(n); + } + + /** + * Continues the chunk upload session with an additional fragment. + * The current file content is not changed. + * Use the uploadId value that was passed to the StartUpload method that started the upload session. + * This method is currently available only on Office 365. + * + * @param uploadId The unique identifier of the upload session. + * @param fileOffset The size of the offset into the file where the fragment starts. + * @param fragment The file contents. + * @returns The size of the total uploaded data in bytes. + */ + protected async continueUpload(uploadId: string, fileOffset: number, fragment: ArrayBuffer | Blob): Promise { + let n = await spPost(this.clone(File, `continueUpload(uploadId=guid'${uploadId}',fileOffset=${fileOffset})`, false), { body: fragment }); + if (typeof n === "object") { + // When OData=verbose the payload has the following shape: + // { ContinueUpload: "20971520" } + n = (n as any).ContinueUpload; + } + return parseFloat(n); + } + + /** + * Uploads the last file fragment and commits the file. The current file content is changed when this method completes. + * Use the uploadId value that was passed to the StartUpload method that started the upload session. + * This method is currently available only on Office 365. + * + * @param uploadId The unique identifier of the upload session. + * @param fileOffset The size of the offset into the file where the fragment starts. + * @param fragment The file contents. + * @returns The newly uploaded file. + */ + protected async finishUpload(uploadId: string, fileOffset: number, fragment: ArrayBuffer | Blob): Promise { + const response = await spPost(this.clone(File, `finishUpload(uploadId=guid'${uploadId}',fileOffset=${fileOffset})`, false), { body: fragment }); + return { + data: response, + file: File(odataUrlFrom(response)), + }; + } +} + +export interface IFile extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag { + readonly listItemAllFields: ISharePointQueryableInstance; + readonly versions: IVersions; + approve(comment?: string): Promise; + cancelUpload(uploadId: string): Promise; + checkin(comment?: string, checkinType?: CheckinType): Promise; + checkout(): Promise; + copyTo(url: string, shouldOverWrite?: boolean): Promise; + deny(comment?: string): Promise; + moveTo(url: string, moveOperations?: MoveOperations): Promise; + publish(comment?: string): Promise; + recycle(): Promise; + undoCheckout(): Promise; + unpublish(comment?: string): Promise; + getText(): Promise; + getBlob(): Promise; + getBuffer(): Promise; + getJSON(): Promise; + setContent(content: string | ArrayBuffer | Blob): Promise; + getItem(...selects: string[]): Promise; + setContentChunked(file: Blob, progress?: (data: IFileUploadProgressData) => void, chunkSize?: number): Promise; +} +export interface _File extends IGetable, IDeleteableWithETag { } +export const File = spInvokableFactory(_File); + +/** + * Describes a collection of Version objects + * + */ +@defaultPath("versions") +export class _Versions extends _SharePointQueryableCollection implements IVersions { + + /** + * Gets a version by id + * + * @param versionId The id of the version to retrieve + */ + public getById(versionId: number): IVersion { + return Version(this).concat(`(${versionId})`); + } + + /** + * Deletes all the file version objects in the collection. + * + */ + public deleteAll(): Promise { + return spPost(Versions(this, "deleteAll")); + } + + /** + * Deletes the specified version of the file. + * + * @param versionId The ID of the file version to delete. + */ + public deleteById(versionId: number): Promise { + return spPost(this.clone(Versions, `deleteById(vid=${versionId})`)); + } + + /** + * Recycles the specified version of the file. + * + * @param versionId The ID of the file version to delete. + */ + public recycleByID(versionId: number): Promise { + return spPost(this.clone(Versions, `recycleByID(vid=${versionId})`)); + } + + /** + * Deletes the file version object with the specified version label. + * + * @param label The version label of the file version to delete, for example: 1.2 + */ + public deleteByLabel(label: string): Promise { + return spPost(this.clone(Versions, `deleteByLabel(versionlabel='${escapeQueryStrValue(label)}')`)); + } + + /** + * Recycles the file version object with the specified version label. + * + * @param label The version label of the file version to delete, for example: 1.2 + */ + public recycleByLabel(label: string): Promise { + return spPost(this.clone(Versions, `recycleByLabel(versionlabel='${escapeQueryStrValue(label)}')`)); + } + + /** + * Creates a new file version from the file specified by the version label. + * + * @param label The version label of the file version to restore, for example: 1.2 + */ + public restoreByLabel(label: string): Promise { + return spPost(this.clone(Versions, `restoreByLabel(versionlabel='${escapeQueryStrValue(label)}')`)); + } +} + +export interface IVersions extends IGetable, ISharePointQueryableCollection { + getById(versionId: number): IVersion; + deleteAll(): Promise; + deleteById(versionId: number): Promise; + recycleByID(versionId: number): Promise; + deleteByLabel(label: string): Promise; + recycleByLabel(label: string): Promise; + restoreByLabel(label: string): Promise; +} +export interface _Versions extends IGetable { } +export const Versions = spInvokableFactory(_Versions); + +/** + * Describes a single Version instance + * + */ +@deleteableWithETag() +export class _Version extends _SharePointQueryableInstance { } + +export interface IVersion extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag { } +export interface _Version extends IGetable, IDeleteableWithETag { } +export const Version = spInvokableFactory(_Version); + +export enum CheckinType { + Minor = 0, + Major = 1, + Overwrite = 2, +} + +export interface IFileAddResult { + file: IFile; + data: any; +} + +export enum MoveOperations { + Overwrite = 1, + AllowBrokenThickets = 8, +} + +export enum TemplateFileType { + StandardPage = 0, + WikiPage = 1, + FormPage = 2, + ClientSidePage = 3, +} diff --git a/packages/sp/src/files/web.ts b/packages/sp/src/files/web.ts new file mode 100644 index 000000000..4c11d57f1 --- /dev/null +++ b/packages/sp/src/files/web.ts @@ -0,0 +1,34 @@ +import { _Web } from "../webs/types"; +import { File, IFile } from "./types"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +declare module "../webs/types" { + interface _Web { + getFileByServerRelativeUrl(fileRelativeUrl: string): IFile; + getFileByServerRelativePath(fileRelativeUrl: string): IFile; + } + interface IWeb { + + /** + * Gets a file by server relative url + * + * @param fileRelativeUrl The server relative path to the file (including /sites/ if applicable) + */ + getFileByServerRelativeUrl(fileRelativeUrl: string): IFile; + + /** + * Gets a file by server relative url if your file name contains # and % characters + * + * @param fileRelativeUrl The server relative path to the file (including /sites/ if applicable) + */ + getFileByServerRelativePath(fileRelativeUrl: string): IFile; + } +} + +_Web.prototype.getFileByServerRelativeUrl = function (this: _Web, fileRelativeUrl: string): IFile { + return File(this, `getFileByServerRelativeUrl('${escapeQueryStrValue(fileRelativeUrl)}')`); +}; + +_Web.prototype.getFileByServerRelativePath = function (this: _Web, fileRelativeUrl: string): IFile { + return File(this, `getFileByServerRelativePath(decodedUrl='${escapeQueryStrValue(fileRelativeUrl)}')`); +}; diff --git a/packages/sp/src/folders/index.ts b/packages/sp/src/folders/index.ts new file mode 100644 index 000000000..02f3d1b17 --- /dev/null +++ b/packages/sp/src/folders/index.ts @@ -0,0 +1,12 @@ +import "./item"; +import "./list"; +import "./web"; + +export { + Folder, + IFolderAddResult, + IFolderUpdateResult, + Folders, + IFolder, + IFolders, +} from "./types"; diff --git a/packages/sp/src/folders/item.ts b/packages/sp/src/folders/item.ts new file mode 100644 index 000000000..dbbba7185 --- /dev/null +++ b/packages/sp/src/folders/item.ts @@ -0,0 +1,19 @@ +import { addProp } from "@pnp/odata"; +import { _Item } from "../items/types"; +import { Folder, IFolder } from "./types"; + +/** +* Extend Web +*/ +declare module "../items/types" { + interface _Item { + readonly folder: IFolder; + + } + interface IItem { + readonly folder: IFolder; + + } +} + +addProp(_Item, "folder", Folder, "folder"); diff --git a/packages/sp/src/folders/list.ts b/packages/sp/src/folders/list.ts new file mode 100644 index 000000000..a19d338fa --- /dev/null +++ b/packages/sp/src/folders/list.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { Folder, IFolder } from "./types"; + +/** +* Extend Web +*/ +declare module "../lists/types" { + interface _List { + readonly rootFolder: IFolder; + } + interface IList { + readonly rootFolder: IFolder; + } +} + +addProp(_List, "rootFolder", Folder, "rootFolder"); diff --git a/packages/sp/src/folders/types.ts b/packages/sp/src/folders/types.ts new file mode 100644 index 000000000..8b1b625c3 --- /dev/null +++ b/packages/sp/src/folders/types.ts @@ -0,0 +1,236 @@ +import { extend, TypedHash } from "@pnp/common"; +import { + SharePointQueryable, + SharePointQueryableCollection, + SharePointQueryableInstance, + _SharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryableCollection, + ISharePointQueryableInstance, + ISharePointQueryable, + spInvokableFactory, +} from "../sharepointqueryable"; +import { odataUrlFrom } from "../odata"; +import { IItem, Item } from "../items/types"; +import { IGetable, body } from "@pnp/odata"; +import { defaultPath, deleteableWithETag, IDeleteableWithETag } from "../decorators"; +import { spPost } from "../operations"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +/** + * Describes a collection of Folder objects + * + */ +@defaultPath("folders") +export class _Folders extends _SharePointQueryableCollection implements IFolders { + + /** + * Gets a folder by folder name + * + */ + public getByName(name: string): IFolder { + return Folder(this).concat(`('${escapeQueryStrValue(name)}')`); + } + + /** + * Adds a new folder to the current folder (relative) or any folder (absolute) + * + * @param url The relative or absolute url where the new folder will be created. Urls starting with a forward slash are absolute. + * @returns The new Folder and the raw response. + */ + public async add(url: string): Promise { + + const data = await spPost(this.clone(Folders, `add('${escapeQueryStrValue(url)}')`)); + + return { + data, + folder: this.getByName(url), + }; + } +} + +export interface IFolders extends IGetable, ISharePointQueryableCollection { + getByName(name: string): IFolder; + add(url: string): Promise; +} +export interface _Folders extends IGetable { } +export const Folders = spInvokableFactory(_Folders); + +/** + * Describes a single Folder instance + * + */ +@deleteableWithETag() +export class _Folder extends _SharePointQueryableInstance implements IFolder { + + /** + * Specifies the sequence in which content types are displayed. + * + */ + public get contentTypeOrder(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "contentTypeOrder"); + } + + /** + * Gets this folder's sub folders + * + */ + public get folders(): IFolders { + return Folders(this); + } + + /** + * Gets this folder's list item field values + * + */ + public get listItemAllFields(): ISharePointQueryableInstance { + return SharePointQueryableInstance(this, "listItemAllFields"); + } + + /** + * Gets the parent folder, if available + * + */ + public get parentFolder(): IFolder { + return Folder(this, "parentFolder"); + } + + /** + * Gets this folder's properties + * + */ + public get properties(): _SharePointQueryableInstance { + return new _SharePointQueryableInstance(this, "properties"); + } + + /** + * Gets this folder's server relative url + * + */ + public get serverRelativeUrl(): ISharePointQueryable { + return SharePointQueryable(this, "serverRelativeUrl"); + } + + /** + * Gets a value that specifies the content type order. + * + */ + public get uniqueContentTypeOrder(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "uniqueContentTypeOrder"); + } + + // TODO:: this typing is broken + public update: any = this._update>("SP.Folder", data => ({ data, folder: this })); + + /** + * Moves the folder to the Recycle Bin and returns the identifier of the new Recycle Bin item. + */ + public recycle(): Promise { + return spPost(this.clone(Folder, "recycle")); + } + + /** + * Gets the associated list item for this folder, loading the default properties + */ + public getItem(...selects: string[]): Promise { + + const q = this.listItemAllFields; + return q.select.apply(q, selects).get().then((d: any) => { + + return extend(Item(odataUrlFrom(d)), d); + }); + } + + /** + * Moves a folder to destination path + * + * @param destUrl Absolute or relative URL of the destination path + */ + public async moveTo(destUrl: string): Promise { + + const srcUrl = await this.select("ServerRelativeUrl")(); + + const webBaseUrl = this.toUrl().split("/_api")[0]; + const hostUrl = webBaseUrl.replace("://", "___").split("/")[0].replace("___", "://"); + return spPost(SharePointQueryable(`${webBaseUrl}/_api/SP.MoveCopyUtil.MoveFolder()`), body({ + destUrl: destUrl.indexOf("http") === 0 ? destUrl : `${hostUrl}${destUrl}`, + srcUrl: `${hostUrl}${srcUrl}`, + })); + } +} + +export interface IFolder extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag { + /** + * Specifies the sequence in which content types are displayed. + * + */ + readonly contentTypeOrder: ISharePointQueryableCollection; + + /** + * Gets this folder's sub folders + * + */ + readonly folders: IFolders; + + /** + * Gets this folder's list item field values + * + */ + readonly listItemAllFields: ISharePointQueryableInstance; + + /** + * Gets the parent folder, if available + * + */ + readonly parentFolder: IFolder; + + /** + * Gets this folder's properties + * + */ + readonly properties: _SharePointQueryableInstance; + + /** + * Gets this folder's server relative url + * + */ + readonly serverRelativeUrl: ISharePointQueryable; + + /** + * Gets a value that specifies the content type order. + * + */ + readonly uniqueContentTypeOrder: ISharePointQueryableCollection; + + update: (type: string, mapper: (data: Data, props: Props) => Return) => (props: Props) => Promise; + + /** + * Moves the folder to the Recycle Bin and returns the identifier of the new Recycle Bin item. + */ + recycle(): Promise; + + /** + * Gets the associated list item for this folder, loading the default properties + */ + getItem(...selects: string[]): Promise; + + /** + * Moves a folder to destination path + * + * @param destUrl Absolute or relative URL of the destination path + */ + moveTo(destUrl: string): Promise; +} + +export interface _Folder extends IGetable, IDeleteableWithETag { } +export const Folder = spInvokableFactory(_Folder); + +export interface IFolderAddResult { + folder: IFolder; + data: any; +} + +export interface IFolderUpdateResult { + folder: IFolder; + data: any; +} diff --git a/packages/sp/src/folders/web.ts b/packages/sp/src/folders/web.ts new file mode 100644 index 000000000..3120a32bf --- /dev/null +++ b/packages/sp/src/folders/web.ts @@ -0,0 +1,51 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { Folders, IFolders, Folder, IFolder } from "./types"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +declare module "../webs/types" { + interface _Web { + readonly folders: IFolders; + readonly rootFolder: IFolder; + getFolderByServerRelativeUrl(folderRelativeUrl: string): IFolder; + getFolderByServerRelativePath(folderRelativeUrl: string): IFolder; + } + interface IWeb { + + /** + * Gets the collection of folders in this web + */ + readonly folders: IFolders; + + /** + * Gets the root folder of the web + */ + readonly rootFolder: IFolder; + + /** + * Gets a folder by server relative url + * + * @param folderRelativeUrl The server relative path to the folder (including /sites/ if applicable) + */ + getFolderByServerRelativeUrl(folderRelativeUrl: string): IFolder; + + /** + * Gets a folder by server relative relative path if your folder name contains # and % characters + * This works only in SharePoint online. + * + * @param folderRelativeUrl The server relative path to the folder (including /sites/ if applicable) + */ + getFolderByServerRelativePath(folderRelativeUrl: string): IFolder; + } +} + +addProp(_Web, "folders", Folders); +addProp(_Web, "rootFolder", Folder, "rootFolder"); + +_Web.prototype.getFolderByServerRelativeUrl = function (this: _Web, folderRelativeUrl: string): IFolder { + return Folder(this, `getFolderByServerRelativeUrl('${escapeQueryStrValue(folderRelativeUrl)}')`); +}; + +_Web.prototype.getFolderByServerRelativePath = function (this: _Web, folderRelativeUrl: string): IFolder { + return Folder(this, `getFolderByServerRelativePath(decodedUrl='${escapeQueryStrValue(folderRelativeUrl)}')`); +}; diff --git a/packages/sp/src/forms/index.ts b/packages/sp/src/forms/index.ts new file mode 100644 index 000000000..d71830e72 --- /dev/null +++ b/packages/sp/src/forms/index.ts @@ -0,0 +1,8 @@ +import "./list"; + +export { + Form, + Forms, + IForm, + IForms, +} from "./types"; diff --git a/packages/sp/src/forms/list.ts b/packages/sp/src/forms/list.ts new file mode 100644 index 000000000..d0c36ca97 --- /dev/null +++ b/packages/sp/src/forms/list.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { Forms, IForms } from "./types"; + +/** +* Extend Item +*/ +declare module "../lists/types" { + interface _List { + readonly forms: IForms; + } + interface IList { + readonly forms: IForms; + } +} + +addProp(_List, "forms", Forms, "file"); diff --git a/packages/sp/src/forms/types.ts b/packages/sp/src/forms/types.ts new file mode 100644 index 000000000..3239081d4 --- /dev/null +++ b/packages/sp/src/forms/types.ts @@ -0,0 +1,41 @@ +import { IGetable } from "@pnp/odata"; +import { + _SharePointQueryableInstance, + ISharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { defaultPath } from "../decorators"; + +/** + * Describes a collection of Field objects + * + */ +@defaultPath("forms") +export class _Forms extends _SharePointQueryableCollection implements IForms { + /** + * Gets a form by id + * + * @param id The guid id of the item to retrieve + */ + public getById(id: string): IForm { + return Form(this).concat(`('${id}')`); + } +} + +export interface IForms extends IGetable, ISharePointQueryableCollection { + getById(id: string): IForm; +} +export interface _Forms extends IGetable { } +export const Forms = spInvokableFactory(_Forms); + +/** + * Describes a single of Form instance + * + */ +export class _Form extends _SharePointQueryableInstance implements IForm { } + +export interface IForm extends IGetable, ISharePointQueryableInstance { } +export interface _Form extends IGetable { } +export const Form = spInvokableFactory(_Form); diff --git a/packages/sp/src/hubsites/index.ts b/packages/sp/src/hubsites/index.ts new file mode 100644 index 000000000..650406e90 --- /dev/null +++ b/packages/sp/src/hubsites/index.ts @@ -0,0 +1,32 @@ +import { SPRest } from "../rest"; +import { HubSites, IHubSites } from "./types"; + +// extend everything if they include the root +import "./site"; +import "./web"; + +export { + HubSite, + HubSites, + IHubSite, + IHubSiteData, + IHubSiteWebData, + IHubSites, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly hubSites: IHubSites; + } +} + +Reflect.defineProperty(SPRest.prototype, "hubSites", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return HubSites(this._baseUrl).configure(this._options); + }, +}); diff --git a/packages/sp/src/hubsites/site.ts b/packages/sp/src/hubsites/site.ts new file mode 100644 index 000000000..62ddf0534 --- /dev/null +++ b/packages/sp/src/hubsites/site.ts @@ -0,0 +1,43 @@ +import { _Site, Site } from "../sites/types"; +import { spPost } from "../operations"; + +/** + * Extend Site + */ +declare module "../sites/types" { + interface _Site { + joinHubSite(siteId: string): Promise; + registerHubSite(): Promise; + unRegisterHubSite(): Promise; + } + interface ISite { + joinHubSite(siteId: string): Promise; + registerHubSite(): Promise; + unRegisterHubSite(): Promise; + } +} + +/** + * Associates a site collection to a hub site. + * + * @param siteId Id of the hub site collection you want to join. + * If you want to disassociate the site collection from hub site, then + * pass the siteId as 00000000-0000-0000-0000-000000000000 + */ +_Site.prototype.joinHubSite = function (this: _Site, siteId: string): Promise { + return spPost(this.clone(Site, `joinHubSite('${siteId}')`)); +}; + +/** + * Registers the current site collection as hub site collection + */ +_Site.prototype.registerHubSite = function (this: _Site): Promise { + return spPost(this.clone(Site, `registerHubSite`)); +}; + +/** + * Unregisters the current site collection as hub site collection. + */ +_Site.prototype.unRegisterHubSite = function (this: _Site): Promise { + return spPost(this.clone(Site, `unRegisterHubSite`)); +}; diff --git a/packages/sp/src/hubsites/types.ts b/packages/sp/src/hubsites/types.ts new file mode 100644 index 000000000..c2876aa68 --- /dev/null +++ b/packages/sp/src/hubsites/types.ts @@ -0,0 +1,64 @@ +import { IGetable } from "@pnp/odata"; +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { INavigationNode } from "../navigation/types"; +import { defaultPath } from "../decorators"; + +export interface IHubSiteData { + Id?: string; + Title?: string; + SiteId?: string; + TenantInstanceId?: string; + SiteUrl?: string; + LogoUrl?: string; + Description?: string; + Targets?: string; +} + +export interface IHubSiteWebData { + ThemeKey: string; + Name: string; + Url: string; + LogoUrl: string; + UsesMetadataNavigation: boolean; + Navigation?: INavigationNode; +} + +/** + * Describes a collection of Hub Sites + * + */ +@defaultPath("_api/hubsites") +export class _HubSites extends _SharePointQueryableCollection implements IHubSites { + + /** + * Gets a Hub Site from the collection by id + * + * @param id The Id of the Hub Site + */ + public getById(id: string): IHubSite { + return HubSite(this, `GetById?hubSiteId='${id}'`); + + } +} + +export interface IHubSites extends IGetable, ISharePointQueryableCollection { + getById(id: string): IHubSite; +} +export interface _HubSites extends IGetable {} +export const HubSites = spInvokableFactory(_HubSites); + + + +/** + * Represents a hub site instance + */ +export class _HubSite extends _SharePointQueryableInstance implements IHubSite { } + +export interface IHubSite extends IGetable, _SharePointQueryableInstance {} +export interface _HubSite extends IGetable {} +export const HubSite = spInvokableFactory(_HubSite); diff --git a/packages/sp/src/hubsites/web.ts b/packages/sp/src/hubsites/web.ts new file mode 100644 index 000000000..a78ad8d4e --- /dev/null +++ b/packages/sp/src/hubsites/web.ts @@ -0,0 +1,34 @@ +import { _Web, Web } from "../webs/types"; +import { IHubSiteWebData } from "./types"; +import { spPost } from "../operations"; + +declare module "../webs/types" { + interface _Web { + hubSiteData(forceRefresh?: boolean): Promise; + syncHubSiteTheme(): Promise; + } + interface IWeb { + + /** + * Gets hub site data for the current web. + * + * @param forceRefresh Default value is false. When false, the data is returned from the server's cache. + * When true, the cache is refreshed with the latest updates and then returned. + * Use this if you just made changes and need to see those changes right away. + */ + hubSiteData(forceRefresh?: boolean): Promise; + + /** + * Applies theme updates from the parent hub site collection. + */ + syncHubSiteTheme(): Promise; + } +} + +_Web.prototype.hubSiteData = function (this: _Web, forceRefresh = false): Promise { + return this.clone(Web, `hubSiteData(${forceRefresh})`)(); +}; + +_Web.prototype.syncHubSiteTheme = function (this: _Web): Promise { + return spPost(this.clone(Web, `syncHubSiteTheme`)); +}; diff --git a/packages/sp/src/items/index.ts b/packages/sp/src/items/index.ts new file mode 100644 index 000000000..0b26b2caa --- /dev/null +++ b/packages/sp/src/items/index.ts @@ -0,0 +1,17 @@ +import "./list"; + +export { + Item, + Items, + IItem, + IItems, + ItemVersion, + ItemVersions, + IItemVersion, + IItemVersions, + IItemAddResult, + IItemUpdateResult, + IItemUpdateResultData, + PagedItemCollection, + ILikeData, +} from "./types"; diff --git a/packages/sp/src/items/list.ts b/packages/sp/src/items/list.ts new file mode 100644 index 000000000..0a3bd7788 --- /dev/null +++ b/packages/sp/src/items/list.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { Items, IItems } from "./types"; + +/** +* Extend Item +*/ +declare module "../lists/types" { + interface _List { + readonly items: IItems; + } + interface IList { + readonly items: IItems; + } +} + +addProp(_List, "items", Items); diff --git a/packages/sp/src/items/types.ts b/packages/sp/src/items/types.ts new file mode 100644 index 000000000..e9c75b6f2 --- /dev/null +++ b/packages/sp/src/items/types.ts @@ -0,0 +1,581 @@ +import { + SharePointQueryable, + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + ISharePointQueryable, + SharePointQueryableInstance, + spInvokableFactory, +} from "../sharepointqueryable"; +import { extend, TypedHash, hOP } from "@pnp/common"; +import { IListItemFormUpdateValue } from "../lists/types"; +import { ODataParser, IGetable, body, headers } from "@pnp/odata"; +import { IList } from "../lists"; +import { Logger, LogLevel } from "@pnp/logging"; +import { metadata } from "../utils/metadata"; +import { _List } from "../lists/types"; +import { defaultPath, deleteableWithETag, IDeleteableWithETag } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Describes a collection of Item objects + * + */ +@defaultPath("items") +export class _Items extends _SharePointQueryableCollection implements IItems { + + /** + * Gets an Item by id + * + * @param id The integer id of the item to retrieve + */ + public getById(id: number): IItem { + return Item(this).concat(`(${id})`); + } + + /** + * Gets BCS Item by string id + * + * @param stringId The string id of the BCS item to retrieve + */ + public getItemByStringId(stringId: string): IItem { + // creates an item with the parent list path and append out method call + return Item(this.parentUrl, `getItemByStringId('${stringId}')`); + } + + /** + * Skips the specified number of items (https://msdn.microsoft.com/en-us/library/office/fp142385.aspx#sectionSection6) + * + * @param skip The starting id where the page should start, use with top to specify pages + * @param reverse It true the PagedPrev=true parameter is added allowing backwards navigation in the collection + */ + public skip(skip: number, reverse = false): this { + if (reverse) { + this.query.set("$skiptoken", encodeURIComponent(`Paged=TRUE&PagedPrev=TRUE&p_ID=${skip}`)); + } else { + this.query.set("$skiptoken", encodeURIComponent(`Paged=TRUE&p_ID=${skip}`)); + } + return this; + } + + /** + * Gets a collection designed to aid in paging through data + * + */ + public getPaged(): Promise> { + return this.usingParser(new PagedItemCollectionParser(this))(); + } + + /** + * Gets all the items in a list, regardless of count. Does not support batching or caching + * + * @param requestSize Number of items to return in each request (Default: 2000) + * @param acceptHeader Allows for setting the value of the Accept header for SP 2013 support + */ + public getAll(requestSize = 2000, acceptHeader = "application/json;odata=nometadata"): Promise { + + Logger.write("Calling items.getAll should be done sparingly. Ensure this is the correct choice. If you are unsure, it is not.", LogLevel.Warning); + + // this will be used for the actual query + // and we set no metadata here to try and reduce traffic + const items = Items(this, "").top(requestSize).configure({ + headers: { + "Accept": acceptHeader, + }, + }); + + // let's copy over the odata query params that can be applied + // $top - allow setting the page size this way (override what we did above) + // $select - allow picking the return fields (good behavior) + // $filter - allow setting a filter, though this may fail due for large lists + this.query.forEach((v: string, k: string) => { + if (/^\$select|filter|top|expand$/i.test(k)) { + items.query.set(k, v); + } + }); + + // give back the promise + return new Promise((resolve, reject) => { + + // this will eventually hold the items we return + const itemsCollector: any[] = []; + + // action that will gather up our results recursively + const gatherer = (last: PagedItemCollection) => { + + // collect that set of results + [].push.apply(itemsCollector, last.results); + + // if we have more, repeat - otherwise resolve with the collected items + if (last.hasNext) { + last.getNext().then(gatherer).catch(reject); + } else { + resolve(itemsCollector); + } + }; + + // start the cycle + items.getPaged().then(gatherer).catch(reject); + }); + } + + /** + * Adds a new item to the collection + * + * @param properties The new items's properties + * @param listItemEntityTypeFullName The type name of the list's entities + */ + public async add(properties: TypedHash = {}, listItemEntityTypeFullName: string = null): Promise { + + const removeDependency = this.addBatchDependency(); + + const listItemEntityType = await this.ensureListItemEntityTypeName(listItemEntityTypeFullName); + + const postBody = body(extend(metadata(listItemEntityType), properties)); + + const promise = spPost<{ Id: number }>(this.clone(Items, ""), postBody).then((data) => { + return { + data: data, + item: this.getById(data.Id), + }; + }); + + removeDependency(); + + return promise; + } + + /** + * Ensures we have the proper list item entity type name, either from the value provided or from the list + * + * @param candidatelistItemEntityTypeFullName The potential type name + */ + private ensureListItemEntityTypeName(candidatelistItemEntityTypeFullName: string): Promise { + + return candidatelistItemEntityTypeFullName ? + Promise.resolve(candidatelistItemEntityTypeFullName) : + this.getParent(_List).getListItemEntityTypeFullName(); + } +} + +export interface IItems extends IGetable, ISharePointQueryableCollection { + /** + * Gets an Item by id + * + * @param id The integer id of the item to retrieve + */ + getById(id: number): IItem; + + /** + * Gets BCS Item by string id + * + * @param stringId The string id of the BCS item to retrieve + */ + getItemByStringId(stringId: string): IItem; + + /** + * Skips the specified number of items (https://msdn.microsoft.com/en-us/library/office/fp142385.aspx#sectionSection6) + * + * @param skip The starting id where the page should start, use with top to specify pages + * @param reverse It true the PagedPrev=true parameter is added allowing backwards navigation in the collection + */ + skip(skip: number, reverse?: boolean): this; + + /** + * Gets a collection designed to aid in paging through data + * + */ + getPaged(): Promise>; + + /** + * Gets all the items in a list, regardless of count. Does not support batching or caching + * + * @param requestSize Number of items to return in each request (Default: 2000) + * @param acceptHeader Allows for setting the value of the Accept header for SP 2013 support + */ + getAll(requestSize?: number, acceptHeader?: string): Promise; + + /** + * Adds a new item to the collection + * + * @param properties The new items's properties + * @param listItemEntityTypeFullName The type name of the list's entities + */ + add(properties?: TypedHash, listItemEntityTypeFullName?: string): Promise; +} +export interface _Items extends IGetable { } +export const Items = spInvokableFactory(_Items); + +/** + * Descrines a single Item instance + * + */ +@deleteableWithETag() +export class _Item extends _SharePointQueryableInstance implements IItem { + + /** + * Gets the effective base permissions for the item + * + */ + public get effectiveBasePermissions(): ISharePointQueryable { + return SharePointQueryable(this, "EffectiveBasePermissions"); + } + + /** + * Gets the effective base permissions for the item in a UI context + * + */ + public get effectiveBasePermissionsForUI(): ISharePointQueryable { + return SharePointQueryable(this, "EffectiveBasePermissionsForUI"); + } + + /** + * Gets the field values for this list item in their HTML representation + * + */ + public get fieldValuesAsHTML(): ISharePointQueryableInstance { + return SharePointQueryableInstance(this, "FieldValuesAsHTML"); + } + + /** + * Gets the field values for this list item in their text representation + * + */ + public get fieldValuesAsText(): ISharePointQueryableInstance { + return SharePointQueryableInstance(this, "FieldValuesAsText"); + } + + /** + * Gets the field values for this list item for use in editing controls + * + */ + public get fieldValuesForEdit(): ISharePointQueryableInstance { + return SharePointQueryableInstance(this, "FieldValuesForEdit"); + } + + /** + * Gets the collection of versions associated with this item + */ + public get versions(): IItemVersions { + return ItemVersions(this); + } + + public get list(): IList { + return this.getParent(_List, this.parentUrl.substr(0, this.parentUrl.lastIndexOf("/"))); + } + + /** + * Updates this list intance with the supplied properties + * + * @param properties A plain object hash of values to update for the list + * @param eTag Value used in the IF-Match header, by default "*" + * @param listItemEntityTypeFullName The type name of the list's entities + */ + public async update(properties: TypedHash, eTag = "*", listItemEntityTypeFullName: string = null): Promise { + + const removeDependency = this.addBatchDependency(); + + const listItemEntityType = await this.ensureListItemEntityTypeName(listItemEntityTypeFullName); + + const postBody = body(extend(metadata(listItemEntityType), properties), headers({ + "IF-Match": eTag, + "X-HTTP-Method": "MERGE", + })); + + removeDependency(); + + const data = await spPost(this.clone(Item).usingParser(new ItemUpdatedParser()), postBody); + + return { + data: data, + item: this, + }; + } + + /** + * Gets the collection of people who have liked this item + */ + public getLikedBy(): Promise { + return spPost(this.clone(Item, "likedBy")); + } + + /** + * Likes this item as the current user + */ + public like(): Promise { + return spPost(this.clone(Item, "like")); + } + + /** + * Unlikes this item as the current user + */ + public unlike(): Promise { + return spPost(this.clone(Item, "unlike")); + } + + /** + * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item. + */ + public recycle(): Promise { + return spPost(this.clone(Item, "recycle")); + } + + /** + * Gets a string representation of the full URL to the WOPI frame. + * If there is no associated WOPI application, or no associated action, an empty string is returned. + * + * @param action Display mode: 0: view, 1: edit, 2: mobileView, 3: interactivePreview + */ + public async getWopiFrameUrl(action = 0): Promise { + const i = this.clone(Item, "getWOPIFrameUrl(@action)"); + i.query.set("@action", action); + + const data = await spPost(i); + + // handle verbose mode + if (hOP(data, "GetWOPIFrameUrl")) { + return data.GetWOPIFrameUrl; + } + + return data; + } + + /** + * Validates and sets the values of the specified collection of fields for the list item. + * + * @param formValues The fields to change and their new values. + * @param bNewDocumentUpdate true if the list item is a document being updated after upload; otherwise false. + */ + public validateUpdateListItem(formValues: IListItemFormUpdateValue[], bNewDocumentUpdate = false): Promise { + return spPost(this.clone(Item, "validateupdatelistitem"), body({ formValues, bNewDocumentUpdate })); + } + + /** + * Get the like by information for a modern site page + */ + public getLikedByInformation(): Promise { + return this.clone(Item, "likedByInformation").expand("likedby")(); + } + + /** + * Ensures we have the proper list item entity type name, either from the value provided or from the list + * + * @param candidatelistItemEntityTypeFullName The potential type name + */ + private ensureListItemEntityTypeName(candidatelistItemEntityTypeFullName: string): Promise { + + return candidatelistItemEntityTypeFullName ? + Promise.resolve(candidatelistItemEntityTypeFullName) : + this.list.getListItemEntityTypeFullName(); + } +} + +export interface IItem extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag { + + /** + * Gets the effective base permissions for the item + * + */ + readonly effectiveBasePermissions: ISharePointQueryable; + + /** + * Gets the effective base permissions for the item in a UI context + * + */ + readonly effectiveBasePermissionsForUI: ISharePointQueryable; + + /** + * Gets the field values for this list item in their HTML representation + * + */ + readonly fieldValuesAsHTML: ISharePointQueryableInstance; + + /** + * Gets the field values for this list item in their text representation + * + */ + readonly fieldValuesAsText: ISharePointQueryableInstance; + + /** + * Gets the field values for this list item for use in editing controls + * + */ + readonly fieldValuesForEdit: ISharePointQueryableInstance; + /** + * Gets the collection of versions associated with this item + */ + readonly versions: IItemVersions; + + readonly list: IList; + + /** + * Updates this list intance with the supplied properties + * + * @param properties A plain object hash of values to update for the list + * @param eTag Value used in the IF-Match header, by default "*" + * @param listItemEntityTypeFullName The type name of the list's entities + */ + update(properties: TypedHash, eTag?: string, listItemEntityTypeFullName?: string): Promise; + + /** + * Gets the collection of people who have liked this item + */ + getLikedBy(): Promise; + + /** + * Likes this item as the current user + */ + like(): Promise; + + /** + * Unlikes this item as the current user + */ + unlike(): Promise; + + /** + * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item. + */ + recycle(): Promise; + + /** + * Gets a string representation of the full URL to the WOPI frame. + * If there is no associated WOPI application, or no associated action, an empty string is returned. + * + * @param action Display mode: 0: view, 1: edit, 2: mobileView, 3: interactivePreview + */ + getWopiFrameUrl(action?: number): Promise; + + + /** + * Validates and sets the values of the specified collection of fields for the list item. + * + * @param formValues The fields to change and their new values. + * @param newDocumentUpdate true if the list item is a document being updated after upload; otherwise false. + */ + validateUpdateListItem(formValues: IListItemFormUpdateValue[], newDocumentUpdate?: boolean): Promise; + + /** + * Get the like by information for a modern site page + */ + getLikedByInformation(): Promise; +} +export interface _Item extends IGetable, IDeleteableWithETag { } +export const Item = spInvokableFactory(_Item); + +export interface IItemAddResult { + item: IItem; + data: any; +} + +export interface IItemUpdateResult { + item: IItem; + data: IItemUpdateResultData; +} + +export interface IItemUpdateResultData { + "odata.etag": string; +} + +/** + * Describes a collection of Version objects + * + */ +@defaultPath("versions") +export class _ItemVersions extends _SharePointQueryableCollection implements IItemVersions { + /** + * Gets a version by id + * + * @param versionId The id of the version to retrieve + */ + public getById(versionId: number): IItemVersion { + return ItemVersion(this).concat(`(${versionId})`); + } +} + +export interface IItemVersions extends IGetable, ISharePointQueryableCollection { + getById(versionId: number): IItemVersion; +} +export interface _ItemVersions extends IGetable { } +export const ItemVersions = spInvokableFactory(_ItemVersions); + +/** + * Describes a single Version instance + * + */ +@deleteableWithETag() +export class _ItemVersion extends _SharePointQueryableInstance {} + +export interface IItemVersion extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag {} +export interface _ItemVersion extends IGetable, IDeleteableWithETag { } +export const ItemVersion = spInvokableFactory(_ItemVersion); + +/** + * Provides paging functionality for list items + */ +export class PagedItemCollection { + + constructor(private parent: _Items, private nextUrl: string, public results: T) { } + + /** + * If true there are more results available in the set, otherwise there are not + */ + public get hasNext(): boolean { + return typeof this.nextUrl === "string" && this.nextUrl.length > 0; + } + + /** + * Gets the next set of results, or resolves to null if no results are available + */ + public getNext(): Promise> { + + if (this.hasNext) { + const items = Items(this.nextUrl, null).configureFrom(this.parent); + return items.getPaged(); + } + + return new Promise(r => r(null)); + } +} + +class PagedItemCollectionParser extends ODataParser> { + + constructor(private _parent: _Items) { + super(); + } + + public parse(r: Response): Promise> { + + return new Promise((resolve, reject) => { + + if (this.handleError(r, reject)) { + r.json().then(json => { + const nextUrl = hOP(json, "d") && hOP(json.d, "__next") ? json.d.__next : json["odata.nextLink"]; + resolve(new PagedItemCollection(this._parent, nextUrl, this.parseODataJSON(json))); + }); + } + }); + } +} + +class ItemUpdatedParser extends ODataParser { + public parse(r: Response): Promise { + + return new Promise((resolve, reject) => { + + if (this.handleError(r, reject)) { + resolve({ + "odata.etag": r.headers.get("etag"), + }); + } + }); + } +} + +export interface ILikeData { + name: string; + loginName: string; + id: number; + email: string; + creationDate: string; +} diff --git a/packages/sp/src/lists/index.ts b/packages/sp/src/lists/index.ts new file mode 100644 index 000000000..01316b700 --- /dev/null +++ b/packages/sp/src/lists/index.ts @@ -0,0 +1,20 @@ +import "./web"; + +export { + List, + IList, + Lists, + ILists, + IListAddResult, + IListUpdateResult, + IListEnsureResult, + ControlMode, + ICamlQuery, + IChangeLogItemQuery, + IListFormData, + IListItemCollectionPosition, + IListItemFormUpdateValue, + IRenderListData, + IRenderListDataOptions, + IRenderListDataParameters, +} from "./types"; diff --git a/packages/sp/src/lists/types.ts b/packages/sp/src/lists/types.ts new file mode 100644 index 000000000..a6960b650 --- /dev/null +++ b/packages/sp/src/lists/types.ts @@ -0,0 +1,694 @@ +import { extend, TypedHash, hOP } from "@pnp/common"; +import { IGetable, body, headers } from "@pnp/odata"; +import { + SharePointQueryable, + SharePointQueryableCollection, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableInstance, + _SharePointQueryableCollection, + ISharePointQueryable, + spInvokableFactory, +} from "../sharepointqueryable"; +import { IChangeQuery } from "../types"; +import { odataUrlFrom } from "../odata"; +import { metadata } from "../utils/metadata"; +import { defaultPath, deleteableWithETag, IDeleteableWithETag } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Describes a collection of List objects + * + */ +@defaultPath("lists") +export class _Lists extends _SharePointQueryableCollection implements ILists { + + /** + * Gets a list from the collection by guid id + * + * @param id The Id of the list (GUID) + */ + public getById(id: string): IList { + return List(this).concat(`('${id}')`); + } + + /** + * Gets a list from the collection by title + * + * @param title The title of the list + */ + public getByTitle(title: string): IList { + return List(this, `getByTitle('${title}')`); + } + + /** + * Adds a new list to the collection + * + * @param title The new list's title + * @param desc The new list's description + * @param template The list template value + * @param enableContentTypes If true content types will be allowed and enabled, otherwise they will be disallowed and not enabled + * @param additionalSettings Will be passed as part of the list creation body + */ + public async add(title: string, desc = "", template = 100, enableContentTypes = false, additionalSettings: TypedHash = {}): Promise { + + const addSettings = Object.assign({ + "AllowContentTypes": enableContentTypes, + "BaseTemplate": template, + "ContentTypesEnabled": enableContentTypes, + "Description": desc, + "Title": title, + }, metadata("SP.List"), additionalSettings); + + const data = await spPost(this, body(addSettings)); + + return { data, list: this.getByTitle(addSettings.Title) }; + } + + /** + * Ensures that the specified list exists in the collection (note: this method not supported for batching) + * + * @param title The new list's title + * @param desc The new list's description + * @param template The list template value + * @param enableContentTypes If true content types will be allowed and enabled, otherwise they will be disallowed and not enabled + * @param additionalSettings Will be passed as part of the list creation body or used to update an existing list + */ + public ensure( + title: string, + desc = "", + template = 100, + enableContentTypes = false, + additionalSettings: TypedHash = {}): Promise { + + if (this.hasBatch) { + throw Error("The ensure list method is not supported for use in a batch."); + } + + return new Promise((resolve, reject) => { + + const addOrUpdateSettings = extend(additionalSettings, { Title: title, Description: desc, ContentTypesEnabled: enableContentTypes }, true); + + const list: IList = this.getByTitle(addOrUpdateSettings.Title); + + list.get().then(_ => { + + list.update(addOrUpdateSettings).then(d => { + resolve({ created: false, data: d, list: this.getByTitle(addOrUpdateSettings.Title) }); + }).catch(e => reject(e)); + + }).catch(_ => { + + this.add(title, desc, template, enableContentTypes, addOrUpdateSettings).then((r) => { + resolve({ created: true, data: r.data, list: this.getByTitle(addOrUpdateSettings.Title) }); + }).catch((e) => reject(e)); + }); + }); + } + + /** + * Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages. + */ + public async ensureSiteAssetsLibrary(): Promise { + const json = await spPost(this.clone(Lists, "ensuresiteassetslibrary")); + return List(odataUrlFrom(json)); + } + + /** + * Gets a list that is the default location for wiki pages. + */ + public async ensureSitePagesLibrary(): Promise { + const json = await spPost(this.clone(Lists, "ensuresitepageslibrary")); + return List(odataUrlFrom(json)); + } +} + +export interface ILists extends IGetable, ISharePointQueryableCollection { + /** + * Gets a list from the collection by guid id + * + * @param id The Id of the list (GUID) + */ + getById(id: string): IList; + + /** + * Gets a list from the collection by title + * + * @param title The title of the list + */ + getByTitle(title: string): IList; + + /** + * Adds a new list to the collection + * + * @param title The new list's title + * @param description The new list's description + * @param template The list template value + * @param enableContentTypes If true content types will be allowed and enabled, otherwise they will be disallowed and not enabled + * @param additionalSettings Will be passed as part of the list creation body + */ + add(title: string, description?: string, template?: number, enableContentTypes?: boolean, additionalSettings?: TypedHash): Promise; + + /** + * Ensures that the specified list exists in the collection (note: this method not supported for batching) + * + * @param title The new list's title + * @param desc The new list's description + * @param template The list template value + * @param enableContentTypes If true content types will be allowed and enabled, otherwise they will be disallowed and not enabled + * @param additionalSettings Will be passed as part of the list creation body or used to update an existing list + */ + ensure(title: string, desc?: string, template?: number, enableContentTypes?: boolean, additionalSettings?: TypedHash): Promise; + + /** + * Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages. + */ + ensureSiteAssetsLibrary(): Promise; + + /** + * Gets a list that is the default location for wiki pages. + */ + ensureSitePagesLibrary(): Promise; +} +export interface _Lists extends IGetable { } +export const Lists = spInvokableFactory(_Lists); + +/** + * Describes a single List instance + * + */ +@deleteableWithETag() +export class _List extends _SharePointQueryableInstance { + + /** + * Gets the effective base permissions of this list + * + */ + public get effectiveBasePermissions(): ISharePointQueryable { + return SharePointQueryable(this, "EffectiveBasePermissions"); + } + + /** + * Gets the event receivers attached to this list + * + */ + public get eventReceivers(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "EventReceivers"); + } + + /** + * Gets the related fields of this list + * + */ + public get relatedFields(): ISharePointQueryable { + return SharePointQueryable(this, "getRelatedFields"); + } + + /** + * Gets the IRM settings for this list + * + */ + public get informationRightsManagementSettings(): ISharePointQueryable { + return SharePointQueryable(this, "InformationRightsManagementSettings"); + } + + /** + * Updates this list intance with the supplied properties + * + * @param properties A plain object hash of values to update for the list + * @param eTag Value used in the IF-Match header, by default "*" + */ + public async update(properties: TypedHash, eTag = "*"): Promise { + + const postBody = body(extend({ + "__metadata": { "type": "SP.List" }, + }, properties), headers({ + "IF-Match": eTag, + "X-HTTP-Method": "MERGE", + })); + + const data = await spPost(this, postBody); + + let list: IList = this; + + if (hOP(properties, "Title")) { + list = this.getParent(_List, this.parentUrl, `getByTitle('${properties.Title}')`); + } + + return { + data, + list, + }; + } + /* tslint:enable */ + + /** + * Returns the collection of changes from the change log that have occurred within the list, based on the specified query. + */ + public getChanges(query: IChangeQuery): Promise { + + return spPost(this.clone(List, "getchanges"), body({ query: extend({ "__metadata": { "type": "SP.ChangeQuery" } }, query) })); + } + + /** + * Returns a collection of items from the list based on the specified query. + * + * @param CamlQuery The Query schema of Collaborative Application Markup + * Language (CAML) is used in various ways within the context of Microsoft SharePoint Foundation + * to define queries against list data. + * see: + * + * https://msdn.microsoft.com/en-us/library/office/ms467521.aspx + * + * @param expands A URI with a $expand System Query Option indicates that Entries associated with + * the Entry or Collection of Entries identified by the Resource Path + * section of the URI must be represented inline (i.e. eagerly loaded). + * see: + * + * https://msdn.microsoft.com/en-us/library/office/fp142385.aspx + * + * http://www.odata.org/documentation/odata-version-2-0/uri-conventions/#ExpandSystemQueryOption + */ + public getItemsByCAMLQuery(query: ICamlQuery, ...expands: string[]): Promise { + + const q = this.clone(List, "getitems"); + return spPost(q.expand.apply(q, expands), body({ "query": extend({ "__metadata": { "type": "SP.CamlQuery" } }, query) })); + } + + /** + * See: https://msdn.microsoft.com/en-us/library/office/dn292554.aspx + */ + public getListItemChangesSinceToken(query: IChangeLogItemQuery): Promise { + + const o = this.clone(List, "getlistitemchangessincetoken").usingParser({ parse(r: Response) { return r.text(); } }); + return spPost(o, body({ "query": extend({ "__metadata": { "type": "SP.ChangeLogItemQuery" } }, query) })); + } + + /** + * Moves the list to the Recycle Bin and returns the identifier of the new Recycle Bin item. + */ + public async recycle(): Promise { + const data = await spPost(this.clone(List, "recycle")); + return hOP(data, "Recycle") ? data.Recycle : data; + } + + /** + * Renders list data based on the view xml provided + */ + public async renderListData(viewXml: string): Promise { + + const q = this.clone(List, "renderlistdata(@viewXml)"); + q.query.set("@viewXml", `'${viewXml}'`); + const data = await spPost(q); + + // data will be a string, so we parse it again + return JSON.parse(hOP(data, "RenderListData") ? data.RenderListData : data); + } + + /** + * Returns the data for the specified query view + * + * @param parameters The parameters to be used to render list data as JSON string. + * @param overrideParameters The parameters that are used to override and extend the regular SPRenderListDataParameters. + */ + public renderListDataAsStream(parameters: IRenderListDataParameters, overrideParameters: any = null): Promise { + + const postBody = body({ + overrideParameters: extend(metadata("SP.RenderListDataOverrideParameters"), overrideParameters), + parameters: extend(metadata("SP.RenderListDataParameters"), parameters), + }); + + return spPost(this.clone(List, "RenderListDataAsStream", true), postBody); + } + + /** + * Gets the field values and field schema attributes for a list item. + */ + public async renderListFormData(itemId: number, formId: string, mode: ControlMode): Promise { + const data = await spPost(this.clone(List, `renderlistformdata(itemid=${itemId}, formid='${formId}', mode='${mode}')`)); + // data will be a string, so we parse it again + return JSON.parse(hOP(data, "RenderListFormData") ? data.RenderListFormData : data); + } + + /** + * Reserves a list item ID for idempotent list item creation. + */ + public async reserveListItemId(): Promise { + const data = await spPost(this.clone(List, "reservelistitemid")); + return hOP(data, "ReserveListItemId") ? data.ReserveListItemId : data; + } + + /** + * Returns the ListItemEntityTypeFullName for this list, used when adding/updating list items. Does not support batching. + * + */ + public getListItemEntityTypeFullName(): Promise { + return this.clone(List, null, false).select("ListItemEntityTypeFullName").get<{ ListItemEntityTypeFullName: string }>().then(o => o.ListItemEntityTypeFullName); + } + + /** + * Creates an item using path (in a folder), validates and sets its field values. + * + * @param formValues The fields to change and their new values. + * @param decodedUrl Path decoded url; folder's server relative path. + * @param bNewDocumentUpdate true if the list item is a document being updated after upload; otherwise false. + * @param checkInComment Optional check in comment. + */ + public async addValidateUpdateItemUsingPath( + formValues: IListItemFormUpdateValue[], + decodedUrl: string, + bNewDocumentUpdate = false, + checkInComment?: string, + ): Promise { + const res = await spPost(this.clone(List, "AddValidateUpdateItemUsingPath()"), body({ + bNewDocumentUpdate, + checkInComment, + formValues, + listItemCreateInfo: { + FolderPath: { + DecodedUrl: decodedUrl, + __metadata: { type: "SP.ResourcePath" }, + }, + __metadata: { type: "SP.ListItemCreationInformationUsingPath" }, + }, + })); + + return hOP(res, "AddValidateUpdateItemUsingPath") ? res.AddValidateUpdateItemUsingPath : res; + } +} + +export interface IList extends IGetable, ISharePointQueryableInstance, IDeleteableWithETag { + + /** + * Gets the effective base permissions of this list + * + */ + readonly effectiveBasePermissions: ISharePointQueryable; + + /** + * Gets the event receivers attached to this list + * + */ + readonly eventReceivers: ISharePointQueryableCollection; + + /** + * Gets the related fields of this list + * + */ + readonly relatedFields: ISharePointQueryable; + + /** + * Gets the IRM settings for this list + * + */ + readonly informationRightsManagementSettings: ISharePointQueryable; + + /** + * Updates this list intance with the supplied properties + * + * @param properties A plain object hash of values to update for the list + * @param eTag Value used in the IF-Match header, by default "*" + */ + update(properties: TypedHash, eTag?: string): Promise; + + /** + * Returns the collection of changes from the change log that have occurred within the list, based on the specified query. + */ + getChanges(query: IChangeQuery): Promise; + + getItemsByCAMLQuery(query: ICamlQuery, ...expands: string[]): Promise; + + /** + * See: https://msdn.microsoft.com/en-us/library/office/dn292554.aspx + */ + getListItemChangesSinceToken(query: IChangeLogItemQuery): Promise; + + /** + * Moves the list to the Recycle Bin and returns the identifier of the new Recycle Bin item. + */ + recycle(): Promise; + + /** + * Renders list data based on the view xml provided + */ + renderListData(viewXml: string): Promise; + + /** + * Returns the data for the specified query view + * + * @param parameters The parameters to be used to render list data as JSON string. + * @param overrideParameters The parameters that are used to override and extend the regular SPRenderListDataParameters. + */ + renderListDataAsStream(parameters: IRenderListDataParameters, overrideParameters?: any): Promise; + + /** + * Gets the field values and field schema attributes for a list item. + */ + renderListFormData(itemId: number, formId: string, mode: ControlMode): Promise; + + /** + * Reserves a list item ID for idempotent list item creation. + */ + reserveListItemId(): Promise; + + /** + * Returns the ListItemEntityTypeFullName for this list, used when adding/updating list items. Does not support batching. + * + */ + getListItemEntityTypeFullName(): Promise; + + /** + * Creates an item using path (in a folder), validates and sets its field values. + * + * @param formValues The fields to change and their new values. + * @param decodedUrl Path decoded url; folder's server relative path. + * @param bNewDocumentUpdate true if the list item is a document being updated after upload; otherwise false. + * @param comment Optional check in comment. + */ + addValidateUpdateItemUsingPath(formValues: IListItemFormUpdateValue[], decodedUrl: string, bNewDocumentUpdate?: boolean, comment?: string): Promise; +} +export interface _List extends IGetable, IDeleteableWithETag { } +export const List = spInvokableFactory(_List); + +export interface IListAddResult { + list: IList; + data: any; +} + +export interface IListUpdateResult { + list: IList; + data: any; +} + +export interface IListEnsureResult { + list: IList; + created: boolean; + data: any; +} + +/** + * Specifies a Collaborative Application Markup Language (CAML) query on a list or joined lists. + */ +export interface ICamlQuery { + + /** + * Gets or sets a value that indicates whether the query returns dates in Coordinated Universal Time (UTC) format. + */ + DatesInUtc?: boolean; + + /** + * Gets or sets a value that specifies the server relative URL of a list folder from which results will be returned. + */ + FolderServerRelativeUrl?: string; + + /** + * Gets or sets a value that specifies the information required to get the next page of data for the list view. + */ + ListItemCollectionPosition?: IListItemCollectionPosition; + + /** + * Gets or sets value that specifies the XML schema that defines the list view. + */ + ViewXml?: string; +} + +/** + * Specifies the information required to get the next page of data for a list view. + */ +export interface IListItemCollectionPosition { + /** + * Gets or sets a value that specifies information, as name-value pairs, required to get the next page of data for a list view. + */ + PagingInfo: string; +} + +/** + * Represents the input parameter of the GetListItemChangesSinceToken method. + */ +export interface IChangeLogItemQuery { + /** + * The change token for the request. + */ + ChangeToken?: string; + + /** + * The XML element that defines custom filtering for the query. + */ + Contains?: string; + + /** + * The records from the list to return and their return order. + */ + Query?: string; + + /** + * The options for modifying the query. + */ + QueryOptions?: string; + + /** + * RowLimit + */ + RowLimit?: string; + + /** + * The names of the fields to include in the query result. + */ + ViewFields?: string; + + /** + * The GUID of the view. + */ + ViewName?: string; +} + +export interface IListFormData { + ContentType?: string; + Title?: string; + Author?: string; + Editor?: string; + Created?: Date; + Modified: Date; + Attachments?: any; + ListSchema?: any; + FormControlMode?: number; + FieldControlModes?: { + Title?: number, + Author?: number, + Editor?: number, + Created?: number, + Modified?: number, + Attachments?: number, + }; + WebAttributes?: { + WebUrl?: string, + EffectivePresenceEnabled?: boolean, + AllowScriptableWebParts?: boolean, + PermissionCustomizePages?: boolean, + LCID?: number, + CurrentUserId?: number, + }; + ItemAttributes?: { + Id?: number, + FsObjType?: number, + ExternalListItem?: boolean, + Url?: string, + EffectiveBasePermissionsLow?: number, + EffectiveBasePermissionsHigh?: number, + }; + ListAttributes?: { + Id?: string, + BaseType?: number, + Direction?: string, + ListTemplateType?: number, + DefaultItemOpen?: number, + EnableVersioning?: boolean, + }; + CSRCustomLayout?: boolean; + PostBackRequired?: boolean; + PreviousPostBackHandled?: boolean; + UploadMode?: boolean; + SubmitButtonID?: string; + ItemContentTypeName?: string; + ItemContentTypeId?: string; + JSLinks?: string; +} + +export enum IRenderListDataOptions { + None = 0, + ContextInfo = 1, + ListData = 2, + ListSchema = 4, + MenuView = 8, + ListContentType = 16, + FileSystemItemId = 32, + ClientFormSchema = 64, + QuickLaunch = 128, + Spotlight = 256, + Visualization = 512, + ViewMetadata = 1024, + DisableAutoHyperlink = 2048, + EnableMediaTAUrls = 4096, + ParentInfo = 8192, + PageContextInfo = 16384, + ClientSideComponentManifest = 32768, +} + +export interface IRenderListDataParameters { + AllowMultipleValueFilterForTaxonomyFields?: boolean; + DatesInUtc?: boolean; + ExpandGroups?: boolean; + FirstGroupOnly?: boolean; + FolderServerRelativeUrl?: string; + ImageFieldsToTryRewriteToCdnUrls?: string; + OverrideViewXml?: string; + Paging?: string; + RenderOptions?: IRenderListDataOptions; + ReplaceGroup?: boolean; + ViewXml?: string; +} + +/** + * Represents properties of a list item field and its value. + */ +export interface IListItemFormUpdateValue { + + /** + * The error message result after validating the value for the field. + */ + ErrorMessage?: string; + + /** + * The internal name of the field. + */ + FieldName?: string; + + /** + * The value of the field, in string format. + */ + FieldValue?: string; + + /** + * Indicates whether there was an error result after validating the value for the field. + */ + HasException?: boolean; +} + +export interface IRenderListData { + Row: any[]; + FirstRow: number; + FolderPermissions: string; + LastRow: number; + FilterLink: string; + ForceNoHierarchy: string; + HierarchyHasIndention: string; +} + +/** + * Determines the display mode of the given control or view + */ +export enum ControlMode { + Display = 1, + Edit = 2, + New = 3, +} diff --git a/packages/sp/src/lists/web.ts b/packages/sp/src/lists/web.ts new file mode 100644 index 000000000..94e2a4cdf --- /dev/null +++ b/packages/sp/src/lists/web.ts @@ -0,0 +1,68 @@ +import { addProp } from "@pnp/odata"; +import { _Web, Web } from "../webs/types"; +import { Lists, ILists, IList, List } from "./types"; +import { odataUrlFrom } from "../odata"; +import { ISharePointQueryableCollection, SharePointQueryableCollection } from "../sharepointqueryable"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +declare module "../webs/types" { + interface _Web { + readonly lists: ILists; + readonly siteUserInfoList: IList; + readonly defaultDocumentLibrary: IList; + readonly customListTemplates: ISharePointQueryableCollection; + getList(listRelativeUrl: string): IList; + getCatalog(type: number): Promise; + } + interface IWeb { + + /** + * Gets the collection of all lists that are contained in the Web site + */ + readonly lists: ILists; + + /** + * Gets the UserInfo list of the site collection that contains the Web site + */ + readonly siteUserInfoList: IList; + + /** + * Get a reference the default documents library of a web + */ + readonly defaultDocumentLibrary: IList; + + /** + * Gets the collection of all list definitions and list templates that are available + */ + readonly customListTemplates: ISharePointQueryableCollection; + + /** + * Gets a list by server relative url (list's root folder) + * + * @param listRelativeUrl The server relative path to the list's root folder (including /sites/ if applicable) + */ + getList(listRelativeUrl: string): IList; + + /** + * Returns the list gallery on the site + * + * @param type The gallery type - WebTemplateCatalog = 111, WebPartCatalog = 113 ListTemplateCatalog = 114, + * MasterPageCatalog = 116, SolutionCatalog = 121, ThemeCatalog = 123, DesignCatalog = 124, AppDataCatalog = 125 + */ + getCatalog(type: number): Promise; + } +} + +addProp(_Web, "lists", Lists); +addProp(_Web, "siteUserInfoList", List, "siteuserinfolist"); +addProp(_Web, "defaultDocumentLibrary", List, "DefaultDocumentLibrary"); +addProp(_Web, "customListTemplates", SharePointQueryableCollection, "getcustomlisttemplates"); + +_Web.prototype.getList = function (this: _Web, listRelativeUrl: string): IList { + return List(this, `getList('${escapeQueryStrValue(listRelativeUrl)}')`); +}; + +_Web.prototype.getCatalog = async function (this: _Web, type: number): Promise { + const data = await this.clone(Web, `getcatalog(${type})`).select("Id").get(); + return List(odataUrlFrom(data)); +}; diff --git a/packages/sp/src/navigation/index.ts b/packages/sp/src/navigation/index.ts new file mode 100644 index 000000000..bbb3d49bb --- /dev/null +++ b/packages/sp/src/navigation/index.ts @@ -0,0 +1,31 @@ +import { SPRest } from "../rest"; +import { Navigation, INavigation } from "./types"; +import { addProp } from "@pnp/odata"; + +import "./web"; + +export { + INavNodeUpdateResult, + INavigation, + INavigationNode, + INavigationNodeAddResult, + INavigationNodes, + INavigationService, + Navigation, + NavigationNode, + NavigationNodes, + NavigationService, + IMenuNode, + IMenuNodeCollection, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly navigation: INavigation; + } +} + +addProp(SPRest, "navigation", Navigation); diff --git a/packages/sp/src/navigation/types.ts b/packages/sp/src/navigation/types.ts new file mode 100644 index 000000000..5894059eb --- /dev/null +++ b/packages/sp/src/navigation/types.ts @@ -0,0 +1,234 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + ISharePointQueryable, + _SharePointQueryableCollection, + _SharePointQueryable, + spInvokableFactory, +} from "../sharepointqueryable"; +import { extend, TypedHash } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { IGetable, body, headers } from "@pnp/odata"; +import { defaultPath, deleteable, IDeleteable } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Result from adding a navigation node + * + */ +export interface INavigationNodeAddResult { + data: any; + node: INavigationNode; +} + +/** + * Represents a collection of navigation nodes + * + */ +export class _NavigationNodes extends _SharePointQueryableCollection implements INavigationNodes { + + /** + * Gets a navigation node by id + * + * @param id The id of the node + */ + public getById(id: number): INavigationNode { + return NavigationNode(this).concat(`(${id})`); + } + + /** + * Adds a new node to the collection + * + * @param title Display name of the node + * @param url The url of the node + * @param visible If true the node is visible, otherwise it is hidden (default: true) + */ + public async add(title: string, url: string, visible = true): Promise { + + const postBody = body(extend(metadata("SP.NavigationNode"), { + IsVisible: visible, + Title: title, + Url: url, + })); + + const data = await spPost(this.clone(NavigationNodes, null), postBody); + + return { + data, + node: this.getById(data.Id), + }; + } + + /** + * Moves a node to be after another node in the navigation + * + * @param nodeId Id of the node to move + * @param previousNodeId Id of the node after which we move the node specified by nodeId + */ + public moveAfter(nodeId: number, previousNodeId: number): Promise { + + const postBody = body({ + nodeId: nodeId, + previousNodeId: previousNodeId, + }); + + return spPost(this.clone(NavigationNodes, "MoveAfter"), postBody); + } +} + +export interface INavigationNodes extends IGetable, ISharePointQueryableCollection { + getById(id: number): INavigationNode; + add(title: string, url: string, visible?: boolean): Promise; + moveAfter(nodeId: number, previousNodeId: number): Promise; +} +export interface _NavigationNodes extends IGetable { } +export const NavigationNodes = spInvokableFactory(_NavigationNodes); + + +/** + * Represents an instance of a navigation node + * + */ +@deleteable() +export class _NavigationNode extends _SharePointQueryableInstance { + + /** + * Represents the child nodes of this node + */ + public get children(): INavigationNodes { + return NavigationNodes(this, "children"); + } + + /** + * Updates this node + * + * @param properties Properties used to update this node + */ + public async update(properties: TypedHash): Promise { + + const postBody = body(extend(metadata("SP.NavigationNode"), properties), headers({ "X-HTTP-Method": "MERGE" })); + + const data = await spPost(this, postBody); + + return { + data, + node: this, + }; + } +} + +export interface INavigationNode extends IGetable, ISharePointQueryableInstance, IDeleteable { + readonly children: INavigationNodes; + update(properties: TypedHash): Promise; +} +export interface _NavigationNode extends IGetable, IDeleteable { } +export const NavigationNode = spInvokableFactory(_NavigationNode); + +export interface INavNodeUpdateResult { + data: any; + node: INavigationNode; +} + +/** + * Exposes the navigation components + * + */ +@defaultPath("navigation") +export class _Navigation extends _SharePointQueryable { + + /** + * Gets the quicklaunch navigation nodes for the current context + * + */ + public get quicklaunch(): INavigationNodes { + return NavigationNodes(this, "quicklaunch"); + } + + /** + * Gets the top bar navigation nodes for the current context + * + */ + public get topNavigationBar(): INavigationNodes { + return NavigationNodes(this, "topnavigationbar"); + } +} + +export interface INavigation extends IGetable, ISharePointQueryable { + readonly quicklaunch: INavigationNodes; + readonly topNavigationBar: INavigationNodes; +} +export interface _Navigation extends IGetable { } +export const Navigation = spInvokableFactory(_Navigation); + +/** + * Represents the top level navigation service + */ +export class _NavigationService extends _SharePointQueryable implements INavigationService { + + constructor(path: string = null) { + super("_api/navigation", path); + } + + /** + * The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. + * + * @param menuNodeKey MenuNode.Key of the start node within the SiteMapProvider If no key is provided the SiteMapProvider.RootNode will be the root of the menu state. + * @param depth Depth of the dump. If no value is provided a dump with the depth of 10 is returned + * @param mapProviderName The name identifying the SiteMapProvider to be used + * @param customProperties comma seperated list of custom properties to be returned. + */ + public getMenuState(menuNodeKey: string = null, depth = 10, mapProviderName: string = null, customProperties: string = null): Promise { + + return spPost(NavigationService("MenuState"), body({ + customProperties, + depth, + mapProviderName, + menuNodeKey, + })); + } + + /** + * Tries to get a SiteMapNode.Key for a given URL within a site collection. + * + * @param currentUrl A url representing the SiteMapNode + * @param mapProviderName The name identifying the SiteMapProvider to be used + */ + public getMenuNodeKey(currentUrl: string, mapProviderName: string = null): Promise { + + return spPost(NavigationService("MenuNodeKey"), body({ + currentUrl, + mapProviderName, + })); + } +} + +export interface INavigationService { + getMenuState(menuNodeKey?: string, depth?: number, mapProviderName?: string, customProperties?: string): Promise; + getMenuNodeKey(currentUrl: string, mapProviderName?: string): Promise; +} + +export const NavigationService = (path?: string) => new _NavigationService(path); + +export interface IMenuNode { + CustomProperties: any[]; + FriendlyUrlSegment: string; + IsDeleted: boolean; + IsHidden: boolean; + Key: string; + Nodes: IMenuNode[]; + NodeType: number; + SimpleUrl: string; + Title: string; +} + +export interface IMenuNodeCollection { + FriendlyUrlPrefix: string; + Nodes: IMenuNode[]; + SimpleUrl: string; + SPSitePrefix: string; + SPWebPrefix: string; + StartingNodeKey: string; + StartingNodeTitle: string; + Version: Date; +} diff --git a/packages/sp/src/navigation/web.ts b/packages/sp/src/navigation/web.ts new file mode 100644 index 000000000..d7297ae47 --- /dev/null +++ b/packages/sp/src/navigation/web.ts @@ -0,0 +1,19 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { Navigation, INavigation } from "./types"; + +declare module "../webs/types" { + interface _Web { + navigation: INavigation; + } + interface IWeb { + + /** + * Gets a navigation object that represents navigation on the Web site, + * including the Quick Launch area and the top navigation bar + */ + navigation: INavigation; + } +} + +addProp(_Web, "navigation", Navigation); diff --git a/packages/sp/src/net/digestcache.ts b/packages/sp/src/net/digestcache.ts new file mode 100644 index 000000000..7715ddfdf --- /dev/null +++ b/packages/sp/src/net/digestcache.ts @@ -0,0 +1,58 @@ +import { SPHttpClient } from "./sphttpclient"; +import { combine, extend } from "@pnp/common"; +import { ODataParser } from "@pnp/odata"; +import { SPRuntimeConfig } from "../config/splibconfig"; + +export class CachedDigest { + public expiration: Date; + public value: string; +} + +// allows for the caching of digests across all HttpClient's which each have their own DigestCache wrapper. +const digests = new Map(); + +export class DigestCache { + + constructor(private _httpClient: SPHttpClient, private _digests: Map = digests) { } + + public getDigest(webUrl: string): Promise { + + const cachedDigest: CachedDigest = this._digests.get(webUrl); + if (cachedDigest !== undefined) { + const now = new Date(); + if (now < cachedDigest.expiration) { + return Promise.resolve(cachedDigest.value); + } + } + + const url = combine(webUrl, "/_api/contextinfo"); + + const headers = { + "Accept": "application/json;odata=verbose", + "Content-Type": "application/json;odata=verbose;charset=utf-8", + }; + + return this._httpClient.fetchRaw(url, { + cache: "no-cache", + credentials: "same-origin", + headers: extend(headers, SPRuntimeConfig.headers, true), + method: "POST", + }).then((response) => { + const parser = new ODataParser(); + return parser.parse(response).then((d: any) => d.GetContextWebInformation); + }).then((data: any) => { + const newCachedDigest = new CachedDigest(); + newCachedDigest.value = data.FormDigestValue; + const seconds = data.FormDigestTimeoutSeconds; + const expiration = new Date(); + expiration.setTime(expiration.getTime() + 1000 * seconds); + newCachedDigest.expiration = expiration; + this._digests.set(webUrl, newCachedDigest); + return newCachedDigest.value; + }); + } + + public clear() { + this._digests.clear(); + } +} diff --git a/packages/sp/src/net/sphttpclient.ts b/packages/sp/src/net/sphttpclient.ts new file mode 100644 index 000000000..a9000e908 --- /dev/null +++ b/packages/sp/src/net/sphttpclient.ts @@ -0,0 +1,175 @@ +import { DigestCache } from "./digestcache"; +import { + extend, + mergeHeaders, + IFetchOptions, + IRequestClient, + getCtxCallback, + IHttpClientImpl, +} from "@pnp/common"; +import { SPRuntimeConfig } from "../config/splibconfig"; +import { extractWebUrl } from "../utils/extractweburl"; +import { clientTagMethod } from "../decorators"; + +export class SPHttpClient implements IRequestClient { + + private _digestCache: DigestCache; + + constructor(private _impl: IHttpClientImpl = SPRuntimeConfig.fetchClientFactory()) { + this._digestCache = new DigestCache(this); + } + + public fetch(url: string, options: IFetchOptions = {}): Promise { + + let opts = extend(options, { cache: "no-cache", credentials: "same-origin" }, true); + + const headers = new Headers(); + + // first we add the global headers so they can be overwritten by any passed in locally to this call + mergeHeaders(headers, SPRuntimeConfig.headers); + + // second we add the local options so we can overwrite the globals + mergeHeaders(headers, options.headers); + + // lastly we apply any default headers we need that may not exist + if (!headers.has("Accept")) { + headers.append("Accept", "application/json"); + } + + if (!headers.has("Content-Type")) { + headers.append("Content-Type", "application/json;odata=verbose;charset=utf-8"); + } + + if (!headers.has("X-ClientService-ClientTag")) { + + const methodName = clientTagMethod.getClientTag(headers); + let clientTag = `PnPCoreJS:@pnp-$$Version$$:${methodName}`; + + if (clientTag.length > 32) { + clientTag = clientTag.substr(0, 32); + } + + headers.append("X-ClientService-ClientTag", clientTag); + } + + if (!headers.has("User-Agent")) { + // this marks the requests for understanding by the service + // does not work in browsers + headers.append("User-Agent", "NONISV|SharePointPnP|PnPCoreJS/$$Version$$"); + } + + opts = extend(opts, { headers: headers }); + + if (opts.method && opts.method.toUpperCase() !== "GET") { + + // if we have either a request digest or an authorization header we don't need a digest + if (!headers.has("X-RequestDigest") && !headers.has("Authorization")) { + return this._digestCache.getDigest(extractWebUrl(url)) + .then((digest) => { + headers.append("X-RequestDigest", digest); + return this.fetchRaw(url, opts); + }); + } + } + + return this.fetchRaw(url, opts); + } + + public fetchRaw(url: string, options: IFetchOptions = {}): Promise { + + // here we need to normalize the headers + const rawHeaders = new Headers(); + mergeHeaders(rawHeaders, options.headers); + options = extend(options, { headers: rawHeaders }); + + const retry = (ctx: RetryContext): void => { + + // handles setting the proper timeout for a retry + const setRetry = (response: Response) => { + let delay; + + if (response.headers.has("Retry-After")) { + // if we have gotten a header, use that value as the delay value + delay = parseInt(response.headers.get("Retry-After"), 10); + } else { + // grab our current delay + delay = ctx.delay; + + // Increment our counters. + ctx.delay *= 2; + } + + ctx.attempts++; + + // If we have exceeded the retry count, reject. + if (ctx.retryCount <= ctx.attempts) { + ctx.reject(Error(`Retry count exceeded (${ctx.retryCount}) for request. Response status: [${response.status}] ${response.statusText}`)); + } else { + // Set our retry timeout for {delay} milliseconds. + setTimeout(getCtxCallback(this, retry, ctx), delay); + } + }; + + // send the actual request + this._impl.fetch(url, options).then((response) => { + + if (response.status === 429) { + // we have been throttled + setRetry(response); + } else { + ctx.resolve(response); + } + + }).catch((response: Response) => { + + if (response.status === 503) { + // http status code 503, we can retry this + setRetry(response); + } else { + ctx.reject(response); + } + }); + }; + + return new Promise((resolve, reject) => { + + const retryContext: RetryContext = { + attempts: 0, + delay: 100, + reject: reject, + resolve: resolve, + retryCount: 7, + }; + + retry.call(this, retryContext); + }); + } + + public get(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "GET" }); + return this.fetch(url, opts); + } + + public post(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "POST" }); + return this.fetch(url, opts); + } + + public patch(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "PATCH" }); + return this.fetch(url, opts); + } + + public delete(url: string, options: IFetchOptions = {}): Promise { + const opts = extend(options, { method: "DELETE" }); + return this.fetch(url, opts); + } +} + +interface RetryContext { + attempts: number; + delay: number; + reject: (reason?: any) => void; + resolve: (value?: Response | PromiseLike) => void; + retryCount: number; +} diff --git a/packages/sp/src/odata.ts b/packages/sp/src/odata.ts new file mode 100644 index 000000000..2f21cb27c --- /dev/null +++ b/packages/sp/src/odata.ts @@ -0,0 +1,90 @@ +import { ISharePointQueryableConstructor } from "./sharepointqueryable"; +import { extend, combine, hOP } from "@pnp/common"; +import { Logger, LogLevel } from "@pnp/logging"; +import { ODataParser } from "@pnp/odata"; +import { extractWebUrl } from "./utils/extractweburl"; + +export function odataUrlFrom(candidate: any): string { + + const parts: string[] = []; + const s = ["odata.type", "odata.editLink", "__metadata", "odata.metadata"]; + + if (hOP(candidate, s[0]) && candidate[s[0]] === "SP.Web") { + // webs return an absolute url in the editLink + if (hOP(candidate, s[1])) { + parts.push(candidate[s[1]]); + } else if (hOP(candidate, s[2])) { + // we are dealing with verbose, which has an absolute uri + parts.push(candidate.__metadata.uri); + } + + } else { + + if (hOP(candidate, s[3]) && hOP(candidate, s[1])) { + // we are dealign with minimal metadata (default) + parts.push(extractWebUrl(candidate[s[3]]), "_api", candidate[s[1]]); + } else if (hOP(candidate, s[1])) { + parts.push("_api", candidate[s[1]]); + } else if (hOP(candidate, s[2])) { + // we are dealing with verbose, which has an absolute uri + parts.push(candidate.__metadata.uri); + } + } + + if (parts.length < 1) { + Logger.write("No uri information found in ODataEntity parsing, chaining will fail for this object.", LogLevel.Warning); + return ""; + } + + return combine(...parts); +} + +class SPODataEntityParserImpl extends ODataParser { + + constructor(protected factory: ISharePointQueryableConstructor) { + super(); + } + + public hydrate = (d: D) => { + const o = new this.factory(odataUrlFrom(d), null); + return extend(o, d); + } + + public parse(r: Response): Promise { + return super.parse(r).then((d: any) => { + const o = new this.factory(odataUrlFrom(d), null); + return extend(o, d); + }); + } +} + +class SPODataEntityArrayParserImpl extends ODataParser<(T & D)[]> { + + constructor(protected factory: ISharePointQueryableConstructor) { + super(); + } + + public hydrate = (d: D[]) => { + return d.map(v => { + const o = new this.factory(odataUrlFrom(v), null); + return extend(o, v); + }); + } + + public parse(r: Response): Promise<(T & D)[]> { + return super.parse(r).then((d: D[]) => { + return d.map(v => { + const o = new this.factory(odataUrlFrom(v), null); + return extend(o, v); + }); + }); + } +} + +export function spODataEntity(factory: ISharePointQueryableConstructor): ODataParser { + return new SPODataEntityParserImpl(factory); +} + +export function spODataEntityArray(factory: ISharePointQueryableConstructor): ODataParser<(T & DataType)[]> { + return new SPODataEntityArrayParserImpl(factory); +} diff --git a/packages/sp/src/operations.ts b/packages/sp/src/operations.ts new file mode 100644 index 000000000..0fcf6120a --- /dev/null +++ b/packages/sp/src/operations.ts @@ -0,0 +1,58 @@ +import { defaultPipelineBinder, IOperation, cloneQueryableData, headers } from "@pnp/odata"; +import { SPHttpClient } from "./net/sphttpclient"; +import { ISharePointQueryable } from "./sharepointqueryable"; +import { IFetchOptions, mergeOptions, objectDefinedNotNull } from "@pnp/common"; +import { toAbsoluteUrl } from "./utils/toabsoluteurl"; + +const spClientBinder = defaultPipelineBinder(() => new SPHttpClient()); + +const send = (operation: IOperation): (o: ISharePointQueryable, options?: IFetchOptions) => Promise => { + + return async function (o: ISharePointQueryable, options?: IFetchOptions): Promise { + + const data = cloneQueryableData(o.data); + const batchDependency = objectDefinedNotNull(data.batch) ? data.batch.addDependency() : () => { return; }; + const url = await toAbsoluteUrl(o.toUrlAndQuery()); + + mergeOptions(data.options, options); + + return operation(Object.assign({}, data, { + batchDependency, + url, + })); + }; +}; + +export const spGet = (o: ISharePointQueryable, options?: IFetchOptions): Promise => { + // Fix for #304 - when we clone objects we in some cases then execute a get request + // in these cases the caching settings were getting dropped from the request + // this tracks if the object from which this was cloned was caching and applies that to an immediate get request + // does not affect objects cloned from this as we are using different fields to track the settings so it won't + // be triggered + if (o.data.cloneParentWasCaching) { + o.usingCaching(o.data.cloneParentCacheOptions); + } + + // if we are forcing caching set that in the data here + if ((o)._forceCaching) { + o.data.useCaching = true; + } + + return send(spClientBinder("GET"))(o, options); +}; + +export const spPost = (o: ISharePointQueryable, options?: IFetchOptions): Promise => send(spClientBinder("POST"))(o, options); + +export const spDelete = (o: ISharePointQueryable, options?: IFetchOptions): Promise => send(spClientBinder("DELETE"))(o, options); + +export const spPatch = (o: ISharePointQueryable, options?: IFetchOptions): Promise => send(spClientBinder("PATCH"))(o, options); + +export const spPostDelete = (o: ISharePointQueryable, options?: IFetchOptions): Promise => { + const opts = Object.assign(headers({ "X-HTTP-Method": "DELETE" }), options); + return send(spClientBinder("POST"))(o, opts); +}; + +export const spPostDeleteETag = (o: ISharePointQueryable, options?: IFetchOptions, eTag = "*"): Promise => { + const opts = Object.assign(headers({ "X-HTTP-Method": "DELETE", "IF-Match": eTag }), options); + return send(spClientBinder("POST"))(o, opts); +}; diff --git a/packages/sp/src/profiles/index.ts b/packages/sp/src/profiles/index.ts new file mode 100644 index 000000000..cda7b0481 --- /dev/null +++ b/packages/sp/src/profiles/index.ts @@ -0,0 +1,33 @@ +import { SPRest } from "../rest"; +import { Profiles, IProfiles } from "./types"; + +export { + IProfiles, + Profiles, + IClientPeoplePickerQueryParameters, + IFollowedContent, + IHashTag, + IHashTagCollection, + IPeoplePickerEntity, + IPeoplePickerEntityData, + IPeoplePickerQuerySettings, + IUserProfile, + UrlZone, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly profiles: IProfiles; + } +} + +Reflect.defineProperty(SPRest.prototype, "profiles", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return Profiles(this._baseUrl); + }, +}); diff --git a/packages/sp/src/profiles/types.ts b/packages/sp/src/profiles/types.ts new file mode 100644 index 000000000..5a1a2bd82 --- /dev/null +++ b/packages/sp/src/profiles/types.ts @@ -0,0 +1,758 @@ +import { + _SharePointQueryableInstance, + SharePointQueryableCollection, + ISharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryable, + ISharePointQueryable, + spInvokableFactory, +} from "../sharepointqueryable"; +import { extend } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { IGetable, body } from "@pnp/odata"; +import { PrincipalType, PrincipalSource } from "../types"; +import { defaultPath } from "../decorators"; +import { spPost } from "../operations"; + +export class _Profiles extends _SharePointQueryableInstance implements IProfiles { + + private clientPeoplePickerQuery: ClientPeoplePickerQuery; + private profileLoader: ProfileLoader; + + /** + * Creates a new instance of the UserProfileQuery class + * + * @param baseUrl The url or SharePointQueryable which forms the parent of this user profile query + */ + constructor(baseUrl: string | ISharePointQueryable, path = "_api/sp.userprofiles.peoplemanager") { + super(baseUrl, path); + + this.clientPeoplePickerQuery = (new ClientPeoplePickerQuery(baseUrl)).configureFrom(this); + this.profileLoader = (new ProfileLoader(baseUrl)).configureFrom(this); + } + + /** + * The url of the edit profile page for the current user + */ + public get editProfileLink(): Promise { + return this.clone(Profiles, "EditProfileLink").get(); + } + + /** + * A boolean value that indicates whether the current user's "People I'm Following" list is public + */ + public get isMyPeopleListPublic(): Promise { + return this.clone(Profiles, "IsMyPeopleListPublic").get(); + } + + /** + * A boolean value that indicates whether the current user is being followed by the specified user + * + * @param loginName The account name of the user + */ + public amIFollowedBy(loginName: string): Promise { + const q = this.clone(Profiles, "amifollowedby(@v)"); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return q.get(); + } + + /** + * A boolean value that indicates whether the current user is following the specified user + * + * @param loginName The account name of the user + */ + public amIFollowing(loginName: string): Promise { + const q = this.clone(Profiles, "amifollowing(@v)"); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return q.get(); + } + + /** + * Gets tags that the current user is following + * + * @param maxCount The maximum number of tags to retrieve (default is 20) + */ + public getFollowedTags(maxCount = 20): Promise { + return this.clone(Profiles, `getfollowedtags(${maxCount})`).get(); + } + + /** + * Gets the people who are following the specified user + * + * @param loginName The account name of the user + */ + public getFollowersFor(loginName: string): Promise { + const q = this.clone(Profiles, "getfollowersfor(@v)"); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return q.get(); + } + + /** + * Gets the people who are following the current user + * + */ + public get myFollowers(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "getmyfollowers"); + } + + /** + * Gets user properties for the current user + * + */ + public get myProperties(): _SharePointQueryableInstance { + return new _Profiles(this, "getmyproperties"); + } + + /** + * Gets the people who the specified user is following + * + * @param loginName The account name of the user. + */ + public getPeopleFollowedBy(loginName: string): Promise { + const q = this.clone(Profiles, "getpeoplefollowedby(@v)"); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return q.get(); + } + + /** + * Gets user properties for the specified user. + * + * @param loginName The account name of the user. + */ + public getPropertiesFor(loginName: string): Promise { + const q = this.clone(Profiles, "getpropertiesfor(@v)"); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return q.get(); + } + + /** + * Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first + * + */ + public get trendingTags(): Promise { + const q = this.clone(Profiles, null); + q.concat(".gettrendingtags"); + return q.get(); + } + + /** + * Gets the specified user profile property for the specified user + * + * @param loginName The account name of the user + * @param propertyName The case-sensitive name of the property to get + */ + public getUserProfilePropertyFor(loginName: string, propertyName: string): Promise { + const q = this.clone(Profiles, `getuserprofilepropertyfor(accountname=@v, propertyname='${propertyName}')`); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return q.get(); + } + + /** + * Removes the specified user from the user's list of suggested people to follow + * + * @param loginName The account name of the user + */ + public hideSuggestion(loginName: string): Promise { + const q = this.clone(Profiles, "hidesuggestion(@v)"); + q.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return spPost(q); + } + + /** + * A boolean values that indicates whether the first user is following the second user + * + * @param follower The account name of the user who might be following the followee + * @param followee The account name of the user who might be followed by the follower + */ + public isFollowing(follower: string, followee: string): Promise { + const q = this.clone(Profiles, null); + q.concat(`.isfollowing(possiblefolloweraccountname=@v, possiblefolloweeaccountname=@y)`); + q.query.set("@v", `'${encodeURIComponent(follower)}'`); + q.query.set("@y", `'${encodeURIComponent(followee)}'`); + return q.get(); + } + + /** + * Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. + * + * @param profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB + */ + public setMyProfilePic(profilePicSource: Blob): Promise { + let buffer: any = null; + const reader = new FileReader(); + reader.onload = (e: any) => buffer = e.target.result; + reader.readAsArrayBuffer(profilePicSource); + const request = new _Profiles(this, "setmyprofilepicture"); + return spPost(request, body(String.fromCharCode.apply(null, new Uint16Array(buffer)))); + } + + /** + * Sets single value User Profile property + * + * @param accountName The account name of the user + * @param propertyName Property name + * @param propertyValue Property value + */ + public setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise { + + return spPost(this.clone(Profiles, "SetSingleValueProfileProperty"), body({ + accountName: accountName, + propertyName: propertyName, + propertyValue: propertyValue, + })); + } + + /** + * Sets multi valued User Profile property + * + * @param accountName The account name of the user + * @param propertyName Property name + * @param propertyValues Property values + */ + public setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise { + + return spPost(this.clone(Profiles, "SetMultiValuedProfileProperty"), body({ + accountName: accountName, + propertyName: propertyName, + propertyValues: propertyValues, + })); + } + + /** + * Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) + * + * @param emails The email addresses of the users to provision sites for + */ + public createPersonalSiteEnqueueBulk(...emails: string[]): Promise { + return this.profileLoader.createPersonalSiteEnqueueBulk(emails); + } + + /** + * Gets the user profile of the site owner + * + */ + public get ownerUserProfile(): Promise { + return this.profileLoader.ownerUserProfile; + } + + /** + * Gets the user profile for the current user + */ + public get userProfile(): Promise { + return this.profileLoader.userProfile; + } + + /** + * Enqueues creating a personal site for this user, which can be used to share documents, web pages, and other files + * + * @param interactiveRequest true if interactively (web) initiated request, or false (default) if non-interactively (client) initiated request + */ + public createPersonalSite(interactiveRequest = false): Promise { + return this.profileLoader.createPersonalSite(interactiveRequest); + } + + /** + * Sets the privacy settings for this profile + * + * @param share true to make all social data public; false to make all social data private + */ + public shareAllSocialData(share: boolean): Promise { + return this.profileLoader.shareAllSocialData(share); + } + + /** + * Resolves user or group using specified query parameters + * + * @param queryParams The query parameters used to perform resolve + */ + public clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise { + return this.clientPeoplePickerQuery.clientPeoplePickerResolveUser(queryParams); + } + + /** + * Searches for users or groups using specified query parameters + * + * @param queryParams The query parameters used to perform search + */ + public clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise { + return this.clientPeoplePickerQuery.clientPeoplePickerSearchUser(queryParams); + } +} + +export interface IProfiles extends IGetable, ISharePointQueryableInstance { + readonly editProfileLink: Promise; + + /** + * A boolean value that indicates whether the current user's "People I'm Following" list is public + */ + readonly isMyPeopleListPublic: Promise; + + /** + * Gets the people who are following the current user + * + */ + readonly myFollowers: ISharePointQueryableCollection; + + /** + * Gets user properties for the current user + * + */ + readonly myProperties: ISharePointQueryableInstance; + + /** + * Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first + * + */ + readonly trendingTags: Promise; + + /** + * Gets the user profile of the site owner + * + */ + readonly ownerUserProfile: Promise; + + /** + * Gets the user profile for the current user + */ + readonly userProfile: Promise; + + /** + * A boolean value that indicates whether the current user is being followed by the specified user + * + * @param loginName The account name of the user + */ + amIFollowedBy(loginName: string): Promise; + + /** + * A boolean value that indicates whether the current user is following the specified user + * + * @param loginName The account name of the user + */ + amIFollowing(loginName: string): Promise; + + /** + * Gets tags that the current user is following + * + * @param maxCount The maximum number of tags to retrieve (default is 20) + */ + getFollowedTags(maxCount?: number): Promise; + + /** + * Gets the people who are following the specified user + * + * @param loginName The account name of the user + */ + getFollowersFor(loginName: string): Promise; + + /** + * Gets the people who the specified user is following + * + * @param loginName The account name of the user. + */ + getPeopleFollowedBy(loginName: string): Promise; + + /** + * Gets user properties for the specified user. + * + * @param loginName The account name of the user. + */ + getPropertiesFor(loginName: string): Promise; + + /** + * Gets the specified user profile property for the specified user + * + * @param loginName The account name of the user + * @param propertyName The case-sensitive name of the property to get + */ + getUserProfilePropertyFor(loginName: string, propertyName: string): Promise; + + /** + * Removes the specified user from the user's list of suggested people to follow + * + * @param loginName The account name of the user + */ + hideSuggestion(loginName: string): Promise; + + /** + * A boolean values that indicates whether the first user is following the second user + * + * @param follower The account name of the user who might be following the followee + * @param followee The account name of the user who might be followed by the follower + */ + isFollowing(follower: string, followee: string): Promise; + + /** + * Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. + * + * @param profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB + */ + setMyProfilePic(profilePicSource: Blob): Promise; + + /** + * Sets single value User Profile property + * + * @param accountName The account name of the user + * @param propertyName Property name + * @param propertyValue Property value + */ + setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise; + + /** + * Sets multi valued User Profile property + * + * @param accountName The account name of the user + * @param propertyName Property name + * @param propertyValues Property values + */ + setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise; + + /** + * Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) + * + * @param emails The email addresses of the users to provision sites for + */ + createPersonalSiteEnqueueBulk(...emails: string[]): Promise; + + /** + * Enqueues creating a personal site for this user, which can be used to share documents, web pages, and other files + * + * @param interactiveRequest true if interactively (web) initiated request, or false (default) if non-interactively (client) initiated request + */ + createPersonalSite(interactiveRequest?: boolean): Promise; + + /** + * Sets the privacy settings for this profile + * + * @param share true to make all social data public; false to make all social data private + */ + shareAllSocialData(share: boolean): Promise; + + /** + * Resolves user or group using specified query parameters + * + * @param queryParams The query parameters used to perform resolve + */ + clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise; + + /** + * Searches for users or groups using specified query parameters + * + * @param queryParams The query parameters used to perform search + */ + clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise; +} +export interface _Profiles extends IGetable { } +export const Profiles = spInvokableFactory(_Profiles); + + +@defaultPath("_api/sp.userprofiles.profileloader.getprofileloader") +class ProfileLoader extends _SharePointQueryable { + + /** + * Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Doesn't support batching + * + * @param emails The email addresses of the users to provision sites for + */ + public createPersonalSiteEnqueueBulk(emails: string[]): Promise { + + return spPost(this.clone(ProfileLoaderFactory, "createpersonalsiteenqueuebulk", false), body({ "emailIDs": emails })); + } + + /** + * Gets the user profile of the site owner. + * + */ + public get ownerUserProfile(): Promise { + let q = this.getParent(ProfileLoader, this.parentUrl, "_api/sp.userprofiles.profileloader.getowneruserprofile"); + + if (this.hasBatch) { + q = q.inBatch(this.batch); + } + + return spPost(q); + } + + /** + * Gets the user profile of the current user. + * + */ + public get userProfile(): Promise { + return spPost(this.clone(ProfileLoaderFactory, "getuserprofile")); + } + + /** + * Enqueues creating a personal site for this user, which can be used to share documents, web pages, and other files. + * + * @param interactiveRequest true if interactively (web) initiated request, or false (default) if non-interactively (client) initiated request + */ + public createPersonalSite(interactiveRequest = false): Promise { + return spPost(this.clone(ProfileLoaderFactory, `getuserprofile/createpersonalsiteenque(${interactiveRequest})`)); + } + + /** + * Sets the privacy settings for this profile + * + * @param share true to make all social data public; false to make all social data private. + */ + public shareAllSocialData(share: boolean): Promise { + return spPost(this.clone(ProfileLoaderFactory, `getuserprofile/shareallsocialdata(${share})`)); + } +} + +const ProfileLoaderFactory = (baseUrl: string | ISharePointQueryable, path?: string) => { + return new ProfileLoader(baseUrl, path); +}; + +@defaultPath("_api/sp.ui.applicationpages.clientpeoplepickerwebserviceinterface") +class ClientPeoplePickerQuery extends _SharePointQueryable { + + /** + * Resolves user or group using specified query parameters + * + * @param queryParams The query parameters used to perform resolve + */ + public async clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise { + const q = this.clone(ClientPeoplePickerFactory, null); + q.concat(".clientpeoplepickerresolveuser"); + const res = await spPost(q, this.getBodyFrom(queryParams)); + + return JSON.parse(typeof res === "object" ? res.ClientPeoplePickerResolveUser : res); + } + + /** + * Searches for users or groups using specified query parameters + * + * @param queryParams The query parameters used to perform search + */ + public async clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise { + const q = this.clone(ClientPeoplePickerFactory, null); + q.concat(".clientpeoplepickersearchuser"); + const res = await spPost(q, this.getBodyFrom(queryParams)); + + return JSON.parse(typeof res === "object" ? res.ClientPeoplePickerSearchUser : res); + } + + /** + * Creates ClientPeoplePickerQueryParameters request body + * + * @param queryParams The query parameters to create request body + */ + private getBodyFrom(queryParams: IClientPeoplePickerQueryParameters): { body: string } { + return body({ "queryParams": extend(metadata("SP.UI.ApplicationPages.ClientPeoplePickerQueryParameters"), queryParams) }); + } +} + +const ClientPeoplePickerFactory = (baseUrl: string | ISharePointQueryable, path?: string) => { + return new ClientPeoplePickerQuery(baseUrl, path); +}; + +/** + * Client people picker query parameters + */ +export interface IClientPeoplePickerQueryParameters { + /** + * Gets or sets a value that specifies whether e-mail addresses can be used to perform search. + */ + AllowEmailAddresses?: boolean; + /** + * Gets or sets a value that specifies whether multiple entities are allowed. + */ + AllowMultipleEntities?: boolean; + /** + * Gets or sets a value that specifies whether only e-mail addresses can be used to perform search. + */ + AllowOnlyEmailAddresses?: boolean; + /** + * Gets or sets a value that specifies whether all URL zones are used to perform search. + */ + AllUrlZones?: boolean; + /** + * Gets or sets a value that specifies claim providers that are used to perform search. + */ + EnabledClaimProviders?: string; + /** + * Gets or sets a value that specifies whether claims are forced (if yes, multiple results for single entity can be returned). + */ + ForceClaims?: boolean; + /** + * Gets or sets a value that specifies limit of results returned. + */ + MaximumEntitySuggestions: number; + /** + * Gets or sets a value that specifies principal sources to perform search. + */ + PrincipalSource?: PrincipalSource; + /** + * Gets or sets a value that specifies principal types to search for. + */ + PrincipalType?: PrincipalType; + /** + * Gets or sets a value that specifies additional query settings. + */ + QuerySettings?: IPeoplePickerQuerySettings; + /** + * Gets or sets a value that specifies the term to search for. + */ + QueryString: string; + /** + * Gets or sets a value that specifies ID of the SharePoint Group that will be used to perform search. + */ + SharePointGroupID?: number; + /** + * Gets or sets a value that specifies URL zones that are used to perform search. + */ + UrlZone?: UrlZone; + /** + * Gets or sets a value that specifies whether search is limited to specific URL zone. + */ + UrlZoneSpecified?: boolean; + /** + * Gets or sets a value that specifies GUID of the Web Application that is used to perform search. + */ + WebApplicationID?: string; +} + +export interface IHashTagCollection { + Items: IHashTag[]; +} + +/** + * People picker query settings + */ +export interface IPeoplePickerQuerySettings { + ExcludeAllUsersOnTenantClaim?: boolean; +} + +/** + * People picker entity + */ +export interface IPeoplePickerEntity { + Description: string; + DisplayText: string; + EntityData: IPeoplePickerEntityData; + EntityType: string; + IsResolved: boolean; + Key: string; + MultipleMatches: IPeoplePickerEntityData[]; + ProviderDisplayName: string; + ProviderName: string; +} + +/** + * People picker entity data + */ +export interface IPeoplePickerEntityData { + AccountName?: string; + Department?: string; + Email?: string; + IsAltSecIdPresent?: string; + MobilePhone?: string; + ObjectId?: string; + OtherMails?: string; + PrincipalType?: string; + SPGroupID?: string; + SPUserID?: string; + Title?: string; +} + +/** + * Specifies the originating zone of a request received. + */ +export const enum UrlZone { + /** + * Specifies the default zone used for requests unless another zone is specified. + */ + DefaultZone, + /** + * Specifies an intranet zone. + */ + Intranet, + /** + * Specifies an Internet zone. + */ + Internet, + /** + * Specifies a custom zone. + */ + Custom, + /** + * Specifies an extranet zone. + */ + Extranet, +} + +export interface IHashTag { + /** + * The hash tag's internal name. + */ + Name?: string; + /** + * The number of times that the hash tag is used. + */ + UseCount?: number; +} + +export interface IFollowedContent { + FollowedDocumentsUrl: string; + FollowedSitesUrl: string; +} + +export interface IUserProfile { + /** + * An object containing the user's FollowedDocumentsUrl and FollowedSitesUrl. + */ + FollowedContent?: IFollowedContent; + /** + * The account name of the user. (SharePoint Online only) + */ + AccountName?: string; + /** + * The display name of the user. (SharePoint Online only) + */ + DisplayName?: string; + /** + * The FirstRun flag of the user. (SharePoint Online only) + */ + O15FirstRunExperience?: number; + /** + * The personal site of the user. + */ + PersonalSite?: string; + /** + * The capabilities of the user's personal site. Represents a bitwise PersonalSiteCapabilities value: + * None = 0; Profile Value = 1; Social Value = 2; Storage Value = 4; MyTasksDashboard Value = 8; Education Value = 16; Guest Value = 32. + */ + PersonalSiteCapabilities?: number; + /** + * The error thrown when the user's personal site was first created, if any. (SharePoint Online only) + */ + PersonalSiteFirstCreationError?: string; + /** + * The date and time when the user's personal site was first created. (SharePoint Online only) + */ + PersonalSiteFirstCreationTime?: Date; + /** + * The status for the state of the personal site instantiation + */ + PersonalSiteInstantiationState?: number; + /** + * The date and time when the user's personal site was last created. (SharePoint Online only) + */ + PersonalSiteLastCreationTime?: Date; + /** + * The number of attempts made to create the user's personal site. (SharePoint Online only) + */ + PersonalSiteNumberOfRetries?: number; + /** + * Indicates whether the user's picture is imported from Exchange. + */ + PictureImportEnabled?: boolean; + /** + * The public URL of the personal site of the current user. (SharePoint Online only) + */ + PublicUrl?: string; + /** + * The URL used to create the user's personal site. + */ + UrlToCreatePersonalSite?: string; +} diff --git a/packages/sp/src/regional-settings/index.ts b/packages/sp/src/regional-settings/index.ts new file mode 100644 index 000000000..24f26444b --- /dev/null +++ b/packages/sp/src/regional-settings/index.ts @@ -0,0 +1,10 @@ +import "./web"; + +export { + IRegionalSettings, + ITimeZone, + ITimeZones, + RegionalSettings, + TimeZone, + TimeZones, +} from "./types"; diff --git a/packages/sp/src/regional-settings/types.ts b/packages/sp/src/regional-settings/types.ts new file mode 100644 index 000000000..abd245e90 --- /dev/null +++ b/packages/sp/src/regional-settings/types.ts @@ -0,0 +1,131 @@ +import { + _SharePointQueryableInstance, + SharePointQueryableCollection, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { defaultPath } from "../decorators"; +import { spODataEntity } from "../odata"; +import { dateAdd, hOP } from "@pnp/common"; +import { IGetable } from "@pnp/odata"; +import { spPost } from "../operations"; + +/** + * Describes regional settings ODada object + */ +@defaultPath("regionalsettings") +export class _RegionalSettings extends _SharePointQueryableInstance implements IRegionalSettings { + /** + * Gets the collection of languages used in a server farm. + */ + public get installedLanguages(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "installedlanguages"); + } + + /** + * Gets the collection of language packs that are installed on the server. + */ + public get globalInstalledLanguages(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "globalinstalledlanguages"); + } + + /** + * Gets time zone + */ + public get timeZone(): ITimeZone { + return TimeZone(this); + } + + /** + * Gets time zones + */ + public get timeZones(): ITimeZones { + return TimeZones(this); + } +} + +export interface IRegionalSettings extends IGetable, ISharePointQueryableInstance { + readonly installedLanguages: ISharePointQueryableCollection; + readonly globalInstalledLanguages: ISharePointQueryableCollection; + readonly timeZone: ITimeZone; + readonly timeZones: ITimeZones; +} +export interface _RegionalSettings extends IGetable { } +export const RegionalSettings = spInvokableFactory(_RegionalSettings); + +/** + * Describes TimeZone ODada object + */ +@defaultPath("timezone") +export class _TimeZone extends _SharePointQueryableInstance { + /** + * Gets an Local Time by UTC Time + * + * @param utcTime UTC Time as Date or ISO String + */ + public async utcToLocalTime(utcTime: string | Date): Promise { + + let dateIsoString: string; + + if (typeof utcTime === "string") { + dateIsoString = utcTime; + } else { + dateIsoString = utcTime.toISOString(); + } + + const res = await spPost(this.clone(TimeZone, `utctolocaltime('${dateIsoString}')`)); + return hOP(res, "UTCToLocalTime") ? res.UTCToLocalTime : res; + } + + /** + * Gets an UTC Time by Local Time + * + * @param localTime Local Time as Date or ISO String + */ + public async localTimeToUTC(localTime: string | Date): Promise { + + let dateIsoString: string; + + if (typeof localTime === "string") { + dateIsoString = localTime; + } else { + dateIsoString = dateAdd(localTime, "minute", localTime.getTimezoneOffset() * -1).toISOString(); + } + + const res = await spPost(this.clone(TimeZone, `localtimetoutc('${dateIsoString}')`)); + + return hOP(res, "LocalTimeToUTC") ? res.LocalTimeToUTC : res; + } +} + +export interface ITimeZone extends IGetable, ISharePointQueryableInstance { + utcToLocalTime(utcTime: string | Date): Promise; + localTimeToUTC(localTime: string | Date): Promise; +} +export interface _TimeZone extends IGetable { } +export const TimeZone = spInvokableFactory(_TimeZone); + +/** + * Describes time zones queriable collection + */ +@defaultPath("timezones") +export class _TimeZones extends _SharePointQueryableCollection implements ITimeZones { + // https://msdn.microsoft.com/en-us/library/office/jj247008.aspx - timezones ids + /** + * Gets an TimeZone by id + * + * @param id The integer id of the timezone to retrieve + */ + public getById(id: number): Promise { + // do the post and merge the result into a TimeZone instance so the data and methods are available + return spPost(this.clone(TimeZones, `GetById(${id})`).usingParser(spODataEntity(_TimeZone))); + } +} + +export interface ITimeZones extends IGetable, ISharePointQueryableInstance { + getById(id: number): Promise; +} +export interface _TimeZones extends IGetable { } +export const TimeZones = spInvokableFactory(_TimeZones); diff --git a/packages/sp/src/regional-settings/web.ts b/packages/sp/src/regional-settings/web.ts new file mode 100644 index 000000000..c1fe37764 --- /dev/null +++ b/packages/sp/src/regional-settings/web.ts @@ -0,0 +1,18 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { RegionalSettings, IRegionalSettings } from "./types"; + +declare module "../webs/types" { + interface _Web { + regionalSettings: IRegionalSettings; + } + interface IWeb { + + /** + * Regional settings for this web + */ + regionalSettings: IRegionalSettings; + } +} + +addProp(_Web, "regionalSettings", RegionalSettings); diff --git a/packages/sp/src/related-items/index.ts b/packages/sp/src/related-items/index.ts new file mode 100644 index 000000000..7ce1fd56e --- /dev/null +++ b/packages/sp/src/related-items/index.ts @@ -0,0 +1,5 @@ +export { + IRelatedItem, + IRelatedItemManager, + RelatedItemManager, +} from "./types"; diff --git a/packages/sp/src/related-items/types.ts b/packages/sp/src/related-items/types.ts new file mode 100644 index 000000000..8af2db7ac --- /dev/null +++ b/packages/sp/src/related-items/types.ts @@ -0,0 +1,163 @@ +import { _SharePointQueryable } from "../sharepointqueryable"; +import { extractWebUrl } from "../utils/extractweburl"; +import { defaultPath } from "../decorators"; +import { spPost } from "../operations"; +import { body } from "@pnp/odata"; + +@defaultPath("_api/SP.RelatedItemManager") +export class _RelatedItemManager extends _SharePointQueryable implements IRelatedItemManager { + + public getRelatedItems(sourceListName: string, sourceItemId: number): Promise { + + const query = this.clone(RelatedItemManager, null); + query.concat(".GetRelatedItems"); + + return spPost(query, body({ + SourceItemID: sourceItemId, + SourceListName: sourceListName, + })); + } + + public getPageOneRelatedItems(sourceListName: string, sourceItemId: number): Promise { + + const query = this.clone(RelatedItemManager, null); + query.concat(".GetPageOneRelatedItems"); + + return spPost(query, body({ + SourceItemID: sourceItemId, + SourceListName: sourceListName, + })); + } + + public addSingleLink(sourceListName: string, + sourceItemId: number, + sourceWebUrl: string, + targetListName: string, + targetItemID: number, + targetWebUrl: string, + tryAddReverseLink = false): Promise { + + const query = this.clone(RelatedItemManager, null); + query.concat(".AddSingleLink"); + + return spPost(query, body({ + SourceItemID: sourceItemId, + SourceListName: sourceListName, + SourceWebUrl: sourceWebUrl, + TargetItemID: targetItemID, + TargetListName: targetListName, + TargetWebUrl: targetWebUrl, + TryAddReverseLink: tryAddReverseLink, + })); + } + + public addSingleLinkToUrl(sourceListName: string, sourceItemId: number, targetItemUrl: string, tryAddReverseLink = false): Promise { + + const query = this.clone(RelatedItemManager, null); + query.concat(".AddSingleLinkToUrl"); + + return spPost(query, body({ + SourceItemID: sourceItemId, + SourceListName: sourceListName, + TargetItemUrl: targetItemUrl, + TryAddReverseLink: tryAddReverseLink, + })); + } + + /** + * Adds a related item link from an item specified by url, to an item specified by list name and item id + * + * @param sourceItemUrl The source item url + * @param targetListName The target list name or list id + * @param targetItemId The target item id + * @param tryAddReverseLink If set to true try to add the reverse link (will not return error if it fails) + */ + public addSingleLinkFromUrl(sourceItemUrl: string, targetListName: string, targetItemId: number, tryAddReverseLink = false): Promise { + + const query = this.clone(RelatedItemManager, null); + query.concat(".AddSingleLinkFromUrl"); + + return spPost(query, body({ + SourceItemUrl: sourceItemUrl, + TargetItemID: targetItemId, + TargetListName: targetListName, + TryAddReverseLink: tryAddReverseLink, + })); + } + + public deleteSingleLink(sourceListName: string, + sourceItemId: number, + sourceWebUrl: string, + targetListName: string, + targetItemId: number, + targetWebUrl: string, + tryDeleteReverseLink = false): Promise { + + const query = this.clone(RelatedItemManager, null); + query.concat(".DeleteSingleLink"); + + return spPost(query, body({ + SourceItemID: sourceItemId, + SourceListName: sourceListName, + SourceWebUrl: sourceWebUrl, + TargetItemID: targetItemId, + TargetListName: targetListName, + TargetWebUrl: targetWebUrl, + TryDeleteReverseLink: tryDeleteReverseLink, + })); + } +} + +export interface IRelatedItemManager { + + getRelatedItems(sourceListName: string, sourceItemId: number): Promise; + + getPageOneRelatedItems(sourceListName: string, sourceItemId: number): Promise; + + addSingleLink(sourceListName: string, + sourceItemId: number, + sourceWebUrl: string, + targetListName: string, + targetItemID: number, + targetWebUrl: string, + tryAddReverseLink?: boolean): Promise; + + /** + * Adds a related item link from an item specified by list name and item id, to an item specified by url + * + * @param sourceListName The source list name or list id + * @param sourceItemId The source item id + * @param targetItemUrl The target item url + * @param tryAddReverseLink If set to true try to add the reverse link (will not return error if it fails) + */ + addSingleLinkToUrl(sourceListName: string, sourceItemId: number, targetItemUrl: string, tryAddReverseLink?: boolean): Promise; + + /** + * Adds a related item link from an item specified by url, to an item specified by list name and item id + * + * @param sourceItemUrl The source item url + * @param targetListName The target list name or list id + * @param targetItemId The target item id + * @param tryAddReverseLink If set to true try to add the reverse link (will not return error if it fails) + */ + addSingleLinkFromUrl(sourceItemUrl: string, targetListName: string, targetItemId: number, tryAddReverseLink?: boolean): Promise; + + deleteSingleLink(sourceListName: string, + sourceItemId: number, + sourceWebUrl: string, + targetListName: string, + targetItemId: number, + targetWebUrl: string, + tryDeleteReverseLink?: boolean): Promise; +} + +export const RelatedItemManager = (url: string): IRelatedItemManager => new _RelatedItemManager(extractWebUrl(url)); + +export interface IRelatedItem { + ListId: string; + ItemId: number; + Url: string; + Title: string; + WebId: string; + IconUrl: string; +} diff --git a/packages/sp/src/related-items/web.ts b/packages/sp/src/related-items/web.ts new file mode 100644 index 000000000..f9878a6c8 --- /dev/null +++ b/packages/sp/src/related-items/web.ts @@ -0,0 +1,23 @@ +import { _Web } from "../webs/types"; +import { RelatedItemManager, IRelatedItemManager } from "./types"; + +declare module "../webs/types" { + interface _Web { + relatedItems: IRelatedItemManager; + } + interface IWeb { + + /** + * The related items manager associated with this web + */ + relatedItems: IRelatedItemManager; + } +} + +Reflect.defineProperty(_Web.prototype, "relatedItems", { + configurable: true, + enumerable: true, + get: function (this: _Web) { + return RelatedItemManager(this.toUrl()); + }, +}); diff --git a/packages/sp/src/rest.ts b/packages/sp/src/rest.ts new file mode 100644 index 000000000..ee9c898fe --- /dev/null +++ b/packages/sp/src/rest.ts @@ -0,0 +1,41 @@ +import { IConfigOptions } from "@pnp/common"; +import { + setup as _setup, + SPConfiguration, +} from "./config/splibconfig"; + +/** + * Root of the SharePoint REST module + */ +export class SPRest { + + /** + * Creates a new instance of the SPRest class + * + * @param options Additional options + * @param baseUrl A string that should form the base part of the url + */ + constructor(protected _options: IConfigOptions = {}, protected _baseUrl = "") { } + + /** + * Configures instance with additional options and baseUrl. + * Provided configuration used by other objects in a chain + * + * @param options Additional options + * @param baseUrl A string that should form the base part of the url + */ + public configure(options: IConfigOptions, baseUrl = ""): SPRest { + return new SPRest(options, baseUrl); + } + + /** + * Global SharePoint configuration options + * + * @param config The SharePoint configuration to apply + */ + public setup(config: SPConfiguration) { + _setup(config); + } +} + +export const sp = new SPRest(); diff --git a/packages/sp/src/search/index.ts b/packages/sp/src/search/index.ts new file mode 100644 index 000000000..05fd183bb --- /dev/null +++ b/packages/sp/src/search/index.ts @@ -0,0 +1,57 @@ +import { SPRest } from "../rest"; +import { SearchQueryInit } from "./types"; +import { _Search } from "./query"; +import { ICachingOptions } from "@pnp/odata"; +import { SearchResults, SearchFactory } from "./query"; +import { ISearchSuggestQuery, ISearchSuggestResult, SuggestFactory } from "./suggest"; + +export * from "./types"; + +export { + ISearch, + Search, + SearchFactory, + SearchQueryBuilder, + SearchResults, +} from "./query"; + +export { + ISuggest, + IPersonalResultSuggestion, + ISearchSuggestQuery, + ISearchSuggestResult, + Suggest, + SuggestFactory, +} from "./suggest"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + search(query: SearchQueryInit): Promise; + searchWithCaching(query: SearchQueryInit, options?: ICachingOptions): Promise; + searchSuggest(query: string | ISearchSuggestQuery): Promise; + } +} + +SPRest.prototype.search = function (this: SPRest, query: SearchQueryInit): Promise { + return SearchFactory(this._baseUrl, null, this._options)(query); +}; + +SPRest.prototype.searchWithCaching = function (this: SPRest, query: SearchQueryInit, options?: ICachingOptions): Promise { + return (new _Search(this._baseUrl, null)).configure(this._options).usingCaching(options).execute(query); +}; + +SPRest.prototype.searchSuggest = function (this: SPRest, query: string | ISearchSuggestQuery): Promise { + + let finalQuery: ISearchSuggestQuery; + + if (typeof query === "string") { + finalQuery = { querytext: query }; + } else { + finalQuery = query; + } + + return SuggestFactory(this._baseUrl, null, this._options)(finalQuery); +}; diff --git a/packages/sp/src/search/query.ts b/packages/sp/src/search/query.ts new file mode 100644 index 000000000..ce515a972 --- /dev/null +++ b/packages/sp/src/search/query.ts @@ -0,0 +1,289 @@ +import { _SharePointQueryableInstance, ISharePointQueryable } from "../sharepointqueryable"; +import { extend, hOP, getHashCode, objectDefinedNotNull, isArray, IConfigOptions } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { CachingOptions, body } from "@pnp/odata"; +import { ISearchQuery, ISearchResponse, ISearchResult, ISearchQueryBuilder, SearchQueryInit } from "./types"; +import { spPost } from "../operations"; +import { defaultPath } from "../decorators"; + +const funcs = new Map([ + ["text", "Querytext"], + ["template", "QueryTemplate"], + ["sourceId", "SourceId"], + ["trimDuplicatesIncludeId", ""], + ["startRow", ""], + ["rowLimit", ""], + ["rankingModelId", ""], + ["rowsPerPage", ""], + ["selectProperties", ""], + ["culture", ""], + ["timeZoneId", ""], + ["refinementFilters", ""], + ["refiners", ""], + ["hiddenConstraints", ""], + ["sortList", ""], + ["timeout", ""], + ["hithighlightedProperties", ""], + ["clientType", ""], + ["personalizationData", ""], + ["resultsURL", ""], + ["queryTag", ""], + ["properties", ""], + ["queryTemplatePropertiesUrl", ""], + ["reorderingRules", ""], + ["hitHighlightedMultivaluePropertyLimit", ""], + ["collapseSpecification", ""], + ["uiLanguage", ""], + ["desiredSnippetLength", ""], + ["maxSnippetLength", ""], + ["summaryLength", ""], +]); + +const props = new Map([]); + +function toPropCase(str: string) { + return str.replace(/^(.)/, ($1) => $1.toUpperCase()); +} + +/** + * Creates a new instance of the SearchQueryBuilder + * + * @param queryText Initial query text + * @param _query Any initial query configuration + */ +export function SearchQueryBuilder(queryText = "", _query = {}): ISearchQueryBuilder { + + return new Proxy({ + query: Object.assign({ + Querytext: queryText, + }, _query), + }, + { + get(self, propertyKey, proxy) { + + const pk = propertyKey.toString(); + + if (pk === "toSearchQuery") { + return () => self.query; + } + + if (funcs.has(pk)) { + return (...value: any[]) => { + const mappedPk = funcs.get(pk); + self.query[mappedPk.length > 0 ? mappedPk : toPropCase(pk)] = value.length > 1 ? value : value[0]; + return proxy; + }; + } + const propKey = props.has(pk) ? props.get(pk) : toPropCase(pk); + self.query[propKey] = true; + return proxy; + }, + }); +} + +/** + * Describes the search API + * + */ +@defaultPath("_api/search/postquery") +export class _Search extends _SharePointQueryableInstance { + + /** + * @returns Promise + */ + public async execute(queryInit: SearchQueryInit): Promise { + + const query = this.parseQuery(queryInit); + + const postBody = body({ + request: extend( + metadata("Microsoft.Office.Server.Search.REST.SearchRequest"), + Object.assign( + {}, + query, + { + HitHighlightedProperties: this.fixArrProp(query.HitHighlightedProperties), + Properties: this.fixArrProp(query.Properties), + RefinementFilters: this.fixArrProp(query.RefinementFilters), + ReorderingRules: this.fixArrProp(query.ReorderingRules), + SelectProperties: this.fixArrProp(query.SelectProperties), + SortList: this.fixArrProp(query.SortList), + })), + }); + + // if we are using caching with this search request, then we need to handle some work upfront to enable that + if (this.data.useCaching) { + + // force use of the cache for this request if .usingCaching was called + this._forceCaching = true; + + // because all the requests use the same url they would collide in the cache we use a special key + const cacheKey = `PnPjs.SearchWithCaching(${getHashCode(postBody.body)})`; + + if (objectDefinedNotNull(this.data.cachingOptions)) { + // if our key ends in the postquery url we overwrite it + if (/\/_api\/search\/postquery$/i.test(this.data.cachingOptions.key)) { + this.data.cachingOptions.key = cacheKey; + } + } else { + this.data.cachingOptions = new CachingOptions(cacheKey); + } + } + + const data = await spPost(this, postBody); + return new SearchResults(data, this.toUrl(), query); + } + + /** + * Fix array property + * + * @param prop property to fix for container struct + */ + private fixArrProp(prop: any): { results: any[] } { + if (typeof prop === "undefined") { + return ({ results: [] }); + } + prop = isArray(prop) ? prop : [prop]; + return hOP(prop, "results") ? prop : { results: prop }; + } + + /** + * Translates one of the query initializers into a SearchQuery instance + * + * @param query + */ + private parseQuery(query: SearchQueryInit): ISearchQuery { + + let finalQuery: ISearchQuery; + + if (typeof query === "string") { + finalQuery = { Querytext: query }; + } else if ((query as ISearchQueryBuilder).toSearchQuery) { + finalQuery = (query as ISearchQueryBuilder).toSearchQuery(); + } else { + finalQuery = query; + } + + return finalQuery; + } +} + +export interface ISearch { + (queryInit: SearchQueryInit): Promise; +} + +export const Search: ISearch = (queryInit: SearchQueryInit) => (new _Search("").execute(queryInit)); + +export const SearchFactory = (baseUrl: string | ISharePointQueryable, path = "", options: IConfigOptions = {}): ISearch => (queryInit: SearchQueryInit) => { + return (new _Search(baseUrl, path)).configure(options).execute(queryInit); +}; + +/** + * Describes the SearchResults class, which returns the formatted and raw version of the query response + */ +export class SearchResults { + + /** + * Creates a new instance of the SearchResult class + * + */ + constructor(rawResponse: any, + private _url: string, + private _query: ISearchQuery, + private _raw: ISearchResponse = null, + private _primary: ISearchResult[] = null) { + + this._raw = rawResponse.postquery ? rawResponse.postquery : rawResponse; + } + + public get ElapsedTime(): number { + return this.RawSearchResults.ElapsedTime; + } + + public get RowCount(): number { + return this.RawSearchResults.PrimaryQueryResult.RelevantResults.RowCount; + } + + public get TotalRows(): number { + return this.RawSearchResults.PrimaryQueryResult.RelevantResults.TotalRows; + } + + public get TotalRowsIncludingDuplicates(): number { + return this.RawSearchResults.PrimaryQueryResult.RelevantResults.TotalRowsIncludingDuplicates; + } + + public get RawSearchResults(): ISearchResponse { + return this._raw; + } + + public get PrimarySearchResults(): ISearchResult[] { + if (this._primary === null) { + this._primary = this.formatSearchResults(this._raw.PrimaryQueryResult.RelevantResults.Table.Rows); + } + return this._primary; + } + + /** + * Gets a page of results + * + * @param pageNumber Index of the page to return. Used to determine StartRow + * @param pageSize Optional, items per page (default = 10) + */ + public getPage(pageNumber: number, pageSize?: number): Promise { + + // if we got all the available rows we don't have another page + if (this.TotalRows < this.RowCount) { + return Promise.resolve(null); + } + + // if pageSize is supplied, then we use that regardless of any previous values + // otherwise get the previous RowLimit or default to 10 + const rows = pageSize !== undefined ? pageSize : hOP(this._query, "RowLimit") ? this._query.RowLimit : 10; + + const query: ISearchQuery = extend(this._query, { + RowLimit: rows, + StartRow: rows * (pageNumber - 1), + }); + + // we have reached the end + if (query.StartRow > this.TotalRows) { + return Promise.resolve(null); + } + + return SearchFactory(this._url, null)(query); + } + + /** + * Formats a search results array + * + * @param rawResults The array to process + */ + protected formatSearchResults(rawResults: any): ISearchResult[] { + + const results = new Array(); + const tempResults = rawResults.results ? rawResults.results : rawResults; + + for (const tempResult of tempResults) { + + const cells: { Key: string, Value: any }[] = tempResult.Cells.results ? tempResult.Cells.results : tempResult.Cells; + + results.push(cells.reduce((res, cell) => { + + Object.defineProperty(res, cell.Key, + { + configurable: false, + enumerable: true, + value: cell.Value, + writable: false, + }); + + return res; + + }, {})); + } + + return results; + } +} + + diff --git a/packages/sp/src/search/suggest.ts b/packages/sp/src/search/suggest.ts new file mode 100644 index 000000000..fb4f4b829 --- /dev/null +++ b/packages/sp/src/search/suggest.ts @@ -0,0 +1,139 @@ +import { _SharePointQueryableInstance, ISharePointQueryable } from "../sharepointqueryable"; +import { hOP, IConfigOptions } from "@pnp/common"; +import { defaultPath } from "../decorators"; + +@defaultPath("_api/search/suggest") +export class _SearchSuggest extends _SharePointQueryableInstance { + + public async execute(query: ISearchSuggestQuery): Promise { + this.mapQueryToQueryString(query); + const response = await this.get(); + const mapper = hOP(response, "suggest") ? (s_1: string) => response.suggest[s_1].results : (s_2: string) => response[s_2]; + return { + PeopleNames: mapper("PeopleNames"), + PersonalResults: mapper("PersonalResults"), + Queries: mapper("Queries"), + }; + } + + private mapQueryToQueryString(query: ISearchSuggestQuery): void { + + const setProp = (q: ISearchSuggestQuery) => (checkProp: string) => (sp: string) => { + if (hOP(q, checkProp)) { + this.query.set(sp, q[checkProp].toString()); + } + }; + + this.query.set("querytext", `'${query.querytext}'`); + + const querySetter = setProp(query); + + querySetter("count")("inumberofquerysuggestions"); + querySetter("personalCount")("inumberofresultsuggestions"); + querySetter("preQuery")("fprequerysuggestions"); + querySetter("hitHighlighting")("fhithighlighting"); + querySetter("capitalize")("fcapitalizefirstletters"); + querySetter("culture")("culture"); + querySetter("stemming")("enablestemming"); + querySetter("includePeople")("showpeoplenamesuggestions"); + querySetter("queryRules")("enablequeryrules"); + querySetter("prefixMatch")("fprefixmatchallterms"); + } +} + +export interface ISuggest { + (query: ISearchSuggestQuery): Promise; +} + +export const Suggest: ISuggest = (query: ISearchSuggestQuery) => (new _SearchSuggest("").execute(query)); + +export const SuggestFactory = (baseUrl: string | ISharePointQueryable, path = "", options: IConfigOptions = {}): ISuggest => (query: ISearchSuggestQuery) => { + return (new _SearchSuggest(baseUrl, path)).configure(options).execute(query); +}; + +/** + * Defines a query execute against the search/suggest endpoint (see https://msdn.microsoft.com/en-us/library/office/dn194079.aspx) + */ +export interface ISearchSuggestQuery { + + [key: string]: string | number | boolean; + + /** + * A string that contains the text for the search query. + */ + querytext: string; + + /** + * The number of query suggestions to retrieve. Must be greater than zero (0). The default value is 5. + */ + count?: number; + + /** + * The number of personal results to retrieve. Must be greater than zero (0). The default value is 5. + */ + personalCount?: number; + + /** + * A Boolean value that specifies whether to retrieve pre-query or post-query suggestions. true to return pre-query suggestions; otherwise, false. The default value is false. + */ + preQuery?: boolean; + + /** + * A Boolean value that specifies whether to hit-highlight or format in bold the query suggestions. true to format in bold the terms in the returned query suggestions + * that match terms in the specified query; otherwise, false. The default value is true. + */ + hitHighlighting?: boolean; + + /** + * A Boolean value that specifies whether to capitalize the first letter in each term in the returned query suggestions. true to capitalize the first letter in each term; + * otherwise, false. The default value is false. + */ + capitalize?: boolean; + + /** + * The locale ID (LCID) for the query (see https://msdn.microsoft.com/en-us/library/cc233982.aspx). + */ + culture?: string; + + /** + * A Boolean value that specifies whether stemming is enabled. true to enable stemming; otherwise, false. The default value is true. + */ + stemming?: boolean; + + /** + * A Boolean value that specifies whether to include people names in the returned query suggestions. true to include people names in the returned query suggestions; + * otherwise, false. The default value is true. + */ + includePeople?: boolean; + + /** + * A Boolean value that specifies whether to turn on query rules for this query. true to turn on query rules; otherwise, false. The default value is true. + */ + queryRules?: boolean; + + /** + * A Boolean value that specifies whether to return query suggestions for prefix matches. true to return query suggestions based on prefix matches, otherwise, false when + * query suggestions should match the full query word. + */ + prefixMatch?: boolean; +} + +export interface ISearchSuggestResult { + readonly PeopleNames: string[]; + readonly PersonalResults: IPersonalResultSuggestion[]; + readonly Queries: any[]; +} + +export interface IESearchSuggestResult { + readonly PeopleNames: string[]; + readonly PersonalResults: IPersonalResultSuggestion[]; + readonly Queries: any[]; +} + +export interface IPersonalResultSuggestion { + readonly HighlightedTitle?: string; + readonly IsBestBet?: boolean; + readonly Title?: string; + readonly TypeId?: string; + readonly Url?: string; +} diff --git a/packages/sp/src/search/types.ts b/packages/sp/src/search/types.ts new file mode 100644 index 000000000..3acecf184 --- /dev/null +++ b/packages/sp/src/search/types.ts @@ -0,0 +1,471 @@ +export type SearchQueryInit = string | ISearchQuery | ISearchQueryBuilder; + +export interface ISearchQueryBuilder { + query: any; + readonly bypassResultTypes: this; + readonly enableStemming: this; + readonly enableInterleaving: this; + readonly enableFql: this; + readonly enableNicknames: this; + readonly enablePhonetic: this; + readonly trimDuplicates: this; + readonly processBestBets: this; + readonly enableQueryRules: this; + readonly enableSorting: this; + readonly generateBlockRankLog: this; + readonly processPersonalFavorites: this; + readonly enableOrderingHitHighlightedProperty: this; + + culture(culture: number): this; + rowLimit(n: number): this; + startRow(n: number): this; + sourceId(id: string): this; + text(queryText: string): this; + template(template: string): this; + trimDuplicatesIncludeId(n: number): this; + rankingModelId(id: string): this; + rowsPerPage(n: number): this; + selectProperties(...properties: string[]): this; + timeZoneId(id: number): this; + refinementFilters(...filters: string[]): this; + refiners(refiners: string): this; + hiddenConstraints(constraints: string): this; + sortList(...sorts: ISort[]): this; + timeout(milliseconds: number): this; + hithighlightedProperties(...properties: string[]): this; + clientType(clientType: string): this; + personalizationData(data: string): this; + resultsURL(url: string): this; + queryTag(tags: string): this; + properties(...properties: ISearchProperty[]): this; + queryTemplatePropertiesUrl(url: string): this; + reorderingRules(...rules: IReorderingRule[]): this; + hitHighlightedMultivaluePropertyLimit(limit: number): this; + collapseSpecification(spec: string): this; + uiLanguage(lang: number): this; + desiredSnippetLength(len: number): this; + maxSnippetLength(len: number): this; + summaryLength(len: number): this; + + /* included method */ + toSearchQuery(): ISearchQuery; +} + +/** + * Describes the SearchQuery interface + */ +export interface ISearchQuery { + + /** + * A string that contains the text for the search query. + */ + Querytext?: string; + + /** + * A string that contains the text that replaces the query text, as part of a query transform. + */ + QueryTemplate?: string; + + /** + * A Boolean value that specifies whether the result tables that are returned for + * the result block are mixed with the result tables that are returned for the original query. + */ + EnableInterleaving?: boolean; + + /** + * A Boolean value that specifies whether stemming is enabled. + */ + EnableStemming?: boolean; + + /** + * A Boolean value that specifies whether duplicate items are removed from the results. + */ + TrimDuplicates?: boolean; + + /** + * A Boolean value that specifies whether the exact terms in the search query are used to find matches, or if nicknames are used also. + */ + EnableNicknames?: boolean; + + /** + * A Boolean value that specifies whether the query uses the FAST Query Language (FQL). + */ + EnableFQL?: boolean; + + /** + * A Boolean value that specifies whether the phonetic forms of the query terms are used to find matches. + */ + EnablePhonetic?: boolean; + + /** + * A Boolean value that specifies whether to perform result type processing for the query. + */ + BypassResultTypes?: boolean; + + /** + * A Boolean value that specifies whether to return best bet results for the query. + * This parameter is used only when EnableQueryRules is set to true, otherwise it is ignored. + */ + ProcessBestBets?: boolean; + + /** + * A Boolean value that specifies whether to enable query rules for the query. + */ + EnableQueryRules?: boolean; + + /** + * A Boolean value that specifies whether to sort search results. + */ + EnableSorting?: boolean; + + /** + * Specifies whether to return block rank log information in the BlockRankLog property of the interleaved result table. + * A block rank log contains the textual information on the block score and the documents that were de-duplicated. + */ + GenerateBlockRankLog?: boolean; + + /** + * The result source ID to use for executing the search query. + */ + SourceId?: string; + + /** + * The ID of the ranking model to use for the query. + */ + RankingModelId?: string; + + /** + * The first row that is included in the search results that are returned. + * You use this parameter when you want to implement paging for search results. + */ + StartRow?: number; + + /** + * The maximum number of rows overall that are returned in the search results. + * Compared to RowsPerPage, RowLimit is the maximum number of rows returned overall. + */ + RowLimit?: number; + + /** + * The maximum number of rows to return per page. + * Compared to RowLimit, RowsPerPage refers to the maximum number of rows to return per page, + * and is used primarily when you want to implement paging for search results. + */ + RowsPerPage?: number; + + /** + * The managed properties to return in the search results. + */ + SelectProperties?: string[]; + + /** + * The locale ID (LCID) for the query. + */ + Culture?: number; + + /** + * The set of refinement filters used when issuing a refinement query (FQL) + */ + RefinementFilters?: string[]; + + /** + * The set of refiners to return in a search result. + */ + Refiners?: string; + + /** + * The additional query terms to append to the query. + */ + HiddenConstraints?: string; + + /** + * The list of properties by which the search results are ordered. + */ + SortList?: ISort[]; + + /** + * The amount of time in milliseconds before the query request times out. + */ + Timeout?: number; + + /** + * The properties to highlight in the search result summary when the property value matches the search terms entered by the user. + */ + HitHighlightedProperties?: string[]; + + /** + * The type of the client that issued the query. + */ + ClientType?: string; + + /** + * The GUID for the user who submitted the search query. + */ + PersonalizationData?: string; + + /** + * The URL for the search results page. + */ + ResultsUrl?: string; + + /** + * Custom tags that identify the query. You can specify multiple query tags + */ + QueryTag?: string; + + /** + * Properties to be used to configure the search query + */ + Properties?: ISearchProperty[]; + + /** + * A Boolean value that specifies whether to return personal favorites with the search results. + */ + ProcessPersonalFavorites?: boolean; + + /** + * The location of the queryparametertemplate.xml file. This file is used to enable anonymous users to make Search REST queries. + */ + QueryTemplatePropertiesUrl?: string; + + /** + * Special rules for reordering search results. + * These rules can specify that documents matching certain conditions are ranked higher or lower in the results. + * This property applies only when search results are sorted based on rank. + */ + ReorderingRules?: IReorderingRule[]; + + /** + * The number of properties to show hit highlighting for in the search results. + */ + HitHighlightedMultivaluePropertyLimit?: number; + + /** + * A Boolean value that specifies whether the hit highlighted properties can be ordered. + */ + EnableOrderingHitHighlightedProperty?: boolean; + + /** + * The managed properties that are used to determine how to collapse individual search results. + * Results are collapsed into one or a specified number of results if they match any of the individual collapse specifications. + * In a collapse specification, results are collapsed if their properties match all individual properties in the collapse specification. + */ + CollapseSpecification?: string; + + /** + * The locale identifier (LCID) of the user interface + */ + UIlanguage?: number; + + /** + * The preferred number of characters to display in the hit-highlighted summary generated for a search result. + */ + DesiredSnippetLength?: number; + + /** + * The maximum number of characters to display in the hit-highlighted summary generated for a search result. + */ + MaxSnippetLength?: number; + + /** + * The number of characters to display in the result summary for a search result. + */ + SummaryLength?: number; + +} + +/** + * Provides hints at the properties which may be available on the result object + */ +export interface ISearchResult { + + Rank?: number; + DocId?: number; + WorkId?: number; + Title?: string; + Author?: string; + Size?: number; + Path?: string; + Description?: string; + Write?: Date; + LastModifiedTime?: Date; + CollapsingStatus?: number; + HitHighlightedSummary?: string; + HitHighlightedProperties?: string; + contentclass?: string; + PictureThumbnailURL?: string; + ServerRedirectedURL?: string; + ServerRedirectedEmbedURL?: string; + ServerRedirectedPreviewURL?: string; + FileExtension?: string; + ContentTypeId?: string; + ParentLink?: string; + ViewsLifetime?: number; + ViewsRecent?: number; + SectionNames?: string; + SectionIndexes?: string; + SiteLogo?: string; + SiteDescription?: string; + importance?: number; + SiteName?: string; + IsDocument?: boolean; + FileType?: string; + IsContainer?: boolean; + WebTemplate?: string; + SPWebUrl?: string; + UniqueId?: string; + ProgId?: string; + OriginalPath?: string; + RenderTemplateId?: string; + PartitionId?: string; + UrlZone?: number; + Culture?: string; +} + +export interface ISearchResponse { + ElapsedTime: number; + Properties?: { Key: string, Value: any, ValueType: string }[]; + PrimaryQueryResult?: IResultTableCollection; + SecondaryQueryResults?: IResultTableCollection; + SpellingSuggestion?: string; + TriggeredRules?: any[]; +} + +export interface IResultTableCollection { + + QueryErrors?: Map; + QueryId?: string; + QueryRuleId?: string; + CustomResults?: IResultTable; + RefinementResults?: IResultTable; + RelevantResults?: IResultTable; + SpecialTermResults?: IResultTable; +} + +export interface IRefiner { + Name: string; + Entries: { RefinementCount: string; RefinementName: string; RefinementToken: string; RefinementValue: string; }[]; +} +export interface IResultTable { + GroupTemplateId?: string; + ItemTemplateId?: string; + Properties?: { Key: string, Value: any, ValueType: string }[]; + Table?: { Rows: { Cells: { Key: string, Value: any, ValueType: string }[] }[] }; + Refiners?: IRefiner[]; + ResultTitle?: string; + ResultTitleUrl?: string; + RowCount?: number; + TableType?: string; + TotalRows?: number; + TotalRowsIncludingDuplicates?: number; +} + +/** + * Defines how search results are sorted. + */ +export interface ISort { + + /** + * The name for a property by which the search results are ordered. + */ + Property: string; + + /** + * The direction in which search results are ordered. + */ + Direction: SortDirection; +} + +/** + * Defines one search property + */ +export interface ISearchProperty { + Name: string; + Value: ISearchPropertyValue; +} + +/** + * Defines one search property value. Set only one of StrlVal/BoolVal/IntVal/StrArray. + */ +export interface ISearchPropertyValue { + StrVal?: string; + BoolVal?: boolean; + IntVal?: number; + StrArray?: string[]; + QueryPropertyValueTypeIndex: QueryPropertyValueType; +} + +/** + * defines the SortDirection enum + */ +export enum SortDirection { + Ascending = 0, + Descending = 1, + FQLFormula = 2, +} + +/** + * Defines how ReorderingRule interface, used for reordering results + */ +export interface IReorderingRule { + + /** + * The value to match on + */ + MatchValue: string; + + /** + * The rank boosting + */ + Boost: number; + + /** + * The rank boosting + */ + MatchType: ReorderingRuleMatchType; +} + +/** + * defines the ReorderingRuleMatchType enum + */ +export enum ReorderingRuleMatchType { + ResultContainsKeyword = 0, + TitleContainsKeyword = 1, + TitleMatchesKeyword = 2, + UrlStartsWith = 3, + UrlExactlyMatches = 4, + ContentTypeIs = 5, + FileExtensionMatches = 6, + ResultHasTag = 7, + ManualCondition = 8, +} + +/** + * Specifies the type value for the property + */ +export enum QueryPropertyValueType { + None = 0, + StringType = 1, + Int32Type = 2, + BooleanType = 3, + StringArrayType = 4, + UnSupportedType = 5, +} + +export class SearchBuiltInSourceId { + public static readonly Documents = "e7ec8cee-ded8-43c9-beb5-436b54b31e84"; + public static readonly ItemsMatchingContentType = "5dc9f503-801e-4ced-8a2c-5d1237132419"; + public static readonly ItemsMatchingTag = "e1327b9c-2b8c-4b23-99c9-3730cb29c3f7"; + public static readonly ItemsRelatedToCurrentUser = "48fec42e-4a92-48ce-8363-c2703a40e67d"; + public static readonly ItemsWithSameKeywordAsThisItem = "5c069288-1d17-454a-8ac6-9c642a065f48"; + public static readonly LocalPeopleResults = "b09a7990-05ea-4af9-81ef-edfab16c4e31"; + public static readonly LocalReportsAndDataResults = "203fba36-2763-4060-9931-911ac8c0583b"; + public static readonly LocalSharePointResults = "8413cd39-2156-4e00-b54d-11efd9abdb89"; + public static readonly LocalVideoResults = "78b793ce-7956-4669-aa3b-451fc5defebf"; + public static readonly Pages = "5e34578e-4d08-4edc-8bf3-002acf3cdbcc"; + public static readonly Pictures = "38403c8c-3975-41a8-826e-717f2d41568a"; + public static readonly Popular = "97c71db1-58ce-4891-8b64-585bc2326c12"; + public static readonly RecentlyChangedItems = "ba63bbae-fa9c-42c0-b027-9a878f16557c"; + public static readonly RecommendedItems = "ec675252-14fa-4fbe-84dd-8d098ed74181"; + public static readonly Wiki = "9479bf85-e257-4318-b5a8-81a180f5faa1"; +} diff --git a/packages/sp/src/security/funcs.ts b/packages/sp/src/security/funcs.ts new file mode 100644 index 000000000..836b9a15a --- /dev/null +++ b/packages/sp/src/security/funcs.ts @@ -0,0 +1,101 @@ +import { SecurableQueryable, IBasePermissions, PermissionKind } from "./types"; +import { _SharePointQueryableInstance, _SharePointQueryable, SharePointQueryableInstance, SharePointQueryable } from "../sharepointqueryable"; +import { hOP } from "@pnp/common"; +import { spPost } from "../operations"; + +// export function +/** +* Gets the effective permissions for the user supplied +* +* @param loginName The claims username for the user (ex: i:0#.f|membership|user@domain.com) +*/ +export async function getUserEffectivePermissions(this: SecurableQueryable, loginName: string): Promise { + + const q = this.clone(SharePointQueryableInstance, "getUserEffectivePermissions(@user)"); + q.query.set("@user", `'${encodeURIComponent(loginName)}'`); + const r = await q.get(); + // handle verbose mode + return hOP(r, "GetUserEffectivePermissions") ? r.GetUserEffectivePermissions : r; +} + +/** + * Gets the effective permissions for the current user + */ +export async function getCurrentUserEffectivePermissions(this: SecurableQueryable): Promise { + + // remove need to reference Web here, which created a circular build issue + const w = new _SharePointQueryableInstance("_api/web", "currentuser"); + const user = await w.configureFrom(this).select("LoginName")<{ LoginName: string }>(); + return getUserEffectivePermissions.call(this, user.LoginName); +} + +/** + * Breaks the security inheritance at this level optinally copying permissions and clearing subscopes + * + * @param copyRoleAssignments If true the permissions are copied from the current parent scope + * @param clearSubscopes Optional. true to make all child securable objects inherit role assignments from the current object + */ +export function breakRoleInheritance(this: SecurableQueryable, copyRoleAssignments = false, clearSubscopes = false): Promise { + return spPost(this.clone(SharePointQueryable, `breakroleinheritance(copyroleassignments=${copyRoleAssignments}, clearsubscopes=${clearSubscopes})`)); +} + +/** + * Removes the local role assignments so that it re-inherit role assignments from the parent object. + * + */ +export function resetRoleInheritance(this: SecurableQueryable): Promise { + return spPost(this.clone(SharePointQueryable, "resetroleinheritance")); +} + +/** + * Determines if a given user has the appropriate permissions + * + * @param loginName The user to check + * @param permission The permission being checked + */ +export async function userHasPermissions(this: SecurableQueryable, loginName: string, permission: PermissionKind): Promise { + + const perms = await getUserEffectivePermissions.call(this, loginName); + return this.hasPermissions(perms, permission); +} + +/** + * Determines if the current user has the requested permissions + * + * @param permission The permission we wish to check + */ +export async function currentUserHasPermissions(this: SecurableQueryable, permission: PermissionKind): Promise { + + const perms = await getCurrentUserEffectivePermissions.call(this); + return this.hasPermissions(perms, permission); +} + +/** + * Taken from sp.js, checks the supplied permissions against the mask + * + * @param value The security principal's permissions on the given object + * @param perm The permission checked against the value + */ +/* tslint:disable:no-bitwise */ +export function hasPermissions(value: IBasePermissions, perm: PermissionKind): boolean { + + if (!perm) { + return true; + } + if (perm === PermissionKind.FullMask) { + return (value.High & 32767) === 32767 && value.Low === 65535; + } + + perm = perm - 1; + let num = 1; + + if (perm >= 0 && perm < 32) { + num = num << perm; + return 0 !== (value.Low & num); + } else if (perm >= 32 && perm < 64) { + num = num << perm - 32; + return 0 !== (value.High & num); + } + return false; +} +/* tslint:enable */ diff --git a/packages/sp/src/security/index.ts b/packages/sp/src/security/index.ts new file mode 100644 index 000000000..6a273f09a --- /dev/null +++ b/packages/sp/src/security/index.ts @@ -0,0 +1,26 @@ +import "./item"; +import "./list"; +import "./web"; + +export { + /** + * Roles + */ + IRoleDefinitions, + IRoleDefinition, + IRoleAssignment, + IRoleAssignments, + IRoleDefinitionAddResult, + IRoleDefinitionUpdateResult, + RoleAssignment, + RoleAssignments, + RoleDefinition, + RoleDefinitions, + + /** + * Interfaces & types + */ + IBasePermissions, + PermissionKind, + SecurableQueryable, +} from "./types"; diff --git a/packages/sp/src/security/item.ts b/packages/sp/src/security/item.ts new file mode 100644 index 000000000..bac96aa98 --- /dev/null +++ b/packages/sp/src/security/item.ts @@ -0,0 +1,35 @@ +import { addProp } from "@pnp/odata"; +import { _Item } from "../items/types"; +import { RoleAssignments, ISecurableMethods } from "./types"; +import { _SharePointQueryableInstance, SharePointQueryableInstance } from "../sharepointqueryable"; +import { + getUserEffectivePermissions, + getCurrentUserEffectivePermissions, + breakRoleInheritance, + resetRoleInheritance, + userHasPermissions, + currentUserHasPermissions, + hasPermissions, +} from "./funcs"; + +/** +* Extend Item +*/ +declare module "../items/types" { + interface _Item extends ISecurableMethods { + } + interface IItem extends ISecurableMethods { + } +} + +addProp(_Item, "roleAssignments", RoleAssignments); +addProp(_Item, "firstUniqueAncestorSecurableObject", SharePointQueryableInstance); + +_Item.prototype.getUserEffectivePermissions = getUserEffectivePermissions; +_Item.prototype.getCurrentUserEffectivePermissions = getCurrentUserEffectivePermissions; +_Item.prototype.breakRoleInheritance = breakRoleInheritance; +_Item.prototype.resetRoleInheritance = resetRoleInheritance; +_Item.prototype.userHasPermissions = userHasPermissions; +_Item.prototype.currentUserHasPermissions = currentUserHasPermissions; +_Item.prototype.hasPermissions = hasPermissions; + diff --git a/packages/sp/src/security/list.ts b/packages/sp/src/security/list.ts new file mode 100644 index 000000000..99c3667c2 --- /dev/null +++ b/packages/sp/src/security/list.ts @@ -0,0 +1,36 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { RoleAssignments, ISecurableMethods } from "./types"; +import { SharePointQueryableInstance } from "../sharepointqueryable"; +import { + getUserEffectivePermissions, + getCurrentUserEffectivePermissions, + breakRoleInheritance, + resetRoleInheritance, + userHasPermissions, + currentUserHasPermissions, + hasPermissions, +} from "./funcs"; + +/** +* Extend Item +*/ +declare module "../lists/types" { + interface _List extends ISecurableMethods { + + } + interface IList extends ISecurableMethods { + + } +} + +addProp(_List, "roleAssignments", RoleAssignments); +addProp(_List, "firstUniqueAncestorSecurableObject", SharePointQueryableInstance); + +_List.prototype.getUserEffectivePermissions = getUserEffectivePermissions; +_List.prototype.getCurrentUserEffectivePermissions = getCurrentUserEffectivePermissions; +_List.prototype.breakRoleInheritance = breakRoleInheritance; +_List.prototype.resetRoleInheritance = resetRoleInheritance; +_List.prototype.userHasPermissions = userHasPermissions; +_List.prototype.currentUserHasPermissions = currentUserHasPermissions; +_List.prototype.hasPermissions = hasPermissions; diff --git a/packages/sp/src/security/types.ts b/packages/sp/src/security/types.ts new file mode 100644 index 000000000..9d91c7be2 --- /dev/null +++ b/packages/sp/src/security/types.ts @@ -0,0 +1,452 @@ +import { extend, TypedHash, hOP } from "@pnp/common"; +import { IGetable, body, headers } from "@pnp/odata"; +import { + _SharePointQueryableInstance, + SharePointQueryableCollection, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { SiteGroups, ISiteGroups } from "../site-groups/types"; +import { IBasePermissions } from "./types"; +import { metadata } from "../utils/metadata"; +import { _Web } from "../webs/types"; +import { _List } from "../lists/types"; +import { _Item } from "../items/types"; +import { defaultPath, IDeleteable, deleteable } from "../decorators"; +import { spPost } from "../operations"; + +export type SecurableQueryable = _Web | _List | _Item; + +/** + * Describes a set of role assignments for the current scope + * + */ +@defaultPath("roleassignments") +export class _RoleAssignments extends _SharePointQueryableCollection implements IRoleAssignments { + + /** + * Gets the role assignment associated with the specified principal id from the collection. + * + * @param id The id of the role assignment + */ + public getById(id: number): IRoleAssignment { + return RoleAssignment(this).concat(`(${id})`); + } + + /** + * Adds a new role assignment with the specified principal and role definitions to the collection + * + * @param principalId The id of the user or group to assign permissions to + * @param roleDefId The id of the role definition that defines the permissions to assign + * + */ + public add(principalId: number, roleDefId: number): Promise { + return spPost(this.clone(RoleAssignments, `addroleassignment(principalid=${principalId}, roledefid=${roleDefId})`)); + } + + /** + * Removes the role assignment with the specified principal and role definition from the collection + * + * @param principalId The id of the user or group in the role assignment + * @param roleDefId The id of the role definition in the role assignment + * + */ + public remove(principalId: number, roleDefId: number): Promise { + return spPost(this.clone(RoleAssignments, `removeroleassignment(principalid=${principalId}, roledefid=${roleDefId})`)); + } +} + +export interface IRoleAssignments extends IGetable, ISharePointQueryableCollection { + getById(id: number): IRoleAssignment; + add(principalId: number, roleDefId: number): Promise; + remove(principalId: number, roleDefId: number): Promise; +} +export interface _RoleAssignments extends IGetable { } +export const RoleAssignments = spInvokableFactory(_RoleAssignments); + +/** + * Describes a role assignment + * + */ +@deleteable() +export class _RoleAssignment extends _SharePointQueryableInstance implements IRoleAssignment { + + /** + * Gets the groups that directly belong to the access control list (ACL) for this securable object + * + */ + public get groups(): ISiteGroups { + return SiteGroups(this, "groups"); + } + + /** + * Gets the role definition bindings for this role assignment + * + */ + public get bindings(): ISharePointQueryableCollection { + return SharePointQueryableCollection(this, "roledefinitionbindings"); + } +} + +export interface IRoleAssignment extends IGetable, ISharePointQueryableInstance, IDeleteable { + readonly groups: ISiteGroups; + readonly bindings: ISharePointQueryableCollection; +} +export interface _RoleAssignment extends IGetable, IDeleteable { } +export const RoleAssignment = spInvokableFactory(_RoleAssignment); + +/** + * Describes a collection of role definitions + * + */ +@defaultPath("roledefinitions") +export class _RoleDefinitions extends _SharePointQueryableCollection implements IRoleDefinitions { + + /** + * Gets the role definition with the specified id from the collection + * + * @param id The id of the role definition + * + */ + public getById(id: number): IRoleDefinition { + return RoleDefinition(this, `getById(${id})`); + } + + /** + * Gets the role definition with the specified name + * + * @param name The name of the role definition + * + */ + public getByName(name: string): IRoleDefinition { + return RoleDefinition(this, `getbyname('${name}')`); + } + + /** + * Gets the role definition with the specified role type + * + * @param roleTypeKind The roletypekind of the role definition (None=0, Guest=1, Reader=2, Contributor=3, WebDesigner=4, Administrator=5, Editor=6, System=7) + * + */ + public getByType(roleTypeKind: number): IRoleDefinition { + return RoleDefinition(this, `getbytype(${roleTypeKind})`); + } + + /** + * Creates a role definition + * + * @param name The new role definition's name + * @param description The new role definition's description + * @param order The order in which the role definition appears + * @param basePermissions The permissions mask for this role definition + * + */ + public async add(name: string, description: string, order: number, basePermissions: IBasePermissions): Promise { + + const postBody = body({ + BasePermissions: extend({ __metadata: { type: "SP.BasePermissions" } }, basePermissions), + Description: description, + Name: name, + Order: order, + __metadata: { "type": "SP.RoleDefinition" }, + }); + + const data = await spPost(this, postBody); + + return { + data: data, + definition: this.getById(data.Id), + }; + } +} + +export interface IRoleDefinitions extends IGetable, ISharePointQueryableCollection { + getById(id: number): IRoleDefinition; + getByName(name: string): IRoleDefinition; + getByType(roleTypeKind: number): IRoleDefinition; + add(name: string, description: string, order: number, basePermissions: IBasePermissions): Promise; +} +export interface _RoleDefinitions extends IGetable { } +export const RoleDefinitions = spInvokableFactory(_RoleDefinitions); + +/** + * Describes a role definition + * + */ +@deleteable() +export class _RoleDefinition extends _SharePointQueryableInstance implements IRoleDefinition { + + /** + * Updates this role definition with the supplied properties + * + * @param properties A plain object hash of values to update for the role definition + */ + /* tslint:disable no-string-literal */ + public async update(properties: TypedHash): Promise { + + const s = ["BasePermissions"]; + if (hOP(properties, s[0]) !== undefined) { + properties[s[0]] = extend({ __metadata: { type: "SP." + s[0] } }, properties[s[0]]); + } + + const postBody = body(extend(metadata("SP.RoleDefinition"), properties), headers({ "X-HTTP-Method": "MERGE" })); + + const data = await spPost(this, postBody); + + let definition: IRoleDefinition = this; + if (hOP(properties, "Name")) { + const parent = this.getParent(_RoleDefinitions, this.parentUrl, ""); + definition = parent.getByName((properties["Name"])); + } + return { + data, + definition, + }; + } + /* tslint:enable */ +} + +export interface IRoleDefinition extends IGetable, ISharePointQueryableInstance, IDeleteable { + update(properties: TypedHash): Promise; +} +export interface _RoleDefinition extends IGetable, IDeleteable { } +export const RoleDefinition = spInvokableFactory(_RoleDefinition); + +export interface ISecurableMethods { + readonly roleAssignments: IRoleAssignments; + readonly firstUniqueAncestorSecurableObject: _SharePointQueryableInstance; + getUserEffectivePermissions(loginName: string): Promise; + getCurrentUserEffectivePermissions(): Promise; + breakRoleInheritance(copyRoleAssignments?: boolean, clearSubscopes?: boolean): Promise; + resetRoleInheritance(): Promise; + userHasPermissions(loginName: string, permission: PermissionKind): Promise; + currentUserHasPermissions(permission: PermissionKind): Promise; + hasPermissions(value: IBasePermissions, perm: PermissionKind): boolean; +} + +/** + * Result from updating a role definition + * + */ +export interface IRoleDefinitionUpdateResult { + definition: IRoleDefinition; + data: any; +} + +/** + * Result from adding a role definition + * + */ +export interface IRoleDefinitionAddResult { + definition: IRoleDefinition; + data: any; +} + +export interface IBasePermissions { + Low: number; + High: number; +} + +export enum PermissionKind { + + /** + * Has no permissions on the Site. Not available through the user interface. + */ + EmptyMask = 0, + + /** + * View items in lists, documents in document libraries, and Web discussion comments. + */ + ViewListItems = 1, + + /** + * Add items to lists, documents to document libraries, and Web discussion comments. + */ + AddListItems = 2, + + /** + * Edit items in lists, edit documents in document libraries, edit Web discussion comments + * in documents, and customize Web Part Pages in document libraries. + */ + EditListItems = 3, + + /** + * Delete items from a list, documents from a document library, and Web discussion + * comments in documents. + */ + DeleteListItems = 4, + + /** + * Approve a minor version of a list item or document. + */ + ApproveItems = 5, + + /** + * View the source of documents with server-side file handlers. + */ + OpenItems = 6, + + /** + * View past versions of a list item or document. + */ + ViewVersions = 7, + + /** + * Delete past versions of a list item or document. + */ + DeleteVersions = 8, + + /** + * Discard or check in a document which is checked out to another user. + */ + CancelCheckout = 9, + + /** + * Create, change, and delete personal views of lists. + */ + ManagePersonalViews = 10, + + /** + * Create and delete lists, add or remove columns in a list, and add or remove public views of a list. + */ + ManageLists = 12, + + /** + * View forms, views, and application pages, and enumerate lists. + */ + ViewFormPages = 13, + + /** + * Make content of a list or document library retrieveable for anonymous users through SharePoint search. + * The list permissions in the site do not change. + */ + AnonymousSearchAccessList = 14, + + /** + * Allow users to open a Site, list, or folder to access items inside that container. + */ + Open = 17, + + /** + * View pages in a Site. + */ + ViewPages = 18, + + /** + * Add, change, or delete HTML pages or Web Part Pages, and edit the Site using + * a Windows SharePoint Services compatible editor. + */ + AddAndCustomizePages = 19, + + /** + * Apply a theme or borders to the entire Site. + */ + ApplyThemeAndBorder = 20, + + /** + * Apply a style sheet (.css file) to the Site. + */ + ApplyStyleSheets = 21, + + /** + * View reports on Site usage. + */ + ViewUsageData = 22, + + /** + * Create a Site using Self-Service Site Creation. + */ + CreateSSCSite = 23, + + /** + * Create subsites such as team sites, Meeting Workspace sites, and Document Workspace sites. + */ + ManageSubwebs = 24, + + /** + * Create a group of users that can be used anywhere within the site collection. + */ + CreateGroups = 25, + + /** + * Create and change permission levels on the Site and assign permissions to users + * and groups. + */ + ManagePermissions = 26, + + /** + * Enumerate files and folders in a Site using Microsoft Office SharePoint Designer + * and WebDAV interfaces. + */ + BrowseDirectories = 27, + + /** + * View information about users of the Site. + */ + BrowseUserInfo = 28, + + /** + * Add or remove personal Web Parts on a Web Part Page. + */ + AddDelPrivateWebParts = 29, + + /** + * Update Web Parts to display personalized information. + */ + UpdatePersonalWebParts = 30, + + /** + * Grant the ability to perform all administration tasks for the Site as well as + * manage content, activate, deactivate, or edit properties of Site scoped Features + * through the object model or through the user interface (UI). When granted on the + * root Site of a Site Collection, activate, deactivate, or edit properties of + * site collection scoped Features through the object model. To browse to the Site + * Collection Features page and activate or deactivate Site Collection scoped Features + * through the UI, you must be a Site Collection administrator. + */ + ManageWeb = 31, + + /** + * Content of lists and document libraries in the Web site will be retrieveable for anonymous users through + * SharePoint search if the list or document library has AnonymousSearchAccessList set. + */ + AnonymousSearchAccessWebLists = 32, + + /** + * Use features that launch client applications. Otherwise, users must work on documents + * locally and upload changes. + */ + UseClientIntegration = 37, + + /** + * Use SOAP, WebDAV, or Microsoft Office SharePoint Designer interfaces to access the Site. + */ + UseRemoteAPIs = 38, + + /** + * Manage alerts for all users of the Site. + */ + ManageAlerts = 39, + + /** + * Create e-mail alerts. + */ + CreateAlerts = 40, + + /** + * Allows a user to change his or her user information, such as adding a picture. + */ + EditMyUserInfo = 41, + + /** + * Enumerate permissions on Site, list, folder, document, or list item. + */ + EnumeratePermissions = 63, + + /** + * Has all permissions on the Site. Not available through the user interface. + */ + FullMask = 65, +} diff --git a/packages/sp/src/security/web.ts b/packages/sp/src/security/web.ts new file mode 100644 index 000000000..bd11f4ca1 --- /dev/null +++ b/packages/sp/src/security/web.ts @@ -0,0 +1,46 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { RoleDefinitions, IRoleDefinitions, RoleAssignments, ISecurableMethods } from "./types"; +import { SharePointQueryableInstance } from "../sharepointqueryable"; +import { + getUserEffectivePermissions, + getCurrentUserEffectivePermissions, + breakRoleInheritance, + resetRoleInheritance, + userHasPermissions, + currentUserHasPermissions, + hasPermissions, +} from "./funcs"; + +/** +* Extend Web +*/ +declare module "../webs/types" { + interface _Web extends ISecurableMethods { + roleDefinitions: IRoleDefinitions; + } + interface IWeb extends ISecurableMethods { + roleDefinitions: IRoleDefinitions; + } +} + +addProp(_Web, "roleDefinitions", RoleDefinitions); +addProp(_Web, "roleAssignments", RoleAssignments); +addProp(_Web, "firstUniqueAncestorSecurableObject", SharePointQueryableInstance); + +_Web.prototype.getUserEffectivePermissions = getUserEffectivePermissions; +_Web.prototype.getCurrentUserEffectivePermissions = getCurrentUserEffectivePermissions; +_Web.prototype.breakRoleInheritance = breakRoleInheritance; +_Web.prototype.resetRoleInheritance = resetRoleInheritance; +_Web.prototype.userHasPermissions = userHasPermissions; +_Web.prototype.currentUserHasPermissions = currentUserHasPermissions; +_Web.prototype.hasPermissions = hasPermissions; + + + + + + + + + diff --git a/packages/sp/src/sharepointqueryable.ts b/packages/sp/src/sharepointqueryable.ts new file mode 100644 index 000000000..21ad02477 --- /dev/null +++ b/packages/sp/src/sharepointqueryable.ts @@ -0,0 +1,274 @@ +import { combine, isUrlAbsolute, extend, jsS, IFetchOptions } from "@pnp/common"; +import { Queryable, IQueryable, invokableFactory, IGetable } from "@pnp/odata"; +import { Logger, LogLevel } from "@pnp/logging"; +import { SPBatch } from "./batch"; +import { metadata } from "./utils/metadata"; +import { spGet, spPost } from "./operations"; + +export interface ISharePointQueryableConstructor { + new(baseUrl: string | ISharePointQueryable, path?: string): T; +} + +export const spInvokableFactory = (f: ISharePointQueryableConstructor) => (baseUrl: string | ISharePointQueryable, path?: string): T => { + return invokableFactory(f)(baseUrl, path); +}; + +/** + * SharePointQueryable Base Class + * + */ +export class _SharePointQueryable extends Queryable implements ISharePointQueryable { + + protected _forceCaching: boolean; + + /** + * Creates a new instance of the SharePointQueryable class + * + * @constructor + * @param baseUrl A string or SharePointQueryable that should form the base part of the url + * + */ + constructor(baseUrl: string | ISharePointQueryable, path?: string) { + + let url = ""; + let parentUrl = ""; + const query = new Map(); + + if (typeof baseUrl === "string") { + // we need to do some extra parsing to get the parent url correct if we are + // being created from just a string. + + if (isUrlAbsolute(baseUrl) || baseUrl.lastIndexOf("/") < 0) { + parentUrl = baseUrl; + url = combine(baseUrl, path); + } else if (baseUrl.lastIndexOf("/") > baseUrl.lastIndexOf("(")) { + // .../items(19)/fields + const index = baseUrl.lastIndexOf("/"); + parentUrl = baseUrl.slice(0, index); + path = combine(baseUrl.slice(index), path); + url = combine(parentUrl, path); + } else { + // .../items(19) + const index = baseUrl.lastIndexOf("("); + parentUrl = baseUrl.slice(0, index); + url = combine(baseUrl, path); + } + } else { + + parentUrl = baseUrl.toUrl(); + url = combine(parentUrl, path || ""); + const target = baseUrl.query.get("@target"); + if (target !== undefined) { + query.set("@target", target); + } + } + + // init base with correct values for data seed + super({ + parentUrl, + query, + url, + }); + + // post init actions + if (typeof baseUrl !== "string") { + this.configureFrom(baseUrl); + } + this._forceCaching = false; + } + + /** + * Gets the full url with query information + * + */ + public toUrlAndQuery(): string { + + const aliasedParams = new Map(this.query); + + let url = this.toUrl().replace(/'!(@.*?)::(.*?)'/ig, (match, labelName, value) => { + Logger.write(`Rewriting aliased parameter from match ${match} to label: ${labelName} value: ${value}`, LogLevel.Verbose); + aliasedParams.set(labelName, `'${value}'`); + return labelName; + }); + + if (aliasedParams.size > 0) { + const char = url.indexOf("?") > -1 ? "&" : "?"; + url += `${char}${Array.from(aliasedParams).map((v: [string, string]) => v[0] + "=" + v[1]).join("&")}`; + } + + return url; + } + + /** + * Choose which fields to return + * + * @param selects One or more fields to return + */ + public select(...selects: string[]): this { + if (selects.length > 0) { + this.query.set("$select", selects.join(",")); + } + return this; + } + + public get(options?: IFetchOptions): Promise { + return spGet(this, options); + } + + /** + * Expands fields such as lookups to get additional data + * + * @param expands The Fields for which to expand the values + */ + public expand(...expands: string[]): this { + if (expands.length > 0) { + this.query.set("$expand", expands.join(",")); + } + return this; + } + + /** + * Clones this SharePointQueryable into a new SharePointQueryable instance of T + * @param factory Constructor used to create the new instance + * @param additionalPath Any additional path to include in the clone + * @param includeBatch If true this instance's batch will be added to the cloned instance + */ + public clone(factory: (...args: any[]) => T, additionalPath?: string, includeBatch = true): T { + + const clone: T = super.cloneTo(factory(this, additionalPath), { includeBatch }); + + // handle sp specific clone actions + const t = "@target"; + if (this.query.has(t)) { + clone.query.set(t, this.query.get(t)); + } + + return clone; + } + + /** + * The default action for this object (unless overridden spGet) + * + * @param options optional request options + */ + public defaultAction(options?: IFetchOptions): Promise { + return spGet(this, options); + } + + /** + * Gets a parent for this instance as specified + * + * @param factory The contructor for the class to create + */ + protected getParent( + factory: ISharePointQueryableConstructor, + baseUrl: string | ISharePointQueryable = this.parentUrl, + path?: string, + batch?: SPBatch): T { + + let parent = new factory(baseUrl, path).configureFrom(this); + + const t = "@target"; + if (this.query.has(t)) { + parent.query.set(t, this.query.get(t)); + } + if (batch !== undefined) { + parent = parent.inBatch(batch); + } + return parent; + } +} + +export interface ISharePointQueryable extends IGetable, IQueryable { + select(...selects: string[]): this; + expand(...expands: string[]): this; + clone(factory: (...args: any[]) => T, additionalPath?: string, includeBatch?: boolean): T; + get(options?: IFetchOptions): Promise; +} +export interface _SharePointQueryable extends IGetable { } +export const SharePointQueryable = spInvokableFactory(_SharePointQueryable); + +/** + * Represents a REST collection which can be filtered, paged, and selected + * + */ +export class _SharePointQueryableCollection extends _SharePointQueryable implements ISharePointQueryableCollection { + + /** + * Filters the returned collection (https://msdn.microsoft.com/en-us/library/office/fp142385.aspx#bk_supported) + * + * @param filter The string representing the filter query + */ + public filter(filter: string): this { + this.query.set("$filter", filter); + return this; + } + + /** + * Orders based on the supplied fields + * + * @param orderby The name of the field on which to sort + * @param ascending If false DESC is appended, otherwise ASC (default) + */ + public orderBy(orderBy: string, ascending = true): this { + const o = "$orderby"; + const query = this.query.has(o) ? this.query.get(o).split(",") : []; + query.push(`${orderBy} ${ascending ? "asc" : "desc"}`); + this.query.set(o, query.join(",")); + return this; + } + + /** + * Skips the specified number of items + * + * @param skip The number of items to skip + */ + public skip(skip: number): this { + this.query.set("$skip", skip.toString()); + return this; + } + + /** + * Limits the query to only return the specified number of items + * + * @param top The query row limit + */ + public top(top: number): this { + this.query.set("$top", top.toString()); + return this; + } +} +export interface ISharePointQueryableCollection extends IGetable, ISharePointQueryable { + filter(filter: string): this; + orderBy(orderBy: string, ascending?: boolean): this; + skip(skip: number): this; + top(top: number): this; + get(options?: IFetchOptions): Promise; +} +export interface _SharePointQueryableCollection extends IGetable { } +export const SharePointQueryableCollection = spInvokableFactory(_SharePointQueryableCollection); + +/** + * Represents an instance that can be selected + * + */ +export class _SharePointQueryableInstance extends _SharePointQueryable implements ISharePointQueryableInstance { + + /** + * Curries the update function into the common pieces + * + * @param type + * @param mapper + */ + protected _update(type: string, mapper: (data: Data, props: Props) => Return): (props: Props) => Promise { + return (props: any) => spPost(this, { + body: jsS(extend(metadata(type), props)), + headers: { + "X-HTTP-Method": "MERGE", + }, + }).then((d: Data) => mapper(d, props)); + } +} +export interface ISharePointQueryableInstance extends IGetable, ISharePointQueryable { } +export interface _SharePointQueryableInstance extends IGetable { } +export const SharePointQueryableInstance = spInvokableFactory(_SharePointQueryableInstance); diff --git a/packages/sp/src/sharing/file.ts b/packages/sp/src/sharing/file.ts new file mode 100644 index 000000000..21c68bada --- /dev/null +++ b/packages/sp/src/sharing/file.ts @@ -0,0 +1,99 @@ +import { _File } from "../files/types"; +import { + ISharingEmailData, + ISharingResult, + SharingRole, + ISharedFuncs, +} from "./types"; +import { + shareWith, + getShareLink, + checkPermissions, + getSharingInformation, + getObjectSharingSettings, + unshareObject, + deleteLinkByKind, + unshareLink, +} from "./funcs"; + +/** +* Extend _File +*/ +declare module "../files/types" { + interface _File extends ISharedFuncs { + shareWith(loginNames: string | string[], role?: SharingRole, requireSignin?: boolean, emailData?: ISharingEmailData): Promise; + } + interface IFile extends ISharedFuncs { + shareWith(loginNames: string | string[], role?: SharingRole, requireSignin?: boolean, emailData?: ISharingEmailData): Promise; + } +} + +/** + * Shares this item with one or more users + * + * @param loginNames string or string[] of resolved login names to which this item will be shared + * @param role The role (View | Edit) applied to the share + * @param shareEverything Share everything in this folder, even items with unique permissions. + * @param requireSignin If true the user must signin to view link, otherwise anyone with the link can access the resource + * @param emailData Optional, if inlucded an email will be sent. Note subject currently has no effect. + */ +_File.prototype.shareWith = function ( + this: _File, + loginNames: string | string[], + role: SharingRole = SharingRole.View, + requireSignin = false, + emailData?: ISharingEmailData): Promise { + + return shareWith(this, loginNames, role, requireSignin, false, emailData); +}; + +/** + * Gets a link suitable for sharing for this item + * + * @param kind The type of link to share + * @param expiration The optional expiration date + */ +_File.prototype.getShareLink = getShareLink; + +/** + * Checks Permissions on the list of Users and returns back role the users have on the Item. + * + * @param recipients The array of Entities for which Permissions need to be checked. + */ +_File.prototype.checkSharingPermissions = checkPermissions; + +/** + * Get Sharing Information. + * + * @param request The SharingInformationRequest Object. + * @param expands Expand more fields. + * + */ +_File.prototype.getSharingInformation = getSharingInformation; + +/** + * Gets the sharing settings of an item. + * + * @param useSimplifiedRoles Determines whether to use simplified roles. + */ +_File.prototype.getObjectSharingSettings = getObjectSharingSettings; + +/** + * Unshare this item + */ +_File.prototype.unshare = unshareObject; + +/** + * Deletes a sharing link by kind + * + * @param kind Deletes a sharing link by the kind of link + */ +_File.prototype.deleteSharingLinkByKind = deleteLinkByKind; + +/** + * Removes the specified link to the item. + * + * @param kind The kind of link to be deleted. + * @param shareId + */ +_File.prototype.unshareLink = unshareLink; diff --git a/packages/sp/src/sharing/folder.ts b/packages/sp/src/sharing/folder.ts new file mode 100644 index 000000000..dd9d3f7d6 --- /dev/null +++ b/packages/sp/src/sharing/folder.ts @@ -0,0 +1,101 @@ +import { + _Folder, +} from "../folders/types"; +import { + ISharingEmailData, + ISharingResult, + SharingRole, + ISharedFuncs, +} from "./types"; +import { + shareWith, + getShareLink, + checkPermissions, + getSharingInformation, + getObjectSharingSettings, + unshareObject, + deleteLinkByKind, + unshareLink, +} from "./funcs"; + +/** +* Extend Folder +*/ +declare module "../folders/types" { + interface _Folder extends ISharedFuncs { + shareWith(loginNames: string | string[], role?: SharingRole, requireSignin?: boolean, shareEverything?: boolean, emailData?: ISharingEmailData): Promise; + } + interface IFolder extends ISharedFuncs { + shareWith(loginNames: string | string[], role?: SharingRole, requireSignin?: boolean, shareEverything?: boolean, emailData?: ISharingEmailData): Promise; + } +} + +/** + * Shares this item with one or more users + * + * @param loginNames string or string[] of resolved login names to which this item will be shared + * @param role The role (View | Edit) applied to the share + * @param shareEverything Share everything in this folder, even items with unique permissions. + * @param requireSignin If true the user must signin to view link, otherwise anyone with the link can access the resource + * @param emailData Optional, if inlucded an email will be sent. Note subject currently has no effect. + */ +_Folder.prototype.shareWith = async function ( + loginNames: string | string[], + role: SharingRole = SharingRole.View, + requireSignin = false, + shareEverything = false, + emailData?: ISharingEmailData): Promise { + + return shareWith(this, loginNames, role, requireSignin, shareEverything, emailData); +}; + +/** + * Gets a link suitable for sharing for this item + * + * @param kind The type of link to share + * @param expiration The optional expiration date + */ +_Folder.prototype.getShareLink = getShareLink; + +/** + * Checks Permissions on the list of Users and returns back role the users have on the Item. + * + * @param recipients The array of Entities for which Permissions need to be checked. + */ +_Folder.prototype.checkSharingPermissions = checkPermissions; + +/** + * Get Sharing Information. + * + * @param request The SharingInformationRequest Object. + * @param expands Expand more fields. + * + */ +_Folder.prototype.getSharingInformation = getSharingInformation; + +/** + * Gets the sharing settings of an item. + * + * @param useSimplifiedRoles Determines whether to use simplified roles. + */ +_Folder.prototype.getObjectSharingSettings = getObjectSharingSettings; + +/** + * Unshare this item + */ +_Folder.prototype.unshare = unshareObject; + +/** + * Deletes a sharing link by kind + * + * @param kind Deletes a sharing link by the kind of link + */ +_Folder.prototype.deleteSharingLinkByKind = deleteLinkByKind; + +/** + * Removes the specified link to the item. + * + * @param kind The kind of link to be deleted. + * @param shareId + */ +_Folder.prototype.unshareLink = unshareLink; diff --git a/packages/sp/src/sharing/funcs.ts b/packages/sp/src/sharing/funcs.ts new file mode 100644 index 000000000..d8e1f864c --- /dev/null +++ b/packages/sp/src/sharing/funcs.ts @@ -0,0 +1,241 @@ +import { body } from "@pnp/odata"; +import { jsS, extend } from "@pnp/common"; +import { SharePointQueryableCollection, _SharePointQueryableInstance, SharePointQueryableInstance } from "../sharepointqueryable"; +import { extractWebUrl } from "../utils/extractweburl"; +import { Web, _Web } from "../webs/types"; +import { _File } from "../files/types"; +import { + ShareableQueryable, + ISharingResult, + SharingRole, + IShareObjectOptions, + SharingLinkKind, + IShareLinkResponse, + ISharingInformationRequest, + ISharingRecipient, + ISharingEntityPermission, + ISharingInformation, + IObjectSharingSettings, + ISharingEmailData, + RoleType, +} from "./types"; +import { spPost } from "../operations"; + +/** + * Shares an object based on the supplied options + * + * @param options The set of options to send to the ShareObject method + * @param bypass If true any processing is skipped and the options are sent directly to the ShareObject method + */ +export async function shareObject(o: ShareableQueryable, options: IShareObjectOptions, bypass = false): Promise { + + if (bypass) { + + // if the bypass flag is set send the supplied parameters directly to the service + return sendShareObjectRequest(o, options); + } + + // extend our options with some defaults + options = extend(options, { + group: null, + includeAnonymousLinkInEmail: false, + propagateAcl: false, + useSimplifiedRoles: true, + }, true); + + const roleValue = await getRoleValue(options.role, options.group); + + // handle the multiple input types + if (!Array.isArray(options.loginNames)) { + options.loginNames = [options.loginNames]; + } + + const userStr = jsS(options.loginNames.map(Key => ({ Key }))); + + let postBody = { + peoplePickerInput: userStr, + roleValue: roleValue, + url: options.url, + }; + + if (options.emailData !== undefined && options.emailData !== null) { + postBody = extend(postBody, { + emailBody: options.emailData.body, + emailSubject: options.emailData.subject !== undefined ? options.emailData.subject : "Shared with you.", + sendEmail: true, + }); + } + + return sendShareObjectRequest(o, postBody); +} + +/** + * Gets a sharing link for the supplied + * + * @param kind The kind of link to share + * @param expiration The optional expiration for this link + */ +export function getShareLink(this: ShareableQueryable, kind: SharingLinkKind, expiration: Date = null): Promise { + + // date needs to be an ISO string or null + const expString = expiration !== null ? expiration.toISOString() : null; + + // clone using the factory and send the request + return spPost(this.clone(SharePointQueryableInstance, "shareLink"), body({ + request: { + createLink: true, + emailData: null, + settings: { + expiration: expString, + linkKind: kind, + }, + }, + })); +} + +/** + * Checks Permissions on the list of Users and returns back role the users have on the Item. + * + * @param recipients The array of Entities for which Permissions need to be checked. + */ +export function checkPermissions(this: ShareableQueryable, recipients: ISharingRecipient[]): Promise { + + return spPost(this.clone(SharePointQueryableInstance, "checkPermissions"), body({ recipients })); +} + +/** + * Get Sharing Information. + * + * @param request The SharingInformationRequest Object. + * @param expands Expand more fields. + * + */ +export function getSharingInformation(this: ShareableQueryable, request: ISharingInformationRequest = null, expands?: string[]): Promise { + + return spPost(this.clone(SharePointQueryableInstance, "getSharingInformation").expand(...expands), body({ request })); +} + +/** + * Gets the sharing settings of an item. + * + * @param useSimplifiedRoles Determines whether to use simplified roles. + */ +export function getObjectSharingSettings(this: ShareableQueryable, useSimplifiedRoles = true): Promise { + + return spPost(this.clone(SharePointQueryableInstance, "getObjectSharingSettings"), body({ useSimplifiedRoles })); +} + +/** + * Unshares this object + */ +export function unshareObject(this: ShareableQueryable): Promise { + return spPost(this.clone(SharePointQueryableInstance, "unshareObject")); +} + +/** + * Deletes a link by type + * + * @param kind Deletes a sharing link by the kind of link + */ +export function deleteLinkByKind(this: ShareableQueryable, linkKind: SharingLinkKind): Promise { + return spPost(this.clone(SharePointQueryableInstance, "deleteLinkByKind"), body({ linkKind })); +} + +/** + * Removes the specified link to the item. + * + * @param kind The kind of link to be deleted. + * @param shareId + */ +export function unshareLink(this: ShareableQueryable, linkKind: SharingLinkKind, shareId = "00000000-0000-0000-0000-000000000000"): Promise { + return spPost(this.clone(SharePointQueryableInstance, "unshareLink"), body({ linkKind, shareId })); +} + +/** + * Shares this instance with the supplied users + * + * @param loginNames Resolved login names to share + * @param role The role + * @param requireSignin True to require the user is authenticated, otherwise false + * @param propagateAcl True to apply this share to all children + * @param emailData If supplied an email will be sent with the indicated properties + */ +export async function shareWith( + o: ShareableQueryable, + loginNames: string | string[], + role: SharingRole, + requireSignin = false, + propagateAcl = false, + emailData?: ISharingEmailData): Promise { + + // handle the multiple input types + if (!Array.isArray(loginNames)) { + loginNames = [loginNames]; + } + + const userStr = jsS(loginNames.map(login => { return { Key: login }; })); + const roleFilter = role === SharingRole.Edit ? RoleType.Contributor : RoleType.Reader; + + // start by looking up the role definition id we need to set the roleValue + // remove need to reference Web here, which created a circular build issue + const w = SharePointQueryableCollection("_api/web", "roledefinitions"); + const def = await w.select("Id").filter(`RoleTypeKind eq ${roleFilter}`).get(); + if (!Array.isArray(def) || def.length < 1) { + throw Error(`Could not locate a role defintion with RoleTypeKind ${roleFilter}`); + } + let postBody = { + includeAnonymousLinkInEmail: requireSignin, + peoplePickerInput: userStr, + propagateAcl: propagateAcl, + roleValue: `role:${def[0].Id}`, + useSimplifiedRoles: true, + }; + if (emailData !== undefined) { + postBody = extend(postBody, { + emailBody: emailData.body, + emailSubject: emailData.subject !== undefined ? emailData.subject : "", + sendEmail: true, + }); + } + + return spPost(o.clone(SharePointQueryableInstance, "shareObject"), body(postBody)); +} + +function sendShareObjectRequest(o: ShareableQueryable, options: any): Promise { + return spPost(Web(extractWebUrl(o.toUrl()), "/_api/SP.Web.ShareObject").expand("UsersWithAccessRequests", "GroupsSharedWith"), body(options)); +} + +/** + * Calculates the roleValue string used in the sharing query + * + * @param role The Sharing Role + * @param group The Group type + */ +async function getRoleValue(role: SharingRole, group: RoleType): Promise { + + // we will give group precedence, because we had to make a choice + if (group !== undefined && group !== null) { + + switch (group) { + case RoleType.Contributor: + const g1 = await Web("_api/web", "associatedmembergroup").select("Id")<{ Id: number; }>(); + return `group: ${g1.Id}`; + case RoleType.Reader: + case RoleType.Guest: + const g2 = await Web("_api/web", "associatedvisitorgroup").select("Id")<{ Id: number; }>(); + return `group: ${g2.Id}`; + default: + throw Error("Could not determine role value for supplied value. Contributor, Reader, and Guest are supported"); + } + } else { + + // TODO:: update this section once we update role defs factory + const roleFilter = role === SharingRole.Edit ? RoleType.Contributor : RoleType.Reader; + const def = await SharePointQueryableCollection("_api/web", "roledefinitions").select("Id").top(1).filter(`RoleTypeKind eq ${roleFilter}`)<{ Id: number; }[]>(); + if (def.length < 1) { + throw Error("Could not locate associated role definition for supplied role. Edit and View are supported"); + } + return `role: ${def[0].Id}`; + } +} + diff --git a/packages/sp/src/sharing/index.ts b/packages/sp/src/sharing/index.ts new file mode 100644 index 000000000..af7b55802 --- /dev/null +++ b/packages/sp/src/sharing/index.ts @@ -0,0 +1,27 @@ +import "./file"; +import "./folder"; +import "./item"; +import "./web"; + +export { + ISharingEmailData, + ISharingEntityPermission, + ISharingInformation, + ISharingInformationRequest, + ISharingLinkInfo, + ISharingRecipient, + ISharingResult, + IUserSharingResult, + SPSharedObjectType, + SharingDomainRestrictionMode, + SharingLinkKind, + SharingOperationStatusCode, + SharingRole, + IInvitationCreationResult, + IObjectSharingSettings, + IShareLinkRequest, + IShareLinkResponse, + IShareLinkSettings, + IShareObjectOptions, + RoleType, +} from "./types"; diff --git a/packages/sp/src/sharing/item.ts b/packages/sp/src/sharing/item.ts new file mode 100644 index 000000000..3a023d837 --- /dev/null +++ b/packages/sp/src/sharing/item.ts @@ -0,0 +1,98 @@ +import { _Item } from "../items/types"; +import { + ISharingEmailData, + ISharingResult, + SharingRole, + ISharedFuncs, +} from "./types"; + +import { + shareWith, + getShareLink, + checkPermissions, + getSharingInformation, + getObjectSharingSettings, + unshareObject, + deleteLinkByKind, + unshareLink, +} from "./funcs"; + +/** + * Extend _Web + */ +declare module "../items/types" { + interface _Item extends ISharedFuncs { + shareWith(loginNames: string | string[], role?: SharingRole, requireSignin?: boolean, emailData?: ISharingEmailData): Promise; + } + interface IItem extends ISharedFuncs { + shareWith(loginNames: string | string[], role?: SharingRole, requireSignin?: boolean, emailData?: ISharingEmailData): Promise; + } +} + +/** + * Gets a link suitable for sharing for this item + * + * @param kind The type of link to share + * @param expiration The optional expiration date + */ +_Item.prototype.getShareLink = getShareLink; + +/** + * Shares this item with one or more users + * + * @param loginNames string or string[] of resolved login names to which this item will be shared + * @param role The role (View | Edit) applied to the share + * @param emailData Optional, if inlucded an email will be sent. Note subject currently has no effect. + */ +_Item.prototype.shareWith = function ( + this: _Item, + loginNames: string | string[], + role: SharingRole = SharingRole.View, + requireSignin = false, + emailData?: ISharingEmailData): Promise { + + return shareWith(this, loginNames, role, requireSignin, false, emailData); +}; + +/** + * Checks Permissions on the list of Users and returns back role the users have on the Item. + * + * @param recipients The array of Entities for which Permissions need to be checked. + */ +_Item.prototype.checkSharingPermissions = checkPermissions; + +/** + * Get Sharing Information. + * + * @param request The SharingInformationRequest Object. + * @param expands Expand more fields. + * + */ +_Item.prototype.getSharingInformation = getSharingInformation; + +/** + * Gets the sharing settings of an item. + * + * @param useSimplifiedRoles Determines whether to use simplified roles. + */ +_Item.prototype.getObjectSharingSettings = getObjectSharingSettings; + +/** + * Unshare this item + */ +_Item.prototype.unshare = unshareObject; + +/** + * Deletes a sharing link by kind + * + * @param kind Deletes a sharing link by the kind of link + */ +_Item.prototype.deleteSharingLinkByKind = deleteLinkByKind; + +/** + * Removes the specified link to the item. + * + * @param kind The kind of link to be deleted. + * @param shareId + */ +_Item.prototype.unshareLink = unshareLink; diff --git a/packages/sp/src/sharing/types.ts b/packages/sp/src/sharing/types.ts new file mode 100644 index 000000000..72cb0e859 --- /dev/null +++ b/packages/sp/src/sharing/types.ts @@ -0,0 +1,584 @@ +import { IPrincipalInfo } from "../types"; +import { _Web } from "../webs/types"; +import { _File } from "../files/types"; +import { _Item } from "../items/types"; +import { _Folder } from "../folders/types"; + +export type ShareableQueryable = _Web | _File | _Folder | _Item; + +/** + * Indicates the role of the sharing link + */ +export enum SharingRole { + None = 0, + View = 1, + Edit = 2, + Owner = 3, +} + +export enum SPSharedObjectType { + Unknown = 0, + File = 1, + Folder = 2, + Item = 3, + List = 4, + Web = 5, + Max = 6, +} + +export enum SharingDomainRestrictionMode { + None = 0, + AllowList = 1, + BlockList = 2, +} + +export enum SharingOperationStatusCode { + /** + * The share operation completed without errors. + */ + CompletedSuccessfully = 0, + /** + * The share operation completed and generated requests for access. + */ + AccessRequestsQueued = 1, + /** + * The share operation failed as there were no resolved users. + */ + NoResolvedUsers = -1, + /** + * The share operation failed due to insufficient permissions. + */ + AccessDenied = -2, + /** + * The share operation failed when attempting a cross site share, which is not supported. + */ + CrossSiteRequestNotSupported = -3, + /** + * The sharing operation failed due to an unknown error. + */ + UnknowError = -4, + /** + * The text you typed is too long. Please shorten it. + */ + EmailBodyTooLong = -5, + /** + * The maximum number of unique scopes in the list has been exceeded. + */ + ListUniqueScopesExceeded = -6, + /** + * The share operation failed because a sharing capability is disabled in the site. + */ + CapabilityDisabled = -7, + /** + * The specified object for the share operation is not supported. + */ + ObjectNotSupported = -8, + /** + * A SharePoint group cannot contain another SharePoint group. + */ + NestedGroupsNotSupported = -9, +} + +export enum SharingLinkKind { + /** + * Uninitialized link + */ + Uninitialized = 0, + /** + * Direct link to the object being shared + */ + Direct = 1, + /** + * Organization-shareable link to the object being shared with view permissions + */ + OrganizationView = 2, + /** + * Organization-shareable link to the object being shared with edit permissions + */ + OrganizationEdit = 3, + /** + * View only anonymous link + */ + AnonymousView = 4, + /** + * Read/Write anonymous link + */ + AnonymousEdit = 5, + /** + * Flexible sharing Link where properties can change without affecting link URL + */ + Flexible = 6, +} + +export interface ISharedFuncs { + getShareLink(kind?: SharingLinkKind, expiration?: Date): Promise; + checkSharingPermissions(recipients: ISharingRecipient[]): Promise; + getSharingInformation(request?: ISharingInformationRequest, expands?: string[]): Promise; + getObjectSharingSettings(useSimplifiedRoles?: boolean): Promise; + unshare(): Promise; + deleteSharingLinkByKind(kind: SharingLinkKind): Promise; + unshareLink(kind: SharingLinkKind, shareId?: string): Promise; +} + +export interface IShareObjectOptions { + url?: string; + loginNames?: string | string[]; + role: SharingRole; + emailData?: ISharingEmailData; + group?: RoleType; + propagateAcl?: boolean; + includeAnonymousLinkInEmail?: boolean; + useSimplifiedRoles?: boolean; +} + +/** + * Represents email data. + */ +export interface ISharingEmailData { + + /** + * The e-mail subject. + */ + subject?: string; + + /** + * The e-mail body. + */ + body: string; +} + +export interface IShareLinkSettings { + /** + * The optional unique identifier of an existing sharing link to be retrieved and updated if necessary. + */ + shareId?: string; + + /** + * The kind of the sharing link to be created. + */ + linkKind: SharingLinkKind; + + /** + * A date/time string for which the format conforms to the ISO 8601:2004(E) complete representation for calendar date and time of day and + * which represents the time and date of expiry for the anonymous link. Both the minutes and hour value must be specified for the + * difference between the local and UTC time. Midnight is represented as 00:00:00. + */ + expiration?: string; + + /** + * The role to be used for the sharing link. This is required for Flexible links, and ignored for legacy link kinds. + */ + role?: SharingRole; + + /** + * Indicates if the sharing link, should support anonymous access. This is required for Flexible links, and ignored for legacy link kinds. + */ + allowAnonymousAccess?: boolean; +} + +export interface IShareLinkRequest { + + /** + * A string of JSON representing users in people picker format. Only needed if an e-mail notification should be sent. + */ + peoplePickerInput?: string; + + /** + * Whether to create the link or not if it doesn't exist yet. + */ + createLink: boolean; + + /** + * The e-mail data. Only needed if an e-mail notification should be sent. + */ + emailData?: ISharingEmailData; + + /** + * The settings for the sharing link to be created/updated + */ + settings: IShareLinkSettings; +} + +/** + * Represents a response for sharing a link + */ +export interface IShareLinkResponse { + /** + * A SharingLinkInfo that represents the sharing link. Will be populated if sharing operation is returning a sharing link. + */ + sharingLinkInfo: ISharingLinkInfo; +} + +export interface ISharingLinkInfo { + + AllowsAnonymousAccess: boolean; + Created: string; + CreatedBy: IPrincipalInfo; + Expiration: string; + IsActive: boolean; + IsEditLink: boolean; + IsFormsLink: boolean; + IsUnhealthy: boolean; + LastModified: string; + LastModifiedBy: IPrincipalInfo; + LinkKind: SharingLinkKind; + ShareId: string; + Url: string; +} + +export interface ISharingResult { + + /** + * The relative URL of a page which can be navigated to, to show permissions. + */ + PermissionsPageRelativeUrl?: string; + + /** + * A collection of users which have new pending access requests as a result of sharing. + */ + UsersWithAccessRequests?: any[]; // SPSharingUserCollection + + /** + * An enumeration which summarizes the result of the sharing operation. + */ + StatusCode?: SharingOperationStatusCode; + + /** + * An error message about the failure if sharing was unsuccessful. + */ + ErrorMessage?: string; + + /** + * A list of UserSharingResults from attempting to share a securable with unique permissions. + */ + UniquelyPermissionedUsers?: IUserSharingResult[]; + /** + * Groups which were granted permissions. + */ + GroupsSharedWith?: any[]; // SPGroupCollection + + /** + * The SharePoint group users were added to, if any were added to a group. + */ + GroupUsersAddedTo?: any; // SPGroup + + /** + * A list of users being added to a SharePoint permissions goup + */ + UsersAddedToGroup?: IUserSharingResult[]; + + /** + * A list of SPInvitationCreationResult for external users being invited to have access. + */ + InvitedUsers?: IInvitationCreationResult[]; + + /** + * The name of the securable being shared. + */ + Name?: string; + + /** + * The url of the securable being shared. + */ + Url?: string; + + /** + * IconUrl + */ + IconUrl?: string; +} + +export interface IInvitationCreationResult { + Succeeded?: boolean; + Email?: string; + InvitationLink?: string; +} + +export interface IUserSharingResult { + IsUserKnown?: boolean; + Status?: boolean; + Message?: string; + User?: string; + DisplayName?: string; + Email?: string; + CurrentRole?: SharingRole; + AllowedRoles?: SharingRole[]; + InvitationLink?: string; +} + +export interface ISharingRecipient { + email?: string; + alias?: string; +} + +export interface ISharingEntityPermission { + /** + * The Input Entity provided to the Call. + */ + inputEntity: string; + /** + * The Resolved Entity after resolving using PeoplePicker API. + */ + resolvedEntity: string; + /** + * Does the Entity have Access to the Securable Object + */ + hasAccess: boolean; + /** + * Role of the Entity on ListItem + */ + role: SharingRole; +} + +export interface ISharingInformationRequest { + /** + * Max Principal's to return. + */ + maxPrincipalsToReturn: number; + /** + * Supported Features (For future use by Office Client). + */ + clientSupportedFeatures: string; +} + +export interface IObjectSharingSettings { + /** + * The URL pointing to the containing SPWeb object + */ + WebUrl: string; + /** + * The unique ID of the parent list (if applicable) + */ + ListId?: string; + /** + * The list item ID (if applicable) + */ + ItemId?: string; + /** + * The object title + */ + ItemName: string; + /** + * The server relative object URL + */ + ItemUrl: string; + /** + * Contains information about the sharing state of a shareable object + */ + ObjectSharingInformation: any; // SPObjectSharingInformation + /** + * Boolean indicating whether the sharing context operates under the access request mode + */ + AccessRequestMode: boolean; + /** + * Boolean indicating whether the sharing context operates under the permissions only mode + * (i.e. adding to a group or hiding the groups dropdown in the SharePoint UI) + */ + PermissionsOnlyMode: boolean; + /** + * URL of the site from which the shared object inherits permissions + */ + InheritingWebLink: string; + /** + * Boolean flag denoting if guest users are enabled for the site collection + */ + ShareByEmailEnabled: boolean; + /** + * Boolean indicating whether the current user is a guest user + */ + IsGuestUser: boolean; + /** + * Boolean indicating whether the site has the standard "Editor" role + */ + HasEditRole: boolean; + /** + * Boolean indicating whether the site has the standard "Reader" role + */ + HasReadRole: boolean; + /** + * Boolean indicating whether the object to share is a picture library + */ + IsPictureLibrary: boolean; + /** + * Boolean indicating whether the folder object can be shared + */ + CanShareFolder: boolean; + /** + * Boolean indicating whether email invitations can be sent + */ + CanSendEmail: boolean; + /** + * Default share link type + */ + DefaultShareLinkType: SharingLinkKind; + /** + * Boolean indicating whether the object to share supports ACL propagation + */ + SupportsAclPropagation: boolean; + /** + * Boolean indicating whether the current user can only share within the tenancy + */ + CanCurrentUserShareInternally: boolean; + /** + * Boolean indicating whether the current user can share outside the tenancy, by inviting external users + */ + CanCurrentUserShareExternally: boolean; + /** + * Boolean indicating whether the current user can retrieve an anonymous View link, if one has already been created + * If one has not been created, the user cannot create one + */ + CanCurrentUserRetrieveReadonlyLink: boolean; + /** + * Boolean indicating whether the current user can create or disable an anonymous Edit link + */ + CanCurrentUserManageReadonlyLink: boolean; + /** + * Boolean indicating whether the current user can retrieve an anonymous Edit link, if one has already been created + * If one has not been created, the user cannot create one + */ + CanCurrentUserRetrieveReadWriteLink: boolean; + /** + * Boolean indicating whether the current user can create or disable an anonymous Edit link + */ + CanCurrentUserManageReadWriteLink: boolean; + /** + * Boolean indicating whether the current user can retrieve an organization View link, if one has already been created + * If one has not been created, the user cannot create one + */ + CanCurrentUserRetrieveOrganizationReadonlyLink: boolean; + /** + * Boolean indicating whether the current user can create or disable an organization Edit link + */ + CanCurrentUserManageOrganizationReadonlyLink: boolean; + /** + * Boolean indicating whether the current user can retrieve an organization Edit link, if one has already been created + * If one has not been created, the user cannot create one + */ + CanCurrentUserRetrieveOrganizationReadWriteLink: boolean; + /** + * Boolean indicating whether the current user can create or disable an organization Edit link + */ + CanCurrentUserManageOrganizationReadWriteLink: boolean; + /** + * Boolean indicating whether the current user can make use of Share-By-Link + */ + CanSendLink: boolean; + /** + * Boolean indicating whether the client logic should warn the user + * that they are about to share with external email addresses. + */ + ShowExternalSharingWarning: boolean; + /** + * A list of SharingPermissionInformation objects that can be used to share + */ + SharingPermissions: any[]; // SPSharingPermissionInformationCollection + /** + * A dictionary object that lists the display name and the id of + * the SharePoint simplified roles (edit, view) + */ + SimplifiedRoles: { [key: string]: string }; + /** + * A dictionary object that lists the display name and the id of the SharePoint groups + */ + GroupsList: { [key: string]: string }; + /** + * A dictionary object that lists the display name and the id of the SharePoint regular roles + */ + Roles: { [key: string]: string }; + /** + * An object containing the SharePoint UI specific sharing settings. + */ + SharePointSettings: any; // SharePointSharingSettings + /** + * Boolean indicating whether the current user is a site collection administrator + */ + IsUserSiteAdmin: boolean; + /** + * A value that indicates number of days an anonymous link can be valid before it expires + */ + RequiredAnonymousLinkExpirationInDays: number; +} + +export interface ISharingInformation { + /** + * External Sharing. + */ + canAddExternalPrincipal?: boolean; + /** + * Internal Sharing. + */ + canAddInternalPrincipal?: boolean; + /** + * Can Send Email. + */ + canSendEmail?: boolean; + /** + * Can Use Simplified Roles present in Roles Enum. + */ + canUseSimplifiedRoles?: boolean; + /** + * Has Unique Permissions. + */ + hasUniquePermissions?: boolean; + /** + * Current Users Role on the Item. + */ + currentRole?: SharingRole; + /** + * Does the User+Item require Approval from Admin for Sharing. + */ + requiresAccessApproval?: boolean; + /** + * (Owners only)Whether there are pending access requests for the securable object. + */ + hasPendingAccessRequests?: boolean; + /** + * (Owners only)The link to the access requests page for the securable object, or an empty string if the link is not available. + */ + pendingAccessRequestsLink?: string; + /** + * sharedObjectType + */ + sharedObjectType?: SPSharedObjectType; + /** + * Url for the Securable Object (Encoded). + */ + directUrl?: string; + /** + * Parent Web Url for the Securable Object (Encoded). + */ + webUrl?: string; + /** + * Default SharingLinkKind. + */ + defaultLinkKind?: SharingLinkKind; + /** + * Tenant's SharingDomainRestrictionMode. + */ + domainRestrictionMode?: SharingDomainRestrictionMode; + /** + * Tenant's RestrictedDomains. + */ + RestrictedDomains?: string; + /** + * Tenant's Anonymous Link Expiration Restriction in Days. + */ + anonymousLinkExpirationRestrictionDays?: number; + /** + * The PermissionCollection that are on the Securable Object (Princpals & Links) + */ + permissionsInformation?: any; // PermissionCollection + /** + * PickerSettings used by the PeoplePicker Control. + */ + pickerSettings?: any; // PickerSettings +} + +export enum RoleType { + None = 0, + Guest = 1, + Reader = 2, + Contributor = 3, + WebDesigner = 4, + Administrator = 5, +} diff --git a/packages/sp/src/sharing/web.ts b/packages/sp/src/sharing/web.ts new file mode 100644 index 000000000..93cfc74e3 --- /dev/null +++ b/packages/sp/src/sharing/web.ts @@ -0,0 +1,116 @@ +import { _Web, Web } from "../webs/types"; +import { ISharingEmailData, ISharingResult, SharingRole } from "./types"; +import { _SharePointQueryableInstance } from "../sharepointqueryable"; +import { extractWebUrl } from "../utils/extractweburl"; +import { RoleType } from "./types"; +import { shareObject } from "./funcs"; +import { combine } from "@pnp/common"; +import { body } from "@pnp/odata"; +import { spPost } from "../operations"; + +/** + * Extend _Web + */ +declare module "../webs/types" { + interface _Web { + shareWith: (loginNames: string | string[], role?: SharingRole, emailData?: ISharingEmailData) => Promise; + shareObject: (url: string, + loginNames: string | string[], + role: SharingRole, + emailData?: ISharingEmailData, + group?: RoleType, + propagateAcl?: boolean, + includeAnonymousLinkInEmail?: boolean, + useSimplifiedRoles?: boolean) => Promise; + shareObjectRaw(options: any): Promise; + unshareObject(url: string): Promise; + } + interface IWeb { + shareWith: (loginNames: string | string[], role?: SharingRole, emailData?: ISharingEmailData) => Promise; + shareObject: (url: string, + loginNames: string | string[], + role: SharingRole, + emailData?: ISharingEmailData, + group?: RoleType, + propagateAcl?: boolean, + includeAnonymousLinkInEmail?: boolean, + useSimplifiedRoles?: boolean) => Promise; + shareObjectRaw(options: any): Promise; + unshareObject(url: string): Promise; + } +} + +/** + * Shares this web with the supplied users + * @param loginNames The resolved login names to share + * @param role The role to share this web + * @param emailData Optional email data + */ +_Web.prototype.shareWith = async function ( + this: _Web, + loginNames: string | string[], + role: SharingRole = SharingRole.View, + emailData?: ISharingEmailData): Promise { + + const dependency = this.addBatchDependency(); + // remove need to reference Web here, which created a circular build issue + const web = new _SharePointQueryableInstance(extractWebUrl(this.toUrl()), "/_api/web/url"); + + const url = await web.get(); + dependency(); + + return this.shareObject(combine(url, "/_layouts/15/aclinv.aspx?forSharing=1&mbypass=1"), loginNames, role, emailData); +}; + +/** + * Provides direct access to the static web.ShareObject method + * + * @param url The url to share + * @param loginNames Resolved loginnames string[] of a single login name string + * @param roleValue Role value + * @param emailData Optional email data + * @param groupId Optional group id + * @param propagateAcl + * @param includeAnonymousLinkInEmail + * @param useSimplifiedRoles + */ +_Web.prototype.shareObject = function ( + this: _Web, + url: string, + loginNames: string | string[], + role: SharingRole, + emailData?: ISharingEmailData, + group?: RoleType, + propagateAcl = false, + includeAnonymousLinkInEmail = false, + useSimplifiedRoles = true): Promise { + + return shareObject(this, { + emailData: emailData, + group: group, + includeAnonymousLinkInEmail: includeAnonymousLinkInEmail, + loginNames: loginNames, + propagateAcl: propagateAcl, + role: role, + url: url, + useSimplifiedRoles: useSimplifiedRoles, + }); +}; + +/** + * Supplies a method to pass any set of arguments to ShareObject + * + * @param options The set of options to send to ShareObject + */ +_Web.prototype.shareObjectRaw = function (this: _Web, options: any): Promise { + return shareObject(this, options, true); +}; + +/** + * Supplies a method to pass any set of arguments to ShareObject + * + * @param options The set of options to send to ShareObject + */ +_Web.prototype.unshareObject = function (this: _Web, url: string): Promise { + return spPost(Web(this, "unshareObject"), body({ url })); +}; diff --git a/packages/sp/src/site-designs/index.ts b/packages/sp/src/site-designs/index.ts new file mode 100644 index 000000000..aeaf37cd8 --- /dev/null +++ b/packages/sp/src/site-designs/index.ts @@ -0,0 +1,28 @@ +import { SPRest } from "../rest"; +import { ISiteDesigns, SiteDesigns } from "./types"; + +export { + ISiteDesignCreationInfo, + ISiteDesignInfo, + ISiteDesignPrincipals, + ISiteDesignUpdateInfo, + ISiteDesigns, + SiteDesigns, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly siteDesigns: ISiteDesigns; + } +} + +Reflect.defineProperty(SPRest.prototype, "siteDesigns", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return SiteDesigns(this._baseUrl); + }, +}); diff --git a/packages/sp/src/site-designs/types.ts b/packages/sp/src/site-designs/types.ts new file mode 100644 index 000000000..f8c533844 --- /dev/null +++ b/packages/sp/src/site-designs/types.ts @@ -0,0 +1,243 @@ +import { _SharePointQueryable, ISharePointQueryable } from "../sharepointqueryable"; +import { extractWebUrl } from "../utils/extractweburl"; +import { headers, body } from "@pnp/odata"; +import { spPost } from "../operations"; + +/** + * Implements the site designs API REST methods + * + */ +export class _SiteDesigns extends _SharePointQueryable implements ISiteDesigns, ISharePointQueryable { + /** + * Creates a new instance of the SiteDesigns method class + * + * @param baseUrl The parent url provider + * @param methodName The static method name to call on the utility class + */ + constructor(baseUrl: string | ISharePointQueryable, methodName = "") { + const url = typeof baseUrl === "string" ? baseUrl : baseUrl.toUrl(); + super(extractWebUrl(url), `_api/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.${methodName}`); + } + + public execute(props: any): Promise { + return spPost(this, body(props, headers({ "Content-Type": "application/json;charset=utf-8" }))); + } + + /** + * Creates a new site design available to users when they create a new site from the SharePoint home page. + * + * @param creationInfo A sitedesign creation information object + */ + public async createSiteDesign(creationInfo: ISiteDesignCreationInfo): Promise { + return await this.clone(SiteDesignsCloneFactory, `CreateSiteDesign`).execute({ info: creationInfo }); + } + + /** + * Applies a site design to an existing site collection. + * + * @param siteDesignId The ID of the site design to apply. + * @param webUrl The URL of the site collection where you want to apply the site design. + */ + public async applySiteDesign(siteDesignId: string, webUrl: string): Promise { + return await this.clone(SiteDesignsCloneFactory, `ApplySiteDesign`).execute({ siteDesignId: siteDesignId, "webUrl": webUrl }); + } + + /** + * Gets a list of information about existing site designs. + */ + public async getSiteDesigns(): Promise { + return await this.clone(SiteDesignsCloneFactory, `GetSiteDesigns`).execute({}); + } + + /** + * Gets information about a specific site design. + * @param id The ID of the site design to get information about. + */ + public async getSiteDesignMetadata(id: string): Promise { + return await this.clone(SiteDesignsCloneFactory, `GetSiteDesignMetadata`).execute({ id: id }); + } + + /** + * Updates a site design with new values. In the REST call, all parameters are optional except the site script Id. + * If you had previously set the IsDefault parameter to TRUE and wish it to remain true, you must pass in this parameter again (otherwise it will be reset to FALSE). + * @param updateInfo A sitedesign update information object + */ + public async updateSiteDesign(updateInfo: ISiteDesignUpdateInfo): Promise { + return await this.clone(SiteDesignsCloneFactory, `UpdateSiteDesign`).execute({ updateInfo: updateInfo }); + } + + /** + * Deletes a site design. + * @param id The ID of the site design to delete. + */ + public async deleteSiteDesign(id: string): Promise { + return await this.clone(SiteDesignsCloneFactory, `DeleteSiteDesign`).execute({ id: id }); + } + + /** + * Gets a list of principals that have access to a site design. + * @param id The ID of the site design to get rights information from. + */ + public async getSiteDesignRights(id: string): Promise { + return await this.clone(SiteDesignsCloneFactory, `GetSiteDesignRights`).execute({ id: id }); + } + + /** + * Grants access to a site design for one or more principals. + * @param id The ID of the site design to grant rights on. + * @param principalNames An array of one or more principals to grant view rights. + * Principals can be users or mail-enabled security groups in the form of "alias" or "alias@.com" + * @param grantedRights Always set to 1. This represents the View right. + */ + public async grantSiteDesignRights(id: string, principalNames: string[], grantedRights = 1): Promise { + return await this.clone(SiteDesignsCloneFactory, `GrantSiteDesignRights`) + .execute({ + "grantedRights": grantedRights.toString(), + "id": id, + "principalNames": principalNames, + }); + } + + /** + * Revokes access from a site design for one or more principals. + * @param id The ID of the site design to revoke rights from. + * @param principalNames An array of one or more principals to revoke view rights from. + * If all principals have rights revoked on the site design, the site design becomes viewable to everyone. + */ + public async revokeSiteDesignRights(id: string, principalNames: string[]): Promise { + return await this.clone(SiteDesignsCloneFactory, `RevokeSiteDesignRights`) + .execute({ + "id": id, + "principalNames": principalNames, + }); + } +} + +export interface ISiteDesigns { + getSiteDesigns(): Promise; + createSiteDesign(creationInfo: ISiteDesignCreationInfo): Promise; + applySiteDesign(siteDesignId: string, webUrl: string): Promise; + getSiteDesignMetadata(id: string): Promise; + updateSiteDesign(updateInfo: ISiteDesignUpdateInfo): Promise; + deleteSiteDesign(id: string): Promise; + getSiteDesignRights(id: string): Promise; + grantSiteDesignRights(id: string, principalNames: string[], grantedRights?: number): Promise; + revokeSiteDesignRights(id: string, principalNames: string[]): Promise; +} + +export const SiteDesigns = (baseUrl: string | ISharePointQueryable): ISiteDesigns => new _SiteDesigns(baseUrl); + +type SiteDesignsCloneType = ISiteDesigns & ISharePointQueryable & { execute(props: any): Promise }; +const SiteDesignsCloneFactory = (baseUrl: string | ISharePointQueryable): SiteDesignsCloneType => SiteDesigns(baseUrl); + +export interface ISiteDesignInfo { + /** + * The ID of the site design to apply. + */ + Id: string; + /** + * The display name of the site design. + */ + Title: string; + /** + * Identifies which base template to add the design to. Use the value 64 for the Team site template, and the value 68 for the Communication site template. + */ + WebTemplate: string; + /** + * An array of one or more site scripts. Each is identified by an ID. The scripts will run in the order listed. + */ + SiteScriptIds: string[]; + /** + * The display description of site design. + */ + Description: string; + /** + * The URL of a preview image. If none is specified, SharePoint uses a generic image. + */ + PreviewImageUrl: string; + /** + * The alt text description of the image for accessibility. + */ + PreviewImageAltText: string; + /** + * True if the site design is applied as the default site design; otherwise, false. + * For more information see Customize a default site design https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/customize-default-site-design. + */ + IsDefault: boolean; + Version: string; +} + +export interface ISiteDesignCreationInfo { + /** + * The display name of the site design. + */ + Title: string; + /** + * Identifies which base template to add the design to. Use the value 64 for the Team site template, and the value 68 for the Communication site template. + */ + WebTemplate: string; + /** + * An array of one or more site scripts. Each is identified by an ID. The scripts will run in the order listed. + */ + SiteScriptIds?: string[]; + /** + * (Optional) The display description of site design. + */ + Description?: string; + /** + * (Optional) The URL of a preview image. If none is specified, SharePoint uses a generic image. + */ + PreviewImageUrl?: string; + /** + * (Optional) The alt text description of the image for accessibility. + */ + PreviewImageAltText?: string; + /** + * (Optional) True if the site design is applied as the default site design; otherwise, false. + * For more information see Customize a default site design https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/customize-default-site-design. + */ + IsDefault?: boolean; +} + +export interface ISiteDesignUpdateInfo { + /** + * The ID of the site design to apply. + */ + Id: string; + /** + * (Optional) The new display name of the updated site design. + */ + Title?: string; + /** + * (Optional) The new template to add the site design to. Use the value 64 for the Team site template, and the value 68 for the Communication site template. + */ + WebTemplate?: string; + /** + * (Optional) A new array of one or more site scripts. Each is identified by an ID. The scripts run in the order listed. + */ + SiteScriptIds?: string[]; + /** + * (Optional) The new display description of the updated site design. + */ + Description?: string; + /** + * (Optional) The new URL of a preview image. + */ + PreviewImageUrl?: string; + /** + * (Optional) The new alt text description of the image for accessibility. + */ + PreviewImageAltText?: string; + /** + * (Optional) True if the site design is applied as the default site design; otherwise, false. + * For more information see Customize a default site design https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/customize-default-site-design. + * If you had previously set the IsDefault parameter to TRUE and wish it to remain true, you must pass in this parameter again (otherwise it will be reset to FALSE). + */ + IsDefault?: boolean; +} + +export interface ISiteDesignPrincipals { + DisplayName: string; + PrincipalName: string; + Rights: number; +} diff --git a/packages/sp/src/site-groups/index.ts b/packages/sp/src/site-groups/index.ts new file mode 100644 index 000000000..df6c410a0 --- /dev/null +++ b/packages/sp/src/site-groups/index.ts @@ -0,0 +1,11 @@ +import "./web"; + +export { + ISiteGroup, + IGroupAddResult, + IGroupUpdateResult, + ISiteGroups, + SiteGroup, + SiteGroupAddResult, + SiteGroups, +} from "./types"; diff --git a/packages/sp/src/site-groups/types.ts b/packages/sp/src/site-groups/types.ts new file mode 100644 index 000000000..8e9b98caf --- /dev/null +++ b/packages/sp/src/site-groups/types.ts @@ -0,0 +1,143 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { SiteUsers, ISiteUsers } from "../site-users/types"; +import { extend, TypedHash, hOP } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { IGetable, body } from "@pnp/odata"; +import { defaultPath } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Describes a collection of site groups + * + */ +@defaultPath("sitegroups") +export class _SiteGroups extends _SharePointQueryableCollection implements ISiteGroups { + + /** + * Gets a group from the collection by id + * + * @param id The id of the group to retrieve + */ + public getById(id: number): ISiteGroup { + return SiteGroup(this).concat(`(${id})`); + } + + /** + * Adds a new group to the site collection + * + * @param props The group properties object of property names and values to be set for the group + */ + public async add(properties: TypedHash): Promise { + + const postBody = body(extend(metadata("SP.Group"), properties)); + + const data = await spPost(this, postBody); + return { + data, + group: this.getById(data.Id), + }; + } + + /** + * Gets a group from the collection by name + * + * @param groupName The name of the group to retrieve + */ + public getByName(groupName: string): ISiteGroup { + return SiteGroup(this, `getByName('${groupName}')`); + } + + /** + * Removes the group with the specified member id from the collection + * + * @param id The id of the group to remove + */ + public removeById(id: number): Promise { + return spPost(this.clone(SiteGroups, `removeById('${id}')`)); + } + + /** + * Removes the cross-site group with the specified name from the collection + * + * @param loginName The name of the group to remove + */ + public removeByLoginName(loginName: string): Promise { + return spPost(this.clone(SiteGroups, `removeByLoginName('${loginName}')`)); + } +} + +export interface ISiteGroups extends IGetable, ISharePointQueryableCollection { + getById(id: number): ISiteGroup; + add(properties: TypedHash): Promise; + getByName(groupName: string): ISiteGroup; + removeById(id: number): Promise; + removeByLoginName(loginName: string): Promise; +} +export interface _SiteGroups extends IGetable { } +export const SiteGroups = spInvokableFactory(_SiteGroups); + +/** + * Describes a single group + * + */ +export class _SiteGroup extends _SharePointQueryableInstance implements ISiteGroup { + + /** + * Gets the users for this group + * + */ + public get users(): ISiteUsers { + return SiteUsers(this, "users"); + } + + public update = this._update, any>("SP.Group", (d, p) => { + + let retGroup: ISiteGroup = this; + + if (hOP(p, "Title")) { + /* tslint:disable-next-line no-string-literal */ + retGroup = this.getParent(_SiteGroup, this.parentUrl, `getByName('${p["Title"]}')`); + } + + return { + data: d, + group: retGroup, + }; + }); +} + +export interface ISiteGroup extends IGetable, ISharePointQueryableInstance { + readonly users: ISiteUsers; + update(props: TypedHash): Promise; +} +export interface _SiteGroup extends IGetable { } +export const SiteGroup = spInvokableFactory(_SiteGroup); + +export interface SiteGroupAddResult { + group: ISiteGroup; + data: any; +} + +/** + * Results from updating a group + * + */ +export interface IGroupUpdateResult { + group: ISiteGroup; + data: any; +} + +/** + * Results from adding a group + * + */ +export interface IGroupAddResult { + group: ISiteGroup; + data: any; +} diff --git a/packages/sp/src/site-groups/web.ts b/packages/sp/src/site-groups/web.ts new file mode 100644 index 000000000..456834fd3 --- /dev/null +++ b/packages/sp/src/site-groups/web.ts @@ -0,0 +1,72 @@ +import { addProp } from "@pnp/odata"; +import { _Web, Web } from "../webs/types"; +import { ISiteGroups, SiteGroups } from "./types"; +import { spPost } from "../operations"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +declare module "../webs/types" { + interface _Web { + readonly siteGroups: ISiteGroups; + readonly associatedOwnerGroup: ISiteGroups; + readonly associatedMemberGroup: ISiteGroups; + readonly associatedVisitorGroup: ISiteGroups; + createDefaultAssociatedGroups(groupNameSeed: string, siteOwner: string, copyRoleAssignments?: boolean, clearSubscopes?: boolean, siteOwner2?: string): Promise; + } + interface IWeb { + + /** + * The site groups + */ + readonly siteGroups: ISiteGroups; + + /** + * The web's owner group + */ + readonly associatedOwnerGroup: ISiteGroups; + + /** + * The web's member group + */ + readonly associatedMemberGroup: ISiteGroups; + + /** + * The web's visitor group + */ + readonly associatedVisitorGroup: ISiteGroups; + + /** + * Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. + * The target site must have unique permissions and no associated members / owners / visitors groups + * + * @param groupNameSeed The base group name. E.g. 'TestSite' would produce 'TestSite Members' etc. + * @param siteOwner The user login name to be added to the site Owners group. Default is the current user + * @param copyRoleAssignments Optional. If true the permissions are copied from the current parent scope + * @param clearSubscopes Optional. true to make all child securable objects inherit role assignments from the current object + * @param siteOwner2 Optional. The second user login name to be added to the site Owners group. Default is empty + */ + createDefaultAssociatedGroups(groupNameSeed: string, siteOwner: string, copyRoleAssignments?: boolean, clearSubscopes?: boolean, siteOwner2?: string): Promise; + } +} + +addProp(_Web, "siteGroups", SiteGroups); +addProp(_Web, "associatedOwnerGroup", SiteGroups, "associatedownergroup"); +addProp(_Web, "associatedMemberGroup", SiteGroups, "associatedmembergroup"); +addProp(_Web, "associatedVisitorGroup", SiteGroups, "associatedvisitorgroup"); + +_Web.prototype.createDefaultAssociatedGroups = async function ( + this: _Web, + groupNameSeed: string, + siteOwner: string, + copyRoleAssignments = false, + clearSubscopes = true, + siteOwner2?: string): Promise { + + await this.breakRoleInheritance(copyRoleAssignments, clearSubscopes); + + const q = this.clone(Web, "createDefaultAssociatedGroups(userLogin=@u,userLogin2=@v,groupNameSeed=@s)"); + q.query.set("@u", `'${escapeQueryStrValue(siteOwner || "")}'`); + q.query.set("@v", `'${escapeQueryStrValue(siteOwner2 || "")}'`); + q.query.set("@s", `'${escapeQueryStrValue(groupNameSeed || "")}'`); + return spPost(q); +}; + diff --git a/packages/sp/src/site-scripts/index.ts b/packages/sp/src/site-scripts/index.ts new file mode 100644 index 000000000..65c74758f --- /dev/null +++ b/packages/sp/src/site-scripts/index.ts @@ -0,0 +1,26 @@ +import { SPRest } from "../rest"; +import { ISiteScripts, SiteScripts } from "./types"; + +export { + SiteScripts, + ISiteScripts, + ISiteScriptInfo, + ISiteScriptUpdateInfo, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly siteScripts: ISiteScripts; + } +} + +Reflect.defineProperty(SPRest.prototype, "siteScripts", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return SiteScripts(this._baseUrl); + }, +}); diff --git a/packages/sp/src/site-scripts/types.ts b/packages/sp/src/site-scripts/types.ts new file mode 100644 index 000000000..b171ff97f --- /dev/null +++ b/packages/sp/src/site-scripts/types.ts @@ -0,0 +1,106 @@ +import { _SharePointQueryable, ISharePointQueryable } from "../sharepointqueryable"; +import { extractWebUrl } from "../utils/extractweburl"; +import { spPost } from "../operations"; +import { body } from "@pnp/odata"; + +/** + * Implements the site script API REST methods + * + */ +export class _SiteScripts extends _SharePointQueryable implements ISiteScripts { + /** + * Creates a new instance of the SiteScripts method class + * + * @param baseUrl The parent url provider + * @param methodName The static method name to call on the utility class + */ + constructor(baseUrl: string | ISharePointQueryable, methodName = "") { + const url = typeof baseUrl === "string" ? baseUrl : baseUrl.toUrl(); + super(extractWebUrl(url), `_api/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.${methodName}`); + } + + public execute(props: any): Promise { + return spPost(this, body(props)); + } + + /** + * Gets a list of information on all existing site scripts. + */ + public getSiteScripts(): Promise { + return this.clone(SiteScriptsCloneFactory, "GetSiteScripts", true).execute({}); + } + + /** + * Creates a new site script. + * + * @param title The display name of the site design. + * @param content JSON value that describes the script. For more information, see JSON reference. + */ + public async createSiteScript(title: string, description: string, content: any): Promise { + return await this.clone(SiteScriptsCloneFactory, + `CreateSiteScript(Title=@title,Description=@desc)?@title='${encodeURIComponent(title)}'&@desc='${encodeURIComponent(description)}'`) + .execute(content); + } + + /** + * Gets information about a specific site script. It also returns the JSON of the script. + * + * @param id The ID of the site script to get information about. + */ + public async getSiteScriptMetadata(id: string): Promise { + return await this.clone(SiteScriptsCloneFactory, "GetSiteScriptMetadata").execute({ id: id }); + } + + /** + * Deletes a site script. + * + * @param id The ID of the site script to delete. + */ + public async deleteSiteScript(id: string): Promise { + await this.clone(SiteScriptsCloneFactory, "DeleteSiteScript").execute({ id: id }); + } + + /** + * Updates a site script with new values. In the REST call, all parameters are optional except the site script Id. + * + * @param siteScriptUpdateInfo Object that contains the information to update a site script. + * Make sure you stringify the content object or pass it in the second 'content' parameter + * @param content (Optional) A new JSON script defining the script actions. For more information, see Site design JSON schema. + */ + public async updateSiteScript(siteScriptUpdateInfo: ISiteScriptUpdateInfo, content?: any): Promise { + if (content) { + siteScriptUpdateInfo.Content = JSON.stringify(content); + } + + return await this.clone(SiteScriptsCloneFactory, "UpdateSiteScript").execute({ updateInfo: siteScriptUpdateInfo }); + } +} + +export interface ISiteScripts { + getSiteScripts(): Promise; + createSiteScript(title: string, description: string, content: any): Promise; + getSiteScriptMetadata(id: string): Promise; + deleteSiteScript(id: string): Promise; + updateSiteScript(siteScriptUpdateInfo: ISiteScriptUpdateInfo, content?: any): Promise; +} + +export const SiteScripts = (baseUrl: string | ISharePointQueryable): ISiteScripts => new _SiteScripts(baseUrl); + +type SiteScriptsCloneType = ISiteScripts & ISharePointQueryable & { execute(props: any): Promise }; +const SiteScriptsCloneFactory = (baseUrl: string | ISharePointQueryable): SiteScriptsCloneType => SiteScripts(baseUrl); + +export interface ISiteScriptInfo { + Id: string; + Title: string; + Description: string; + Content: string; + Version: string; +} + +export interface ISiteScriptUpdateInfo { + Id: string; + Title?: string; + Description?: string; + Content?: string; + Version?: string; +} diff --git a/packages/sp/src/site-users/index.ts b/packages/sp/src/site-users/index.ts new file mode 100644 index 000000000..3b5b3aeb6 --- /dev/null +++ b/packages/sp/src/site-users/index.ts @@ -0,0 +1,11 @@ +import "./web"; + +export { + ISiteUser, + IWebEnsureUserResult, + SiteUser, + SiteUsers, + ISiteUsers, + ISiteUserProps, + IUserUpdateResult, +} from "./types"; diff --git a/packages/sp/src/site-users/types.ts b/packages/sp/src/site-users/types.ts new file mode 100644 index 000000000..c44eba1f7 --- /dev/null +++ b/packages/sp/src/site-users/types.ts @@ -0,0 +1,152 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { SiteGroups, ISiteGroups } from "../site-groups/types"; +import { TypedHash, extend } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import {IGetable, body } from "@pnp/odata"; +import { defaultPath, IDeleteable, deleteable } from "../decorators"; +import { spPost } from "../operations"; + + +/** + * Describes a collection of all site collection users + * + */ +@defaultPath("siteusers") +export class _SiteUsers extends _SharePointQueryableCollection implements ISiteUsers { + + /** + * Gets a user from the collection by id + * + * @param id The id of the user to retrieve + */ + public getById(id: number): ISiteUser { + return SiteUser(this, `getById(${id})`); + } + + /** + * Gets a user from the collection by email + * + * @param email The email address of the user to retrieve + */ + public getByEmail(email: string): ISiteUser { + return SiteUser(this, `getByEmail('${email}')`); + } + + /** + * Gets a user from the collection by login name + * + * @param loginName The login name of the user to retrieve + */ + public getByLoginName(loginName: string): ISiteUser { + return SiteUser(this).concat(`('!@v::${encodeURIComponent(loginName)}')`); + } + + /** + * Removes a user from the collection by id + * + * @param id The id of the user to remove + */ + public removeById(id: number): Promise { + return spPost(this.clone(SiteUsers, `removeById(${id})`)); + } + + /** + * Removes a user from the collection by login name + * + * @param loginName The login name of the user to remove + */ + public removeByLoginName(loginName: string): Promise { + const o = this.clone(SiteUsers, `removeByLoginName(@v)`); + o.query.set("@v", `'${encodeURIComponent(loginName)}'`); + return spPost(o); + } + + /** + * Adds a user to a group + * + * @param loginName The login name of the user to add to the group + * + */ + public async add(loginName: string): Promise { + + await spPost(this.clone(SiteUsers, null), body(extend(metadata("SP.User"), { LoginName: loginName }))); + + return this.getByLoginName(loginName); + } +} + +export interface ISiteUsers extends IGetable, ISharePointQueryableCollection { + getById(id: number): ISiteUser; + getByEmail(email: string): ISiteUser; + getByLoginName(loginName: string): ISiteUser; + removeById(id: number): Promise; + removeByLoginName(loginName: string): Promise; + add(loginName: string): Promise; +} +export interface _SiteUsers extends IGetable { } +export const SiteUsers = spInvokableFactory(_SiteUsers); + +/** + * Describes a single user + * + */ +@deleteable() +export class _SiteUser extends _SharePointQueryableInstance implements ISiteUser { + + /** + * Gets the groups for this user + * + */ + public get groups(): ISiteGroups { + return SiteGroups(this, "groups"); + } + + /** + * Updates this user instance with the supplied properties + * + * @param properties A plain object of property names and values to update for the user + */ + public update: (props: TypedHash) => Promise = this._update, any>("SP.User", data => ({ data, user: this })); +} + +export interface ISiteUser extends IGetable, ISharePointQueryableInstance, IDeleteable { + readonly groups: ISiteGroups; + update(props: TypedHash): Promise; +} +export interface _SiteUser extends IGetable, IDeleteable { } +export const SiteUser = spInvokableFactory(_SiteUser); + +export interface ISiteUserProps { + Email: string; + Id: number; + IsHiddenInUI: boolean; + IsShareByEmailGuestUser: boolean; + IsSiteAdmin: boolean; + LoginName: string; + PrincipalType: number; + Title: string; +} + +/** + * Properties that provide both a getter, and a setter. + * + */ +export interface IUserUpdateResult { + user: ISiteUser; + data: any; +} + +/** + * Result from ensuring a user + * + */ +export interface IWebEnsureUserResult { + data: ISiteUserProps; + user: ISiteUser; +} diff --git a/packages/sp/src/site-users/web.ts b/packages/sp/src/site-users/web.ts new file mode 100644 index 000000000..3af289615 --- /dev/null +++ b/packages/sp/src/site-users/web.ts @@ -0,0 +1,56 @@ +import { addProp, body } from "@pnp/odata"; +import { _Web, Web } from "../webs/types"; +import { ISiteUsers, SiteUsers, ISiteUser, SiteUser, IWebEnsureUserResult } from "./types"; +import { odataUrlFrom } from "../odata"; +import { spPost } from "../operations"; + +declare module "../webs/types" { + interface _Web { + readonly siteUsers: ISiteUsers; + readonly currentUser: ISiteUser; + ensureUser(loginName: string): Promise; + getUserById(id: number): ISiteUser; + } + interface IWeb { + + /** + * The site users + */ + readonly siteUsers: ISiteUsers; + + /** + * Information on the current user + */ + readonly currentUser: ISiteUser; + + /** + * Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web. + * + * @param loginName The login name of the user (ex: i:0#.f|membership|user@domain.onmicrosoft.com) + */ + ensureUser(loginName: string): Promise; + + /** + * Returns the user corresponding to the specified member identifier for the current site + * + * @param id The id of the user + */ + getUserById(id: number): ISiteUser; + } +} + +addProp(_Web, "siteUsers", SiteUsers); +addProp(_Web, "currentUser", SiteUser, "currentuser"); + +_Web.prototype.ensureUser = async function (this: _Web, logonName: string): Promise { + + const data = await spPost(this.clone(Web, "ensureuser"), body({ logonName })); + return { + data, + user: SiteUser(odataUrlFrom(data)), + }; +}; + +_Web.prototype.getUserById = function (id: number): ISiteUser { + return SiteUser(this, `getUserById(${id})`); +}; diff --git a/packages/sp/src/sites/index.ts b/packages/sp/src/sites/index.ts new file mode 100644 index 000000000..3165fb37c --- /dev/null +++ b/packages/sp/src/sites/index.ts @@ -0,0 +1,28 @@ +import { SPRest } from "../rest"; +import { ISite, Site } from "./types"; + +export { + IOpenWebByIdResult, + ISite, + Site, + IContextInfo, + IDocumentLibraryInformation, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly site: ISite; + } +} + +Reflect.defineProperty(SPRest.prototype, "site", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return Site(this._baseUrl).configure(this._options); + }, +}); + diff --git a/packages/sp/src/sites/types.ts b/packages/sp/src/sites/types.ts new file mode 100644 index 000000000..528b5e03c --- /dev/null +++ b/packages/sp/src/sites/types.ts @@ -0,0 +1,262 @@ +import { SharePointQueryable, _SharePointQueryableInstance, ISharePointQueryableInstance, spInvokableFactory } from "../sharepointqueryable"; +import { defaultPath } from "../decorators"; +import { Web, IWeb } from "../webs/types"; +import { SPBatch } from "../batch"; +import { hOP, jsS, extend } from "@pnp/common"; +import { SPHttpClient } from "../net/sphttpclient"; +import { IGetable, body, headers } from "@pnp/odata"; +import { odataUrlFrom } from "../odata"; +import { spPost } from "../operations"; + +/** + * Describes a site collection + * + */ +@defaultPath("_api/site") +export class _Site extends _SharePointQueryableInstance { + + /** + * Gets the root web of the site collection + * + */ + public get rootWeb(): IWeb { + return Web(this, "rootweb"); + } + + /** + * Gets a Web instance representing the root web of the site collection + * correctly setup for chaining within the library + */ + public async getRootWeb(): Promise { + const web = await this.rootWeb.select("Url")<{ Url: string }>(); + return Web(web.Url); + } + + /** + * Gets the context information for this site collection + */ + public async getContextInfo(): Promise { + + const q = Site(this.parentUrl, "_api/contextinfo"); + const data = await spPost(q); + + if (hOP(data, "GetContextWebInformation")) { + const info = data.GetContextWebInformation; + info.SupportedSchemaVersions = info.SupportedSchemaVersions.results; + return info; + } else { + return data; + } + } + + /** + * Gets the document libraries on a site. Static method. (SharePoint Online only) + * + * @param absoluteWebUrl The absolute url of the web whose document libraries should be returned + */ + public async getDocumentLibraries(absoluteWebUrl: string): Promise { + + const q = SharePointQueryable("", "_api/sp.web.getdocumentlibraries(@v)"); + q.query.set("@v", "'" + absoluteWebUrl + "'"); + const data = await q(); + + return hOP(data, "GetDocumentLibraries") ? data.GetDocumentLibraries : data; + } + + /** + * Gets the site url from a page url + * + * @param absolutePageUrl The absolute url of the page + */ + public async getWebUrlFromPageUrl(absolutePageUrl: string): Promise { + const q = SharePointQueryable("", "_api/sp.web.getweburlfrompageurl(@v)"); + q.query.set("@v", `'${absolutePageUrl}'`); + const data = await q(); + + return hOP(data, "GetWebUrlFromPageUrl") ? data.GetWebUrlFromPageUrl : data; + } + + /** + * Creates a new batch for requests within the context of this site collection + * + */ + public createBatch(): SPBatch { + return new SPBatch(this.parentUrl); + } + + /** + * Opens a web by id (using POST) + * + * @param webId The GUID id of the web to open + */ + public async openWebById(webId: string): Promise { + + const data = await spPost(this.clone(Site, `openWebById('${webId}')`)); + return { + data, + web: Web(odataUrlFrom(data)), + }; + } + + /** + * Creates a Modern communication site. + * + * @param title The title of the site to create + * @param lcid The language to use for the site. If not specified will default to 1033 (English). + * @param shareByEmailEnabled If set to true, it will enable sharing files via Email. By default it is set to false + * @param url The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site. + * @param description The description of the communication site. + * @param classification The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information + * @param siteDesignId The Guid of the site design to be used. + * You can use the below default OOTB GUIDs: + * Topic: 00000000-0000-0000-0000-000000000000 + * Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 + * Blank: f6cc5403-0d63-442e-96c0-285923709ffc + */ + + public async createCommunicationSite( + title: string, + lcid = 1033, + shareByEmailEnabled = false, + url: string, + description = "", + classification = "", + siteDesignId = "00000000-0000-0000-0000-000000000000", + ): Promise { + + const props = { + Classification: classification, + Description: description, + Lcid: lcid, + ShareByEmailEnabled: shareByEmailEnabled, + SiteDesignId: siteDesignId, + Title: title, + Url: url, + WebTemplate: "SITEPAGEPUBLISHING#0", + WebTemplateExtensionId: "00000000-0000-0000-0000-000000000000", + }; + + const postBody = + body({ + "request": + extend({ + "__metadata": { "type": "Microsoft.SharePoint.Portal.SPSiteCreationRequest" }, + }, props), + }, + headers({ + "Accept": "application/json;odata=verbose", + "Content-Type": "application/json;odata=verbose;charset=utf-8", + })); + + const d: any = await this.getRootWeb(); + const client = new SPHttpClient(); + const methodUrl = `${d.parentUrl}/_api/SPSiteManager/Create`; + const r = await client.post(methodUrl, postBody); + return await r.json(); + } + + /** + * Creates a Modern team site backed by Office 365 group. For use in SP Online only. This will not work with App-only tokens + * + * @param displayName The title or display name of the Modern team site to be created + * @param alias Alias of the underlying Office 365 Group + * @param isPublic Defines whether the Office 365 Group will be public (default), or private. + * @param lcid The language to use for the site. If not specified will default to English (1033). + * @param description The description of the site to be created. + * @param classification The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information + * @param owners The Owners of the site to be created + */ + + public async createModernTeamSite( + displayName: string, + alias: string, + isPublic = true, + lcid = 1033, + description = "", + classification = "", + owners?: string[], + ): Promise { + + const postBody = jsS({ + alias: alias, + displayName: displayName, + isPublic: isPublic, + optionalParams: { + Classification: classification, + CreationOptions: { + "results": [`SPSiteLanguage:${lcid}`], + }, + Description: description, + Owners: { + "results": owners ? owners : [], + }, + }, + }); + + const d: any = await this.getRootWeb(); + const client = new SPHttpClient(); + const methodUrl = `${d.parentUrl}/_api/GroupSiteManager/CreateGroupEx`; + const r = await client.post(methodUrl, { + body: postBody, + headers: { + "Accept": "application/json;odata=verbose", + "Content-Type": "application/json;odata=verbose;charset=utf-8", + }, + }); + return await r.json(); + } +} + +export interface ISite extends IGetable, ISharePointQueryableInstance { + readonly rootWeb: IWeb; + getRootWeb(): Promise; + getContextInfo(): Promise; + getDocumentLibraries(absoluteWebUrl: string): Promise; + getWebUrlFromPageUrl(absolutePageUrl: string): Promise; + createBatch(): SPBatch; + openWebById(webId: string): Promise; + createCommunicationSite( + title: string, + lcid?: number, + shareByEmailEnabled?: boolean, + url?: string, + description?: string, + classification?: string, + siteDesignId?: string): Promise; + createModernTeamSite( + displayName: string, + alias: string, + isPublic?: boolean, + lcid?: number, + description?: string, + classification?: string, + owners?: string[]): Promise; + +} +export interface _Site extends IGetable { } +export const Site = spInvokableFactory(_Site); + +/** + * The result of opening a web by id: contains the data returned as well as a chainable web instance + */ +export interface IOpenWebByIdResult { + data: any; + web: IWeb; +} + +export interface IContextInfo { + FormDigestTimeoutSeconds?: number; + FormDigestValue?: number; + LibraryVersion?: string; + SiteFullUrl?: string; + SupportedSchemaVersions?: string[]; + WebFullUrl?: string; +} + +export interface IDocumentLibraryInformation { + AbsoluteUrl?: string; + Modified?: Date; + ModifiedFriendlyDisplay?: string; + ServerRelativeUrl?: string; + Title?: string; +} diff --git a/packages/sp/src/social/index.ts b/packages/sp/src/social/index.ts new file mode 100644 index 000000000..907cb489b --- /dev/null +++ b/packages/sp/src/social/index.ts @@ -0,0 +1,33 @@ +import { SPRest } from "../rest"; +import { ISocial, Social } from "./types"; + +export { + IMySocial, + ISocial, + IMySocialData, + ISocialActor, + ISocialActorInfo, + MySocial, + Social, + SocialActorType, + SocialActorTypes, + SocialFollowResult, + SocialStatusCode, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly social: ISocial; + } +} + +Reflect.defineProperty(SPRest.prototype, "social", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return Social(this._baseUrl); + }, +}); diff --git a/packages/sp/src/social/types.ts b/packages/sp/src/social/types.ts new file mode 100644 index 000000000..4bf011c17 --- /dev/null +++ b/packages/sp/src/social/types.ts @@ -0,0 +1,393 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryable, + spInvokableFactory, + _SharePointQueryable, +} from "../sharepointqueryable"; + +import { defaultPath } from "../decorators"; +import { hOP, IFetchOptions } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { body } from "@pnp/odata"; +import { spPost } from "../operations"; + +/** + * Exposes social following methods + */ +@defaultPath("_api/social.following") +export class _Social extends _SharePointQueryableInstance implements ISocial { + + public get my(): IMySocial { + return MySocial(this); + } + + /** + * Gets a URI to a site that lists the current user's followed sites. + */ + public getFollowedSitesUri(): Promise { + return this.clone(SocialCloneFactory, "FollowedSitesUri").get().then(r => { + return r.FollowedSitesUri || r; + }); + } + + /** + * Gets a URI to a site that lists the current user's followed documents. + */ + public getFollowedDocumentsUri(): Promise { + return this.clone(SocialCloneFactory, "FollowedDocumentsUri").get().then(r => { + return r.FollowedDocumentsUri || r; + }); + } + + /** + * Makes the current user start following a user, document, site, or tag + * + * @param actorInfo The actor to start following + */ + public follow(actorInfo: ISocialActorInfo): Promise { + return spPost(this.clone(SocialCloneFactory, "follow"), this.createSocialActorInfoRequestBody(actorInfo)); + } + + /** + * Indicates whether the current user is following a specified user, document, site, or tag + * + * @param actorInfo The actor to find the following status for + */ + public isFollowed(actorInfo: ISocialActorInfo): Promise { + return spPost(this.clone(SocialCloneFactory, "isfollowed"), this.createSocialActorInfoRequestBody(actorInfo)); + } + + /** + * Makes the current user stop following a user, document, site, or tag + * + * @param actorInfo The actor to stop following + */ + public stopFollowing(actorInfo: ISocialActorInfo): Promise { + return spPost(this.clone(SocialCloneFactory, "stopfollowing"), this.createSocialActorInfoRequestBody(actorInfo)); + } + + /** + * Creates SocialActorInfo request body + * + * @param actorInfo The actor to create request body + */ + private createSocialActorInfoRequestBody(actorInfo: ISocialActorInfo): IFetchOptions { + return body({ + "actor": + Object.assign(metadata("SP.Social.SocialActorInfo"), { + Id: null, + }, actorInfo), + }); + } +} + +export interface ISocial { + readonly my: IMySocial; + getFollowedSitesUri(): Promise; + getFollowedDocumentsUri(): Promise; + follow(actorInfo: ISocialActorInfo): Promise; + isFollowed(actorInfo: ISocialActorInfo): Promise; + stopFollowing(actorInfo: ISocialActorInfo): Promise; +} +export const Social = (baseUrl: string | ISharePointQueryable): ISocial => new _Social(baseUrl); + +const SocialCloneFactory = (baseUrl: string | ISharePointQueryable): ISocial & ISharePointQueryable => Social(baseUrl); + +@defaultPath("my") +export class _MySocial extends _SharePointQueryableInstance implements IMySocial { + + /** + * Gets users, documents, sites, and tags that the current user is following. + * + * @param types Bitwise set of SocialActorTypes to retrieve + */ + public followed(types: SocialActorTypes): Promise { + return this.clone(MySocialCloneFactory, `followed(types=${types})`)().then(r => { + return hOP(r, "Followed") ? r.Followed.results : r; + }); + } + + /** + * Gets the count of users, documents, sites, and tags that the current user is following. + * + * @param types Bitwise set of SocialActorTypes to retrieve + */ + public followedCount(types: SocialActorTypes): Promise { + return this.clone(MySocialCloneFactory, `followedcount(types=${types})`)().then(r => { + return r.FollowedCount || r; + }); + } + + /** + * Gets the users who are following the current user. + */ + public followers(): Promise { + return this.clone(MySocialCloneFactory, "followers")().then(r => { + return hOP(r, "Followers") ? r.Followers.results : r; + }); + } + + /** + * Gets users who the current user might want to follow. + */ + public suggestions(): Promise { + return this.clone(MySocialCloneFactory, "suggestions")().then(r => { + return hOP(r, "Suggestions") ? r.Suggestions.results : r; + }); + } +} +/** + * Defines the public methods exposed by the my endpoint + */ +export interface IMySocial { + /** + * Gets this user's data + */ + get(): Promise; + /** + * Gets users, documents, sites, and tags that the current user is following. + * + * @param types Bitwise set of SocialActorTypes to retrieve + */ + followed(types: SocialActorTypes): Promise; + /** + * Gets the count of users, documents, sites, and tags that the current user is following. + * + * @param types Bitwise set of SocialActorTypes to retrieve + */ + followedCount(types: SocialActorTypes): Promise; + /** + * Gets the users who are following the current user. + */ + followers(): Promise; + /** + * Gets users who the current user might want to follow. + */ + suggestions(): Promise; +} + +export const MySocial = spInvokableFactory(_MySocial); + +const MySocialCloneFactory = (baseUrl: string | ISharePointQueryable, path?: string): IMySocial & ISharePointQueryable => MySocial(baseUrl, path); + +/** + * Social actor info + * + */ +export interface ISocialActorInfo { + AccountName?: string; + ActorType: SocialActorType; + ContentUri?: string; + Id?: string; + TagGuid?: string; +} + +/** + * Social actor type + * + */ +export const enum SocialActorType { + User, + Document, + Site, + Tag, +} + +/** + * Social actor type + * + */ +/* tslint:disable:no-bitwise */ +export const enum SocialActorTypes { + None = 0, + User = 1 << SocialActorType.User, + Document = 1 << SocialActorType.Document, + Site = 1 << SocialActorType.Site, + Tag = 1 << SocialActorType.Tag, + /** + * The set excludes documents and sites that do not have feeds. + */ + ExcludeContentWithoutFeeds = 268435456, + /** + * The set includes group sites + */ + IncludeGroupsSites = 536870912, + /** + * The set includes only items created within the last 24 hours + */ + WithinLast24Hours = 1073741824, +} +/* tslint:enable */ + +/** + * Result from following + * + */ +export const enum SocialFollowResult { + Ok = 0, + AlreadyFollowing = 1, + LimitReached = 2, + InternalError = 3, +} + +/** + * Specifies an exception or status code. + */ +export const enum SocialStatusCode { + /** + * The operation completed successfully + */ + OK, + /** + * The request is invalid. + */ + InvalidRequest, + /** + * The current user is not authorized to perform the operation. + */ + AccessDenied, + /** + * The target of the operation was not found. + */ + ItemNotFound, + /** + * The operation is invalid for the target's current state. + */ + InvalidOperation, + /** + * The operation completed without modifying the target. + */ + ItemNotModified, + /** + * The operation failed because an internal error occurred. + */ + InternalError, + /** + * The operation failed because the server could not access the distributed cache. + */ + CacheReadError, + /** + * The operation succeeded but the server could not update the distributed cache. + */ + CacheUpdateError, + /** + * No personal site exists for the current user, and no further information is available. + */ + PersonalSiteNotFound, + /** + * No personal site exists for the current user, and a previous attempt to create one failed. + */ + FailedToCreatePersonalSite, + /** + * No personal site exists for the current user, and a previous attempt to create one was not authorized. + */ + NotAuthorizedToCreatePersonalSite, + /** + * No personal site exists for the current user, and no attempt should be made to create one. + */ + CannotCreatePersonalSite, + /** + * The operation was rejected because an internal limit had been reached. + */ + LimitReached, + /** + * The operation failed because an error occurred during the processing of the specified attachment. + */ + AttachmentError, + /** + * The operation succeeded with recoverable errors; the returned data is incomplete. + */ + PartialData, + /** + * A required SharePoint feature is not enabled. + */ + FeatureDisabled, + /** + * The site's storage quota has been exceeded. + */ + StorageQuotaExceeded, + /** + * The operation failed because the server could not access the database. + */ + DatabaseError, +} + +export interface ISocialActor { + /** + * Gets the actor type. + */ + ActorType: SocialActorType; + /** + * Gets the actor's unique identifier. + */ + Id: string; + /** + * Gets the actor's canonical URI. + */ + Uri: string; + /** + * Gets the actor's display name. + */ + Name: string; + /** + * Returns true if the current user is following the actor, false otherwise. + */ + IsFollowed: boolean; + /** + * Gets a code that indicates recoverable errors that occurred during actor retrieval + */ + Status: SocialStatusCode; + /** + * Returns true if the Actor can potentially be followed, false otherwise. + */ + CanFollow: boolean; + /** + * Gets the actor's image URI. Only valid when ActorType is User, Document, or Site + */ + ImageUri: string; + /** + * Gets the actor's account name. Only valid when ActorType is User + */ + AccountName: string; + /** + * Gets the actor's email address. Only valid when ActorType is User + */ + EmailAddress: string; + /** + * Gets the actor's title. Only valid when ActorType is User + */ + Title: string; + /** + * Gets the text of the actor's most recent post. Only valid when ActorType is User + */ + StatusText: string; + /** + * Gets the URI of the actor's personal site. Only valid when ActorType is User + */ + PersonalSiteUri: string; + /** + * Gets the URI of the actor's followed content folder. Only valid when this represents the current user + */ + FollowedContentUri: string; + /** + * Gets the actor's content URI. Only valid when ActorType is Document, or Site + */ + ContentUri: string; + /** + * Gets the actor's library URI. Only valid when ActorType is Document + */ + LibraryUri: string; + /** + * Gets the actor's tag GUID. Only valid when ActorType is Tag + */ + TagGuid: string; +} + +/** + * Defines the properties retrurned from the my endpoint + */ +export interface IMySocialData { + SocialActor: ISocialActor; + MyFollowedDocumentsUri: string; + MyFollowedSitesUri: string; +} diff --git a/packages/sp/src/sp.ts b/packages/sp/src/sp.ts new file mode 100644 index 000000000..69f1a3f87 --- /dev/null +++ b/packages/sp/src/sp.ts @@ -0,0 +1,48 @@ +export { + odataUrlFrom, + spODataEntity, + spODataEntityArray, +} from "./odata"; + +export { + ISharePointQueryable, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + SharePointQueryableInstance, + SharePointQueryableCollection, + ISharePointQueryableConstructor, + SharePointQueryable, + spInvokableFactory, +} from "./sharepointqueryable"; + +export { + SPBatch, +} from "./batch"; + +export * from "./decorators"; + +export * from "./operations"; + +export { + SPConfiguration, + SPConfigurationPart, +} from "./config/splibconfig"; + +export { + SPHttpClient, +} from "./net/sphttpclient"; + +export { + SPRest, + sp, +} from "./rest"; + +export * from "./types"; + +export { + toAbsoluteUrl, +} from "./utils/toabsoluteurl"; + +export { + extractWebUrl, +} from "./utils/extractweburl"; diff --git a/packages/sp/src/sputilities/index.ts b/packages/sp/src/sputilities/index.ts new file mode 100644 index 000000000..8fe3c3b3c --- /dev/null +++ b/packages/sp/src/sputilities/index.ts @@ -0,0 +1,27 @@ +import { SPRest } from "../rest"; +import { IUtilities, Utilities } from "./types"; + +export { + ICreateWikiPageResult, + IEmailProperties, + IUtilities, + IWikiPageCreationInfo, + Utilities, +} from "./types"; + +/** + * Extend rest + */ +declare module "../rest" { + interface SPRest { + readonly utility: IUtilities; + } +} + +Reflect.defineProperty(SPRest.prototype, "utility", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return Utilities(this._baseUrl, ""); + }, +}); diff --git a/packages/sp/src/sputilities/types.ts b/packages/sp/src/sputilities/types.ts new file mode 100644 index 000000000..2a14e8917 --- /dev/null +++ b/packages/sp/src/sputilities/types.ts @@ -0,0 +1,202 @@ +import { _SharePointQueryable, ISharePointQueryable, spInvokableFactory } from "../sharepointqueryable"; +import { extend, TypedHash } from "@pnp/common"; +import { SPBatch } from "../batch"; +import { ICachingOptions, IGetable, body } from "@pnp/odata"; +import { odataUrlFrom } from "../odata"; +import { IPrincipalInfo, PrincipalType, PrincipalSource } from "../types"; +import { metadata } from "../utils/metadata"; +import { File, IFile } from "../files/types"; +import { extractWebUrl } from "../utils/extractweburl"; +import { spPost } from "../operations"; + +/** + * Allows for calling of the static SP.Utilities.Utility methods by supplying the method name + */ +export class _Utilities extends _SharePointQueryable implements IUtilities { + + /** + * Creates a new instance of the Utility method class + * + * @param baseUrl The parent url provider + * @param methodName The static method name to call on the utility class + */ + constructor(baseUrl: string | ISharePointQueryable, methodName: string) { + const url = typeof baseUrl === "string" ? baseUrl : baseUrl.toUrl(); + super(extractWebUrl(url), `_api/SP.Utilities.Utility.${methodName}`); + } + + public excute(props: any): Promise { + return spPost(this, body(props)); + } + + /** + * Sends an email based on the supplied properties + * + * @param props The properties of the email to send + */ + public sendEmail(props: IEmailProperties): Promise { + + const params = { + properties: extend(metadata("SP.Utilities.EmailProperties"), { + Body: props.Body, + From: props.From, + Subject: props.Subject, + }), + }; + + if (props.To && props.To.length > 0) { + + params.properties = extend(params.properties, { + To: { results: props.To }, + }); + } + + if (props.CC && props.CC.length > 0) { + + params.properties = extend(params.properties, { + CC: { results: props.CC }, + }); + } + + if (props.BCC && props.BCC.length > 0) { + + params.properties = extend(params.properties, { + BCC: { results: props.BCC }, + }); + } + + if (props.AdditionalHeaders) { + params.properties = extend(params.properties, { + AdditionalHeaders: props.AdditionalHeaders, + }); + } + + return this.clone(UtilitiesCloneFactory, "SendEmail", true).excute(params); + } + + public getCurrentUserEmailAddresses(): Promise { + + return this.clone(UtilitiesCloneFactory, "GetCurrentUserEmailAddresses", true).excute({}); + } + + public resolvePrincipal(input: string, + scopes: PrincipalType, + sources: PrincipalSource, + inputIsEmailOnly: boolean, + addToUserInfoList: boolean, + matchUserInfoList = false): Promise { + + const params = { + addToUserInfoList: addToUserInfoList, + input: input, + inputIsEmailOnly: inputIsEmailOnly, + matchUserInfoList: matchUserInfoList, + scopes: scopes, + sources: sources, + }; + + return this.clone(UtilitiesCloneFactory, "ResolvePrincipalInCurrentContext", true).excute(params); + } + + public searchPrincipals(input: string, + scopes: PrincipalType, + sources: PrincipalSource, + groupName: string, + maxCount: number): Promise { + + const params = { + groupName: groupName, + input: input, + maxCount: maxCount, + scopes: scopes, + sources: sources, + }; + + return this.clone(UtilitiesCloneFactory, "SearchPrincipalsUsingContextWeb", true).excute(params); + } + + public createEmailBodyForInvitation(pageAddress: string): Promise { + + const params = { + pageAddress: pageAddress, + }; + + return this.clone(UtilitiesCloneFactory, "CreateEmailBodyForInvitation", true).excute(params); + } + + public expandGroupsToPrincipals(inputs: string[], maxCount = 30): Promise { + + const params = { + inputs: inputs, + maxCount: maxCount, + }; + + return this.clone(UtilitiesCloneFactory, "ExpandGroupsToPrincipals", true).excute(params); + } + + public createWikiPage(info: IWikiPageCreationInfo): Promise { + + return this.clone(UtilitiesCloneFactory, "CreateWikiPageInContextWeb", true).excute({ + parameters: info, + }).then(r => { + return { + data: r, + file: File(odataUrlFrom(r)), + }; + }); + } +} + +export interface IUtilities { + usingCaching(options?: ICachingOptions): this; + inBatch(batch: SPBatch): this; + sendEmail(props: IEmailProperties): Promise; + getCurrentUserEmailAddresses(): Promise; + resolvePrincipal(email: string, + scopes: PrincipalType, + sources: PrincipalSource, + inputIsEmailOnly: boolean, + addToUserInfoList: boolean, + matchUserInfoList?: boolean): Promise; + searchPrincipals(input: string, + scopes: PrincipalType, + sources: PrincipalSource, + groupName: string, + maxCount: number): Promise; + createEmailBodyForInvitation(pageAddress: string): Promise; + expandGroupsToPrincipals(inputs: string[], maxCount?: number): Promise; + createWikiPage(info: IWikiPageCreationInfo): Promise; +} +export interface _Utilities extends IGetable { } +export const Utilities = spInvokableFactory(_Utilities); + +type UtilitiesCloneType = IUtilities & ISharePointQueryable & { excute(props: any): Promise }; +const UtilitiesCloneFactory = (baseUrl: string | ISharePointQueryable, path?: string): UtilitiesCloneType => Utilities(baseUrl, path); + +export interface ICreateWikiPageResult { + data: any; + file: IFile; +} + +export interface IEmailProperties { + + To: string[]; + CC?: string[]; + BCC?: string[]; + Subject: string; + Body: string; + AdditionalHeaders?: TypedHash; + From?: string; +} + +export interface IWikiPageCreationInfo { + /** + * The server-relative-url of the wiki page to be created. + */ + ServerRelativeUrl: string; + + /** + * The wiki content to be set in the wiki page. + */ + WikiHtmlContent: string; +} diff --git a/packages/sp/src/subscriptions/index.ts b/packages/sp/src/subscriptions/index.ts new file mode 100644 index 000000000..c208f5631 --- /dev/null +++ b/packages/sp/src/subscriptions/index.ts @@ -0,0 +1,10 @@ +import "./list"; + +export { + ISubscription, + ISubscriptionAddResult, + ISubscriptionUpdateResult, + ISubscriptions, + Subscription, + Subscriptions, +} from "./types"; diff --git a/packages/sp/src/subscriptions/list.ts b/packages/sp/src/subscriptions/list.ts new file mode 100644 index 000000000..e37eb1a05 --- /dev/null +++ b/packages/sp/src/subscriptions/list.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { Subscriptions, ISubscriptions } from "./types"; + +/** +* Extend List +*/ +declare module "../lists/types" { + interface _List { + readonly subscriptions: ISubscriptions; + } + interface IList { + readonly subscriptions: ISubscriptions; + } +} + +addProp(_List, "subscriptions", Subscriptions); diff --git a/packages/sp/src/subscriptions/types.ts b/packages/sp/src/subscriptions/types.ts new file mode 100644 index 000000000..f02db4c34 --- /dev/null +++ b/packages/sp/src/subscriptions/types.ts @@ -0,0 +1,116 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { IGetable, body, headers } from "@pnp/odata"; +import { defaultPath } from "../decorators"; +import { spPost, spDelete, spPatch } from "../operations"; + +/** + * Describes a collection of webhook subscriptions + * + */ +@defaultPath("subscriptions") +export class _Subscriptions extends _SharePointQueryableCollection implements ISubscriptions { + + /** + * Returns all the webhook subscriptions or the specified webhook subscription + * + * @param subscriptionId The id of a specific webhook subscription to retrieve, omit to retrieve all the webhook subscriptions + */ + public getById(subscriptionId: string): ISubscription { + return Subscription(this).concat(`('${subscriptionId}')`); + } + + /** + * Creates a new webhook subscription + * + * @param notificationUrl The url to receive the notifications + * @param expirationDate The date and time to expire the subscription in the form YYYY-MM-ddTHH:mm:ss+00:00 (maximum of 6 months) + * @param clientState A client specific string (optional) + */ + public async add(notificationUrl: string, expirationDate: string, clientState?: string): Promise { + + const postBody: any = { + "expirationDateTime": expirationDate, + "notificationUrl": notificationUrl, + "resource": this.toUrl(), + }; + + if (clientState) { + postBody.clientState = clientState; + } + + const data = await spPost(this, body(postBody, headers({ "Content-Type": "application/json" }))); + return { data, subscription: this.getById(data.id) }; + } +} + +export interface ISubscriptions extends IGetable, ISharePointQueryableCollection { + getById(subscriptionId: string): ISubscription; + add(notificationUrl: string, expirationDate: string, clientState?: string): Promise; +} +export interface _Subscriptions extends IGetable { } +export const Subscriptions = spInvokableFactory(_Subscriptions); + +/** + * Describes a single webhook subscription instance + * + */ +export class _Subscription extends _SharePointQueryableInstance implements ISubscription { + + /** + * Renews this webhook subscription + * + * @param expirationDate The date and time to expire the subscription in the form YYYY-MM-ddTHH:mm:ss+00:00 (maximum of 6 months, optional) + * @param notificationUrl The url to receive the notifications (optional) + * @param clientState A client specific string (optional) + */ + public async update(expirationDate?: string, notificationUrl?: string, clientState?: string): Promise { + + const postBody: any = {}; + + if (expirationDate) { + postBody.expirationDateTime = expirationDate; + } + + if (notificationUrl) { + postBody.notificationUrl = notificationUrl; + } + + if (clientState) { + postBody.clientState = clientState; + } + + const data = await spPatch(this, body(postBody, headers({ "Content-Type": "application/json" }))); + return { data, subscription: this }; + } + + /** + * Removes this webhook subscription + * + */ + public delete(): Promise { + return spDelete(this); + } +} + +export interface ISubscription extends IGetable, ISharePointQueryableInstance { + update(expirationDate?: string, notificationUrl?: string, clientState?: string): Promise; + delete(): Promise; +} +export interface _Subscription extends IGetable { } +export const Subscription = spInvokableFactory(_Subscription); + +export interface ISubscriptionAddResult { + subscription: ISubscription; + data: any; +} + +export interface ISubscriptionUpdateResult { + subscription: ISubscription; + data: any; +} diff --git a/packages/sp/src/types.ts b/packages/sp/src/types.ts new file mode 100644 index 000000000..f10ac4181 --- /dev/null +++ b/packages/sp/src/types.ts @@ -0,0 +1,258 @@ +// reference: https://msdn.microsoft.com/en-us/library/office/dn600183.aspx + +/** + * Represents the unique sequential location of a change within the change log. + */ +export interface IChangeToken { + /** + * Gets or sets a string value that contains the serialized representation of the change token generated by the protocol server. + */ + StringValue: string; +} + +/** + * Defines a query that is performed against the change log. + */ +export interface IChangeQuery { + /** + * Gets or sets a value that specifies whether add changes are included in the query. + */ + Add?: boolean; + + /** + * Gets or sets a value that specifies whether changes to alerts are included in the query. + */ + Alert?: boolean; + + /** + * Gets or sets a value that specifies the end date and end time for changes that are returned through the query. + */ + ChangeTokenEnd?: IChangeToken; + + /** + * Gets or sets a value that specifies the start date and start time for changes that are returned through the query. + */ + ChangeTokenStart?: IChangeToken; + + /** + * Gets or sets a value that specifies whether changes to content types are included in the query. + */ + ContentType?: boolean; + + /** + * Gets or sets a value that specifies whether deleted objects are included in the query. + */ + DeleteObject?: boolean; + + /** + * Gets or sets a value that specifies whether changes to fields are included in the query. + */ + Field?: boolean; + + /** + * Gets or sets a value that specifies whether changes to files are included in the query. + */ + File?: boolean; + + /** + * Gets or sets value that specifies whether changes to folders are included in the query. + */ + Folder?: boolean; + + /** + * Gets or sets a value that specifies whether changes to groups are included in the query. + */ + Group?: boolean; + + /** + * Gets or sets a value that specifies whether adding users to groups is included in the query. + */ + GroupMembershipAdd?: boolean; + + /** + * Gets or sets a value that specifies whether deleting users from the groups is included in the query. + */ + GroupMembershipDelete?: boolean; + + /** + * Gets or sets a value that specifies whether general changes to list items are included in the query. + */ + Item?: boolean; + + /** + * Gets or sets a value that specifies whether changes to lists are included in the query. + */ + List?: boolean; + + /** + * Gets or sets a value that specifies whether move changes are included in the query. + */ + Move?: boolean; + + /** + * Gets or sets a value that specifies whether changes to the navigation structure of a site collection are included in the query. + */ + Navigation?: boolean; + + /** + * Gets or sets a value that specifies whether renaming changes are included in the query. + */ + Rename?: boolean; + + /** + * Gets or sets a value that specifies whether restoring items from the recycle bin or from backups is included in the query. + */ + Restore?: boolean; + + /** + * Gets or sets a value that specifies whether adding role assignments is included in the query. + */ + RoleAssignmentAdd?: boolean; + + /** + * Gets or sets a value that specifies whether adding role assignments is included in the query. + */ + RoleAssignmentDelete?: boolean; + + /** + * Gets or sets a value that specifies whether adding role assignments is included in the query. + */ + RoleDefinitionAdd?: boolean; + + /** + * Gets or sets a value that specifies whether adding role assignments is included in the query. + */ + RoleDefinitionDelete?: boolean; + + /** + * Gets or sets a value that specifies whether adding role assignments is included in the query. + */ + RoleDefinitionUpdate?: boolean; + + /** + * Gets or sets a value that specifies whether modifications to security policies are included in the query. + */ + SecurityPolicy?: boolean; + + /** + * Gets or sets a value that specifies whether changes to site collections are included in the query. + */ + Site?: boolean; + + /** + * Gets or sets a value that specifies whether updates made using the item SystemUpdate method are included in the query. + */ + SystemUpdate?: boolean; + + /** + * Gets or sets a value that specifies whether update changes are included in the query. + */ + Update?: boolean; + + /** + * Gets or sets a value that specifies whether changes to users are included in the query. + */ + User?: boolean; + + /** + * Gets or sets a value that specifies whether changes to views are included in the query. + */ + View?: boolean; + + /** + * Gets or sets a value that specifies whether changes to Web sites are included in the query. + */ + Web?: boolean; +} + +/** + * Specifies the type of a principal. + */ +/* tslint:disable:no-bitwise */ +export const enum PrincipalType { + /** + * Enumeration whose value specifies no principal type. + */ + None = 0, + /** + * Enumeration whose value specifies a user as the principal type. + */ + User = 1, + /** + * Enumeration whose value specifies a distribution list as the principal type. + */ + DistributionList = 2, + /** + * Enumeration whose value specifies a security group as the principal type. + */ + SecurityGroup = 4, + /** + * Enumeration whose value specifies a group as the principal type. + */ + SharePointGroup = 8, + /** + * Enumeration whose value specifies all principal types. + */ + All = SharePointGroup | SecurityGroup | DistributionList | User, +} +/* tslint:enable:no-bitwise */ + +/** + * Specifies the source of a principal. + */ +/* tslint:disable:no-bitwise */ +export const enum PrincipalSource { + /** + * Enumeration whose value specifies no principal source. + */ + None = 0, + /** + * Enumeration whose value specifies user information list as the principal source. + */ + UserInfoList = 1, + /** + * Enumeration whose value specifies Active Directory as the principal source. + */ + Windows = 2, + /** + * Enumeration whose value specifies the current membership provider as the principal source. + */ + MembershipProvider = 4, + /** + * Enumeration whose value specifies the current role provider as the principal source. + */ + RoleProvider = 8, + /** + * Enumeration whose value specifies all principal sources. + */ + All = RoleProvider | MembershipProvider | Windows | UserInfoList, +} +/* tslint:enable:no-bitwise */ + +export interface IPrincipalInfo { + Department: string; + DisplayName: string; + Email: string; + JobTitle: string; + LoginName: string; + Mobile: string; + PrincipalId: number; + PrincipalType: PrincipalType; + SIPAddress: string; +} + +export enum PageType { + Invalid = -1, + DefaultView, + NormalView, + DialogView, + View, + DisplayForm, + DisplayFormDialog, + EditForm, + EditFormDialog, + NewForm, + NewFormDialog, + SolutionForm, + PAGE_MAXITEMS, +} diff --git a/packages/sp/src/user-custom-actions/index.ts b/packages/sp/src/user-custom-actions/index.ts new file mode 100644 index 000000000..3dd74defc --- /dev/null +++ b/packages/sp/src/user-custom-actions/index.ts @@ -0,0 +1,12 @@ +import "./list"; +import "./web"; +import "./site"; + +export { + IUserCustomAction, + IUserCustomActions, + IUserCustomActionAddResult, + IUserCustomActionUpdateResult, + UserCustomAction, + UserCustomActions, +} from "./types"; diff --git a/packages/sp/src/user-custom-actions/list.ts b/packages/sp/src/user-custom-actions/list.ts new file mode 100644 index 000000000..be455d1b5 --- /dev/null +++ b/packages/sp/src/user-custom-actions/list.ts @@ -0,0 +1,25 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { UserCustomActions, IUserCustomActions } from "./types"; + +/** +* Extend Item +*/ +declare module "../lists/types" { + interface _List { + /** + * Get all custom actions on a site collection + * + */ + readonly userCustomActions: IUserCustomActions; + } + interface IList { + /** + * Get all custom actions on a site collection + * + */ + readonly userCustomActions: IUserCustomActions; + } +} + +addProp(_List, "userCustomActions", UserCustomActions); diff --git a/packages/sp/src/user-custom-actions/site.ts b/packages/sp/src/user-custom-actions/site.ts new file mode 100644 index 000000000..4fe59d984 --- /dev/null +++ b/packages/sp/src/user-custom-actions/site.ts @@ -0,0 +1,17 @@ +import { addProp } from "@pnp/odata"; +import { _Site } from "../sites/types"; +import { UserCustomActions, IUserCustomActions } from "./types"; + +/** +* Extend Web +*/ +declare module "../sites/types" { + interface _Site { + readonly userCustomActions: IUserCustomActions; + } + interface ISite { + readonly userCustomActions: IUserCustomActions; + } +} + +addProp(_Site, "userCustomActions", UserCustomActions); diff --git a/packages/sp/src/user-custom-actions/types.ts b/packages/sp/src/user-custom-actions/types.ts new file mode 100644 index 000000000..59d81004e --- /dev/null +++ b/packages/sp/src/user-custom-actions/types.ts @@ -0,0 +1,98 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { extend, TypedHash } from "@pnp/common"; +import { IGetable, body } from "@pnp/odata"; +import { defaultPath, deleteable, IDeleteable } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Describes a collection of user custom actions + * + */ +@defaultPath("usercustomactions") +export class _UserCustomActions extends _SharePointQueryableCollection implements IUserCustomActions { + + /** + * Returns the user custom action with the specified id + * + * @param id The GUID id of the user custom action to retrieve + */ + public getById(id: string): IUserCustomAction { + return UserCustomAction(this).concat(`('${id}')`); + } + + /** + * Creates a user custom action + * + * @param properties The information object of property names and values which define the new user custom action + * + */ + public async add(properties: TypedHash): Promise { + + const data = await spPost(this, body(extend({ __metadata: { "type": "SP.UserCustomAction" } }, properties))); + return { + action: this.getById(data.Id), + data, + }; + } + + /** + * Deletes all user custom actions in the collection + * + */ + public clear(): Promise { + return spPost(this.clone(UserCustomActions, "clear")); + } +} + +export interface IUserCustomActions extends IGetable, ISharePointQueryableCollection { + getById(id: string): IUserCustomAction; + add(properties: TypedHash): Promise; + clear(): Promise; +} +export interface _UserCustomActions extends IGetable { } +export const UserCustomActions = spInvokableFactory(_UserCustomActions); + +/** + * Describes a single user custom action + * + */ +@deleteable() +export class _UserCustomAction extends _SharePointQueryableInstance implements IUserCustomAction { + + /** + * Updates this user custom action with the supplied properties + * + * @param properties An information object of property names and values to update for this user custom action + */ + public update: any = this._update>("SP.UserCustomAction", (data) => ({ data, action: this })); +} + +export interface IUserCustomAction extends IGetable, ISharePointQueryableInstance, IDeleteable { + update(props: TypedHash): IUserCustomActionUpdateResult; +} +export interface _UserCustomAction extends IGetable, IDeleteable { } +export const UserCustomAction = spInvokableFactory(_UserCustomAction); + +/** + * Result from adding a user custom action + * + */ +export interface IUserCustomActionAddResult { + data: any; + action: IUserCustomAction; +} + +/** + * Result from udating a user custom action + * + */ +export interface IUserCustomActionUpdateResult { + data: any; + action: IUserCustomAction; +} diff --git a/packages/sp/src/user-custom-actions/web.ts b/packages/sp/src/user-custom-actions/web.ts new file mode 100644 index 000000000..9be765cc3 --- /dev/null +++ b/packages/sp/src/user-custom-actions/web.ts @@ -0,0 +1,20 @@ +import { addProp } from "@pnp/odata"; +import { _Web } from "../webs/types"; +import { UserCustomActions, IUserCustomActions } from "./types"; + +/** +* Extend Web +*/ +declare module "../webs/types" { + interface _Web { + readonly userCustomActions: IUserCustomActions; + } + interface IWeb { + /** + * Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection + */ + readonly userCustomActions: IUserCustomActions; + } +} + +addProp(_Web, "userCustomActions", UserCustomActions); diff --git a/packages/sp/src/utils/escapeSingleQuote.ts b/packages/sp/src/utils/escapeSingleQuote.ts new file mode 100644 index 000000000..5d0345add --- /dev/null +++ b/packages/sp/src/utils/escapeSingleQuote.ts @@ -0,0 +1,11 @@ +import { stringIsNullOrEmpty } from "@pnp/common"; + +export function escapeQueryStrValue(value: string): string { + + if (stringIsNullOrEmpty(value)) { + return ""; + } + + // replace all instance of ' with '' + return encodeURIComponent(value.replace(/\'/ig, "''")); +} diff --git a/packages/sp/src/utils/extractweburl.ts b/packages/sp/src/utils/extractweburl.ts new file mode 100644 index 000000000..9ef57c60a --- /dev/null +++ b/packages/sp/src/utils/extractweburl.ts @@ -0,0 +1,21 @@ +import { stringIsNullOrEmpty } from "@pnp/common"; + +export function extractWebUrl(candidateUrl: string): string { + + if (stringIsNullOrEmpty(candidateUrl)) { + return ""; + } + + let index = candidateUrl.indexOf("_api/"); + + if (index < 0) { + index = candidateUrl.indexOf("_vti_bin/"); + } + + if (index > -1) { + return candidateUrl.substr(0, index); + } + + // if all else fails just give them what they gave us back + return candidateUrl; +} diff --git a/packages/sp/src/utils/file-names.ts b/packages/sp/src/utils/file-names.ts new file mode 100644 index 000000000..0112785f6 --- /dev/null +++ b/packages/sp/src/utils/file-names.ts @@ -0,0 +1,35 @@ +const InvalidFileFolderNameCharsOnlineRegex = /["*:<>?/\\|\x00-\x1f\x7f-\x9f]/g; +const InvalidFileFolderNameCharsOnPremiseRegex = /["#%*:<>?/\\|\x00-\x1f\x7f-\x9f]/g; + +/** + * Checks if file or folder name contains invalid characters + * + * @param input File or folder name to check + * @param onPremise Set to true for SharePoint On-Premise + * @returns True if contains invalid chars, false otherwise + */ +export function containsInvalidFileFolderChars(input: string, onPremise = false): boolean { + if (onPremise) { + return InvalidFileFolderNameCharsOnPremiseRegex.test(input); + } else { + return InvalidFileFolderNameCharsOnlineRegex.test(input); + } +} + +/** + * Removes invalid characters from file or folder name + * + * @param input File or folder name + * @param replacer Value that will replace invalid characters + * @param onPremise Set to true for SharePoint On-Premise + * @returns File or folder name with replaced invalid characters + */ +export function stripInvalidFileFolderChars(input: string, replacer = "", onPremise = false): string { + if (onPremise) { + return input.replace(InvalidFileFolderNameCharsOnPremiseRegex, replacer); + } else { + return input.replace(InvalidFileFolderNameCharsOnlineRegex, replacer); + } +} + +// TODO:: global method to steralize filename inputs diff --git a/packages/sp/src/utils/metadata.ts b/packages/sp/src/utils/metadata.ts new file mode 100644 index 000000000..a17e6e76b --- /dev/null +++ b/packages/sp/src/utils/metadata.ts @@ -0,0 +1,5 @@ +export function metadata(type: string) { + return { + "__metadata": { "type": type }, + }; +} diff --git a/packages/sp/src/utils/toabsoluteurl.ts b/packages/sp/src/utils/toabsoluteurl.ts new file mode 100644 index 000000000..d6dd81aa1 --- /dev/null +++ b/packages/sp/src/utils/toabsoluteurl.ts @@ -0,0 +1,48 @@ +declare var global: { location: string, _spPageContextInfo?: { webAbsoluteUrl?: string, webServerRelativeUrl?: string } }; +import { combine, isUrlAbsolute, hOP } from "@pnp/common"; +import { SPRuntimeConfig } from "../config/splibconfig"; + +/** + * Ensures that a given url is absolute for the current web based on context + * + * @param candidateUrl The url to make absolute + * + */ +export function toAbsoluteUrl(candidateUrl: string): Promise { + + return new Promise((resolve) => { + + if (isUrlAbsolute(candidateUrl)) { + // if we are already absolute, then just return the url + return resolve(candidateUrl); + } + + if (SPRuntimeConfig.baseUrl !== null) { + // base url specified either with baseUrl of spfxContext config property + return resolve(combine(SPRuntimeConfig.baseUrl, candidateUrl)); + } + + if (global._spPageContextInfo !== undefined) { + + // operating in classic pages + if (hOP(global._spPageContextInfo, "webAbsoluteUrl")) { + return resolve(combine(global._spPageContextInfo.webAbsoluteUrl, candidateUrl)); + } else if (hOP(global._spPageContextInfo, "webServerRelativeUrl")) { + return resolve(combine(global._spPageContextInfo.webServerRelativeUrl, candidateUrl)); + } + } + + // does window.location exist and have a certain path part in it? + if (global.location !== undefined) { + const baseUrl = global.location.toString().toLowerCase(); + ["/_layouts/", "/siteassets/"].forEach((s: string) => { + const index = baseUrl.indexOf(s); + if (index > 0) { + return resolve(combine(baseUrl.substr(0, index), candidateUrl)); + } + }); + } + + return resolve(candidateUrl); + }); +} diff --git a/packages/sp/src/views/index.ts b/packages/sp/src/views/index.ts new file mode 100644 index 000000000..241e4df48 --- /dev/null +++ b/packages/sp/src/views/index.ts @@ -0,0 +1,12 @@ +import "./list"; + +export { + IView, + IViewFields, + IViews, + View, + ViewFields, + Views, + IViewAddResult, + IViewUpdateResult, +} from "./types"; diff --git a/packages/sp/src/views/list.ts b/packages/sp/src/views/list.ts new file mode 100644 index 000000000..4280b021a --- /dev/null +++ b/packages/sp/src/views/list.ts @@ -0,0 +1,30 @@ +import { addProp } from "@pnp/odata"; +import { _List } from "../lists/types"; +import { Views, IViews, IView, View } from "./types"; + +/** +* Extend Item +*/ +declare module "../lists/types" { + interface _List { + readonly views: IViews; + readonly defaultView: IView; + getView(id: string): IView; + } + interface IList { + readonly views: IViews; + readonly defaultView: IView; + getView(id: string): IView; + } +} + +addProp(_List, "views", Views); +addProp(_List, "defaultView", View, "DefaultView"); + +/** + * Gets a view by view guid id + * + */ +_List.prototype.getView = function (this: _List, viewId: string): IView { + return View(this, `getView('${viewId}')`); +}; diff --git a/packages/sp/src/views/types.ts b/packages/sp/src/views/types.ts new file mode 100644 index 000000000..2447415bd --- /dev/null +++ b/packages/sp/src/views/types.ts @@ -0,0 +1,178 @@ +import { + _SharePointQueryableInstance, + ISharePointQueryableInstance, + ISharePointQueryableCollection, + _SharePointQueryableCollection, + spInvokableFactory, +} from "../sharepointqueryable"; +import { TypedHash } from "@pnp/common"; +import { metadata } from "../utils/metadata"; +import { IGetable, body } from "@pnp/odata"; +import { defaultPath, IDeleteable, deleteable } from "../decorators"; +import { spPost } from "../operations"; + +/** + * Describes the views available in the current context + * + */ +@defaultPath("views") +export class _Views extends _SharePointQueryableCollection implements IViews { + + /** + * Gets a view by guid id + * + * @param id The GUID id of the view + */ + public getById(id: string): IView { + return View(this).concat(`('${id}')`); + } + + /** + * Gets a view by title (case-sensitive) + * + * @param title The case-sensitive title of the view + */ + public getByTitle(title: string): IView { + return View(this, `getByTitle('${title}')`); + } + + /** + * Adds a new view to the collection + * + * @param title The new views's title + * @param personalView True if this is a personal view, otherwise false, default = false + * @param additionalSettings Will be passed as part of the view creation body + */ + public async add(title: string, personalView = false, additionalSettings: TypedHash = {}): Promise { + + const postBody = body(Object.assign(metadata("SP.View"), { + "PersonalView": personalView, + "Title": title, + }, additionalSettings)); + + const data = await spPost(this.clone(Views, null), postBody); + + return { + data, + view: this.getById(data.Id), + }; + } +} + +export interface IViews extends IGetable, ISharePointQueryableCollection { + getById(id: string): IView; + getByTitle(title: string): IView; + add(title: string, personalView?: boolean, additionalSettings?: TypedHash): Promise; +} +export interface _Views extends IGetable { } +export const Views = spInvokableFactory(_Views); + +/** + * Describes a single View instance + * + */ +@deleteable() +export class _View extends _SharePointQueryableInstance implements IView { + + public get fields(): IViewFields { + return ViewFields(this); + } + + /** + * Updates this view intance with the supplied properties + * + * @param properties A plain object hash of values to update for the view + */ + public update: any = this._update>("SP.View", data => ({ data, view: this })); + + /** + * Returns the list view as HTML. + * + */ + public renderAsHtml(): Promise { + return this.clone(View, "renderashtml")(); + } + + /** + * Sets the view schema + * + * @param viewXml The view XML to set + */ + public setViewXml(viewXml: string): Promise { + return spPost(this.clone(View, "SetViewXml"), body({ viewXml })); + } +} + +export interface IView extends IGetable, ISharePointQueryableInstance, IDeleteable { + readonly fields: IViewFields; + update(props: TypedHash): IViewUpdateResult; + renderAsHtml(): Promise; + setViewXml(viewXml: string): Promise; +} +export interface _View extends IGetable, IDeleteable { } +export const View = spInvokableFactory(_View); + +@defaultPath("viewfields") +export class _ViewFields extends _SharePointQueryableCollection implements IViewFields { + /** + * Gets a value that specifies the XML schema that represents the collection. + */ + public getSchemaXml(): Promise { + return this.clone(ViewFields, "schemaxml")(); + } + + /** + * Adds the field with the specified field internal name or display name to the collection. + * + * @param fieldTitleOrInternalName The case-sensitive internal name or display name of the field to add. + */ + public add(fieldTitleOrInternalName: string): Promise { + return spPost(this.clone(ViewFields, `addviewfield('${fieldTitleOrInternalName}')`)); + } + + /** + * Moves the field with the specified field internal name to the specified position in the collection. + * + * @param field The case-sensitive internal name of the field to move. + * @param index The zero-based index of the new position for the field. + */ + public move(field: string, index: number): Promise { + return spPost(this.clone(ViewFields, "moveviewfieldto"), body({ field, index })); + } + + /** + * Removes all the fields from the collection. + */ + public removeAll(): Promise { + return spPost(this.clone(ViewFields, "removeallviewfields")); + } + + /** + * Removes the field with the specified field internal name from the collection. + * + * @param fieldInternalName The case-sensitive internal name of the field to remove from the view. + */ + public remove(fieldInternalName: string): Promise { + return spPost(this.clone(ViewFields, `removeviewfield('${fieldInternalName}')`)); + } +} + +export interface IViewFields extends IGetable, ISharePointQueryableCollection { + getSchemaXml(): Promise; + add(fieldTitleOrInternalName: string): Promise; + move(fieldInternalName: string, index: number): Promise; + removeAll(): Promise; + remove(fieldInternalName: string): Promise; +} +export interface _ViewFields extends IGetable { } +export const ViewFields = spInvokableFactory(_ViewFields); + +export interface IViewAddResult { + view: IView; + data: any; +} + +export interface IViewUpdateResult { + view: IView; + data: any; +} diff --git a/packages/sp/src/webparts/file.ts b/packages/sp/src/webparts/file.ts new file mode 100644 index 000000000..b9a0f8f1d --- /dev/null +++ b/packages/sp/src/webparts/file.ts @@ -0,0 +1,30 @@ +import { _File } from "../files/types"; +import { WebPartsPersonalizationScope, ILimitedWebPartManager, LimitedWebPartManager } from "./types"; + +/** +* Extend Item +*/ +declare module "../files/types" { + interface _File { + /** + * Specifies the control set used to access, modify, or add Web Parts associated with this Web Part Page and view. + * An exception is thrown if the file is not an ASPX page. + * + * @param scope The WebPartsPersonalizationScope view on the Web Parts page. + */ + getLimitedWebPartManager(scope?: WebPartsPersonalizationScope): ILimitedWebPartManager; + } + interface IFile { + /** + * Specifies the control set used to access, modify, or add Web Parts associated with this Web Part Page and view. + * An exception is thrown if the file is not an ASPX page. + * + * @param scope The WebPartsPersonalizationScope view on the Web Parts page. + */ + getLimitedWebPartManager(scope?: WebPartsPersonalizationScope): ILimitedWebPartManager; + } +} + +_File.prototype.getLimitedWebPartManager = function (this: _File, scope = WebPartsPersonalizationScope.Shared): ILimitedWebPartManager { + return LimitedWebPartManager(this, `getLimitedWebPartManager(scope=${scope})`); +}; diff --git a/packages/sp/src/webparts/index.ts b/packages/sp/src/webparts/index.ts new file mode 100644 index 000000000..c1d24fb4c --- /dev/null +++ b/packages/sp/src/webparts/index.ts @@ -0,0 +1,11 @@ +import "./file"; + +export { + ILimitedWebPartManager, + WebPartsPersonalizationScope, + WebPartDefinitions, + WebPartDefinition, + LimitedWebPartManager, + IWebPartDefinitions, + IWebPartDefinition, +} from "./types"; diff --git a/packages/sp/src/webparts/types.ts b/packages/sp/src/webparts/types.ts new file mode 100644 index 000000000..a094034f2 --- /dev/null +++ b/packages/sp/src/webparts/types.ts @@ -0,0 +1,158 @@ +import { + _SharePointQueryableInstance, + _SharePointQueryableCollection, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + _SharePointQueryable, + ISharePointQueryable, + spInvokableFactory, +} from "../sharepointqueryable"; +import { IGetable, body } from "@pnp/odata"; +import { SharePointQueryableInstance } from "../sp"; +import { spPost } from "../operations"; + +export class _LimitedWebPartManager extends _SharePointQueryable implements ILimitedWebPartManager { + + /** + * Gets the set of web part definitions contained by this web part manager + * + */ + public get webparts(): IWebPartDefinitions { + return WebPartDefinitions(this, "webparts"); + } + + /** + * Exports a webpart definition + * + * @param id the GUID id of the definition to export + */ + public export(id: string): Promise { + + return spPost(this.clone(LimitedWebPartManagerCloneFactory, "ExportWebPart"), body({ webPartId: id })); + } + + /** + * Imports a webpart + * + * @param xml webpart definition which must be valid XML in the .dwp or .webpart format + */ + public import(xml: string): Promise { + + return spPost(this.clone(LimitedWebPartManagerCloneFactory, "ImportWebPart"), body({ webPartXml: xml })); + } +} + +export interface ILimitedWebPartManager { + readonly webparts: IWebPartDefinitions; + export(id: string): Promise; + import(xml: string): Promise; +} + +export const LimitedWebPartManager = (baseUrl: string | ISharePointQueryable, path?: string): ILimitedWebPartManager => new _LimitedWebPartManager(baseUrl, path); + +type LimitedWebPartManagerCloneType = ILimitedWebPartManager & ISharePointQueryable; +const LimitedWebPartManagerCloneFactory = (baseUrl: string | ISharePointQueryable, path?: string): LimitedWebPartManagerCloneType => LimitedWebPartManager(baseUrl, path); + +export class _WebPartDefinitions extends _SharePointQueryableCollection { + + /** + * Gets a web part definition from the collection by id + * + * @param id The storage ID of the SPWebPartDefinition to retrieve + */ + public getById(id: string): IWebPartDefinition { + return WebPartDefinition(this, `getbyid('${id}')`); + } + + /** + * Gets a web part definition from the collection by storage id + * + * @param id The WebPart.ID of the SPWebPartDefinition to retrieve + */ + public getByControlId(id: string): IWebPartDefinition { + return WebPartDefinition(this, `getByControlId('${id}')`); + } +} + +export interface IWebPartDefinitions extends IGetable, ISharePointQueryableCollection { + getById(id: string): IWebPartDefinition; + getByControlId(id: string): IWebPartDefinition; +} +export interface _WebPartDefinitions extends IGetable { } +export const WebPartDefinitions = spInvokableFactory(_WebPartDefinitions); + + +export class _WebPartDefinition extends _SharePointQueryableInstance implements IWebPartDefinition { + + public get webpart(): ISharePointQueryableInstance { + return SharePointQueryableInstance(this, "webpart"); + } + + public saveChanges(): Promise { + + return spPost(this.clone(WebPartDefinition, "SaveWebPartChanges")); + } + + public moveTo(zoneId: string, zoneIndex: number): Promise { + + return spPost(this.clone(WebPartDefinition, `MoveWebPartTo(zoneID='${zoneId}', zoneIndex=${zoneIndex})`)); + } + + public close(): Promise { + + return spPost(this.clone(WebPartDefinition, "CloseWebPart")); + } + + public open(): Promise { + + return spPost(this.clone(WebPartDefinition, "OpenWebPart")); + + } + + public delete(): Promise { + + return spPost(this.clone(WebPartDefinition, "DeleteWebPart")); + } +} + +export interface IWebPartDefinition extends IGetable, ISharePointQueryableInstance { + /** + * Gets the webpart information associated with this definition + */ + readonly webpart: ISharePointQueryableInstance; + + /** + * Saves changes to the Web Part made using other properties and methods on the SPWebPartDefinition object + */ + saveChanges(): Promise; + + /** + * Moves the Web Part to a different location on a Web Part Page + * + * @param zoneId The ID of the Web Part Zone to which to move the Web Part + * @param zoneIndex A Web Part zone index that specifies the position at which the Web Part is to be moved within the destination Web Part zone + */ + moveTo(zoneId: string, zoneIndex: number): Promise; + + /** + * Closes the Web Part. If the Web Part is already closed, this method does nothing + */ + close(): Promise; + + /** + * Opens the Web Part. If the Web Part is already closed, this method does nothing + */ + open(): Promise; + + /** + * Removes a webpart from a page, all settings will be lost + */ + delete(): Promise; +} +export interface IWebPartDefinition extends IGetable { } +export const WebPartDefinition = spInvokableFactory(_WebPartDefinition); + +export enum WebPartsPersonalizationScope { + User = 0, + Shared = 1, +} diff --git a/packages/sp/src/webs/index.ts b/packages/sp/src/webs/index.ts new file mode 100644 index 000000000..63fbb9a19 --- /dev/null +++ b/packages/sp/src/webs/index.ts @@ -0,0 +1,42 @@ +import { Web, IWeb } from "./types"; +import { SPRest } from "../rest"; +import { SPBatch } from "../batch"; + +export { + IWeb, + IWebs, + Web, + IWebAddResult, + IWebUpdateResult, + Webs, + IWebInfo, + IStorageEntity, +} from "./types"; + +declare module "../rest" { + interface SPRest { + + /** + * Access to the current web instance + */ + readonly web: IWeb; + + /** + * Creates a new batch object for use with the SharePointQueryable.addToBatch method + * + */ + createBatch(): SPBatch; + } +} + +Reflect.defineProperty(SPRest.prototype, "web", { + configurable: true, + enumerable: true, + get: function (this: SPRest) { + return Web(this._baseUrl).configure(this._options); + }, +}); + +SPRest.prototype.createBatch = function (this: SPRest): SPBatch { + return this.web.createBatch(); +}; diff --git a/packages/sp/src/webs/types.ts b/packages/sp/src/webs/types.ts new file mode 100644 index 000000000..06be87bdb --- /dev/null +++ b/packages/sp/src/webs/types.ts @@ -0,0 +1,326 @@ +import { extend, TypedHash } from "@pnp/common"; +import { IGetable, body, headers } from "@pnp/odata"; +import { + _SharePointQueryableInstance, + SharePointQueryableCollection, + _SharePointQueryableCollection, + ISharePointQueryableCollection, + ISharePointQueryableInstance, + spInvokableFactory, + SharePointQueryableInstance, +} from "../sharepointqueryable"; +import { defaultPath, deleteable, IDeleteable, clientTagMethod } from "../decorators"; +import { IChangeQuery } from "../types"; +import { odataUrlFrom } from "../odata"; +import { SPBatch } from "../batch"; +import { metadata } from "../utils/metadata"; +import { Site, IOpenWebByIdResult } from "../sites"; +import { spPost, spGet } from "../operations"; +import { escapeQueryStrValue } from "../utils/escapeSingleQuote"; + +@defaultPath("webs") +export class _Webs extends _SharePointQueryableCollection implements IWebs { + + @clientTagMethod("ws.add") + public async add(title: string, url: string, description = "", template = "STS", language = 1033, inheritPermissions = true): Promise { + + const postBody = body({ + "parameters": + extend(metadata("SP.WebCreationInformation"), + { + Description: description, + Language: language, + Title: title, + Url: url, + UseSamePermissionsAsParentSite: inheritPermissions, + WebTemplate: template, + }), + }); + + const data = await spPost(this.clone(Webs, "add"), postBody); + + return { + data, + web: Web(odataUrlFrom(data).replace(/_api\/web\/?/i, "")), + }; + } +} + +/** + * Describes a collection of webs + */ +export interface IWebs extends IGetable, ISharePointQueryableCollection { + + /** + * Adds a new web to the collection + * + * @param title The new web's title + * @param url The new web's relative url + * @param description The new web's description + * @param template The new web's template internal name (default = STS) + * @param language The locale id that specifies the new web's language (default = 1033 [English, US]) + * @param inheritPermissions When true, permissions will be inherited from the new web's parent (default = true) + */ + add(title: string, url: string, description?: string, template?: string, language?: number, inheritPermissions?: boolean): Promise; +} +export interface _Webs extends IGetable { } +export const Webs = spInvokableFactory(_Webs); + +/** + * Describes a web + * + */ +@defaultPath("_api/web") +@deleteable() +export class _Web extends _SharePointQueryableInstance implements IWeb { + + public get webs(): IWebs { + return Webs(this); + } + + @clientTagMethod("w.getParentWeb") + public getParentWeb(): Promise { + return spGet(this.select("ParentWeb/Id").expand("ParentWeb")) + .then(({ ParentWeb }) => ParentWeb ? Site(this.parentUrl).openWebById(ParentWeb.Id) : null); + } + + public getSubwebsFilteredForCurrentUser(nWebTemplateFilter = -1, nConfigurationFilter = -1): IWebs { + const o = this.clone(Webs, `getSubwebsFilteredForCurrentUser(nWebTemplateFilter=${nWebTemplateFilter},nConfigurationFilter=${nConfigurationFilter})`); + return clientTagMethod.configure(o, "w.getSubwebsFilteredForCurrentUser"); + } + + public get allProperties(): ISharePointQueryableInstance { + return this.clone(SharePointQueryableInstance, "allproperties"); + } + + public get webinfos(): ISharePointQueryableCollection { + return clientTagMethod.configure(SharePointQueryableCollection(this, "webinfos"), "w.webinfos"); + } + + public createBatch(): SPBatch { + return new SPBatch(this.parentUrl); + } + + @clientTagMethod("w.update") + public async update(properties: TypedHash): Promise { + + const postBody = body(extend(metadata("SP.Web"), properties), headers({ "X-HTTP-Method": "MERGE" })); + + const data = await spPost(this, postBody); + + return { data, web: this }; + } + + @clientTagMethod("w.applyTheme") + public applyTheme(colorPaletteUrl: string, fontSchemeUrl: string, backgroundImageUrl: string, shareGenerated: boolean): Promise { + + const postBody = body({ + backgroundImageUrl, + colorPaletteUrl, + fontSchemeUrl, + shareGenerated, + }); + + return spPost(this.clone(Web, "applytheme"), postBody); + } + + @clientTagMethod("w.applyWebTemplate") + public applyWebTemplate(template: string): Promise { + + const q = this.clone(Web, "applywebtemplate"); + q.concat(`(webTemplate='${escapeQueryStrValue(template)}')`); + return spPost(q); + } + + public availableWebTemplates(language = 1033, includeCrossLanugage = true): ISharePointQueryableCollection { + const path = `getavailablewebtemplates(lcid=${language}, doincludecrosslanguage=${includeCrossLanugage})`; + return clientTagMethod.configure(SharePointQueryableCollection(this, path), "w.availableWebTemplates"); + } + + @clientTagMethod("w.getChanges") + public getChanges(query: IChangeQuery): Promise { + + const postBody = body({ "query": extend({ "__metadata": { "type": "SP.ChangeQuery" } }, query) }); + return spPost(this.clone(Web, "getchanges"), postBody); + } + + @clientTagMethod("w.mapToIcon") + public mapToIcon(filename: string, size = 0, progId = ""): Promise { + return spGet(this.clone(Web, `maptoicon(filename='${escapeQueryStrValue(filename)}', progid='${escapeQueryStrValue(progId)}', size=${size})`)); + } + + @clientTagMethod("w.getStorageEntity") + public getStorageEntity(key: string): Promise { + return spGet(this.clone(Web, `getStorageEntity('${escapeQueryStrValue(key)}')`)); + } + + @clientTagMethod("w.setStorageEntity") + public setStorageEntity(key: string, value: string, description = "", comments = ""): Promise { + return spPost(this.clone(Web, `setStorageEntity`), body({ + comments, + description, + key, + value, + })); + } + + @clientTagMethod("w.removeStorageEntity") + public removeStorageEntity(key: string): Promise { + return spPost(this.clone(Web, `removeStorageEntity('${escapeQueryStrValue(key)}')`)); + } +} + +export interface IWeb extends IGetable, ISharePointQueryableInstance, IDeleteable { + + /** + * Gets this web's subwebs + * + */ + readonly webs: IWebs; + + /** + * Allows access to the web's all properties collection + */ + readonly allProperties: ISharePointQueryableInstance; + + /** + * Gets a collection of WebInfos for this web's subwebs + * + */ + readonly webinfos: ISharePointQueryableCollection; + + /** + * Gets this web's parent web and data + * + */ + getParentWeb(): Promise; + + /** + * Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member. + * + * @param nWebTemplateFilter Specifies the site definition (default = -1) + * @param nConfigurationFilter A 16-bit integer that specifies the identifier of a configuration (default = -1) + */ + getSubwebsFilteredForCurrentUser(nWebTemplateFilter?: number, nConfigurationFilter?: number): IWebs; + + /** + * Creates a new batch for requests within the context of this web + * + */ + createBatch(): SPBatch; + + /** + * Updates this web instance with the supplied properties + * + * @param properties A plain object hash of values to update for the web + */ + update(properties: TypedHash): Promise; + + /** + * Applies the theme specified by the contents of each of the files specified in the arguments to the site + * + * @param colorPaletteUrl The server-relative URL of the color palette file + * @param fontSchemeUrl The server-relative URL of the font scheme + * @param backgroundImageUrl The server-relative URL of the background image + * @param shareGenerated When true, the generated theme files are stored in the root site. When false, they are stored in this web + */ + applyTheme(colorPaletteUrl: string, fontSchemeUrl: string, backgroundImageUrl: string, shareGenerated: boolean): Promise; + + /** + * Applies the specified site definition or site template to the Web site that has no template applied to it + * + * @param template Name of the site definition or the name of the site template + */ + applyWebTemplate(template: string): Promise; + + /** + * Returns a collection of site templates available for the site + * + * @param language The locale id of the site templates to retrieve (default = 1033 [English, US]) + * @param includeCrossLanguage When true, includes language-neutral site templates; otherwise false (default = true) + */ + availableWebTemplates(language?: number, includeCrossLanugage?: boolean): ISharePointQueryableCollection; + + /** + * Returns the collection of changes from the change log that have occurred within the list, based on the specified query + * + * @param query The change query + */ + getChanges(query: IChangeQuery): Promise; + + /** + * Returns the name of the image file for the icon that is used to represent the specified file + * + * @param filename The file name. If this parameter is empty, the server returns an empty string + * @param size The size of the icon: 16x16 pixels = 0, 32x32 pixels = 1 (default = 0) + * @param progId The ProgID of the application that was used to create the file, in the form OLEServerName.ObjectName + */ + mapToIcon(filename: string, size?: number, progId?: string): Promise; + + /** + * Returns the tenant property corresponding to the specified key in the app catalog site + * + * @param key Id of storage entity to be set + */ + getStorageEntity(key: string): Promise; + + /** + * This will set the storage entity identified by the given key (MUST be called in the context of the app catalog) + * + * @param key Id of storage entity to be set + * @param value Value of storage entity to be set + * @param description Description of storage entity to be set + * @param comments Comments of storage entity to be set + */ + setStorageEntity(key: string, value: string, description?: string, comments?: string): Promise; + + /** + * This will remove the storage entity identified by the given key + * + * @param key Id of storage entity to be removed + */ + removeStorageEntity(key: string): Promise; +} +export interface _Web extends IGetable, IDeleteable { } +export const Web = spInvokableFactory(_Web); + +/** + * Result from adding a web + * + */ +export interface IWebAddResult { + data: any; + web: IWeb; +} + +/** + * Result from updating a web + * + */ +export interface IWebUpdateResult { + data: any; + web: IWeb; +} + +export interface IWebInfo { + Configuration: number; + Created: string; + Description: string; + Id: string; + Language: number; + LastItemModifiedDate: string; + LastItemUserModifiedDate: string; + "odata.editLink": string; + "odata.id": string; + "odata.type": string; + ServerRelativeUrl: string; + Title: string; + WebTemplate: string; + WebTemplateId: number; +} + +export interface IStorageEntity { + Value: string | null; + Comment: string | null; + Description: string | null; +} diff --git a/packages/sp/tsconfig.es5.json b/packages/sp/tsconfig.es5.json new file mode 100644 index 000000000..a73e0f09f --- /dev/null +++ b/packages/sp/tsconfig.es5.json @@ -0,0 +1,28 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "strictNullChecks": false + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "./presets/**/*.ts" + ], + "references": [ + { + "path": "../common/tsconfig.es5.json" + }, + { + "path": "../logging/tsconfig.es5.json" + }, + { + "path": "../odata/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/sp/tsconfig.json b/packages/sp/tsconfig.json new file mode 100644 index 000000000..fac039a40 --- /dev/null +++ b/packages/sp/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strictNullChecks": false + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + "../common/index.ts", + "../common/src/**/*.ts", + "../logging/index.ts", + "../logging/src/**/*.ts", + "../odata/index.ts", + "../odata/src/**/*.ts", + "./presets/**/*.ts" + ], + "references": [ + { + "path": "../common" + }, + { + "path": "../logging" + }, + { + "path": "../odata" + } + ] +} \ No newline at end of file diff --git a/packages/tsconfig.es5.json b/packages/tsconfig.es5.json new file mode 100644 index 000000000..b808bace6 --- /dev/null +++ b/packages/tsconfig.es5.json @@ -0,0 +1,37 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "es5", + "outDir": "../build/packages-es5" + }, + "include": [], + "references": [ + { + "path": "./common/tsconfig.es5.json" + }, + { + "path": "./config-store/tsconfig.es5.json" + }, + { + "path": "./graph/tsconfig.es5.json" + }, + { + "path": "./logging/tsconfig.es5.json" + }, + { + "path": "./nodejs/tsconfig.es5.json" + }, + { + "path": "./odata/tsconfig.es5.json" + }, + { + "path": "./pnpjs/tsconfig.es5.json" + }, + { + "path": "./sp/tsconfig.es5.json" + }, + { + "path": "./sp-addinhelpers/tsconfig.es5.json" + } + ] +} \ No newline at end of file diff --git a/packages/tsconfig.json b/packages/tsconfig.json new file mode 100644 index 000000000..0573a2db6 --- /dev/null +++ b/packages/tsconfig.json @@ -0,0 +1,70 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "es2015", + "composite": true, + "declaration": true, + "declarationMap": true, + "removeComments": false, + "types": [], + "lib": [ + "es2015", + "dom" + ], + "baseUrl": ".", + "rootDir": ".", + "outDir": "../build/packages", + "strict": true, + "allowUnreachableCode": false, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "pretty": true, + "experimentalDecorators": true, + "noFallthroughCasesInSwitch": true, + "inlineSources": true, + "sourceMap": true, + "skipDefaultLibCheck": true, + "downlevelIteration": true, + "skipLibCheck": true, + "strictNullChecks": false, + "preserveConstEnums": true, + "importHelpers": true, + "paths": { + "@pnp/*": [ + "./*" + ] + } + }, + "include": [], + "references": [ + { + "path": "./common" + }, + { + "path": "./config-store" + }, + { + "path": "./graph" + }, + { + "path": "./logging" + }, + { + "path": "./nodejs" + }, + { + "path": "./odata" + }, + { + "path": "./pnpjs" + }, + { + "path": "./sp" + }, + { + "path": "./sp-addinhelpers" + } + ] +} \ No newline at end of file diff --git a/pnp-build.js b/pnp-build.js new file mode 100644 index 000000000..ebd146a16 --- /dev/null +++ b/pnp-build.js @@ -0,0 +1,24 @@ +const tasks = require("./build/tools/buildsystem").Tasks.Build, + path = require("path"); + +module.exports = { + + packageRoot: path.resolve("./packages/"), + + exclude: ["documentation"], + + preBuildTasks: [ + // function | { packages: [], task: function } + ], + + // these tsconfig files will all be transpiled per the settings in the file + buildTargets: [ + path.resolve("./packages/tsconfig.json"), + path.resolve("./packages/tsconfig.es5.json"), + ], + + postBuildTasks: [ + // this task is scoped to the sp files within the task + tasks.replaceSPHttpVersion, + ], +}; diff --git a/pnp-debug.js b/pnp-debug.js new file mode 100644 index 000000000..52d2700db --- /dev/null +++ b/pnp-debug.js @@ -0,0 +1,22 @@ +const tasks = require("./build/tools/buildsystem").Tasks.Build, + path = require("path"); + +module.exports = { + + packageRoot: path.resolve("./debug/"), + + exclude: [], + + preBuildTasks: [ + // function OR { packages: [], task: function } + ], + + // these tsconfig files will all be transpiled per the settings in the file + buildTargets: [ + path.resolve("./debug/launch/tsconfig.json"), + ], + + postBuildTasks: [ + tasks.replaceDebug, + ], +}; diff --git a/pnp-package.js b/pnp-package.js new file mode 100644 index 000000000..2983d181e --- /dev/null +++ b/pnp-package.js @@ -0,0 +1,27 @@ +// build funcs +const tasks = require("./build/tools/buildsystem").Tasks.Package, + path = require("path"); + +module.exports = { + + packageTargets: [{ + // we only need to package the main tsconfig as we are just using it for references + // we have previously built all the things + packageTarget: path.resolve("./packages/tsconfig.json"), + outDir: path.resolve("./dist/packages/"), + }], + + prePackageTasks: [], + + packageTasks: [ + tasks.webpack, + tasks.rollup, + ], + + postPackageTasks: [ + tasks.writePackageFiles, + tasks.copyDefs, + tasks.copyDocs, + tasks.copyStaticAssets, + ], +}; diff --git a/pnp-publish-beta.js b/pnp-publish-beta.js new file mode 100644 index 000000000..84ccdac09 --- /dev/null +++ b/pnp-publish-beta.js @@ -0,0 +1,13 @@ +const tasks = require("./build/tools/buildsystem").Tasks.Publish, + path = require("path"); + +module.exports = { + + packageRoot: path.resolve("./dist/packages"), + + prePublishTasks: [], + + publishTasks: [tasks.publishBetaPackage], + + postPublishTasks: [], +} diff --git a/pnp-publish.js b/pnp-publish.js new file mode 100644 index 000000000..455d6e0c3 --- /dev/null +++ b/pnp-publish.js @@ -0,0 +1,13 @@ +const tasks = require("./build/tools/buildsystem").Tasks.Publish, + path = require("path"); + +module.exports = { + + packageRoot: path.resolve("./dist/packages"), + + prePublishTasks: [], + + publishTasks: [tasks.publishPackage], + + postPublishTasks: [], +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 000000000..ddd184503 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,86 @@ + +const getSubDirNames = require("./tools/node-utils/getSubDirectoryNames"), + sourcemaps = require("rollup-plugin-sourcemaps"), + uglify = require("rollup-plugin-uglify"), + globals = require("rollup-plugin-node-globals"), + nodeResolve = require("rollup-plugin-node-resolve"), + banner = require("./banner"); + +const packageSources = getSubDirNames("./build/packages/"); +const packageSourcesEs5 = getSubDirNames("./build/packages-es5/"); + +const libraryNameGen = (name) => name === "pnpjs" ? "pnp" : `pnp.${name}`; + +const globalPackageRefs = packageSources.reduce((o, c) => { + o[`@pnp/${c}`] = libraryNameGen(c); + return o; +}, {}); + +const sharedPlugins = [ + sourcemaps(), + globals(), + nodeResolve({ + only: ["tslib"], + }), +]; + +const externals = packageSources.map(c => `@pnp/${c}`).concat(["adal-angular/dist/adal.min.js", "adal-node"]); + +const es2015ConfigGen = (moduleName) => Object.assign({}, { + + input: `./build/packages/${moduleName}/index.js`, + plugins: [...sharedPlugins], + external: externals, + output: { + file: `./dist/packages/${moduleName}/dist/${moduleName}.js`, + format: "es", + sourcemap: true, + banner, + } +}); + +const es5ConfigGen = (moduleName) => Object.assign({}, { + + input: `./build/packages-es5/${moduleName}/index.js`, + plugins: [...sharedPlugins], + external: externals, + output: [{ + file: `./dist/packages/${moduleName}/dist/${moduleName}.es5.umd.js`, + format: "umd", + name: libraryNameGen(moduleName), + sourcemap: true, + globals: globalPackageRefs, + banner, + }, + { + file: `./dist/packages/${moduleName}/dist/${moduleName}.es5.js`, + format: "es", + sourcemap: true, + banner, + }] +}); + +const es5MinConfigGen = (moduleName) => Object.assign({}, { + + input: `./build/packages-es5/${moduleName}/index.js`, + plugins: [...sharedPlugins, uglify.uglify({ + output: { + comments: (node, comment) => comment.type === "comment2" ? /@license/i.test(comment.value) : false, + } + })], + external: externals, + output: [{ + file: `./dist/packages/${moduleName}/dist/${moduleName}.es5.umd.min.js`, + format: "umd", + name: libraryNameGen(moduleName), + sourcemap: true, + globals: globalPackageRefs, + banner, + }] +}); + +module.exports = [ + ...packageSources.map(pkgName => es2015ConfigGen(pkgName)), + ...packageSourcesEs5.map(pkgName => es5ConfigGen(pkgName)), + ...packageSourcesEs5.map(pkgName => es5MinConfigGen(pkgName)) +]; diff --git a/settings.example.js b/settings.example.js new file mode 100644 index 000000000..ee539c142 --- /dev/null +++ b/settings.example.js @@ -0,0 +1,24 @@ +var settings = { + + spsave: { + username: "develina.devsson@mydevtenant.onmicrosoft.com", + password: "pass@word1", + siteUrl: "https://mydevtenant.sharepoint.com/" + }, + testing: { + enableWebTests: true, + sp: { + id: "{ client id }", + secret: "{ client secret }", + url: "{ site collection url }", + notificationUrl: "{ notification url }", + }, + graph: { + tenant: "{tenant.onmicrosoft.com}", + id: "{your app id}", + secret: "{your secret}" + }, + } +} + +module.exports = settings; diff --git a/test/main.ts b/test/main.ts new file mode 100644 index 000000000..8f9bef42a --- /dev/null +++ b/test/main.ts @@ -0,0 +1,213 @@ +// import { Logger, LogLevel, ConsoleListener } from "@pnp/logging"; +import { getGUID, combine, extend } from "@pnp/common"; +import { graph } from "@pnp/graph"; +import { AdalFetchClient, SPFetchClient } from "@pnp/nodejs"; +import { sp } from "@pnp/sp"; +// import { Web } from "@pnp/sp/src/webs"; +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import "mocha"; + +chai.use(chaiAsPromised); + +declare var process: any; + +// Logger.activeLogLevel = LogLevel.Verbose; +// Logger.subscribe(new ConsoleListener()); + +export interface ISettingsTestingPart { + enableWebTests: boolean; + graph?: { + id: string; + secret: string; + tenant: string; + }; + sp?: { + webUrl?: string; + id: string; + notificationUrl: string | null; + secret: string; + url: string; + }; +} + +export interface ISettings { + testing: ISettingsTestingPart; +} + +// we need to load up the appropriate settings based on where we are running +let settings: ISettings = null; +let mode = "cmd"; +let site: string = null; +let skipWeb = false; + +for (let i = 0; i < process.argv.length; i++) { + const arg = process.argv[i]; + if (/^--pnp-test-mode/i.test(arg)) { + mode = process.argv[++i]; + } + if (/^--pnp-test-site/i.test(arg)) { + site = process.argv[++i]; + } + if (/^--skip-web/i.test(arg)) { + skipWeb = true; + } +} + +switch (mode) { + + case "travis": + + settings = { + testing: { + enableWebTests: true, + graph: { + id: "", + secret: "", + tenant: "", + }, + sp: { + id: process.env.PnPTesting_ClientId, + notificationUrl: process.env.PnPTesting_NotificationUrl || null, + secret: process.env.PnPTesting_ClientSecret, + url: process.env.PnPTesting_SiteUrl, + }, + }, + }; + + break; + case "travis-noweb": + + settings = { + testing: { + enableWebTests: false, + }, + }; + + break; + default: + + settings = require("../../../settings"); + if (skipWeb) { + settings.testing.enableWebTests = false; + } + + break; +} + +function spTestSetup(ts: ISettingsTestingPart): Promise { + + return new Promise((resolve, reject) => { + if (site && site.length > 0) { + // we have a site url provided, we'll use that + sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient(site, ts.sp.id, ts.sp.secret); + }, + }, + }); + ts.sp.webUrl = site; + return resolve(); + } + + sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient(ts.sp.url, ts.sp.id, ts.sp.secret); + }, + }, + }); + + // create the web in which we will test + const d = new Date(); + const g = getGUID(); + + sp.web.webs.add(`PnP-JS-Core Testing ${d.toDateString()}`, g).then(() => { + + const url = combine(ts.sp.url, g); + + // set the testing web url so our tests have access if needed + ts.sp.webUrl = url; + + // re-setup the node client to use the new web + sp.setup({ + + sp: { + // headers: { + // "Accept": "application/json;odata=verbose", + // }, + fetchClientFactory: () => { + return new SPFetchClient(url, ts.sp.id, ts.sp.secret); + }, + }, + }); + + resolve(); + + }).catch(e => reject(e)); + }); +} + +function graphTestSetup(ts: ISettingsTestingPart): Promise { + + return new Promise((resolve) => { + + graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient(ts.graph.tenant, ts.graph.id, ts.graph.secret); + }, + }, + }); + + resolve(); + }); +} + +export let testSettings: ISettingsTestingPart = extend(settings.testing, { webUrl: "" }); + +before(function (done: MochaDone) { + + // this may take some time, don't timeout early + this.timeout(90000); + + // establish the connection to sharepoint + if (testSettings.enableWebTests) { + + Promise.all([ + // un comment this to delete older subsites + // cleanUpAllSubsites(), + spTestSetup(testSettings), + graphTestSetup(testSettings), + ]).then(_ => done()).catch(e => { + + console.log("Error creating testing sub-site: " + JSON.stringify(e)); + done(e); + }); + } else { + done(); + } +}); + +after(() => { + + // could remove the sub web here? + // clean up other stuff? + // write some logging? +}); + +// this can be used to clean up lots of test sub webs :) +// function cleanUpAllSubsites(): Promise { +// return sp.site.rootWeb.webs.select("Title").get().then((w) => { +// w.forEach((element: any) => { +// const web = Web(element["odata.id"], ""); +// web.webs.select("Title").get().then((sw: any[]) => { +// return Promise.all(sw.map((value) => { +// const web2 = Web(value["odata.id"], ""); +// return web2.delete(); +// })); +// }).then(() => { web.delete(); }); +// }); +// }); +// } diff --git a/test/sp/web.ts b/test/sp/web.ts new file mode 100644 index 000000000..2d72f0fd2 --- /dev/null +++ b/test/sp/web.ts @@ -0,0 +1,314 @@ +import { combine, getRandomString } from "@pnp/common"; +import { expect } from "chai"; +import "@pnp/sp/src/webs"; +import "@pnp/sp/src/content-types/web"; +import "@pnp/sp/src/lists/web"; +import "@pnp/sp/src/navigation/web"; +import "@pnp/sp/src/site-users/web"; +import "@pnp/sp/src/site-groups/web"; +import "@pnp/sp/src/folders/web"; +import "@pnp/sp/src/files/web"; +import "@pnp/sp/src/user-custom-actions/web"; +import "@pnp/sp/src/security"; +import "@pnp/sp/src/appcatalog"; +import "@pnp/sp/src/related-items/web"; +import { sp } from "@pnp/sp"; +import { testSettings } from "../main"; +import { IInvokableTest } from "types"; + +describe("Webs", function () { + + if (testSettings.enableWebTests) { + + it(".add 1", function () { + + const title = `Test_ChildWebAdd1_${getRandomString(8)}`; + return expect(sp.web.webs.add(title, title)).to.eventually.be.fulfilled; + }); + + it(".add 2", function () { + + const title = `Test_ChildWebAdd2_${getRandomString(8)}`; + return expect(sp.web.webs.add(title, title, "description", "FunSite#0", 1033, false)).to.eventually.be.fulfilled; + }); + } +}); + +describe("Web", () => { + + if (testSettings.enableWebTests) { + + describe("Invokable Properties", () => { + + const tests: IInvokableTest[] = [ + { desc: ".webs", test: sp.web.webs }, + { desc: ".contentTypes", test: sp.web.contentTypes }, + { desc: ".lists", test: sp.web.lists }, + { desc: ".siteUserInfoList", test: sp.web.siteUserInfoList }, + { desc: ".defaultDocumentLibrary", test: sp.web.defaultDocumentLibrary }, + { desc: ".customListTemplates", test: sp.web.customListTemplates }, + { desc: ".navigation", test: sp.web.navigation }, + { desc: ".siteUsers", test: sp.web.siteUsers }, + { desc: ".siteGroups", test: sp.web.siteGroups }, + { desc: ".folders", test: sp.web.folders }, + { desc: ".userCustomActions", test: sp.web.userCustomActions }, + { desc: ".roleDefinitions", test: sp.web.roleDefinitions }, + { desc: ".customListTemplate", test: sp.web.customListTemplates }, + { desc: ".currentUser", test: sp.web.currentUser }, + { desc: ".allProperties", test: sp.web.allProperties }, + { desc: ".webinfos", test: sp.web.webinfos }, + { desc: ".features", test: sp.web.features }, + { desc: ".fields", test: sp.web.fields }, + { desc: ".availablefields", test: sp.web.availablefields }, + { desc: ".folders", test: sp.web.folders }, + { desc: ".rootFolder", test: sp.web.rootFolder }, + { desc: ".regionalSettings", test: sp.web.regionalSettings }, + { desc: ".associatedOwnerGroup", test: sp.web.associatedOwnerGroup }, + { desc: ".associatedMemberGroup", test: sp.web.associatedMemberGroup }, + { desc: ".associatedVisitorGroup", test: sp.web.associatedVisitorGroup }, + ]; + + tests.forEach((testObj) => { + + const { test, desc } = testObj; + it(desc, () => expect((test)()).to.eventually.be.fulfilled); + }); + }); + + it(".getParentWeb", async function () { + + const v = await sp.web.getParentWeb(); + return expect(v).to.haveOwnProperty("data"); + }); + + it(".getSubwebsFilteredForCurrentUser", async function () { + + return expect(sp.web.getSubwebsFilteredForCurrentUser()()).to.eventually.be.fulfilled; + }); + + it(".update", function () { + + const p = sp.web.select("Title").get<{ Title: string }>().then(function (w) { + + const newTitle = w.Title + " updated"; + sp.web.update({ Title: newTitle }).then(function () { + + sp.web.select("Title").get<{ Title: string }>().then(function (w2) { + if (w2.Title !== newTitle) { + throw Error("Update web failed"); + } + }); + }); + }); + + return expect(p).to.eventually.be.fulfilled; + }); + + // commenting out this test as the code hasn't changed in years and it takes longer than any other test + // it(".applyTheme", function () { + + // // this takes a long time to process + // this.timeout(60000); + + // const index = testSettings.sp.url.indexOf("/sites/"); + // const colorUrl = "/" + combine(testSettings.sp.url.substr(index), "/_catalogs/theme/15/palette011.spcolor"); + // const fontUrl = "/" + combine(testSettings.sp.url.substr(index), "/_catalogs/theme/15/fontscheme007.spfont"); + + // return expect(sp.web.applyTheme(colorUrl, fontUrl, "", false)).to.eventually.be.fulfilled; + // }); + + it(".applyWebTemplate", async function () { + + this.timeout(60000); + + const { web } = await sp.web.webs.add("ApplyWebTemplateTest", getRandomString(6), "Testing", "STS"); + const templates = (await web.availableWebTemplates().select("Name")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name)); + + const template = templates.length > 0 ? templates[0].Name : "STS#0"; + + // this will be rejected because a template was already applied and we can't + // through REST create a site with no template + return expect(web.applyWebTemplate(template)).to.eventually.be.rejected; + }); + + it(".availableWebTemplates", function () { + + return expect(sp.web.availableWebTemplates()()).to.eventually.be.an.instanceOf(Array).and.be.not.empty; + }); + + it(".getChanges", function () { + + return expect(sp.web.getChanges({ + Add: true, + })).to.eventually.be.fulfilled; + }); + + it(".mapToIcon", function () { + + return expect(sp.web.mapToIcon("test.docx")).to.eventually.be.fulfilled; + }); + + it(".delete", async function () { + + this.timeout(60000); + const url = getRandomString(4); + const result = await sp.web.webs.add("Better be deleted!", url); + return expect(result.web.delete()).to.eventually.be.fulfilled; + }); + + it("storage entity", async function () { + + const key = `testingkey_${getRandomString(4)}`; + const value = "Test Value"; + + const web = await sp.getTenantAppCatalogWeb(); + + after(async () => { + await web.removeStorageEntity(key); + }); + + await web.setStorageEntity(key, value); + const v = await web.getStorageEntity(key); + return expect(v.Value).to.equal(value); + }); + + it("storage entity with '", async function () { + + const key = `testingkey'${getRandomString(4)}`; + const value = "Test Value"; + + const web = await sp.getTenantAppCatalogWeb(); + + after(async () => { + await web.removeStorageEntity(key); + }); + + await web.setStorageEntity(key, value); + const v = await web.getStorageEntity(key); + return expect(v.Value).to.equal(value); + }); + + describe("appcatalog", () => { + + it(".getAppCatalog", async function () { + + const appCatWeb = await sp.getTenantAppCatalogWeb(); + const p = appCatWeb.getAppCatalog()(); + return expect(p).to.eventually.be.fulfilled; + }); + }); + + describe("client-side-pages", () => { + + it(".getClientSideWebParts", async function () { + + return expect(sp.web.getClientSideWebParts()).to.eventually.be.fulfilled; + }); + + it(".addClientSidePage"); + }); + + describe("files", () => { + + let path = ""; + + before(async () => { + + const w = await sp.web.select("ServerRelativeUrl")<{ ServerRelativeUrl: string }>(); + path = combine("/", w.ServerRelativeUrl, "SitePages", "Home.aspx"); + }); + + it(".getFileByServerRelativeUrl", async function () { + + return expect(sp.web.getFileByServerRelativeUrl(path)()).to.eventually.be.fulfilled; + }); + + it(".getFileByServerRelativePath", async function () { + + return expect(sp.web.getFileByServerRelativePath(path)()).to.eventually.be.fulfilled; + }); + }); + + describe("folders", () => { + + let path = ""; + + before(async () => { + + const w = await sp.web.select("ServerRelativeUrl")<{ ServerRelativeUrl: string }>(); + path = combine("/", w.ServerRelativeUrl, "SitePages"); + }); + + it(".getFolderByServerRelativeUrl", async function () { + + return expect(sp.web.getFolderByServerRelativeUrl(path)()).to.eventually.be.fulfilled; + }); + + it(".getFolderByServerRelativePath", async function () { + + return expect(sp.web.getFolderByServerRelativePath(path)()).to.eventually.be.fulfilled; + }); + }); + + describe("hub-sites", () => { + + it(".hubSiteData", async function () { + + return expect(sp.web.hubSiteData()).to.eventually.be.fulfilled; + }); + + it(".hubSiteData force refresh", async function () { + + return expect(sp.web.hubSiteData(true)).to.eventually.be.fulfilled; + }); + + it(".syncHubSiteTheme", async function () { + + return expect(sp.web.syncHubSiteTheme()).to.eventually.be.fulfilled; + }); + }); + + describe("lists", () => { + + it(".getList", async function () { + + const w = await sp.web.select("ServerRelativeUrl")<{ ServerRelativeUrl: string }>(); + const url = combine(w.ServerRelativeUrl, "SitePages"); + return expect(sp.web.getList(url)()).to.eventually.be.fulfilled; + }); + + it(".getCatalog", function () { + + return expect(sp.site.rootWeb.getCatalog(113)).to.eventually.be.fulfilled; + }); + }); + + describe("related-items", () => { + + it(".relatedItems", function () { + + return expect(sp.web.relatedItems).to.not.be.null; + }); + }); + + describe("site-groups", () => { + + it(".createDefaultAssociatedGroups", async function () { + + const users = await sp.web.siteUsers.select("LoginName").top(2)(); + return expect(sp.web.createDefaultAssociatedGroups("Testing", users[0].LoginName)).to.eventually.be.fulfilled; + }); + }); + + describe("site-users", () => { + + it(".ensureUser"); + + it(".getUserById", async function () { + + const users = await sp.web.siteUsers(); + return expect(sp.web.getUserById(users[0].Id)()).to.eventually.be.fulfilled; + }); + }); + } +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..0fcb012c7 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "commonjs", + "baseUrl": ".", + "rootDir": "../", + "outDir": "../build/testing", + "declaration": false, + "downlevelIteration": true, + "types": [ + "chai", + "chai-as-promised", + "node", + "mocha", + "sharepoint" + ], + "lib": [ + "es2015", + "dom" + ], + "paths": { + "@pnp/*": [ + "../packages/*" + ] + }, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "sourceMap": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "removeComments": true, + "importHelpers": true + }, + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 000000000..324366a01 --- /dev/null +++ b/test/types.ts @@ -0,0 +1,4 @@ +export interface IInvokableTest { + desc: string; + test: (...args: any[]) => Promise; +} diff --git a/tools/buildsystem/index.ts b/tools/buildsystem/index.ts new file mode 100644 index 000000000..f77a3c800 --- /dev/null +++ b/tools/buildsystem/index.ts @@ -0,0 +1 @@ +export * from "./src/buildsystem"; diff --git a/tools/buildsystem/package.json b/tools/buildsystem/package.json new file mode 100644 index 000000000..4423ee8ea --- /dev/null +++ b/tools/buildsystem/package.json @@ -0,0 +1,25 @@ +{ + "name": "@pnp/buildsystem", + "private": true, + "version": "1.0.0", + "description": "pnp - the build system used within the @pnp repo", + "main": "./index.js", + "typings": "./index", + "dependencies": {}, + "peerDependencies": {}, + "engines": { + "node": "^8.9.4" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} diff --git a/tools/buildsystem/readme.md b/tools/buildsystem/readme.md new file mode 100644 index 000000000..5eddaef39 --- /dev/null +++ b/tools/buildsystem/readme.md @@ -0,0 +1,23 @@ +# Build system + +This project contains the system used to build, package, and publish the npm packages created for each package in the ./packages folder. + +## Builder + +Responsible for building the code (transpiling the TS into JS), controlled by pnp-build.js configuration file + +`gulp build` + +## Packager + +Handles creating the actual package directories as they will be published to NPM. Controlled by the pnp-package.js configuration file. + +`gulp package` + +## Publisher + +Publishes the packages to NPM + +`gulp publish` + +`gulp publish-beta` diff --git a/tools/buildsystem/src/builder.ts b/tools/buildsystem/src/builder.ts new file mode 100644 index 000000000..544ecc5aa --- /dev/null +++ b/tools/buildsystem/src/builder.ts @@ -0,0 +1,57 @@ +declare var require: (s: string) => any; +const path = require("path"), + colors = require("ansi-colors"), + log = require("fancy-log"); + +import { BuildSchema, BuildTask } from "./tasks/build/schema"; +import { build } from "./tasks/build/build"; + +/** + * Engine function to process build files + * + * @param version The version to be written into all the build packages + * @param config The build configuration object + * @param callback (err?) => void + */ +export async function builder(version: string, config: BuildSchema): Promise { + + try { + + // run any pre-build tasks + await runTasks("pre-build", config.preBuildTasks || [], version, config); + + log(`${colors.bgBlue(" ")} Processing build targets.`); + // run build targets + await build(version, config); + log(`${colors.bgGreen(" ")} Processed build targets.`); + + // run any post-build tasks + await runTasks("post-build", config.postBuildTasks || [], version, config); + + } catch (e) { + + log(`${colors.bgRed(" ")} ${colors.bold(colors.red(`Build error`))}.`); + log(`${colors.bgRed(" ")} ${colors.bold(colors.red("Error:"))} ${colors.bold(colors.white(typeof e === "string" ? e : JSON.stringify(e)))}`); + throw e; + } +} + +async function runTasks(name: string, tasks: BuildTask[], version: string, config: BuildSchema): Promise { + + log(`${colors.bgBlue(" ")} Beginning (${tasks.length}) ${name} tasks.`); + for (let i = 0; i < tasks.length; i++) { + + const task = tasks[i]; + + if (typeof task === "undefined" || task === null) { + continue; + } + + if (typeof task === "function") { + await task(version, config); + } else { + await task.task(version, config, task.packages); + } + } + log(`${colors.bgGreen(" ")} Finished ${name} tasks.`); +} diff --git a/tools/buildsystem/src/buildsystem.ts b/tools/buildsystem/src/buildsystem.ts new file mode 100644 index 000000000..5691303ec --- /dev/null +++ b/tools/buildsystem/src/buildsystem.ts @@ -0,0 +1,27 @@ +export * from "./builder"; +export * from "./packager"; +export * from "./publisher"; +import * as _Tasks from "./tasks"; +export const Tasks = _Tasks; + +// we need to hoist these so they are exported as interfaces at the top level +export { + BuildSchema, + BuildFunction, + BuildTask, + BuildTaskScoped, +} from "./tasks/build"; + +export { + PackageFunction, + PackageTask, + PackageSchema, + PackageTaskScoped, +} from "./tasks/package"; + +export { + PublishFunction, + PublishSchema, + PublishTask, + PublishTaskScoped, +} from "./tasks/publish"; diff --git a/tools/buildsystem/src/lib/getSubDirectoryNames.ts b/tools/buildsystem/src/lib/getSubDirectoryNames.ts new file mode 100644 index 000000000..70213d2cb --- /dev/null +++ b/tools/buildsystem/src/lib/getSubDirectoryNames.ts @@ -0,0 +1,7 @@ +// after: https://stackoverflow.com/questions/18112204/get-all-directories-within-directory-nodejs +const { lstatSync, readdirSync } = require("fs"); +const { join } = require("path"); + +const isDirectory = (root, dirName) => lstatSync(join(root, dirName)).isDirectory(); + +export default (root) => readdirSync(root).filter(dirName => isDirectory(root, dirName)); \ No newline at end of file diff --git a/tools/buildsystem/src/packager.ts b/tools/buildsystem/src/packager.ts new file mode 100644 index 000000000..21cb1005e --- /dev/null +++ b/tools/buildsystem/src/packager.ts @@ -0,0 +1,52 @@ +declare var require: (s: string) => any; +const colors = require("ansi-colors"); +const log = require("fancy-log"); + +import { PackageSchema, PackageTask } from "./tasks/package/schema"; + +/** + * Engine function to process build files + * + * @param version The version to be written into all the build packages + * @param config The build configuration object + * @param callback (err?) => void + */ +export async function packager(version: string, config: PackageSchema): Promise { + + try { + + // run any pre-package tasks + await runTasks("pre-package", version, config.prePackageTasks || [], config); + + // run any package tasks + await runTasks("package", version, config.packageTasks || [], config); + + // run any post-package tasks + await runTasks("post-package", version, config.postPackageTasks || [], config); + + } catch (e) { + + log(`${colors.bgRed(" ")} ${colors.bold(colors.red(`Packaging error`))}.`); + log(`${colors.bgRed(" ")} ${colors.bold(colors.red("Error:"))} ${colors.bold(colors.white(typeof e === "string" ? e : JSON.stringify(e)))}`); + throw e; + } +} + +async function runTasks(name: string, version: string, tasks: PackageTask[], config: PackageSchema): Promise { + + log(`${colors.bgBlue(" ")} Beginning (${tasks.length}) ${name} tasks.`); + for (let i = 0; i < tasks.length; i++) { + + const task = tasks[i]; + if (typeof task === "undefined" || task === null) { + continue; + } + + if (typeof task === "function") { + await task(version, config); + } else { + await task.task(version, config, task.packages); + } + } + log(`${colors.bgGreen(" ")} Finished ${name} tasks.`); +} diff --git a/tools/buildsystem/src/publisher.ts b/tools/buildsystem/src/publisher.ts new file mode 100644 index 000000000..0c6de776f --- /dev/null +++ b/tools/buildsystem/src/publisher.ts @@ -0,0 +1,53 @@ +declare var require: (s: string) => any; +const colors = require("ansi-colors"); +const log = require("fancy-log"); + +import { PublishSchema, PublishTask } from "./tasks/publish/schema"; + +/** + * Engine function to process publish files + * + * @param version The version to be written into all the published packages + * @param config The build configuration object + * @param callback (err?) => void + */ +export async function publisher(version: string, config: PublishSchema): Promise { + + + try { + + // run any pre-publish tasks + await runTasks("pre-publish", version, config.prePublishTasks || [], config); + + // run any publish tasks + await runTasks("publish", version, config.publishTasks || [], config); + + // run any post-publish tasks + await runTasks("post-publish", version, config.postPublishTasks || [], config); + + } catch (e) { + + log(`${colors.bgRed(" ")} ${colors.bold(colors.red(`Publishing error`))}.`); + log(`${colors.bgRed(" ")} ${colors.bold(colors.red("Error:"))} ${colors.bold(colors.white(typeof e === "string" ? e : JSON.stringify(e)))}`); + throw e; + } +} + +async function runTasks(name: string, version: string, tasks: PublishTask[], config: PublishSchema): Promise { + + log(`${colors.bgBlue(" ")} Beginning (${tasks.length}) ${name} tasks.`); + for (let i = 0; i < tasks.length; i++) { + + const task = tasks[i]; + if (typeof task === "undefined" || task === null) { + continue; + } + + if (typeof task === "function") { + await task(version, config); + } else { + await task.task(version, config, task.packages); + } + } + log(`${colors.bgGreen(" ")} Finished ${name} tasks.`); +} diff --git a/tools/buildsystem/src/tasks/build/build.ts b/tools/buildsystem/src/tasks/build/build.ts new file mode 100644 index 000000000..50fec7dc4 --- /dev/null +++ b/tools/buildsystem/src/tasks/build/build.ts @@ -0,0 +1,32 @@ +declare var require: (s: string) => any; +const path = require("path"); +const log = require("fancy-log"); + +import { exec } from "child_process"; +import { BuildSchema } from "./schema"; + +const tscPath = path.resolve("./node_modules/.bin/tsc"); + +/** + * Builds the project based on the supplied tsconfig.json file + * + * @param ctx The build context + */ +export function build(_0: string, config: BuildSchema) { + + // for each build target we need to invoke tsc + + return Promise.all(config.buildTargets.map(buildTarget => new Promise((resolve, reject) => { + // exec a child process to run a tsc build based on the project file in each + // package directory. Build is now fully managed via tsconfig.json files + exec(`${tscPath} -b ${buildTarget}`, (error, stdout, stderr) => { + + if (error === null) { + resolve(); + } else { + console.error(error); + reject(stdout); + } + }); + }))); +} diff --git a/tools/buildsystem/src/tasks/build/index.ts b/tools/buildsystem/src/tasks/build/index.ts new file mode 100644 index 000000000..e9d3f2464 --- /dev/null +++ b/tools/buildsystem/src/tasks/build/index.ts @@ -0,0 +1,4 @@ +export * from "./build"; +export * from "./replace-debug"; +export * from "./replace-sp-http-version"; +export * from "./schema"; diff --git a/tools/buildsystem/src/tasks/build/replace-debug.ts b/tools/buildsystem/src/tasks/build/replace-debug.ts new file mode 100644 index 000000000..c64284ef4 --- /dev/null +++ b/tools/buildsystem/src/tasks/build/replace-debug.ts @@ -0,0 +1,57 @@ +declare var require: (s: string) => any; +import { BuildSchema } from "./schema"; +const path = require("path"); +import * as replace from "replace-in-file"; + +interface TSConfig { + compilerOptions: { + outDir: string; + }; +} + +/** + * Repalces the $$Version$$ and rewrites the local require statements for debugging + * + * @param ctx The build context + */ +export function replaceDebug(version: string, config: BuildSchema) { + + const optionsVersion = { + files: [], + from: /\$\$Version\$\$/ig, + to: version, + }; + + const optionsRequireTemplate = { + from: /require\(['|"]@pnp\/[\w-\/]*?['|"]/ig, + }; + + const requireOptionsCollection = []; + + for (let i = 0; i < config.buildTargets.length; i++) { + + // read our outDir from the build target (which will be a tsconfig file) + const buildConfig: TSConfig = require(config.buildTargets[i]); + const sourceRoot = path.resolve(path.dirname(config.buildTargets[i])); + const outDir = buildConfig.compilerOptions.outDir; + + optionsVersion.files.push(path.resolve(sourceRoot, outDir, "sp/src/net/sphttpclient.js")); + optionsVersion.files.push(path.resolve(sourceRoot, outDir, "sp/src/batch.js")); + + requireOptionsCollection.push(Object.assign({}, optionsRequireTemplate, { + files: [ + path.resolve(sourceRoot, outDir, "**/*.js"), + path.resolve(sourceRoot, outDir, "**/*.d.ts"), + ], + to: (match) => { + const m = /require\(['|"]@pnp\/([\w-\/]*?)['|"]/ig.exec(match); + return `require("${path.resolve(sourceRoot, outDir, `packages/${m[1]}`).replace(/\\/g, "/")}"`; + }, + })); + } + + return Promise.all([ + replace(optionsVersion), + ...requireOptionsCollection.map(c => replace(c)), + ]).catch(e => console.error); +} diff --git a/tools/buildsystem/src/tasks/build/replace-sp-http-version.ts b/tools/buildsystem/src/tasks/build/replace-sp-http-version.ts new file mode 100644 index 000000000..e5ec87f4a --- /dev/null +++ b/tools/buildsystem/src/tasks/build/replace-sp-http-version.ts @@ -0,0 +1,37 @@ +declare var require: (s: string) => any; +const path = require("path"); +import { BuildSchema } from "./schema"; +import * as replace from "replace-in-file"; + +interface TSConfig { + compilerOptions: { + outDir: string; + }; +} + +/** + * Replaces the $$Version$$ string in the SharePoint HttpClient + * + * @param version The version number + * @param ctx The build context + */ +export function replaceSPHttpVersion(version: string, config: BuildSchema) { + + const options = { + files: [], + from: /\$\$Version\$\$/ig, + to: version, + }; + + for (let i = 0; i < config.buildTargets.length; i++) { + + // read our outDir from the build target (which will be a tsconfig file) + const buildConfig: TSConfig = require(config.buildTargets[i]); + const buildRoot = path.resolve(path.dirname(config.buildTargets[i])); + + options.files.push(path.resolve(buildRoot, buildConfig.compilerOptions.outDir, "sp/src/net/sphttpclient.js")); + options.files.push(path.resolve(buildRoot, buildConfig.compilerOptions.outDir, "sp/src/batch.js")); + } + + return replace(options); +} diff --git a/tools/buildsystem/src/tasks/build/schema.ts b/tools/buildsystem/src/tasks/build/schema.ts new file mode 100644 index 000000000..aae5631ee --- /dev/null +++ b/tools/buildsystem/src/tasks/build/schema.ts @@ -0,0 +1,19 @@ +export type BuildFunction = (version: string, config: BuildSchema, packages?: string[]) => Promise; + +export interface BuildTaskScoped { + packages: string[]; + task: BuildFunction; +} + +export type BuildTask = BuildFunction | BuildTaskScoped; + +export interface BuildSchema { + + packageRoot: string; + + preBuildTasks: BuildTask[]; + + buildTargets: string[]; + + postBuildTasks: BuildTask[]; +} diff --git a/tools/buildsystem/src/tasks/index.ts b/tools/buildsystem/src/tasks/index.ts new file mode 100644 index 000000000..ee7b839f5 --- /dev/null +++ b/tools/buildsystem/src/tasks/index.ts @@ -0,0 +1,7 @@ +import * as _Build from "./build/index"; +import * as _Package from "./package/index"; +import * as _Publish from "./publish/index"; + +export let Build = _Build; +export let Package = _Package; +export let Publish = _Publish; diff --git a/tools/buildsystem/src/tasks/package/copy-defs.ts b/tools/buildsystem/src/tasks/package/copy-defs.ts new file mode 100644 index 000000000..d5f576358 --- /dev/null +++ b/tools/buildsystem/src/tasks/package/copy-defs.ts @@ -0,0 +1,48 @@ +declare var require: (s: string) => any; +const pump = require("pump"); +import { src, dest } from "gulp"; +import { PackageSchema } from "./schema"; +const path = require("path"); + +interface TSConfig { + compilerOptions: { + outDir: string; + }; +} + +export function copyDefs(version: string, config: PackageSchema) { + + const promises: Promise[] = []; + + for (let i = 0; i < config.packageTargets.length; i++) { + + const packageTarget = config.packageTargets[i]; + + // read the outdir from the packagetarget + const buildConfig: TSConfig = require(packageTarget.packageTarget); + const sourceRoot = path.resolve(path.dirname(packageTarget.packageTarget)); + const buildOutDir = path.resolve(sourceRoot, buildConfig.compilerOptions.outDir); + + promises.push(new Promise((resolve, reject) => { + + pump([ + src(["./**/*.d.ts"], { + cwd: buildOutDir, + }), + dest(path.resolve(packageTarget.outDir), { + overwrite: true, + }), + ], (err: (Error | null)) => { + + if (err !== undefined) { + console.error(err); + reject(err); + } else { + resolve(); + } + }); + })); + } + + return Promise.all(promises); +} diff --git a/tools/buildsystem/src/tasks/package/copy-docs.ts b/tools/buildsystem/src/tasks/package/copy-docs.ts new file mode 100644 index 000000000..b6c98d525 --- /dev/null +++ b/tools/buildsystem/src/tasks/package/copy-docs.ts @@ -0,0 +1,39 @@ +declare var require: (s: string) => any; +const pump = require("pump"); +import { src, dest } from "gulp"; +import { PackageSchema } from "./schema"; +const path = require("path"); + +export function copyDocs(version: string, config: PackageSchema) { + + const promises: Promise[] = []; + + for (let i = 0; i < config.packageTargets.length; i++) { + + const packageTarget = config.packageTargets[i]; + + const sourceRoot = path.resolve(path.dirname(packageTarget.packageTarget)); + + promises.push(new Promise((resolve, reject) => { + + pump([ + src(["./**/*.md"], { + cwd: sourceRoot, + }), + dest(path.resolve(packageTarget.outDir), { + overwrite: true, + }), + ], (err: (Error | null)) => { + + if (err !== undefined) { + console.error(err); + reject(err); + } else { + resolve(); + } + }); + })); + } + + return Promise.all(promises); +} diff --git a/tools/buildsystem/src/tasks/package/copy-static-assets.ts b/tools/buildsystem/src/tasks/package/copy-static-assets.ts new file mode 100644 index 000000000..76199ba12 --- /dev/null +++ b/tools/buildsystem/src/tasks/package/copy-static-assets.ts @@ -0,0 +1,40 @@ +declare var require: (s: string) => any; +import { PackageSchema } from "./schema"; +import getSubDirNames from "../../lib/getSubDirectoryNames"; +const path = require("path"), + fs = require("fs"); + +interface TSConfig { + compilerOptions: { + outDir: string; + }; +} + +export function copyStaticAssets(version: string, config: PackageSchema) { + + const projectRoot = path.resolve(__dirname, "../../../../../.."); + + const licensePath = path.resolve(projectRoot, "LICENSE"); + const readmePath = path.resolve(projectRoot, "./packages/readme.md"); + + for (let i = 0; i < config.packageTargets.length; i++) { + + const packageTarget = config.packageTargets[i]; + + const buildConfig: TSConfig = require(packageTarget.packageTarget); + const sourceRoot = path.resolve(path.dirname(packageTarget.packageTarget)); + const buildOutDir = path.resolve(sourceRoot, buildConfig.compilerOptions.outDir); + + // get the sub directories from the output, these will match the folder structure\ + // in the .ts source directory + const builtFolders = getSubDirNames(buildOutDir); + + for (let j = 0; j < builtFolders.length; j++) { + const dest = path.resolve(packageTarget.outDir, builtFolders[j]); + fs.createReadStream(licensePath).pipe(fs.createWriteStream(path.join(dest, "LICENSE"))); + fs.createReadStream(readmePath).pipe(fs.createWriteStream(path.join(dest, "readme.md"))); + } + } + + return Promise.resolve(); +} diff --git a/tools/buildsystem/src/tasks/package/index.ts b/tools/buildsystem/src/tasks/package/index.ts new file mode 100644 index 000000000..a62e0193d --- /dev/null +++ b/tools/buildsystem/src/tasks/package/index.ts @@ -0,0 +1,7 @@ +export * from "./copy-defs"; +export * from "./copy-docs"; +export * from "./copy-static-assets"; +export * from "./rollup"; +export * from "./schema"; +export * from "./webpack"; +export * from "./write-package-files"; diff --git a/tools/buildsystem/src/tasks/package/rollup.ts b/tools/buildsystem/src/tasks/package/rollup.ts new file mode 100644 index 000000000..619648f89 --- /dev/null +++ b/tools/buildsystem/src/tasks/package/rollup.ts @@ -0,0 +1,23 @@ +declare var require: (s: string) => any; +const path = require("path"); + +import { exec } from "child_process"; + +const rollupPath = path.resolve("./node_modules/.bin/rollup"); + +export function rollup() { + + return new Promise((resolve, reject) => { + + // exec webpack in the root of the project, the webpack.config.js file handles all configuration + exec(`${rollupPath} -c`, (error, stdout) => { + + if (error === null) { + resolve(); + } else { + console.error(error); + reject(stdout); + } + }); + }); +} diff --git a/tools/buildsystem/src/tasks/package/schema.ts b/tools/buildsystem/src/tasks/package/schema.ts new file mode 100644 index 000000000..16881c21e --- /dev/null +++ b/tools/buildsystem/src/tasks/package/schema.ts @@ -0,0 +1,24 @@ +export type PackageFunction = (version: string, config: PackageSchema, packages?: string[]) => Promise; + +export interface PackageTaskScoped { + packages: string[]; + task: PackageFunction; +} + +export interface PackageTargetMap { + packageTarget: string; + outDir: string; +} + +export type PackageTask = PackageFunction | PackageTaskScoped; + +export interface PackageSchema { + + packageTargets: PackageTargetMap[]; + + prePackageTasks: PackageTask[]; + + packageTasks: PackageTask[]; + + postPackageTasks: PackageTask[]; +} diff --git a/tools/buildsystem/src/tasks/package/webpack.ts b/tools/buildsystem/src/tasks/package/webpack.ts new file mode 100644 index 000000000..3bb3801f1 --- /dev/null +++ b/tools/buildsystem/src/tasks/package/webpack.ts @@ -0,0 +1,23 @@ +declare var require: (s: string) => any; +const path = require("path"); + +import { exec } from "child_process"; + +const webpackPath = path.resolve("./node_modules/.bin/webpack"); + +export function webpack() { + + return new Promise((resolve, reject) => { + + // exec webpack in the root of the project, the webpack.config.js file handles all configuration + exec(`${webpackPath}`, (error, stdout) => { + + if (error === null) { + resolve(); + } else { + console.error(error); + reject(stdout); + } + }); + }); +} diff --git a/tools/buildsystem/src/tasks/package/write-package-files.ts b/tools/buildsystem/src/tasks/package/write-package-files.ts new file mode 100644 index 000000000..8830d6a52 --- /dev/null +++ b/tools/buildsystem/src/tasks/package/write-package-files.ts @@ -0,0 +1,78 @@ +declare var require: (s: string) => any; +const fs = require("fs"), + path = require("path"); + +// import { src, dest } from "gulp"; +// const pump = require("pump"); + +import { PackageSchema } from "./schema"; +import getSubDirNames from "../../lib/getSubDirectoryNames"; + +interface TSConfig { + compilerOptions: { + outDir: string; + }; +} + +/** + * Writes the package.json for the dist package. This should be last in the pipeline as that allows previous tasks + * to update the pkgObj as needed before it is written to the fs here. This task does handle the statndard rewrites + * + * @param ctx The build context + */ +export function writePackageFiles(version: string, config: PackageSchema) { + + const promises: Promise[] = []; + + for (let i = 0; i < config.packageTargets.length; i++) { + + const packageTarget = config.packageTargets[i]; + + // read the outdir from the packagetarget + const buildConfig: TSConfig = require(packageTarget.packageTarget); + const sourceRoot = path.resolve(path.dirname(packageTarget.packageTarget)); + const buildOutDir = path.resolve(sourceRoot, buildConfig.compilerOptions.outDir); + + // get the sub directories from the output, these will match the folder structure\ + // in the .ts source directory + const builtFolders = getSubDirNames(buildOutDir); + + for (let j = 0; j < builtFolders.length; j++) { + + // read the package.json from the root of the original source + const pkg = require(path.resolve(sourceRoot, builtFolders[j], "package.json")); + + pkg.version = version; + pkg.main = `./dist/${builtFolders[j]}.es5.umd.js`; + pkg.module = `./dist/${builtFolders[j]}.es5.js`; + pkg.es2015 = `./dist/${builtFolders[j]}.js`; + + // update our peer dependencies and dependencies placeholder if needed + for (const key in pkg.peerDependencies) { + if (pkg.peerDependencies[key] === "0.0.0-PLACEHOLDER") { + pkg.peerDependencies[key] = version; + } + } + + for (const key in pkg.dependencies) { + if (pkg.dependencies[key] === "0.0.0-PLACEHOLDER") { + pkg.dependencies[key] = version; + } + } + + promises.push(new Promise((resolve, reject) => { + fs.writeFile(path.resolve(packageTarget.outDir, builtFolders[j], "package.json"), JSON.stringify(pkg, null, 4), (err) => { + + if (err) { + console.error(err); + reject(err); + } else { + resolve(); + } + }); + })); + } + } + + return Promise.all(promises); +} diff --git a/tools/buildsystem/src/tasks/publish/index.ts b/tools/buildsystem/src/tasks/publish/index.ts new file mode 100644 index 000000000..a0a8f2389 --- /dev/null +++ b/tools/buildsystem/src/tasks/publish/index.ts @@ -0,0 +1,3 @@ +export * from "./publish-package"; +export * from "./publish-beta-package"; +export * from "./schema"; diff --git a/tools/buildsystem/src/tasks/publish/publish-beta-package.ts b/tools/buildsystem/src/tasks/publish/publish-beta-package.ts new file mode 100644 index 000000000..0ad39d0de --- /dev/null +++ b/tools/buildsystem/src/tasks/publish/publish-beta-package.ts @@ -0,0 +1,45 @@ +import { exec } from "child_process"; +import { PublishSchema } from "./schema"; +const colors = require("ansi-colors"); +import * as path from "path"; +import getSubDirNames from "../../lib/getSubDirectoryNames"; +const log = require("fancy-log"); + +/** + * Minifies the files created in es5 format into the target dist folder + * + * @param ctx The build context + */ +export function publishBetaPackage(version: string, config: PublishSchema): Promise { + + const promises: Promise[] = []; + + const publishRoot = path.resolve(config.packageRoot); + const packageFolders = getSubDirNames(publishRoot).filter(name => name !== "documentation"); + + for (let i = 0; i < packageFolders.length; i++) { + + promises.push(new Promise((resolve, reject) => { + + const packagePath = path.resolve(publishRoot, packageFolders[i]); + + log(`${colors.bgBlue(" ")} Publishing BETA ${packagePath}`); + + exec("npm publish --tag beta --access public", + { + cwd: path.resolve(publishRoot, packageFolders[i]), + }, (error, stdout, stderr) => { + + if (error === null) { + log(`${colors.bgGreen(" ")} Published BETA ${packagePath}`); + resolve(); + } else { + console.error(error); + reject(stdout); + } + }); + })); + } + + return Promise.all(promises); +} diff --git a/tools/buildsystem/src/tasks/publish/publish-package.ts b/tools/buildsystem/src/tasks/publish/publish-package.ts new file mode 100644 index 000000000..71c5cf1fb --- /dev/null +++ b/tools/buildsystem/src/tasks/publish/publish-package.ts @@ -0,0 +1,45 @@ +import { exec } from "child_process"; +import { PublishSchema } from "./schema"; +const colors = require("ansi-colors"); +import * as path from "path"; +import getSubDirNames from "../../lib/getSubDirectoryNames"; +const log = require("fancy-log"); + +/** + * Minifies the files created in es5 format into the target dist folder + * + * @param ctx The build context + */ +export function publishPackage(version: string, config: PublishSchema): Promise { + + const promises: Promise[] = []; + + const publishRoot = path.resolve(config.packageRoot); + const packageFolders = getSubDirNames(publishRoot).filter(name => name !== "documentation"); + + for (let i = 0; i < packageFolders.length; i++) { + + promises.push(new Promise((resolve, reject) => { + + const packagePath = path.resolve(publishRoot, packageFolders[i]); + + log(`${colors.bgBlue(" ")} Publishing ${packagePath}`); + + exec("npm publish --access public", + { + cwd: path.resolve(publishRoot, packageFolders[i]), + }, (error, stdout, stderr) => { + + if (error === null) { + log(`${colors.bgGreen(" ")} Published ${packagePath}`); + resolve(); + } else { + console.error(error); + reject(error); + } + }); + })); + } + + return Promise.all(promises); +} diff --git a/tools/buildsystem/src/tasks/publish/schema.ts b/tools/buildsystem/src/tasks/publish/schema.ts new file mode 100644 index 000000000..10fc1296a --- /dev/null +++ b/tools/buildsystem/src/tasks/publish/schema.ts @@ -0,0 +1,19 @@ +export type PublishFunction = (version: string, config: PublishSchema, packages?: string[]) => Promise; + +export interface PublishTaskScoped { + packages: string[]; + task: PublishFunction; +} + +export type PublishTask = PublishFunction | PublishTaskScoped; + +export interface PublishSchema { + + packageRoot: string; + + prePublishTasks: PublishTask[]; + + publishTasks: PublishTask[]; + + postPublishTasks: PublishTask[]; +} diff --git a/tools/buildsystem/tsconfig.json b/tools/buildsystem/tsconfig.json new file mode 100644 index 000000000..65216ea00 --- /dev/null +++ b/tools/buildsystem/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "baseUrl": ".", + "rootDir": ".", + "outDir": "../../build/tools/buildsystem", + "declaration": true, + "declarationMap": true, + "types": [ + "node" + ], + "lib": [ + "es6", + "dom" + ], + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + }, + "include": [ + "./index.ts", + "../node-utils/getSubDirectorieNames.js", + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/tools/dev-server/CHANGELOG.md b/tools/dev-server/CHANGELOG.md new file mode 100644 index 000000000..28ffdfbec --- /dev/null +++ b/tools/dev-server/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +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). + +## 0.0.2 + +- Updated dependency versions to latest \ No newline at end of file diff --git a/tools/dev-server/index.js b/tools/dev-server/index.js new file mode 100644 index 000000000..2d4585f08 --- /dev/null +++ b/tools/dev-server/index.js @@ -0,0 +1,72 @@ +const + connect = require("connect"), + serveStatic = require("serve-static"), + reload = require("tiny-lr"), + opn = require("opn"), + connectReload = require("connect-livereload"), + watch = require("watch"), + url = require("url"); + +module.exports = (opts) => { + + return new Promise((resolve, reject) => { + + const options = Object.assign({ + root: "./", + path: "/", + port: 8888, + livereload: true, + open: true, + debug: false, + logFunc: (m) => console.log(m), + }, opts); + + const serveUrl = url.resolve(`http://localhost:${options.port}/`, options.path).toString(); + + if (options.debug) { + options.logFunc(`@pnp/dev-server:: options: ${JSON.stringify(options, null, 4)}`); + options.logFunc(`@pnp/dev-server:: serveUrl: ${serveUrl}`); + } + + const _server = connect(); + + if (options.livereload) { + + const lrServer = reload(); + + lrServer.listen(); + + // watch docs folder to trigger reload once docs are rebuilt by watch:docs + watch.watchTree(options.root, (filename) => { + + if (options.debug) { + logFunc(`@pnp/dev-server:: watch triggered, filename: ${filename}`); + } + + lrServer.changed({ + body: { + files: filename + } + }); + }); + + // this middleware injects the script for the reload file into the page response + _server.use(connectReload()); + } + + // server the static files + _server.use(options.path, serveStatic(options.root)); + + // start the server and listen, return the server instance + resolve(_server.listen(options.port, () => { + + if (options.debug) { + options.logFunc(`Server started and listening at ${serveUrl}`); + } + + if (options.open) { + opn(serveUrl); + } + })); + }); +} diff --git a/tools/dev-server/package.json b/tools/dev-server/package.json new file mode 100644 index 000000000..082d189de --- /dev/null +++ b/tools/dev-server/package.json @@ -0,0 +1,22 @@ +{ + "private": true, + "name": "@pnp/dev-server", + "version": "0.0.2", + "description": "Simple development server", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/pnp/pnpjs/tree/dev/tools/dev-server" + }, + "author": "Patrick Rodgers", + "license": "MIT", + "dependencies": { + "connect": "3.6.6", + "connect-livereload": "0.6.0", + "opn": "5.3.0", + "serve-static": "1.13.2", + "tiny-lr": "1.1.1", + "url": "0.11.0", + "watch": "1.0.2" + } +} diff --git a/tools/dev-server/readme.md b/tools/dev-server/readme.md new file mode 100644 index 000000000..5e1c1ef5d --- /dev/null +++ b/tools/dev-server/readme.md @@ -0,0 +1,35 @@ +# @pnp/dev-server + +A simple server for serving the docs site while editing documentation and assets. Created as a learning exercise and to provide the bare-minimum of required functionality. + +`npm install @pnp/dev-server --save` + +```JavaScript +const serverFactory = require("@pnp/dev-server"); + +serverFactory({ + root: "./wwwroot", + path: "/serve/path" +}).then(server => { + + console.log(`server.listening: ${server.listening}`); +}).catch(e => { + + done(e); +}); +``` + +## Options + +The below object represents the available options as well as their defaults + +```JavaScript +{ + root: "./", // string, directory from which files are served + path: "/", // string, path after http://localhost + port: 8888, // port, from which files are served + livereload: true, // enable live reload in the browser + open: true, // open the served url in the default browser + debug: false, // enable some debugging information, written to console (minimal) +} +``` diff --git a/tools/generator-package/generators/app/index.js b/tools/generator-package/generators/app/index.js new file mode 100644 index 000000000..eda988bd0 --- /dev/null +++ b/tools/generator-package/generators/app/index.js @@ -0,0 +1,48 @@ +'use strict'; +const Generator = require('yeoman-generator'); +const chalk = require('chalk'); +const yosay = require('yosay'); + +module.exports = class extends Generator { + prompting() { + // Have Yeoman greet the user. + this.log(yosay( + 'Welcome to the world-class ' + chalk.red('generator-package') + ' generator!' + )); + + const prompts = [{ + type: 'input', + name: 'name', + message: 'name?', + default: this.appname + }, + { + type: 'input', + name: 'description', + message: 'description?', + default: "" + }]; + + return this.prompt(prompts).then(props => { + // To access props later use this.props.someAnswer; + this.props = props; + }); + } + + writing() { + + this.fs.copyTpl( + this.templatePath("**/*.*"), + this.destinationPath(), + { + name: this.props.name, + description: this.props.description, + } + ); + + this.fs.write(`./src/${this.props.name}.ts`, "// TODO:: add package exports to this file"); + + } + + install() { } +}; diff --git a/tools/generator-package/generators/app/templates/index.ts b/tools/generator-package/generators/app/templates/index.ts new file mode 100644 index 000000000..083a1518f --- /dev/null +++ b/tools/generator-package/generators/app/templates/index.ts @@ -0,0 +1 @@ +export * from "./src/<%= name %>"; diff --git a/tools/generator-package/generators/app/templates/package.json b/tools/generator-package/generators/app/templates/package.json new file mode 100644 index 000000000..48e8f9a58 --- /dev/null +++ b/tools/generator-package/generators/app/templates/package.json @@ -0,0 +1,22 @@ +{ + "name": "@pnp/<%= name %>", + "version": "0.0.0-PLACEHOLDER", + "description": "pnp - <%= description %>", + "main": "./index.js", + "typings": "./index", + "dependencies": { + "tslib": "1.9.3" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + } +} \ No newline at end of file diff --git a/tools/generator-package/generators/app/templates/tsconfig.es2015.json b/tools/generator-package/generators/app/templates/tsconfig.es2015.json new file mode 100644 index 000000000..1cd3cfc52 --- /dev/null +++ b/tools/generator-package/generators/app/templates/tsconfig.es2015.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./index.ts", + "./src/**/*.ts" + ], + "references": [] +} diff --git a/tools/generator-package/generators/app/templates/tsconfig.es5.json b/tools/generator-package/generators/app/templates/tsconfig.es5.json new file mode 100644 index 000000000..0a22c1b4a --- /dev/null +++ b/tools/generator-package/generators/app/templates/tsconfig.es5.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.es5.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "../../build/packages/<%= name %>/es5", + }, + "include": [ + "./index.ts", + "./src/**/*.ts", + ], + "references": [] +} diff --git a/tools/generator-package/generators/app/templates/tsconfig.json b/tools/generator-package/generators/app/templates/tsconfig.json new file mode 100644 index 000000000..26caeeaac --- /dev/null +++ b/tools/generator-package/generators/app/templates/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.es2015.json", +} \ No newline at end of file diff --git a/tools/generator-package/package.json b/tools/generator-package/package.json new file mode 100644 index 000000000..30f779ed5 --- /dev/null +++ b/tools/generator-package/package.json @@ -0,0 +1,28 @@ +{ + "private": true, + "name": "generator-package", + "version": "0.0.1", + "description": "Generates a scaffolded package folder", + "author": { + "name": "SharePoint PnP" + }, + "files": [ + "generators" + ], + "main": "generators/index.js", + "keywords": [ + "yeoman-generator" + ], + "devDependencies": { + "yeoman-test": "^1.6.0", + "yeoman-assert": "^3.0.0" + }, + "dependencies": { + "yeoman-generator": "^1.0.0", + "chalk": "^1.1.3", + "yosay": "^2.0.0" + }, + "scripts": {}, + "repository": "https://github.com/pnp/pnp.git", + "license": "MIT" +} \ No newline at end of file diff --git a/tools/generator-package/readme.md b/tools/generator-package/readme.md new file mode 100644 index 000000000..082518fba --- /dev/null +++ b/tools/generator-package/readme.md @@ -0,0 +1,25 @@ +# generator-package + +This package is meant for use when creating a new package within the "packages" folder of the @pnp project. This will scaffold a project +to match what is expected from the build system as well as line up with the rest of the packages to keep things consistent. + +### Use + +``` +// switch to this folder in the command line +$cd ./tools/generator-packgage + +// install this as a generator globally +$npm install . -g + +// create and switch to the package folder +$cd ./packages +$mkdir {package name} +$cd {package name} +$ yo package +``` + +### Notes + +* This should never be published to npm +* This serves no purpose other than to help create consistent package structures diff --git a/tools/gulptasks/args.js b/tools/gulptasks/args.js new file mode 100644 index 000000000..8de4c8747 --- /dev/null +++ b/tools/gulptasks/args.js @@ -0,0 +1,42 @@ +const yargs = require('yargs').argv; + +/** + * Updates the configuration file based on any command line args supplied + * + * @param inConfig The configuration file before changes are made + */ +const processConfigCmdLine = (inConfig) => { + + // update to only process specific packages + if (yargs.packages || yargs.p) { + + let packageNames = (yargs.packages || yargs.p).split(",").map(s => s.trim()); + + if (!Array.isArray(packageNames)) { + packageNames = [packageNames]; + } + + // lowercase our input + packageNames = packageNames.map(name => name.toLowerCase()); + + const processingPackages = []; + + for(let i = 0; i < packageNames.length; i++) { + + // see of we have any package entries and pass them along as-is + const found = inConfig.packages.filter(p => { + return ((typeof p === "string" && p === packageNames[i]) || (p.name === packageNames[i])); + }); + + [].push.apply(processingPackages, found); + } + + inConfig.packages = processingPackages; + } + + return inConfig; +} + +module.exports = { + processConfigCmdLine: processConfigCmdLine +}; diff --git a/tools/gulptasks/build.js b/tools/gulptasks/build.js new file mode 100644 index 000000000..a9c604e97 --- /dev/null +++ b/tools/gulptasks/build.js @@ -0,0 +1,96 @@ +//****************************************************************************** +//* build.js +//* +//* Defines a custom gulp task for compiling TypeScript source code into +//* js files. It outputs the details as to what it generated to the console. +//****************************************************************************** + +const gulp = require("gulp"), + replace = require('gulp-replace'), + pkg = require("../../package.json"), + exec = require('child_process').exec, + path = require("path"), + pump = require('pump'), + fs = require("fs"), + cmdLine = require("./args").processConfigCmdLine; + +const tscPath = path.join("./node_modules/.bin/tsc"); + +// give outselves a single reference to the projectRoot +const projectRoot = path.resolve(__dirname, "../.."); + +/** + * Builds the build system for use by sub tasks + */ +gulp.task("bootstrap-buildsystem", (done) => { + + exec(`${tscPath} -b ./tools/buildsystem/tsconfig.json --force`, { + cwd: path.resolve(__dirname, "../.."), + }, (error, stdout, stderr) => { + + if (error === null) { + // now we copy over the package.json + fs.createReadStream('./tools/buildsystem/package.json') + .pipe(fs.createWriteStream('./build/tools/buildsystem/package.json')) + .on("close", () => done()); + } else { + done(stdout); + } + }); +}); + +/** + * Does the main build that is used by package and publish + */ +gulp.task("build", ["lint", "bootstrap-buildsystem"], (done) => { + + // create an instance of the engine used to process builds + const engine = require(path.join(projectRoot, "./build/tools/buildsystem")).builder; + const config = cmdLine(require(path.join(projectRoot, "./pnp-build.js"))); + + engine(pkg.version, config).then(done).catch(e => done(e)); +}); + +/** + * Builds the files for debugging (F5 in code) + */ +gulp.task("build:debug", ["clean-build-debugging", "bootstrap-buildsystem"], (done) => { + + // create an instance of the engine used to process builds + const engine = require(path.join(projectRoot, "./build/tools/buildsystem")).builder; + const config = cmdLine(require(path.join(projectRoot, "./pnp-debug.js"))); + + engine(pkg.version, config).then(done).catch(e => done(e)); +}); + +/** + * Builds the tests and src for testing + */ +gulp.task("build:test", ["clean", "lint:tests"], (done) => { + + exec(`${tscPath} -p ./test/tsconfig.json`, { + cwd: projectRoot, + }, (error, stdout, stderr) => { + + if (error === null) { + + pump([ + gulp.src(path.join(projectRoot, "./build/testing") + "/**/*.js"), + replace("$$Version$$", pkg.version), + replace(/require\(['|"]@pnp\//ig, `require("${path.resolve("./build/testing/packages/").replace(/\\/g, "/")}/`), + gulp.dest("./build/testing"), + ], (err) => { + + if (err !== undefined) { + done(err); + } else { + done(); + } + }); + + } else { + console.log(stdout); + done(error); + } + }); +}); diff --git a/tools/gulptasks/clean.js b/tools/gulptasks/clean.js new file mode 100644 index 000000000..dafc5d3c9 --- /dev/null +++ b/tools/gulptasks/clean.js @@ -0,0 +1,47 @@ +//****************************************************************************** +//* clean.js +//* +//* Defines a custom gulp task for removing all output files that were +//* autogenerated by other gulp tasks +//****************************************************************************** + +const gulp = require("gulp"), + del = require("del"), + yargs = require("yargs").argv, + log = require("fancy-log"), + colors = require("ansi-colors"); + +gulp.task('clean', (done) => { + + if (yargs.noclean || yargs.nc) { + log(`${colors.bgWhite(" ")} Skipping clean due to flag.`); + return done(); + } + + const directories = [ + "./dist", + "./serve", + "./site", + ]; + + log(`${colors.bgBlue(" ")} Cleaning directories: ${directories.join(", ")}.`); + del(directories).then(() => { + log(`${colors.bgGreen(" ")} Cleaned directories: ${directories.join(", ")}.`); + done(); + }).catch(e => { + log(`${colors.bgRed(" ")} Error cleaning directories: ${directories.join(", ")}.`); + done(e); + }); +}); + +gulp.task("clean-build", (done) => { + del("./build").then(() => { + done(); + }); +}); + +gulp.task("clean-build-debugging", (done) => { + del("./build/debugging").then(() => { + done(); + }); +}); diff --git a/tools/gulptasks/index.js b/tools/gulptasks/index.js new file mode 100644 index 000000000..173d5dd2f --- /dev/null +++ b/tools/gulptasks/index.js @@ -0,0 +1,8 @@ +require("./clean.js"); +require("./lint.js"); +require("./build.js"); +require("./package.js"); +require("./test.js"); +require("./serve.js"); +require("./travisci.js"); +require("./publish.js"); diff --git a/tools/gulptasks/lint.js b/tools/gulptasks/lint.js new file mode 100644 index 000000000..872585872 --- /dev/null +++ b/tools/gulptasks/lint.js @@ -0,0 +1,68 @@ +//****************************************************************************** +//* lint.js +//* +//* Defines a custom gulp task for ensuring that all source code in +//* this repository follows recommended TypeScript practices. +//* +//* Rule violations are output automatically to the console. +//****************************************************************************** + +const gulp = require("gulp"), + gulpTslint = require("gulp-tslint"), + tslint = require("tslint"), + pump = require("pump"); + +// const tscPath = path.join("./node_modules/.bin/tsc"); + +gulp.task("lint", (done) => { + + const config = tslint.Configuration.loadConfigurationFromPath("./tslint.json"); + + pump([ + gulp.src([ + "./packages/**/src/**/*.ts", + "!./packages/**/*.test.ts", + "!**/node_modules/**", + "!**/*.d.ts" + ]), + gulpTslint({ configuration: config, formatter: "prose" }), + gulpTslint.report({ emitError: false }), + ], (err) => { + + if (err !== undefined) { + done(err); + } else { + done(); + } + }); +}); + +gulp.task("lint:tests", (done) => { + + var program = tslint.Linter.createProgram("./test/tsconfig.json"); + + // we need to load and override the configuration + const config = tslint.Configuration.loadConfigurationFromPath("./tslint.json"); + config.rules.set("no-unused-expression", { ruleSeverity: "off" }); + + pump([ + gulp.src([ + "./test/**/*.ts", + "!**/node_modules/**", + "!**/*.d.ts" + ]), + gulpTslint({ + configuration: config, + formatter: "prose", + program, + }), + gulpTslint.report({ emitError: false }), + ], (err) => { + + if (err !== undefined) { + done(err); + } else { + done(); + } + }); +}); diff --git a/tools/gulptasks/package.js b/tools/gulptasks/package.js new file mode 100644 index 000000000..97296518d --- /dev/null +++ b/tools/gulptasks/package.js @@ -0,0 +1,33 @@ +//****************************************************************************** +//* package.js +//* +//* Defines a custom gulp task for creaing pnp.js, pnp.min.js, +//* and pnp.min.js.map in the dist folder +//****************************************************************************** +const gulp = require("gulp"), + path = require("path"), + pkg = require("../../package.json"), + cmdLine = require("./args").processConfigCmdLine; + +// give outselves a single reference to the projectRoot +const projectRoot = path.resolve(__dirname, "../.."); + +// package the assets to dist +// gulp.task("package:assets", () => { +// gulp.src(config.paths.assetsGlob).pipe(gulp.dest(config.paths.dist)); +// }); + +// used by the sync task to rebuild code +// TODO:: +gulp.task("package:sync", ["package:code"]); + +/** + * Packages the build files into their dist folders ready for publishing to npm + */ +gulp.task("package", ["bootstrap-buildsystem", "build"], (done) => { + + const engine = require(path.join(projectRoot, "./build/tools/buildsystem")).packager; + const config = cmdLine(require(path.join(projectRoot, "./pnp-package.js"))); + + engine(pkg.version, config).then(done).catch(e => done(e)); +}); diff --git a/tools/gulptasks/publish.js b/tools/gulptasks/publish.js new file mode 100644 index 000000000..129f48d7a --- /dev/null +++ b/tools/gulptasks/publish.js @@ -0,0 +1,138 @@ +//****************************************************************************** +//* publish.js +//* +//* Defines a custom gulp task for publishing this repository to npm in +//* both main and beta versions for different branches +//* +//****************************************************************************** + +const gulp = require("gulp"), + path = require("path"), + pkg = require("../../package.json"), + cmdLine = require("./args").processConfigCmdLine, + exec = require("child_process").execSync, + log = require("fancy-log"), + replace = require("replace-in-file"); + + +// give outselves a single reference to the projectRoot +const projectRoot = path.resolve(__dirname, "../.."); + +function chainCommands(commands) { + + return commands.reduce((chain, cmd) => chain.then(new Promise((resolve, reject) => { + + try { + log(cmd); + exec(cmd, { stdio: "inherit" }); + resolve(); + } catch (e) { + reject(e); + throw e; + } + + })), Promise.resolve()); +} + +function doPublish(configFileName) { + + const engine = require(path.join(projectRoot, "./build/tools/buildsystem")).publisher; + const config = cmdLine(require(path.join(projectRoot, configFileName))); + + return engine(pkg.version, config); +} + +/** + * Dynamically creates and executes a script to publish things + * + */ +function runPublishScript() { + + const script = []; + + // merge dev -> master + script.push( + "git checkout dev", + "git pull", + "git checkout master", + "git pull", + "git merge dev", + "npm install"); + + // version here to all subsequent actions have the new version available in package.json + script.push("npm version patch"); + + // push the updates to master (version info) + script.push("git push"); + + // package and publish to npm + script.push("gulp publish:packages"); + + // merge master back to dev for updated version # + script.push( + "git checkout master", + "git pull", + "git checkout dev", + "git pull", + "git merge master", + "git push"); + + // always leave things on the dev branch + script.push("git checkout dev"); + + return chainCommands(script); +} + +gulp.task("publish:packages", ["package"], (done) => { + + doPublish("./pnp-publish.js").then(done).catch(done); +}); + +gulp.task("publish:packages-beta", ["package"], (done) => { + + doPublish("./pnp-publish-beta.js").then(done).catch(done); +}); + +gulp.task("publish-beta", (done) => { + + chainCommands([ + + // beta releases are done from dev branch + "git checkout dev", + + // update package version + "npm version prerelease", + + // push updates to dev + "git push", + + // package and publish the packages to npm + "gulp publish:packages-beta", + + // always leave things on the dev branch + "git checkout dev", + + ]).then(done).catch(done); +}); + +gulp.task("publish", ["clean", "clean-build"], (done) => { + + runPublishScript().then(_ => { + + // now the version number will be updated in this package + const updatedPkg = require("../../package.json"); + + // here we need to update the version in the mkdocs.yml file + replace({ + files: [path.resolve(projectRoot, "mkdocs.yml")], + from: /version: '[0-9\.-]+'/ig, + to: `version: '${updatedPkg.version}'`, + }); + + // update the docs site + exec("mkdocs gh-deploy", { stdio: "inherit" }); + + done(); + + }).catch(done); +}); diff --git a/tools/gulptasks/serve.js b/tools/gulptasks/serve.js new file mode 100644 index 000000000..dc45dac73 --- /dev/null +++ b/tools/gulptasks/serve.js @@ -0,0 +1,69 @@ +//****************************************************************************** +//* serve.js +//* +//* Defines a custom gulp task for serving up content from the server-root +//* local folder, setup file/folder watchers so that changes are reflected +//* on file save, and open the default browser to the default html page. +//****************************************************************************** + +const path = require("path"); + +// give outselves a single reference to the projectRoot +const projectRoot = path.resolve(__dirname, "../.."); + +const gulp = require("gulp"), + webpack = require('webpack'), + server = require("webpack-dev-server"), + cmdLine = require("./args").processConfigCmdLine, + pkg = require(path.join(projectRoot, "package.json")), + log = require("fancy-log"), + colors = require("ansi-colors"), + getSubDirNames = require("../node-utils/getSubDirectoryNames"), + config = require(path.resolve("./debug/serve/webpack.config.js")); + + +gulp.task("serve", (done) => { + + // check to see if you used a flag to serve a single package + const args = cmdLine({}); + + if (args.hasOwnProperty("packages") && args.packages.length > 0) { + + if (args.packages.length > 1) { + throw new Error("You can only specify a single package when using serve."); + } + + log(`Serving package: ${args.packages[0]}`); + + // update the entry point to be the package that was requested + config.entry = `./packages/${args.packages[0]}/index.ts`; + + // update to use the config file for build of a specific package + configFileName = "tsconfig.es5.json"; + + // update the library to match what would be generated + if (args.packages[0].toLowerCase() === "pnpjs") { + library = "pnp"; + } else { + library = `pnp.${args.packages[0]}`; + } + } + + const serverSettings = { + publicPath: "/assets/", + stats: { + colors: true + }, + https: true + }; + + // Start a webpack-dev-server + new server(webpack(config), serverSettings).listen(8080, "localhost", (err) => { + + if (err) { + return done(new gutil.PluginError("serve", err)); + } + + log("File will be served from:", colors.bgBlue(colors.white("https://localhost:8080/assets/pnp.js"))); + }); +}); diff --git a/tools/gulptasks/test.js b/tools/gulptasks/test.js new file mode 100644 index 000000000..1214f7f37 --- /dev/null +++ b/tools/gulptasks/test.js @@ -0,0 +1,75 @@ +//****************************************************************************** +//* test.js +//* +//* Defines a custom gulp task for executing the unit tests (with mocha) and +//* also reporting on code coverage (with istanbul). +//****************************************************************************** + +const gulp = require("gulp"), + mocha = require("gulp-mocha"), + istanbul = require("gulp-istanbul"), + path = require("path"), + yargs = require('yargs').argv, + fs = require("fs"), + cmdLine = require("./args").processConfigCmdLine; + +gulp.task("_istanbul:hook", ["build:test"], () => { + // we hook the built packages + return gulp.src("./testing/packages/**/*.js") + .pipe(istanbul()) + .pipe(istanbul.hookRequire()); +}); + +function getAllPackageFolderNames() { + + const root = path.resolve("./packages"); + return fs.readdirSync(root).filter(dirName => { + dir = path.join(root, dirName); + const stat = fs.statSync(dir); + return stat && stat.isDirectory(); + }); +} + +gulp.task("test", ["clean", "build:test", "_istanbul:hook"], () => { + + // when using single, grab only that test.js file - otherwise use the entire test.js glob + + // we use the built *.test.js files here + const args = cmdLine({ packages: getAllPackageFolderNames() }); + let paths = ["./build/testing/test/main.js"]; + + // update to only process specific packages + if (yargs.packages || yargs.p) { + + if (yargs.single || yargs.s) { + // and only a single set of tests + paths.push(path.resolve(`./build/testing/test/${args.packages[0]}/`, (yargs.single || yargs.s) + ".js")); + } else { + paths = args.packages.map(p => `./build/testing/test/${p}/**/*.js`); + } + + } else { + paths.push("./build/testing/**/*.js"); + } + + const reporter = yargs.verbose ? "spec" : "dot"; + + return gulp.src(paths) + .pipe(mocha({ + ui: "bdd", + reporter: reporter, + timeout: 40000, + "pnp-test-mode": "cmd", + "pnp-test-site": yargs.site || "''", + "skip-web": yargs.skipWeb, + slow: 3000, + })) + .pipe(istanbul.writeReports({ + reporters: ["text", "text-summary"] + })).once("error", function () { + process.exit(1); + }) + .once("end", function () { + process.exit(); + }); +}); diff --git a/tools/gulptasks/travisci.js b/tools/gulptasks/travisci.js new file mode 100644 index 000000000..268b3ecc4 --- /dev/null +++ b/tools/gulptasks/travisci.js @@ -0,0 +1,79 @@ +//****************************************************************************** +//* travisci.js +//* +//* Defines a set of gulp tasks used to integrate with travisci +//****************************************************************************** + +const gulp = require("gulp"), + mocha = require("gulp-mocha"), + tslint = require("tslint"), + pump = require("pump"), + gulpTslint = require("gulp-tslint"); + +gulp.task("travis:lint", (done) => { + + pump([ + gulp.src([ + "./packages/**/*.ts", + "!./packages/**/*.test.ts", + "!**/node_modules/**", + "!**/*.d.ts" + ]), + gulpTslint({ formatter: "prose" }), + gulpTslint.report({ emitError: true }), + ], (err) => { + + if (err !== undefined) { + done(err); + } else { + done(); + } + }); +}); + +gulp.task("travis:webtest", ["travis:prereqs", "build:test"], () => { + + return gulp.src(["./build/testing/test/main.js", "./build/testing/**/*.test.js"]) + .pipe(mocha({ + ui: "bdd", + reporter: "spec", + timeout: 60000, + "pnp-test-mode": "travis", + retries: 2, + slow: 5000, + ignoreTimeouts: true, + })) + .once("error", () => { + process.exit(1); + }) + .once("end", () => { + process.exit(); + }); +}); + +gulp.task("travis:test", ["travis:prereqs", "build:test"], () => { + + return gulp.src(["./build/testing/test/main.js", "./build/testing/**/*.test.js"]) + .pipe(mocha({ + ui: "bdd", + reporter: "spec", + timeout: 1000, + "pnp-test-mode": "travis-noweb", + retries: 2, + slow: 300, + })) + .once("error", () => { + process.exit(1); + }) + .once("end", () => { + process.exit(); + }); +}); + +gulp.task("travis:prereqs", ["travis:lint", "package"]); + +// runs when someone executes a PR from a fork +gulp.task("travis:pull-request", ["travis:prereqs", "travis:test"]); + +// runs when things are pushed/merged +gulp.task("travis:push", ["travis:prereqs", "travis:webtest"]); diff --git a/tools/node-utils/getSubDirectoryNames.js b/tools/node-utils/getSubDirectoryNames.js new file mode 100644 index 000000000..c31f54ea4 --- /dev/null +++ b/tools/node-utils/getSubDirectoryNames.js @@ -0,0 +1,7 @@ +// after: https://stackoverflow.com/questions/18112204/get-all-directories-within-directory-nodejs +const { lstatSync, readdirSync } = require("fs"); +const { join } = require("path"); + +const isDirectory = (root, dirName) => lstatSync(join(root, dirName)).isDirectory(); + +module.exports = (root) => readdirSync(root).filter(dirName => isDirectory(root, dirName)); diff --git a/tools/polyfill-ie11/.gitignore b/tools/polyfill-ie11/.gitignore new file mode 100644 index 000000000..0f7a61c11 --- /dev/null +++ b/tools/polyfill-ie11/.gitignore @@ -0,0 +1 @@ +./dist \ No newline at end of file diff --git a/tools/polyfill-ie11/.npmignore b/tools/polyfill-ie11/.npmignore new file mode 100644 index 000000000..3772ff9bb --- /dev/null +++ b/tools/polyfill-ie11/.npmignore @@ -0,0 +1,6 @@ +*.ts +tsconfig.json +webpack.config.js +.gitignore +.npmignore +!dist/*.* diff --git a/tools/polyfill-ie11/CHANGELOG.md b/tools/polyfill-ie11/CHANGELOG.md new file mode 100644 index 000000000..36d91631c --- /dev/null +++ b/tools/polyfill-ie11/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog +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). + +## Unreleased + +## 1.0.1 - 2019-15-1 + +- Added Array.fill polyfill to bundle + +## 1.0.0 - 2018-11-18 + +### Added +- Everything diff --git a/tools/polyfill-ie11/LICENSE b/tools/polyfill-ie11/LICENSE new file mode 100644 index 000000000..af77dab54 --- /dev/null +++ b/tools/polyfill-ie11/LICENSE @@ -0,0 +1,25 @@ +SharePoint Patterns and Practices (PnP) + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/polyfill-ie11/index.ts b/tools/polyfill-ie11/index.ts new file mode 100644 index 000000000..fca1d6436 --- /dev/null +++ b/tools/polyfill-ie11/index.ts @@ -0,0 +1,6 @@ +import "core-js/modules/es6.promise"; +import "core-js/modules/es6.array.iterator.js"; +import "core-js/modules/es6.array.from.js"; +import "core-js/modules/es6.array.fill.js"; +import "es6-map/implement"; +import "whatwg-fetch"; diff --git a/tools/polyfill-ie11/package.json b/tools/polyfill-ie11/package.json new file mode 100644 index 000000000..f1294026a --- /dev/null +++ b/tools/polyfill-ie11/package.json @@ -0,0 +1,39 @@ +{ + "name": "@pnp/polyfill-ie11", + "version": "1.0.1", + "description": "pnp - provides required polyfills for ie11", + "main": "./dist/index.js", + "types": "./dist", + "scripts": { + "bundle": "webpack" + }, + "peerDependencies": { + "@pnp/logging": "*", + "@pnp/common": "*", + "@pnp/odata": "*", + "@pnp/sp": "*" + }, + "dependencies": { + "@types/core-js": "2.5.0", + "core-js": "2.6.2", + "es6-map": "0.1.5", + "whatwg-fetch": "3.0.0" + }, + "author": { + "name": "Microsoft and other contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs", + "repository": { + "type": "git", + "url": "git:github.com/pnp/pnpjs" + }, + "devDependencies": { + "awesome-typescript-loader": "5.2.1", + "typescript": "^3.2.2", + "webpack": "4.28.4" + } +} diff --git a/tools/polyfill-ie11/readme.md b/tools/polyfill-ie11/readme.md new file mode 100644 index 000000000..9976c0cab --- /dev/null +++ b/tools/polyfill-ie11/readme.md @@ -0,0 +1,22 @@ +![SharePoint Patterns and Practices](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) + +The SharePoint Patterns and Practices client side libraries were created to help enable developers to do their best work, without worrying about the exact +REST api details. Built with feedback from the community they represent a way to simplify your day-to-day dev cycle while relying on tested and proven +patterns. + +Please use [http://aka.ms/sppnp](http://aka.ms/sppnp) for the latest updates around the whole *SharePoint Patterns and Practices (PnP) initiative*. + +## Source & Docs + +This code is managed within the [main pnp repo](https://github.com/pnp/pnp), please report issues and submit pull requests there. + +Please see the public site for [package documentation](https://pnp.github.io/pnpjs/). + +### Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### "Sharing is Caring" + +### Disclaimer +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** \ No newline at end of file diff --git a/tools/polyfill-ie11/searchquerybuilder.ts b/tools/polyfill-ie11/searchquerybuilder.ts new file mode 100644 index 000000000..65f675adf --- /dev/null +++ b/tools/polyfill-ie11/searchquerybuilder.ts @@ -0,0 +1,204 @@ +import { Sort, ReorderingRule, SearchProperty, SearchQuery, ISearchQueryBuilder } from "@pnp/sp"; + +export function SearchQueryBuilder(queryText = "", _query = {}): ISearchQueryBuilder { + return new SearchQueryBuilderImpl(queryText, _query); +} + +/** + * Allows for the fluent construction of search queries + */ +class SearchQueryBuilderImpl { + + constructor(queryText = "", private _query = {}) { + + if (typeof queryText === "string" && queryText.length > 0) { + + this.extendQuery({ Querytext: queryText }); + } + } + + public get query(): any { + return this._query; + } + + public text(queryText: string): this { + return this.extendQuery({ Querytext: queryText }); + } + + public template(template: string): this { + return this.extendQuery({ QueryTemplate: template }); + } + + public sourceId(id: string): this { + return this.extendQuery({ SourceId: id }); + } + + public get enableInterleaving(): this { + return this.extendQuery({ EnableInterleaving: true }); + } + + public get enableStemming(): this { + return this.extendQuery({ EnableStemming: true }); + } + + public get trimDuplicates(): this { + return this.extendQuery({ TrimDuplicates: true }); + } + + public trimDuplicatesIncludeId(n: number): this { + return this.extendQuery({ TrimDuplicatesIncludeId: n }); + } + + public get enableNicknames(): this { + return this.extendQuery({ EnableNicknames: true }); + } + + public get enableFql(): this { + return this.extendQuery({ EnableFQL: true }); + } + + public get enablePhonetic(): this { + return this.extendQuery({ EnablePhonetic: true }); + } + + public get bypassResultTypes(): this { + return this.extendQuery({ BypassResultTypes: true }); + } + + public get processBestBets(): this { + return this.extendQuery({ ProcessBestBets: true }); + } + + public get enableQueryRules(): this { + return this.extendQuery({ EnableQueryRules: true }); + } + + public get enableSorting(): this { + return this.extendQuery({ EnableSorting: true }); + } + + public get generateBlockRankLog(): this { + return this.extendQuery({ GenerateBlockRankLog: true }); + } + + public rankingModelId(id: string): this { + return this.extendQuery({ RankingModelId: id }); + } + + public startRow(n: number): this { + return this.extendQuery({ StartRow: n }); + } + + public rowLimit(n: number): this { + return this.extendQuery({ RowLimit: n }); + } + + public rowsPerPage(n: number): this { + return this.extendQuery({ RowsPerPage: n }); + } + + public selectProperties(...properties: string[]): this { + return this.extendQuery({ SelectProperties: properties }); + } + + public culture(culture: number): this { + return this.extendQuery({ Culture: culture }); + } + + public timeZoneId(id: number): this { + return this.extendQuery({ TimeZoneId: id }); + } + + public refinementFilters(...filters: string[]): this { + return this.extendQuery({ RefinementFilters: filters }); + } + + public refiners(refiners: string): this { + return this.extendQuery({ Refiners: refiners }); + } + + public hiddenConstraints(constraints: string): this { + return this.extendQuery({ HiddenConstraints: constraints }); + } + + public sortList(...sorts: Sort[]): this { + return this.extendQuery({ SortList: sorts }); + } + + public timeout(milliseconds: number): this { + return this.extendQuery({ Timeout: milliseconds }); + } + + public hithighlightedProperties(...properties: string[]): this { + return this.extendQuery({ HitHighlightedProperties: properties }); + } + + public clientType(clientType: string): this { + return this.extendQuery({ ClientType: clientType }); + } + + public personalizationData(data: string): this { + return this.extendQuery({ PersonalizationData: data }); + } + + public resultsURL(url: string): this { + return this.extendQuery({ ResultsUrl: url }); + } + + public queryTag(tags: string): this { + return this.extendQuery({ QueryTag: tags }); + } + + public properties(...properties: SearchProperty[]): this { + return this.extendQuery({ Properties: properties }); + } + + public get processPersonalFavorites(): this { + return this.extendQuery({ ProcessPersonalFavorites: true }); + } + + public queryTemplatePropertiesUrl(url: string): this { + return this.extendQuery({ QueryTemplatePropertiesUrl: url }); + } + + public reorderingRules(...rules: ReorderingRule[]): this { + return this.extendQuery({ ReorderingRules: rules }); + } + + public hitHighlightedMultivaluePropertyLimit(limit: number): this { + return this.extendQuery({ HitHighlightedMultivaluePropertyLimit: limit }); + } + + public get enableOrderingHitHighlightedProperty(): this { + return this.extendQuery({ EnableOrderingHitHighlightedProperty: true }); + } + + public collapseSpecification(spec: string): this { + return this.extendQuery({ CollapseSpecification: spec }); + } + + public uiLanguage(lang: number): this { + return this.extendQuery({ UILanguage: lang }); + } + + public desiredSnippetLength(len: number): this { + return this.extendQuery({ DesiredSnippetLength: len }); + } + + public maxSnippetLength(len: number): this { + return this.extendQuery({ MaxSnippetLength: len }); + } + + public summaryLength(len: number): this { + return this.extendQuery({ SummaryLength: len }); + } + + public toSearchQuery(): SearchQuery { + return this._query; + } + + private extendQuery(part: any): this { + this._query = Object.assign({}, this._query, part); + return this; + } +} diff --git a/tools/polyfill-ie11/tsconfig.json b/tools/polyfill-ie11/tsconfig.json new file mode 100644 index 000000000..83c13b74d --- /dev/null +++ b/tools/polyfill-ie11/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "baseUrl": ".", + "rootDir": ".", + "outDir": "./dist", + "declaration": true, + "moduleResolution": "node", + "skipLibCheck": true, + "types": [ + "core-js" + ], + "lib": [ + "dom", + "es2016" + ] + }, + "files": [ + "./index.ts", + "./searchquerybuilder.ts" + ] +} diff --git a/tools/polyfill-ie11/webpack.config.js b/tools/polyfill-ie11/webpack.config.js new file mode 100644 index 000000000..b6f059efd --- /dev/null +++ b/tools/polyfill-ie11/webpack.config.js @@ -0,0 +1,30 @@ +const path = require("path"); + +module.exports = { + entry: { + "index": "./index.ts", + "searchquerybuilder": "./searchquerybuilder.ts", + }, + mode: "production", + devtool: false, + module: { + rules: [ + { + test: /\.ts$/, + use: [ + { + loader: "awesome-typescript-loader", + options: { + useCache: false, + errorsAsWarnings: true, + }, + }, + ], + }], + }, + output: { + path: path.resolve(__dirname, "dist"), + filename: "[name].js", + libraryTarget: "umd", + }, +}; diff --git a/tools/tests/.gitignore b/tools/tests/.gitignore new file mode 100644 index 000000000..0f7a61c11 --- /dev/null +++ b/tools/tests/.gitignore @@ -0,0 +1 @@ +./dist \ No newline at end of file diff --git a/tools/tests/.npmignore b/tools/tests/.npmignore new file mode 100644 index 000000000..3772ff9bb --- /dev/null +++ b/tools/tests/.npmignore @@ -0,0 +1,6 @@ +*.ts +tsconfig.json +webpack.config.js +.gitignore +.npmignore +!dist/*.* diff --git a/tools/tests/CHANGELOG.md b/tools/tests/CHANGELOG.md new file mode 100644 index 000000000..9aa5e6e72 --- /dev/null +++ b/tools/tests/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +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). + +## Unreleased + +## 1.0.0 - 2019-04-20 + +### Added +- Everything diff --git a/tools/tests/LICENSE b/tools/tests/LICENSE new file mode 100644 index 000000000..af77dab54 --- /dev/null +++ b/tools/tests/LICENSE @@ -0,0 +1,25 @@ +SharePoint Patterns and Practices (PnP) + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/tests/package.json b/tools/tests/package.json new file mode 100644 index 000000000..b46fe198d --- /dev/null +++ b/tools/tests/package.json @@ -0,0 +1,34 @@ +{ + "name": "@pnp/tests", + "version": "0.0.1", + "description": "A lightweight attribute based testing framework", + "main": "./dist/index.js", + "types": "./dist", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prepack": "webpack" + }, + "repository": { + "type": "git", + "url": "git://github.com/pnp/pnpjs.git" + }, + "author": "Patrick Rodgers", + "license": "MIT", + "bugs": { + "url": "https://github.com/pnp/pnpjs/issues" + }, + "homepage": "https://github.com/pnp/pnpjs#readme", + "devDependencies": { + "awesome-typescript-loader": "^5.2.1", + "typescript": "^3.4.3", + "webpack": "^4.30.0" + }, + "dependencies": { + "@types/chai": "^4.1.7", + "@types/chai-as-promised": "^7.1.0", + "@types/mocha": "^5.2.6", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "mocha": "^6.1.3" + } +} diff --git a/tools/tests/readme.md b/tools/tests/readme.md new file mode 100644 index 000000000..9976c0cab --- /dev/null +++ b/tools/tests/readme.md @@ -0,0 +1,22 @@ +![SharePoint Patterns and Practices](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) + +The SharePoint Patterns and Practices client side libraries were created to help enable developers to do their best work, without worrying about the exact +REST api details. Built with feedback from the community they represent a way to simplify your day-to-day dev cycle while relying on tested and proven +patterns. + +Please use [http://aka.ms/sppnp](http://aka.ms/sppnp) for the latest updates around the whole *SharePoint Patterns and Practices (PnP) initiative*. + +## Source & Docs + +This code is managed within the [main pnp repo](https://github.com/pnp/pnp), please report issues and submit pull requests there. + +Please see the public site for [package documentation](https://pnp.github.io/pnpjs/). + +### Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### "Sharing is Caring" + +### Disclaimer +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** \ No newline at end of file diff --git a/tools/tests/src/index.ts b/tools/tests/src/index.ts new file mode 100644 index 000000000..c94bd0879 --- /dev/null +++ b/tools/tests/src/index.ts @@ -0,0 +1,4 @@ +// need a bin to remove tests from files for production build\ + +// attribute which creates a test in mocha + diff --git a/tools/tests/tsconfig.json b/tools/tests/tsconfig.json new file mode 100644 index 000000000..83c13b74d --- /dev/null +++ b/tools/tests/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "baseUrl": ".", + "rootDir": ".", + "outDir": "./dist", + "declaration": true, + "moduleResolution": "node", + "skipLibCheck": true, + "types": [ + "core-js" + ], + "lib": [ + "dom", + "es2016" + ] + }, + "files": [ + "./index.ts", + "./searchquerybuilder.ts" + ] +} diff --git a/tools/tests/webpack.config.js b/tools/tests/webpack.config.js new file mode 100644 index 000000000..b6f059efd --- /dev/null +++ b/tools/tests/webpack.config.js @@ -0,0 +1,30 @@ +const path = require("path"); + +module.exports = { + entry: { + "index": "./index.ts", + "searchquerybuilder": "./searchquerybuilder.ts", + }, + mode: "production", + devtool: false, + module: { + rules: [ + { + test: /\.ts$/, + use: [ + { + loader: "awesome-typescript-loader", + options: { + useCache: false, + errorsAsWarnings: true, + }, + }, + ], + }], + }, + output: { + path: path.resolve(__dirname, "dist"), + filename: "[name].js", + libraryTarget: "umd", + }, +}; diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000..16589f195 --- /dev/null +++ b/tslint.json @@ -0,0 +1,112 @@ +{ + "linterOptions": { + "exclude": [ + "/**/node_modules/**", + "/**/*.d.ts", + "/**/*.test.ts" + ] + }, + "rules": { + "no-consecutive-blank-lines": false, + "class-name": false, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "indent": [ + true, + "spaces" + ], + "label-position": true, + "max-line-length": [ + true, + 180 + ], + "member-access": true, + "member-ordering": [ + true, + { + "order": "fields-first" + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": false, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-inferrable-types": true, + "no-shadowed-variable": true, + "no-string-literal": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": [ + true, + "ignore-jsdoc" + ], + "no-unused-expression": true, + "no-use-before-declare": false, + "no-var-keyword": true, + "object-literal-sort-keys": true, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "double", + "avoid-escape" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "trailing-comma": [ + true, + { + "singleline": "never", + "multiline": "always" + } + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..121fa4c23 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,65 @@ +/** + * This webpack configuration file will create all of the bundles for each project. + * It expects that everything is built prior to bundling. (gulp build) + */ +const path = require("path"), + getSubDirNames = require("./tools/node-utils/getSubDirectoryNames"), + publishConfig = require("./pnp-publish"), + banner = require("./banner"), + webpack = require("webpack"); + +// static values +// we always bundle the es5 output +const buildOutputRoot = "./build/packages-es5/"; + +// get all the packages, but filter nodejs & documentation as we don't bundle that as it is never used in the browser +const packageSources = getSubDirNames(buildOutputRoot).filter(name => name !== "nodejs" && name !== "documentation"); +const getDistFolder = (name) => path.join(publishConfig.packageRoot, name); +const libraryNameGen = (name) => name === "pnpjs" ? "pnp" : `pnp.${name}`; + +// this is a config stub used to init the build configs. +const common = { + cache: true, + devtool: "source-map", + resolve: { + alias: {}, + }, + plugins: [ + new webpack.BannerPlugin({ + banner, + raw: true, + }), + ] +}; + +// we need to setup the alias values for the local packages for bundling +for (let i = 0; i < packageSources.length; i++) { + common.resolve.alias[`@pnp/${packageSources[i]}`] = path.resolve(buildOutputRoot, packageSources[i]); +} + +const bundleTemplate = (name, targetFolder) => Object.assign({}, common, { + mode: "development", + entry: path.resolve(buildOutputRoot, `${name}/index.js`), + output: { + filename: `${name}.es5.umd.bundle.js`, + library: libraryNameGen(name), + libraryTarget: "umd", + path: path.join(targetFolder, "dist"), + }, +}); + +const bundleTemplateMin = (name, targetFolder) => Object.assign({}, common, { + mode: "production", + entry: path.resolve(buildOutputRoot, `${name}/index.js`), + output: { + filename: `${name}.es5.umd.bundle.min.js`, + library: libraryNameGen(name), + libraryTarget: "umd", + path: path.join(targetFolder, "dist"), + }, +}); + +module.exports = [ + ...packageSources.map(pkgName => bundleTemplate(pkgName, getDistFolder(pkgName))), + ...packageSources.map(pkgName => bundleTemplateMin(pkgName, getDistFolder(pkgName))) +];