diff --git a/examples/service-clients/odsp-client/shared-tree-demo/package.json b/examples/service-clients/odsp-client/shared-tree-demo/package.json index 103fa138e49a..03205229e4d8 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/package.json +++ b/examples/service-clients/odsp-client/shared-tree-demo/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@azure/msal-browser": "^2.34.0", + "@fluidframework/core-utils": "workspace:~", "@fluidframework/odsp-client": "workspace:~", "@fluidframework/odsp-doclib-utils": "workspace:~", "css-loader": "^6.11.0", diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/clientProps.ts b/examples/service-clients/odsp-client/shared-tree-demo/src/clientProps.ts index 427aa4e97736..77f606183135 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/clientProps.ts +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/clientProps.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { OdspClientProps, OdspConnectionConfig } from "@fluidframework/odsp-client/beta"; +// eslint-disable-next-line import/no-internal-modules +import { OdspClientProps, OdspConnectionConfig } from "@fluidframework/odsp-client/internal"; import { OdspTestTokenProvider } from "./tokenProvider.js"; @@ -23,7 +24,6 @@ const connectionConfig: OdspConnectionConfig = { tokenProvider: new OdspTestTokenProvider(props.clientId), siteUrl: props.siteUrl, driveId: props.driveId, - filePath: "", }; export const clientProps: OdspClientProps = { diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts index c7be63803416..02424e9e8915 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts @@ -3,12 +3,17 @@ * Licensed under the MIT License. */ -import { OdspClient, OdspContainerServices } from "@fluidframework/odsp-client/beta"; -import { ContainerSchema, IFluidContainer, SharedTree } from "fluid-framework"; +import { + createOdspClient, + OdspContainerServices, + OdspContainerAttachFunctor, + // eslint-disable-next-line import/no-internal-modules +} from "@fluidframework/odsp-client/internal"; +import { ContainerSchema, SharedTree, IFluidContainer } from "fluid-framework"; import { clientProps } from "./clientProps.js"; -const client = new OdspClient(clientProps); +const client = createOdspClient(clientProps); /** * This function will create a container if no item Id is passed on the hash portion of the URL. @@ -37,16 +42,13 @@ export const createFluidData = async ( ): Promise<{ services: OdspContainerServices; container: IFluidContainer; + createFn: OdspContainerAttachFunctor; }> => { // The client will create a new detached container using the schema // A detached container will enable the app to modify the container before attaching it to the client - const { - container, - services, - }: { container: IFluidContainer; services: OdspContainerServices } = - await client.createContainer(schema); + const { container, services, createFn } = await client.createContainer(schema); - return { services, container }; + return { services, container, createFn }; }; export const containerSchema: ContainerSchema = { diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx b/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx index d28044240a08..ffe2268d54fa 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx @@ -3,7 +3,11 @@ * Licensed under the MIT License. */ -import { IFluidContainer, ITree } from "fluid-framework"; +// eslint-disable-next-line import/no-internal-modules +import { assert } from "@fluidframework/core-utils/internal"; +// eslint-disable-next-line import/no-internal-modules +import { OdspContainerAttachFunctor } from "@fluidframework/odsp-client/internal"; +import { ITree, IFluidContainer } from "fluid-framework"; import React from "react"; import ReactDOM from "react-dom"; @@ -24,9 +28,10 @@ async function start(): Promise { let itemId: string = location.hash.slice(1); const createNew = itemId.length === 0; let container: IFluidContainer; + let createFn: OdspContainerAttachFunctor | undefined; if (createNew) { - ({ container } = await createFluidData(containerSchema)); + ({ container, createFn } = await createFluidData(containerSchema)); } else { ({ container } = await loadFluidData(itemId, containerSchema)); } @@ -95,7 +100,9 @@ async function start(): Promise { // If the app is in a `createNew` state - no itemId, and the container is detached, we attach the container. // This uploads the container to the service and connects to the collaboration session. - itemId = await container.attach({ filePath: "foo/bar", fileName: "shared-tree-demo" }); + assert(createFn !== undefined, "createFn is undefined"); + const res = await createFn({ filePath: "foo/bar", fileName: "shared-tree-demo" }); + itemId = res.itemId; // The newly attached container is given a unique ID that can be used to access the container in another session // eslint-disable-next-line require-atomic-updates diff --git a/examples/utils/bundle-size-tests/src/odspClient.ts b/examples/utils/bundle-size-tests/src/odspClient.ts index 89df0d209837..65c86f221590 100644 --- a/examples/utils/bundle-size-tests/src/odspClient.ts +++ b/examples/utils/bundle-size-tests/src/odspClient.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. */ -import { OdspClient } from "@fluidframework/odsp-client/internal"; +import { createOdspClient } from "@fluidframework/odsp-client/internal"; export function apisToBundle() { - new OdspClient({} as any); + createOdspClient({} as any); } diff --git a/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md b/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md index 45699779bda1..36eef7c19b5e 100644 --- a/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md +++ b/packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md @@ -7,7 +7,7 @@ // @alpha (undocumented) export type CacheContentType = "snapshot" | "ops" | "snapshotWithLoadingGroupId"; -// @alpha (undocumented) +// @alpha export interface HostStoragePolicy { avoidPrefetchSnapshotCache?: boolean; cacheCreateNewSummary?: boolean; @@ -58,6 +58,19 @@ export interface IFileEntry { resolvedUrl: IResolvedUrl; } +// @alpha +export type IOdspCreateArgs = { + siteUrl: string; + driveId: string; + isClpCompliantApp?: boolean; +} & ({ + itemId: string; +} | { + filePath?: string; + fileName: string; + createShareLinkType?: ISharingLinkKind; +}); + // @alpha export interface IOdspError extends Omit, IOdspErrorAugmentations { // (undocumented) @@ -71,15 +84,24 @@ export interface IOdspErrorAugmentations { serverEpoch?: string; } +// @alpha +export interface IOdspOpenArgs { + driveId: string; + fileVersion: string | undefined; + isClpCompliantApp?: boolean; + itemId: string; + sharingLinkToRedeem?: string; + siteUrl: string; + summarizer: boolean; +} + // @alpha (undocumented) export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { // (undocumented) codeHint?: { containerPackageName?: string; }; - // (undocumented) dataStorePath?: string; - // (undocumented) endpoints: { snapshotStorageUrl: string; attachmentPOSTStorageUrl: string; @@ -88,18 +110,14 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { }; // (undocumented) fileName: string; - // (undocumented) + filePath?: string; fileVersion: string | undefined; - // (undocumented) hashedDocumentId: string; - // (undocumented) isClpCompliantApp?: boolean; // (undocumented) odspResolvedUrl: true; shareLinkInfo?: ShareLinkInfoType; - // (undocumented) summarizer: boolean; - // (undocumented) tokens: {}; // (undocumented) type: "fluid"; @@ -107,13 +125,10 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { url: string; } -// @alpha (undocumented) +// @alpha export interface IOdspUrlParts { - // (undocumented) driveId: string; - // (undocumented) itemId: string; - // (undocumented) siteUrl: string; } @@ -233,6 +248,7 @@ export interface OdspResourceTokenFetchOptions extends TokenFetchOptions { // @alpha export interface ShareLinkInfoType { createLink?: { + createKind: ISharingLinkKind; link?: ISharingLink; error?: any; shareId?: string; diff --git a/packages/drivers/odsp-driver-definitions/package.json b/packages/drivers/odsp-driver-definitions/package.json index d3dc34839e44..cfce975e2f79 100644 --- a/packages/drivers/odsp-driver-definitions/package.json +++ b/packages/drivers/odsp-driver-definitions/package.json @@ -108,6 +108,13 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {} + "broken": { + "Interface_ShareLinkInfoType": { + "forwardCompat": false + }, + "Interface_IOdspResolvedUrl": { + "forwardCompat": false + } + } } } diff --git a/packages/drivers/odsp-driver-definitions/src/factory.ts b/packages/drivers/odsp-driver-definitions/src/factory.ts index cc579baacd12..0097a1fec3ab 100644 --- a/packages/drivers/odsp-driver-definitions/src/factory.ts +++ b/packages/drivers/odsp-driver-definitions/src/factory.ts @@ -86,6 +86,7 @@ export interface ICollabSessionOptions { } /** + * Various policies controlling behavior of ODSP driver * @legacy * @alpha */ diff --git a/packages/drivers/odsp-driver-definitions/src/index.ts b/packages/drivers/odsp-driver-definitions/src/index.ts index 847555848019..407c539b49ac 100644 --- a/packages/drivers/odsp-driver-definitions/src/index.ts +++ b/packages/drivers/odsp-driver-definitions/src/index.ts @@ -23,6 +23,8 @@ export { } from "./odspCache.js"; export { IOdspResolvedUrl, + IOdspOpenArgs, + IOdspCreateArgs, IOdspUrlParts, ISharingLink, ISharingLinkKind, diff --git a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts index 6b1a23096b27..94154ab05df4 100644 --- a/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts +++ b/packages/drivers/odsp-driver-definitions/src/resolvedUrl.ts @@ -6,12 +6,25 @@ import { IResolvedUrl } from "@fluidframework/driver-definitions/internal"; /** + * Identifies a file in SharePoint. + * This is required information to do any Graph / Vroom REST API calls. * @legacy * @alpha */ export interface IOdspUrlParts { + /** + * Site URL where file is located + */ siteUrl: string; + + /** + * driveId where file is located. + */ driveId: string; + + /** + * itemId within a drive that identifies a file. + */ itemId: string; } @@ -77,6 +90,12 @@ export interface ShareLinkInfoType { * from the /snapshot api response. */ createLink?: { + /** + * Kind of the link requested at creation time. + * Should be equal to the value in {@link ShareLinkInfoType.createLink.link} property, but may differ if ODSP created different type of link + */ + createKind: ISharingLinkKind; + /** * Share link created when the file is created for the first time with /snapshot api call. */ @@ -96,6 +115,7 @@ export interface ShareLinkInfoType { */ sharingLinkToRedeem?: string; } + /** * @legacy * @alpha @@ -107,9 +127,14 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { // URL to send to fluid, contains the documentId and the path url: string; - // A hashed identifier that is unique to this document + /** + * A hashed identifier that is unique to this document + */ hashedDocumentId: string; + /** + * Endpoints for various REST calls + */ endpoints: { snapshotStorageUrl: string; attachmentPOSTStorageUrl: string; @@ -117,22 +142,41 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { deltaStorageUrl: string; }; - // Tokens are not obtained by the ODSP driver using the resolve flow, the app must provide them. + /** + * Tokens are not obtained by the ODSP driver using the resolve flow, the app must provide them. + */ // eslint-disable-next-line @typescript-eslint/ban-types tokens: {}; fileName: string; + /** + * Path to a file. Required on file creation path. Not used on file open path. + */ + filePath?: string; + + /** + * Tells driver if a given container instance is a summarizer instance. + */ summarizer: boolean; + /* + * Contains hints to the application on what code version to load to interact with data model. + * containerPackageName property is used for adding the package name to the request headers. + * This may be used for preloading the container package when loading Fluid content. + */ codeHint?: { - // containerPackageName is used for adding the package name to the request headers. - // This may be used for preloading the container package when loading Fluid content. containerPackageName?: string; }; + /** + * If provided, tells version of a file to open + */ fileVersion: string | undefined; + /** + * This field can be used by the application code to create deep links into document + */ dataStorePath?: string; /** @@ -142,5 +186,97 @@ export interface IOdspResolvedUrl extends IResolvedUrl, IOdspUrlParts { */ shareLinkInfo?: ShareLinkInfoType; + /** + * Should be set to true only by application that is CLP compliant, for CLP compliant workflow. + * This argument has no impact if application is not properly registered with Sharepoint. + */ isClpCompliantApp?: boolean; } + +/** + * A set of inputs to the driver for file open scenarios. + * @legacy + * @alpha + */ +export interface IOdspOpenArgs { + /** + * {@inheritDoc (IOdspUrlParts:interface).siteUrl} + */ + siteUrl: string; + + /** + * {@inheritDoc (IOdspUrlParts:interface).driveId} + */ + driveId: string; + + /** + * {@inheritDoc (IOdspUrlParts:interface).itemId} + */ + itemId: string; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).summarizer} + */ + summarizer: boolean; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).fileVersion} + */ + fileVersion: string | undefined; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).isClpCompliantApp} + */ + isClpCompliantApp?: boolean; + + /** + * {@inheritDoc (ShareLinkInfoType:interface).sharingLinkToRedeem} + */ + sharingLinkToRedeem?: string; +} + +/** + * A set of inputs for the driver to create a new file or a FF partition on existing file. + * @legacy + * @alpha + */ +export type IOdspCreateArgs = { + /** + * {@inheritDoc (IOdspUrlParts:interface).siteUrl} + */ + siteUrl: string; + + /** + * {@inheritDoc (IOdspUrlParts:interface).driveId} + */ + driveId: string; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).isClpCompliantApp} + */ + isClpCompliantApp?: boolean; +} & ( + | { + /** + * Microsoft internal only. Creates alternate partition with FF content. + * Can be only used for a files that provisioned to support FF protocol on alternative paritions. + * {@link (IOdspUrlParts:interface).itemId} + */ + itemId: string; + } + | { + /** + * Path to a file within site. If not provided, files will be created in the root of the collection. + */ + filePath?: string; + + /** + * {@inheritDoc (IOdspResolvedUrl:interface).fileName} + */ + fileName: string; + /** + * Instructs ODSP to create a sharing link as part of file creation. + */ + createShareLinkType?: ISharingLinkKind; + } +); diff --git a/packages/drivers/odsp-driver-definitions/src/test/types/validateOdspDriverDefinitionsPrevious.generated.ts b/packages/drivers/odsp-driver-definitions/src/test/types/validateOdspDriverDefinitionsPrevious.generated.ts index 1e3527c4653e..fb07d949c8db 100644 --- a/packages/drivers/odsp-driver-definitions/src/test/types/validateOdspDriverDefinitionsPrevious.generated.ts +++ b/packages/drivers/odsp-driver-definitions/src/test/types/validateOdspDriverDefinitionsPrevious.generated.ts @@ -166,6 +166,7 @@ declare type current_as_old_for_Interface_IOdspErrorAugmentations = requireAssig * typeValidation.broken: * "Interface_IOdspResolvedUrl": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_IOdspResolvedUrl = requireAssignableTo, TypeOnly> /* @@ -463,6 +464,7 @@ declare type current_as_old_for_Interface_OdspResourceTokenFetchOptions = requir * typeValidation.broken: * "Interface_ShareLinkInfoType": {"forwardCompat": false} */ +// @ts-expect-error compatibility expected to be broken declare type old_as_current_for_Interface_ShareLinkInfoType = requireAssignableTo, TypeOnly> /* diff --git a/packages/drivers/odsp-driver/src/createFile.ts b/packages/drivers/odsp-driver/src/createFile.ts index 329b005d33c3..7985792f4987 100644 --- a/packages/drivers/odsp-driver/src/createFile.ts +++ b/packages/drivers/odsp-driver/src/createFile.ts @@ -13,6 +13,7 @@ import { InstrumentedStorageTokenFetcher, OdspErrorTypes, ShareLinkInfoType, + ISharingLinkKind, } from "@fluidframework/odsp-driver-definitions/internal"; import { ITelemetryLoggerExt, @@ -90,7 +91,10 @@ export async function createNewFluidFile( itemId = content.itemId; summaryHandle = content.id; - shareLinkInfo = extractShareLinkData(content, enableSingleRequestForShareLinkWithCreate); + shareLinkInfo = + !enableSingleRequestForShareLinkWithCreate || newFileInfo.createLinkType === undefined + ? undefined + : extractShareLinkData(content, newFileInfo.createLinkType); } const odspUrl = createOdspUrl({ ...newFileInfo, itemId, dataStorePath: "/" }); @@ -129,38 +133,36 @@ export async function createNewFluidFile( * the response if it is available. * In case there was an error in creation of the sharing link, error is provided back in the response, * and does not impact the creation of file in ODSP. - * @param requestedSharingLinkKind - Kind of sharing link requested to be created along with the creation of file. * @param response - Response object received from the /snapshot api call + * @param createKind - kind of link client asked for * @returns Sharing link information received in the response from a successful creation of a file. */ function extractShareLinkData( response: ICreateFileResponse, - enableSingleRequestForShareLinkWithCreate?: boolean, + createKind: ISharingLinkKind, ): ShareLinkInfoType | undefined { - let shareLinkInfo: ShareLinkInfoType | undefined; - if (enableSingleRequestForShareLinkWithCreate) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { sharing } = response; - if (!sharing) { - return; - } - /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ - shareLinkInfo = { - createLink: { - link: sharing.sharingLink - ? { - scope: sharing.sharingLink.scope, - role: sharing.sharingLink.type, - webUrl: sharing.sharingLink.webUrl, - ...sharing.sharingLink, - } - : undefined, - error: sharing.error, - shareId: sharing.shareId, - }, - }; - /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { sharing } = response; + if (!sharing) { + return; } + /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ + const shareLinkInfo: ShareLinkInfoType = { + createLink: { + createKind, + link: sharing.sharingLink + ? { + scope: sharing.sharingLink.scope, + role: sharing.sharingLink.type, + webUrl: sharing.sharingLink.webUrl, + ...sharing.sharingLink, + } + : undefined, + error: sharing.error, + shareId: sharing.shareId, + }, + }; + /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ return shareLinkInfo; } diff --git a/packages/drivers/odsp-driver/src/createOdspUrl.ts b/packages/drivers/odsp-driver/src/createOdspUrl.ts index 53e69214cbae..e662e245721d 100644 --- a/packages/drivers/odsp-driver/src/createOdspUrl.ts +++ b/packages/drivers/odsp-driver/src/createOdspUrl.ts @@ -10,7 +10,7 @@ import { OdspFluidDataStoreLocator } from "./contractsPublic.js"; */ /** - * Encodes ODC/SPO information into a URL format that can be handled by the Loader + * Encodes ODC/SPO information into a URL format that can be parsed later by decodeOdspUrl(). * @param l -The property bag of necessary properties to locate a Fluid data store and craft a url for it * @legacy * @alpha diff --git a/packages/drivers/odsp-driver/src/index.ts b/packages/drivers/odsp-driver/src/index.ts index a8a661ea3ac6..3036b42c802d 100644 --- a/packages/drivers/odsp-driver/src/index.ts +++ b/packages/drivers/odsp-driver/src/index.ts @@ -35,7 +35,11 @@ export { OdspDocumentServiceFactoryWithCodeSplit } from "./odspDocumentServiceFa export { createOdspCreateContainerRequest } from "./createOdspCreateContainerRequest.js"; // URI Resolver functionality, URI management -export { OdspDriverUrlResolver } from "./odspDriverUrlResolver.js"; +export { + OdspDriverUrlResolver, + createOpenOdspResolvedUrl, + createCreateOdspResolvedUrl, +} from "./odspDriverUrlResolver.js"; export { OdspDriverUrlResolverForShareLink, ShareLinkFetcherProps, diff --git a/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts b/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts index 3f4fc9b044d9..84c6ef4256bb 100644 --- a/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts +++ b/packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts @@ -24,8 +24,6 @@ import { ISharingLinkKind, ISocketStorageDiscovery, OdspResourceTokenFetchOptions, - SharingLinkRole, - SharingLinkScope, TokenFetchOptions, TokenFetcher, } from "@fluidframework/odsp-driver-definitions/internal"; @@ -105,7 +103,7 @@ export class OdspDocumentServiceFactoryCore }; let fileInfo: INewFileInfo | IExistingFileInfo; - let createShareLinkParam: ISharingLinkKind | undefined; + let createLinkType: ISharingLinkKind | undefined; if (odspResolvedUrl.itemId) { fileInfo = { type: "Existing", @@ -114,20 +112,22 @@ export class OdspDocumentServiceFactoryCore itemId: odspResolvedUrl.itemId, }; } else if (odspResolvedUrl.fileName) { - const [, queryString] = odspResolvedUrl.url.split("?"); - const searchParams = new URLSearchParams(queryString); - const filePath = searchParams.get("path"); + const filePath = odspResolvedUrl.filePath; if (filePath === undefined || filePath === null) { throw new Error("File path should be provided!!"); } - createShareLinkParam = getSharingLinkParams(this.hostPolicy, searchParams); + + createLinkType = this.hostPolicy.enableSingleRequestForShareLinkWithCreate + ? odspResolvedUrl.shareLinkInfo?.createLink?.createKind + : undefined; + fileInfo = { type: "New", driveId: odspResolvedUrl.driveId, siteUrl: odspResolvedUrl.siteUrl, filePath, filename: odspResolvedUrl.fileName, - createLinkType: createShareLinkParam, + createLinkType, }; } else { throw new Error("A new or existing file must be specified to create container!"); @@ -161,9 +161,7 @@ export class OdspDocumentServiceFactoryCore { eventName: "CreateNew", isWithSummaryUpload: true, - createShareLinkParam: createShareLinkParam - ? JSON.stringify(createShareLinkParam) - : undefined, + createShareLinkParam: createLinkType ? JSON.stringify(createLinkType) : undefined, enableSingleRequestForShareLinkWithCreate: this.hostPolicy.enableSingleRequestForShareLinkWithCreate, }, @@ -323,29 +321,3 @@ export class OdspDocumentServiceFactoryCore ); } } - -/** - * Extract the sharing link kind from the resolved URL's query paramerters - */ -function getSharingLinkParams( - hostPolicy: HostStoragePolicy, - searchParams: URLSearchParams, -): ISharingLinkKind | undefined { - // extract request parameters for creation of sharing link (if provided) if the feature is enabled - let createShareLinkParam: ISharingLinkKind | undefined; - if (hostPolicy.enableSingleRequestForShareLinkWithCreate) { - const createLinkScope = searchParams.get("createLinkScope"); - const createLinkRole = searchParams.get("createLinkRole"); - if (createLinkScope && SharingLinkScope[createLinkScope]) { - createShareLinkParam = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - scope: SharingLinkScope[createLinkScope], - ...(createLinkRole && SharingLinkRole[createLinkRole] - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - { role: SharingLinkRole[createLinkRole] } - : {}), - }; - } - } - return createShareLinkParam; -} diff --git a/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts b/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts index 886346e7dbcc..8ae1cd25bc6e 100644 --- a/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts +++ b/packages/drivers/odsp-driver/src/odspDriverUrlResolver.ts @@ -14,11 +14,17 @@ import { import { NonRetryableError } from "@fluidframework/driver-utils/internal"; import { IOdspResolvedUrl, + IOdspOpenArgs, + IOdspCreateArgs, OdspErrorTypes, + SharingLinkRole, + SharingLinkScope, + ISharingLinkKind, } from "@fluidframework/odsp-driver-definitions/internal"; import { ClpCompliantAppHeader } from "./contractsPublic.js"; import { createOdspUrl } from "./createOdspUrl.js"; +import { locatorQueryParamName } from "./odspFluidFileLink.js"; import { getHashedDocumentId } from "./odspPublicUtils.js"; import { getApiRoot } from "./odspUrlHelper.js"; import { getOdspResolvedUrl } from "./odspUtils.js"; @@ -90,6 +96,147 @@ function removeBeginningSlash(str: string): string { const isFluidPackage = (pkg: Record): boolean => typeof pkg === "object" && typeof pkg?.name === "string" && typeof pkg?.fluid === "object"; +export function removeNavParam(link: string): string { + const url = new URL(link); + const params = new URLSearchParams(url.search); + params.delete(locatorQueryParamName); + url.search = params.toString(); + return url.href; +} + +/** + * Generates IOdspResolvedUrl.rul value. + * @param hashedDocumentId - documentId + * @param dataStorePath - data store path + * @returns url + */ +function getUrlForOdspResolvedUrl(hashedDocumentId: string, dataStorePath: string): string { + return `https://placeholder/placeholder/${hashedDocumentId}/${removeBeginningSlash(dataStorePath)}`; +} + +/** + * Creates IOdspResolvedUrl from IOdspOpenArgs + * @param input - IOdspOpenArgs + * @returns IOdspResolvedUrl + * @alpha + */ +export async function createOpenOdspResolvedUrl( + input: IOdspOpenArgs, +): Promise { + const { siteUrl, driveId, itemId, fileVersion } = input; + + const hashedDocumentId = await getHashedDocumentId(driveId, itemId); + assert(!hashedDocumentId.includes("/"), 0x0a8 /* "Docid should not contain slashes!!" */); + + // We need to remove the nav param if set by host when setting the sharelink as otherwise the shareLinkId + // when redeeming the share link during the redeem fallback for trees latest call becomes greater than + // the eligible length. + const sharingLinkToRedeem = + input.sharingLinkToRedeem === undefined + ? undefined + : removeNavParam(input.sharingLinkToRedeem); + + const endpoints = { + snapshotStorageUrl: getSnapshotUrl(siteUrl, driveId, itemId, fileVersion), + attachmentPOSTStorageUrl: getAttachmentPOSTUrl(siteUrl, driveId, itemId, fileVersion), + attachmentGETStorageUrl: getAttachmentGETUrl(siteUrl, driveId, itemId, fileVersion), + deltaStorageUrl: getDeltaStorageUrl(siteUrl, driveId, itemId, fileVersion), + }; + + const dataStorePath = ""; + + return { + ...input, + + type: "fluid", + odspResolvedUrl: true, + + id: hashedDocumentId, + hashedDocumentId, + + tokens: {}, + + // only used in create-new flows + fileName: "", + + url: getUrlForOdspResolvedUrl(hashedDocumentId, dataStorePath), + + endpoints, + + shareLinkInfo: { + sharingLinkToRedeem, + }, + + dataStorePath, + }; +} + +/** + * Creates IOdspResolvedUrl from IOdspOpenArgs + * @param input - IOdspOpenArgs + * @returns IOdspResolvedUrl + * @alpha + */ +export function createCreateOdspResolvedUrl(input: IOdspCreateArgs): IOdspResolvedUrl { + const { + siteUrl, + itemId = "", + fileName = "", + filePath = "", + createShareLinkType, + } = input as IOdspCreateArgs & { + createShareLinkType?: ISharingLinkKind; + } & ( + | { + itemId: string; + fileName?: undefined; + filePath?: undefined; + } + | { + itemId?: undefined; + filePath: string; + fileName: string; + } + ); + + return { + ...input, + + type: "fluid", + odspResolvedUrl: true, + summarizer: false, + + id: "odspCreateNew", + hashedDocumentId: "", + + tokens: {}, + + fileVersion: undefined, + + itemId, + filePath, + fileName, + + url: `https://${siteUrl}?&version=null`, + + endpoints: { + snapshotStorageUrl: "", + attachmentGETStorageUrl: "", + attachmentPOSTStorageUrl: "", + deltaStorageUrl: "", + }, + + shareLinkInfo: + createShareLinkType === undefined + ? undefined + : { + createLink: { + createKind: createShareLinkType, + }, + }, + }; +} + /** * Resolver to resolve urls like the ones created by createOdspUrl which is driver inner * url format. Ex: `${siteUrl}?driveId=${driveId}&itemId=${itemId}&path=${path}` @@ -110,6 +257,7 @@ export class OdspDriverUrlResolver implements IUrlResolver { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const fileName: string = request.headers[DriverHeader.createNew].fileName; const driveID = searchParams.get("driveId"); + // Be carefully here - this is filePath (see createOdspCreateContainerRequest()), not dataStorePath (see createOdspUrl()) const filePath = searchParams.get("path"); const packageName = searchParams.get("containerPackageName"); // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- false positive @@ -120,66 +268,50 @@ export class OdspDriverUrlResolver implements IUrlResolver { { driverVersion: pkgVersion }, ); } - return { - endpoints: { - snapshotStorageUrl: "", - attachmentGETStorageUrl: "", - attachmentPOSTStorageUrl: "", - deltaStorageUrl: "", - }, - tokens: {}, - type: "fluid", - odspResolvedUrl: true, - id: "odspCreateNew", - url: `https://${siteURL}?${queryString}&version=null`, + + const createKind = getSharingLinkParams(searchParams); + + const createResolvedUrl = createCreateOdspResolvedUrl({ siteUrl: siteURL, - hashedDocumentId: "", driveId: driveID, - itemId: "", + filePath, fileName, - summarizer: false, + createShareLinkType: createKind, + isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], + }); + return { + ...createResolvedUrl, codeHint: { containerPackageName: packageName ?? undefined, }, - fileVersion: undefined, - shareLinkInfo: undefined, - isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], }; } - const { siteUrl, driveId, itemId, path, containerPackageName, fileVersion } = - decodeOdspUrl(request.url); - const hashedDocumentId = await getHashedDocumentId(driveId, itemId); - assert(!hashedDocumentId.includes("/"), 0x0a8 /* "Docid should not contain slashes!!" */); - - const documentUrl = `https://placeholder/placeholder/${hashedDocumentId}/${removeBeginningSlash( - path, - )}`; + const { + siteUrl, + driveId, + itemId, + path: dataStorePath, + containerPackageName, + fileVersion, + } = decodeOdspUrl(request.url); const summarizer = !!request.headers?.[DriverHeader.summarizingClient]; - return { - type: "fluid", - odspResolvedUrl: true, - endpoints: { - snapshotStorageUrl: getSnapshotUrl(siteUrl, driveId, itemId, fileVersion), - attachmentPOSTStorageUrl: getAttachmentPOSTUrl(siteUrl, driveId, itemId, fileVersion), - attachmentGETStorageUrl: getAttachmentGETUrl(siteUrl, driveId, itemId, fileVersion), - deltaStorageUrl: getDeltaStorageUrl(siteUrl, driveId, itemId, fileVersion), - }, - id: hashedDocumentId, - tokens: {}, - url: documentUrl, - hashedDocumentId, + const openResolvedUrl = await createOpenOdspResolvedUrl({ siteUrl, driveId, itemId, - dataStorePath: path, - fileName: "", summarizer, + fileVersion, + isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], + }); + + return { + ...openResolvedUrl, + dataStorePath, + url: getUrlForOdspResolvedUrl(openResolvedUrl.hashedDocumentId, dataStorePath), codeHint: { containerPackageName, }, - fileVersion, - isClpCompliantApp: request.headers?.[ClpCompliantAppHeader.isClpCompliantApp], }; } @@ -231,10 +363,19 @@ export class OdspDriverUrlResolver implements IUrlResolver { } } +/** + * Decodes URL created by createOdspUrl() + * @param url - Url to decode + * @returns a structure with all decoded fields + */ export function decodeOdspUrl(url: string): { siteUrl: string; driveId: string; itemId: string; + /** + * Note - path is the OdspFluidDataStoreLocator.dataStorePath ! + * Not filePath + */ path: string; containerPackageName?: string; fileVersion?: string; @@ -273,3 +414,23 @@ export function decodeOdspUrl(url: string): { fileVersion: fileVersion ? decodeURIComponent(fileVersion) : undefined, }; } + +/** + * Extract the sharing link kind from the resolved URL's query paramerters + */ +function getSharingLinkParams(searchParams: URLSearchParams): ISharingLinkKind | undefined { + // extract request parameters for creation of sharing link (if provided) if the feature is enabled + const createLinkScope = searchParams.get("createLinkScope"); + const createLinkRole = searchParams.get("createLinkRole"); + if (createLinkScope && SharingLinkScope[createLinkScope]) { + return { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + scope: SharingLinkScope[createLinkScope], + ...(createLinkRole && SharingLinkRole[createLinkRole] + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + { role: SharingLinkRole[createLinkRole] } + : {}), + }; + } + return undefined; +} diff --git a/packages/drivers/odsp-driver/src/odspDriverUrlResolverForShareLink.ts b/packages/drivers/odsp-driver/src/odspDriverUrlResolverForShareLink.ts index f1d01ff66a37..9a6cc3d27e27 100644 --- a/packages/drivers/odsp-driver/src/odspDriverUrlResolverForShareLink.ts +++ b/packages/drivers/odsp-driver/src/odspDriverUrlResolverForShareLink.ts @@ -21,12 +21,8 @@ import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal"; import { OdspFluidDataStoreLocator, SharingLinkHeader } from "./contractsPublic.js"; import { createOdspUrl } from "./createOdspUrl.js"; import { getFileLink } from "./getFileLink.js"; -import { OdspDriverUrlResolver } from "./odspDriverUrlResolver.js"; -import { - getLocatorFromOdspUrl, - locatorQueryParamName, - storeLocatorInOdspUrl, -} from "./odspFluidFileLink.js"; +import { OdspDriverUrlResolver, removeNavParam } from "./odspDriverUrlResolver.js"; +import { getLocatorFromOdspUrl, storeLocatorInOdspUrl } from "./odspFluidFileLink.js"; import { createOdspLogger, getOdspResolvedUrl } from "./odspUtils.js"; /** @@ -152,7 +148,7 @@ export class OdspDriverUrlResolverForShareLink implements IUrlResolver { // when redeeming the share link during the redeem fallback for trees latest call becomes greater than // the eligible length. odspResolvedUrl.shareLinkInfo = Object.assign(odspResolvedUrl.shareLinkInfo ?? {}, { - sharingLinkToRedeem: this.removeNavParam(request.url), + sharingLinkToRedeem: removeNavParam(request.url), }); } if (odspResolvedUrl.itemId) { @@ -163,14 +159,6 @@ export class OdspDriverUrlResolverForShareLink implements IUrlResolver { return odspResolvedUrl; } - private removeNavParam(link: string): string { - const url = new URL(link); - const params = new URLSearchParams(url.search); - params.delete(locatorQueryParamName); - url.search = params.toString(); - return url.href; - } - private async getShareLinkPromise(resolvedUrl: IOdspResolvedUrl): Promise { if (this.shareLinkFetcherProps === undefined) { throw new Error("Failed to get share link because share link fetcher props are missing"); diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md index 699ce0f5bdbe..4812cbec1df8 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md @@ -53,9 +53,6 @@ export namespace ConnectionStateType { // @public export type ConnectionStateType = ConnectionStateType.Disconnected | ConnectionStateType.EstablishingConnection | ConnectionStateType.CatchingUp | ConnectionStateType.Connected; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -333,7 +330,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionStateType; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md index 921e843a5e30..8f30677331bd 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md @@ -53,9 +53,6 @@ export namespace ConnectionStateType { // @public export type ConnectionStateType = ConnectionStateType.Disconnected | ConnectionStateType.EstablishingConnection | ConnectionStateType.CatchingUp | ConnectionStateType.Connected; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -371,7 +368,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionStateType; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md index 4359632909b6..08103386a105 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md @@ -53,9 +53,6 @@ export namespace ConnectionStateType { // @public export type ConnectionStateType = ConnectionStateType.Disconnected | ConnectionStateType.EstablishingConnection | ConnectionStateType.CatchingUp | ConnectionStateType.Connected; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -361,7 +358,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionStateType; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md index d2c098c4278a..42f7282864aa 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md @@ -53,9 +53,6 @@ export namespace ConnectionStateType { // @public export type ConnectionStateType = ConnectionStateType.Disconnected | ConnectionStateType.EstablishingConnection | ConnectionStateType.CatchingUp | ConnectionStateType.Connected; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -333,7 +330,7 @@ export type IEventTransformer = TEvent extends { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionStateType; diff --git a/packages/framework/fluid-framework/src/index.ts b/packages/framework/fluid-framework/src/index.ts index dc1f162a9936..16e05ce27556 100644 --- a/packages/framework/fluid-framework/src/index.ts +++ b/packages/framework/fluid-framework/src/index.ts @@ -21,7 +21,6 @@ export type { export { AttachState } from "@fluidframework/container-definitions"; export { ConnectionState } from "@fluidframework/container-loader"; export type { - ContainerAttachProps, ContainerSchema, IConnection, IFluidContainer, diff --git a/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md b/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md index 3e583349f1a5..e15560eedb22 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.alpha.api.md @@ -7,9 +7,6 @@ // @public export type CompatibilityMode = "1" | "2"; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -24,7 +21,7 @@ export interface IConnection { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionState; diff --git a/packages/framework/fluid-static/api-report/fluid-static.beta.api.md b/packages/framework/fluid-static/api-report/fluid-static.beta.api.md index d79be0f31535..aabc41887525 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.beta.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.beta.api.md @@ -7,9 +7,6 @@ // @public export type CompatibilityMode = "1" | "2"; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -24,7 +21,7 @@ export interface IConnection { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionState; diff --git a/packages/framework/fluid-static/api-report/fluid-static.public.api.md b/packages/framework/fluid-static/api-report/fluid-static.public.api.md index 35d6cf0a0dc8..0349305c63f1 100644 --- a/packages/framework/fluid-static/api-report/fluid-static.public.api.md +++ b/packages/framework/fluid-static/api-report/fluid-static.public.api.md @@ -7,9 +7,6 @@ // @public export type CompatibilityMode = "1" | "2"; -// @public -export type ContainerAttachProps = T; - // @public export interface ContainerSchema { readonly dynamicObjectTypes?: readonly SharedObjectKind[]; @@ -24,7 +21,7 @@ export interface IConnection { // @public @sealed export interface IFluidContainer extends IEventProvider { - attach(props?: ContainerAttachProps): Promise; + attach(props?: unknown): Promise; readonly attachState: AttachState; connect(): void; readonly connectionState: ConnectionState; diff --git a/packages/framework/fluid-static/package.json b/packages/framework/fluid-static/package.json index c2ba993e0f47..e9c354dca2d9 100644 --- a/packages/framework/fluid-static/package.json +++ b/packages/framework/fluid-static/package.json @@ -139,6 +139,11 @@ "typescript": "~5.4.5" }, "typeValidation": { - "broken": {} + "broken": { + "RemovedTypeAlias_ContainerAttachProps": { + "forwardCompat": false, + "backCompat": false + } + } } } diff --git a/packages/framework/fluid-static/src/fluidContainer.ts b/packages/framework/fluid-static/src/fluidContainer.ts index 6eee0e775c34..b1a10e347d3b 100644 --- a/packages/framework/fluid-static/src/fluidContainer.ts +++ b/packages/framework/fluid-static/src/fluidContainer.ts @@ -13,7 +13,7 @@ import type { IContainer } from "@fluidframework/container-definitions/internal" import type { IEvent, IEventProvider, IFluidLoadable } from "@fluidframework/core-interfaces"; import type { SharedObjectKind } from "@fluidframework/shared-object-base"; -import type { ContainerAttachProps, ContainerSchema, IRootDataObject } from "./types.js"; +import type { ContainerSchema, IRootDataObject } from "./types.js"; /** * Extract the type of 'initialObjects' from the given {@link ContainerSchema} type. @@ -170,7 +170,7 @@ export interface IFluidContainer; + attach(props?: unknown): Promise; /** * Attempts to connect the container to the delta stream and process operations. @@ -323,7 +323,7 @@ class FluidContainer * The reason is because externally we are presenting a separation between the service and the `FluidContainer`, * but internally this separation is not there. */ - public async attach(props?: ContainerAttachProps): Promise { + public async attach(param?: unknown): Promise { if (this.container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state."); } diff --git a/packages/framework/fluid-static/src/index.ts b/packages/framework/fluid-static/src/index.ts index b101d4425ffd..cdef127e24aa 100644 --- a/packages/framework/fluid-static/src/index.ts +++ b/packages/framework/fluid-static/src/index.ts @@ -20,7 +20,6 @@ export { createServiceAudience } from "./serviceAudience.js"; export type { CompatibilityMode, ContainerSchema, - ContainerAttachProps, IConnection, IMember, IProvideRootDataObject, diff --git a/packages/framework/fluid-static/src/test/types/validateFluidStaticPrevious.generated.ts b/packages/framework/fluid-static/src/test/types/validateFluidStaticPrevious.generated.ts index 26f692b78575..17593ceb3b21 100644 --- a/packages/framework/fluid-static/src/test/types/validateFluidStaticPrevious.generated.ts +++ b/packages/framework/fluid-static/src/test/types/validateFluidStaticPrevious.generated.ts @@ -38,18 +38,16 @@ declare type current_as_old_for_TypeAlias_CompatibilityMode = requireAssignableT * If this test starts failing, it indicates a change that is not forward compatible. * To acknowledge the breaking change, add the following to package.json under * typeValidation.broken: - * "TypeAlias_ContainerAttachProps": {"forwardCompat": false} + * "RemovedTypeAlias_ContainerAttachProps": {"forwardCompat": false} */ -declare type old_as_current_for_TypeAlias_ContainerAttachProps = requireAssignableTo, TypeOnly> /* * Validate backward compatibility by using the current type in place of the old type. * If this test starts failing, it indicates a change that is not backward compatible. * To acknowledge the breaking change, add the following to package.json under * typeValidation.broken: - * "TypeAlias_ContainerAttachProps": {"backCompat": false} + * "RemovedTypeAlias_ContainerAttachProps": {"backCompat": false} */ -declare type current_as_old_for_TypeAlias_ContainerAttachProps = requireAssignableTo, TypeOnly> /* * Validate forward compatibility by using the old type in place of the current type. diff --git a/packages/framework/fluid-static/src/types.ts b/packages/framework/fluid-static/src/types.ts index 0551103089c6..e251d139e902 100644 --- a/packages/framework/fluid-static/src/types.ts +++ b/packages/framework/fluid-static/src/types.ts @@ -57,12 +57,6 @@ export interface DataObjectClass { new (...args: any[]): T; } -/** - * Represents properties that can be attached to a container. - * @public - */ -export type ContainerAttachProps = T; - /** * Declares the Fluid objects that will be available in the {@link IFluidContainer | Container}. * diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts index 82964ed939c9..0f7a5826bfa8 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/OdspClientFactory.ts @@ -7,7 +7,11 @@ import { IConfigProviderBase, type ITelemetryBaseLogger, } from "@fluidframework/core-interfaces"; -import { OdspClient, OdspConnectionConfig } from "@fluidframework/odsp-client/internal"; +import { + type IOdspClient, + createOdspClient as createOdspClientCore, + OdspConnectionConfig, +} from "@fluidframework/odsp-client/internal"; import { MockLogger, createMultiSinkLogger } from "@fluidframework/telemetry-utils/internal"; import { OdspTestTokenProvider } from "./OdspTokenFactory.js"; @@ -107,13 +111,13 @@ export function getCredentials(): IOdspLoginCredentials[] { /** * This function will determine if local or remote mode is required (based on FLUID_CLIENT), and return a new - * {@link OdspClient} instance based on the mode by setting the Connection config accordingly. + * {@link IOdspClient} instance based on the mode by setting the Connection config accordingly. */ export function createOdspClient( creds: IOdspLoginCredentials, logger?: MockLogger, configProvider?: IConfigProviderBase, -): OdspClient { +): IOdspClient { const siteUrl = process.env.odsp__client__siteUrl as string; const driveId = process.env.odsp__client__driveId as string; const clientId = process.env.odsp__client__clientId as string; @@ -137,7 +141,6 @@ export function createOdspClient( siteUrl, tokenProvider: new OdspTestTokenProvider(credentials), driveId, - filePath: "", }; const getLogger = (): ITelemetryBaseLogger | undefined => { const testLogger = getTestLogger?.(); @@ -149,7 +152,7 @@ export function createOdspClient( } return logger ?? testLogger; }; - return new OdspClient({ + return createOdspClientCore({ connection: connectionProps, logger: getLogger(), configProvider, diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/audience.spec.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/test/audience.spec.ts index 9385a18945d8..9c544feb6029 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/audience.spec.ts +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/audience.spec.ts @@ -10,7 +10,7 @@ import { ConnectionState } from "@fluidframework/container-loader"; import { ConfigTypes, IConfigProviderBase } from "@fluidframework/core-interfaces"; import { ContainerSchema } from "@fluidframework/fluid-static"; import { SharedMap } from "@fluidframework/map/internal"; -import { OdspClient } from "@fluidframework/odsp-client/internal"; +import { type IOdspClient } from "@fluidframework/odsp-client/internal"; import { timeoutPromise } from "@fluidframework/test-utils/internal"; import { createOdspClient, getCredentials } from "./OdspClientFactory.js"; @@ -22,7 +22,7 @@ const configProvider = (settings: Record): IConfigProviderB describe("Fluid audience", () => { const connectTimeoutMs = 10_000; - let client: OdspClient; + let client: IOdspClient; let schema: ContainerSchema; const [client1Creds, client2Creds] = getCredentials(); diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts index d0dd8f2e2fcb..cc62bce44093 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/containerCreate.spec.ts @@ -9,14 +9,14 @@ import { AttachState } from "@fluidframework/container-definitions"; import { ConnectionState } from "@fluidframework/container-loader"; import { ContainerSchema } from "@fluidframework/fluid-static"; import { SharedMap } from "@fluidframework/map/internal"; -import { OdspClient } from "@fluidframework/odsp-client/internal"; +import { type IOdspClient } from "@fluidframework/odsp-client/internal"; import { timeoutPromise } from "@fluidframework/test-utils/internal"; import { createOdspClient, getCredentials } from "./OdspClientFactory.js"; describe("Container create scenarios", () => { const connectTimeoutMs = 10_000; - let client: OdspClient; + let client: IOdspClient; let schema: ContainerSchema; const [clientCreds] = getCredentials(); @@ -136,7 +136,7 @@ describe("Container create scenarios", () => { * Expected behavior: an error should be thrown when trying to get a non-existent container. */ it("cannot load improperly created container (cannot load a non-existent container)", async () => { - const containerAndServicesP = client.getContainer("containerConfig", schema); + const containerAndServicesP = client.getContainer("containerConfig" /* itemId */, schema); const errorFn = (error: Error): boolean => { assert.notStrictEqual(error.message, undefined, "Odsp Client error is undefined"); diff --git a/packages/service-clients/end-to-end-tests/odsp-client/src/test/ddsTests.spec.ts b/packages/service-clients/end-to-end-tests/odsp-client/src/test/ddsTests.spec.ts index 9e46b88e2a43..3bd4b9a7bf9d 100644 --- a/packages/service-clients/end-to-end-tests/odsp-client/src/test/ddsTests.spec.ts +++ b/packages/service-clients/end-to-end-tests/odsp-client/src/test/ddsTests.spec.ts @@ -9,7 +9,7 @@ import { ConnectionState } from "@fluidframework/container-loader"; import { IFluidHandle } from "@fluidframework/core-interfaces"; import { ContainerSchema } from "@fluidframework/fluid-static"; import { SharedMap } from "@fluidframework/map/internal"; -import { OdspClient } from "@fluidframework/odsp-client/internal"; +import { type IOdspClient } from "@fluidframework/odsp-client/internal"; import { timeoutPromise } from "@fluidframework/test-utils/internal"; import { createOdspClient, getCredentials } from "./OdspClientFactory.js"; @@ -18,7 +18,7 @@ import { mapWait } from "./utils.js"; describe("Fluid data updates", () => { const connectTimeoutMs = 10_000; - let client: OdspClient; + let client: IOdspClient; const schema = { initialObjects: { map1: SharedMap, diff --git a/packages/service-clients/odsp-client/README.md b/packages/service-clients/odsp-client/README.md index caeed45481ba..b8eee4ac851a 100644 --- a/packages/service-clients/odsp-client/README.md +++ b/packages/service-clients/odsp-client/README.md @@ -104,9 +104,11 @@ const containerSchema = { ], }; const odspClient = new OdspClient(clientProps); -const { container, services } = await odspClient.createContainer(containerSchema); +const { container, services, createFn } = await odspClient.createContainer(containerSchema); + +const response = await createFn(); +const itemId = response.itemId; -const itemId = await container.attach(); ``` ## Using Fluid Containers diff --git a/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md b/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md index e519c7569499..94c049c1ae35 100644 --- a/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md +++ b/packages/service-clients/odsp-client/api-report/odsp-client.alpha.api.md @@ -4,45 +4,72 @@ ```ts -// @beta -export type IOdspAudience = IServiceAudience; +// @alpha +export function createOdspClient(properties: OdspClientProps): IOdspClient; // @beta -export interface IOdspTokenProvider { - fetchStorageToken(siteUrl: string, refresh: boolean): Promise; - fetchWebsocketToken(siteUrl: string, refresh: boolean): Promise; -} +export type IOdspAudience = IServiceAudience; -// @beta @sealed -export class OdspClient { - constructor(properties: OdspClientProps); - // (undocumented) +// @alpha +export interface IOdspClient { createContainer(containerSchema: T): Promise<{ container: IFluidContainer; services: OdspContainerServices; + createFn: OdspContainerAttachFunctor; }>; - // (undocumented) - getContainer(id: string, containerSchema: T): Promise<{ + getContainer(itemId: string, containerSchema: T, options?: OdspContainerOpenOptions): Promise<{ container: IFluidContainer; services: OdspContainerServices; }>; } -// @beta (undocumented) +// @beta +export interface IOdspTokenProvider { + fetchStorageToken(siteUrl: string, refresh: boolean): Promise; + fetchWebsocketToken(siteUrl: string, refresh: boolean): Promise; +} + +// @alpha (undocumented) export interface OdspClientProps { readonly configProvider?: IConfigProviderBase; readonly connection: OdspConnectionConfig; + readonly hostPolicy?: HostStoragePolicy; readonly logger?: ITelemetryBaseLogger; + readonly persistedCache?: IPersistedCache; } // @beta export interface OdspConnectionConfig { driveId: string; - filePath: string; + isClpCompliant?: boolean; siteUrl: string; tokenProvider: IOdspTokenProvider; } +// @alpha +export type OdspContainerAttachArgs = { + filePath?: string; + fileName?: string; + createShareLinkType?: ISharingLinkKind; +} | { + itemId: string; +}; + +// @alpha +export type OdspContainerAttachFunctor = (param?: OdspContainerAttachArgs) => Promise; + +// @alpha +export interface OdspContainerAttachResult { + itemId: string; + shareLinkInfo?: ShareLinkInfoType; +} + +// @alpha +export interface OdspContainerOpenOptions { + fileVersion?: string; + sharingLinkToRedeem?: string; +} + // @beta export interface OdspContainerServices { audience: IOdspAudience; diff --git a/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md b/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md index 235a8f244202..c9f5d4cd4253 100644 --- a/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md +++ b/packages/service-clients/odsp-client/api-report/odsp-client.beta.api.md @@ -13,32 +13,10 @@ export interface IOdspTokenProvider { fetchWebsocketToken(siteUrl: string, refresh: boolean): Promise; } -// @beta @sealed -export class OdspClient { - constructor(properties: OdspClientProps); - // (undocumented) - createContainer(containerSchema: T): Promise<{ - container: IFluidContainer; - services: OdspContainerServices; - }>; - // (undocumented) - getContainer(id: string, containerSchema: T): Promise<{ - container: IFluidContainer; - services: OdspContainerServices; - }>; -} - -// @beta (undocumented) -export interface OdspClientProps { - readonly configProvider?: IConfigProviderBase; - readonly connection: OdspConnectionConfig; - readonly logger?: ITelemetryBaseLogger; -} - // @beta export interface OdspConnectionConfig { driveId: string; - filePath: string; + isClpCompliant?: boolean; siteUrl: string; tokenProvider: IOdspTokenProvider; } diff --git a/packages/service-clients/odsp-client/src/index.ts b/packages/service-clients/odsp-client/src/index.ts index 55c28a72e802..1f7eb75abc3b 100644 --- a/packages/service-clients/odsp-client/src/index.ts +++ b/packages/service-clients/odsp-client/src/index.ts @@ -20,6 +20,11 @@ export type { IOdspAudience, OdspMember, TokenResponse, + OdspContainerAttachArgs, + OdspContainerAttachFunctor, + OdspContainerAttachResult, + OdspContainerOpenOptions, + IOdspClient, } from "./interfaces.js"; -export { OdspClient } from "./odspClient.js"; +export { createOdspClient } from "./odspClient.js"; export { type IOdspTokenProvider } from "./token.js"; diff --git a/packages/service-clients/odsp-client/src/interfaces.ts b/packages/service-clients/odsp-client/src/interfaces.ts index 5b251ffb0301..60a7b672fd38 100644 --- a/packages/service-clients/odsp-client/src/interfaces.ts +++ b/packages/service-clients/odsp-client/src/interfaces.ts @@ -7,7 +7,18 @@ import type { IConfigProviderBase, ITelemetryBaseLogger, } from "@fluidframework/core-interfaces"; -import type { IMember, IServiceAudience } from "@fluidframework/fluid-static"; +import type { + IMember, + IServiceAudience, + ContainerSchema, + IFluidContainer, +} from "@fluidframework/fluid-static"; +import type { + ISharingLinkKind, + ShareLinkInfoType, + IPersistedCache, + HostStoragePolicy, +} from "@fluidframework/odsp-driver-definitions/internal"; import type { IOdspTokenProvider } from "./token.js"; @@ -34,12 +45,14 @@ export interface OdspConnectionConfig { driveId: string; /** - * Specifies the file path where Fluid files are created. If passed an empty string, the Fluid files will be created at the root level. + * Should be set to true only by application that is CLP compliant, for CLP compliant workflow. + * This argument has no impact if application is not properly registered with Sharepoint. */ - filePath: string; + isClpCompliant?: boolean; } + /** - * @beta + * @alpha */ export interface OdspClientProps { /** @@ -56,22 +69,101 @@ export interface OdspClientProps { * Base interface for providing configurations to control experimental features. If unsure, leave this undefined. */ readonly configProvider?: IConfigProviderBase; + + /** + * Optional. This interface can be implemented by the host to provide durable caching across sessions. + */ + readonly persistedCache?: IPersistedCache; + + /** + * Optional. Defines various policies controlling behavior of ODSP driver + */ + readonly hostPolicy?: HostStoragePolicy; } /** - * @legacy + * Specifies location / name of the file. + * If no argument is provided, file with random name (uuid) will be created. + * Please see {@link OdspContainerAttachFunctor} for more details * @alpha */ -export interface OdspContainerAttachProps { +export type OdspContainerAttachArgs = + | { + /** + * The file path where Fluid containers are created. If undefined, the file is created at the root. + */ + filePath?: string; + + /** + * The file name of the Fluid file. If undefined, the file is named with a GUID. + * If a file with such name exists, file with different name is created - Sharepoint will + * add (2), (3), ... to file name to make it unique and avoid conflict on creation. + */ + fileName?: string; + + /** + * If provided, will instrcuct Sharepoint to create a sharing link as part of file creation flow. + */ + createShareLinkType?: ISharingLinkKind; + } + | { + /** + * (Microsoft internal only) Files supporting FF format on alternate partition could point to existing file. + */ + itemId: string; + }; + +/** + * An object type returned by attach call. + * Please see {@link OdspContainerAttachFunctor} for more details + * @alpha + */ +export interface OdspContainerAttachResult { /** - * The file path where Fluid containers are created. If undefined, the file is created at the root. + * An ID of the document created. This ID could be passed to future IOdspClient.getContainer() call */ - filePath: string | undefined; + itemId: string; /** - * The file name of the Fluid file. If undefined, the file is named with a GUID. + * If OdspContainerAttachArgs.createShareLinkType was provided as part of OdspContainerAttachArgs payload, + * `shareLinkInfo` will contain sharing link information for created file. */ - fileName: string | undefined; + shareLinkInfo?: ShareLinkInfoType; +} + +/** + * Signature of the createFn callback returned by IOdspClient.createContainer(). + * Used to attach container to stroage (create container in storage). + * @param param - Specifies where file should be created and how it should be named. If not provided, + * file with random name (uuid) will be created in the root of the drive. + * @param options - options controlling creation. + * @alpha + */ +export type OdspContainerAttachFunctor = ( + param?: OdspContainerAttachArgs, +) => Promise; + +/** + * Interface describing various options controling container open + * @alpha + */ +export interface OdspContainerOpenOptions { + /** + * A sharing link could be provided to identify a file. This link has to be in very specific format - see + * OdspContainerAttachResult.sharingLink. + * When sharing link is provided, it uniquely identifies a file in Sharepoint - OdspConnectionConfig information + * (part of OdspClientProps.connection provided to createOdspClient()) is ignored in such case. + * + * This is used to save the network calls while doing trees/latest call as if the client does not have + * permission then this link can be redeemed for the permissions in the same network call. + */ + sharingLinkToRedeem?: string; + + /** + * Can specify specific file version to open. If specified, opened container will be read-only. + * If not specified, current (latest, read-write) version of the file is opened. + */ + fileVersion?: string; } /** @@ -132,3 +224,37 @@ export interface TokenResponse { */ fromCache?: boolean; } + +/** + * IOdspClient provides the ability to manipulate Fluid containers backed by the ODSP service within the context of Microsoft 365 (M365) tenants. + * @alpha + */ +export interface IOdspClient { + /** + * Creates a new container in memory. Calling attach() on returned container will create container in storage. + * @param containerSchema - schema of the created container + */ + createContainer( + containerSchema: T, + ): Promise<{ + container: IFluidContainer; + services: OdspContainerServices; + createFn: OdspContainerAttachFunctor; + }>; + + /** + * Opens existing container. If container does not exist, the call will fail with an error with errorType = DriverErrorTypes.fileNotFoundOrAccessDeniedError. + * @param itemId - ID of the container in storage. Used together with OdspClientProps.connection info (see createOdspClient()) to identify a file in Sharepoint. + * @param options - various options controlling container flow. + * This argument has no impact if application is not properly registered with Sharepoint. + * @param containerSchema - schema of the container. + */ + getContainer( + itemId: string, + containerSchema: T, + options?: OdspContainerOpenOptions, + ): Promise<{ + container: IFluidContainer; + services: OdspContainerServices; + }>; +} diff --git a/packages/service-clients/odsp-client/src/odspClient.ts b/packages/service-clients/odsp-client/src/odspClient.ts index 26c6ae56da21..fd8877eef8b5 100644 --- a/packages/service-clients/odsp-client/src/odspClient.ts +++ b/packages/service-clients/odsp-client/src/odspClient.ts @@ -16,13 +16,13 @@ import { type ITelemetryBaseLogger, } from "@fluidframework/core-interfaces"; import { assert } from "@fluidframework/core-utils/internal"; -import type { IClient } from "@fluidframework/driver-definitions"; -import type { IDocumentServiceFactory } from "@fluidframework/driver-definitions/internal"; import type { - ContainerAttachProps, - ContainerSchema, - IFluidContainer, -} from "@fluidframework/fluid-static"; + IDocumentServiceFactory, + IClient, + IUrlResolver, + IResolvedUrl, +} from "@fluidframework/driver-definitions/internal"; +import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static"; import type { IRootDataObject } from "@fluidframework/fluid-static/internal"; import { createDOProviderContainerRuntimeFactory, @@ -31,45 +31,46 @@ import { } from "@fluidframework/fluid-static/internal"; import { OdspDocumentServiceFactory, - OdspDriverUrlResolver, - createOdspCreateContainerRequest, - createOdspUrl, isOdspResolvedUrl, + createOpenOdspResolvedUrl, + createCreateOdspResolvedUrl, } from "@fluidframework/odsp-driver/internal"; -import type { OdspResourceTokenFetchOptions } from "@fluidframework/odsp-driver-definitions/internal"; +import type { + OdspResourceTokenFetchOptions, + IOdspOpenArgs, + IOdspCreateArgs, +} from "@fluidframework/odsp-driver-definitions/internal"; import { wrapConfigProviderWithDefaults } from "@fluidframework/telemetry-utils/internal"; import { v4 as uuid } from "uuid"; -import type { TokenResponse } from "./interfaces.js"; import type { + TokenResponse, OdspClientProps, - OdspConnectionConfig, - OdspContainerAttachProps, + OdspContainerAttachArgs, + OdspContainerAttachFunctor, OdspContainerServices, + OdspContainerAttachResult, + OdspContainerOpenOptions, + OdspConnectionConfig, + IOdspClient, } from "./interfaces.js"; import { createOdspAudienceMember } from "./odspAudience.js"; import { type IOdspTokenProvider } from "./token.js"; +type OdspSiteLocation = Omit; + async function getStorageToken( options: OdspResourceTokenFetchOptions, tokenProvider: IOdspTokenProvider, ): Promise { - const tokenResponse: TokenResponse = await tokenProvider.fetchStorageToken( - options.siteUrl, - options.refresh, - ); - return tokenResponse; + return tokenProvider.fetchStorageToken(options.siteUrl, options.refresh); } async function getWebsocketToken( options: OdspResourceTokenFetchOptions, tokenProvider: IOdspTokenProvider, ): Promise { - const tokenResponse: TokenResponse = await tokenProvider.fetchWebsocketToken( - options.siteUrl, - options.refresh, - ); - return tokenResponse; + return tokenProvider.fetchWebsocketToken(options.siteUrl, options.refresh); } /** @@ -89,28 +90,88 @@ function wrapConfigProvider(baseConfigProvider?: IConfigProviderBase): IConfigPr return wrapConfigProviderWithDefaults(baseConfigProvider, odspClientFeatureGates); } +class OdspFileOpenUrlResolver implements IUrlResolver { + public constructor(private readonly input: IOdspOpenArgs) {} + + public async resolve(_request: IRequest): Promise { + return createOpenOdspResolvedUrl(this.input); + } + + public async getAbsoluteUrl(): Promise { + throw new Error("getAbsoluteUrl() calls are not supported in OdspClient scenarios."); + } +} + +class OdspFileCreateUrlResolver implements IUrlResolver { + private input?: IOdspCreateArgs; + + public constructor() {} + + public update(input: IOdspCreateArgs): void { + assert(this.input === undefined, "Can update only once"); + this.input = input; + } + + public async resolve(_request: IRequest): Promise { + assert(this.input !== undefined, "update() not called"); + return createCreateOdspResolvedUrl(this.input); + } + + public async getAbsoluteUrl(): Promise { + throw new Error("getAbsoluteUrl() calls are not supported in OdspClient scenarios."); + } +} + +/** + * Creates OdspClient + * @param driverFactory - driver factory to use + * @param connectionConfig - connection config, specifis token callback and location of the files + * @param logger - (options) logger to use + * @param configProvider - (optional) overwrires + * @returns IOdspClient + */ +function createOdspClientCore( + driverFactory: IDocumentServiceFactory, + connectionConfig: OdspSiteLocation, + logger?: ITelemetryBaseLogger, + configProvider?: IConfigProviderBase, +): IOdspClient { + return new OdspClient(driverFactory, connectionConfig, logger, configProvider); +} + +/** + * Creates OdspClient + * @param properties - properties + * @returns IOdspClient + * @alpha + */ +export function createOdspClient(properties: OdspClientProps): IOdspClient { + return createOdspClientCore( + new OdspDocumentServiceFactory( + async (options) => getStorageToken(options, properties.connection.tokenProvider), + async (options) => getWebsocketToken(options, properties.connection.tokenProvider), + properties.persistedCache, + properties.hostPolicy, + ), + properties.connection, + properties.logger, + properties.configProvider, + ); +} + /** * OdspClient provides the ability to have a Fluid object backed by the ODSP service within the context of Microsoft 365 (M365) tenants. - * @sealed - * @beta */ -export class OdspClient { - private readonly documentServiceFactory: IDocumentServiceFactory; - private readonly urlResolver: OdspDriverUrlResolver; - private readonly configProvider: IConfigProviderBase | undefined; - private readonly connectionConfig: OdspConnectionConfig; - private readonly logger: ITelemetryBaseLogger | undefined; - - public constructor(properties: OdspClientProps) { - this.connectionConfig = properties.connection; - this.logger = properties.logger; - this.documentServiceFactory = new OdspDocumentServiceFactory( - async (options) => getStorageToken(options, this.connectionConfig.tokenProvider), - async (options) => getWebsocketToken(options, this.connectionConfig.tokenProvider), - ); +class OdspClient implements IOdspClient { + private readonly configProvider: IConfigProviderBase; - this.urlResolver = new OdspDriverUrlResolver(); - this.configProvider = wrapConfigProvider(properties.configProvider); + public constructor( + private readonly documentServiceFactory: IDocumentServiceFactory, + protected readonly connectionConfig: OdspSiteLocation, + private readonly logger?: ITelemetryBaseLogger, + configProvider?: IConfigProviderBase, + ) { + this.configProvider = wrapConfigProvider(configProvider); } public async createContainer( @@ -118,46 +179,78 @@ export class OdspClient { ): Promise<{ container: IFluidContainer; services: OdspContainerServices; + createFn: OdspContainerAttachFunctor; }> { - const loader = this.createLoader(containerSchema); + const resolver = new OdspFileCreateUrlResolver(); + const loader = this.createLoader(containerSchema, resolver); const container = await loader.createDetachedContainer({ package: "no-dynamic-package", config: {}, }); - const fluidContainer = await this.createFluidContainer(container, this.connectionConfig); + const rootDataObject = await this.getContainerEntryPoint(container); + const fluidContainer = createFluidContainer({ + container, + rootDataObject, + }); + + const createFn = OdspClient.createContainerAttachCallback( + container, + this.connectionConfig, + resolver, + ); + + fluidContainer.attach = async (): Promise => { + const res = await createFn(); + return res.itemId; + }; const services = await this.getContainerServices(container); - return { container: fluidContainer as IFluidContainer, services }; + return { container: fluidContainer, services, createFn }; } public async getContainer( - id: string, + itemId: string, containerSchema: T, + options?: OdspContainerOpenOptions, ): Promise<{ container: IFluidContainer; services: OdspContainerServices; }> { - const loader = this.createLoader(containerSchema); - const url = createOdspUrl({ + const resolvedUrl: IOdspOpenArgs = { + summarizer: false, + + // Identity of a file siteUrl: this.connectionConfig.siteUrl, driveId: this.connectionConfig.driveId, - itemId: id, - dataStorePath: "", - }); - const container = await loader.resolve({ url }); + itemId, + + fileVersion: options?.fileVersion, - const fluidContainer = createFluidContainer({ + sharingLinkToRedeem: options?.sharingLinkToRedeem, + + isClpCompliantApp: this.connectionConfig.isClpCompliant === true, + }; + + const loader = this.createLoader( + containerSchema, + new OdspFileOpenUrlResolver(resolvedUrl), + ); + // Url does not matter, as our URL resolver will provide fixed output. + // Put some easily editifiable string for easier debugging + const container = await loader.resolve({ url: "" }); + + const fluidContainer = createFluidContainer({ container, rootDataObject: await this.getContainerEntryPoint(container), }); const services = await this.getContainerServices(container); - return { container: fluidContainer as IFluidContainer, services }; + return { container: fluidContainer, services }; } - private createLoader(schema: ContainerSchema): Loader { + private createLoader(schema: ContainerSchema, urlResolver: IUrlResolver): Loader { const runtimeFactory = createDOProviderContainerRuntimeFactory({ schema, compatibilityMode: "2", @@ -181,7 +274,7 @@ export class OdspClient { }; return new Loader({ - urlResolver: this.urlResolver, + urlResolver, documentServiceFactory: this.documentServiceFactory, codeLoader, logger: this.logger, @@ -190,28 +283,43 @@ export class OdspClient { }); } - private async createFluidContainer( + private static createContainerAttachCallback( container: IContainer, - connection: OdspConnectionConfig, - ): Promise { - const rootDataObject = await this.getContainerEntryPoint(container); - + connectionConfig: OdspSiteLocation, + resolver: OdspFileCreateUrlResolver, + ): OdspContainerAttachFunctor { /** * See {@link FluidContainer.attach} */ - const attach = async ( - odspProps?: ContainerAttachProps, - ): Promise => { - const createNewRequest: IRequest = createOdspCreateContainerRequest( - connection.siteUrl, - connection.driveId, - odspProps?.filePath ?? "", - odspProps?.fileName ?? uuid(), - ); + return async (odspProps?: OdspContainerAttachArgs): Promise => { if (container.attachState !== AttachState.Detached) { throw new Error("Cannot attach container. Container is not in detached state"); } - await container.attach(createNewRequest); + + const base = { + siteUrl: connectionConfig.siteUrl, + driveId: connectionConfig.driveId, + isClpCompliantApp: connectionConfig.isClpCompliant === true, + }; + + const resolved: IOdspCreateArgs = + odspProps !== undefined && "itemId" in odspProps + ? { + ...base, + itemId: odspProps.itemId, + } + : { + ...base, + filePath: odspProps?.filePath ?? "", + fileName: odspProps?.fileName ?? uuid(), + createShareLinkType: odspProps?.createShareLinkType, + }; + + resolver.update(resolved); + + // Url does not matter, as our URL resolver will provide fixed output. + // Put some easily editifiable string for easier debugging + await container.attach({ url: "OdspClient dummy url" }); const resolvedUrl = container.resolvedUrl; @@ -219,16 +327,11 @@ export class OdspClient { throw new Error("Resolved Url not available on attached container"); } - /** - * A unique identifier for the file within the provided SharePoint Embedded container ID. When you attach a container, - * a new `itemId` is created in the user's drive, which developers can use for various operations - * like updating, renaming, moving the Fluid file, changing permissions, and more. `itemId` is used to load the container. - */ - return resolvedUrl.itemId; + return { + itemId: resolvedUrl.itemId, + shareLinkInfo: resolvedUrl.shareLinkInfo, + }; }; - const fluidContainer = createFluidContainer({ container, rootDataObject }); - fluidContainer.attach = attach; - return fluidContainer; } private async getContainerServices(container: IContainer): Promise { diff --git a/packages/service-clients/odsp-client/src/test/odspClient.spec.ts b/packages/service-clients/odsp-client/src/test/odspClient.spec.ts index df287e84fc49..3e3638cca1da 100644 --- a/packages/service-clients/odsp-client/src/test/odspClient.spec.ts +++ b/packages/service-clients/odsp-client/src/test/odspClient.spec.ts @@ -10,8 +10,8 @@ import type { IConfigProviderBase } from "@fluidframework/core-interfaces"; import { type ContainerSchema } from "@fluidframework/fluid-static"; import { SharedMap } from "@fluidframework/map/internal"; -import type { OdspConnectionConfig } from "../interfaces.js"; -import { OdspClient } from "../odspClient.js"; +import type { OdspConnectionConfig, IOdspClient } from "../interfaces.js"; +import { createOdspClient as createOdspClientCore } from "../odspClient.js"; import { OdspTestTokenProvider } from "./odspTestTokenProvider.js"; @@ -36,18 +36,17 @@ const clientCreds: OdspTestCredentials = { /** * Creates an instance of the odsp-client with the specified test credentials. * - * @returns OdspClient - An instance of the odsp-client. + * @returns IOdspClient - An instance of the odsp-client. */ -function createOdspClient(props: { configProvider?: IConfigProviderBase } = {}): OdspClient { +function createOdspClient(props: { configProvider?: IConfigProviderBase } = {}): IOdspClient { // Configuration for connecting to the ODSP service. const connectionProperties: OdspConnectionConfig = { tokenProvider: new OdspTestTokenProvider(clientCreds), // Token provider using the provided test credentials. siteUrl: "", driveId: "", - filePath: "", }; - return new OdspClient({ + return createOdspClientCore({ connection: connectionProperties, configProvider: props.configProvider, }); @@ -55,7 +54,7 @@ function createOdspClient(props: { configProvider?: IConfigProviderBase } = {}): describe("OdspClient", () => { // const connectTimeoutMs = 5000; - let client: OdspClient; + let client: IOdspClient; let schema: ContainerSchema; beforeEach(() => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 913d22369160..034edf727368 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4268,6 +4268,9 @@ importers: '@azure/msal-browser': specifier: ^2.34.0 version: 2.38.3 + '@fluidframework/core-utils': + specifier: workspace:~ + version: link:../../../../packages/common/core-utils '@fluidframework/odsp-client': specifier: workspace:~ version: link:../../../../packages/service-clients/odsp-client